| import { lazy, Suspense } from "react"; |
| import { ResultTable } from "./ResultTable"; |
| import { DatabaseIcon, WindIcon } from "./icons"; |
| import type { ConversationMessage } from "./types"; |
|
|
| const AnalysisChart = lazy(() => |
| import("./AnalysisChart").then((module) => ({ |
| default: module.AnalysisChart, |
| })), |
| ); |
|
|
| const analysisLabels: Record<string, string> = { |
| rank_cities: "City ranking", |
| threshold_cities: "Threshold analysis", |
| compare_cities: "City comparison", |
| time_trend: "Time trend", |
| relationship: "Relationship analysis", |
| strongest_weather_relationship: "Weather-factor ranking", |
| seasonal_profile: "Seasonal profile", |
| funding_lookup: "NCAP funding", |
| custom_sql_analysis: "Custom validated SQL", |
| }; |
|
|
| export function ChatMessage({ |
| message, |
| onPrompt, |
| }: { |
| message: ConversationMessage; |
| onPrompt: (prompt: string) => void; |
| }) { |
| if (message.role === "user") { |
| return ( |
| <article className="message message--user"> |
| <div className="user-bubble">{message.content}</div> |
| </article> |
| ); |
| } |
|
|
| return ( |
| <article className={`message message--assistant ${message.error ? "has-error" : ""}`}> |
| <div className="assistant-avatar"> |
| <WindIcon /> |
| </div> |
| <div className="assistant-body"> |
| <div className="assistant-meta"> |
| <strong>VayuChat</strong> |
| <span>Gemini 3.5 Flash-Lite</span> |
| </div> |
| {message.error ? ( |
| <div className="answer-error" role="alert"> |
| <strong>Analysis stopped</strong> |
| <p>{message.error}</p> |
| </div> |
| ) : ( |
| <> |
| <p className="answer-lead">{message.result?.answer ?? message.content}</p> |
| {message.result?.analysis_type ? ( |
| <div className="result-provenance" aria-label="Analysis provenance"> |
| <span> |
| <DatabaseIcon /> |
| {analysisLabels[message.result.analysis_type] ?? "Validated analysis"} |
| </span> |
| {message.result.elapsed_ms > 0 ? ( |
| <span>{(message.result.elapsed_ms / 1000).toFixed(1)}s</span> |
| ) : null} |
| </div> |
| ) : null} |
| {message.result?.key_points.length ? ( |
| <ul className="key-points"> |
| {message.result.key_points.map((point) => ( |
| <li key={point}>{point}</li> |
| ))} |
| </ul> |
| ) : null} |
| {message.result ? ( |
| <Suspense fallback={<div className="chart-loading" aria-hidden="true" />}> |
| <AnalysisChart result={message.result} /> |
| </Suspense> |
| ) : null} |
| {message.result ? <ResultTable result={message.result} /> : null} |
| {message.result?.method_note || message.result?.query ? ( |
| <details className="method-details"> |
| <summary> |
| <DatabaseIcon /> |
| How this was calculated |
| </summary> |
| {message.result.method_note ? <p>{message.result.method_note}</p> : null} |
| {message.result.query ? <pre>{message.result.query}</pre> : null} |
| </details> |
| ) : null} |
| {message.result?.suggested_questions.length ? ( |
| <section className="follow-ups" aria-label="Suggested follow-up questions"> |
| <p>Ask a follow-up</p> |
| <div> |
| {message.result.suggested_questions.map((question) => ( |
| <button key={question} onClick={() => onPrompt(question)}> |
| {question} |
| </button> |
| ))} |
| </div> |
| </section> |
| ) : null} |
| </> |
| )} |
| </div> |
| </article> |
| ); |
| } |
|
|