feat(advisor): Case Workspace v2 — Evidence grouping, human names, 3-column, summary

Reworks the advisor toward a Compliance Case Workspace (review feedback):
- Rename user-facing "Quellen" -> "Evidence".
- Evidence grouped by document/regulation family (count + expandable) — no more
  unsorted DSK/DSK/DPF/... jumble.
- Human-readable regulation names via a display registry (DSK Sdm B51 -> "DSK
  Standard-Datenschutzmodell (SDM)" / Kapitel B51); generic, bridges G2.
- Evidence summary "Antwort basiert auf" with meaningful counts; Regelwerke = distinct
  FAMILIES (fixes the inflated count). NO fabricated trust score (needs a defined basis).
- Expanded mode = 3-column workspace (question+summary | answer | evidence, independent
  scroll) + history switcher; narrow mode stays stacked.
- Prompt: push aggressive markdown structure (## per aspect, numbered phases).

Deferred/coordinated on board: C8 diagrams (RAG contract), answer<->evidence coupling
[1] (needs LLM citation anchors — phase 2), G1 retrieval relevance + G2 metadata (RAG).
tsc clean, 17 vitest, check-loc 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-07-01 10:38:06 +02:00
parent 3884038b06
commit 591cae5ebc
12 changed files with 362 additions and 125 deletions
@@ -3,26 +3,49 @@
import { useState } from 'react'
import { ChevronDown, ChevronRight, ExternalLink } from 'lucide-react'
import type { KnowledgeUnit } from '@/lib/sdk/advisor/evidence'
import { resolveRegulation } from '@/lib/sdk/advisor/regulation-display'
/**
* A source rendered as a hierarchical Knowledge Unit (Regelwerk → Section → Paragraph → Footnote),
* not a text-list line. [öffnen] resolves to the original source when available; the optional
* snippet lets the user peek the cited text.
* A single evidence unit. Standalone: friendly regulation name + hierarchy. Compact (inside a
* document group): chapter/section only (the group already names the regulation). [öffnen] opens
* the original source; the optional snippet lets the user peek the cited text.
*/
export function KnowledgeUnitCard({ unit }: { unit: KnowledgeUnit }) {
export function KnowledgeUnitCard({ unit, compact }: { unit: KnowledgeUnit; compact?: boolean }) {
const [open, setOpen] = useState(false)
const crumbs = [unit.section, unit.subsection, unit.paragraph, unit.footnoteRef].filter(Boolean)
const d = resolveRegulation(unit.regulation)
const crumbs = [unit.section, unit.subsection, unit.paragraph, unit.footnoteRef].filter(
(x): x is string => Boolean(x),
)
const href = unit.open?.originalUrl
const canOpen = href && /^https?:\/\//i.test(href)
const canOpen = !!href && /^https?:\/\//i.test(href)
let header: string
let sub: string[]
if (!compact) {
header = d.familyLabel
sub = crumbs
} else if (d.chapter) {
header = `Kapitel ${d.chapter}`
sub = crumbs
} else {
header = crumbs[0] || unit.label || d.familyLabel
sub = crumbs.slice(1)
}
return (
<div className="rounded-lg border border-gray-200 bg-white p-2.5">
<div
className={
compact
? 'rounded-md border border-gray-100 bg-gray-50 p-2'
: 'rounded-lg border border-gray-200 bg-white p-2.5'
}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="truncate text-xs font-semibold text-gray-900">{unit.regulation.short}</div>
{crumbs.length > 0 ? (
<div className="truncate text-xs font-semibold text-gray-900">{header}</div>
{sub.length > 0 ? (
<div className="mt-0.5 flex flex-wrap items-center gap-x-1 text-[11px] text-gray-500">
{crumbs.map((c, i) => (
{sub.map((c, i) => (
<span key={i} className="flex items-center gap-1">
{i > 0 && <span className="text-gray-300"></span>}
{c}
@@ -30,8 +53,9 @@ export function KnowledgeUnitCard({ unit }: { unit: KnowledgeUnit }) {
))}
</div>
) : (
!compact &&
unit.label &&
unit.label !== unit.regulation.short && (
unit.label !== header && (
<div className="mt-0.5 text-[11px] text-gray-500">{unit.label}</div>
)
)}