@@ -213,6 +247,20 @@ export function InputPanel({
+ {/* Show categories used */}
+ {progress.categoriesUsed && progress.categoriesUsed.length > 0 && (
+
+
Categories:
+
+ {progress.categoriesUsed.map((cat, i) => (
+
+ {cat.name}
+
+ ))}
+
+
+ )}
+
{progress.completedChains.length > 0 && (
Completed chains:
@@ -220,7 +268,7 @@ export function InputPanel({
{progress.completedChains.map((chain, i) => (
- {chain.material} → {chain.function} → {chain.usage} → {chain.user}
+ {formatChain(chain)}
))}
@@ -232,6 +280,22 @@ export function InputPanel({
};
const collapseItems = [
+ {
+ key: 'categories',
+ label: 'Category Settings',
+ children: (
+
+ ),
+ },
{
key: 'llm',
label: 'LLM Parameters',
diff --git a/frontend/src/components/MindmapD3.tsx b/frontend/src/components/MindmapD3.tsx
index 06838f4..b374b8e 100644
--- a/frontend/src/components/MindmapD3.tsx
+++ b/frontend/src/components/MindmapD3.tsx
@@ -117,8 +117,19 @@ export const MindmapD3 = forwardRef
(
d._children = undefined;
});
- // Category labels for header
- const categoryLabels = ['', '材料', '功能', '用途', '使用族群'];
+ // Dynamically extract category labels from the tree based on depth
+ // Each depth level corresponds to a category
+ const categoryByDepth: Record = {};
+ root.descendants().forEach((d: TreeNode) => {
+ if (d.depth > 0 && d.data.category && !categoryByDepth[d.depth]) {
+ categoryByDepth[d.depth] = d.data.category;
+ }
+ });
+ const maxDepthWithCategory = Math.max(...Object.keys(categoryByDepth).map(Number), 0);
+ const categoryLabels = [''];
+ for (let i = 1; i <= maxDepthWithCategory; i++) {
+ categoryLabels.push(categoryByDepth[i] || '');
+ }
const headerHeight = 40;
function update(source: TreeNode) {
@@ -143,12 +154,27 @@ export const MindmapD3 = forwardRef(
// Draw category headers with background
g.selectAll('.category-header-group').remove();
const maxDepth = Math.max(...descendants.map(d => d.depth));
- const categoryColors: Record = {
- '材料': isDark ? '#854eca' : '#722ed1',
- '功能': isDark ? '#13a8a8' : '#13c2c2',
- '用途': isDark ? '#d87a16' : '#fa8c16',
- '使用族群': isDark ? '#49aa19' : '#52c41a',
- };
+
+ // Dynamic color palette for categories
+ const colorPalette = [
+ { dark: '#854eca', light: '#722ed1' }, // purple
+ { dark: '#13a8a8', light: '#13c2c2' }, // cyan
+ { dark: '#d87a16', light: '#fa8c16' }, // orange
+ { dark: '#49aa19', light: '#52c41a' }, // green
+ { dark: '#1677ff', light: '#1890ff' }, // blue
+ { dark: '#eb2f96', light: '#f759ab' }, // magenta
+ { dark: '#faad14', light: '#ffc53d' }, // gold
+ { dark: '#a0d911', light: '#bae637' }, // lime
+ ];
+
+ // Generate colors dynamically based on category position
+ const categoryColors: Record = {};
+ categoryLabels.forEach((label, index) => {
+ if (label && index > 0) {
+ const colorIndex = (index - 1) % colorPalette.length;
+ categoryColors[label] = isDark ? colorPalette[colorIndex].dark : colorPalette[colorIndex].light;
+ }
+ });
for (let depth = 1; depth <= Math.min(maxDepth, categoryLabels.length - 1); depth++) {
const label = categoryLabels[depth];
diff --git a/frontend/src/hooks/useAttribute.ts b/frontend/src/hooks/useAttribute.ts
index 2987852..4a36178 100644
--- a/frontend/src/hooks/useAttribute.ts
+++ b/frontend/src/hooks/useAttribute.ts
@@ -3,9 +3,9 @@ import type {
AttributeNode,
HistoryItem,
StreamProgress,
- StreamAnalyzeResponse,
- CausalChain
+ StreamAnalyzeResponse
} from '../types';
+import { CategoryMode } from '../types';
import { analyzeAttributesStream } from '../services/api';
export function useAttribute() {
@@ -24,7 +24,10 @@ export function useAttribute() {
query: string,
model?: string,
temperature?: number,
- chainCount: number = 5
+ chainCount: number = 5,
+ categoryMode: CategoryMode = CategoryMode.DYNAMIC_AUTO,
+ customCategories?: string[],
+ suggestedCategoryCount: number = 3
) => {
// 重置狀態
setProgress({
@@ -39,8 +42,40 @@ export function useAttribute() {
try {
await analyzeAttributesStream(
- { query, chain_count: chainCount, model, temperature },
{
+ query,
+ chain_count: chainCount,
+ model,
+ temperature,
+ category_mode: categoryMode,
+ custom_categories: customCategories,
+ suggested_category_count: suggestedCategoryCount
+ },
+ {
+ onStep0Start: () => {
+ setProgress(prev => ({
+ ...prev,
+ step: 'step0',
+ message: '正在分析類別...',
+ }));
+ },
+
+ onStep0Complete: (result) => {
+ setProgress(prev => ({
+ ...prev,
+ step0Result: result,
+ message: '類別分析完成',
+ }));
+ },
+
+ onCategoriesResolved: (categories) => {
+ setProgress(prev => ({
+ ...prev,
+ categoriesUsed: categories,
+ message: `使用 ${categories.length} 個類別`,
+ }));
+ },
+
onStep1Start: () => {
setProgress(prev => ({
...prev,
@@ -148,7 +183,7 @@ export function useAttribute() {
});
}, []);
- const isLoading = progress.step === 'step1' || progress.step === 'chains';
+ const isLoading = progress.step === 'step0' || progress.step === 'step1' || progress.step === 'chains';
return {
loading: isLoading,
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index e4cc845..634cf72 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -3,17 +3,24 @@ import type {
StreamAnalyzeRequest,
StreamAnalyzeResponse,
Step1Result,
- CausalChain
+ CausalChain,
+ Step0Result,
+ CategoryDefinition,
+ DynamicStep1Result,
+ DynamicCausalChain
} 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) => void;
+ onStep1Complete?: (result: Step1Result | DynamicStep1Result) => void;
onChainStart?: (index: number, total: number) => void;
- onChainComplete?: (index: number, chain: CausalChain) => void;
+ onChainComplete?: (index: number, chain: CausalChain | DynamicCausalChain) => void;
onChainError?: (index: number, error: string) => void;
onDone?: (response: StreamAnalyzeResponse) => void;
onError?: (error: string) => void;
@@ -65,6 +72,15 @@ export async function analyzeAttributesStream(
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;
diff --git a/frontend/src/styles/mindmap.css b/frontend/src/styles/mindmap.css
index 7053c6a..da36ab1 100644
--- a/frontend/src/styles/mindmap.css
+++ b/frontend/src/styles/mindmap.css
@@ -126,6 +126,38 @@
fill: #fff;
}
+.mindmap-light .node-rect.depth-5 {
+ fill: #1890ff;
+ stroke: #096dd9;
+}
+.mindmap-light .node-text.depth-5 {
+ fill: #fff;
+}
+
+.mindmap-light .node-rect.depth-6 {
+ fill: #f759ab;
+ stroke: #eb2f96;
+}
+.mindmap-light .node-text.depth-6 {
+ fill: #fff;
+}
+
+.mindmap-light .node-rect.depth-7 {
+ fill: #ffc53d;
+ stroke: #faad14;
+}
+.mindmap-light .node-text.depth-7 {
+ fill: #fff;
+}
+
+.mindmap-light .node-rect.depth-8 {
+ fill: #bae637;
+ stroke: #a0d911;
+}
+.mindmap-light .node-text.depth-8 {
+ fill: #fff;
+}
+
.mindmap-light .link {
stroke: #bfbfbf;
}
@@ -221,6 +253,38 @@
fill: #fff;
}
+.mindmap-dark .node-rect.depth-5 {
+ fill: #1677ff;
+ stroke: #4096ff;
+}
+.mindmap-dark .node-text.depth-5 {
+ fill: #fff;
+}
+
+.mindmap-dark .node-rect.depth-6 {
+ fill: #eb2f96;
+ stroke: #f759ab;
+}
+.mindmap-dark .node-text.depth-6 {
+ fill: #fff;
+}
+
+.mindmap-dark .node-rect.depth-7 {
+ fill: #faad14;
+ stroke: #ffc53d;
+}
+.mindmap-dark .node-text.depth-7 {
+ fill: #fff;
+}
+
+.mindmap-dark .node-rect.depth-8 {
+ fill: #a0d911;
+ stroke: #bae637;
+}
+.mindmap-dark .node-text.depth-8 {
+ fill: #fff;
+}
+
.mindmap-dark .link {
stroke: #434343;
}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index aec7f5b..3013754 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -52,26 +52,64 @@ export interface CausalChain {
user: string;
}
+// ===== Dynamic category system types =====
+
+export interface CategoryDefinition {
+ name: string;
+ description?: string;
+ is_fixed: boolean;
+ order: number;
+}
+
+export interface Step0Result {
+ categories: CategoryDefinition[];
+}
+
+export interface DynamicStep1Result {
+ attributes: Record;
+}
+
+export interface DynamicCausalChain {
+ chain: Record;
+}
+
+export const CategoryMode = {
+ FIXED_ONLY: 'fixed_only',
+ FIXED_PLUS_CUSTOM: 'fixed_plus_custom',
+ CUSTOM_ONLY: 'custom_only',
+ DYNAMIC_AUTO: 'dynamic_auto',
+} as const;
+
+export type CategoryMode = typeof CategoryMode[keyof typeof CategoryMode];
+
export interface StreamAnalyzeRequest {
query: string;
model?: string;
temperature?: number;
chain_count: number;
+ // Dynamic category support
+ category_mode?: CategoryMode;
+ custom_categories?: string[];
+ suggested_category_count?: number;
}
export interface StreamProgress {
- step: 'idle' | 'step1' | 'chains' | 'done' | 'error';
- step1Result?: Step1Result;
+ step: 'idle' | 'step0' | 'step1' | 'chains' | 'done' | 'error';
+ step0Result?: Step0Result;
+ categoriesUsed?: CategoryDefinition[];
+ step1Result?: Step1Result | DynamicStep1Result;
currentChainIndex: number;
totalChains: number;
- completedChains: CausalChain[];
+ completedChains: (CausalChain | DynamicCausalChain)[];
message: string;
error?: string;
}
export interface StreamAnalyzeResponse {
query: string;
- step1_result: Step1Result;
- causal_chains: CausalChain[];
+ step0_result?: Step0Result;
+ categories_used: CategoryDefinition[];
+ step1_result: Step1Result | DynamicStep1Result;
+ causal_chains: (CausalChain | DynamicCausalChain)[];
attributes: AttributeNode;
}