feat(agent): SSE — progressive Themen-Tabs (Phase 2)
Der Compliance-Check streamt jetzt progressive Events; der Impressum-Tab
erscheint, sobald das Thema fertig ist, statt am Ende alles auf einmal.
Additiv — das Polling fürs finale Ergebnis bleibt.
- backend: _sse.py (Queue/emit/event_generator) + Endpoint
/compliance-check/{id}/stream; _update emittiert progress,
run_agent_outputs emittiert topic (laeuft jetzt frueh nach Phase B),
Orchestrator emittiert complete/error.
- frontend: SSE-Proxy-Route + EventSource in ComplianceCheckTab merged
topic-Events in agent_outputs -> Tab erscheint progressiv.
- Tests: backend 5 passed (SSE + agent_outputs); tsc 0 neue Fehler,
vitest 2 passed, check-loc 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import React, { useState, useCallback, useRef } from 'react'
|
||||
import { ComplianceResultTabs } from './ComplianceResultTabs'
|
||||
import { DocumentRow } from './DocumentRow'
|
||||
import { PreScanWizard, useScanContext, isContextComplete } from './PreScanWizard'
|
||||
@@ -35,6 +35,9 @@ export function ComplianceCheckTab() {
|
||||
if (typeof window === 'undefined') return []
|
||||
try { return JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY) || '[]') } catch { return [] }
|
||||
})
|
||||
// SSE: progressive Themen-Tabs (additiv zum Polling).
|
||||
const esRef = useRef<EventSource | null>(null)
|
||||
React.useEffect(() => () => { try { esRef.current?.close() } catch { /* noop */ } }, [])
|
||||
|
||||
// Persist URLs and texts (not loading/error state)
|
||||
React.useEffect(() => {
|
||||
@@ -117,6 +120,38 @@ export function ComplianceCheckTab() {
|
||||
reader.readAsText(file)
|
||||
}, [updateDoc])
|
||||
|
||||
// SSE: füllt agent_outputs progressiv, sobald ein Thema fertig ist.
|
||||
// Das Polling unten liefert weiterhin das finale Gesamtergebnis.
|
||||
const openTopicStream = useCallback((checkId: string) => {
|
||||
try { esRef.current?.close() } catch { /* noop */ }
|
||||
const partial: any = { results: [], agent_outputs: {} }
|
||||
const es = new EventSource(
|
||||
`/api/sdk/v1/agent/compliance-check/${checkId}/stream`,
|
||||
)
|
||||
esRef.current = es
|
||||
es.onmessage = (ev) => {
|
||||
try {
|
||||
const data = JSON.parse(ev.data)
|
||||
if (data.type === 'topic' && data.topic && data.output) {
|
||||
partial.agent_outputs = {
|
||||
...partial.agent_outputs, [data.topic]: data.output,
|
||||
}
|
||||
setResults((prev: any) =>
|
||||
(prev && Array.isArray(prev.results) && prev.results.length > 0)
|
||||
? prev // finales Ergebnis schon da → behalten
|
||||
: { ...partial },
|
||||
)
|
||||
} else if (data.type === 'progress') {
|
||||
if (data.msg) setProgress(data.msg)
|
||||
if (typeof data.pct === 'number') setProgressPct(data.pct)
|
||||
} else if (data.type === 'complete' || data.type === 'stream_close') {
|
||||
try { es.close() } catch { /* noop */ }
|
||||
}
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
es.onerror = () => { try { es.close() } catch { /* noop */ } }
|
||||
}, [])
|
||||
|
||||
const filledCount = Object.values(docs).filter(d => d.url.trim() || d.text.trim()).length
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -157,6 +192,7 @@ export function ComplianceCheckTab() {
|
||||
if (!check_id) throw new Error('Keine Check-ID erhalten')
|
||||
setActiveCheckId(check_id)
|
||||
localStorage.setItem(STORAGE_KEY_CHECK_ID, check_id)
|
||||
openTopicStream(check_id)
|
||||
|
||||
// Poll for results (max 25 min = 500 polls x 3s)
|
||||
let attempts = 0
|
||||
@@ -201,6 +237,7 @@ export function ComplianceCheckTab() {
|
||||
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
||||
setProgress('')
|
||||
setProgressPct(0)
|
||||
try { esRef.current?.close() } catch { /* noop */ }
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user