File size: 3,915 Bytes
e8a6c67 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | <script lang="ts">
import type { TraceNode } from '$lib/types/trace';
import TierBadge from '$lib/components/glyphs/TierBadge.svelte';
import StatusGlyph from './StatusGlyph.svelte';
import Self from './TraceRow.svelte';
interface Props {
node: TraceNode;
depth?: number;
defaultOpen?: boolean;
}
let { node, depth = 0, defaultOpen = false }: Props = $props();
let open = $state(defaultOpen);
let copied = $state(false);
let hasChildren = $derived(!!node.children?.length);
let hasOutput = $derived(node.output != null || !!node.error);
let canExpand = $derived(hasChildren || hasOutput);
let indent = $derived(depth * 16);
function toggle() {
if (canExpand) open = !open;
}
let outputIsObject = $derived(
node.output != null && typeof node.output === 'object'
);
let formattedOutput = $derived.by(() => {
if (node.error) return node.error;
if (node.output == null) return '';
if (typeof node.output === 'string') return node.output;
try {
return JSON.stringify(node.output, null, 2);
} catch {
return String(node.output);
}
});
let outputLabel = $derived(
node.status === 'error' ? 'Error'
: node.status === 'silent' ? 'Silent reason'
: 'Output'
);
async function copyOutput(ev: MouseEvent) {
ev.stopPropagation();
try {
await navigator.clipboard.writeText(formattedOutput);
copied = true;
setTimeout(() => (copied = false), 1500);
} catch {
/* ignore — older browser, no clipboard permission */
}
}
</script>
<div class="trace-row trace-row-{node.status}" style:padding-left="{indent + 12}px">
<button
type="button"
class="trace-row-toggle"
onclick={toggle}
aria-expanded={canExpand ? open : undefined}
aria-label="{node.name}, {node.ms}ms, {node.status}{node.note ? ', ' + node.note : ''}"
disabled={!canExpand}
>
<span class="trace-tree-glyph" aria-hidden="true">
{hasChildren ? (open ? '▾' : '▸') : (hasOutput ? (open ? '▾' : '▸') : '·')}
</span>
<span class="trace-status-col"><StatusGlyph status={node.status} /></span>
<span class="trace-name-col">
<span class="trace-name">{node.name}</span>
{#if node.note}<span class="trace-note"> · {node.note}</span>{/if}
{#if node.docId}<span class="trace-doc-id" title="cited in briefing as [{node.docId}]">[{node.docId}]</span>{/if}
</span>
<span class="trace-ms-col">{node.ms}ms</span>
<span class="trace-tier-col">
{#if node.tier}<TierBadge tier={node.tier} compact />{/if}
{#if node.status === 'silent'}<span class="trace-silent-tag">silent</span>{/if}
</span>
</button>
{#if open && hasOutput}
<div class="trace-output-panel" style:margin-left="{indent + 44}px">
<div class="trace-output-head">
<span class="trace-output-label trace-output-label-{node.status}">{outputLabel}</span>
{#if node.model}
<span class="trace-output-model">model: <code>{node.model}</code></span>
{/if}
{#if node.claims != null}
<span class="trace-output-claims-count">{node.claims} claim{node.claims === 1 ? '' : 's'} cited</span>
{/if}
{#if formattedOutput}
<button
type="button"
class="trace-output-copy"
onclick={copyOutput}
aria-label="Copy {outputLabel.toLowerCase()} to clipboard"
>
{copied ? 'Copied' : 'Copy'}
</button>
{/if}
</div>
{#if outputIsObject}
<pre class="trace-output-pre">{formattedOutput}</pre>
{:else}
<p class="trace-output-text">{formattedOutput}</p>
{/if}
</div>
{/if}
</div>
{#if open && hasChildren && node.children}
{#each node.children as child (child.id)}
<Self node={child} depth={depth + 1} defaultOpen={child.status === 'fan'} />
{/each}
{/if}
|