Files
breakpilot-compliance/admin-compliance/lib/sdk/advisor/copy.ts
T
Benjamin Admin e8ea179228 feat(advisor): topic threads, per-question delete/copy, fullscreen
Adds case management to the Compliance Advisor widget.

- topic threads: cases group into threads; the left menu shows each
  thread's first question as the Thema with expandable follow-ups.
  Send = follow-up to the active thread (carries the thread's prior Q&A
  as history for contextual answers); "+" starts a new topic.
- delete: a trash action per question (menu + stacked view).
- copy: single Q&A (question + answer + evidence + footnotes) or a whole
  thread, as Markdown to the clipboard (pure formatters in copy.ts).
- fullscreen: compact -> panel -> fullscreen view.
- route.ts consumes an optional bounded `history` so follow-ups are
  contextual for both the widget and the workspace consumer.

Tests: copy formatter unit tests + Playwright specs (threads/new-topic,
delete, fullscreen, copy affordance). No deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-01 18:51:17 +02:00

49 lines
1.9 KiB
TypeScript

// Pure formatters for "copy to clipboard": a single case (Q + answer + evidence) or a whole thread.
// No DOM/clipboard here (that is the caller's side effect) so this stays testable.
import type { AdvisorResponse, EvidenceUnit } from './contract'
import { resolveRegulation } from './regulation-display'
export interface CopyableCase {
question: string
response: AdvisorResponse | null
}
function evidenceLine(e: EvidenceUnit): string {
const { familyLabel } = resolveRegulation({ code: e.regulation_code || e.document, short: e.document })
const loc = [e.section, e.paragraph].filter(Boolean).join(' ')
const ref = [familyLabel, loc].filter(Boolean).join(', ')
return e.url ? `- ${ref} (${e.url})` : `- ${ref}`
}
/** One case as portable Markdown: question, answer (or general definition), and its evidence. */
export function formatCaseForCopy(c: CopyableCase): string {
const parts: string[] = [`### Frage`, c.question.trim()]
const r = c.response
const answer = (r?.answer || r?.general_answer || '').trim()
parts.push('', '### Antwort', answer || '(keine Antwort)')
if (r && r.evidence.length > 0) {
parts.push('', '### Belege', ...r.evidence.map(evidenceLine))
}
if (r && r.footnotes.length > 0) {
parts.push(
'',
'### Fußnoten',
...r.footnotes.map((f, i) => {
const head = f.ref || `Fußnote ${i + 1}`
const src = [f.document, f.section].filter(Boolean).join(' / ')
return `- ${[head, src].filter(Boolean).join(' — ')}${f.text ? `: ${f.text}` : ''}`
}),
)
}
return parts.join('\n')
}
/** A whole topic thread: a title heading followed by every case, separated by rules. */
export function formatThreadForCopy(title: string, cases: CopyableCase[]): string {
const header = `# ${title.trim() || 'Compliance-Advisor-Verlauf'}`
const body = cases.map(formatCaseForCopy).join('\n\n---\n\n')
return `${header}\n\n${body}`
}