feat(advisor): Evidence Workspace — structured panes, markdown, sources as knowledge units
Rebuilds the Compliance Advisor floating widget from a plain chat into an Evidence
Workspace: pinned last question, markdown-rendered answer (clean prose), and separate
panes for Sources (hierarchical Knowledge Units), Figures (C8, conditional) and
Footnotes (C-FN), plus a stats bar (Quellen/Regelwerke/Diagramme/Fußnoten). Scrollable
turn history; stays a floating icon on every SDK page.
Architecture (user direction): the frontend renders ONLY structured evidence and NEVER
parses the answer text. The proxy now returns a JSON AdvisorEvidenceMeta line followed
by the streamed markdown answer; advisor-rag exposes structured results; an adapter maps
RAG/compiler output to the frontend envelope. Figures/footnotes wire in once the
RAG-ingestion contract lands (requested on the board) — figures pane is conditional.
- lib/sdk/advisor/{evidence,evidence-adapter}.ts (+ adapter test, 7 cases)
- components/sdk/advisor/* panes + in-house safe Markdown (no new dep, no dangerouslySetInnerHTML) + test
- useAdvisorStream (meta-line parse + streamed answer) + useAdvisorEmail (escaped)
- proxy: evidence-meta-v1 envelope + clean-prose prompt (no inline citations)
- tsc clean, 11 vitest pass, check-loc 0. ESLint not installed in this node_modules -> CI lints on push.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type { AdvisorTurn } from './useAdvisorStream'
|
||||
import { StickyQuestion } from './StickyQuestion'
|
||||
import { TurnView } from './TurnView'
|
||||
import { AdvisorEmptyState } from './EmptyState'
|
||||
|
||||
/**
|
||||
* The Evidence Workspace body: a pinned "last question" + a scrollable history of turns, each
|
||||
* showing the answer alongside its sources / figures / footnotes. Scroll up to revisit a past
|
||||
* answer with its full evidence.
|
||||
*/
|
||||
export function EvidenceWorkspace({
|
||||
turns,
|
||||
exampleQuestions,
|
||||
onExample,
|
||||
}: {
|
||||
turns: AdvisorTurn[]
|
||||
exampleQuestions: string[]
|
||||
onExample: (q: string) => void
|
||||
}) {
|
||||
const endRef = useRef<HTMLDivElement>(null)
|
||||
const latest = turns[turns.length - 1]
|
||||
|
||||
// Scroll to the newest turn when a question is added (not on every streamed token,
|
||||
// so the user can scroll up to review history while the answer streams).
|
||||
useEffect(() => {
|
||||
endRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [turns.length])
|
||||
|
||||
if (turns.length === 0) {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto bg-gray-50">
|
||||
<AdvisorEmptyState exampleQuestions={exampleQuestions} onExampleClick={onExample} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto bg-gray-50">
|
||||
{latest && <StickyQuestion question={latest.question} />}
|
||||
<div className="space-y-4 p-4">
|
||||
{turns.map((t, i) => (
|
||||
<TurnView key={t.id} turn={t} showQuestion={i !== turns.length - 1} />
|
||||
))}
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user