SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
8.2 KB · 164 lines tsx
Raw Blame History
1'use client';23import Link from 'next/link';4import { useEffect, useState } from 'react';5import { Bar } from '@/components/ui/primitives';6import { fmt, fmtDelta, fmtInt } from '@/lib/format';7import { COMPONENT_LABEL, pressureColor } from '@/lib/pressure';8import { Time } from '@/lib/time';9import type { Explain, GlobalPressure } from '@/lib/types';1011function scopeHref(scopeType: string, scopeId: string | null): string | null {12  if (!scopeId) return null;13  if (scopeType === 'region') return `/internet/${scopeId}`;14  if (scopeType === 'country') return `/country/${scopeId.toLowerCase()}`;15  if (scopeType === 'asn') return `/asn/${scopeId}`;16  if (scopeType === 'service') return `/service/${scopeId}`;17  if (scopeType === 'bgp') return '/bgp';18  return null;19}2021/** "Why is Global Pressure 42.7?" — explain rows, then components → signals from /api/v1/explain. */22export function ExplainPanel({ global, onClose }: { global: GlobalPressure; onClose: () => void }) {23  const [deep, setDeep] = useState<Explain | null>(null);24  const [err, setErr] = useState<string | null>(null);25  const [openComp, setOpenComp] = useState<string | null>(null);2627  useEffect(() => {28    const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose();29    window.addEventListener('keydown', onKey);30    document.body.style.overflow = 'hidden';31    fetch('/api/v1/explain')32      .then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status)))))33      .then((d: Explain) => setDeep(d))34      .catch(() => setErr('Deep explanation unavailable'));35    return () => {36      window.removeEventListener('keydown', onKey);37      document.body.style.overflow = '';38    };39  }, [onClose]);4041  return (42    <div className="fixed inset-0 z-[90] flex justify-end bg-bg/70" onClick={onClose} role="presentation">43      <aside role="dialog" aria-modal="true" aria-labelledby="explain-title" className="flex h-full w-full max-w-[640px] flex-col border-l border-line bg-panel" onClick={(e) => e.stopPropagation()}>44        <header className="flex items-start justify-between gap-4 border-b border-line px-5 py-4">45          <div>46            <p className="label">Explain</p>47            <h2 id="explain-title" className="mt-1 text-[17px] font-medium">48              Why is Global Pressure <span className="num" style={{ color: pressureColor(global.level) }}>{fmt(global.pressure)}</span>?49            </h2>50            <p className="mt-1 text-[11.5px] text-ink-2">51              Engine cycle <Time ts={global.ts} style="time" className="num" /> · confidence {Math.round(global.confidence * 100)} %52            </p>53          </div>54          <button type="button" onClick={onClose} className="rounded-[3px] border border-line px-2 py-1 text-xs text-ink-2 hover:text-ink" aria-label="Close">55            esc56          </button>57        </header>5859        <div className="flex-1 overflow-y-auto px-5 py-4">60          <p className="label mb-2">Contributions</p>61          <ul className="divide-y divide-line">62            {global.explain.map((row, i) => {63              const href = scopeHref(row.scope_type, row.scope_id);64              return (65                <li key={i} className="flex items-baseline gap-3 py-2 text-[13px]">66                  <span className="num w-14 shrink-0 text-right" style={{ color: row.points >= 0 ? pressureColor(Math.min(100, 30 + row.points * 3)) : 'var(--p-calm)' }}>67                    {fmtDelta(row.points)}68                  </span>69                  <span className="flex-1 text-ink">{row.text.replace(/^[+−-]\s?[\d.]+\s?(points?)?\s?(from|because)?\s?/i, (m) => (m.trim().endsWith('because') ? 'Because ' : ''))}</span>70                  <span className="label shrink-0">{COMPONENT_LABEL[row.component] ?? row.component}</span>71                  {href && (72                    <Link href={href} className="shrink-0 text-xs text-accent hover:underline" onClick={onClose}>73                      open →74                    </Link>75                  )}76                </li>77              );78            })}79          </ul>8081          <p className="label mb-2 mt-6">Components → signals</p>82          {err && <p className="text-xs text-warn">{err}</p>}83          {!deep && !err && <p className="text-xs text-ink-3">Loading signal breakdown…</p>}84          {deep && (85            <ul className="divide-y divide-line">86              {deep.components.map((c) => {87                const isOpen = openComp === c.id;88                return (89                  <li key={c.id}>90                    <button type="button" onClick={() => setOpenComp(isOpen ? null : c.id)} className="grid w-full grid-cols-[1fr_auto_auto_auto] items-center gap-3 py-2 text-left text-[13px]" aria-expanded={isOpen}>91                      <span className="text-ink">92                        <span className="mr-2 inline-block w-3 text-ink-3">{isOpen ? '−' : '+'}</span>93                        {COMPONENT_LABEL[c.id] ?? c.id}94                      </span>95                      <span className="num text-right" style={{ color: pressureColor(c.score) }}>96                        {fmt(c.score)}97                      </span>98                      <span className="num text-right text-ink-3">× {fmt(c.weight, 2)}</span>99                      <span className="num w-12 text-right text-ink">{fmtDelta(c.contribution)}</span>100                    </button>101                    {isOpen && (102                      <table className="tbl mb-3">103                        <thead>104                          <tr>105                            <th>Signal</th>106                            <th>Scope</th>107                            <th className="r">Current</th>108                            <th className="r">Median</th>109                            <th className="r">MAD</th>110                            <th className="r">z</th>111                            <th className="r">n</th>112                            <th className="r">Stress</th>113                            <th className="r">Pts</th>114                          </tr>115                        </thead>116                        <tbody>117                          {c.signals.map((s) => (118                            <tr key={`${s.signal_id}-${s.scope_id}`}>119                              <td className="max-w-[200px] truncate text-ink" title={s.label}>120                                {s.label}121                              </td>122                              <td className="text-ink-2">{s.scope_id ? `${s.scope_type}:${s.scope_id}` : s.scope_type}</td>123                              <td className="num r">{fmt(s.current, s.current < 1 ? 3 : 1)}</td>124                              <td className="num r text-ink-2">{fmt(s.baseline_median, s.baseline_median < 1 ? 3 : 1)}</td>125                              <td className="num r text-ink-2">{fmt(s.mad, s.mad < 1 ? 3 : 1)}</td>126                              <td className="num r" style={{ color: Math.abs(s.robust_z) >= 3 ? 'var(--p-high)' : 'var(--ink)' }}>127                                {fmt(s.robust_z)}128                              </td>129                              <td className="num r text-ink-2">{fmtInt(s.samples)}</td>130                              <td className="r">131                                <Bar value={s.stress * 100} className="inline-block w-10 align-middle" />132                              </td>133                              <td className="num r">{fmtDelta(s.contribution)}</td>134                            </tr>135                          ))}136                        </tbody>137                      </table>138                    )}139                  </li>140                );141              })}142            </ul>143          )}144          {deep?.notes?.length ? (145            <ul className="mt-5 space-y-1 text-[11.5px] text-ink-3">146              {deep.notes.map((n, i) => (147                <li key={i}>· {n}</li>148              ))}149            </ul>150          ) : null}151          {deep?.excluded_probes?.length ? <p className="mt-3 text-[11.5px] text-warn">Excluded probes (self-exclusion): {deep.excluded_probes.join(', ')}</p> : null}152        </div>153        <footer className="border-t border-line px-5 py-3 text-[11.5px] text-ink-3">154          Composite observability index, not scientific truth. Weights and thresholds in{' '}155          <Link href="/methodology" className="text-accent hover:underline" onClick={onClose}>156            Methodology157          </Link>158          .159        </footer>160      </aside>161    </div>162  );163}164