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>
  );
}