Skip to content

Commit f55cfed

Browse files
CreatmanCEOclaude
andcommitted
feat: UX improvements — loading indicator, Run Eval, CSV auto-analyze, command bar
1. Chat panel width fix (min-w-[40%] shrink-0) + "Analyzing..." bouncing dots indicator 2. Run Eval button in MetricsPanel — triggers backend eval, polls for results 3. CSV upload auto-triggers AI analysis in chat after validation 4. CommandBar with 9 quick commands (analysis, monitoring, data, reports) 5. Replaced emoji with professional bullet points in welcome message 6. POST /api/metrics/run endpoint for background eval execution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5daafa0 commit f55cfed

6 files changed

Lines changed: 174 additions & 16 deletions

File tree

backend/eval/metrics_api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ async def get_metrics():
8686
return {"source": "sample", "models": SAMPLE_METRICS}
8787

8888

89+
@router.post("/run", tags=["metrics"])
90+
async def trigger_eval():
91+
"""Trigger eval pipeline. Runs in background."""
92+
import asyncio
93+
from eval.batch_runner import run_eval as _run_eval
94+
95+
asyncio.create_task(_run_eval())
96+
return {"status": "started", "message": "Eval pipeline started. Check /api/metrics for results."}
97+
98+
8999
@router.get("/models")
90100
async def list_models():
91101
"""List evaluated models with routing info."""

frontend/src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default function Home() {
1010
</div>
1111

1212
{/* Chat — right 40% (bottom drawer on mobile) */}
13-
<div className="hidden md:flex md:w-[40%] h-full border-l">
13+
<div className="hidden md:flex md:w-[40%] md:min-w-[40%] h-full border-l shrink-0">
1414
<ChatPanel />
1515
</div>
1616

frontend/src/components/Chat/CSVUpload.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22

33
import { useState, useCallback } from "react";
44
import { uploadCSV } from "@/lib/api";
5+
import { useChatStore } from "@/stores/chatStore";
6+
import { useMapStore } from "@/stores/mapStore";
57
import type { ValidationResult } from "@/types";
68

7-
export function CSVUpload() {
9+
interface CSVUploadProps {
10+
onAnalyze?: () => void;
11+
}
12+
13+
export function CSVUpload({ onAnalyze }: CSVUploadProps) {
814
const [isDragging, setIsDragging] = useState(false);
915
const [result, setResult] = useState<ValidationResult | null>(null);
1016
const [error, setError] = useState<string | null>(null);
@@ -23,12 +29,25 @@ export function CSVUpload() {
2329
try {
2430
const res = await uploadCSV(file);
2531
setResult(res);
32+
33+
// Auto-send to chat for AI analysis
34+
if (res.valid || res.warnings.length > 0) {
35+
const summary = `I uploaded CSV "${file.name}": ${res.total_rows} rows, ${res.valid_rows} valid. ` +
36+
(res.errors.length > 0 ? `Errors: ${res.errors.join('; ')}. ` : '') +
37+
(res.warnings.length > 0 ? `Warnings: ${res.warnings.join('; ')}. ` : '') +
38+
`Please analyze this data and highlight any concerns.`;
39+
40+
const { sendMessage } = useChatStore.getState();
41+
const { getApiContext } = useMapStore.getState();
42+
sendMessage(summary, getApiContext());
43+
onAnalyze?.();
44+
}
2645
} catch (err) {
2746
setError((err as Error).message);
2847
} finally {
2948
setIsUploading(false);
3049
}
31-
}, []);
50+
}, [onAnalyze]);
3251

3352
const onDrop = useCallback(
3453
(e: React.DragEvent) => {

frontend/src/components/Chat/ChatPanel.tsx

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useChatStore } from "@/stores/chatStore";
55
import { useMapStore } from "@/stores/mapStore";
66
import { MessageBubble } from "./MessageBubble";
77
import { CSVUpload } from "./CSVUpload";
8+
import { CommandBar } from "./CommandBar";
89
import { MetricsPanel } from "@/components/Metrics/MetricsPanel";
910

1011
type ViewMode = "chat" | "csv" | "metrics";
@@ -67,7 +68,7 @@ export function ChatPanel() {
6768
{/* Panel overlays — shown above messages, not instead of */}
6869
{view === "csv" && (
6970
<div className="border-b max-h-[40%] overflow-y-auto shrink-0">
70-
<CSVUpload />
71+
<CSVUpload onAnalyze={() => setView("chat")} />
7172
</div>
7273
)}
7374

@@ -91,12 +92,12 @@ export function ChatPanel() {
9192
</p>
9293

9394
<p className="text-xs font-medium text-gray-500 uppercase mb-2">What I can do:</p>
94-
<ul className="text-sm text-gray-600 space-y-1 mb-3">
95-
<li>&#x1F4CD; Query wells by location, status, or cluster</li>
96-
<li>&#x26A0;&#xFE0F; Detect anomalies: debit decline, TDS spikes, sensor faults</li>
97-
<li>&#x1F4C8; Analyze time series trends for any parameter</li>
98-
<li>&#x1F4CA; Regional statistics for the current viewport</li>
99-
<li>&#x1F4C4; Validate uploaded CSV observation files</li>
95+
<ul className="text-sm text-gray-600 space-y-1.5 mb-3">
96+
<li className="flex gap-2"><span className="text-gray-400 shrink-0">&bull;</span> Query wells by location, status, or cluster</li>
97+
<li className="flex gap-2"><span className="text-gray-400 shrink-0">&bull;</span> Detect anomalies: debit decline, TDS spikes, sensor faults</li>
98+
<li className="flex gap-2"><span className="text-gray-400 shrink-0">&bull;</span> Analyze time series trends for any parameter</li>
99+
<li className="flex gap-2"><span className="text-gray-400 shrink-0">&bull;</span> Regional statistics for the current viewport</li>
100+
<li className="flex gap-2"><span className="text-gray-400 shrink-0">&bull;</span> Validate uploaded CSV observation files</li>
100101
</ul>
101102

102103
<p className="text-xs font-medium text-gray-500 uppercase mb-2">How to use:</p>
@@ -135,6 +136,22 @@ export function ChatPanel() {
135136
<MessageBubble key={msg.id} message={msg} />
136137
))}
137138

139+
{/* Loading indicator */}
140+
{isLoading && !streamingText && (
141+
<div className="flex justify-start mb-3">
142+
<div className="bg-gray-100 rounded-2xl rounded-bl-md px-4 py-3">
143+
<div className="flex items-center gap-2 text-sm text-gray-500">
144+
<div className="flex gap-1">
145+
<span className="w-2 h-2 bg-blue-400 rounded-full animate-bounce [animation-delay:0ms]" />
146+
<span className="w-2 h-2 bg-blue-400 rounded-full animate-bounce [animation-delay:150ms]" />
147+
<span className="w-2 h-2 bg-blue-400 rounded-full animate-bounce [animation-delay:300ms]" />
148+
</div>
149+
<span>Analyzing...</span>
150+
</div>
151+
</div>
152+
</div>
153+
)}
154+
138155
{/* Streaming text */}
139156
{streamingText && (
140157
<div className="flex justify-start mb-3">
@@ -150,7 +167,17 @@ export function ChatPanel() {
150167
<div ref={messagesEndRef} />
151168
</div>
152169

153-
{/* Input — always visible */}
170+
{/* Quick commands */}
171+
<CommandBar
172+
onExecute={(prompt) => {
173+
const context = getApiContext();
174+
sendMessage(prompt, context);
175+
setView("chat");
176+
}}
177+
disabled={isLoading}
178+
/>
179+
180+
{/* Input */}
154181
<form onSubmit={handleSubmit} className="border-t px-4 py-3">
155182
<div className="flex gap-2">
156183
<input
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use client';
2+
3+
import { useState } from "react";
4+
5+
interface Command {
6+
id: string;
7+
label: string;
8+
description: string;
9+
prompt: string;
10+
category: "analysis" | "monitoring" | "data" | "report";
11+
}
12+
13+
const COMMANDS: Command[] = [
14+
{ id: "scan-anomalies", label: "Scan for anomalies", description: "Check all visible wells", prompt: "Scan all wells in the current viewport for anomalies. Report any debit decline, TDS spikes, or sensor faults.", category: "analysis" },
15+
{ id: "depression-check", label: "Depression cone analysis", description: "Analyze cones in viewport", prompt: "Analyze depression cones for wells in the current viewport. Are any cones overlapping? Is there evidence of well interference?", category: "analysis" },
16+
{ id: "interference-check", label: "Check well interference", description: "Detect mutual influence", prompt: "Check for well interference in the current viewport. Are any wells close enough to affect each other's drawdown?", category: "analysis" },
17+
18+
{ id: "region-overview", label: "Region overview", description: "Summary statistics", prompt: "Give me a complete overview of the region I'm looking at: total wells, active/inactive, average yield, water quality summary, any concerns.", category: "monitoring" },
19+
{ id: "water-quality", label: "Water quality report", description: "TDS and chemical analysis", prompt: "Generate a water quality report for wells in the viewport. Focus on TDS trends, chloride levels, and pH. Flag any wells exceeding UAE drinking water standards.", category: "monitoring" },
20+
{ id: "well-status", label: "Well status check", description: "Status of all wells", prompt: "Report the operational status of all wells in the viewport. Which are active, inactive, or under maintenance? Any that need attention?", category: "monitoring" },
21+
22+
{ id: "trend-analysis", label: "Trend analysis", description: "Debit and water level trends", prompt: "Analyze debit and water level trends for wells in the viewport over the full observation period. Any declining trends?", category: "data" },
23+
{ id: "compare-clusters", label: "Compare clusters", description: "Cross-cluster comparison", prompt: "Compare the well clusters visible in the viewport. Which cluster has the best/worst water quality? Highest/lowest yields?", category: "data" },
24+
25+
{ id: "daily-report", label: "Generate daily report", description: "Summary report", prompt: "Generate a daily monitoring report for all wells in the viewport. Include: operational status, water quality summary, anomalies detected, and recommendations.", category: "report" },
26+
];
27+
28+
const CATEGORY_LABELS: Record<string, string> = {
29+
analysis: "Analysis",
30+
monitoring: "Monitoring",
31+
data: "Data",
32+
report: "Reports",
33+
};
34+
35+
interface Props {
36+
onExecute: (prompt: string) => void;
37+
disabled: boolean;
38+
}
39+
40+
export function CommandBar({ onExecute, disabled }: Props) {
41+
const [isOpen, setIsOpen] = useState(false);
42+
43+
return (
44+
<div className="relative px-4 py-1.5 border-t bg-gray-50/50">
45+
<button
46+
onClick={() => setIsOpen(!isOpen)}
47+
disabled={disabled}
48+
className="flex items-center gap-1.5 text-xs text-gray-500 hover:text-blue-600 transition-colors disabled:opacity-50"
49+
>
50+
<span>Quick commands</span>
51+
<span className={`transition-transform ${isOpen ? "rotate-180" : ""}`}>&#9662;</span>
52+
</button>
53+
54+
{isOpen && (
55+
<div className="absolute bottom-full left-4 right-4 mb-1 bg-white border rounded-lg shadow-lg max-h-[300px] overflow-y-auto z-20">
56+
{(["analysis", "monitoring", "data", "report"] as const).map(category => (
57+
<div key={category}>
58+
<div className="px-3 py-1.5 text-[10px] uppercase font-semibold text-gray-400 bg-gray-50 sticky top-0">
59+
{CATEGORY_LABELS[category]}
60+
</div>
61+
{COMMANDS.filter(c => c.category === category).map(cmd => (
62+
<button
63+
key={cmd.id}
64+
onClick={() => { onExecute(cmd.prompt); setIsOpen(false); }}
65+
className="w-full flex items-start gap-2 px-3 py-2 hover:bg-blue-50 text-left transition-colors"
66+
>
67+
<div>
68+
<div className="text-sm font-medium text-gray-800">{cmd.label}</div>
69+
<div className="text-xs text-gray-400">{cmd.description}</div>
70+
</div>
71+
</button>
72+
))}
73+
</div>
74+
))}
75+
</div>
76+
)}
77+
</div>
78+
);
79+
}

frontend/src/components/Metrics/MetricsPanel.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,27 @@ const poolColors: Record<string, string> = {
3838
export function MetricsPanel() {
3939
const [data, setData] = useState<MetricsResponse | null>(null);
4040
const [error, setError] = useState<string | null>(null);
41+
const [isRunning, setIsRunning] = useState(false);
4142

42-
useEffect(() => {
43+
const fetchMetrics = () => {
4344
fetch("/api/metrics")
4445
.then((r) => r.json())
4546
.then(setData)
4647
.catch((e) => setError(e.message));
47-
}, []);
48+
};
49+
50+
useEffect(() => { fetchMetrics(); }, []);
51+
52+
const handleRunEval = async () => {
53+
setIsRunning(true);
54+
try {
55+
await fetch("/api/metrics/run", { method: "POST" });
56+
// Poll for results after 30s
57+
setTimeout(() => { fetchMetrics(); setIsRunning(false); }, 30000);
58+
} catch {
59+
setIsRunning(false);
60+
}
61+
};
4862

4963
if (error) return <div className="p-4 text-red-500 text-sm">Failed to load metrics: {error}</div>;
5064
if (!data) return <div className="p-4 text-gray-400 text-sm">Loading metrics...</div>;
@@ -55,9 +69,18 @@ export function MetricsPanel() {
5569
<div className="p-4 space-y-4">
5670
<div className="flex items-center justify-between">
5771
<h2 className="font-semibold text-lg">Model Evaluation</h2>
58-
<span className="text-xs text-gray-400 px-2 py-1 bg-gray-100 rounded">
59-
{data.source === "sample" ? "Sample data" : "Eval run"}
60-
</span>
72+
<div className="flex items-center gap-2">
73+
<span className="text-xs text-gray-400 px-2 py-1 bg-gray-100 rounded">
74+
{data.source === "sample" ? "Sample data" : "Eval run"}
75+
</span>
76+
<button
77+
onClick={handleRunEval}
78+
disabled={isRunning}
79+
className="text-xs px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors"
80+
>
81+
{isRunning ? "Running..." : "Run Eval"}
82+
</button>
83+
</div>
6184
</div>
6285

6386
{/* Comparison table */}

0 commit comments

Comments
 (0)