"use client"; import "@assistant-ui/react-markdown/styles/dot.css"; import { MarkdownTextPrimitive, unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, useIsMarkdownCodeBlock, } from "@assistant-ui/react-markdown"; import { useSetAtom } from "jotai"; import { ExternalLinkIcon } from "lucide-react"; import dynamic from "next/dynamic"; import { useParams } from "next/navigation"; import { useTheme } from "next-themes"; import { createContext, memo, type ReactNode, useCallback, useContext, useRef } from "react"; import rehypeKatex from "rehype-katex"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; import { ImagePreview, ImageRoot, ImageZoom } from "@/components/assistant-ui/image"; import "katex/dist/katex.min.css"; import { toast } from "sonner"; import { processChildrenWithCitations } from "@/components/citations/citation-renderer"; import { Skeleton } from "@/components/ui/skeleton"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { useElectronAPI } from "@/hooks/use-platform"; import { documentsApiService } from "@/lib/apis/documents-api.service"; import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser"; import { cn } from "@/lib/utils"; function MarkdownCodeBlockSkeleton() { return (
); } const LazyMarkdownCodeBlock = dynamic( () => import("./markdown-code-block").then((mod) => mod.MarkdownCodeBlock), { loading: () => , } ); // Per-render URL placeholder map propagated to component overrides via // React Context. Replaces the previous module-level `_pendingUrlCitations` // state, which was unsafe under concurrent renders / SSR. type CitationUrlMapRef = { current: CitationUrlMap }; const EMPTY_URL_MAP: CitationUrlMap = new Map(); const CitationUrlMapContext = createContext({ current: EMPTY_URL_MAP }); function useCitationUrlMap(): CitationUrlMap { return useContext(CitationUrlMapContext).current; } /** * Preprocess raw markdown before it reaches the remark/rehype pipeline. * - Replaces URL-based citations with safe placeholders (prevents GFM autolinks) * - Normalises LaTeX delimiters to dollar-sign syntax for remark-math */ function preprocessMarkdown(content: string, urlMapRef: CitationUrlMapRef): string { // Replace URL-based citations with safe placeholders BEFORE markdown parsing. // GFM autolinks would otherwise convert the https://... inside [citation:URL] // into an element, splitting the text and preventing our citation regex // from matching the full pattern. const { content: rewritten, urlMap } = preprocessCitationMarkdown(content); urlMapRef.current = urlMap; content = rewritten; // All math forms are normalised to $$...$$ so we can disable single-dollar // inline math in remark-math (otherwise currency like "$3,120.00 and $0.00" // gets parsed as a LaTeX expression). // 1. Block math: \[...\] → $$...$$ content = content.replace(/\\\[([\s\S]*?)\\\]/g, (_, inner) => `$$${inner}$$`); // 2. Inline math: \(...\) → $$...$$ content = content.replace(/\\\(([\s\S]*?)\\\)/g, (_, inner) => `$$${inner}$$`); // 3. Block: \begin{equation}...\end{equation} → $$...$$ content = content.replace( /\\begin\{equation\}([\s\S]*?)\\end\{equation\}/g, (_, inner) => `$$${inner}$$` ); // 4. Block: \begin{displaymath}...\end{displaymath} → $$...$$ content = content.replace( /\\begin\{displaymath\}([\s\S]*?)\\end\{displaymath\}/g, (_, inner) => `$$${inner}$$` ); // 5. Inline: \begin{math}...\end{math} → $$...$$ content = content.replace( /\\begin\{math\}([\s\S]*?)\\end\{math\}/g, (_, inner) => `$$${inner}$$` ); // 6. Strip backtick wrapping around math: `$$...$$` → $$...$$ and `$...$` → $...$ content = content.replace(/`(\${1,2})((?:(?!\1).)+)\1`/g, "$1$2$1"); // Ensure markdown headings (## ...) always start on their own line. content = content.replace(/([^\n])(#{1,6}\s)/g, "$1\n\n$2"); return content; } const MarkdownTextImpl = () => { const urlMapRef = useRef(EMPTY_URL_MAP); const preprocess = useCallback((content: string) => preprocessMarkdown(content, urlMapRef), []); return ( ); }; export const MarkdownText = memo(MarkdownTextImpl); function extractDomain(url: string): string { try { const parsed = new URL(url); return parsed.hostname.replace(/^www\./, ""); } catch { return ""; } } // Canonical local-file virtual paths are mount-prefixed: // const LOCAL_FILE_PATH_REGEX = /^\/[a-z0-9_-]+\/[^\s`]+(?:\/[^\s`]+)*$/; type AgentFilesystemMount = { mount: string; rootPath: string; }; function normalizeLocalVirtualPathForEditor( candidatePath: string, mounts: AgentFilesystemMount[] ): string { const normalizedCandidate = candidatePath.trim().replace(/\\/g, "/").replace(/\/+/g, "/"); if (!normalizedCandidate) { return candidatePath; } const defaultMount = mounts[0]?.mount; if (!defaultMount) { return normalizedCandidate.startsWith("/") ? normalizedCandidate : `/${normalizedCandidate.replace(/^\/+/, "")}`; } const mountNames = new Set(mounts.map((entry) => entry.mount)); if (normalizedCandidate.startsWith("/")) { const relative = normalizedCandidate.replace(/^\/+/, ""); const [firstSegment] = relative.split("/", 1); if (mountNames.has(firstSegment)) { return `/${relative}`; } return `/${defaultMount}/${relative}`; } const relative = normalizedCandidate.replace(/^\/+/, ""); const [firstSegment] = relative.split("/", 1); if (mountNames.has(firstSegment)) { return `/${relative}`; } return `/${defaultMount}/${relative}`; } function isVirtualFilePathToken(value: string): boolean { if (!LOCAL_FILE_PATH_REGEX.test(value) || value.startsWith("//")) { return false; } const normalized = value.replace(/\/+$/, ""); const segments = normalized.split("/").filter(Boolean); return segments.length >= 2; } function isStandaloneDocumentsPathText(node: ReactNode): string | null { if (typeof node !== "string") return null; const value = node.trim(); if (!value.startsWith("/documents/")) return null; if (value.includes(" ")) return null; const normalized = value.replace(/\/+$/, ""); const leaf = normalized.split("/").filter(Boolean).at(-1) ?? ""; if (!leaf || !leaf.includes(".")) return null; return value; } function FilePathLink({ path, className }: { path: string; className?: string }) { const openEditorPanel = useSetAtom(openEditorPanelAtom); const params = useParams(); const electronAPI = useElectronAPI(); const searchSpaceIdParam = params?.search_space_id; const parsedSearchSpaceId = Array.isArray(searchSpaceIdParam) ? Number(searchSpaceIdParam[0]) : Number(searchSpaceIdParam); const resolvedSearchSpaceId = Number.isFinite(parsedSearchSpaceId) ? parsedSearchSpaceId : undefined; return ( ); } function MarkdownImage({ src, alt }: { src?: string; alt?: string }) { if (!src) return null; const domain = extractDomain(src); return (
{alt && alt !== "Image" && (

{alt}

)} {domain &&

{domain}

}
e.stopPropagation()} > Open
); } const defaultComponents = memoizeMarkdownComponents({ h1: function H1({ className, children, ...props }) { const urlMap = useCitationUrlMap(); return (

{processChildrenWithCitations(children, urlMap)}

); }, h2: function H2({ className, children, ...props }) { const urlMap = useCitationUrlMap(); return (

{processChildrenWithCitations(children, urlMap)}

); }, h3: function H3({ className, children, ...props }) { const urlMap = useCitationUrlMap(); return (

{processChildrenWithCitations(children, urlMap)}

); }, h4: function H4({ className, children, ...props }) { const urlMap = useCitationUrlMap(); return (

{processChildrenWithCitations(children, urlMap)}

); }, h5: function H5({ className, children, ...props }) { const urlMap = useCitationUrlMap(); return (
{processChildrenWithCitations(children, urlMap)}
); }, h6: function H6({ className, children, ...props }) { const urlMap = useCitationUrlMap(); return (
{processChildrenWithCitations(children, urlMap)}
); }, p: function P({ className, children, ...props }) { const urlMap = useCitationUrlMap(); const standalonePath = isStandaloneDocumentsPathText(children); return (

{standalonePath ? ( ) : ( processChildrenWithCitations(children, urlMap) )}

); }, a: function A({ className, children, ...props }) { const urlMap = useCitationUrlMap(); return ( {processChildrenWithCitations(children, urlMap)} ); }, blockquote: function Blockquote({ className, children, ...props }) { const urlMap = useCitationUrlMap(); return (
{processChildrenWithCitations(children, urlMap)}
); }, ul: ({ className, ...props }) => (