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.
91 lines
3.9 KiB
TypeScript
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" }}
|
|
>
|
|
← 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>
|
|
);
|
|
}
|