Files
smart-support/frontend/src/pages/ReplayPage.tsx
Yaojia Wang af53111928 refactor: fix architectural issues across frontend and backend
Address all architecture review findings:

P0 fixes:
- Add API key authentication for admin endpoints (analytics, replay, openapi)
  and WebSocket connections via ADMIN_API_KEY env var
- Add PostgreSQL-backed PgSessionManager and PgInterruptManager for
  multi-worker production deployments (in-memory defaults preserved)

P1 fixes:
- Implement actual tool generation in OpenAPI approve_job endpoint
  using generate_tool_code() and generate_agent_yaml()
- Add missing clarification, interrupt_expired, and tool_result message
  handlers in frontend ChatPage

P2 fixes:
- Replace monkey-patching on CompiledStateGraph with typed GraphContext
- Replace 9-param dispatch_message with WebSocketContext dataclass
- Extract duplicate _envelope() into shared app/api_utils.py
- Replace mutable module-level counter with crypto.randomUUID()
- Remove hardcoded mock data from ReviewPage, use api.ts wrappers
- Remove `as any` type escape from ReplayPage

All 516 tests passing, 0 TypeScript errors.
2026-04-06 15:59:14 +02:00

91 lines
3.9 KiB
TypeScript

import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ReplayTimeline } from "../components/ReplayTimeline";
import { fetchReplay, ReplayStep } from "../api";
export function ReplayPage() {
const { threadId } = useParams<{ threadId: string }>();
const navigate = useNavigate();
const [steps, setSteps] = useState<ReplayStep[]>([]);
const [totalSteps, setTotalSteps] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!threadId) return;
setIsLoading(true);
setError(null);
fetchReplay(threadId, 1, 100)
.then((result) => {
setSteps(result.steps);
setTotalSteps(result.total_steps);
})
.catch((err: Error) => setError(err.message))
.finally(() => setIsLoading(false));
}, [threadId]);
if (!threadId) return null;
return (
<div className="page-container">
<div className="page-header" style={{ marginBottom: "2rem" }}>
<button
onClick={() => navigate("/replay")}
style={{ background: "none", border: "none", color: "var(--text-secondary)", fontSize: "0.875rem", cursor: "pointer", padding: "0 0 0.5rem 0", display: "flex", alignItems: "center", gap: "0.25rem" }}
>
&larr; Back to All Replays
</button>
<h2>Audit Trail: <span style={{ fontFamily: "monospace", color: "var(--brand-primary)" }}>{threadId}</span></h2>
<p>Detailed temporal log of agent reflections, MCP tool calls, and human overrides.</p>
</div>
{error ? (
<div className="error-state">
<p className="error-state__title">Failed to load replay</p>
<p className="error-state__description">{error}</p>
</div>
) : isLoading ? (
<div style={{ display: "grid", gridTemplateColumns: "1fr 3fr", gap: "2rem" }}>
<div className="skeleton-box" style={{ height: "250px", borderRadius: "var(--radius-xl)", background: "var(--bg-surface)" }}></div>
<div className="skeleton-box" style={{ height: "400px", borderRadius: "var(--radius-xl)", background: "var(--bg-surface)" }}></div>
</div>
) : steps.length === 0 ? (
<div className="empty-state">
<p className="empty-state__title">No replay steps found</p>
<p className="empty-state__description">This conversation has no recorded checkpoints.</p>
</div>
) : (
<div style={{ display: "grid", gridTemplateColumns: "1fr 3fr", gap: "2rem" }}>
{/* Sidebar Summary Info */}
<div className="section-card" style={{ alignSelf: "start" }}>
<h3 style={{ fontSize: "1rem", marginBottom: "1.25rem", color: "var(--text-primary)" }}>Session Context</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div>
<div className="stat-label">Thread ID</div>
<div className="stat-value" style={{ fontSize: "0.8125rem", fontFamily: "monospace", wordBreak: "break-all" }}>{threadId}</div>
</div>
<div>
<div className="stat-label">Total Steps</div>
<div className="stat-value">{totalSteps}</div>
</div>
<div>
<div className="stat-label">Time Range</div>
<div style={{ fontSize: "0.8125rem" }}>
{steps[0]?.timestamp ? new Date(steps[0].timestamp).toLocaleString() : "N/A"}
{" \u2013 "}
{steps[steps.length - 1]?.timestamp ? new Date(steps[steps.length - 1].timestamp).toLocaleString() : "N/A"}
</div>
</div>
</div>
</div>
{/* Timeline */}
<div className="section-card" style={{ padding: "2rem" }}>
<ReplayTimeline steps={steps} />
</div>
</div>
)}
</div>
);
}