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:
144
frontend/src/components/ReplayTimeline.tsx
Normal file
144
frontend/src/components/ReplayTimeline.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user