diff --git a/package.json b/package.json index 6e4d909..3d1880d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ }, "dependencies": { "@ant-design/icons": "^5.4.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", "antd": "^5.26.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a84e69a..c4bd8a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,15 @@ importers: '@ant-design/icons': specifier: ^5.4.0 version: 5.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.1.0) '@electron-toolkit/preload': specifier: ^3.0.1 version: 3.0.2(electron@35.5.1) @@ -272,6 +281,28 @@ packages: resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@electron-toolkit/eslint-config-prettier@3.0.0': resolution: {integrity: sha512-YapmIOVkbYdHLuTa+ad1SAVtcqYL9A/SJsc7cxQokmhcwAwonGevNom37jBf9slXegcZ/Slh01I/JARG1yhNFw==} peerDependencies: @@ -4268,6 +4299,31 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) + '@dnd-kit/accessibility@3.1.1(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + '@electron-toolkit/eslint-config-prettier@3.0.0(eslint@9.29.0)(prettier@3.5.3)': dependencies: eslint: 9.29.0 diff --git a/src/renderer/src/components/pages/crosstab/AxisDataManager.tsx b/src/renderer/src/components/pages/crosstab/AxisDataManager.tsx index 2b83321..86aa221 100644 --- a/src/renderer/src/components/pages/crosstab/AxisDataManager.tsx +++ b/src/renderer/src/components/pages/crosstab/AxisDataManager.tsx @@ -1,88 +1,388 @@ import React, { useState } from 'react' -import { Card, Button, Input, Space, Tooltip, Popconfirm, Typography } from 'antd' +import { + Card, + Button, + Input, + Space, + Popconfirm, + Typography, + List, + Tooltip, + message +} from 'antd' import { PlusOutlined, - EditOutlined, DeleteOutlined, + ColumnWidthOutlined, + EditOutlined, + HolderOutlined, SaveOutlined, - UndoOutlined, - ColumnWidthOutlined + CloseOutlined } from '@ant-design/icons' -import { CrosstabMetadata } from '../../../types' +import { CrosstabMetadata, CrosstabAxisDimension } from '../../../types' +import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' +import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' const { Text } = Typography interface AxisDataManagerProps { metadata: CrosstabMetadata | null - horizontalValues: string[] - verticalValues: string[] - onEditHorizontalItem: (index: number, value: string) => void - onDeleteHorizontalItem: (index: number) => void - onAddHorizontalItem: (value: string) => void - onEditVerticalItem: (index: number, value: string) => void - onDeleteVerticalItem: (index: number) => void - onAddVerticalItem: (value: string) => void + onUpdateDimension: (dimensionId: string, dimensionType: 'horizontal' | 'vertical', updates: Partial) => void + onGenerateDimensionValues: (dimensionId: string, dimensionType: 'horizontal' | 'vertical') => void + isGeneratingDimensionValues?: { [dimensionId: string]: boolean } +} + +interface SortableItemProps { + id: string + value: string + index: number + isEditing: boolean + onEdit: () => void + onSave: (newValue: string) => void + onCancel: () => void + onDelete: () => void + editingValue: string + setEditingValue: (value: string) => void +} + +const SortableItem: React.FC = ({ + id, + value, + index, + isEditing, + onEdit, + onSave, + onCancel, + onDelete, + editingValue, + setEditingValue +}) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1 + } + + return ( +
+ +
+ } + /> + + + ) } export default function AxisDataManager({ metadata, - horizontalValues, - verticalValues, - onEditHorizontalItem, - onDeleteHorizontalItem, - onAddHorizontalItem, - onEditVerticalItem, - onDeleteVerticalItem, - onAddVerticalItem + onUpdateDimension, + onGenerateDimensionValues, + isGeneratingDimensionValues }: AxisDataManagerProps) { - const [editingHorizontalIndex, setEditingHorizontalIndex] = useState(null) - const [editingVerticalIndex, setEditingVerticalIndex] = useState(null) - const [newHorizontalValue, setNewHorizontalValue] = useState('') - const [newVerticalValue, setNewVerticalValue] = useState('') - const [showAddHorizontal, setShowAddHorizontal] = useState(false) - const [showAddVertical, setShowAddVertical] = useState(false) + const [editingItem, setEditingItem] = useState<{ dimensionId: string; valueIndex: number } | null>(null) + const [editingValue, setEditingValue] = useState('') + const [newValueInputs, setNewValueInputs] = useState<{ [dimensionId: string]: string }>({}) - const handleEditHorizontalItem = (index: number, value: string) => { - if (value.trim()) { - onEditHorizontalItem(index, value.trim()) + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates + }) + ) + + const handleDragEnd = (event: any, dimensionId: string, dimensionType: 'horizontal' | 'vertical') => { + const { active, over } = event + + if (active.id !== over.id) { + const currentDimension = getDimensionById(dimensionId, dimensionType) + if (currentDimension) { + const oldIndex = currentDimension.values.findIndex((_, index) => `${dimensionId}-${index}` === active.id) + const newIndex = currentDimension.values.findIndex((_, index) => `${dimensionId}-${index}` === over.id) + + const newValues = arrayMove(currentDimension.values, oldIndex, newIndex) + onUpdateDimension(dimensionId, dimensionType, { values: newValues }) + } } - setEditingHorizontalIndex(null) } - const handleEditVerticalItem = (index: number, value: string) => { - if (value.trim()) { - onEditVerticalItem(index, value.trim()) + const handleEditStart = (dimensionId: string, valueIndex: number, currentValue: string) => { + setEditingItem({ dimensionId, valueIndex }) + setEditingValue(currentValue) + } + + const handleEditSave = (dimensionId: string, dimensionType: 'horizontal' | 'vertical', valueIndex: number) => { + if (editingValue.trim()) { + const currentDimension = getDimensionById(dimensionId, dimensionType) + if (currentDimension) { + const newValues = [...currentDimension.values] + newValues[valueIndex] = editingValue.trim() + onUpdateDimension(dimensionId, dimensionType, { values: newValues }) + message.success('修改成功') + } } - setEditingVerticalIndex(null) + setEditingItem(null) + setEditingValue('') + } + + const handleEditCancel = () => { + setEditingItem(null) + setEditingValue('') } - const handleAddHorizontalItem = () => { - if (newHorizontalValue.trim()) { - onAddHorizontalItem(newHorizontalValue.trim()) - setNewHorizontalValue('') - setShowAddHorizontal(false) + const handleDeleteDimensionValue = (dimensionId: string, dimensionType: 'horizontal' | 'vertical', valueIndex: number) => { + const currentDimension = getDimensionById(dimensionId, dimensionType) + if (currentDimension) { + const newValues = currentDimension.values.filter((_, index) => index !== valueIndex) + onUpdateDimension(dimensionId, dimensionType, { values: newValues }) + message.success('删除成功') } } - const handleAddVerticalItem = () => { - if (newVerticalValue.trim()) { - onAddVerticalItem(newVerticalValue.trim()) - setNewVerticalValue('') - setShowAddVertical(false) + const handleAddDimensionValue = (dimensionId: string, dimensionType: 'horizontal' | 'vertical') => { + const newValue = newValueInputs[dimensionId] + if (newValue && newValue.trim()) { + const currentDimension = getDimensionById(dimensionId, dimensionType) + if (currentDimension) { + const newValues = [...currentDimension.values, newValue.trim()] + onUpdateDimension(dimensionId, dimensionType, { values: newValues }) + setNewValueInputs(prev => ({ ...prev, [dimensionId]: '' })) + message.success('添加成功') + } } } - if (horizontalValues.length === 0 && verticalValues.length === 0) { + const getDimensionById = (dimensionId: string, dimensionType: 'horizontal' | 'vertical'): CrosstabAxisDimension | null => { + if (!metadata) return null + const dimensions = dimensionType === 'horizontal' ? metadata.horizontalDimensions : metadata.verticalDimensions + return dimensions.find(dim => dim.id === dimensionId) || null + } + + const renderDimensionValues = (dimension: CrosstabAxisDimension, dimensionType: 'horizontal' | 'vertical') => { + const isGenerating = isGeneratingDimensionValues?.[dimension.id] || false + + return ( +
+ {/* 值列表 */} + {dimension.values.length > 0 ? ( +
+ handleDragEnd(event, dimension.id, dimensionType)} + > + `${dimension.id}-${index}`)} + strategy={verticalListSortingStrategy} + > + ( + handleEditStart(dimension.id, index, value)} + onSave={() => handleEditSave(dimension.id, dimensionType, index)} + onCancel={handleEditCancel} + onDelete={() => handleDeleteDimensionValue(dimension.id, dimensionType, index)} + editingValue={editingValue} + setEditingValue={setEditingValue} + /> + )} + /> + + +
+ ) : ( +
+ 暂无数据,请添加或生成数据 +
+ )} + + {/* 添加新值 */} +
+ setNewValueInputs(prev => ({ ...prev, [dimension.id]: e.target.value }))} + onPressEnter={() => handleAddDimensionValue(dimension.id, dimensionType)} + /> + +
+ + {/* 生成按钮 */} +
+ +
+
+ ) + } + + const renderDimensionCard = (dimension: CrosstabAxisDimension, dimensionType: 'horizontal' | 'vertical') => { + return ( + + {dimension.name} + {dimension.description && ( + + ({dimension.description}) + + )} + + } + style={{ marginBottom: 16 }} + > + {renderDimensionValues(dimension, dimensionType)} + + ) + } + + const renderAxisSection = ( + dimensions: CrosstabAxisDimension[], + dimensionType: 'horizontal' | 'vertical', + title: string + ) => { + if (dimensions.length === 0) { + return ( + +
+ +
+ 暂无{title}维度 +
+ 请先在元数据中添加{title}维度 +
+
+
+ ) + } + + return ( + +
+ {dimensions.map((dimension) => renderDimensionCard(dimension, dimensionType))} +
+
+ ) + } + + if (!metadata) { return (
- 尚未生成轴数据 + 尚未生成多维度轴数据
- - 请先完成主题分析,然后使用左侧步骤中的按钮生成横轴和纵轴数据 - + 请先完成主题分析,然后生成各维度的数据
@@ -92,190 +392,10 @@ export default function AxisDataManager({ return (
{/* 横轴数据 */} - {horizontalValues.length > 0 && ( - } - onClick={() => setShowAddHorizontal(!showAddHorizontal)} - size="small" - > - 添加项目 - - } - > -
- {horizontalValues.map((value, index) => ( -
- {editingHorizontalIndex === index ? ( - { - const newValue = (e.target as HTMLInputElement).value.trim() - handleEditHorizontalItem(index, newValue) - }} - onBlur={(e) => { - const newValue = e.target.value.trim() - handleEditHorizontalItem(index, newValue) - }} - autoFocus - /> - ) : ( - {value} - )} - - -
- ))} - {showAddHorizontal && ( -
- setNewHorizontalValue(e.target.value)} - onPressEnter={handleAddHorizontalItem} - /> - - - - -
- )} -
-
- )} + {renderAxisSection(metadata.horizontalDimensions, 'horizontal', '横轴')} {/* 纵轴数据 */} - {verticalValues.length > 0 && ( - } - onClick={() => setShowAddVertical(!showAddVertical)} - size="small" - > - 添加项目 - - } - > -
- {verticalValues.map((value, index) => ( -
- {editingVerticalIndex === index ? ( - { - const newValue = (e.target as HTMLInputElement).value.trim() - handleEditVerticalItem(index, newValue) - }} - onBlur={(e) => { - const newValue = e.target.value.trim() - handleEditVerticalItem(index, newValue) - }} - autoFocus - /> - ) : ( - {value} - )} - - -
- ))} - {showAddVertical && ( -
- setNewVerticalValue(e.target.value)} - onPressEnter={handleAddVerticalItem} - /> - - - - -
- )} -
-
- )} + {renderAxisSection(metadata.verticalDimensions, 'vertical', '纵轴')}
) } diff --git a/src/renderer/src/components/pages/crosstab/CrosstabChat.tsx b/src/renderer/src/components/pages/crosstab/CrosstabChat.tsx index d019b74..647c4f2 100644 --- a/src/renderer/src/components/pages/crosstab/CrosstabChat.tsx +++ b/src/renderer/src/components/pages/crosstab/CrosstabChat.tsx @@ -9,13 +9,12 @@ import { import { useAppContext } from '../../../store/AppContext' import { createAIService } from '../../../services/aiService' -import { PROMPT_TEMPLATES, extractJsonContent } from './CrosstabUtils' +import { PROMPT_TEMPLATES, extractJsonContent, generateAxisCombinations, generateDimensionPath } from './CrosstabUtils' import { AITask } from '../../../types' import { v4 as uuidv4 } from 'uuid' import StepFlow from './StepFlow' import TopicInput from './TopicInput' import MetadataDisplay from './MetadataDisplay' -import MetadataEditor from './MetadataEditor' import AxisDataManager from './AxisDataManager' import CrosstabTable from './CrosstabTable' import PageLineageDisplay from '../../common/PageLineageDisplay' @@ -33,7 +32,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { const { state, dispatch } = useAppContext() const [userInput, setUserInput] = useState('') const [activeTab, setActiveTab] = useState('0') - const [isEditingMetadata, setIsEditingMetadata] = useState(false) const [selectedModel, setSelectedModel] = useState( state.settings.defaultLLMId ) @@ -41,9 +39,8 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { const [isGeneratingRow, setIsGeneratingRow] = useState(null) const [isGeneratingCell, setIsGeneratingCell] = useState(null) const [isGeneratingTopicSuggestions, setIsGeneratingTopicSuggestions] = useState(false) - const [isGeneratingHorizontalSuggestions, setIsGeneratingHorizontalSuggestions] = useState(false) - const [isGeneratingVerticalSuggestions, setIsGeneratingVerticalSuggestions] = useState(false) - const [isGeneratingValueSuggestions, setIsGeneratingValueSuggestions] = useState(false) + const [isGeneratingDimensionSuggestions, setIsGeneratingDimensionSuggestions] = useState<{ [dimensionId: string]: boolean }>({}) + const [isGeneratingDimensionValues, setIsGeneratingDimensionValues] = useState<{ [dimensionId: string]: boolean }>({}) const { message } = App.useApp() const chat = useMemo(() => { @@ -61,7 +58,7 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { }, []) const handleGenerateColumn = useCallback( - async (horizontalItem: string) => { + async (columnPath: string) => { if (!chat || isGeneratingColumn) return const llmConfig = getLLMConfig() @@ -70,114 +67,153 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { return } - if (!chat.crosstabData.metadata || chat.crosstabData.verticalValues.length === 0) { - message.error('请先完成主题设置和轴数据生成') + if (!chat.crosstabData.metadata) { + message.error('请先完成主题设置') return } - setIsGeneratingColumn(horizontalItem) - - const taskId = uuidv4() - - // 先创建AI服务实例 - const aiService = createAIService(llmConfig) - - // 创建AI任务监控 - const task: AITask = { - id: taskId, - requestId: aiService.id, // 使用AI服务的requestId - type: 'crosstab_cell', - status: 'running', - title: '生成列数据', - description: `生成列 "${horizontalItem}" 的所有数据`, - chatId, - modelId: llmConfig.id, - startTime: Date.now(), - context: { - crosstab: { - horizontalItem, - verticalItem: 'all', - metadata: chat.crosstabData.metadata - } - } + const { verticalDimensions } = chat.crosstabData.metadata + const hasVerticalData = verticalDimensions.every(dim => dim.values.length > 0) + + if (!hasVerticalData) { + message.error('请先完成纵轴维度数据生成') + return } - dispatch({ - type: 'ADD_AI_TASK', - payload: { task } - }) + setIsGeneratingColumn(columnPath) + // 生成该列的所有单元格数据 try { - const itemPrompt = PROMPT_TEMPLATES.values - .replace('[METADATA_JSON]', JSON.stringify(chat.crosstabData.metadata, null, 2)) - .replace(/\[HORIZONTAL_ITEM\]/g, horizontalItem) - .replace('[VERTICAL_ITEMS]', JSON.stringify(chat.crosstabData.verticalValues, null, 2)) - const response = await new Promise((resolve, reject) => { - aiService.sendMessage( - [{ id: 'temp', role: 'user', content: itemPrompt, timestamp: Date.now() }], - { - onChunk: () => {}, // 空的chunk处理函数 - onComplete: (response) => resolve(response), - onError: (error) => reject(error) - } - ) - }) - - try { - const parsedData = JSON.parse(extractJsonContent(response)) - const updatedTableData = { ...chat.crosstabData.tableData } - updatedTableData[horizontalItem] = parsedData + // 使用已经导入的函数 + const verticalCombinations = generateAxisCombinations(chat.crosstabData.metadata.verticalDimensions) + const aiService = createAIService(llmConfig) + + const updatedTableData = { ...chat.crosstabData.tableData } + + // 为该列的每个单元格生成数据 + for (const vCombination of verticalCombinations) { + const rowPath = generateDimensionPath(vCombination) + const cellKey = `${columnPath}|${rowPath}` + + const taskId = uuidv4() + const task: AITask = { + id: taskId, + requestId: aiService.id, + type: 'crosstab_cell', + status: 'running', + title: '生成单元格数据', + description: `生成单元格 ${columnPath} × ${rowPath} 的数据`, + chatId, + modelId: llmConfig.id, + startTime: Date.now() + } dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { tableData: updatedTableData } - } + type: 'ADD_AI_TASK', + payload: { task } }) - // 更新任务状态为完成 - dispatch({ - type: 'UPDATE_AI_TASK', - payload: { - taskId, - updates: { - status: 'completed', - endTime: Date.now() + try { + const prompt = PROMPT_TEMPLATES.cell_values + .replace('[METADATA_JSON]', JSON.stringify(chat.crosstabData.metadata, null, 2)) + .replace('[HORIZONTAL_PATH]', columnPath) + .replace('[VERTICAL_PATH]', rowPath) + .replace('[VALUE_DIMENSIONS]', JSON.stringify(chat.crosstabData.metadata.valueDimensions, null, 2)) + + const response = await new Promise((resolve, reject) => { + aiService.sendMessage( + [{ id: 'temp', role: 'user', content: prompt, timestamp: Date.now() }], + { + onChunk: () => {}, + onComplete: (response) => resolve(response), + onError: (error) => reject(error) + } + ) + }) + + const jsonContent = extractJsonContent(response) + const cellValues = JSON.parse(jsonContent) + + // 处理AI生成的数据格式 + const processedCellValues: { [key: string]: string } = {} + if (chat.crosstabData.metadata.valueDimensions.length > 0) { + const valueDimensions = chat.crosstabData.metadata.valueDimensions + + const keys = Object.keys(cellValues) + const hasGenericKeys = keys.some(key => key.match(/^value\d+$/)) + + if (hasGenericKeys) { + valueDimensions.forEach((dimension, index) => { + const genericKey = `value${index + 1}` + if (cellValues[genericKey]) { + processedCellValues[dimension.id] = cellValues[genericKey] + } + }) + } else { + valueDimensions.forEach(dimension => { + if (cellValues[dimension.id]) { + processedCellValues[dimension.id] = cellValues[dimension.id] + } + }) + } + + if (Object.keys(processedCellValues).length === 0 && Object.keys(cellValues).length > 0) { + const firstDimension = valueDimensions[0] + const firstValue = Object.values(cellValues)[0] + processedCellValues[firstDimension.id] = firstValue as string } } - }) - message.success(`列 "${horizontalItem}" 数据生成完成`) - } catch (e) { - message.error(`解析列 "${horizontalItem}" 的数据失败`) - throw e + updatedTableData[cellKey] = processedCellValues + + dispatch({ + type: 'UPDATE_AI_TASK', + payload: { + taskId, + updates: { + status: 'completed', + endTime: Date.now() + } + } + }) + } catch (error) { + dispatch({ + type: 'UPDATE_AI_TASK', + payload: { + taskId, + updates: { + status: 'failed', + endTime: Date.now(), + error: error instanceof Error ? error.message : 'Unknown error' + } + } + }) + console.error(`单元格 ${cellKey} 生成失败:`, error) + } } - } catch (error) { - console.error('Column generation error:', error) - message.error('列数据生成失败,请重试') - - // 更新任务状态为失败 + + // 批量更新表格数据 dispatch({ - type: 'UPDATE_AI_TASK', + type: 'UPDATE_CROSSTAB_DATA', payload: { - taskId, - updates: { - status: 'failed', - endTime: Date.now(), - error: error instanceof Error ? error.message : 'Unknown error' - } + chatId, + data: { tableData: updatedTableData } } }) + + message.success(`列 "${columnPath}" 数据生成完成`) + } catch (error) { + console.error('列数据生成失败:', error) + message.error(`列数据生成失败: ${error.message}`) } finally { setIsGeneratingColumn(null) } }, - [chat, isGeneratingColumn, getLLMConfig, dispatch, chatId, message] + [chat, isGeneratingColumn, getLLMConfig, message, chatId, dispatch] ) const handleGenerateRow = useCallback( - async (verticalItem: string) => { + async (rowPath: string) => { if (!chat || isGeneratingRow) return const llmConfig = getLLMConfig() @@ -186,126 +222,153 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { return } - if (!chat.crosstabData.metadata || chat.crosstabData.horizontalValues.length === 0) { - message.error('请先完成主题设置和轴数据生成') + if (!chat.crosstabData.metadata) { + message.error('请先完成主题设置') return } - setIsGeneratingRow(verticalItem) - - const taskId = uuidv4() - - // 先创建AI服务实例 - const aiService = createAIService(llmConfig) - - // 创建AI任务监控 - const task: AITask = { - id: taskId, - requestId: aiService.id, // 使用AI服务的requestId - type: 'crosstab_cell', - status: 'running', - title: '生成行数据', - description: `生成行 "${verticalItem}" 的所有数据`, - chatId, - modelId: llmConfig.id, - startTime: Date.now(), - context: { - crosstab: { - horizontalItem: 'all', - verticalItem, - metadata: chat.crosstabData.metadata - } - } + const { horizontalDimensions } = chat.crosstabData.metadata + const hasHorizontalData = horizontalDimensions.every(dim => dim.values.length > 0) + + if (!hasHorizontalData) { + message.error('请先完成横轴维度数据生成') + return } - dispatch({ - type: 'ADD_AI_TASK', - payload: { task } - }) + setIsGeneratingRow(rowPath) + // 生成该行的所有单元格数据 try { - const itemPrompt = PROMPT_TEMPLATES.rowValues - .replace('[METADATA_JSON]', JSON.stringify(chat.crosstabData.metadata, null, 2)) - .replace(/\[VERTICAL_ITEM\]/g, verticalItem) - .replace( - '[HORIZONTAL_ITEMS]', - JSON.stringify(chat.crosstabData.horizontalValues, null, 2) - ) - const response = await new Promise((resolve, reject) => { - aiService.sendMessage( - [{ id: 'temp', role: 'user', content: itemPrompt, timestamp: Date.now() }], - { - onChunk: () => {}, // 空的chunk处理函数 - onComplete: (response) => resolve(response), - onError: (error) => reject(error) - } - ) - }) - - try { - const parsedData = JSON.parse(extractJsonContent(response)) - const updatedTableData = { ...chat.crosstabData.tableData } - - // 为每个横轴项目更新该行的数据 - chat.crosstabData.horizontalValues.forEach((horizontalItem) => { - if (!updatedTableData[horizontalItem]) { - updatedTableData[horizontalItem] = {} - } - if (parsedData[horizontalItem]) { - updatedTableData[horizontalItem][verticalItem] = parsedData[horizontalItem] - } - }) + // 使用已经导入的函数 + const horizontalCombinations = generateAxisCombinations(chat.crosstabData.metadata.horizontalDimensions) + const aiService = createAIService(llmConfig) + + const updatedTableData = { ...chat.crosstabData.tableData } + + // 为该行的每个单元格生成数据 + for (const hCombination of horizontalCombinations) { + const columnPath = generateDimensionPath(hCombination) + const cellKey = `${columnPath}|${rowPath}` + + const taskId = uuidv4() + const task: AITask = { + id: taskId, + requestId: aiService.id, + type: 'crosstab_cell', + status: 'running', + title: '生成单元格数据', + description: `生成单元格 ${columnPath} × ${rowPath} 的数据`, + chatId, + modelId: llmConfig.id, + startTime: Date.now() + } dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { tableData: updatedTableData } - } + type: 'ADD_AI_TASK', + payload: { task } }) - // 更新任务状态为完成 - dispatch({ - type: 'UPDATE_AI_TASK', - payload: { - taskId, - updates: { - status: 'completed', - endTime: Date.now() + try { + const prompt = PROMPT_TEMPLATES.cell_values + .replace('[METADATA_JSON]', JSON.stringify(chat.crosstabData.metadata, null, 2)) + .replace('[HORIZONTAL_PATH]', columnPath) + .replace('[VERTICAL_PATH]', rowPath) + .replace('[VALUE_DIMENSIONS]', JSON.stringify(chat.crosstabData.metadata.valueDimensions, null, 2)) + + const response = await new Promise((resolve, reject) => { + aiService.sendMessage( + [{ id: 'temp', role: 'user', content: prompt, timestamp: Date.now() }], + { + onChunk: () => {}, + onComplete: (response) => resolve(response), + onError: (error) => reject(error) + } + ) + }) + + const jsonContent = extractJsonContent(response) + const cellValues = JSON.parse(jsonContent) + + // 处理AI生成的数据格式 + const processedCellValues: { [key: string]: string } = {} + if (chat.crosstabData.metadata.valueDimensions.length > 0) { + const valueDimensions = chat.crosstabData.metadata.valueDimensions + + const keys = Object.keys(cellValues) + const hasGenericKeys = keys.some(key => key.match(/^value\d+$/)) + + if (hasGenericKeys) { + valueDimensions.forEach((dimension, index) => { + const genericKey = `value${index + 1}` + if (cellValues[genericKey]) { + processedCellValues[dimension.id] = cellValues[genericKey] + } + }) + } else { + valueDimensions.forEach(dimension => { + if (cellValues[dimension.id]) { + processedCellValues[dimension.id] = cellValues[dimension.id] + } + }) + } + + if (Object.keys(processedCellValues).length === 0 && Object.keys(cellValues).length > 0) { + const firstDimension = valueDimensions[0] + const firstValue = Object.values(cellValues)[0] + processedCellValues[firstDimension.id] = firstValue as string } } - }) - message.success(`行 "${verticalItem}" 数据生成完成`) - } catch (e) { - message.error(`解析行 "${verticalItem}" 的数据失败`) - throw e + updatedTableData[cellKey] = processedCellValues + + dispatch({ + type: 'UPDATE_AI_TASK', + payload: { + taskId, + updates: { + status: 'completed', + endTime: Date.now() + } + } + }) + } catch (error) { + dispatch({ + type: 'UPDATE_AI_TASK', + payload: { + taskId, + updates: { + status: 'failed', + endTime: Date.now(), + error: error instanceof Error ? error.message : 'Unknown error' + } + } + }) + console.error(`单元格 ${cellKey} 生成失败:`, error) + } } - } catch (error) { - console.error('Row generation error:', error) - message.error('行数据生成失败,请重试') - - // 更新任务状态为失败 + + // 批量更新表格数据 dispatch({ - type: 'UPDATE_AI_TASK', + type: 'UPDATE_CROSSTAB_DATA', payload: { - taskId, - updates: { - status: 'failed', - endTime: Date.now(), - error: error instanceof Error ? error.message : 'Unknown error' - } + chatId, + data: { tableData: updatedTableData } } }) + + message.success(`行 "${rowPath}" 数据生成完成`) + } catch (error) { + console.error('行数据生成失败:', error) + message.error(`行数据生成失败: ${error.message}`) } finally { setIsGeneratingRow(null) } }, - [chat, isGeneratingRow, getLLMConfig, dispatch, chatId, message] + [chat, isGeneratingRow, getLLMConfig, message, chatId, dispatch] ) const handleGenerateCell = useCallback( - async (horizontalItem: string, verticalItem: string) => { + async (columnPath: string, rowPath: string) => { if (!chat || isGeneratingCell) return const llmConfig = getLLMConfig() @@ -319,32 +382,22 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { return } - const cellKey = `${horizontalItem}_${verticalItem}` + const cellKey = `${columnPath}|${rowPath}` setIsGeneratingCell(cellKey) const taskId = uuidv4() - - // 先创建AI服务实例 const aiService = createAIService(llmConfig) - // 创建AI任务监控 const task: AITask = { id: taskId, - requestId: aiService.id, // 使用AI服务的requestId + requestId: aiService.id, type: 'crosstab_cell', status: 'running', title: '生成单元格数据', - description: `生成 "${horizontalItem} × ${verticalItem}" 单元格数据`, + description: `生成单元格 ${columnPath} × ${rowPath} 的数据`, chatId, modelId: llmConfig.id, - startTime: Date.now(), - context: { - crosstab: { - horizontalItem, - verticalItem, - metadata: chat.crosstabData.metadata - } - } + startTime: Date.now() } dispatch({ @@ -353,28 +406,69 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { }) try { - const itemPrompt = PROMPT_TEMPLATES.cellValue + const prompt = PROMPT_TEMPLATES.cell_values .replace('[METADATA_JSON]', JSON.stringify(chat.crosstabData.metadata, null, 2)) - .replace(/\[HORIZONTAL_ITEM\]/g, horizontalItem) - .replace(/\[VERTICAL_ITEM\]/g, verticalItem) + .replace('[HORIZONTAL_PATH]', columnPath) + .replace('[VERTICAL_PATH]', rowPath) + .replace('[VALUE_DIMENSIONS]', JSON.stringify(chat.crosstabData.metadata.valueDimensions, null, 2)) + const response = await new Promise((resolve, reject) => { aiService.sendMessage( - [{ id: 'temp', role: 'user', content: itemPrompt, timestamp: Date.now() }], + [{ id: 'temp', role: 'user', content: prompt, timestamp: Date.now() }], { - onChunk: () => {}, // 空的chunk处理函数 + onChunk: () => {}, onComplete: (response) => resolve(response), onError: (error) => reject(error) } ) }) - const updatedTableData = { ...chat.crosstabData.tableData } - if (!updatedTableData[horizontalItem]) { - updatedTableData[horizontalItem] = {} + const jsonContent = extractJsonContent(response) + const cellValues = JSON.parse(jsonContent) + + // 处理AI生成的数据格式,确保键是实际的值维度ID + const processedCellValues: { [key: string]: string } = {} + + // 如果AI生成的数据使用的是通用键(如value1, value2),需要映射到实际的值维度ID + if (chat.crosstabData.metadata.valueDimensions.length > 0) { + const valueDimensions = chat.crosstabData.metadata.valueDimensions + + // 检查是否使用了通用键格式 + const keys = Object.keys(cellValues) + const hasGenericKeys = keys.some(key => key.match(/^value\d+$/)) + + if (hasGenericKeys) { + // 映射通用键到实际的值维度ID + valueDimensions.forEach((dimension, index) => { + const genericKey = `value${index + 1}` + if (cellValues[genericKey]) { + processedCellValues[dimension.id] = cellValues[genericKey] + } + }) + } else { + // 检查是否直接使用了值维度ID + valueDimensions.forEach(dimension => { + if (cellValues[dimension.id]) { + processedCellValues[dimension.id] = cellValues[dimension.id] + } + }) + } + + // 如果没有找到匹配的键,尝试使用第一个可用的值作为第一个维度的值 + if (Object.keys(processedCellValues).length === 0 && Object.keys(cellValues).length > 0) { + const firstDimension = valueDimensions[0] + const firstValue = Object.values(cellValues)[0] + processedCellValues[firstDimension.id] = firstValue as string + } } - // 直接使用响应内容,因为cellValue模板返回的是纯文本 - updatedTableData[horizontalItem][verticalItem] = response.trim() + console.log('Original cell values:', cellValues) + console.log('Processed cell values:', processedCellValues) + console.log('Value dimensions:', chat.crosstabData.metadata.valueDimensions) + + // 更新表格数据 + const updatedTableData = { ...chat.crosstabData.tableData } + updatedTableData[cellKey] = processedCellValues dispatch({ type: 'UPDATE_CROSSTAB_DATA', @@ -384,7 +478,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) - // 更新任务状态为完成 dispatch({ type: 'UPDATE_AI_TASK', payload: { @@ -396,12 +489,11 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) - message.success(`单元格 "${horizontalItem} × ${verticalItem}" 数据生成完成`) + message.success('单元格数据生成完成') } catch (error) { - console.error('Cell generation error:', error) - message.error('单元格数据生成失败,请重试') - - // 更新任务状态为失败 + console.error('单元格生成失败:', error) + message.error(`单元格生成失败: ${error.message}`) + dispatch({ type: 'UPDATE_AI_TASK', payload: { @@ -421,13 +513,17 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { ) const handleClearColumn = useCallback( - (horizontalItem: string) => { + (columnPath: string) => { if (!chat) return const updatedTableData = { ...chat.crosstabData.tableData } - if (updatedTableData[horizontalItem]) { - delete updatedTableData[horizontalItem] - } + + // 删除所有以该列路径开头的单元格数据 + Object.keys(updatedTableData).forEach(cellKey => { + if (cellKey.startsWith(columnPath + '|')) { + delete updatedTableData[cellKey] + } + }) dispatch({ type: 'UPDATE_CROSSTAB_DATA', @@ -437,21 +533,21 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) - message.success(`列 "${horizontalItem}" 数据已清除`) + message.success(`列 "${columnPath}" 数据已清除`) }, [chat, dispatch, chatId, message] ) const handleClearRow = useCallback( - (verticalItem: string) => { + (rowPath: string) => { if (!chat) return const updatedTableData = { ...chat.crosstabData.tableData } - // 删除所有列中该行的数据 - Object.keys(updatedTableData).forEach((horizontalItem) => { - if (updatedTableData[horizontalItem][verticalItem]) { - delete updatedTableData[horizontalItem][verticalItem] + // 删除所有以该行路径结尾的单元格数据 + Object.keys(updatedTableData).forEach(cellKey => { + if (cellKey.endsWith('|' + rowPath)) { + delete updatedTableData[cellKey] } }) @@ -463,19 +559,20 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) - message.success(`行 "${verticalItem}" 数据已清除`) + message.success(`行 "${rowPath}" 数据已清除`) }, [chat, dispatch, chatId, message] ) const handleClearCell = useCallback( - (horizontalItem: string, verticalItem: string) => { + (columnPath: string, rowPath: string) => { if (!chat) return const updatedTableData = { ...chat.crosstabData.tableData } + const cellKey = `${columnPath}|${rowPath}` - if (updatedTableData[horizontalItem] && updatedTableData[horizontalItem][verticalItem]) { - delete updatedTableData[horizontalItem][verticalItem] + if (updatedTableData[cellKey]) { + delete updatedTableData[cellKey] } dispatch({ @@ -486,13 +583,13 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) - message.success(`单元格 "${horizontalItem} × ${verticalItem}" 数据已清除`) + message.success(`单元格 "${columnPath} × ${rowPath}" 数据已清除`) }, [chat, dispatch, chatId, message] ) const handleCreateChatFromCell = useCallback( - (horizontalItem: string, verticalItem: string, cellContent: string, metadata: any) => { + (columnPath: string, rowPath: string, cellContent: string, metadata: any) => { if (!chat || !metadata) return // 直接dispatch,将所有参数传递给reducer处理 @@ -500,15 +597,15 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { type: 'CREATE_CHAT_FROM_CELL', payload: { folderId: chat.folderId, - horizontalItem, - verticalItem, + horizontalItem: columnPath, + verticalItem: rowPath, cellContent, metadata, sourcePageId: chatId } }) - message.success(`已创建新聊天窗口分析 "${horizontalItem} × ${verticalItem}"`) + message.success(`已创建新聊天窗口分析 "${columnPath} × ${rowPath}"`) }, [chat, dispatch, message, chatId] ) @@ -533,14 +630,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { updateData.metadata = data.metadata setActiveTab('1') // 切换到主题结构tab } - if (data.horizontalValues) { - updateData.horizontalValues = data.horizontalValues - setActiveTab('2') // 切换到轴数据tab - } - if (data.verticalValues) { - updateData.verticalValues = data.verticalValues - setActiveTab('2') // 切换到轴数据tab - } if (data.tableData) { updateData.tableData = data.tableData setActiveTab('3') // 切换到交叉分析表tab @@ -565,139 +654,106 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { [dispatch, chatId] ) - const handleEditMetadata = useCallback(() => { - setIsEditingMetadata(true) - }, []) - - const handleSaveMetadata = useCallback( - (values: any) => { + const handleUpdateMetadata = useCallback( + (metadata: any) => { dispatch({ type: 'UPDATE_CROSSTAB_DATA', payload: { chatId, - data: { metadata: values } + data: { metadata } } }) - setIsEditingMetadata(false) - message.success('主题元数据已更新') }, [dispatch, chatId] ) - const handleEditHorizontalItem = useCallback( - (index: number, value: string) => { - const newValues = [...chat!.crosstabData.horizontalValues] - newValues[index] = value + const handleUpdateDimension = useCallback( + (dimensionId: string, dimensionType: 'horizontal' | 'vertical', updates: any) => { + if (!chat || !chat.crosstabData.metadata) return - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { horizontalValues: newValues } - } - }) - message.success('横轴项目已更新') - }, - [chat, dispatch, chatId] - ) - - const handleDeleteHorizontalItem = useCallback( - (index: number) => { - const newValues = [...chat!.crosstabData.horizontalValues] - const deletedItem = newValues.splice(index, 1)[0] + const metadata = chat.crosstabData.metadata + const dimensionsKey = dimensionType === 'horizontal' ? 'horizontalDimensions' : 'verticalDimensions' + const dimensions = metadata[dimensionsKey] + + const updatedDimensions = dimensions.map(dim => + dim.id === dimensionId ? { ...dim, ...updates } : dim + ) - // 同时删除对应的表格数据 - const newTableData = { ...chat!.crosstabData.tableData } - delete newTableData[deletedItem] + const updatedMetadata = { + ...metadata, + [dimensionsKey]: updatedDimensions + } dispatch({ type: 'UPDATE_CROSSTAB_DATA', payload: { chatId, - data: { - horizontalValues: newValues, - tableData: newTableData - } + data: { metadata: updatedMetadata } } }) - message.success('横轴项目已删除') + message.success('维度已更新') }, - [chat, dispatch, chatId] + [chat, chatId, dispatch, message] ) - const handleAddHorizontalItem = useCallback( - (value: string) => { - const newValues = [...chat!.crosstabData.horizontalValues, value] + const handleGenerateDimensionValues = useCallback( + async (dimensionId: string, dimensionType: 'horizontal' | 'vertical') => { + if (!chat || !chat.crosstabData.metadata) { + message.error('请先完成主题分析') + return + } - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { horizontalValues: newValues } - } - }) - message.success('横轴项目已添加') - }, - [chat, dispatch, chatId] - ) + const llmConfig = getLLMConfig() + if (!llmConfig) { + message.error('请先在设置中配置LLM') + return + } - const handleEditVerticalItem = useCallback( - (index: number, value: string) => { - const newValues = [...chat!.crosstabData.verticalValues] - newValues[index] = value + setIsGeneratingDimensionValues(prev => ({ ...prev, [dimensionId]: true })) - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { verticalValues: newValues } + try { + const dimensions = dimensionType === 'horizontal' + ? chat.crosstabData.metadata.horizontalDimensions + : chat.crosstabData.metadata.verticalDimensions + + const dimension = dimensions.find(d => d.id === dimensionId) + if (!dimension) { + message.error('找不到指定的维度') + return } - }) - message.success('纵轴项目已更新') - }, - [chat, dispatch, chatId] - ) - const handleDeleteVerticalItem = useCallback( - (index: number) => { - const newValues = [...chat!.crosstabData.verticalValues] - const deletedItem = newValues.splice(index, 1)[0] - - // 同时删除对应的表格数据 - const newTableData = { ...chat!.crosstabData.tableData } - Object.keys(newTableData).forEach((horizontalKey) => { - delete newTableData[horizontalKey][deletedItem] - }) + const prompt = PROMPT_TEMPLATES.dimension_values + .replace('[METADATA_JSON]', JSON.stringify(chat.crosstabData.metadata, null, 2)) + .replace('[DIMENSION_ID]', dimension.id) + .replace('[DIMENSION_NAME]', dimension.name) + .replace('[DIMENSION_DESCRIPTION]', dimension.description || '') - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { - verticalValues: newValues, - tableData: newTableData - } - } - }) - message.success('纵轴项目已删除') - }, - [chat, dispatch, chatId] - ) + const aiService = createAIService(llmConfig) + const result = await new Promise((resolve, reject) => { + aiService.sendMessage( + [{ id: 'temp', role: 'user', content: prompt, timestamp: Date.now() }], + { + onChunk: () => {}, + onComplete: (response) => resolve(response), + onError: (error) => reject(error) + } + ) + }) - const handleAddVerticalItem = useCallback( - (value: string) => { - const newValues = [...chat!.crosstabData.verticalValues, value] + const jsonContent = extractJsonContent(result) + const values = JSON.parse(jsonContent) - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { verticalValues: newValues } - } - }) - message.success('纵轴项目已添加') + // 更新维度值 + handleUpdateDimension(dimensionId, dimensionType, { values }) + message.success(`维度"${dimension.name}"的值生成完成`) + } catch (error) { + console.error('维度值生成失败:', error) + message.error(`维度值生成失败: ${error.message}`) + } finally { + setIsGeneratingDimensionValues(prev => ({ ...prev, [dimensionId]: false })) + } }, - [chat, dispatch, chatId] + [chat, getLLMConfig, handleUpdateDimension, message] ) const handleGenerateTopicSuggestions = useCallback(async () => { @@ -717,18 +773,15 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { setIsGeneratingTopicSuggestions(true) const taskId = uuidv4() - - // 先创建AI服务实例 const aiService = createAIService(llmConfig) - // 创建AI任务监控 const task: AITask = { id: taskId, - requestId: aiService.id, // 使用AI服务的requestId + requestId: aiService.id, type: 'crosstab_cell', status: 'running', title: '生成主题候选项', - description: `为主题 "${chat.crosstabData.metadata.Topic}" 生成相关候选项`, + description: `为主题 "${chat.crosstabData.metadata.topic}" 生成相关候选项`, chatId, modelId: llmConfig.id, startTime: Date.now(), @@ -749,13 +802,13 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { try { const prompt = PROMPT_TEMPLATES.topicSuggestions.replace( '[CURRENT_TOPIC]', - chat.crosstabData.metadata.Topic + chat.crosstabData.metadata.topic ) const response = await new Promise((resolve, reject) => { aiService.sendMessage( [{ id: 'temp', role: 'user', content: prompt, timestamp: Date.now() }], { - onChunk: () => {}, // 空的chunk处理函数 + onChunk: () => {}, onComplete: (response) => resolve(response), onError: (error) => reject(error) } @@ -768,7 +821,7 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { // 保存候选项到metadata中 const newMetadata = { ...chat.crosstabData.metadata!, - TopicSuggestions: parsedSuggestions + topicSuggestions: parsedSuggestions } dispatch({ type: 'UPDATE_CROSSTAB_DATA', @@ -778,7 +831,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) - // 更新任务状态为完成 dispatch({ type: 'UPDATE_AI_TASK', payload: { @@ -802,7 +854,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { console.error('Topic suggestions generation error:', error) message.error('主题候选项生成失败,请重试') - // 更新任务状态为失败 dispatch({ type: 'UPDATE_AI_TASK', payload: { @@ -819,126 +870,13 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }, [chat, isGeneratingTopicSuggestions, getLLMConfig, dispatch, chatId, message]) - const handleGenerateHorizontalSuggestions = useCallback(async () => { - if (!chat || isGeneratingHorizontalSuggestions) return - - const llmConfig = getLLMConfig() - if (!llmConfig) { - message.error('请先在设置中配置LLM') - return - } - - if (!chat.crosstabData.metadata) { + const handleGenerateDimensionSuggestions = useCallback(async (dimensionId: string) => { + if (!chat || !chat.crosstabData.metadata) { message.error('请先完成主题设置') return } - setIsGeneratingHorizontalSuggestions(true) - - const taskId = uuidv4() - - // 先创建AI服务实例 - const aiService = createAIService(llmConfig) - - // 创建AI任务监控 - const task: AITask = { - id: taskId, - requestId: aiService.id, // 使用AI服务的requestId - type: 'crosstab_cell', - status: 'running', - title: '生成横轴候选项', - description: `为主题 "${chat.crosstabData.metadata.Topic}" 生成横轴候选项`, - chatId, - modelId: llmConfig.id, - startTime: Date.now(), - context: { - crosstab: { - horizontalItem: 'suggestions', - verticalItem: 'horizontal', - metadata: chat.crosstabData.metadata - } - } - } - - dispatch({ - type: 'ADD_AI_TASK', - payload: { task } - }) - - try { - const prompt = PROMPT_TEMPLATES.horizontalSuggestions - .replace('[METADATA_JSON]', JSON.stringify(chat.crosstabData.metadata, null, 2)) - .replace('[TOPIC]', chat.crosstabData.metadata.Topic) - const response = await new Promise((resolve, reject) => { - aiService.sendMessage( - [{ id: 'temp', role: 'user', content: prompt, timestamp: Date.now() }], - { - onChunk: () => {}, // 空的chunk处理函数 - onComplete: (response) => resolve(response), - onError: (error) => reject(error) - } - ) - }) - - try { - const parsedSuggestions = JSON.parse(extractJsonContent(response)) - if (Array.isArray(parsedSuggestions)) { - // 保存候选项到metadata中 - const newMetadata = { - ...chat.crosstabData.metadata!, - HorizontalAxisSuggestions: parsedSuggestions - } - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { metadata: newMetadata } - } - }) - - // 更新任务状态为完成 - dispatch({ - type: 'UPDATE_AI_TASK', - payload: { - taskId, - updates: { - status: 'completed', - endTime: Date.now() - } - } - }) - - message.success('横轴候选项生成完成') - } else { - throw new Error('返回的不是数组格式') - } - } catch (e) { - message.error('解析横轴候选项失败') - throw e - } - } catch (error) { - console.error('Horizontal suggestions generation error:', error) - message.error('横轴候选项生成失败,请重试') - - // 更新任务状态为失败 - dispatch({ - type: 'UPDATE_AI_TASK', - payload: { - taskId, - updates: { - status: 'failed', - endTime: Date.now(), - error: error instanceof Error ? error.message : 'Unknown error' - } - } - }) - } finally { - setIsGeneratingHorizontalSuggestions(false) - } - }, [chat, isGeneratingHorizontalSuggestions, getLLMConfig, dispatch, chatId, message]) - - const handleGenerateVerticalSuggestions = useCallback(async () => { - if (!chat || isGeneratingVerticalSuggestions) return + if (isGeneratingDimensionSuggestions[dimensionId]) return const llmConfig = getLLMConfig() if (!llmConfig) { @@ -946,151 +884,39 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { return } - if (!chat.crosstabData.metadata) { - message.error('请先完成主题设置') - return - } - - setIsGeneratingVerticalSuggestions(true) + setIsGeneratingDimensionSuggestions(prev => ({ ...prev, [dimensionId]: true })) const taskId = uuidv4() - - // 先创建AI服务实例 const aiService = createAIService(llmConfig) - // 创建AI任务监控 - const task: AITask = { - id: taskId, - requestId: aiService.id, // 使用AI服务的requestId - type: 'crosstab_cell', - status: 'running', - title: '生成纵轴候选项', - description: `为主题 "${chat.crosstabData.metadata.Topic}" 生成纵轴候选项`, - chatId, - modelId: llmConfig.id, - startTime: Date.now(), - context: { - crosstab: { - horizontalItem: 'suggestions', - verticalItem: 'vertical', - metadata: chat.crosstabData.metadata - } - } - } - - dispatch({ - type: 'ADD_AI_TASK', - payload: { task } - }) - - try { - const prompt = PROMPT_TEMPLATES.verticalSuggestions - .replace('[METADATA_JSON]', JSON.stringify(chat.crosstabData.metadata, null, 2)) - .replace('[TOPIC]', chat.crosstabData.metadata.Topic) - const response = await new Promise((resolve, reject) => { - aiService.sendMessage( - [{ id: 'temp', role: 'user', content: prompt, timestamp: Date.now() }], - { - onChunk: () => {}, // 空的chunk处理函数 - onComplete: (response) => resolve(response), - onError: (error) => reject(error) - } - ) - }) - - try { - const parsedSuggestions = JSON.parse(extractJsonContent(response)) - if (Array.isArray(parsedSuggestions)) { - // 保存候选项到metadata中 - const newMetadata = { - ...chat.crosstabData.metadata!, - VerticalAxisSuggestions: parsedSuggestions - } - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { metadata: newMetadata } - } - }) - - // 更新任务状态为完成 - dispatch({ - type: 'UPDATE_AI_TASK', - payload: { - taskId, - updates: { - status: 'completed', - endTime: Date.now() - } - } - }) - - message.success('纵轴候选项生成完成') - } else { - throw new Error('返回的不是数组格式') - } - } catch (e) { - message.error('解析纵轴候选项失败') - throw e - } - } catch (error) { - console.error('Vertical suggestions generation error:', error) - message.error('纵轴候选项生成失败,请重试') - - // 更新任务状态为失败 - dispatch({ - type: 'UPDATE_AI_TASK', - payload: { - taskId, - updates: { - status: 'failed', - endTime: Date.now(), - error: error instanceof Error ? error.message : 'Unknown error' - } - } - }) - } finally { - setIsGeneratingVerticalSuggestions(false) - } - }, [chat, isGeneratingVerticalSuggestions, getLLMConfig, dispatch, chatId, message]) - - const handleGenerateValueSuggestions = useCallback(async () => { - if (!chat || isGeneratingValueSuggestions) return - - const llmConfig = getLLMConfig() - if (!llmConfig) { - message.error('请先在设置中配置LLM') + // 查找维度 + const allDimensions = [ + ...chat.crosstabData.metadata.horizontalDimensions, + ...chat.crosstabData.metadata.verticalDimensions, + ...chat.crosstabData.metadata.valueDimensions + ] + const dimension = allDimensions.find(d => d.id === dimensionId) + + if (!dimension) { + message.error('找不到指定的维度') + setIsGeneratingDimensionSuggestions(prev => ({ ...prev, [dimensionId]: false })) return } - if (!chat.crosstabData.metadata) { - message.error('请先完成主题设置') - return - } - - setIsGeneratingValueSuggestions(true) - - const taskId = uuidv4() - - // 先创建AI服务实例 - const aiService = createAIService(llmConfig) - - // 创建AI任务监控 const task: AITask = { id: taskId, - requestId: aiService.id, // 使用AI服务的requestId + requestId: aiService.id, type: 'crosstab_cell', status: 'running', - title: '生成值候选项', - description: `为主题 "${chat.crosstabData.metadata.Topic}" 生成值的含义候选项`, + title: '生成维度候选项', + description: `为维度 "${dimension.name}" 生成候选项`, chatId, modelId: llmConfig.id, startTime: Date.now(), context: { crosstab: { horizontalItem: 'suggestions', - verticalItem: 'value', + verticalItem: dimensionId, metadata: chat.crosstabData.metadata } } @@ -1102,16 +928,17 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { }) try { - const prompt = PROMPT_TEMPLATES.valueSuggestions - .replace('[TOPIC]', chat.crosstabData.metadata.Topic) - .replace('[HORIZONTAL_AXIS]', chat.crosstabData.metadata.HorizontalAxis) - .replace('[VERTICAL_AXIS]', chat.crosstabData.metadata.VerticalAxis) - .replace('[CURRENT_VALUE]', chat.crosstabData.metadata.Value) + const prompt = PROMPT_TEMPLATES.dimensionSuggestions + .replace('[METADATA_JSON]', JSON.stringify(chat.crosstabData.metadata, null, 2)) + .replace('[DIMENSION_TYPE]', dimension.id.startsWith('h') ? 'horizontal' : dimension.id.startsWith('v') ? 'vertical' : 'value') + .replace('[DIMENSION_NAME]', dimension.name) + .replace('[DIMENSION_DESCRIPTION]', dimension.description || '') + const response = await new Promise((resolve, reject) => { aiService.sendMessage( [{ id: 'temp', role: 'user', content: prompt, timestamp: Date.now() }], { - onChunk: () => {}, // 空的chunk处理函数 + onChunk: () => {}, onComplete: (response) => resolve(response), onError: (error) => reject(error) } @@ -1121,20 +948,33 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { try { const parsedSuggestions = JSON.parse(extractJsonContent(response)) if (Array.isArray(parsedSuggestions)) { - // 保存候选项到metadata中 - const newMetadata = { - ...chat.crosstabData.metadata!, - ValueSuggestions: parsedSuggestions + // 更新维度的建议 + const metadata = chat.crosstabData.metadata + let updatedMetadata = { ...metadata } + + // 根据维度类型更新对应的维度 + if (dimension.id.startsWith('h')) { + updatedMetadata.horizontalDimensions = updatedMetadata.horizontalDimensions.map(d => + d.id === dimensionId ? { ...d, suggestions: parsedSuggestions } : d + ) + } else if (dimension.id.startsWith('v')) { + updatedMetadata.verticalDimensions = updatedMetadata.verticalDimensions.map(d => + d.id === dimensionId ? { ...d, suggestions: parsedSuggestions } : d + ) + } else { + updatedMetadata.valueDimensions = updatedMetadata.valueDimensions.map(d => + d.id === dimensionId ? { ...d, suggestions: parsedSuggestions } : d + ) } + dispatch({ type: 'UPDATE_CROSSTAB_DATA', payload: { chatId, - data: { metadata: newMetadata } + data: { metadata: updatedMetadata } } }) - // 更新任务状态为完成 dispatch({ type: 'UPDATE_AI_TASK', payload: { @@ -1146,19 +986,18 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) - message.success('值的含义候选项生成完成') + message.success('维度候选项生成完成') } else { throw new Error('返回的不是数组格式') } } catch (e) { - message.error('解析值的含义候选项失败') + message.error('解析维度候选项失败') throw e } } catch (error) { - console.error('Value suggestions generation error:', error) - message.error('值的含义候选项生成失败,请重试') + console.error('Dimension suggestions generation error:', error) + message.error('维度候选项生成失败,请重试') - // 更新任务状态为失败 dispatch({ type: 'UPDATE_AI_TASK', payload: { @@ -1171,63 +1010,9 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) } finally { - setIsGeneratingValueSuggestions(false) + setIsGeneratingDimensionSuggestions(prev => ({ ...prev, [dimensionId]: false })) } - }, [chat, isGeneratingValueSuggestions, getLLMConfig, dispatch, chatId, message]) - - const handleSelectHorizontalSuggestion = useCallback( - (suggestion: string) => { - if (!chat) return - - const newMetadata = { - ...chat.crosstabData.metadata!, - HorizontalAxis: suggestion - // 保留候选项,不清除 - } - - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { - metadata: newMetadata, - // 清空相关数据,因为横轴改变了 - horizontalValues: [], - tableData: {} - } - } - }) - message.success('横轴已更新') - }, - [chat, dispatch, chatId, message] - ) - - const handleSelectVerticalSuggestion = useCallback( - (suggestion: string) => { - if (!chat) return - - const newMetadata = { - ...chat.crosstabData.metadata!, - VerticalAxis: suggestion - // 保留候选项,不清除 - } - - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { - metadata: newMetadata, - // 清空相关数据,因为纵轴改变了 - verticalValues: [], - tableData: {} - } - } - }) - message.success('纵轴已更新') - }, - [chat, dispatch, chatId, message] - ) + }, [chat, isGeneratingDimensionSuggestions, getLLMConfig, dispatch, chatId, message]) const handleSelectTopicSuggestion = useCallback( (suggestion: string) => { @@ -1235,8 +1020,7 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { const newMetadata = { ...chat.crosstabData.metadata!, - Topic: suggestion - // 保留候选项,不清除 + topic: suggestion } dispatch({ @@ -1245,9 +1029,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { chatId, data: { metadata: newMetadata, - // 清空所有数据,因为主题改变了 - horizontalValues: [], - verticalValues: [], tableData: {} } } @@ -1257,32 +1038,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { [chat, dispatch, chatId, message] ) - const handleSelectValueSuggestion = useCallback( - (suggestion: string) => { - if (!chat) return - - const newMetadata = { - ...chat.crosstabData.metadata!, - Value: suggestion - // 保留候选项,不清除 - } - - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { - metadata: newMetadata, - // 清空表格数据,因为值的含义改变了 - tableData: {} - } - } - }) - message.success('值的含义已更新') - }, - [chat, dispatch, chatId, message] - ) - if (!chat) { return
交叉视图聊天不存在
} @@ -1365,19 +1120,12 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { > @@ -1392,14 +1140,9 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { > @@ -1414,8 +1157,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { > - - setIsEditingMetadata(false)} - /> ) } diff --git a/src/renderer/src/components/pages/crosstab/CrosstabTable.tsx b/src/renderer/src/components/pages/crosstab/CrosstabTable.tsx index 825778c..ee83da1 100644 --- a/src/renderer/src/components/pages/crosstab/CrosstabTable.tsx +++ b/src/renderer/src/components/pages/crosstab/CrosstabTable.tsx @@ -1,28 +1,38 @@ import React, { useMemo, useState, useEffect } from 'react' -import { Card, Table, Typography, Button, Tooltip } from 'antd' -import { TableOutlined, FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons' -import { CrosstabMetadata } from '../../../types' -import { generateTableData, generateTableColumns } from './CrosstabUtils' +import { Card, Typography, Button, Tooltip, Space, Tag, Dropdown } from 'antd' +import { + TableOutlined, + FullscreenOutlined, + FullscreenExitOutlined, + PlayCircleOutlined, + LoadingOutlined, + DeleteOutlined, + CommentOutlined +} from '@ant-design/icons' +import { CrosstabMetadata, CrosstabMultiDimensionData } from '../../../types' +import { + generateAxisCombinations, + generateDimensionPath, +} from './CrosstabUtils' +import './crosstab-table.css' const { Text } = Typography interface CrosstabTableProps { metadata: CrosstabMetadata | null - horizontalValues: string[] - verticalValues: string[] - tableData: { [key: string]: { [key: string]: string } } - onGenerateColumn?: (horizontalItem: string) => void + tableData: CrosstabMultiDimensionData + onGenerateColumn?: (columnPath: string) => void isGeneratingColumn?: string | null - onGenerateRow?: (verticalItem: string) => void + onGenerateRow?: (rowPath: string) => void isGeneratingRow?: string | null - onGenerateCell?: (horizontalItem: string, verticalItem: string) => void + onGenerateCell?: (columnPath: string, rowPath: string) => void isGeneratingCell?: string | null - onClearColumn?: (horizontalItem: string) => void - onClearRow?: (verticalItem: string) => void - onClearCell?: (horizontalItem: string, verticalItem: string) => void + onClearColumn?: (columnPath: string) => void + onClearRow?: (rowPath: string) => void + onClearCell?: (columnPath: string, rowPath: string) => void onCreateChatFromCell?: ( - horizontalItem: string, - verticalItem: string, + columnPath: string, + rowPath: string, cellContent: string, metadata: CrosstabMetadata | null ) => void @@ -30,8 +40,6 @@ interface CrosstabTableProps { export default function CrosstabTable({ metadata, - horizontalValues, - verticalValues, tableData, onGenerateColumn, isGeneratingColumn, @@ -45,44 +53,52 @@ export default function CrosstabTable({ onCreateChatFromCell }: CrosstabTableProps) { const [isFullscreen, setIsFullscreen] = useState(false) + const [selectedValueDimension, setSelectedValueDimension] = useState('') - // 生成表格数据 - const data = useMemo(() => { - return generateTableData(verticalValues, horizontalValues, tableData) - }, [verticalValues, horizontalValues, tableData]) - - // 生成表格列 - const columns = useMemo(() => { - return generateTableColumns( - metadata, - horizontalValues, - onGenerateColumn, - isGeneratingColumn, - tableData, - onGenerateRow, - isGeneratingRow, - onGenerateCell, - isGeneratingCell, - onClearColumn, - onClearRow, - onClearCell, - onCreateChatFromCell - ) - }, [ - metadata, - horizontalValues, - onGenerateColumn, - isGeneratingColumn, - tableData, - onGenerateRow, - isGeneratingRow, - onGenerateCell, - isGeneratingCell, - onClearColumn, - onClearRow, - onClearCell, - onCreateChatFromCell - ]) + // 初始化选中的值维度 + useEffect(() => { + if (metadata && metadata.valueDimensions.length > 0) { + if (!selectedValueDimension || !metadata.valueDimensions.find(d => d.id === selectedValueDimension)) { + setSelectedValueDimension(metadata.valueDimensions[0].id) + } + } + }, [metadata, selectedValueDimension]) + + // 生成多维度网格数据 + const { horizontalCombinations, verticalCombinations, gridData } = useMemo(() => { + if (!metadata || !metadata.horizontalDimensions.length || !metadata.verticalDimensions.length) { + return { horizontalCombinations: [], verticalCombinations: [], gridData: {} } + } + + const horizontalCombinations = generateAxisCombinations(metadata.horizontalDimensions) + const verticalCombinations = generateAxisCombinations(metadata.verticalDimensions) + + // 生成网格数据 + const gridData: { [key: string]: string } = {} + + verticalCombinations.forEach((vCombination) => { + const vPath = generateDimensionPath(vCombination) + horizontalCombinations.forEach((hCombination) => { + const hPath = generateDimensionPath(hCombination) + const cellKey = `${hPath}|${vPath}` + const cellData = tableData[cellKey] + + if (cellData && selectedValueDimension) { + gridData[cellKey] = cellData[selectedValueDimension] || '' + } else if (cellData && !selectedValueDimension && metadata.valueDimensions.length > 0) { + const firstValueDimension = metadata.valueDimensions[0].id + gridData[cellKey] = cellData[firstValueDimension] || '' + } else if (!gridData[cellKey] && cellData && Object.keys(cellData).length > 0) { + const firstAvailableKey = Object.keys(cellData)[0] + gridData[cellKey] = cellData[firstAvailableKey] || '' + } else { + gridData[cellKey] = '' + } + }) + }) + + return { horizontalCombinations, verticalCombinations, gridData } + }, [metadata, tableData, selectedValueDimension]) // 处理全屏切换 const toggleFullscreen = () => { @@ -105,40 +121,417 @@ export default function CrosstabTable({ } }, [isFullscreen]) - // 创建extra内容(全屏按钮) - const extraContent = ( - -