"use client";
import type { ChatTurn } from "@/lib/chatStore";
import type { SourceDoc } from "@/lib/types";
import { GroundingPill } from "./GroundingPill";
import { SourceChips } from "./SourceChips";
import { useState, type ReactNode } from "react";
import clsx from "clsx";
import ReactMarkdown from "react-markdown";
export function UserMessage({ text }: { text: string }) {
return (
{text}
);
}
export function AssistantMessage({
turn,
onOpenSource,
}: {
turn: ChatTurn;
onOpenSource?: (doc: SourceDoc, index: number) => void;
}) {
const r = turn.response;
const [copied, setCopied] = useState(false);
if (!r) return null;
const copy = async () => {
try {
await navigator.clipboard.writeText(r.answer);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {}
};
const sources = r.source_docs ?? [];
const rejected = r._grounding_status === "rejected_low_similarity";
// Inline citation renderer: replaces [1] [2] markers with clickable badges.
// Numbers are 1-indexed against `sources` (clamped to length).
const renderWithCitations = (text: string): ReactNode[] => {
if (!sources.length || !text) return [text];
const out: ReactNode[] = [];
const re = /\[(\d{1,3})\]/g;
let last = 0;
let m: RegExpExecArray | null;
let key = 0;
while ((m = re.exec(text)) !== null) {
const idx = parseInt(m[1], 10);
if (idx >= 1 && idx <= sources.length) {
if (m.index > last) out.push(text.slice(last, m.index));
const doc = sources[idx - 1];
out.push(
);
last = m.index + m[0].length;
}
}
if (last < text.length) out.push(text.slice(last));
return out.length ? out : [text];
};
// Wrap a markdown children array, expanding string nodes through citation regex.
const transformChildren = (children: ReactNode): ReactNode => {
if (typeof children === "string") return renderWithCitations(children);
if (Array.isArray(children))
return children.map((c, i) =>
typeof c === "string" ? (
{renderWithCitations(c)}
) : (
c
)
);
return children;
};
return (
{/* Header: grounding + actions */}
{/* Answer */}
(
{transformChildren(children)}
),
ul: ({ children }) => (
{children}
),
ol: ({ children }) => (
{children}
),
li: ({ children }) =>
{transformChildren(children)}
,
strong: ({ children }) => (
{children}
),
em: ({ children }) => (
{children}
),
code: ({ children }) => (
{children}
),
pre: ({ children }) => (
{children}
),
h1: ({ children }) => (
{children}
),
h2: ({ children }) => (
{children}
),
h3: ({ children }) => (
{children}
),
a: ({ href, children }) => (
{children}
),
blockquote: ({ children }) => (