| 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", |
| city_average: "City average", |
| 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", |
| station_coverage: "Monitoring coverage", |
| weekday_weekend_profile: "Weekday/weekend comparison", |
| condition_comparison: "Threshold-group comparison", |
| rank_states: "State ranking", |
| coverage_trend: "Coverage trend", |
| funding_rank: "NCAP funding ranking", |
| ncap_threshold_cities: "NCAP pollution threshold", |
| ncap_funding_groups: "NCAP funding groups", |
| pollution_change: "Period change", |
| context_relationship: "Context relationship", |
| threshold_frequency: "Threshold frequency", |
| analysis_unavailable: "Analysis needs refinement", |
| composition: "Composed analysis", |
| }; |
|
|
| function formatArgument(name: string, value: unknown) { |
| if (Array.isArray(value)) { |
| if (value.length === 0) { |
| if (name === "months") return "all months"; |
| if (name === "cities") return "all cities"; |
| return "none"; |
| } |
| return value.map((item) => String(item)).join(", "); |
| } |
| if (typeof value === "string") { |
| return value.replaceAll("_", " "); |
| } |
| return String(value); |
| } |
|
|
| 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?.analysis_steps.length ? ( |
| <details className="method-details"> |
| <summary> |
| <DatabaseIcon /> |
| Analysis recipe |
| </summary> |
| {message.result.analysis_steps.length ? ( |
| <ol className="analysis-recipe"> |
| {message.result.analysis_steps.map((step, index) => ( |
| <li key={`${step.function_name}-${index}`}> |
| <div className="recipe-function"> |
| <span>{String(index + 1).padStart(2, "0")}</span> |
| <code>{step.function_name}</code> |
| </div> |
| <dl> |
| {Object.entries(step.arguments).map(([name, value]) => ( |
| <div key={name}> |
| <dt>{name.replaceAll("_", " ")}</dt> |
| <dd>{formatArgument(name, value)}</dd> |
| </div> |
| ))} |
| </dl> |
| </li> |
| ))} |
| </ol> |
| ) : null} |
| {message.result.method_note ? <p>{message.result.method_note}</p> : 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> |
| ); |
| } |
|
|