harryagasi
feat: fetch and render charts for chat answers via /api/v1/charts
b5e46e4
Raw
History Blame Contribute Delete
5.41 kB
import { useEffect, useRef } from "react";
import { Bot, Sparkles, User } from "lucide-react";
import type { UiMessage } from "./types";
import { MarkdownContent } from "./MarkdownContent";
import { MessageTraceability } from "./MessageTraceability";
import { PlotlyChartBlock } from "./PlotlyChartBlock";
import { cx, compactQuestions, formatDateTime } from "./utils";
export function MessageList({
messages,
scrollTo,
onScrollDone,
businessQuestions = [],
onSelectQuestion,
}: {
messages: UiMessage[];
scrollTo?: "bottom" | "last-ai" | null;
onScrollDone?: () => void;
businessQuestions?: string[];
onSelectQuestion?: (question: string) => void;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const lastAiRef = useRef<HTMLElement>(null);
useEffect(() => {
if (!scrollTo) return;
if (scrollTo === "bottom") {
containerRef.current?.scrollIntoView({ block: "end", behavior: "smooth" });
} else if (scrollTo === "last-ai" && lastAiRef.current) {
lastAiRef.current.scrollIntoView({ block: "start", behavior: "smooth" });
}
onScrollDone?.();
}, [scrollTo]);
if (messages.length === 0) {
const questions = compactQuestions(businessQuestions);
return (
<div className="flex h-full items-center justify-center overflow-y-auto p-6">
<div className="mx-auto w-full max-w-xl text-center">
<div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-emerald-50 text-emerald-600">
<Sparkles className="h-5 w-5" />
</div>
<h3 className="text-sm font-semibold text-slate-900">Start the conversation</h3>
<p className="mt-1 text-sm text-slate-500">
{questions.length > 0
? "Pick one of the business questions below, or type your own question in the chat box."
: "Ask a focused question once the analysis context is ready."}
</p>
{questions.length > 0 && (
<div className="mt-4 flex flex-col gap-2 text-left">
{questions.map((question) => (
<button
key={question}
type="button"
onClick={() => onSelectQuestion?.(question)}
className="rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm text-slate-700 shadow-sm transition hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-800"
>
{question}
</button>
))}
</div>
)}
</div>
</div>
);
}
const lastAiIndex = messages.reduce((last, msg, i) => (msg.role === "ai" ? i : last), -1);
return (
<div ref={containerRef} className="mx-auto flex w-full max-w-5xl flex-col gap-5 px-4 py-6">
{messages.map((message, index) => {
const isUser = message.role === "user";
const isLastAi = index === lastAiIndex;
return (
<article ref={isLastAi ? lastAiRef : null} key={message.id} className={cx("flex min-w-0 gap-3", isUser && "justify-end")}>
{!isUser && (
<div className="mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-slate-900 text-white">
<Bot className="h-4 w-4" />
</div>
)}
<div className={cx("min-w-0 rounded-lg border px-4 py-3 shadow-sm", isUser ? "max-w-[min(36rem,84%)] border-emerald-100 bg-emerald-50" : "w-full max-w-3xl border-slate-200 bg-white")}>
<div className="mb-1 flex items-center justify-between gap-3 text-[11px] text-slate-400">
<span className="font-medium uppercase tracking-wide">{isUser ? "You" : "AI Agent"}</span>
<span>{formatDateTime(message.created_at)}</span>
</div>
{isUser ? <p className="whitespace-pre-wrap break-words text-sm leading-6 text-slate-800 [overflow-wrap:anywhere]">{message.content}</p> : <MarkdownContent content={message.content || "..."} />}
{message.status === "streaming" && message.statusText && <p className="mt-2 text-xs text-slate-500">{message.statusText}</p>}
{message.status === "error" && <p className="mt-2 text-xs text-red-600">{message.statusText ?? "Stream failed"}</p>}
{!isUser && message.charts && message.charts.length > 0 && (
<div className="mt-2 space-y-3">
{message.charts.map((chart) => (
<div key={chart.chart_id}>
{chart.title && <p className="mb-1 text-xs font-semibold text-slate-700">{chart.title}</p>}
<PlotlyChartBlock spec={chart.spec} />
</div>
))}
</div>
)}
{!isUser && (
<MessageTraceability
observability={message.traceability}
loading={message.traceabilityLoading}
error={message.traceabilityError}
sources={message.sources}
/>
)}
</div>
{isUser && (
<div className="mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-emerald-600 text-white">
<User className="h-4 w-4" />
</div>
)}
</article>
);
})}
</div>
);
}