Files
novelty-seeking/frontend/src/services/api.ts
gbanyan 534fdbbcc4 feat: Add Expert Transformation Agent with multi-expert perspective system
- Backend: Add expert transformation router with 3-step SSE pipeline
  - Step 0: Generate diverse expert team (random domains)
  - Step 1: Each expert generates keywords for attributes
  - Step 2: Batch generate descriptions for expert keywords
- Backend: Add simplified prompts for reliable JSON output
- Frontend: Add TransformationPanel with React Flow visualization
- Frontend: Add TransformationInputPanel for expert configuration
  - Expert count (2-8), keywords per expert (1-3)
  - Custom expert domains support
- Frontend: Add expert keyword nodes with expert badges
- Frontend: Improve description card layout (wider cards, more spacing)
- Frontend: Add fallback for missing descriptions with visual indicators

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 16:26:17 +08:00

302 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
ModelListResponse,
StreamAnalyzeRequest,
Step1Result,
Step0Result,
CategoryDefinition,
DynamicStep1Result,
DAGStreamAnalyzeResponse,
TransformationRequest,
TransformationCategoryResult,
ExpertTransformationRequest,
ExpertTransformationCategoryResult,
ExpertProfile
} from '../types';
// 自動使用當前瀏覽器的 hostname支援遠端存取
const API_BASE_URL = `http://${window.location.hostname}:8000/api`;
export interface SSECallbacks {
onStep0Start?: () => void;
onStep0Complete?: (result: Step0Result) => void;
onCategoriesResolved?: (categories: CategoryDefinition[]) => void;
onStep1Start?: () => void;
onStep1Complete?: (result: Step1Result | DynamicStep1Result) => void;
onRelationshipsStart?: () => void;
onRelationshipsComplete?: (count: number) => void;
onDone?: (response: DAGStreamAnalyzeResponse) => void;
onError?: (error: string) => void;
}
export async function analyzeAttributesStream(
request: StreamAnalyzeRequest,
callbacks: SSECallbacks
): Promise<void> {
const response = await fetch(`${API_BASE_URL}/analyze`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 解析 SSE 事件
const lines = buffer.split('\n\n');
buffer = lines.pop() || ''; // 保留未完成的部分
for (const chunk of lines) {
if (!chunk.trim()) continue;
const eventMatch = chunk.match(/event: (\w+)/);
const dataMatch = chunk.match(/data: (.+)/s);
if (eventMatch && dataMatch) {
const eventType = eventMatch[1];
try {
const eventData = JSON.parse(dataMatch[1]);
switch (eventType) {
case 'step0_start':
callbacks.onStep0Start?.();
break;
case 'step0_complete':
callbacks.onStep0Complete?.(eventData.result);
break;
case 'categories_resolved':
callbacks.onCategoriesResolved?.(eventData.categories);
break;
case 'step1_start':
callbacks.onStep1Start?.();
break;
case 'step1_complete':
callbacks.onStep1Complete?.(eventData.result);
break;
case 'relationships_start':
callbacks.onRelationshipsStart?.();
break;
case 'relationships_complete':
callbacks.onRelationshipsComplete?.(eventData.count);
break;
case 'done':
callbacks.onDone?.(eventData);
break;
case 'error':
callbacks.onError?.(eventData.error);
break;
}
} catch (e) {
console.error('Failed to parse SSE event:', e, chunk);
}
}
}
}
}
export async function getModels(): Promise<ModelListResponse> {
const response = await fetch(`${API_BASE_URL}/models`);
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
return response.json();
}
// ===== Transformation Agent API =====
export interface TransformationSSECallbacks {
onKeywordStart?: () => void;
onKeywordComplete?: (keywords: string[]) => void;
onDescriptionStart?: () => void;
onDescriptionComplete?: (count: number) => void;
onDone?: (result: TransformationCategoryResult) => void;
onError?: (error: string) => void;
}
export async function transformCategoryStream(
request: TransformationRequest,
callbacks: TransformationSSECallbacks
): Promise<void> {
const response = await fetch(`${API_BASE_URL}/transformation/category`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 解析 SSE 事件
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const chunk of lines) {
if (!chunk.trim()) continue;
const eventMatch = chunk.match(/event: (\w+)/);
const dataMatch = chunk.match(/data: (.+)/s);
if (eventMatch && dataMatch) {
const eventType = eventMatch[1];
try {
const eventData = JSON.parse(dataMatch[1]);
switch (eventType) {
case 'keyword_start':
callbacks.onKeywordStart?.();
break;
case 'keyword_complete':
callbacks.onKeywordComplete?.(eventData.keywords);
break;
case 'description_start':
callbacks.onDescriptionStart?.();
break;
case 'description_complete':
callbacks.onDescriptionComplete?.(eventData.count);
break;
case 'done':
callbacks.onDone?.(eventData.result);
break;
case 'error':
callbacks.onError?.(eventData.error);
break;
}
} catch (e) {
console.error('Failed to parse SSE event:', e, chunk);
}
}
}
}
}
// ===== Expert Transformation Agent API =====
export interface ExpertTransformationSSECallbacks {
onExpertStart?: () => void;
onExpertComplete?: (experts: ExpertProfile[]) => void;
onKeywordStart?: () => void;
onKeywordProgress?: (data: { attribute: string; count: number }) => void;
onKeywordComplete?: (totalKeywords: number) => void;
onDescriptionStart?: () => void;
onDescriptionComplete?: (count: number) => void;
onDone?: (data: { result: ExpertTransformationCategoryResult; experts: ExpertProfile[] }) => void;
onError?: (error: string) => void;
}
export async function expertTransformCategoryStream(
request: ExpertTransformationRequest,
callbacks: ExpertTransformationSSECallbacks
): Promise<void> {
const response = await fetch(`${API_BASE_URL}/expert-transformation/category`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 解析 SSE 事件
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const chunk of lines) {
if (!chunk.trim()) continue;
const eventMatch = chunk.match(/event: (\w+)/);
const dataMatch = chunk.match(/data: (.+)/s);
if (eventMatch && dataMatch) {
const eventType = eventMatch[1];
try {
const eventData = JSON.parse(dataMatch[1]);
switch (eventType) {
case 'expert_start':
callbacks.onExpertStart?.();
break;
case 'expert_complete':
callbacks.onExpertComplete?.(eventData.experts);
break;
case 'keyword_start':
callbacks.onKeywordStart?.();
break;
case 'keyword_progress':
callbacks.onKeywordProgress?.(eventData);
break;
case 'keyword_complete':
callbacks.onKeywordComplete?.(eventData.total_keywords);
break;
case 'description_start':
callbacks.onDescriptionStart?.();
break;
case 'description_complete':
callbacks.onDescriptionComplete?.(eventData.count);
break;
case 'done':
callbacks.onDone?.(eventData);
break;
case 'error':
callbacks.onError?.(eventData.error);
break;
}
} catch (e) {
console.error('Failed to parse SSE event:', e, chunk);
}
}
}
}
}