File size: 6,151 Bytes
10ab54c
 
b84be35
10ab54c
 
 
b84be35
 
10ab54c
 
 
 
 
b84be35
10ab54c
 
b84be35
10ab54c
 
 
 
 
 
1184305
 
 
 
 
 
 
 
10ab54c
 
 
 
b84be35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c368c91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b84be35
 
c368c91
 
 
b84be35
 
 
c368c91
b84be35
 
 
 
 
10ab54c
 
 
 
 
1184305
10ab54c
 
1184305
10ab54c
 
 
1184305
10ab54c
 
 
1184305
 
 
 
 
10ab54c
1184305
 
 
 
 
 
 
 
 
 
b84be35
 
 
c368c91
b84be35
 
 
 
10ab54c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1184305
 
b84be35
 
 
 
 
 
c368c91
 
b84be35
 
 
10ab54c
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
<script lang="ts">
  import type { Card, StoneTrace } from '$lib/types/card';
  import type { EmissionsSummary } from '$lib/client/agentStream';

  /** Top-of-Findings status row. Mirrors findings.jsx RunHealth44:
   *  Stones · functions fired · evidence cards · wall-clock · silent /
   *  warn / error chips when nonzero. Also surfaces a compact emissions
   *  chip (mWh + tokens) when the backend reports a per-call ledger. */
  interface Props {
    cards: Card[];
    stones: StoneTrace[];
    wallSeconds?: number;
    cacheHit?: number;
    emissions?: EmissionsSummary;
  }

  let { cards, stones, wallSeconds, cacheHit, emissions }: Props = $props();

  function flatten(ms: StoneTrace['members']): StoneTrace['members'] {
    return ms.flatMap((m) => (m.children ? [m, ...flatten(m.children)] : [m]));
  }
  let allMembers = $derived(stones.flatMap((s) => flatten(s.members)));
  let total = $derived(allMembers.length);
  // v0.4.5 split: see SpecialistStatus in lib/types/card.ts.
  let fired = $derived(
    allMembers.filter((m) => m.status === 'fired' || m.status === 'warned').length
  );
  let silent = $derived(allMembers.filter((m) => m.status === 'silent_by_design').length);
  let warn = $derived(allMembers.filter((m) => m.status === 'warned').length);
  let err = $derived(allMembers.filter((m) => m.status === 'errored').length);
  let notInvoked = $derived(allMembers.filter((m) => m.status === 'not_invoked').length);

  let wall = $derived(wallSeconds == null
    ? '—'
    : wallSeconds < 1 ? `${Math.round(wallSeconds * 1000)}ms` : `${wallSeconds.toFixed(1)}s`);

  // Format emissions: prefer mWh under 100, else Wh; tokens with K-suffix.
  let emEnergy = $derived.by(() => {
    if (!emissions || emissions.total_wh === 0) return null;
    const wh = emissions.total_wh;
    if (wh < 0.1) return `${emissions.total_mwh.toFixed(1)} mWh`;
    return `${wh.toFixed(2)} Wh`;
  });
  let emTokens = $derived.by(() => {
    const t = emissions?.tokens?.total;
    if (!t) return null;
    return t >= 1000 ? `${(t / 1000).toFixed(1)}K tok` : `${t} tok`;
  });
  let emHardware = $derived.by(() => {
    if (!emissions) return null;
    const labels = Object.values(emissions.by_hardware).map(h => h.label);
    return labels.length === 1 ? labels[0] : labels.join(' + ');
  });
  // Fraction of calls that came back with a real NVML reading (vs.
  // data-sheet fallback). Surfaced as a small ✓ / ~ badge so the
  // viewer can tell whether the number is measured or estimated.
  let emMeasuredFrac = $derived(
    emissions && emissions.n_calls > 0
      ? (emissions.n_measured ?? 0) / emissions.n_calls
      : 0
  );
  let emMeasuredIcon = $derived(
    emEnergy == null
      ? ''
      : emMeasuredFrac >= 0.9
        ? '✓'           // all (or nearly all) calls measured on GPU
        : emMeasuredFrac > 0
          ? '◐'         // partial coverage
          : '~'         // pure data-sheet estimate
  );
  let emTooltip = $derived.by(() => {
    if (!emissions) return '';
    const measuredLine = emissions.n_measured != null
      ? `${emissions.n_measured}/${emissions.n_calls} calls measured on GPU (others use data-sheet estimate)`
      : '';
    const lines = [
      `${emissions.n_calls} inference calls — ${emissions.total_joules} J total`,
      emHardware ? `Hardware: ${emHardware}` : '',
      measuredLine,
      emissions.tokens.total ? `Tokens: ${emissions.tokens.prompt ?? 0} prompt + ${emissions.tokens.completion ?? 0} completion` : '',
      emissions.method,
    ].filter(Boolean);
    return lines.join('\n');
  });
</script>

<div class="rh">
  <span class="rh-item"><strong>{stones.length}</strong> Stones</span>
  <span class="rh-sep">·</span>
  <span class="rh-item"><strong>{fired}</strong> fired</span>
  {#if silent > 0}
    <span class="rh-sep">·</span>
    <span class="rh-item rh-silent"><strong>{silent}</strong> silent</span>
  {/if}
  {#if warn > 0}
    <span class="rh-sep">·</span>
    <span class="rh-item rh-warn"><strong>{warn}</strong> warned</span>
  {/if}
  {#if err > 0}
    <span class="rh-sep">·</span>
    <span class="rh-item rh-err"><strong>{err}</strong> errored</span>
  {/if}
  {#if notInvoked > 0}
    <span class="rh-sep">·</span>
    <span class="rh-item rh-notinvoked"><strong>{notInvoked}</strong> not invoked</span>
  {/if}
  <span class="rh-sep">·</span>
  <span class="rh-item"><strong>{cards.length}</strong> evidence card{cards.length === 1 ? '' : 's'}</span>
  <span class="rh-sep">·</span>
  <span class="rh-item"><strong>{wall}</strong> wall-clock</span>
  {#if cacheHit != null}
    <span class="rh-sep">·</span>
    <span class="rh-item"><strong>{Math.round(cacheHit * 100)}%</strong> cache</span>
  {/if}
  <span class="rh-sep">·</span>
  <span class="rh-item rh-total"><strong>{total}</strong> registered</span>
  {#if emEnergy}
    <span class="rh-sep">·</span>
    <span class="rh-item rh-em" title={emTooltip}>
      <span class="rh-em-icon" aria-hidden="true">{emMeasuredIcon}</span>
      <strong>{emEnergy}</strong> inference
      {#if emTokens}<span class="rh-em-tok">/ {emTokens}</span>{/if}
    </span>
  {/if}
</div>

<style>
  .rh {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-2);
    padding: var(--s-2) var(--s-4);
    background: var(--paper-deep);
    border-top: 1px solid var(--rule-soft);
    border-bottom: 1px solid var(--rule-soft);
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--ink-tertiary);
    letter-spacing: 0.04em;
  }
  .rh-item strong {
    font-weight: 600;
    color: var(--ink);
    margin-right: 2px;
  }
  .rh-sep { opacity: 0.5; }
  .rh-silent { color: var(--ink-tertiary); }
  .rh-warn { color: #B7791F; }
  .rh-err { color: #B91C1C; }
  .rh-notinvoked { color: var(--ink-tertiary); font-style: italic; }
  .rh-total strong { color: var(--ink-tertiary); }
  .rh-em {
    cursor: help;
    color: var(--ink-secondary);
  }
  .rh-em strong { color: var(--ink); }
  .rh-em-tok { margin-left: 4px; opacity: 0.75; }
  .rh-em-icon {
    margin-right: 4px;
    font-size: 10px;
    color: var(--ink-tertiary);
  }
</style>