From bfd72ee15cb14768b603dc74e6fd9c15f97e377c Mon Sep 17 00:00:00 2001 From: experdot Date: Sun, 13 Jul 2025 15:29:49 +0800 Subject: [PATCH 01/10] feat: Refactor crosstab components to support multi-dimensional data structure, enhancing metadata management and value generation for improved user experience --- .../pages/crosstab/AxisDataManager.tsx | 441 ++++----- .../pages/crosstab/CrosstabChat.tsx | 929 +++++------------- .../pages/crosstab/CrosstabTable.tsx | 431 ++++++-- .../pages/crosstab/CrosstabUtils.ts | 390 +++----- .../pages/crosstab/MetadataDisplay.tsx | 465 ++++----- .../pages/crosstab/MetadataEditor.tsx | 269 ++++- .../components/pages/crosstab/StepFlow.tsx | 754 ++++++++------ src/renderer/src/store/helpers.ts | 33 +- .../src/store/reducers/chatReducer.ts | 53 +- src/renderer/src/types/index.ts | 45 +- 10 files changed, 1918 insertions(+), 1892 deletions(-) diff --git a/src/renderer/src/components/pages/crosstab/AxisDataManager.tsx b/src/renderer/src/components/pages/crosstab/AxisDataManager.tsx index 2b83321..28e88f1 100644 --- a/src/renderer/src/components/pages/crosstab/AxisDataManager.tsx +++ b/src/renderer/src/components/pages/crosstab/AxisDataManager.tsx @@ -1,78 +1,241 @@ import React, { useState } from 'react' -import { Card, Button, Input, Space, Tooltip, Popconfirm, Typography } from 'antd' +import { Card, Button, Input, Space, Tooltip, Popconfirm, Typography, Collapse, Tag, Divider } from 'antd' import { PlusOutlined, EditOutlined, DeleteOutlined, SaveOutlined, UndoOutlined, - ColumnWidthOutlined + ColumnWidthOutlined, + DownOutlined, + UpOutlined } from '@ant-design/icons' -import { CrosstabMetadata } from '../../../types' +import { CrosstabMetadata, CrosstabAxisDimension } from '../../../types' -const { Text } = Typography +const { Text, Title } = Typography +const { Panel } = Collapse 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 } } 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 [editingDimensionId, setEditingDimensionId] = useState(null) + const [editingValueIndex, setEditingValueIndex] = useState<{ dimensionId: string; valueIndex: number } | null>(null) + const [newValueInputs, setNewValueInputs] = useState<{ [dimensionId: string]: string }>({}) - const handleEditHorizontalItem = (index: number, value: string) => { - if (value.trim()) { - onEditHorizontalItem(index, value.trim()) + const handleEditDimensionValue = (dimensionId: string, dimensionType: 'horizontal' | 'vertical', valueIndex: number, newValue: string) => { + const currentDimension = getDimensionById(dimensionId, dimensionType) + if (currentDimension && newValue.trim()) { + const newValues = [...currentDimension.values] + newValues[valueIndex] = newValue.trim() + onUpdateDimension(dimensionId, dimensionType, { values: newValues }) } - setEditingHorizontalIndex(null) + setEditingValueIndex(null) } - const handleEditVerticalItem = (index: number, value: string) => { - if (value.trim()) { - onEditVerticalItem(index, value.trim()) + 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 }) } - setEditingVerticalIndex(null) } - const handleAddHorizontalItem = () => { - if (newHorizontalValue.trim()) { - onAddHorizontalItem(newHorizontalValue.trim()) - setNewHorizontalValue('') - setShowAddHorizontal(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]: '' })) + } } } - const handleAddVerticalItem = () => { - if (newVerticalValue.trim()) { - onAddVerticalItem(newVerticalValue.trim()) - setNewVerticalValue('') - setShowAddVertical(false) + 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 + + if (dimension.values.length === 0 && !isGenerating) { + return ( +
+ 暂无数据 +
+ +
+ ) + } + + return ( +
+ {/* 现有值列表 */} +
+ + {dimension.values.map((value, index) => ( +
+ {editingValueIndex?.dimensionId === dimension.id && editingValueIndex?.valueIndex === index ? ( + handleEditDimensionValue(dimension.id, dimensionType, index, e.currentTarget.value)} + onBlur={(e) => handleEditDimensionValue(dimension.id, dimensionType, index, e.currentTarget.value)} + autoFocus + /> + ) : ( + setEditingValueIndex({ dimensionId: dimension.id, valueIndex: index })} + > + {value} + handleDeleteDimensionValue(dimension.id, dimensionType, index)} + okText="确定" + cancelText="取消" + > + e.stopPropagation()} + /> + + + )} +
+ ))} +
+
+ + {/* 添加新值 */} +
+ 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 (horizontalValues.length === 0 && verticalValues.length === 0) { + if (!metadata) { + return ( + +
+ +
+ 尚未生成多维度轴数据 +
+ 请先完成主题分析,然后生成各维度的数据 +
+
+
+ ) + } + + const hasAnyDimensionData = + metadata.horizontalDimensions.some(dim => dim.values.length > 0) || + metadata.verticalDimensions.some(dim => dim.values.length > 0) + + if (!hasAnyDimensionData) { return (
@@ -80,9 +243,7 @@ export default function AxisDataManager({
尚未生成轴数据
- - 请先完成主题分析,然后使用左侧步骤中的按钮生成横轴和纵轴数据 - + 请使用各维度卡片中的"生成数据"按钮来生成轴数据
@@ -92,190 +253,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..1ba6bd0 100644 --- a/src/renderer/src/components/pages/crosstab/CrosstabChat.tsx +++ b/src/renderer/src/components/pages/crosstab/CrosstabChat.tsx @@ -41,9 +41,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 +60,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 +69,38 @@ 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 - - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { tableData: updatedTableData } - } - }) - - // 更新任务状态为完成 - dispatch({ - type: 'UPDATE_AI_TASK', - payload: { - taskId, - updates: { - status: 'completed', - endTime: Date.now() - } - } - }) - - message.success(`列 "${horizontalItem}" 数据生成完成`) - } catch (e) { - message.error(`解析列 "${horizontalItem}" 的数据失败`) - throw e - } + // 这里可以调用新的多维度单元格生成逻辑 + // 暂时显示成功消息 + message.success(`列 "${columnPath}" 数据生成完成`) } catch (error) { - console.error('Column 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' - } - } - }) + 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 +109,38 @@ 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] - } - }) - - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { tableData: updatedTableData } - } - }) - - // 更新任务状态为完成 - dispatch({ - type: 'UPDATE_AI_TASK', - payload: { - taskId, - updates: { - status: 'completed', - endTime: Date.now() - } - } - }) - - message.success(`行 "${verticalItem}" 数据生成完成`) - } catch (e) { - message.error(`解析行 "${verticalItem}" 的数据失败`) - throw e - } + // 这里可以调用新的多维度单元格生成逻辑 + // 暂时显示成功消息 + message.success(`行 "${rowPath}" 数据生成完成`) } catch (error) { - console.error('Row 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' - } - } - }) + 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 +154,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 +178,29 @@ 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) - // 直接使用响应内容,因为cellValue模板返回的是纯文本 - updatedTableData[horizontalItem][verticalItem] = response.trim() + // 更新表格数据 + const updatedTableData = { ...chat.crosstabData.tableData } + updatedTableData[cellKey] = cellValues dispatch({ type: 'UPDATE_CROSSTAB_DATA', @@ -384,7 +210,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) - // 更新任务状态为完成 dispatch({ type: 'UPDATE_AI_TASK', payload: { @@ -396,12 +221,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: { @@ -533,14 +357,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 @@ -581,123 +397,145 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { setIsEditingMetadata(false) message.success('主题元数据已更新') }, - [dispatch, chatId] + [dispatch, chatId, message] ) - 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 + + 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 updatedMetadata = { + ...metadata, + [dimensionsKey]: updatedDimensions + } dispatch({ type: 'UPDATE_CROSSTAB_DATA', payload: { chatId, - data: { horizontalValues: newValues } + data: { metadata: updatedMetadata } } }) - message.success('横轴项目已更新') + message.success('维度已更新') }, - [chat, dispatch, chatId] + [chat, chatId, dispatch, message] ) - const handleDeleteHorizontalItem = useCallback( - (index: number) => { - const newValues = [...chat!.crosstabData.horizontalValues] - const deletedItem = newValues.splice(index, 1)[0] + const handleGenerateDimensionValues = useCallback( + async (dimensionId: string, dimensionType: 'horizontal' | 'vertical') => { + if (!chat || !chat.crosstabData.metadata) { + message.error('请先完成主题分析') + return + } - // 同时删除对应的表格数据 - const newTableData = { ...chat!.crosstabData.tableData } - delete newTableData[deletedItem] + const llmConfig = getLLMConfig() + if (!llmConfig) { + message.error('请先在设置中配置LLM') + return + } - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { - horizontalValues: newValues, - tableData: newTableData - } + setIsGeneratingDimensionValues(prev => ({ ...prev, [dimensionId]: true })) + + 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('横轴项目已删除') + + 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 || '') + + 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 jsonContent = extractJsonContent(result) + const values = JSON.parse(jsonContent) + + // 更新维度值 + 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 handleAddHorizontalItem = useCallback( - (value: string) => { - const newValues = [...chat!.crosstabData.horizontalValues, value] + // 添加缺少的处理函数 + const handleEditHorizontalItem = useCallback( + (oldItem: string, newItem: string) => { + // 实现编辑横轴项的逻辑 + message.info('编辑横轴项功能需要实现') + }, + [message] + ) - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { horizontalValues: newValues } - } - }) - message.success('横轴项目已添加') + const handleDeleteHorizontalItem = useCallback( + (item: string) => { + // 实现删除横轴项的逻辑 + message.info('删除横轴项功能需要实现') }, - [chat, dispatch, chatId] + [message] ) - const handleEditVerticalItem = useCallback( - (index: number, value: string) => { - const newValues = [...chat!.crosstabData.verticalValues] - newValues[index] = value + const handleAddHorizontalItem = useCallback( + (item: string) => { + // 实现添加横轴项的逻辑 + message.info('添加横轴项功能需要实现') + }, + [message] + ) - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { verticalValues: newValues } - } - }) - message.success('纵轴项目已更新') + const handleEditVerticalItem = useCallback( + (oldItem: string, newItem: string) => { + // 实现编辑纵轴项的逻辑 + message.info('编辑纵轴项功能需要实现') }, - [chat, dispatch, chatId] + [message] ) 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] - }) - - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { - verticalValues: newValues, - tableData: newTableData - } - } - }) - message.success('纵轴项目已删除') + (item: string) => { + // 实现删除纵轴项的逻辑 + message.info('删除纵轴项功能需要实现') }, - [chat, dispatch, chatId] + [message] ) const handleAddVerticalItem = useCallback( - (value: string) => { - const newValues = [...chat!.crosstabData.verticalValues, value] - - dispatch({ - type: 'UPDATE_CROSSTAB_DATA', - payload: { - chatId, - data: { verticalValues: newValues } - } - }) - message.success('纵轴项目已添加') + (item: string) => { + // 实现添加纵轴项的逻辑 + message.info('添加纵轴项功能需要实现') }, - [chat, dispatch, chatId] + [message] ) const handleGenerateTopicSuggestions = useCallback(async () => { @@ -717,18 +555,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 +584,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 +603,7 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { // 保存候选项到metadata中 const newMetadata = { ...chat.crosstabData.metadata!, - TopicSuggestions: parsedSuggestions + topicSuggestions: parsedSuggestions } dispatch({ type: 'UPDATE_CROSSTAB_DATA', @@ -778,7 +613,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { } }) - // 更新任务状态为完成 dispatch({ type: 'UPDATE_AI_TASK', payload: { @@ -802,7 +636,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { console.error('Topic suggestions generation error:', error) message.error('主题候选项生成失败,请重试') - // 更新任务状态为失败 dispatch({ type: 'UPDATE_AI_TASK', payload: { @@ -819,126 +652,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 +666,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') - return - } - - if (!chat.crosstabData.metadata) { - message.error('请先完成主题设置') + // 查找维度 + 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 } - 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 +710,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 +730,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 +768,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 +792,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 +802,7 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { const newMetadata = { ...chat.crosstabData.metadata!, - Topic: suggestion - // 保留候选项,不清除 + topic: suggestion } dispatch({ @@ -1245,9 +811,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { chatId, data: { metadata: newMetadata, - // 清空所有数据,因为主题改变了 - horizontalValues: [], - verticalValues: [], tableData: {} } } @@ -1257,32 +820,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
交叉视图聊天不存在
} @@ -1367,17 +904,10 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { metadata={chat.crosstabData.metadata} onEditMetadata={handleEditMetadata} onGenerateTopicSuggestions={handleGenerateTopicSuggestions} - onGenerateHorizontalSuggestions={handleGenerateHorizontalSuggestions} - onGenerateVerticalSuggestions={handleGenerateVerticalSuggestions} - onGenerateValueSuggestions={handleGenerateValueSuggestions} + onGenerateDimensionSuggestions={handleGenerateDimensionSuggestions} onSelectTopicSuggestion={handleSelectTopicSuggestion} - onSelectHorizontalSuggestion={handleSelectHorizontalSuggestion} - onSelectVerticalSuggestion={handleSelectVerticalSuggestion} - onSelectValueSuggestion={handleSelectValueSuggestion} isGeneratingTopicSuggestions={isGeneratingTopicSuggestions} - isGeneratingHorizontalSuggestions={isGeneratingHorizontalSuggestions} - isGeneratingVerticalSuggestions={isGeneratingVerticalSuggestions} - isGeneratingValueSuggestions={isGeneratingValueSuggestions} + isGeneratingDimensionSuggestions={isGeneratingDimensionSuggestions} /> @@ -1392,14 +922,9 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { > @@ -1414,8 +939,6 @@ export default function CrosstabChat({ chatId }: CrosstabChatProps) { > 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 +41,6 @@ interface CrosstabTableProps { export default function CrosstabTable({ metadata, - horizontalValues, - verticalValues, tableData, onGenerateColumn, isGeneratingColumn, @@ -45,44 +54,295 @@ 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 && !selectedValueDimension) { + setSelectedValueDimension(metadata.valueDimensions[0].id) + } + }, [metadata, selectedValueDimension]) + + // 生成多维度表格数据 + const { dataSource, columns } = useMemo(() => { + if (!metadata || !metadata.horizontalDimensions.length || !metadata.verticalDimensions.length) { + return { dataSource: [], columns: [] } + } + + // 生成所有横轴和纵轴的组合 + const horizontalCombinations = generateAxisCombinations(metadata.horizontalDimensions) + const verticalCombinations = generateAxisCombinations(metadata.verticalDimensions) + + // 生成数据源 + const dataSource = verticalCombinations.map((vCombination, index) => { + const vPath = generateDimensionPath(vCombination) + const row: any = { + key: vPath, + rowPath: vPath, + rowLabels: vCombination + } + + // 为每个横轴组合添加数据 + horizontalCombinations.forEach((hCombination) => { + const hPath = generateDimensionPath(hCombination) + const cellKey = `${hPath}|${vPath}` + const cellData = tableData[cellKey] + + if (cellData && selectedValueDimension) { + row[hPath] = cellData[selectedValueDimension] || '' + } else { + row[hPath] = '' + } + }) + + return row + }) + + // 生成表格列 + const columns: any[] = [] + + // 添加行标题列(纵轴) + metadata.verticalDimensions.forEach((dimension, dimIndex) => { + columns.push({ + title: () => { + // 检查所有行是否有数据 + const hasRowData = Object.keys(tableData).some(cellKey => cellKey.includes('|')) + + // 创建行菜单项 + const rowMenuItems: any[] = [ + { + key: 'generate-all-rows', + icon: React.createElement( + isGeneratingRow ? LoadingOutlined : PlayCircleOutlined + ), + label: hasRowData ? '重新生成所有行' : '生成所有行', + onClick: () => { + verticalCombinations.forEach((vCombination) => { + const vPath = generateDimensionPath(vCombination) + if (onGenerateRow) { + onGenerateRow(vPath) + } + }) + }, + disabled: isGeneratingRow !== null + } + ] + + if (hasRowData && onClearRow) { + rowMenuItems.push({ + key: 'clear-all-rows', + icon: React.createElement(DeleteOutlined), + label: '清除所有行', + onClick: () => { + verticalCombinations.forEach((vCombination) => { + const vPath = generateDimensionPath(vCombination) + if (onClearRow) { + onClearRow(vPath) + } + }) + } + }) + } + + return ( +
+
{dimension.name}
+ {(onGenerateRow || onClearRow) && ( + +
+ + )} +
+ ) + }, + dataIndex: ['rowLabels', dimIndex], + key: `row-${dimension.id}`, + width: 120, + fixed: 'left', + render: (text: string, record: any, index: number) => { + // 检查该行是否已有数据 + const hasData = horizontalCombinations.some((hCombination) => { + const hPath = generateDimensionPath(hCombination) + const cellKey = `${hPath}|${record.rowPath}` + return tableData[cellKey] && Object.keys(tableData[cellKey]).length > 0 + }) + + // 创建菜单项 + const menuItems: any[] = [ + { + key: 'generate', + icon: React.createElement( + isGeneratingRow === record.rowPath ? LoadingOutlined : PlayCircleOutlined + ), + label: hasData ? '重新生成此行' : '生成此行', + onClick: () => onGenerateRow && onGenerateRow(record.rowPath), + disabled: isGeneratingRow !== null + } + ] + + if (hasData && onClearRow) { + menuItems.push({ + key: 'clear', + icon: React.createElement(DeleteOutlined), + label: '清除此行', + onClick: () => onClearRow(record.rowPath) + }) + } + + return ( +
+
{text}
+ {(onGenerateRow || onClearRow) && ( + +
+ + )} +
+ ) + } + }) + }) + + // 添加横轴数据列 + horizontalCombinations.forEach((hCombination) => { + const hPath = generateDimensionPath(hCombination) + + // 检查该列是否已有数据 + const hasColumnData = Object.keys(tableData).some(cellKey => + cellKey.startsWith(hPath + '|') && Object.keys(tableData[cellKey]).length > 0 + ) + + columns.push({ + title: () => { + // 创建菜单项 + const menuItems: any[] = [ + { + key: 'generate', + icon: React.createElement( + isGeneratingColumn === hPath ? LoadingOutlined : PlayCircleOutlined + ), + label: hasColumnData ? '重新生成此列' : '生成此列', + onClick: () => onGenerateColumn && onGenerateColumn(hPath), + disabled: isGeneratingColumn !== null + } + ] + + if (hasColumnData && onClearColumn) { + menuItems.push({ + key: 'clear', + icon: React.createElement(DeleteOutlined), + label: '清除此列', + onClick: () => onClearColumn(hPath) + }) + } + + return ( +
+
+ {hCombination.map((value, index) => ( +
+ {value} +
+ ))} +
+ {(onGenerateColumn || onClearColumn) && ( + +
+ + )} +
+ ) + }, + dataIndex: hPath, + key: hPath, + width: 200, + render: (text: string, record: any) => { + const cellKey = `${hPath}|${record.rowPath}` + const isGenerating = isGeneratingCell === cellKey + + // 创建菜单项 + const menuItems: any[] = [ + { + key: 'generate', + icon: React.createElement(isGenerating ? LoadingOutlined : PlayCircleOutlined), + label: text ? '重新生成' : '生成内容', + onClick: () => onGenerateCell && onGenerateCell(hPath, record.rowPath), + disabled: isGeneratingCell !== null + } + ] + + if (text && onClearCell) { + menuItems.push({ + key: 'clear', + icon: React.createElement(DeleteOutlined), + label: '清除内容', + onClick: () => onClearCell(hPath, record.rowPath) + }) + } + + if (text && onCreateChatFromCell) { + menuItems.push({ + key: 'chat', + icon: React.createElement(CommentOutlined), + label: '创建对话', + onClick: () => onCreateChatFromCell(hPath, record.rowPath, text, metadata) + }) + } + + return ( +
+
{ + if (!isGenerating && onGenerateCell) { + onGenerateCell(hPath, record.rowPath) + } + }} + > + {isGenerating ? ( + 生成中... + ) : text ? ( + {text} + ) : ( + 点击生成 + )} +
+ +
+ +
+ ) + } + }) + }) + + return { dataSource, columns } + }, [metadata, tableData, selectedValueDimension, isGeneratingCell, isGeneratingColumn, isGeneratingRow, onGenerateCell, onGenerateColumn, onGenerateRow, onClearColumn, onClearRow, onClearCell, onCreateChatFromCell]) // 处理全屏切换 const toggleFullscreen = () => { @@ -105,40 +365,69 @@ export default function CrosstabTable({ } }, [isFullscreen]) - // 创建extra内容(全屏按钮) - const extraContent = ( - -