File size: 8,518 Bytes
d1ac27a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/** Stage 12: recommended experiments, minimum-additional-evidence answer,
 * rendered final report with remediation tiers, JSON/markdown export, and the
 * training-mode reveal. */
import { useState } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Collapse from '@mui/material/Collapse';
import Typography from '@mui/material/Typography';
import { Download, Eye, FileJson } from 'lucide-react';
import type { Experiments, Investigation, RcaReport, RevealResponse } from '../../types/rca';
import { BORDER, CONFIDENCE_COLORS, Explainer, SectionCard, StatusPill, tableSx } from './common';

function download(filename: string, text: string, mime: string) {
  const blob = new Blob([text], { type: mime });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

const COST_COLORS: Record<string, string> = { low: '#66bb6a', medium: '#ffb74d', high: '#ef5350' };

export function ExperimentsView({ exp }: { exp: Experiments }) {
  return (
    <>
      <Explainer text={exp.explainer} />
      <SectionCard title="Minimum additional evidence required">
        <Typography variant="body2" sx={{ fontSize: 12.5, color: '#e3eaf0' }}>
          {exp.minimum_additional_evidence}
        </Typography>
        <Box sx={{ display: 'flex', gap: 3, mt: 1 }}>
          <Typography variant="caption" sx={{ color: '#8a949e' }}>
            Current effective n: <b>{exp.power.effective_n.toLocaleString()}</b>
          </Typography>
          <Typography variant="caption" sx={{ color: '#8a949e' }}>
            Needed for the {exp.power.practical_margin_pp.toFixed(1)}pp margin:{' '}
            <b>{exp.power.needed_effective_n.toLocaleString()}</b>
          </Typography>
        </Box>
      </SectionCard>
      <SectionCard title="Recommended experiments — ranked by information gain / cost">
        <Box component="table" sx={tableSx}>
          <thead>
            <tr>
              <th>#</th><th>Design</th><th>Instantiated for this investigation</th>
              <th>Discriminates</th><th>Cost</th><th>Gain</th><th>Priority</th>
            </tr>
          </thead>
          <tbody>
            {exp.experiments.map((d) => (
              <tr key={d.id}>
                <td style={{ fontWeight: 800 }}>{d.rank}</td>
                <td style={{ fontWeight: 700, whiteSpace: 'nowrap' }}>{d.design}</td>
                <td><Typography variant="caption" sx={{ color: '#cfd8e0' }}>{d.description}</Typography></td>
                <td>
                  {d.discriminates.map((h) => (
                    <Chip key={h} size="small" label={h} sx={{ height: 17, fontSize: 9.5, mr: 0.25, mb: 0.25, bgcolor: '#232a31' }} />
                  ))}
                </td>
                <td>
                  <Chip size="small" label={`${d.cost} · ~${d.expected_days}d`} sx={{
                    height: 18, fontSize: 10, fontWeight: 700,
                    bgcolor: `${COST_COLORS[d.cost]}22`, color: COST_COLORS[d.cost],
                  }} />
                </td>
                <td style={{ fontFamily: 'monospace' }}>{d.information_gain.toFixed(1)}</td>
                <td style={{ fontFamily: 'monospace', fontWeight: 800 }}>{d.priority.toFixed(1)}</td>
              </tr>
            ))}
          </tbody>
        </Box>
      </SectionCard>
    </>
  );
}

export function ReportView({ report, inv, reveal, onReveal }: {
  report: RcaReport;
  inv: Investigation;
  reveal: RevealResponse | null;
  onReveal: () => void;
}) {
  const [showMd, setShowMd] = useState(false);
  const ef = report.executive_finding;
  const confColor = CONFIDENCE_COLORS[ef.confidence];
  return (
    <>
      <SectionCard
        title="Executive finding"
        action={
          <Box sx={{ display: 'flex', gap: 1 }}>
            <Button size="small" startIcon={<FileJson size={14} />} onClick={() => download(`${report.investigation_id}-report.json`, JSON.stringify(report, null, 2), 'application/json')}>
              Export JSON
            </Button>
            <Button size="small" startIcon={<Download size={14} />} onClick={() => download(`${report.investigation_id}-report.md`, report.markdown, 'text/markdown')}>
              Export MD
            </Button>
          </Box>
        }
      >
        <Typography variant="h6" sx={{ fontWeight: 800, fontSize: 17 }}>
          {ef.conclusion ?? ef.top_hypothesis}
          <Chip size="small" label={`confidence: ${ef.confidence}`} sx={{ ml: 1.5, fontWeight: 800, bgcolor: `${confColor}22`, color: confColor, border: `1px solid ${confColor}55` }} />
        </Typography>
        <Typography variant="body2" sx={{ color: '#aab4be', mt: 0.5 }}>
          {ef.label} — score {ef.score} (gap to runner-up {ef.score_gap_to_runner_up}). Claims under
          investigation: offline {report.claims.offline_delta_pp >= 0 ? '+' : ''}{report.claims.offline_delta_pp}pp vs shadow{' '}
          {report.claims.shadow_delta_pp}pp on {report.claims.metric}.
        </Typography>
      </SectionCard>

      <SectionCard title="Per-stage summary">
        <Box component="table" sx={tableSx}>
          <thead>
            <tr><th>Stage</th><th>Question</th><th>Verdict</th><th>Mismatches / unknowns</th></tr>
          </thead>
          <tbody>
            {report.stage_summaries.map((s) => (
              <tr key={s.stage}>
                <td style={{ fontWeight: 600, whiteSpace: 'nowrap' }}>{s.stage}</td>
                <td><Typography variant="caption" sx={{ color: '#8a949e' }}>{s.headline}</Typography></td>
                <td><StatusPill status={s.verdict === 'clean' ? 'PASS' : s.verdict === 'mismatch' ? 'MISMATCH' : 'UNKNOWN'} /></td>
                <td>
                  <Typography variant="caption">
                    {[...s.mismatches, ...s.unknowns].join('; ') || '—'}
                  </Typography>
                </td>
              </tr>
            ))}
          </tbody>
        </Box>
      </SectionCard>

      <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 2 }}>
        {(['containment', 'short_term', 'long_term'] as const).map((tier) => (
          <SectionCard key={tier} title={tier === 'containment' ? 'Containment (now)' : tier === 'short_term' ? 'Short term' : 'Long term'}>
            {report.remediation[tier].map((x) => (
              <Typography key={x} variant="body2" sx={{ fontSize: 12, py: 0.25, color: '#cfd8e0' }}>
                • {x}
              </Typography>
            ))}
          </SectionCard>
        ))}
      </Box>

      {report.acknowledged_unknowns.length ? (
        <SectionCard title="Unknowns explicitly acknowledged during this investigation">
          {report.acknowledged_unknowns.map((e, i) => (
            <Typography key={i} variant="caption" sx={{ display: 'block', color: '#ffb74d' }}>
              ⚠ {e.message} — note: {String((e.data as any)?.note ?? '')}
            </Typography>
          ))}
        </SectionCard>
      ) : null}

      <SectionCard
        title="Raw markdown"
        action={<Button size="small" onClick={() => setShowMd((s) => !s)}>{showMd ? 'hide' : 'show'}</Button>}
      >
        <Collapse in={showMd}>
          <Box component="pre" sx={{ fontSize: 11, fontFamily: 'monospace', whiteSpace: 'pre-wrap', color: '#aab4be', m: 0, maxHeight: 420, overflowY: 'auto' }}>
            {report.markdown}
          </Box>
        </Collapse>
      </SectionCard>

      {inv.training_mode ? (
        <SectionCard title="Training mode — the answer key">
          {reveal ? (
            <Box sx={{ p: 1.25, borderRadius: 1, border: `1px solid ${BORDER}`, bgcolor: '#0d1116' }}>
              <Typography variant="body2" sx={{ fontWeight: 800, color: reveal.cause === ef.top_hypothesis ? '#66bb6a' : '#ef5350' }}>
                Planted cause: {reveal.cause}{' '}
                {reveal.cause === ef.top_hypothesis ? '— your board got it right ✓' : `— your board's top pick was ${ef.top_hypothesis} ✗`}
              </Typography>
              <Typography variant="caption" sx={{ color: '#aab4be' }}>{reveal.explanation}</Typography>
            </Box>
          ) : (
            <Button variant="outlined" size="small" startIcon={<Eye size={14} />} onClick={onReveal}>
              Reveal the planted root cause
            </Button>
          )}
        </SectionCard>
      ) : null}
    </>
  );
}