feat: integrate Pi backend and Pi Web

This commit is contained in:
luckyyzh
2026-07-30 19:37:53 +08:00
commit 7392ab9dd7
1390 changed files with 337197 additions and 0 deletions
+102
View File
@@ -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>
);
}