diff --git a/frontend/src/App.css b/frontend/src/App.css index 720eea7..ba54afb 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2977,3 +2977,106 @@ a.app-footer__repo:hover { max-height: 300px; overflow-y: auto; } + +/* ── Debug Payload Viewer ───────────────────────────────────────────────── */ + +.debug-viewer { + margin-top: 0.375rem; + border: 1px solid var(--color-border); + border-radius: 4px; + background: var(--color-bg-surface); + overflow: hidden; +} + +.debug-viewer__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.375rem 0.625rem; + border-bottom: 1px solid var(--color-border); +} + +.debug-viewer__label { + font-size: 0.7rem; + font-weight: 600; + color: var(--color-accent); + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.debug-viewer__export { + background: none; + border: 1px solid var(--color-border); + border-radius: 3px; + color: var(--color-text-secondary); + font-size: 0.7rem; + cursor: pointer; + padding: 0.125rem 0.5rem; + font-family: inherit; + transition: color 0.15s, border-color 0.15s; +} + +.debug-viewer__export:hover { + color: var(--color-accent); + border-color: var(--color-accent); +} + +.debug-viewer__section { + border-top: 1px solid var(--color-border); +} + +.debug-viewer__section:first-of-type { + border-top: none; +} + +.debug-viewer__section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0.625rem; +} + +.debug-viewer__section-toggle { + background: none; + border: none; + color: var(--color-text-primary); + font-size: 0.75rem; + cursor: pointer; + padding: 0; + font-family: inherit; +} + +.debug-viewer__section-toggle:hover { + color: var(--color-accent); +} + +.debug-viewer__copy { + background: none; + border: none; + color: var(--color-text-secondary); + font-size: 0.675rem; + cursor: pointer; + padding: 0.125rem 0.375rem; + font-family: inherit; + border-radius: 3px; + transition: color 0.15s, background 0.15s; +} + +.debug-viewer__copy:hover { + color: var(--color-accent); + background: var(--color-accent-subtle); +} + +.debug-viewer__content { + margin: 0; + padding: 0.5rem 0.75rem; + background: var(--color-bg-transcript); + color: var(--color-text-secondary); + font-size: 0.75rem; + line-height: 1.5; + overflow-x: auto; + max-height: 400px; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-word; +} diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index 650da87..4f6817b 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -389,6 +389,9 @@ export interface PipelineEvent { model: string | null; duration_ms: number | null; payload: Record | null; + system_prompt_text: string | null; + user_prompt_text: string | null; + response_text: string | null; created_at: string | null; } diff --git a/frontend/src/pages/AdminPipeline.tsx b/frontend/src/pages/AdminPipeline.tsx index f903ba0..2a1ac72 100644 --- a/frontend/src/pages/AdminPipeline.tsx +++ b/frontend/src/pages/AdminPipeline.tsx @@ -95,6 +95,105 @@ function JsonViewer({ data }: { data: Record | null }) { ); } +// ── Debug Payload Viewer ───────────────────────────────────────────────────── + +interface DebugSection { + label: string; + content: string; +} + +function DebugPayloadViewer({ event }: { event: PipelineEvent }) { + const sections: DebugSection[] = []; + if (event.system_prompt_text) sections.push({ label: "System Prompt", content: event.system_prompt_text }); + if (event.user_prompt_text) sections.push({ label: "User Prompt", content: event.user_prompt_text }); + if (event.response_text) sections.push({ label: "Response", content: event.response_text }); + + const [openSections, setOpenSections] = useState>({}); + const [copiedKey, setCopiedKey] = useState(null); + + if (sections.length === 0) return null; + + const toggleSection = (label: string) => { + setOpenSections((prev) => ({ ...prev, [label]: !prev[label] })); + }; + + const copyToClipboard = async (label: string, text: string) => { + try { + await navigator.clipboard.writeText(text); + setCopiedKey(label); + setTimeout(() => setCopiedKey(null), 1500); + } catch { + // Fallback for non-HTTPS contexts + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + setCopiedKey(label); + setTimeout(() => setCopiedKey(null), 1500); + } + }; + + const exportAsJson = () => { + const data: Record = { + event_id: event.id, + stage: event.stage, + event_type: event.event_type, + model: event.model, + system_prompt_text: event.system_prompt_text, + user_prompt_text: event.user_prompt_text, + response_text: event.response_text, + }; + const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `debug-${event.stage}-${event.id.slice(0, 8)}.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+
+ LLM Debug + +
+ {sections.map((sec) => { + const isOpen = !!openSections[sec.label]; + return ( +
+
+ + +
+ {isOpen && ( +
{sec.content}
+ )} +
+ ); + })} +
+ ); +} + // ── Event Log ──────────────────────────────────────────────────────────────── function EventLog({ videoId }: { videoId: string }) { @@ -177,6 +276,7 @@ function EventLog({ videoId }: { videoId: string }) { {formatDate(evt.created_at)} + ))}