- Add curated occupations seed files (210 entries in zh/en) with specific domains - Add DBpedia occupations data (2164 entries) for external source option - Refactor expert_source_service to read from local JSON files - Improve keyword generation prompts to leverage expert domain context - Add architecture analysis documentation (ARCHITECTURE_ANALYSIS.md) - Fix expert source selection bug (proper handling of empty custom_experts) - Update frontend to support curated/dbpedia/wikidata expert sources Key changes: - backend/app/data/: Local occupation data files - backend/app/services/expert_source_service.py: Simplified local file reading - backend/app/prompts/expert_transformation_prompt.py: Better domain-aware prompts - Removed expert_cache.py (no longer needed with local files) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
242 lines
7.0 KiB
TypeScript
242 lines
7.0 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { expertTransformCategoryStream } from '../services/api';
|
|
import type {
|
|
ExpertTransformationInput,
|
|
ExpertTransformationProgress,
|
|
ExpertTransformationCategoryResult,
|
|
ExpertTransformationDAGResult,
|
|
ExpertProfile,
|
|
CategoryDefinition,
|
|
ExpertSource,
|
|
} from '../types';
|
|
|
|
interface UseExpertTransformationOptions {
|
|
model?: string;
|
|
temperature?: number;
|
|
expertSource?: ExpertSource;
|
|
}
|
|
|
|
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,
|
|
expert_source: options.expertSource,
|
|
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, options.expertSource]
|
|
);
|
|
|
|
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,
|
|
};
|
|
}
|