feat: Add dynamic category system for attribute analysis
Backend: - Add CategoryMode enum with 4 modes (fixed_only, fixed_plus_custom, custom_only, dynamic_auto) - Add Step 0 for LLM category analysis before attribute generation - Implement dynamic prompts for Step 1/2 that work with N categories - Add execute_step0(), resolve_final_categories(), assemble_dynamic_attribute_tree() - Update SSE events to include step0_start, step0_complete, categories_resolved Frontend: - Add CategorySelector component with mode selection, custom category input, and category count slider - Update types with CategoryDefinition, Step0Result, DynamicStep1Result, DynamicCausalChain - Update api.ts with new SSE event handlers - Update useAttribute hook with category parameters - Integrate CategorySelector into InputPanel - Fix mindmap to dynamically extract and display N categories (was hardcoded to 4) - Add CSS styles for depth 5-8 to support more category levels 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { InputPanel } from './components/InputPanel';
|
||||
import { MindmapPanel } from './components/MindmapPanel';
|
||||
import { useAttribute } from './hooks/useAttribute';
|
||||
import type { MindmapD3Ref } from './components/MindmapD3';
|
||||
import type { CategoryMode } from './types';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
const { Title } = Typography;
|
||||
@@ -27,9 +28,12 @@ function App() {
|
||||
query: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
chainCount?: number
|
||||
chainCount?: number,
|
||||
categoryMode?: CategoryMode,
|
||||
customCategories?: string[],
|
||||
suggestedCategoryCount?: number
|
||||
) => {
|
||||
await analyze(query, model, temperature, chainCount);
|
||||
await analyze(query, model, temperature, chainCount, categoryMode, customCategories, suggestedCategoryCount);
|
||||
};
|
||||
|
||||
const handleExpandAll = useCallback(() => {
|
||||
|
||||
157
frontend/src/components/CategorySelector.tsx
Normal file
157
frontend/src/components/CategorySelector.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useState } from 'react';
|
||||
import { Radio, Space, Input, Button, Tag, Tooltip, Slider, Typography } from 'antd';
|
||||
import { InfoCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import type { CategoryDefinition, CategoryMode, Step0Result } from '../types';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface CategorySelectorProps {
|
||||
mode: CategoryMode;
|
||||
onModeChange: (mode: CategoryMode) => void;
|
||||
customCategories: string[];
|
||||
onCustomCategoriesChange: (cats: string[]) => void;
|
||||
suggestedCount: number;
|
||||
onSuggestedCountChange: (count: number) => void;
|
||||
step0Result?: Step0Result;
|
||||
onStep0Edit?: (cats: CategoryDefinition[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function CategorySelector({
|
||||
mode,
|
||||
onModeChange,
|
||||
customCategories,
|
||||
onCustomCategoriesChange,
|
||||
suggestedCount,
|
||||
onSuggestedCountChange,
|
||||
step0Result,
|
||||
onStep0Edit,
|
||||
disabled
|
||||
}: CategorySelectorProps) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Radio.Group
|
||||
value={mode}
|
||||
onChange={(e) => onModeChange(e.target.value as CategoryMode)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Space direction="vertical">
|
||||
<Radio value="fixed_only">
|
||||
Fixed (材料、功能、用途、使用族群)
|
||||
</Radio>
|
||||
<Radio value="fixed_plus_custom">
|
||||
Fixed + Custom
|
||||
</Radio>
|
||||
<Radio value="custom_only">
|
||||
Custom Only (LLM suggests)
|
||||
</Radio>
|
||||
<Radio value="dynamic_auto">
|
||||
Dynamic (LLM suggests, editable)
|
||||
</Radio>
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
|
||||
{/* 動態模式:類別數量調整 */}
|
||||
{(mode === 'custom_only' || mode === 'dynamic_auto') && (
|
||||
<div>
|
||||
<Text>Suggested Category Count: {suggestedCount}</Text>
|
||||
<Slider
|
||||
min={2}
|
||||
max={8}
|
||||
step={1}
|
||||
value={suggestedCount}
|
||||
onChange={onSuggestedCountChange}
|
||||
marks={{ 2: '2', 3: '3', 5: '5', 8: '8' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 固定+自定義模式 */}
|
||||
{mode === 'fixed_plus_custom' && (
|
||||
<div>
|
||||
<Text type="secondary">Add custom categories:</Text>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 8 }}>
|
||||
<Input
|
||||
placeholder="Category name"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onPressEnter={() => {
|
||||
if (inputValue.trim()) {
|
||||
onCustomCategoriesChange([...customCategories, inputValue.trim()]);
|
||||
setInputValue('');
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
if (inputValue.trim()) {
|
||||
onCustomCategoriesChange([...customCategories, inputValue.trim()]);
|
||||
setInputValue('');
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
|
||||
{customCategories.length > 0 && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{customCategories.map((cat, i) => (
|
||||
<Tag
|
||||
key={i}
|
||||
closable
|
||||
onClose={() => {
|
||||
onCustomCategoriesChange(customCategories.filter((_, idx) => idx !== i));
|
||||
}}
|
||||
>
|
||||
{cat}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 0 結果顯示 */}
|
||||
{step0Result && (mode === 'custom_only' || mode === 'dynamic_auto') && (
|
||||
<div style={{ marginTop: 8, padding: 12, background: 'rgba(0,0,0,0.04)', borderRadius: 4 }}>
|
||||
<Text strong>LLM Suggested:</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{step0Result.categories.map((cat, i) => (
|
||||
<Tag
|
||||
key={i}
|
||||
color="blue"
|
||||
closable={mode === 'dynamic_auto'}
|
||||
onClose={mode === 'dynamic_auto' ? () => {
|
||||
if (onStep0Edit) {
|
||||
onStep0Edit(step0Result.categories.filter((_, idx) => idx !== i));
|
||||
}
|
||||
} : undefined}
|
||||
>
|
||||
{cat.name}
|
||||
{cat.description && (
|
||||
<Tooltip title={cat.description}>
|
||||
<InfoCircleOutlined style={{ marginLeft: 4 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{mode === 'dynamic_auto' && (
|
||||
<Text type="secondary" style={{ fontSize: 12, marginTop: 8, display: 'block' }}>
|
||||
You can remove tags or proceed
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -22,8 +22,9 @@ import {
|
||||
LoadingOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { HistoryItem, AttributeNode, StreamProgress } from '../types';
|
||||
import type { HistoryItem, AttributeNode, StreamProgress, CategoryMode, DynamicCausalChain, CausalChain } from '../types';
|
||||
import { getModels } from '../services/api';
|
||||
import { CategorySelector } from './CategorySelector';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Text } = Typography;
|
||||
@@ -38,7 +39,15 @@ interface InputPanelProps {
|
||||
progress: StreamProgress;
|
||||
history: HistoryItem[];
|
||||
currentResult: AttributeNode | null;
|
||||
onAnalyze: (query: string, model?: string, temperature?: number, chainCount?: number) => Promise<void>;
|
||||
onAnalyze: (
|
||||
query: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
chainCount?: number,
|
||||
categoryMode?: CategoryMode,
|
||||
customCategories?: string[],
|
||||
suggestedCategoryCount?: number
|
||||
) => Promise<void>;
|
||||
onLoadHistory: (item: HistoryItem) => void;
|
||||
onExpandAll?: () => void;
|
||||
onCollapseAll?: () => void;
|
||||
@@ -64,6 +73,10 @@ export function InputPanel({
|
||||
const [loadingModels, setLoadingModels] = useState(false);
|
||||
const [temperature, setTemperature] = useState(0.7);
|
||||
const [chainCount, setChainCount] = useState(5);
|
||||
// Category settings
|
||||
const [categoryMode, setCategoryMode] = useState<CategoryMode>('dynamic_auto' as CategoryMode);
|
||||
const [customCategories, setCustomCategories] = useState<string[]>([]);
|
||||
const [suggestedCategoryCount, setSuggestedCategoryCount] = useState(3);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchModels() {
|
||||
@@ -92,7 +105,15 @@ export function InputPanel({
|
||||
}
|
||||
|
||||
try {
|
||||
await onAnalyze(query.trim(), selectedModel, temperature, chainCount);
|
||||
await onAnalyze(
|
||||
query.trim(),
|
||||
selectedModel,
|
||||
temperature,
|
||||
chainCount,
|
||||
categoryMode,
|
||||
customCategories.length > 0 ? customCategories : undefined,
|
||||
suggestedCategoryCount
|
||||
);
|
||||
setQuery('');
|
||||
} catch {
|
||||
message.error('Analysis failed');
|
||||
@@ -191,14 +212,27 @@ export function InputPanel({
|
||||
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
|
||||
};
|
||||
|
||||
// Helper to format chain display (supports both fixed and dynamic chains)
|
||||
const formatChain = (chain: CausalChain | DynamicCausalChain): string => {
|
||||
if ('chain' in chain) {
|
||||
// Dynamic chain
|
||||
return Object.values(chain.chain).join(' → ');
|
||||
} else {
|
||||
// Fixed chain
|
||||
return `${chain.material} → ${chain.function} → ${chain.usage} → ${chain.user}`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderProgressIndicator = () => {
|
||||
if (progress.step === 'idle' || progress.step === 'done') return null;
|
||||
|
||||
const percent = progress.step === 'step1'
|
||||
? 10
|
||||
: progress.step === 'chains'
|
||||
? 10 + (progress.currentChainIndex / progress.totalChains) * 90
|
||||
: 100;
|
||||
const percent = progress.step === 'step0'
|
||||
? 5
|
||||
: progress.step === 'step1'
|
||||
? 10
|
||||
: progress.step === 'chains'
|
||||
? 10 + (progress.currentChainIndex / progress.totalChains) * 90
|
||||
: 100;
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 16, padding: 12, background: 'rgba(0,0,0,0.04)', borderRadius: 8 }}>
|
||||
@@ -213,6 +247,20 @@ export function InputPanel({
|
||||
</Space>
|
||||
<Progress percent={Math.round(percent)} size="small" status={progress.step === 'error' ? 'exception' : 'active'} />
|
||||
|
||||
{/* Show categories used */}
|
||||
{progress.categoriesUsed && progress.categoriesUsed.length > 0 && (
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>Categories:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{progress.categoriesUsed.map((cat, i) => (
|
||||
<Tag key={i} color={cat.is_fixed ? 'default' : 'blue'}>
|
||||
{cat.name}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{progress.completedChains.length > 0 && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>Completed chains:</Text>
|
||||
@@ -220,7 +268,7 @@ export function InputPanel({
|
||||
{progress.completedChains.map((chain, i) => (
|
||||
<div key={i} style={{ fontSize: 11, padding: '2px 0' }}>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: 4 }} />
|
||||
{chain.material} → {chain.function} → {chain.usage} → {chain.user}
|
||||
{formatChain(chain)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -232,6 +280,22 @@ export function InputPanel({
|
||||
};
|
||||
|
||||
const collapseItems = [
|
||||
{
|
||||
key: 'categories',
|
||||
label: 'Category Settings',
|
||||
children: (
|
||||
<CategorySelector
|
||||
mode={categoryMode}
|
||||
onModeChange={setCategoryMode}
|
||||
customCategories={customCategories}
|
||||
onCustomCategoriesChange={setCustomCategories}
|
||||
suggestedCount={suggestedCategoryCount}
|
||||
onSuggestedCountChange={setSuggestedCategoryCount}
|
||||
step0Result={progress.step0Result}
|
||||
disabled={loading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'llm',
|
||||
label: 'LLM Parameters',
|
||||
|
||||
@@ -117,8 +117,19 @@ export const MindmapD3 = forwardRef<MindmapD3Ref, MindmapD3Props>(
|
||||
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<number, string> = {};
|
||||
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<MindmapD3Ref, MindmapD3Props>(
|
||||
// Draw category headers with background
|
||||
g.selectAll('.category-header-group').remove();
|
||||
const maxDepth = Math.max(...descendants.map(d => d.depth));
|
||||
const categoryColors: Record<string, string> = {
|
||||
'材料': 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<string, string> = {};
|
||||
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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, string[]>;
|
||||
}
|
||||
|
||||
export interface DynamicCausalChain {
|
||||
chain: Record<string, string>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user