export declare const chatTemplate = "\"use client\";\nimport React, { createContext, useMemo } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport {\n ChatInput,\n ChatMessage,\n ChatMessageList,\n ChatPanel,\n ChatProvider,\n ChatSource,\n ChatSources,\n ChatTyping,\n IconButton,\n Prose,\n useBelowBreakpoint,\n useChat,\n type ChatSendContext,\n type ChatSourceData,\n} from \"cherry-styled-components\";\nimport { RotateCcw } from \"lucide-react\";\nimport remarkGfm from \"remark-gfm\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport { MDXRemote, MDXRemoteSerializeResult } from \"next-mdx-remote\";\nimport { serialize } from \"next-mdx-remote/serialize\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { useMDXComponents as getMDXComponents } from \"@/components/MDXComponents\";\n\nconst mdxComponents = getMDXComponents({});\n\ntype RagSource = {\n id: string;\n path: string;\n uri: string;\n score: number;\n};\n\n// Map a retrieval hit onto the chip the transcript renders: docs://\n// becomes an app-relative href, and the slug's last segment becomes a\n// title-cased label.\nfunction toSourceChip(src: RagSource): ChatSourceData {\n const slug = src.uri.replace(\"docs://\", \"\").replace(/^\\/+/, \"\");\n const href = slug ? `/${slug}/` : \"/\";\n const label = slug\n ? slug\n .split(\"/\")\n .pop()!\n .replace(/-/g, \" \")\n .replace(/\\b\\w/g, (c: string) => c.toUpperCase())\n : \"Home\";\n return { id: src.id, label, href };\n}\n\n// Cherry's ChatProvider owns the transcript, panel state, and streaming\n// bookkeeping; this handler owns the transport: POST the question to\n// /api/rag, stream the SSE frames, and patch the assistant bubble as tokens\n// arrive. The provider bounds `history` to the RAG contract (20 entries,\n// 4000 chars each), swallows aborts, and surfaces thrown errors as `error`.\nasync function sendToAssistant(\n question: string,\n { signal, history, setAssistant }: ChatSendContext,\n) {\n const res = await fetch(\"/api/rag\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ question, history }),\n signal,\n });\n\n if (!res.ok) {\n // The body may be a proxy/gateway HTML error page (e.g. Cloudflare\n // 524), not our JSON - never blindly JSON.parse it.\n const contentType = res.headers.get(\"content-type\") || \"\";\n let message = \"Request failed\";\n if (contentType.includes(\"application/json\")) {\n try {\n const errorData = await res.json();\n message = errorData.error || message;\n } catch {\n // Non-JSON despite the header - fall through to the default.\n }\n } else if ([502, 503, 504, 524].includes(res.status)) {\n message = \"The assistant took too long to respond. Please try again.\";\n }\n throw new Error(message);\n }\n\n const reader = res.body?.getReader();\n if (!reader) {\n throw new Error(\"Failed to get response reader\");\n }\n\n const decoder = new TextDecoder();\n const contentParts: string[] = [];\n let sources: ChatSourceData[] = [];\n let buffer = \"\";\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const parts = buffer.split(\"\\n\");\n buffer = parts.pop() ?? \"\";\n\n for (const line of parts) {\n if (!line.startsWith(\"data: \")) continue;\n\n // Guard only the parse: a malformed frame is skipped, while the\n // server's error event below must throw past this loop so it lands\n // in the provider's error state instead of dying in a parse-error\n // log.\n let data;\n try {\n data = JSON.parse(line.slice(6));\n } catch {\n console.error(\"Failed to parse SSE data:\", line);\n continue;\n }\n\n if (data.type === \"metadata\") {\n const allSources: RagSource[] = data.data?.sources ?? [];\n const seen = new Set();\n sources = allSources\n .filter((s) => {\n if (s.score < 0.4 || seen.has(s.uri)) return false;\n seen.add(s.uri);\n return true;\n })\n .map(toSourceChip);\n } else if (data.type === \"content\") {\n contentParts.push(data.data);\n const streamedContent = contentParts.join(\"\");\n setAssistant(streamedContent, { text: streamedContent, sources });\n } else if (data.type === \"error\") {\n throw new Error(data.data);\n } else if (data.type === \"done\") {\n const streamedContent = contentParts.join(\"\");\n let mdxSource: MDXRemoteSerializeResult | null = null;\n try {\n mdxSource = await serialize(streamedContent, {\n parseFrontmatter: false,\n mdxOptions: {\n remarkPlugins: [remarkGfm],\n rehypePlugins: [rehypeHighlight],\n format: \"md\",\n development: false,\n },\n });\n } catch (mdxError: unknown) {\n console.error(\"MDX serialization error:\", mdxError);\n }\n\n setAssistant(\n mdxSource ? (\n \n ) : (\n streamedContent\n ),\n { text: streamedContent, sources },\n );\n }\n }\n }\n}\n\nfunction ChatSourceLink({ source }: { source: ChatSourceData }) {\n const { close } = useChat();\n const isMobileChat = useBelowBreakpoint(\"lg\");\n const router = useRouter();\n\n return (\n {\n if (\n event.defaultPrevented ||\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n ) {\n return;\n }\n // ChatSource renders a plain anchor, so route unmodified left clicks\n // client-side to keep the transcript alive; below lg the panel is a\n // fullscreen dialog, so close it to reveal the navigated page.\n event.preventDefault();\n if (isMobileChat) close();\n router.push(source.href);\n }}\n >\n {source.label}\n \n );\n}\n\nfunction ChatTranscript() {\n const { messages, loading, error } = useChat();\n const lastMessage = messages[messages.length - 1];\n\n return (\n \n {messages.map((message) => (\n \n {message.role === \"assistant\" ? (\n // The docs' typographic context lives on the docs page wrapper,\n // so a rendered MDX answer would arrive margin-less here. Prose\n // $compact is Cherry's document styling for chat replies: a 10px\n // rhythm between elements, inline-code chips, lists, and tables.\n {message.content}\n ) : (\n message.content\n )}\n {message.role === \"assistant\" &&\n message.sources &&\n message.sources.length > 0 && (\n \n {message.sources.map((source) => (\n \n ))}\n \n )}\n \n ))}\n {loading && lastMessage?.role !== \"assistant\" && }\n {error && (\n \n

\n Error: {error}\n

\n
\n )}\n
\n );\n}\n\nfunction ChatActions() {\n const { reset } = useChat();\n\n return (\n \n \n \n );\n}\n\nfunction Chat() {\n return (\n }>\n \n \n \n );\n}\n\n// App-level chat context: only whether the AI assistant is enabled (an LLM\n// provider is configured). Panel state, the transcript, and streaming all\n// live in Cherry's ChatProvider - read those with useChat() from\n// \"cherry-styled-components\".\nconst ChatContext = createContext<{ isChatActive: boolean }>({\n isChatActive: false,\n});\n\ninterface ChatContextProviderProps {\n children: React.ReactNode;\n isChatActive: boolean;\n}\n\nconst ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {\n const value = useMemo(() => ({ isChatActive }), [isChatActive]);\n\n return (\n \n \n {children}\n \n \n );\n};\n\nexport { Chat, ChtProvider, ChatContext };\n";