Files
breakpilot-compliance/admin-compliance/components/sdk/advisor/CaseView.tsx
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

108 lines
4.0 KiB
TypeScript

'use client'
import { Check, Copy, Trash2 } from 'lucide-react'
import type { AdvisorResponse } from '@/lib/sdk/advisor/contract'
import { formatCaseForCopy } from '@/lib/sdk/advisor/copy'
import type { AdvisorCase } from './useAdvisorCase'
import { ClarifyView } from './ClarifyView'
import { EvidenceSummary } from './EvidenceSummary'
import { EvidencePane } from './EvidencePane'
import { VisualEvidencePane } from './VisualEvidencePane'
import { FootnotesPane } from './FootnotesPane'
import { Markdown } from './Markdown'
import { useCitationHighlight } from './useCitationHighlight'
import { useClipboard } from './useClipboard'
export function LoadingDots() {
return (
<div className="flex space-x-1 px-1 py-2" aria-label="Antwort wird erstellt">
<span className="h-2 w-2 animate-bounce rounded-full bg-gray-400" />
<span className="h-2 w-2 animate-bounce rounded-full bg-gray-400" style={{ animationDelay: '0.1s' }} />
<span className="h-2 w-2 animate-bounce rounded-full bg-gray-400" style={{ animationDelay: '0.2s' }} />
</div>
)
}
export function ErrorBox({ msg }: { msg?: string }) {
return (
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{msg || 'Verbindung fehlgeschlagen'}
</div>
)
}
/** Answer mode body (stacked): summary + answer (with [n] coupling) + evidence/visual/footnotes. */
export function AnswerBody({ response }: { response: AdvisorResponse }) {
const { highlightedId, cite } = useCitationHighlight(response.citations)
return (
<div className="space-y-3">
<EvidenceSummary response={response} />
<div className="rounded-lg border border-gray-200 bg-white px-3 py-2">
<Markdown content={response.answer || ''} citations={cite} />
</div>
<EvidencePane evidence={response.evidence} highlightedId={highlightedId} />
<VisualEvidencePane items={response.visual_evidence} />
<FootnotesPane footnotes={response.footnotes} />
</div>
)
}
/** One case rendered stacked (narrow mode). Clarify -> L1 + chips; answer -> full evidence body. */
export function CaseView({
c,
onSelectContext,
busy,
showQuestion,
onRemove,
}: {
c: AdvisorCase
onSelectContext: (ctx: string) => void
busy: boolean
showQuestion?: boolean
onRemove?: () => void
}) {
const r = c.response
const { copiedKey, copy } = useClipboard()
return (
<div className="group space-y-2 border-b border-gray-100 pb-4 last:border-0">
<div className="flex items-start justify-between gap-2">
{showQuestion ? (
<div className="min-w-0 flex-1 text-xs text-gray-500">
<span className="font-medium text-gray-400">Frage:</span> {c.question}
</div>
) : (
<span className="flex-1" />
)}
<div className="flex shrink-0 items-center gap-0.5 text-gray-400 opacity-0 transition-opacity group-hover:opacity-100">
<button
type="button"
title="Frage & Antwort kopieren"
aria-label="Frage & Antwort kopieren"
onClick={() => copy(c.id, formatCaseForCopy(c))}
className="rounded p-0.5 hover:bg-gray-100 hover:text-gray-700"
>
{copiedKey === c.id ? <Check className="h-3.5 w-3.5 text-green-600" /> : <Copy className="h-3.5 w-3.5" />}
</button>
{onRemove && (
<button
type="button"
title="Frage löschen"
aria-label="Frage löschen"
onClick={onRemove}
className="rounded p-0.5 hover:bg-gray-100 hover:text-gray-700"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
)}
</div>
</div>
{c.status === 'loading' && <LoadingDots />}
{c.status === 'error' && <ErrorBox msg={c.error} />}
{r && r.mode === 'clarify' && (
<ClarifyView response={r} onSelectContext={onSelectContext} busy={busy} />
)}
{r && r.mode === 'answer' && <AnswerBody response={r} />}
</div>
)
}