hex-audit 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dashboard/.next/BUILD_ID +1 -1
- package/dashboard/.next/app-build-manifest.json +30 -22
- package/dashboard/.next/app-path-routes-manifest.json +1 -1
- package/dashboard/.next/build-manifest.json +4 -4
- package/dashboard/.next/next-server.js.nft.json +1 -1
- package/dashboard/.next/prerender-manifest.js +1 -1
- package/dashboard/.next/prerender-manifest.json +1 -1
- package/dashboard/.next/required-server-files.json +1 -1
- package/dashboard/.next/routes-manifest.json +1 -1
- package/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/_not-found.html +1 -1
- package/dashboard/.next/server/app/_not-found.rsc +4 -4
- package/dashboard/.next/server/app/access/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/api/notes/done/route.js +1 -0
- package/dashboard/.next/server/app/api/notes/done/route.js.nft.json +1 -0
- package/dashboard/.next/server/app/api/notes/lead/route.js +1 -0
- package/dashboard/.next/server/app/api/notes/lead/route.js.nft.json +1 -0
- package/dashboard/.next/server/app/api/notes/question/route.js +1 -0
- package/dashboard/.next/server/app/api/notes/question/route.js.nft.json +1 -0
- package/dashboard/.next/server/app/api/notes/record/route.js +9 -0
- package/dashboard/.next/server/app/api/notes/record/route.js.nft.json +1 -0
- package/dashboard/.next/server/app/api/notes/route.js +3 -0
- package/dashboard/.next/server/app/api/notes/route.js.nft.json +1 -0
- package/dashboard/.next/server/app/api/progress/route.js +1 -1
- package/dashboard/.next/server/app/calls/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/conformance/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/diagram/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/diagram.html +1 -1
- package/dashboard/.next/server/app/diagram.rsc +4 -4
- package/dashboard/.next/server/app/flows/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/flows.html +1 -1
- package/dashboard/.next/server/app/flows.rsc +4 -4
- package/dashboard/.next/server/app/functions/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/issues/page.js +4 -4
- package/dashboard/.next/server/app/issues/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/notes/page.js +22 -0
- package/dashboard/.next/server/app/notes/page.js.nft.json +1 -0
- package/dashboard/.next/server/app/notes/page_client-reference-manifest.js +1 -0
- package/dashboard/.next/server/app/overleaf/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/page.js +2 -2
- package/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/progress/page.js +1 -1
- package/dashboard/.next/server/app/progress/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/stats/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app-paths-manifest.json +15 -9
- package/dashboard/.next/server/chunks/530.js +1 -1
- package/dashboard/.next/server/chunks/622.js +3 -0
- package/dashboard/.next/server/functions-config-manifest.json +1 -1
- package/dashboard/.next/server/middleware-build-manifest.js +1 -1
- package/dashboard/.next/server/pages/404.html +1 -1
- package/dashboard/.next/server/pages/500.html +1 -1
- package/dashboard/.next/server/server-reference-manifest.js +1 -1
- package/dashboard/.next/server/server-reference-manifest.json +1 -1
- package/dashboard/.next/static/chunks/app/issues/{page-abb062e4cf118340.js → page-72089090c04e96ee.js} +1 -1
- package/dashboard/.next/static/chunks/app/{layout-2c5383c11b1fbd97.js → layout-a95b696089e278c3.js} +1 -1
- package/dashboard/.next/static/chunks/app/notes/page-17af94c67ba79c00.js +1 -0
- package/dashboard/.next/static/chunks/app/page-24bac51dfe007c66.js +1 -0
- package/dashboard/.next/static/chunks/app/progress/page-427fb18da3afb061.js +1 -0
- package/dashboard/.next/static/chunks/{main-8f1e837090aef5cf.js → main-9a811c95265dbd11.js} +1 -1
- package/dashboard/.next/static/css/4a38d8f6f6b7a68f.css +3 -0
- package/dashboard/app/api/notes/done/route.ts +26 -0
- package/dashboard/app/api/notes/lead/route.ts +40 -0
- package/dashboard/app/api/notes/question/route.ts +30 -0
- package/dashboard/app/api/notes/record/route.ts +60 -0
- package/dashboard/app/api/notes/route.ts +25 -0
- package/dashboard/app/notes/NotesClient.tsx +701 -0
- package/dashboard/app/notes/page.tsx +47 -0
- package/dashboard/app/progress/ProgressClient.tsx +29 -8
- package/dashboard/app/progress/page.tsx +2 -0
- package/dashboard/components/DashboardLayout.tsx +22 -1
- package/dashboard/components/MarkdownRenderer.tsx +31 -0
- package/dashboard/next.config.mjs +11 -0
- package/dashboard/whisper-server.py +85 -0
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +23 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/note.d.ts.map +1 -1
- package/dist/commands/note.js +55 -2
- package/dist/commands/note.js.map +1 -1
- package/dist/core/notes.d.ts +68 -0
- package/dist/core/notes.d.ts.map +1 -1
- package/dist/core/notes.js +167 -0
- package/dist/core/notes.js.map +1 -1
- package/dist/core/review.d.ts +14 -0
- package/dist/core/review.d.ts.map +1 -0
- package/dist/core/review.js +80 -0
- package/dist/core/review.js.map +1 -0
- package/dist/core/whisper.d.ts +8 -0
- package/dist/core/whisper.d.ts.map +1 -1
- package/dist/core/whisper.js +55 -0
- package/dist/core/whisper.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/skills/diane.md +65 -36
- package/dist/skills/ingest-aa-report.md +4 -4
- package/package.json +2 -1
- package/dashboard/.next/static/chunks/app/page-1226422e170d8895.js +0 -1
- package/dashboard/.next/static/chunks/app/progress/page-f07ac39d1d27b480.js +0 -1
- package/dashboard/.next/static/css/691f4e9edab008c8.css +0 -3
- /package/dashboard/.next/static/{8MjKGPuSZw4OIpcHY4G0W → ZvCvC4cMbL3GR8kloB9OJ}/_buildManifest.js +0 -0
- /package/dashboard/.next/static/{8MjKGPuSZw4OIpcHY4G0W → ZvCvC4cMbL3GR8kloB9OJ}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { MarkdownRenderer, InlineMarkdown } from '@/components/MarkdownRenderer';
|
|
6
|
+
import type { ContractNote, FnEntry, Question, Lead } from '../../../src/core/notes';
|
|
7
|
+
|
|
8
|
+
interface NotesClientProps {
|
|
9
|
+
general: string;
|
|
10
|
+
contractNotes: ContractNote[];
|
|
11
|
+
contractOptions: string[];
|
|
12
|
+
activeContract: string | null;
|
|
13
|
+
unprocessedCount: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type RecState = 'idle' | 'recording' | 'transcribing';
|
|
17
|
+
const OTHER = '__other__';
|
|
18
|
+
const GENERAL = 'general';
|
|
19
|
+
|
|
20
|
+
function fmt(sec: number): string {
|
|
21
|
+
const m = Math.floor(sec / 60);
|
|
22
|
+
const s = sec % 60;
|
|
23
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function NotesClient({
|
|
27
|
+
general,
|
|
28
|
+
contractNotes,
|
|
29
|
+
contractOptions,
|
|
30
|
+
activeContract,
|
|
31
|
+
unprocessedCount,
|
|
32
|
+
}: NotesClientProps) {
|
|
33
|
+
const router = useRouter();
|
|
34
|
+
|
|
35
|
+
// ── Left rail: General + each contract note ──
|
|
36
|
+
const railItems = useMemo(
|
|
37
|
+
() => [
|
|
38
|
+
{ key: GENERAL, label: 'General' },
|
|
39
|
+
...contractNotes.map((n) => ({ key: n.contract, label: n.contract })),
|
|
40
|
+
],
|
|
41
|
+
[contractNotes],
|
|
42
|
+
);
|
|
43
|
+
const initialKey =
|
|
44
|
+
activeContract && railItems.some((d) => d.key === activeContract) ? activeContract : GENERAL;
|
|
45
|
+
const [selectedKey, setSelectedKey] = useState<string>(initialKey);
|
|
46
|
+
const selectedNote = contractNotes.find((n) => n.contract === selectedKey) ?? null;
|
|
47
|
+
|
|
48
|
+
// ── Recording ──
|
|
49
|
+
const [recState, setRecState] = useState<RecState>('idle');
|
|
50
|
+
const [recordContract, setRecordContract] = useState<string>(
|
|
51
|
+
activeContract ?? contractOptions[0] ?? OTHER,
|
|
52
|
+
);
|
|
53
|
+
const [otherName, setOtherName] = useState('');
|
|
54
|
+
const [elapsed, setElapsed] = useState(0);
|
|
55
|
+
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
const recorderRef = useRef<MediaRecorder | null>(null);
|
|
58
|
+
const chunksRef = useRef<Blob[]>([]);
|
|
59
|
+
const streamRef = useRef<MediaStream | null>(null);
|
|
60
|
+
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
61
|
+
|
|
62
|
+
const effectiveContract = (recordContract === OTHER ? otherName : recordContract).trim();
|
|
63
|
+
|
|
64
|
+
const upload = useCallback(async () => {
|
|
65
|
+
setRecState('transcribing');
|
|
66
|
+
setStatusMsg('Transcribing locally…');
|
|
67
|
+
try {
|
|
68
|
+
const blob = new Blob(chunksRef.current, { type: 'audio/webm' });
|
|
69
|
+
const fd = new FormData();
|
|
70
|
+
fd.append('audio', blob, 'narration.webm');
|
|
71
|
+
fd.append('contract', effectiveContract || 'general');
|
|
72
|
+
const res = await fetch('/api/notes/record', { method: 'POST', body: fd });
|
|
73
|
+
const data = await res.json().catch(() => ({}));
|
|
74
|
+
if (res.status === 422 && data.whisperMissing) {
|
|
75
|
+
setStatusMsg(
|
|
76
|
+
`Audio saved (${data.audioSaved}) but no local Whisper engine is installed. ${data.error}`,
|
|
77
|
+
);
|
|
78
|
+
} else if (!res.ok) {
|
|
79
|
+
setStatusMsg(data.error ?? 'Recording failed');
|
|
80
|
+
} else {
|
|
81
|
+
setStatusMsg(`Session saved for ${data.contract}. Run /diane in Claude Code to process it.`);
|
|
82
|
+
router.refresh();
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
setStatusMsg(err instanceof Error ? err.message : 'Recording failed');
|
|
86
|
+
} finally {
|
|
87
|
+
setRecState('idle');
|
|
88
|
+
}
|
|
89
|
+
}, [effectiveContract, router]);
|
|
90
|
+
|
|
91
|
+
const startRecording = useCallback(async () => {
|
|
92
|
+
setStatusMsg(null);
|
|
93
|
+
try {
|
|
94
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
95
|
+
streamRef.current = stream;
|
|
96
|
+
chunksRef.current = [];
|
|
97
|
+
const mr = new MediaRecorder(stream);
|
|
98
|
+
mr.ondataavailable = (e) => {
|
|
99
|
+
if (e.data.size > 0) chunksRef.current.push(e.data);
|
|
100
|
+
};
|
|
101
|
+
mr.onstop = () => {
|
|
102
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
103
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
104
|
+
void upload();
|
|
105
|
+
};
|
|
106
|
+
recorderRef.current = mr;
|
|
107
|
+
mr.start();
|
|
108
|
+
setElapsed(0);
|
|
109
|
+
timerRef.current = setInterval(() => setElapsed((e) => e + 1), 1000);
|
|
110
|
+
setRecState('recording');
|
|
111
|
+
} catch {
|
|
112
|
+
setStatusMsg('Could not access the microphone. Grant permission and try again.');
|
|
113
|
+
}
|
|
114
|
+
}, [upload]);
|
|
115
|
+
|
|
116
|
+
const stopRecording = useCallback(() => {
|
|
117
|
+
recorderRef.current?.stop();
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const canRecord = recState === 'idle' && effectiveContract.length > 0;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="space-y-sp-5">
|
|
124
|
+
{/* ── Recorder bar ── */}
|
|
125
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-4">
|
|
126
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
127
|
+
<span className="text-heading font-medium text-text-primary">Record narration</span>
|
|
128
|
+
|
|
129
|
+
<select
|
|
130
|
+
value={recordContract}
|
|
131
|
+
onChange={(e) => setRecordContract(e.target.value)}
|
|
132
|
+
disabled={recState !== 'idle'}
|
|
133
|
+
className="rounded-md border border-border-default bg-surface-2 px-3 py-1.5 text-body text-text-primary focus:outline-none focus:ring-2 focus:ring-accent disabled:opacity-50"
|
|
134
|
+
>
|
|
135
|
+
{contractOptions.map((c) => (
|
|
136
|
+
<option key={c} value={c}>
|
|
137
|
+
{c}
|
|
138
|
+
</option>
|
|
139
|
+
))}
|
|
140
|
+
<option value={OTHER}>Other…</option>
|
|
141
|
+
</select>
|
|
142
|
+
|
|
143
|
+
{recordContract === OTHER && (
|
|
144
|
+
<input
|
|
145
|
+
type="text"
|
|
146
|
+
placeholder="Contract name"
|
|
147
|
+
value={otherName}
|
|
148
|
+
onChange={(e) => setOtherName(e.target.value)}
|
|
149
|
+
disabled={recState !== 'idle'}
|
|
150
|
+
className="rounded-md border border-border-default bg-surface-2 px-3 py-1.5 text-body text-text-primary focus:outline-none focus:ring-2 focus:ring-accent disabled:opacity-50"
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{recState === 'recording' ? (
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={stopRecording}
|
|
158
|
+
className="flex items-center gap-2 rounded-md bg-severity-critical px-3 py-1.5 text-body font-medium text-surface-0"
|
|
159
|
+
>
|
|
160
|
+
<span className="h-2.5 w-2.5 rounded-sm bg-surface-0" />
|
|
161
|
+
Stop · {fmt(elapsed)}
|
|
162
|
+
</button>
|
|
163
|
+
) : (
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={startRecording}
|
|
167
|
+
disabled={!canRecord}
|
|
168
|
+
className="flex items-center gap-2 rounded-md bg-accent px-3 py-1.5 text-body font-medium text-surface-0 transition-opacity duration-200 hover:opacity-90 disabled:opacity-40"
|
|
169
|
+
>
|
|
170
|
+
<span className="h-2.5 w-2.5 rounded-full bg-surface-0" />
|
|
171
|
+
{recState === 'transcribing' ? 'Transcribing…' : 'Record'}
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{statusMsg && <p className="mt-3 text-caption text-text-secondary">{statusMsg}</p>}
|
|
177
|
+
{unprocessedCount > 0 && (
|
|
178
|
+
<p className="mt-2 text-caption text-accent">
|
|
179
|
+
{unprocessedCount} narration session{unprocessedCount > 1 ? 's' : ''} waiting — run{' '}
|
|
180
|
+
<code className="font-mono">/diane</code> in Claude Code to process.
|
|
181
|
+
</p>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* ── Notes (rail + structured view) ── */}
|
|
186
|
+
<div className="flex gap-sp-5">
|
|
187
|
+
<nav className="w-44 shrink-0 space-y-0.5">
|
|
188
|
+
{railItems.map((d) => (
|
|
189
|
+
<button
|
|
190
|
+
key={d.key}
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={() => setSelectedKey(d.key)}
|
|
193
|
+
className={`block w-full truncate rounded-md px-3 py-1.5 text-left text-body transition-colors duration-200 ${
|
|
194
|
+
d.key === selectedKey
|
|
195
|
+
? 'bg-accent-subtle font-medium text-accent'
|
|
196
|
+
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'
|
|
197
|
+
}`}
|
|
198
|
+
>
|
|
199
|
+
{d.label}
|
|
200
|
+
</button>
|
|
201
|
+
))}
|
|
202
|
+
</nav>
|
|
203
|
+
|
|
204
|
+
<div className="min-w-0 flex-1">
|
|
205
|
+
{selectedKey === GENERAL ? (
|
|
206
|
+
<GeneralDoc body={general} />
|
|
207
|
+
) : (
|
|
208
|
+
<ContractView
|
|
209
|
+
note={selectedNote}
|
|
210
|
+
contract={selectedKey}
|
|
211
|
+
onMutate={() => router.refresh()}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── General (freeform markdown) ────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
function GeneralDoc({ body }: { body: string }) {
|
|
223
|
+
return (
|
|
224
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-5">
|
|
225
|
+
<div className="mb-3 text-heading font-medium text-text-primary">General</div>
|
|
226
|
+
{body.trim() ? (
|
|
227
|
+
<MarkdownRenderer content={body} />
|
|
228
|
+
) : (
|
|
229
|
+
<p className="text-body text-text-tertiary">
|
|
230
|
+
No protocol-wide notes yet. Record a narration above against{' '}
|
|
231
|
+
<span className="font-medium">Other…</span> as <code className="font-mono">general</code> and
|
|
232
|
+
run <code className="font-mono">/diane</code>.
|
|
233
|
+
</p>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Contract (structured view) ─────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
const SECTIONS = [
|
|
242
|
+
{ id: 'leads', label: 'Leads' },
|
|
243
|
+
{ id: 'description', label: 'Description' },
|
|
244
|
+
{ id: 'questions', label: 'Questions' },
|
|
245
|
+
] as const;
|
|
246
|
+
|
|
247
|
+
function scrollToSection(id: string) {
|
|
248
|
+
document.getElementById(`note-${id}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function ContractView({
|
|
252
|
+
note,
|
|
253
|
+
contract,
|
|
254
|
+
onMutate,
|
|
255
|
+
}: {
|
|
256
|
+
note: ContractNote | null;
|
|
257
|
+
contract: string;
|
|
258
|
+
onMutate: () => void;
|
|
259
|
+
}) {
|
|
260
|
+
const [busy, setBusy] = useState(false);
|
|
261
|
+
|
|
262
|
+
const post = useCallback(
|
|
263
|
+
async (url: string, body: unknown) => {
|
|
264
|
+
setBusy(true);
|
|
265
|
+
try {
|
|
266
|
+
const res = await fetch(url, {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: { 'Content-Type': 'application/json' },
|
|
269
|
+
body: JSON.stringify(body),
|
|
270
|
+
});
|
|
271
|
+
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? 'request failed');
|
|
272
|
+
onMutate();
|
|
273
|
+
} catch {
|
|
274
|
+
/* surface via refresh; keep UI resilient */
|
|
275
|
+
} finally {
|
|
276
|
+
setBusy(false);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
[onMutate],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const isEmpty =
|
|
283
|
+
!note ||
|
|
284
|
+
(note.leads.length === 0 &&
|
|
285
|
+
note.questions.length === 0 &&
|
|
286
|
+
note.description.purpose.length === 0 &&
|
|
287
|
+
note.description.inheritance.length === 0 &&
|
|
288
|
+
note.description.storage.length === 0 &&
|
|
289
|
+
note.description.roles.length === 0 &&
|
|
290
|
+
note.description.functions.length === 0);
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-5">
|
|
294
|
+
{/* Title + mark-done + section nav */}
|
|
295
|
+
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
|
296
|
+
<div className="flex items-center gap-2">
|
|
297
|
+
<span className="text-heading font-medium text-text-primary">{contract}</span>
|
|
298
|
+
{note?.file && (
|
|
299
|
+
<span className="font-mono text-caption text-text-tertiary">{note.file}</span>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
<label className="flex cursor-pointer items-center gap-2 text-caption text-text-secondary">
|
|
303
|
+
<input
|
|
304
|
+
type="checkbox"
|
|
305
|
+
checked={Boolean(note?.marked_done)}
|
|
306
|
+
disabled={busy || !note}
|
|
307
|
+
onChange={(e) => post('/api/notes/done', { contract, done: e.target.checked })}
|
|
308
|
+
className="h-4 w-4 rounded border-border-emphasis bg-surface-1 accent-[var(--accent)]"
|
|
309
|
+
/>
|
|
310
|
+
Mark done reading
|
|
311
|
+
</label>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{isEmpty ? (
|
|
315
|
+
<p className="text-body text-text-tertiary">
|
|
316
|
+
No notes yet — record a narration above and run <code className="font-mono">/diane</code>.
|
|
317
|
+
</p>
|
|
318
|
+
) : (
|
|
319
|
+
<>
|
|
320
|
+
<nav className="mb-5 flex flex-wrap items-center gap-1 border-b border-border-subtle pb-3 text-caption">
|
|
321
|
+
{SECTIONS.map((s) => (
|
|
322
|
+
<button
|
|
323
|
+
key={s.id}
|
|
324
|
+
type="button"
|
|
325
|
+
onClick={() => scrollToSection(s.id)}
|
|
326
|
+
className="rounded-md px-2 py-1 text-text-secondary transition-colors duration-200 hover:bg-surface-2 hover:text-text-primary"
|
|
327
|
+
>
|
|
328
|
+
{s.label}
|
|
329
|
+
</button>
|
|
330
|
+
))}
|
|
331
|
+
</nav>
|
|
332
|
+
|
|
333
|
+
<div className="space-y-6">
|
|
334
|
+
<LeadsSection
|
|
335
|
+
leads={note!.leads}
|
|
336
|
+
contract={contract}
|
|
337
|
+
busy={busy}
|
|
338
|
+
post={post}
|
|
339
|
+
/>
|
|
340
|
+
<DescriptionSection note={note!} />
|
|
341
|
+
<QuestionsSection
|
|
342
|
+
questions={note!.questions}
|
|
343
|
+
contract={contract}
|
|
344
|
+
busy={busy}
|
|
345
|
+
post={post}
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
</>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ─── Leads ──────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
function LeadsSection({
|
|
357
|
+
leads,
|
|
358
|
+
contract,
|
|
359
|
+
busy,
|
|
360
|
+
post,
|
|
361
|
+
}: {
|
|
362
|
+
leads: Lead[];
|
|
363
|
+
contract: string;
|
|
364
|
+
busy: boolean;
|
|
365
|
+
post: (url: string, body: unknown) => Promise<void>;
|
|
366
|
+
}) {
|
|
367
|
+
const open = leads.filter((l) => l.status === 'open');
|
|
368
|
+
const closed = leads.filter((l) => l.status !== 'open');
|
|
369
|
+
const [showClosed, setShowClosed] = useState(false);
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<section id="note-leads">
|
|
373
|
+
<h3 className="mb-2 text-heading font-medium text-text-primary">Leads</h3>
|
|
374
|
+
{open.length === 0 ? (
|
|
375
|
+
<p className="text-body text-text-tertiary">No open leads.</p>
|
|
376
|
+
) : (
|
|
377
|
+
<ul className="space-y-2">
|
|
378
|
+
{open.map((lead) => (
|
|
379
|
+
<li
|
|
380
|
+
key={lead.id}
|
|
381
|
+
className="flex items-start justify-between gap-3 rounded-md border border-border-subtle bg-surface-2 px-3 py-2"
|
|
382
|
+
>
|
|
383
|
+
<span className="min-w-0 flex-1 text-body text-text-primary">
|
|
384
|
+
<InlineMarkdown>{lead.text}</InlineMarkdown>
|
|
385
|
+
</span>
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
disabled={busy}
|
|
389
|
+
onClick={() =>
|
|
390
|
+
post('/api/notes/lead', { contract, id: lead.id, action: 'close', status: 'dismissed' })
|
|
391
|
+
}
|
|
392
|
+
className="shrink-0 rounded-md border border-border-default px-2.5 py-1 text-caption text-text-secondary transition-colors duration-200 hover:bg-surface-3 hover:text-text-primary disabled:opacity-50"
|
|
393
|
+
>
|
|
394
|
+
Close
|
|
395
|
+
</button>
|
|
396
|
+
</li>
|
|
397
|
+
))}
|
|
398
|
+
</ul>
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
{closed.length > 0 && (
|
|
402
|
+
<div className="mt-3">
|
|
403
|
+
<button
|
|
404
|
+
type="button"
|
|
405
|
+
onClick={() => setShowClosed((v) => !v)}
|
|
406
|
+
className="flex items-center gap-1.5 text-caption text-text-tertiary transition-colors duration-200 hover:text-text-secondary"
|
|
407
|
+
>
|
|
408
|
+
<svg
|
|
409
|
+
className={`h-3.5 w-3.5 transition-transform duration-200 ${showClosed ? 'rotate-90' : ''}`}
|
|
410
|
+
fill="none"
|
|
411
|
+
viewBox="0 0 24 24"
|
|
412
|
+
stroke="currentColor"
|
|
413
|
+
strokeWidth={2}
|
|
414
|
+
>
|
|
415
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
|
416
|
+
</svg>
|
|
417
|
+
Closed ({closed.length})
|
|
418
|
+
</button>
|
|
419
|
+
{showClosed && (
|
|
420
|
+
<ul className="mt-2 space-y-1.5">
|
|
421
|
+
{closed.map((lead) => (
|
|
422
|
+
<li
|
|
423
|
+
key={lead.id}
|
|
424
|
+
className="flex items-start justify-between gap-3 rounded-md px-3 py-1.5"
|
|
425
|
+
>
|
|
426
|
+
<span className="min-w-0 flex-1 text-caption text-text-tertiary">
|
|
427
|
+
<span className="line-through"><InlineMarkdown>{lead.text}</InlineMarkdown></span>
|
|
428
|
+
{lead.status === 'logged' && lead.ref && (
|
|
429
|
+
<span className="ml-2 font-mono text-text-secondary">→ {lead.ref}</span>
|
|
430
|
+
)}
|
|
431
|
+
{lead.status === 'dismissed' && (
|
|
432
|
+
<span className="ml-2 text-text-tertiary">dismissed</span>
|
|
433
|
+
)}
|
|
434
|
+
</span>
|
|
435
|
+
<button
|
|
436
|
+
type="button"
|
|
437
|
+
disabled={busy}
|
|
438
|
+
onClick={() => post('/api/notes/lead', { contract, id: lead.id, action: 'reopen' })}
|
|
439
|
+
className="shrink-0 rounded-md px-2 py-0.5 text-caption text-text-secondary transition-colors duration-200 hover:text-accent disabled:opacity-50"
|
|
440
|
+
>
|
|
441
|
+
Reopen
|
|
442
|
+
</button>
|
|
443
|
+
</li>
|
|
444
|
+
))}
|
|
445
|
+
</ul>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</section>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ─── Description ────────────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
function BulletList({ label, items }: { label: string; items: string[] }) {
|
|
456
|
+
if (items.length === 0) return null;
|
|
457
|
+
return (
|
|
458
|
+
<div>
|
|
459
|
+
<h4 className="mb-1 text-body font-medium text-text-secondary">{label}</h4>
|
|
460
|
+
<ul className="list-disc space-y-0.5 pl-5 text-body text-text-primary">
|
|
461
|
+
{items.map((it, i) => (
|
|
462
|
+
<li key={i}>
|
|
463
|
+
<InlineMarkdown>{it}</InlineMarkdown>
|
|
464
|
+
</li>
|
|
465
|
+
))}
|
|
466
|
+
</ul>
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function DescriptionSection({ note }: { note: ContractNote }) {
|
|
472
|
+
const { purpose, inheritance, storage, roles, functions } = note.description;
|
|
473
|
+
const hasAny =
|
|
474
|
+
purpose.length > 0 ||
|
|
475
|
+
inheritance.length > 0 ||
|
|
476
|
+
storage.length > 0 ||
|
|
477
|
+
roles.length > 0 ||
|
|
478
|
+
functions.length > 0;
|
|
479
|
+
|
|
480
|
+
return (
|
|
481
|
+
<section id="note-description">
|
|
482
|
+
<h3 className="mb-3 text-heading font-medium text-text-primary">Description</h3>
|
|
483
|
+
{!hasAny ? (
|
|
484
|
+
<p className="text-body text-text-tertiary">No description recorded yet.</p>
|
|
485
|
+
) : (
|
|
486
|
+
<div className="space-y-4">
|
|
487
|
+
<BulletList label="Purpose" items={purpose} />
|
|
488
|
+
<BulletList label="Inheritance" items={inheritance} />
|
|
489
|
+
<BulletList label="Storage" items={storage} />
|
|
490
|
+
<BulletList label="Roles & modifiers" items={roles} />
|
|
491
|
+
{functions.length > 0 && (
|
|
492
|
+
<div>
|
|
493
|
+
<h4 className="mb-1.5 text-body font-medium text-text-secondary">Functions</h4>
|
|
494
|
+
<div className="space-y-1.5">
|
|
495
|
+
{functions.map((fn) => (
|
|
496
|
+
<FunctionAccordion key={fn.id} fn={fn} />
|
|
497
|
+
))}
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
</div>
|
|
502
|
+
)}
|
|
503
|
+
</section>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function FunctionAccordion({ fn }: { fn: FnEntry }) {
|
|
508
|
+
const [open, setOpen] = useState(false);
|
|
509
|
+
const meta: [string, string | undefined][] = [
|
|
510
|
+
['Purpose', fn.purpose],
|
|
511
|
+
['Access', fn.access],
|
|
512
|
+
['Effects', fn.effects],
|
|
513
|
+
];
|
|
514
|
+
return (
|
|
515
|
+
<div className="rounded-md border border-border-subtle">
|
|
516
|
+
<button
|
|
517
|
+
type="button"
|
|
518
|
+
onClick={() => setOpen((v) => !v)}
|
|
519
|
+
className="flex w-full items-center gap-2 rounded-md bg-surface-2 px-3 py-2 text-left transition-colors duration-200 hover:bg-surface-3"
|
|
520
|
+
>
|
|
521
|
+
<svg
|
|
522
|
+
className={`h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200 ${open ? 'rotate-90' : ''}`}
|
|
523
|
+
fill="none"
|
|
524
|
+
viewBox="0 0 24 24"
|
|
525
|
+
stroke="currentColor"
|
|
526
|
+
strokeWidth={2}
|
|
527
|
+
>
|
|
528
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
|
529
|
+
</svg>
|
|
530
|
+
<span className="min-w-0 truncate font-mono text-body text-text-primary">{fn.sig}</span>
|
|
531
|
+
{fn.warnings.length > 0 && (
|
|
532
|
+
<span className="ml-auto shrink-0 text-caption text-[var(--high)]">
|
|
533
|
+
⚠ {fn.warnings.length}
|
|
534
|
+
</span>
|
|
535
|
+
)}
|
|
536
|
+
</button>
|
|
537
|
+
{open && (
|
|
538
|
+
<div className="space-y-2 px-3 py-2.5">
|
|
539
|
+
{meta
|
|
540
|
+
.filter(([, v]) => v && v.trim())
|
|
541
|
+
.map(([label, v]) => (
|
|
542
|
+
<p key={label} className="text-body text-text-primary">
|
|
543
|
+
<span className="text-text-secondary">{label}: </span>
|
|
544
|
+
<InlineMarkdown>{v as string}</InlineMarkdown>
|
|
545
|
+
</p>
|
|
546
|
+
))}
|
|
547
|
+
{fn.notes.length > 0 && (
|
|
548
|
+
<ul className="list-disc space-y-0.5 pl-5 text-body text-text-primary">
|
|
549
|
+
{fn.notes.map((n, i) => (
|
|
550
|
+
<li key={i}>
|
|
551
|
+
<InlineMarkdown>{n}</InlineMarkdown>
|
|
552
|
+
</li>
|
|
553
|
+
))}
|
|
554
|
+
</ul>
|
|
555
|
+
)}
|
|
556
|
+
{fn.warnings.length > 0 && (
|
|
557
|
+
<ul className="space-y-1">
|
|
558
|
+
{fn.warnings.map((w, i) => (
|
|
559
|
+
<li
|
|
560
|
+
key={i}
|
|
561
|
+
className="flex items-start gap-1.5 rounded-md bg-[var(--high)]/10 px-2 py-1 text-body text-[var(--high)]"
|
|
562
|
+
>
|
|
563
|
+
<span aria-hidden>⚠</span>
|
|
564
|
+
<span className="min-w-0 flex-1">
|
|
565
|
+
<InlineMarkdown>{w}</InlineMarkdown>
|
|
566
|
+
</span>
|
|
567
|
+
</li>
|
|
568
|
+
))}
|
|
569
|
+
</ul>
|
|
570
|
+
)}
|
|
571
|
+
</div>
|
|
572
|
+
)}
|
|
573
|
+
</div>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ─── Questions ──────────────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
function QuestionsSection({
|
|
580
|
+
questions,
|
|
581
|
+
contract,
|
|
582
|
+
busy,
|
|
583
|
+
post,
|
|
584
|
+
}: {
|
|
585
|
+
questions: Question[];
|
|
586
|
+
contract: string;
|
|
587
|
+
busy: boolean;
|
|
588
|
+
post: (url: string, body: unknown) => Promise<void>;
|
|
589
|
+
}) {
|
|
590
|
+
return (
|
|
591
|
+
<section id="note-questions">
|
|
592
|
+
<h3 className="mb-2 text-heading font-medium text-text-primary">Questions</h3>
|
|
593
|
+
{questions.length === 0 ? (
|
|
594
|
+
<p className="text-body text-text-tertiary">No questions recorded yet.</p>
|
|
595
|
+
) : (
|
|
596
|
+
<div className="space-y-1.5">
|
|
597
|
+
{questions.map((q) => (
|
|
598
|
+
<QuestionAccordion
|
|
599
|
+
key={q.id}
|
|
600
|
+
question={q}
|
|
601
|
+
contract={contract}
|
|
602
|
+
busy={busy}
|
|
603
|
+
post={post}
|
|
604
|
+
/>
|
|
605
|
+
))}
|
|
606
|
+
</div>
|
|
607
|
+
)}
|
|
608
|
+
</section>
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function QuestionAccordion({
|
|
613
|
+
question,
|
|
614
|
+
contract,
|
|
615
|
+
busy,
|
|
616
|
+
post,
|
|
617
|
+
}: {
|
|
618
|
+
question: Question;
|
|
619
|
+
contract: string;
|
|
620
|
+
busy: boolean;
|
|
621
|
+
post: (url: string, body: unknown) => Promise<void>;
|
|
622
|
+
}) {
|
|
623
|
+
const answered = Boolean(question.answer && question.answer.trim());
|
|
624
|
+
const [open, setOpen] = useState(false);
|
|
625
|
+
const [editing, setEditing] = useState(false);
|
|
626
|
+
const [draft, setDraft] = useState(question.answer ?? '');
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<div className="rounded-md border border-border-subtle">
|
|
630
|
+
<button
|
|
631
|
+
type="button"
|
|
632
|
+
onClick={() => setOpen((v) => !v)}
|
|
633
|
+
className="flex w-full items-center gap-2 rounded-md bg-surface-2 px-3 py-2 text-left transition-colors duration-200 hover:bg-surface-3"
|
|
634
|
+
>
|
|
635
|
+
<svg
|
|
636
|
+
className={`h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200 ${open ? 'rotate-90' : ''}`}
|
|
637
|
+
fill="none"
|
|
638
|
+
viewBox="0 0 24 24"
|
|
639
|
+
stroke="currentColor"
|
|
640
|
+
strokeWidth={2}
|
|
641
|
+
>
|
|
642
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
|
643
|
+
</svg>
|
|
644
|
+
<span className={`min-w-0 flex-1 text-body ${answered ? 'text-text-primary' : 'text-text-tertiary'}`}>
|
|
645
|
+
<InlineMarkdown>{question.q}</InlineMarkdown>
|
|
646
|
+
</span>
|
|
647
|
+
{!answered && <span className="shrink-0 text-caption text-text-tertiary">open</span>}
|
|
648
|
+
</button>
|
|
649
|
+
{open && (
|
|
650
|
+
<div className="space-y-2 px-3 py-2.5">
|
|
651
|
+
{editing ? (
|
|
652
|
+
<div className="space-y-2">
|
|
653
|
+
<textarea
|
|
654
|
+
value={draft}
|
|
655
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
656
|
+
rows={3}
|
|
657
|
+
className="w-full resize-y rounded-md border border-border-default bg-surface-0 p-2 text-body text-text-primary focus:outline-none focus:ring-2 focus:ring-accent"
|
|
658
|
+
/>
|
|
659
|
+
<div className="flex gap-2">
|
|
660
|
+
<button
|
|
661
|
+
type="button"
|
|
662
|
+
onClick={() => {
|
|
663
|
+
setEditing(false);
|
|
664
|
+
setDraft(question.answer ?? '');
|
|
665
|
+
}}
|
|
666
|
+
className="rounded-md px-2.5 py-1 text-caption text-text-secondary transition-colors duration-200 hover:text-text-primary"
|
|
667
|
+
>
|
|
668
|
+
Cancel
|
|
669
|
+
</button>
|
|
670
|
+
<button
|
|
671
|
+
type="button"
|
|
672
|
+
disabled={busy || !draft.trim()}
|
|
673
|
+
onClick={async () => {
|
|
674
|
+
await post('/api/notes/question', { contract, id: question.id, answer: draft.trim() });
|
|
675
|
+
setEditing(false);
|
|
676
|
+
}}
|
|
677
|
+
className="rounded-md bg-accent px-2.5 py-1 text-caption font-medium text-surface-0 transition-opacity duration-200 hover:opacity-90 disabled:opacity-50"
|
|
678
|
+
>
|
|
679
|
+
Save answer
|
|
680
|
+
</button>
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
683
|
+
) : (
|
|
684
|
+
<div className="flex items-start justify-between gap-3">
|
|
685
|
+
<p className={`min-w-0 flex-1 text-body ${answered ? 'text-text-primary' : 'text-text-tertiary'}`}>
|
|
686
|
+
{answered ? <InlineMarkdown>{question.answer as string}</InlineMarkdown> : '— open'}
|
|
687
|
+
</p>
|
|
688
|
+
<button
|
|
689
|
+
type="button"
|
|
690
|
+
onClick={() => setEditing(true)}
|
|
691
|
+
className="shrink-0 rounded-md border border-border-default px-2.5 py-1 text-caption text-text-secondary transition-colors duration-200 hover:bg-surface-3 hover:text-text-primary"
|
|
692
|
+
>
|
|
693
|
+
{answered ? 'Edit' : 'Answer'}
|
|
694
|
+
</button>
|
|
695
|
+
</div>
|
|
696
|
+
)}
|
|
697
|
+
</div>
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
700
|
+
);
|
|
701
|
+
}
|