feat: complete phase 5 -- error hardening, frontend, Docker, demo, docs

Backend:
- ConversationTracker: Protocol + PostgresConversationTracker for lifecycle tracking
- Error handler: ErrorCategory enum, classify_error(), with_retry() exponential backoff
- Wire PostgresAnalyticsRecorder + ConversationTracker into ws_handler
- Rate limiting (10 msg/10s per thread), edge case hardening
- Health endpoint GET /api/health, version 0.5.0
- Demo seed data script + sample OpenAPI spec

Frontend (all new):
- React Router with NavBar (Chat / Replay / Dashboard / Review)
- ReplayListPage + ReplayPage with ReplayTimeline component
- DashboardPage with MetricCard, range selector, zero-state
- ReviewPage for OpenAPI classification review
- ErrorBanner for WebSocket disconnect handling
- API client (api.ts) with typed fetch wrappers

Infrastructure:
- Frontend Dockerfile (multi-stage node -> nginx)
- nginx.conf with SPA routing + API/WS proxy
- docker-compose.yml with frontend service + healthchecks
- .env.example files (root + backend)

Documentation:
- README.md with quick start and architecture
- Agent configuration guide
- OpenAPI import guide
- Deployment guide
- Demo script

48 new tests, 449 total passing, 92.87% coverage
This commit is contained in:
Yaojia Wang
2026-03-31 21:20:06 +02:00
parent 38644594d2
commit 0e78e5b06b
44 changed files with 3397 additions and 169 deletions

View File

@@ -0,0 +1,144 @@
import { useState } from "react";
import type { ReplayStep } from "../api";
const TYPE_COLORS: Record<string, string> = {
message: "#1976d2",
token: "#388e3c",
tool_call: "#f57c00",
tool_result: "#7b1fa2",
interrupt: "#d32f2f",
interrupt_response: "#c2185b",
error: "#c62828",
};
function TypeBadge({ type }: { type: string }) {
const color = TYPE_COLORS[type] ?? "#555";
return (
<span
style={{
background: color,
color: "#fff",
fontSize: "11px",
fontWeight: 600,
padding: "2px 7px",
borderRadius: "10px",
textTransform: "uppercase",
letterSpacing: "0.5px",
}}
>
{type}
</span>
);
}
function ReplayStepItem({ step }: { step: ReplayStep }) {
const [expanded, setExpanded] = useState(false);
const hasDetails = step.params != null || step.result != null;
return (
<div
style={{
borderLeft: "2px solid #e0e0e0",
paddingLeft: "12px",
marginBottom: "12px",
position: "relative",
}}
>
<div
style={{
position: "absolute",
left: "-5px",
top: "4px",
width: "8px",
height: "8px",
borderRadius: "50%",
background: TYPE_COLORS[step.type] ?? "#555",
}}
/>
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}>
<span style={{ fontSize: "11px", color: "#888" }}>#{step.step}</span>
<TypeBadge type={step.type} />
{step.agent && (
<span style={{ fontSize: "11px", color: "#666", fontStyle: "italic" }}>
{step.agent}
</span>
)}
{step.tool && (
<span style={{ fontSize: "11px", color: "#555" }}>
tool: <strong>{step.tool}</strong>
</span>
)}
<span style={{ fontSize: "11px", color: "#aaa", marginLeft: "auto" }}>
{new Date(step.timestamp).toLocaleTimeString()}
</span>
</div>
{step.content && (
<div
style={{
fontSize: "13px",
color: "#333",
background: "#f9f9f9",
padding: "6px 10px",
borderRadius: "4px",
maxHeight: "80px",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{step.content}
</div>
)}
{hasDetails && (
<button
onClick={() => setExpanded((v) => !v)}
style={{
background: "none",
border: "none",
color: "#1976d2",
cursor: "pointer",
fontSize: "12px",
padding: "2px 0",
}}
>
{expanded ? "Hide details" : "Show details"}
</button>
)}
{expanded && hasDetails && (
<pre
style={{
fontSize: "11px",
background: "#f3f3f3",
padding: "8px",
borderRadius: "4px",
overflow: "auto",
maxHeight: "200px",
}}
>
{JSON.stringify({ params: step.params, result: step.result }, null, 2)}
</pre>
)}
</div>
);
}
interface ReplayTimelineProps {
steps: ReplayStep[];
}
export function ReplayTimeline({ steps }: ReplayTimelineProps) {
if (steps.length === 0) {
return (
<div style={{ color: "#888", fontSize: "14px", padding: "16px 0" }}>
No steps recorded.
</div>
);
}
return (
<div style={{ padding: "8px 0" }}>
{steps.map((step) => (
<ReplayStepItem key={step.step} step={step} />
))}
</div>
);
}