Spaces:
Sleeping
Sleeping
File size: 5,405 Bytes
eb3bd3c 15d8afd 30cd0c9 b5e46e4 15d8afd 30cd0c9 eb3bd3c 15d8afd eb3bd3c 15d8afd eb3bd3c 30cd0c9 15d8afd 30cd0c9 15d8afd 30cd0c9 eb3bd3c 30cd0c9 eb3bd3c 30cd0c9 eb3bd3c 30cd0c9 eb3bd3c 30cd0c9 f18b03d 30cd0c9 f18b03d 30cd0c9 b5e46e4 30cd0c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | 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>
);
}
|