const PERCEPTION_API_BASE = '/api/v1'; const TOKEN_KEY = 'auth_token'; function authHeader(): Record { const t = localStorage.getItem(TOKEN_KEY); return t ? { Authorization: `Bearer ${t}` } : {}; } export type ImpactLevel = 'high' | 'medium' | 'low'; export type EventStatus = 'enacted' | 'draft' | 'consultation'; export type EventSource = 'MIIT' | 'UN-ECE' | 'ISO' | '国标委' | 'EUR-Lex' | 'IATF'; export interface RegulationEvent { id: string; source: EventSource; source_label: string; standard_code: string; title: string; summary: string; impact_level: ImpactLevel; published_at: string; effective_at: string | null; category: string; tags: string[]; source_url: string; status: EventStatus; } export interface PerceptionStats { total: number; high_impact: number; medium_impact: number; low_impact: number; recent_90d: number; } export interface EventListResponse { events: RegulationEvent[]; total: number; } export interface AffectedDoc { doc_id: string; doc_name: string; score: number; snippet: string; clause: string; } export interface AnalysisSSEMessage { type: 'sources' | 'content' | 'done' | 'error'; docs?: AffectedDoc[]; text?: string; } export async function getPerceptionStats(): Promise { const res = await fetch(`${PERCEPTION_API_BASE}/perception/stats`, { headers: authHeader() }); if (!res.ok) throw new Error(`stats failed: ${res.status}`); return res.json() as Promise; } export async function listEvents(params?: { source?: string; impact_level?: string; limit?: number; }): Promise { const query = new URLSearchParams(); if (params?.source) query.set('source', params.source); if (params?.impact_level) query.set('impact_level', params.impact_level); if (params?.limit) query.set('limit', String(params.limit)); const res = await fetch(`${PERCEPTION_API_BASE}/perception/events?${query.toString()}`, { headers: authHeader() }); if (!res.ok) throw new Error(`list events failed: ${res.status}`); return res.json() as Promise; } export async function analyzeEvent( eventId: string, onMessage: (msg: AnalysisSSEMessage) => void, onComplete?: () => void, signal?: AbortSignal, ): Promise { try { const res = await fetch(`${PERCEPTION_API_BASE}/perception/events/${eventId}/analyze`, { method: 'POST', headers: { Accept: 'text/event-stream', ...authHeader() }, signal, }); if (!res.ok || !res.body) throw new Error(`analyze failed: ${res.status}`); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split('\n\n'); buffer = parts.pop() ?? ''; for (const block of parts) { if (!block.trim()) continue; let eventName = 'message'; const dataLines: string[] = []; for (const line of block.split('\n')) { if (line.startsWith('event:')) eventName = line.slice(6).trim(); else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim()); } const payload = dataLines.join('\n'); if (!payload) continue; if (eventName === 'sources') { try { const docs = JSON.parse(payload) as AffectedDoc[]; onMessage({ type: 'sources', docs }); } catch { /* ignore */ } } else if (eventName === 'content') { onMessage({ type: 'content', text: payload }); } else if (eventName === 'done') { onMessage({ type: 'done' }); } else if (eventName === 'error') { onMessage({ type: 'error', text: payload }); } } } if (buffer.trim()) { // flush remaining } onComplete?.(); } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return; onMessage({ type: 'error', text: err instanceof Error ? err.message : String(err) }); onComplete?.(); } }