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>
This commit is contained in:
2025-12-03 16:26:17 +08:00
parent 1ed1dab78f
commit 534fdbbcc4
25 changed files with 3114 additions and 27 deletions

View File

@@ -0,0 +1,238 @@
import { useState, useCallback } from 'react';
import { expertTransformCategoryStream } from '../services/api';
import type {
ExpertTransformationInput,
ExpertTransformationProgress,
ExpertTransformationCategoryResult,
ExpertTransformationDAGResult,
ExpertProfile,
CategoryDefinition,
} from '../types';
interface UseExpertTransformationOptions {
model?: string;
temperature?: number;
}
export function useExpertTransformation(options: UseExpertTransformationOptions = {}) {
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState<ExpertTransformationProgress>({
step: 'idle',
currentCategory: '',
processedCategories: [],
message: '',
});
const [results, setResults] = useState<ExpertTransformationDAGResult | null>(null);
const [error, setError] = useState<string | null>(null);
// Global expert team - generated once and shared across all categories
const [experts, setExperts] = useState<ExpertProfile[] | null>(null);
const transformCategory = useCallback(
async (
query: string,
category: CategoryDefinition,
attributes: string[],
expertConfig: {
expert_count: number;
keywords_per_expert: number;
custom_experts?: string[];
}
): Promise<{
result: ExpertTransformationCategoryResult | null;
experts: ExpertProfile[];
}> => {
return new Promise((resolve) => {
let categoryExperts: ExpertProfile[] = [];
setProgress((prev) => ({
...prev,
step: 'expert',
currentCategory: category.name,
message: `組建專家團隊...`,
}));
expertTransformCategoryStream(
{
query,
category: category.name,
attributes,
expert_count: expertConfig.expert_count,
keywords_per_expert: expertConfig.keywords_per_expert,
custom_experts: expertConfig.custom_experts,
model: options.model,
temperature: options.temperature,
},
{
onExpertStart: () => {
setProgress((prev) => ({
...prev,
step: 'expert',
message: `正在組建專家團隊...`,
}));
},
onExpertComplete: (expertsData) => {
categoryExperts = expertsData;
setExperts(expertsData);
setProgress((prev) => ({
...prev,
experts: expertsData,
message: `專家團隊組建完成(${expertsData.length}位專家)`,
}));
},
onKeywordStart: () => {
setProgress((prev) => ({
...prev,
step: 'keyword',
message: `專家團隊為「${category.name}」的屬性生成關鍵字...`,
}));
},
onKeywordProgress: (data) => {
setProgress((prev) => ({
...prev,
currentAttribute: data.attribute,
message: `為「${data.attribute}」生成了 ${data.count} 個關鍵字`,
}));
},
onKeywordComplete: (totalKeywords) => {
setProgress((prev) => ({
...prev,
message: `共生成了 ${totalKeywords} 個專家關鍵字`,
}));
},
onDescriptionStart: () => {
setProgress((prev) => ({
...prev,
step: 'description',
message: `為「${category.name}」的專家關鍵字生成創新描述...`,
}));
},
onDescriptionComplete: (count) => {
setProgress((prev) => ({
...prev,
message: `生成了 ${count} 個創新描述`,
}));
},
onDone: (data) => {
setProgress((prev) => ({
...prev,
step: 'done',
processedCategories: [...prev.processedCategories, category.name],
message: `${category.name}」處理完成`,
}));
resolve({
result: data.result,
experts: data.experts,
});
},
onError: (err) => {
setProgress((prev) => ({
...prev,
step: 'error',
error: err,
message: `處理「${category.name}」時發生錯誤`,
}));
resolve({
result: null,
experts: categoryExperts,
});
},
}
).catch((err) => {
setProgress((prev) => ({
...prev,
step: 'error',
error: err.message,
message: `處理「${category.name}」時發生錯誤`,
}));
resolve({
result: null,
experts: categoryExperts,
});
});
});
},
[options.model, options.temperature]
);
const transformAll = useCallback(
async (input: ExpertTransformationInput) => {
setLoading(true);
setError(null);
setResults(null);
setExperts(null);
setProgress({
step: 'idle',
currentCategory: '',
processedCategories: [],
message: '開始處理...',
});
const categoryResults: ExpertTransformationCategoryResult[] = [];
let globalExperts: ExpertProfile[] = [];
// Process each category sequentially
for (const category of input.categories) {
const attributes = input.attributesByCategory[category.name] || [];
if (attributes.length === 0) continue;
const { result, experts: categoryExperts } = await transformCategory(
input.query,
category,
attributes,
input.expertConfig
);
// Store global experts from first category
if (globalExperts.length === 0 && categoryExperts.length > 0) {
globalExperts = categoryExperts;
}
if (result) {
categoryResults.push(result);
}
}
// Build final result
const finalResult: ExpertTransformationDAGResult = {
query: input.query,
experts: globalExperts,
results: categoryResults,
};
setResults(finalResult);
setLoading(false);
setProgress((prev) => ({
...prev,
step: 'done',
message: '所有類別處理完成',
}));
return finalResult;
},
[transformCategory]
);
const clearResults = useCallback(() => {
setResults(null);
setError(null);
setExperts(null);
setProgress({
step: 'idle',
currentCategory: '',
processedCategories: [],
message: '',
});
}, []);
return {
loading,
progress,
results,
error,
experts,
transformCategory,
transformAll,
clearResults,
};
}