mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 11:09:34 +00:00
feat: integrate Pi backend and Pi Web
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, type MouseEvent } from "react";
|
||||
import ReactMarkdown, { type Components } from "react-markdown";
|
||||
import { resolveLocalFileHref } from "@/lib/file-links";
|
||||
import { encodeFilePathForApi } from "@/lib/file-paths";
|
||||
import { markdownRehypePlugins, markdownRemarkPlugins, normalizeDisplayMath } from "@/lib/markdown";
|
||||
import { MermaidBlock, CodeBlock } from "./MermaidBlock";
|
||||
|
||||
interface MarkdownBodyProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
isStreaming?: boolean;
|
||||
cwd?: string;
|
||||
onOpenFile?: (filePath: string) => void;
|
||||
}
|
||||
|
||||
export function MarkdownBody({ children, className, isStreaming, cwd, onOpenFile }: MarkdownBodyProps) {
|
||||
const normalizedMarkdown = useMemo(() => normalizeDisplayMath(children), [children]);
|
||||
// Stable renderer identities keep stateful blocks mounted across message hover updates.
|
||||
const components = useMemo<Components>(() => ({
|
||||
code({ className, children, ...props }) {
|
||||
const lang = className?.replace("language-", "").toLowerCase() ?? "";
|
||||
const raw = String(children);
|
||||
const isBlock = className?.includes("language-") || raw.includes("\n");
|
||||
if (isBlock) {
|
||||
if (lang === "mermaid") {
|
||||
return <MermaidBlock code={raw.replace(/\n$/, "")} isStreaming={isStreaming} />;
|
||||
}
|
||||
return <CodeBlock code={raw.replace(/\n$/, "")} lang={lang} />;
|
||||
}
|
||||
return (
|
||||
<code
|
||||
className="markdown-inline-code"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
pre({ children }) {
|
||||
return <>{children}</>;
|
||||
},
|
||||
a({ href, children, ...props }) {
|
||||
// `node` is react-markdown metadata, not a DOM attribute.
|
||||
delete props.node;
|
||||
const filePath = onOpenFile ? resolveLocalFileHref(href, cwd) : null;
|
||||
const openFile = onOpenFile;
|
||||
if (!filePath || !openFile) {
|
||||
return (
|
||||
<a href={href} {...props} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
if (event.defaultPrevented || event.button !== 0) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
||||
const target = event.currentTarget.getAttribute("target");
|
||||
if (target && target !== "_self") return;
|
||||
event.preventDefault();
|
||||
openFile(filePath);
|
||||
};
|
||||
|
||||
return (
|
||||
<a href={href} {...props} onClick={handleClick}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
img({ src, alt, ...props }) {
|
||||
delete props.node;
|
||||
const filePath = typeof src === "string" ? resolveLocalFileHref(src, cwd) : null;
|
||||
const imageSrc = filePath
|
||||
? `/api/files/${encodeFilePathForApi(filePath)}?type=read`
|
||||
: src;
|
||||
// Dynamic local paths are served directly by the file API.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img src={imageSrc} alt={alt ?? ""} loading="lazy" {...props} />;
|
||||
},
|
||||
table({ children }) {
|
||||
return (
|
||||
<div className="markdown-table-wrap">
|
||||
<table>{children}</table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}), [cwd, isStreaming, onOpenFile]);
|
||||
|
||||
return (
|
||||
<div className={["markdown-body", className].filter(Boolean).join(" ")}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={markdownRemarkPlugins}
|
||||
rehypePlugins={markdownRehypePlugins}
|
||||
components={components}
|
||||
>
|
||||
{normalizedMarkdown}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user