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}