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:
238
frontend/src/hooks/useExpertTransformation.ts
Normal file
238
frontend/src/hooks/useExpertTransformation.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user