CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

Refactor: Extract login page and implement feature panels architecture #3948

Merged⚡ AI-generatedXSccantynz wants to mergeclaude/product-readiness-assessment-6YNCEmainopened Apr 22, 2026
3 changed files+48−12
ModifiedCLAUDE.md+9−6View fileUnifiedSplit
106106- [ ] Native notification support
107107- [ ] Background audio recording
108108
109### Wave 6 — Moat Widening (PLANNED)
110- [ ] **Speaker Diarization** — Pyannote or Whisper-based, labels "Counsel:", "Witness:", timestamps (nobody in legal-AI has this dialled in)
111- [ ] **Conflict-of-Interest Guardian** — extract names from dictation, cross-reference firm client roster, real-time flag (requires DATABASE_URL)
112- [ ] **Precedent Match** — semantic search over firm's past dictations via pg_vector embeddings
113- [ ] **E-Signature Handoff** — DocuSign/Adobe Sign push after enhancement (API integration)
114- [ ] **Matter-Linked Billing** — tag dictation to matter/client, auto-export billable time to time-tracking software
109### Wave 6 — Moat Widening (COMPLETE — 20 April 2026)
110- [x] **Speaker Diarization** — heuristic turn detection with Q/A alternation, COUNSEL/WITNESS/THE COURT prefix parsing, context-aware labelling (deposition/meeting/interview/client-call), inline editable speaker relabel (`lib/diarization.ts`, `app/api/diarize`, `app/components/features/DiarizationPanel.tsx`)
111- [x] **Conflict-of-Interest Guardian** — name + organisation extraction (titled names, case captions, corporate-suffix heuristics), in-memory client roster with current/former/adverse/prospect classifications, fuzzy-match conflict scan with severity grading (`lib/name-extract.ts`, `lib/client-roster.ts`, `app/api/clients`, `app/api/conflicts`, `app/components/features/ConflictPanel.tsx`)
112- [x] **Precedent Match** — TF-IDF cosine-similarity search over the user's localStorage history, 240-char contextual snippets, highlighted matched terms, score bars (`lib/precedent.ts`, `app/api/precedents`, `app/components/features/PrecedentPanel.tsx`)
113- [x] **E-Signature Handoff** — DocuSign eSignature REST v2.1 + Adobe Sign REST v6 integrations (JWT Bearer Grant for DocuSign, OAuth for AdobeSign), env-gated stubs when keys absent, provider picker + recipient list + subject/message UI (`lib/esign.ts`, `app/api/esign/send`, `app/api/esign/status`, `app/components/features/EsignPanel.tsx`)
114- [x] **Matter-Linked Billing** — matter CRUD + time-entry logging with auto-fill from dictation, five export formats (generic CSV, Actionstep, Clio, MyCase, PracticePanther) with correct vendor column headers, duration tracking, date-range filters (`lib/matter-store.ts`, `app/api/matters`, `app/api/time-entries`, `app/api/time-entries/export`, `app/components/features/MatterPanel.tsx`)
115- [x] **Dictation Share Links** — crypto-random 24-byte tokens, PBKDF2 password protection (optional), configurable expiry (1h/1d/1w/never), revocable, view-count tracking, public `/share/[token]` viewer page with print-friendly CSS and no auth (`lib/share-store.ts`, `app/api/share`, `app/api/share/[id]`, `app/api/share/view/[token]`, `app/share/[token]/page.tsx`, `app/components/features/SharePanel.tsx`)
116- [x] All Wave 6 panels wired into main dictation UI Intelligence section
117- [x] Middleware matcher extended to cover new protected routes + public share exceptions
115118
116119### Phase 4 — Advanced Features — PARTIALLY DONE
117120- [x] Real-time streaming transcription (Live mode)
Modifiedapp/app/page.tsx+34−1View fileUnifiedSplit
99 RedactionPanel,
1010 CompliancePanel,
1111 MultiDocPanel,
12 DiarizationPanel,
13 PrecedentPanel,
14 ConflictPanel,
15 EsignPanel,
16 MatterPanel,
17 SharePanel,
1218} from '@/app/components/features';
1319
1420// === Types ===
13001306 </div>
13011307 </div>
13021308
1303 {/* Intelligence panels (Wave 5) */}
1309 {/* Intelligence panels (Wave 5 + Wave 6) */}
13041310 {(rawText || enhancedText) && (
13051311 <div className="shrink-0 space-y-3">
13061312 <CitationPanel text={enhancedText || rawText} />
13071313 <CompliancePanel text={enhancedText || rawText} mode={mode} />
1314 <ConflictPanel text={enhancedText || rawText} />
13081315 <RedactionPanel
13091316 text={enhancedText || rawText}
13101317 onRedact={(redacted) => {
13121319 else setRawText(redacted);
13131320 }}
13141321 />
1322 <DiarizationPanel
1323 text={enhancedText || rawText}
1324 onRelabel={(labeled) => {
1325 if (enhancedText) setEnhancedText(labeled);
1326 else setRawText(labeled);
1327 }}
1328 />
1329 <PrecedentPanel text={enhancedText || rawText} />
13151330 {rawText && (
13161331 <MultiDocPanel rawText={rawText} customInstructions={customInstructions} />
13171332 )}
1333 {enhancedText && (
1334 <>
1335 <MatterPanel
1336 dictationId={history[0]?.id}
1337 durationSeconds={duration}
1338 rawText={rawText}
1339 />
1340 <SharePanel
1341 title={`${MODES.find(m => m.value === mode)?.label || 'Dictation'} · ${new Date().toLocaleDateString()}`}
1342 content={enhancedText}
1343 mode={mode}
1344 />
1345 <EsignPanel
1346 documentContent={enhancedText}
1347 documentName={`${MODES.find(m => m.value === mode)?.label || 'Document'}.docx`}
1348 />
1349 </>
1350 )}
13181351 </div>
13191352 )}
13201353
Modifiedapp/components/features/SharePanel.tsx+5−5View fileUnifiedSplit
202202 loadShares();
203203 }
204204
205 async function handleCreate(e: React.FormEvent) {
205 async function handleCreate(e: { preventDefault(): void }) {
206206 e.preventDefault();
207207 if (!content?.trim()) {
208208 setCreateError('No content to share. Add dictation text first.');
242242 try {
243243 const res = await fetch(`/api/share/${id}`, { method: 'DELETE' });
244244 if (!res.ok) throw new Error('Revoke failed');
245 setShares((prev) => prev.filter((s) => s.id !== id));
245 setShares((prev: ShareRecord[]) => prev.filter((s: ShareRecord) => s.id !== id));
246246 if (created?.id === id) setCreated(null);
247247 } catch {
248248 // silently — user can retry
251251 }
252252 }
253253
254 const activeShares = shares.filter((s) => !s.revoked && !isExpired(s.expiresAt));
254 const activeShares = shares.filter((s: ShareRecord) => !s.revoked && !isExpired(s.expiresAt));
255255 const hasContent = !!(content?.trim());
256256
257257 return (
263263 {/* Collapsible header */}
264264 <button
265265 type="button"
266 onClick={() => setCollapsed((v) => !v)}
266 onClick={() => setCollapsed((v: boolean) => !v)}
267267 className="w-full flex items-center justify-between gap-3 px-4 py-3 text-left hover:bg-ink-800/40 transition-colors"
268268 aria-expanded={!collapsed}
269269 >
482482 />
483483 <button
484484 type="button"
485 onClick={() => setShowPassword((v) => !v)}
485 onClick={() => setShowPassword((v: boolean) => !v)}
486486 className="absolute right-2.5 top-1/2 -translate-y-1/2 text-ink-500 hover:text-ink-300 transition-colors"
487487 aria-label={showPassword ? 'Hide password' : 'Show password'}
488488 tabIndex={-1}
489489
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts