feat: DebugPayloadViewer component with copy, export, collapsible sections
This commit is contained in:
parent
0c4208bc07
commit
ff1b24867f
3 changed files with 206 additions and 0 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -389,6 +389,9 @@ export interface PipelineEvent {
|
|||
model: string | null;
|
||||
duration_ms: number | null;
|
||||
payload: Record<string, unknown> | null;
|
||||
system_prompt_text: string | null;
|
||||
user_prompt_text: string | null;
|
||||
response_text: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,105 @@ function JsonViewer({ data }: { data: Record<string, unknown> | 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<Record<string, boolean>>({});
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(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<string, string | null> = {
|
||||
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 (
|
||||
<div className="debug-viewer">
|
||||
<div className="debug-viewer__header">
|
||||
<span className="debug-viewer__label">LLM Debug</span>
|
||||
<button className="debug-viewer__export" onClick={exportAsJson} title="Export debug payload as JSON">
|
||||
↓ JSON
|
||||
</button>
|
||||
</div>
|
||||
{sections.map((sec) => {
|
||||
const isOpen = !!openSections[sec.label];
|
||||
return (
|
||||
<div key={sec.label} className="debug-viewer__section">
|
||||
<div className="debug-viewer__section-header">
|
||||
<button
|
||||
className="debug-viewer__section-toggle"
|
||||
onClick={() => toggleSection(sec.label)}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{isOpen ? "▾" : "▸"} {sec.label}
|
||||
</button>
|
||||
<button
|
||||
className="debug-viewer__copy"
|
||||
onClick={() => void copyToClipboard(sec.label, sec.content)}
|
||||
title={`Copy ${sec.label}`}
|
||||
>
|
||||
{copiedKey === sec.label ? "✓ Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<pre className="debug-viewer__content">{sec.content}</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Event Log ────────────────────────────────────────────────────────────────
|
||||
|
||||
function EventLog({ videoId }: { videoId: string }) {
|
||||
|
|
@ -177,6 +276,7 @@ function EventLog({ videoId }: { videoId: string }) {
|
|||
<span className="pipeline-event__time">{formatDate(evt.created_at)}</span>
|
||||
</div>
|
||||
<JsonViewer data={evt.payload} />
|
||||
<DebugPayloadViewer event={evt} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue