129 lines
3.8 KiB
TypeScript
129 lines
3.8 KiB
TypeScript
const PERCEPTION_API_BASE = '/api/v1';
|
|
|
|
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<PerceptionStats> {
|
|
const res = await fetch(`${PERCEPTION_API_BASE}/perception/stats`);
|
|
if (!res.ok) throw new Error(`stats failed: ${res.status}`);
|
|
return res.json() as Promise<PerceptionStats>;
|
|
}
|
|
|
|
export async function listEvents(params?: {
|
|
source?: string;
|
|
impact_level?: string;
|
|
limit?: number;
|
|
}): Promise<EventListResponse> {
|
|
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()}`);
|
|
if (!res.ok) throw new Error(`list events failed: ${res.status}`);
|
|
return res.json() as Promise<EventListResponse>;
|
|
}
|
|
|
|
export async function analyzeEvent(
|
|
eventId: string,
|
|
onMessage: (msg: AnalysisSSEMessage) => void,
|
|
onComplete?: () => void,
|
|
signal?: AbortSignal,
|
|
): Promise<void> {
|
|
try {
|
|
const res = await fetch(`${PERCEPTION_API_BASE}/perception/events/${eventId}/analyze`, {
|
|
method: 'POST',
|
|
headers: { Accept: 'text/event-stream' },
|
|
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?.();
|
|
}
|
|
}
|