Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |