From 3ade73a5cd611e9f118e5d341968a3d6382810bc Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 10:53:26 +0800 Subject: [PATCH 01/16] refactor: Update import path for externalChatImporter and remove unused externalChatImporter file to streamline codebase --- .../components/settings/DataManagement.tsx | 2 +- .../src/utils/externalChatImporter.ts | 612 ------------------ .../utils/externalChatImporter/converters.ts | 130 ++++ .../externalChatImporter/formatDetector.ts | 29 + .../utils/externalChatImporter/importer.ts | 226 +++++++ .../src/utils/externalChatImporter/index.ts | 29 + .../src/utils/externalChatImporter/types.ts | 112 ++++ .../src/utils/externalChatImporter/utils.ts | 32 + 8 files changed, 559 insertions(+), 613 deletions(-) delete mode 100644 src/renderer/src/utils/externalChatImporter.ts create mode 100644 src/renderer/src/utils/externalChatImporter/converters.ts create mode 100644 src/renderer/src/utils/externalChatImporter/formatDetector.ts create mode 100644 src/renderer/src/utils/externalChatImporter/importer.ts create mode 100644 src/renderer/src/utils/externalChatImporter/index.ts create mode 100644 src/renderer/src/utils/externalChatImporter/types.ts create mode 100644 src/renderer/src/utils/externalChatImporter/utils.ts diff --git a/src/renderer/src/components/settings/DataManagement.tsx b/src/renderer/src/components/settings/DataManagement.tsx index e60eacd..a0b3c98 100644 --- a/src/renderer/src/components/settings/DataManagement.tsx +++ b/src/renderer/src/components/settings/DataManagement.tsx @@ -30,7 +30,7 @@ import { parseExternalChatHistory, importSelectedChats, SelectableChatItem -} from '../../utils/externalChatImporter' +} from '../../utils/externalChatImporter/index' import { PageFolder } from '../../types/type' const { Text, Paragraph } = Typography diff --git a/src/renderer/src/utils/externalChatImporter.ts b/src/renderer/src/utils/externalChatImporter.ts deleted file mode 100644 index 84ac437..0000000 --- a/src/renderer/src/utils/externalChatImporter.ts +++ /dev/null @@ -1,612 +0,0 @@ -import { Page, ChatMessage, RegularChat } from '../types/type' -import { v4 as uuidv4 } from 'uuid' -import { MessageTree } from '../components/pages/chat/messageTree' - -// 导入限制常量 -const MAX_IMPORT_LIMIT = 50 - -// DeepSeek导出格式的类型定义 -export interface DeepSeekMessage { - id: string - parent: string | null - children: string[] - message: { - files: any[] - search_results: any - model: string - reasoning_content: string | null - content: string - inserted_at: string - } | null -} - -export interface DeepSeekChat { - id: string - title: string - inserted_at: string - updated_at: string - mapping: { [key: string]: DeepSeekMessage } -} - -// OpenAI导出格式的类型定义 -export interface OpenAIMessage { - id: string - message: { - id: string - author: { - role: 'user' | 'assistant' | 'system' - name?: string | null - metadata?: any - } - create_time: number | null - update_time: number | null - content: { - content_type: string - parts: string[] - } - status: string - end_turn?: boolean | null - weight: number - metadata?: any - recipient?: string - channel?: string | null - } | null - parent: string | null - children: string[] -} - -export interface OpenAIChat { - title: string - create_time: number - update_time: number - mapping: { [key: string]: OpenAIMessage } - moderation_results?: any[] - current_node?: string - plugin_ids?: string[] | null - conversation_id: string - conversation_template_id?: string | null - gizmo_id?: string | null - gizmo_type?: string | null - is_archived?: boolean - is_starred?: boolean | null - safe_urls?: string[] - blocked_urls?: string[] - default_model_slug?: string - conversation_origin?: string | null - voice?: string | null - async_status?: string | null - disabled_tool_ids?: string[] - is_do_not_remember?: boolean | null - memory_scope?: string - sugar_item_id?: string | null - id: string -} - -// 支持的聊天格式类型 -export type ChatFormat = 'deepseek' | 'openai' | 'unknown' - -// 导入结果 -export interface ImportResult { - success: boolean - pages: Page[] - folder?: { id: string; name: string } - successCount: number - errorCount: number - message: string -} - -// 可选择的聊天项 -export interface SelectableChatItem { - id: string - title: string - messageCount: number - createTime: number - formatType: ChatFormat - originalData: DeepSeekChat | OpenAIChat -} - -// 解析结果 -export interface ParseResult { - success: boolean - formatType: ChatFormat - pages: SelectableChatItem[] - message: string -} - -/** - * 检测聊天历史的格式类型 - */ -export function detectChatFormat(data: any): ChatFormat { - if (Array.isArray(data)) { - const firstItem = data[0] - if (firstItem && firstItem.mapping && firstItem.title && firstItem.inserted_at) { - return 'deepseek' - } - // 检查OpenAI数组格式 - if ( - firstItem && - firstItem.mapping && - firstItem.title && - firstItem.create_time && - firstItem.conversation_id - ) { - return 'openai' - } - } - - if (data.title && data.mapping && data.create_time) { - return 'openai' - } - - return 'unknown' -} - -/** - * 将OpenAI格式的消息转换为应用内部格式 - */ -export function convertOpenAIMessages(mapping: { [key: string]: OpenAIMessage }): ChatMessage[] { - const messages: ChatMessage[] = [] - const processedIds = new Set() - - // 递归处理消息节点 - const processMessage = (nodeId: string, parentId?: string): void => { - if (processedIds.has(nodeId) || !mapping[nodeId]) return - - const node = mapping[nodeId] - if (!node.message || !node.message.content || !node.message.content.parts) return - - processedIds.add(nodeId) - - // 跳过系统消息和空消息 - if ( - node.message.author.role === 'system' || - node.message.content.parts.join('').trim() === '' - ) { - // 处理子消息 - node.children.forEach((childId) => { - processMessage(childId, parentId) - }) - return - } - - const message: ChatMessage = { - id: node.id, - role: node.message.author.role === 'assistant' ? 'assistant' : 'user', - content: node.message.content.parts.join('\n'), - timestamp: node.message.create_time ? node.message.create_time * 1000 : Date.now(), - parentId: parentId, - children: node.children.length > 0 ? node.children : undefined - } - - messages.push(message) - - // 处理子消息 - node.children.forEach((childId) => { - processMessage(childId, node.id) - }) - } - - // 找到根节点(parent为null的节点) - const rootNodes = Object.values(mapping).filter((node) => node.parent === null) - - rootNodes.forEach((rootNode) => { - rootNode.children.forEach((childId) => { - processMessage(childId) - }) - }) - - return messages -} - -/** - * 将DeepSeek格式的消息树转换为应用内部的消息数组 - */ -export function convertDeepSeekMessages(mapping: { - [key: string]: DeepSeekMessage -}): ChatMessage[] { - const messages: ChatMessage[] = [] - const processedIds = new Set() - - // 递归处理消息节点 - const processMessage = (nodeId: string, parentId?: string): void => { - if (processedIds.has(nodeId) || nodeId === 'root') return - - const node = mapping[nodeId] - if (!node || !node.message) return - - processedIds.add(nodeId) - - // 根据消息内容和模型信息推断角色 - let role: 'user' | 'assistant' = 'user' - - // 如果有模型信息是助手回复 - if (node.message.model) { - role = 'assistant' - } - - // 如果有父消息,根据层级关系判断角色 - if (parentId) { - const parentMessage = messages.find((m) => m.id === parentId) - if (parentMessage) { - role = parentMessage.role === 'user' ? 'assistant' : 'user' - } - } - - // 如果是根节点的直接子节点,通常是用户消息 - const rootNode = mapping['root'] - if (rootNode && rootNode.children.includes(nodeId)) { - role = 'user' - } - - // 转换为应用内部的消息格式 - const message: ChatMessage = { - id: nodeId, - role, - content: node.message.content, - timestamp: new Date(node.message.inserted_at).getTime(), - parentId: parentId, - children: node.children.length > 0 ? node.children : undefined, - modelId: node.message.model, - reasoning_content: node.message.reasoning_content || undefined - } - - messages.push(message) - - // 处理子消息 - node.children.forEach((childId) => { - processMessage(childId, nodeId) - }) - } - - // 从根节点开始处理 - const rootNode = mapping['root'] - if (rootNode && rootNode.children) { - rootNode.children.forEach((childId) => { - processMessage(childId) - }) - } - - return messages -} - -/** - * 处理DeepSeek格式的聊天数据 - */ -function processDeepSeekData( - data: DeepSeekChat[], - folderId?: string -): { pages: Page[]; successCount: number; errorCount: number; skippedCount: number } { - const pages: Page[] = [] - let successCount = 0 - let errorCount = 0 - let skippedCount = 0 - - // 限制处理的数据量 - const limitedData = data.slice(0, MAX_IMPORT_LIMIT) - skippedCount = Math.max(0, data.length - MAX_IMPORT_LIMIT) - - limitedData.forEach((chatData: DeepSeekChat) => { - try { - if (chatData.mapping && chatData.title) { - const messages = convertDeepSeekMessages(chatData.mapping) - - // 使用MessageTree来生成正确的currentPath - const messageTree = new MessageTree(messages) - const currentPath = messageTree.getCurrentPath() - - const chat: Page = { - id: uuidv4(), // 生成新的ID避免冲突 - type: 'regular', - title: chatData.title, - messages, - currentPath, // 设置正确的当前路径 - folderId, // 分配到指定文件夹 - createdAt: new Date(chatData.inserted_at).getTime(), - updatedAt: new Date(chatData.updated_at).getTime() - } - - pages.push(chat) - successCount++ - } - } catch (error) { - console.error('处理单个DeepSeek聊天时出错:', error) - errorCount++ - } - }) - - return { pages, successCount, errorCount, skippedCount } -} - -/** - * 处理OpenAI格式的聊天数据 - */ -function processOpenAIData( - data: OpenAIChat[] | OpenAIChat, - folderId?: string -): { pages: Page[]; successCount: number; errorCount: number; skippedCount: number } { - const pages: Page[] = [] - let successCount = 0 - let errorCount = 0 - let skippedCount = 0 - - // 统一处理为数组格式 - const chatArray = Array.isArray(data) ? data : [data] - - // 限制处理的数据量 - const limitedData = chatArray.slice(0, MAX_IMPORT_LIMIT) - skippedCount = Math.max(0, chatArray.length - MAX_IMPORT_LIMIT) - - limitedData.forEach((chatData: OpenAIChat) => { - try { - if (chatData.mapping && chatData.title) { - const messages = convertOpenAIMessages(chatData.mapping) - - // 使用MessageTree来生成正确的currentPath - const messageTree = new MessageTree(messages) - const currentPath = messageTree.getCurrentPath() - - const chat: Page = { - id: uuidv4(), - type: 'regular', - title: chatData.title, - messages, - currentPath, // 设置正确的当前路径 - folderId, - createdAt: chatData.create_time * 1000, - updatedAt: chatData.update_time * 1000 - } - - pages.push(chat) - successCount++ - } - } catch (error) { - console.error('处理单个OpenAI聊天时出错:', error) - errorCount++ - } - }) - - return { pages, successCount, errorCount, skippedCount } -} - -/** - * 生成导入文件夹名称 - */ -function generateFolderName(formatType: ChatFormat, data: any): string { - const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD格式 - - if (formatType === 'deepseek') { - // 尝试从DeepSeek数据中获取日期 - if (Array.isArray(data) && data.length > 0) { - const firstChat = data[0] - if (firstChat.inserted_at) { - const chatDate = new Date(firstChat.inserted_at).toISOString().split('T')[0] - return `DeepSeek导入-${chatDate}` - } - } - return `DeepSeek导入-${today}` - } else if (formatType === 'openai') { - // 尝试从OpenAI数据中获取日期 - if (Array.isArray(data) && data.length > 0 && data[0].create_time) { - const chatDate = new Date(data[0].create_time * 1000).toISOString().split('T')[0] - return `OpenAI导入-${chatDate}` - } else if (data.create_time) { - const chatDate = new Date(data.create_time * 1000).toISOString().split('T')[0] - return `OpenAI导入-${chatDate}` - } - return `OpenAI导入-${today}` - } else { - return `外部导入-${today}` - } -} - -/** - * 解析外部聊天历史文件,返回可选择的聊天列表 - */ -export function parseExternalChatHistory(jsonContent: string): ParseResult { - try { - const externalData = JSON.parse(jsonContent) - const formatType = detectChatFormat(externalData) - - if (formatType === 'unknown') { - return { - success: false, - formatType: 'unknown', - pages: [], - message: '不支持的文件格式。目前支持DeepSeek和OpenAI导出的聊天历史格式。' - } - } - - const pages: SelectableChatItem[] = [] - - if (formatType === 'deepseek') { - if (Array.isArray(externalData)) { - externalData.forEach((chatData: DeepSeekChat, index) => { - if (chatData.mapping && chatData.title) { - // 计算消息数量 - const messageCount = Object.keys(chatData.mapping).filter( - (key) => key !== 'root' && chatData.mapping[key]?.message - ).length - - pages.push({ - id: `deepseek_${index}_${chatData.id}`, - title: chatData.title, - messageCount, - createTime: new Date(chatData.inserted_at).getTime(), - formatType: 'deepseek', - originalData: chatData - }) - } - }) - } - } else if (formatType === 'openai') { - const chatArray = Array.isArray(externalData) ? externalData : [externalData] - chatArray.forEach((chatData: OpenAIChat, index) => { - if (chatData.mapping && chatData.title) { - // 计算有效消息数量 - const messageCount = Object.values(chatData.mapping).filter( - (node) => - node.message && - node.message.author.role !== 'system' && - node.message.content.parts.join('').trim() !== '' - ).length - - pages.push({ - id: `openai_${index}_${chatData.conversation_id}`, - title: chatData.title, - messageCount, - createTime: chatData.create_time * 1000, - formatType: 'openai', - originalData: chatData - }) - } - }) - } - - return { - success: true, - formatType, - pages, - message: `找到 ${pages.length} 个可导入的聊天记录` - } - } catch (error) { - console.error('解析外部数据失败:', error) - return { - success: false, - formatType: 'unknown', - pages: [], - message: '解析失败,请检查文件格式是否正确' - } - } -} - -/** - * 导入选中的聊天记录 - */ -export function importSelectedChats( - selectedItems: SelectableChatItem[], - folderName?: string -): ImportResult { - if (selectedItems.length === 0) { - return { - success: false, - pages: [], - successCount: 0, - errorCount: 0, - message: '未选择任何聊天记录' - } - } - - const pages: Page[] = [] - let successCount = 0 - let errorCount = 0 - - const folderId = uuidv4() - const finalFolderName = folderName || `外部导入-${new Date().toISOString().split('T')[0]}` - - selectedItems.forEach((item) => { - try { - let messages: ChatMessage[] = [] - - if (item.formatType === 'deepseek') { - const chatData = item.originalData as DeepSeekChat - messages = convertDeepSeekMessages(chatData.mapping) - } else if (item.formatType === 'openai') { - const chatData = item.originalData as OpenAIChat - messages = convertOpenAIMessages(chatData.mapping) - } - - if (messages.length > 0) { - // 使用MessageTree来生成正确的currentPath - const messageTree = new MessageTree(messages) - const currentPath = messageTree.getCurrentPath() - - const chat: RegularChat = { - id: uuidv4(), // 生成新的ID避免冲突 - title: item.title, - type: 'regular', - messages, - currentPath, - folderId, - createdAt: item.createTime, - updatedAt: item.createTime - } - - pages.push(chat) - successCount++ - } - } catch (error) { - console.error('处理单个聊天时出错:', error) - errorCount++ - } - }) - - if (pages.length > 0) { - let message = `导入成功!共导入 ${successCount} 个聊天记录到文件夹"${finalFolderName}"` - if (errorCount > 0) { - message += `,失败 ${errorCount} 个` - } - - return { - success: true, - pages, - folder: { id: folderId, name: finalFolderName }, - successCount, - errorCount, - message - } - } else { - return { - success: false, - pages: [], - successCount: 0, - errorCount: errorCount || 1, - message: '未找到有效的聊天记录' - } - } -} - -/** - * 导入外部聊天历史的主要函数 (保持向后兼容) - */ -export function importExternalChatHistory(jsonContent: string): ImportResult { - try { - const parseResult = parseExternalChatHistory(jsonContent) - - if (!parseResult.success) { - return { - success: false, - pages: [], - successCount: 0, - errorCount: 1, - message: parseResult.message - } - } - - // 限制为50条记录 - const limitedChats = parseResult.pages.slice(0, MAX_IMPORT_LIMIT) - const skippedCount = Math.max(0, parseResult.pages.length - MAX_IMPORT_LIMIT) - - // 生成文件夹名称 - const folderName = generateFolderName( - parseResult.formatType, - parseResult.pages[0]?.originalData - ) - - const result = importSelectedChats(limitedChats, folderName) - - if (result.success && skippedCount > 0) { - result.message += `(跳过 ${skippedCount} 个,已达到50个限制)` - } - - return result - } catch (error) { - console.error('导入外部数据失败:', error) - return { - success: false, - pages: [], - successCount: 0, - errorCount: 1, - message: '导入失败,请检查文件格式是否正确' - } - } -} diff --git a/src/renderer/src/utils/externalChatImporter/converters.ts b/src/renderer/src/utils/externalChatImporter/converters.ts new file mode 100644 index 0000000..95076c0 --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/converters.ts @@ -0,0 +1,130 @@ +import { ChatMessage } from '../../types/type' +import { OpenAIMessage, DeepSeekMessage } from './types' + +/** + * 将OpenAI格式的消息转换为应用内部格式 + */ +export function convertOpenAIMessages(mapping: { [key: string]: OpenAIMessage }): ChatMessage[] { + const messages: ChatMessage[] = [] + const processedIds = new Set() + + // 递归处理消息节点 + const processMessage = (nodeId: string, parentId?: string): void => { + if (processedIds.has(nodeId) || !mapping[nodeId]) return + + const node = mapping[nodeId] + if (!node.message || !node.message.content || !node.message.content.parts) return + + processedIds.add(nodeId) + + // 跳过系统消息和空消息 + if ( + node.message.author.role === 'system' || + node.message.content.parts.join('').trim() === '' + ) { + // 处理子消息 + node.children.forEach((childId) => { + processMessage(childId, parentId) + }) + return + } + + const message: ChatMessage = { + id: node.id, + role: node.message.author.role === 'assistant' ? 'assistant' : 'user', + content: node.message.content.parts.join('\n'), + timestamp: node.message.create_time ? node.message.create_time * 1000 : Date.now(), + parentId: parentId, + children: node.children.length > 0 ? node.children : undefined + } + + messages.push(message) + + // 处理子消息 + node.children.forEach((childId) => { + processMessage(childId, node.id) + }) + } + + // 找到根节点(parent为null的节点) + const rootNodes = Object.values(mapping).filter((node) => node.parent === null) + + rootNodes.forEach((rootNode) => { + rootNode.children.forEach((childId) => { + processMessage(childId) + }) + }) + + return messages +} + +/** + * 将DeepSeek格式的消息树转换为应用内部的消息数组 + */ +export function convertDeepSeekMessages(mapping: { + [key: string]: DeepSeekMessage +}): ChatMessage[] { + const messages: ChatMessage[] = [] + const processedIds = new Set() + + // 递归处理消息节点 + const processMessage = (nodeId: string, parentId?: string): void => { + if (processedIds.has(nodeId) || nodeId === 'root') return + + const node = mapping[nodeId] + if (!node || !node.message) return + + processedIds.add(nodeId) + + // 根据消息内容和模型信息推断角色 + let role: 'user' | 'assistant' = 'user' + + // 如果有模型信息是助手回复 + if (node.message.model) { + role = 'assistant' + } + + // 如果有父消息,根据层级关系判断角色 + if (parentId) { + const parentMessage = messages.find((m) => m.id === parentId) + if (parentMessage) { + role = parentMessage.role === 'user' ? 'assistant' : 'user' + } + } + + // 如果是根节点的直接子节点,通常是用户消息 + const rootNode = mapping['root'] + if (rootNode && rootNode.children.includes(nodeId)) { + role = 'user' + } + + // 转换为应用内部的消息格式 + const message: ChatMessage = { + id: nodeId, + role, + content: node.message.content, + timestamp: new Date(node.message.inserted_at).getTime(), + parentId: parentId, + children: node.children.length > 0 ? node.children : undefined, + modelId: node.message.model, + reasoning_content: node.message.reasoning_content || undefined + } + + messages.push(message) + + // 处理子消息 + node.children.forEach((childId) => { + processMessage(childId, nodeId) + }) + } + + // 从根节点开始处理 + const rootNode = mapping['root'] + if (rootNode && rootNode.children) { + rootNode.children.forEach((childId) => { + processMessage(childId) + }) + } + + return messages +} diff --git a/src/renderer/src/utils/externalChatImporter/formatDetector.ts b/src/renderer/src/utils/externalChatImporter/formatDetector.ts new file mode 100644 index 0000000..76225bc --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/formatDetector.ts @@ -0,0 +1,29 @@ +import { ChatFormat } from './types' + +/** + * 检测聊天历史的格式类型 + */ +export function detectChatFormat(data: any): ChatFormat { + if (Array.isArray(data)) { + const firstItem = data[0] + if (firstItem && firstItem.mapping && firstItem.title && firstItem.inserted_at) { + return 'deepseek' + } + // 检查OpenAI数组格式 + if ( + firstItem && + firstItem.mapping && + firstItem.title && + firstItem.create_time && + firstItem.conversation_id + ) { + return 'openai' + } + } + + if (data.title && data.mapping && data.create_time) { + return 'openai' + } + + return 'unknown' +} diff --git a/src/renderer/src/utils/externalChatImporter/importer.ts b/src/renderer/src/utils/externalChatImporter/importer.ts new file mode 100644 index 0000000..d804b80 --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/importer.ts @@ -0,0 +1,226 @@ +import { v4 as uuidv4 } from 'uuid' +import { Page, ChatMessage, RegularChat } from '../../types/type' +import { MessageTree } from '../../components/pages/chat/messageTree' +import { + MAX_IMPORT_LIMIT, + SelectableChatItem, + ParseResult, + ImportResult, + DeepSeekChat, + OpenAIChat +} from './types' +import { detectChatFormat } from './formatDetector' +import { convertDeepSeekMessages, convertOpenAIMessages } from './converters' +import { generateFolderName } from './utils' + +/** + * 解析外部聊天历史文件,返回可选择的聊天列表 + */ +export function parseExternalChatHistory(jsonContent: string): ParseResult { + try { + const externalData = JSON.parse(jsonContent) + const formatType = detectChatFormat(externalData) + + if (formatType === 'unknown') { + return { + success: false, + formatType: 'unknown', + pages: [], + message: '不支持的文件格式。目前支持DeepSeek和OpenAI导出的聊天历史格式。' + } + } + + const pages: SelectableChatItem[] = [] + + if (formatType === 'deepseek') { + if (Array.isArray(externalData)) { + externalData.forEach((chatData: DeepSeekChat, index) => { + if (chatData.mapping && chatData.title) { + // 计算消息数量 + const messageCount = Object.keys(chatData.mapping).filter( + (key) => key !== 'root' && chatData.mapping[key]?.message + ).length + + pages.push({ + id: `deepseek_${index}_${chatData.id}`, + title: chatData.title, + messageCount, + createTime: new Date(chatData.inserted_at).getTime(), + formatType: 'deepseek', + originalData: chatData + }) + } + }) + } + } else if (formatType === 'openai') { + const chatArray = Array.isArray(externalData) ? externalData : [externalData] + chatArray.forEach((chatData: OpenAIChat, index) => { + if (chatData.mapping && chatData.title) { + // 计算有效消息数量 + const messageCount = Object.values(chatData.mapping).filter( + (node) => + node.message && + node.message.author.role !== 'system' && + node.message.content.parts.join('').trim() !== '' + ).length + + pages.push({ + id: `openai_${index}_${chatData.conversation_id}`, + title: chatData.title, + messageCount, + createTime: chatData.create_time * 1000, + formatType: 'openai', + originalData: chatData + }) + } + }) + } + + return { + success: true, + formatType, + pages, + message: `找到 ${pages.length} 个可导入的聊天记录` + } + } catch (error) { + console.error('解析外部数据失败:', error) + return { + success: false, + formatType: 'unknown', + pages: [], + message: '解析失败,请检查文件格式是否正确' + } + } +} + +/** + * 导入选中的聊天记录 + */ +export function importSelectedChats( + selectedItems: SelectableChatItem[], + folderName?: string +): ImportResult { + if (selectedItems.length === 0) { + return { + success: false, + pages: [], + successCount: 0, + errorCount: 0, + message: '未选择任何聊天记录' + } + } + + const pages: Page[] = [] + let successCount = 0 + let errorCount = 0 + + const folderId = uuidv4() + const finalFolderName = folderName || `外部导入-${new Date().toISOString().split('T')[0]}` + + selectedItems.forEach((item) => { + try { + let messages: ChatMessage[] = [] + + if (item.formatType === 'deepseek') { + const chatData = item.originalData as DeepSeekChat + messages = convertDeepSeekMessages(chatData.mapping) + } else if (item.formatType === 'openai') { + const chatData = item.originalData as OpenAIChat + messages = convertOpenAIMessages(chatData.mapping) + } + + if (messages.length > 0) { + // 使用MessageTree来生成正确的currentPath + const messageTree = new MessageTree(messages) + const currentPath = messageTree.getCurrentPath() + + const chat: RegularChat = { + id: uuidv4(), // 生成新的ID避免冲突 + title: item.title, + type: 'regular', + messages, + currentPath, + folderId, + createdAt: item.createTime, + updatedAt: item.createTime + } + + pages.push(chat) + successCount++ + } + } catch (error) { + console.error('处理单个聊天时出错:', error) + errorCount++ + } + }) + + if (pages.length > 0) { + let message = `导入成功!共导入 ${successCount} 个聊天记录到文件夹"${finalFolderName}"` + if (errorCount > 0) { + message += `,失败 ${errorCount} 个` + } + + return { + success: true, + pages, + folder: { id: folderId, name: finalFolderName }, + successCount, + errorCount, + message + } + } else { + return { + success: false, + pages: [], + successCount: 0, + errorCount: errorCount || 1, + message: '未找到有效的聊天记录' + } + } +} + +/** + * 导入外部聊天历史的主要函数 (保持向后兼容) + */ +export function importExternalChatHistory(jsonContent: string): ImportResult { + try { + const parseResult = parseExternalChatHistory(jsonContent) + + if (!parseResult.success) { + return { + success: false, + pages: [], + successCount: 0, + errorCount: 1, + message: parseResult.message + } + } + + // 限制为50条记录 + const limitedChats = parseResult.pages.slice(0, MAX_IMPORT_LIMIT) + const skippedCount = Math.max(0, parseResult.pages.length - MAX_IMPORT_LIMIT) + + // 生成文件夹名称 + const folderName = generateFolderName( + parseResult.formatType, + parseResult.pages[0]?.originalData + ) + + const result = importSelectedChats(limitedChats, folderName) + + if (result.success && skippedCount > 0) { + result.message += `(跳过 ${skippedCount} 个,已达到50个限制)` + } + + return result + } catch (error) { + console.error('导入外部数据失败:', error) + return { + success: false, + pages: [], + successCount: 0, + errorCount: 1, + message: '导入失败,请检查文件格式是否正确' + } + } +} diff --git a/src/renderer/src/utils/externalChatImporter/index.ts b/src/renderer/src/utils/externalChatImporter/index.ts new file mode 100644 index 0000000..29b3f2f --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/index.ts @@ -0,0 +1,29 @@ +// 导出类型定义 +export type { + DeepSeekMessage, + DeepSeekChat, + OpenAIMessage, + OpenAIChat, + ChatFormat, + ImportResult, + SelectableChatItem, + ParseResult +} from './types' + +export { MAX_IMPORT_LIMIT } from './types' + +// 导出格式检测 +export { detectChatFormat } from './formatDetector' + +// 导出消息转换器 +export { convertOpenAIMessages, convertDeepSeekMessages } from './converters' + +// 导出工具函数 +export { generateFolderName } from './utils' + +// 导出核心导入功能 +export { + parseExternalChatHistory, + importSelectedChats, + importExternalChatHistory +} from './importer' diff --git a/src/renderer/src/utils/externalChatImporter/types.ts b/src/renderer/src/utils/externalChatImporter/types.ts new file mode 100644 index 0000000..e62bda8 --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/types.ts @@ -0,0 +1,112 @@ +import { Page } from '../../types/type' + +// 导入限制常量 +export const MAX_IMPORT_LIMIT = 50 + +// DeepSeek导出格式的类型定义 +export interface DeepSeekMessage { + id: string + parent: string | null + children: string[] + message: { + files: any[] + search_results: any + model: string + reasoning_content: string | null + content: string + inserted_at: string + } | null +} + +export interface DeepSeekChat { + id: string + title: string + inserted_at: string + updated_at: string + mapping: { [key: string]: DeepSeekMessage } +} + +// OpenAI导出格式的类型定义 +export interface OpenAIMessage { + id: string + message: { + id: string + author: { + role: 'user' | 'assistant' | 'system' + name?: string | null + metadata?: any + } + create_time: number | null + update_time: number | null + content: { + content_type: string + parts: string[] + } + status: string + end_turn?: boolean | null + weight: number + metadata?: any + recipient?: string + channel?: string | null + } | null + parent: string | null + children: string[] +} + +export interface OpenAIChat { + title: string + create_time: number + update_time: number + mapping: { [key: string]: OpenAIMessage } + moderation_results?: any[] + current_node?: string + plugin_ids?: string[] | null + conversation_id: string + conversation_template_id?: string | null + gizmo_id?: string | null + gizmo_type?: string | null + is_archived?: boolean + is_starred?: boolean | null + safe_urls?: string[] + blocked_urls?: string[] + default_model_slug?: string + conversation_origin?: string | null + voice?: string | null + async_status?: string | null + disabled_tool_ids?: string[] + is_do_not_remember?: boolean | null + memory_scope?: string + sugar_item_id?: string | null + id: string +} + +// 支持的聊天格式类型 +export type ChatFormat = 'deepseek' | 'openai' | 'unknown' + +// 导入结果 +export interface ImportResult { + success: boolean + pages: Page[] + folder?: { id: string; name: string } + successCount: number + errorCount: number + message: string +} + +// 可选择的聊天项 +export interface SelectableChatItem { + id: string + title: string + messageCount: number + createTime: number + formatType: ChatFormat + originalData: DeepSeekChat | OpenAIChat +} + +// 解析结果 +export interface ParseResult { + success: boolean + formatType: ChatFormat + pages: SelectableChatItem[] + message: string +} diff --git a/src/renderer/src/utils/externalChatImporter/utils.ts b/src/renderer/src/utils/externalChatImporter/utils.ts new file mode 100644 index 0000000..689184b --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/utils.ts @@ -0,0 +1,32 @@ +import { ChatFormat } from './types' + +/** + * 生成导入文件夹名称 + */ +export function generateFolderName(formatType: ChatFormat, data: any): string { + const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD格式 + + if (formatType === 'deepseek') { + // 尝试从DeepSeek数据中获取日期 + if (Array.isArray(data) && data.length > 0) { + const firstChat = data[0] + if (firstChat.inserted_at) { + const chatDate = new Date(firstChat.inserted_at).toISOString().split('T')[0] + return `DeepSeek导入-${chatDate}` + } + } + return `DeepSeek导入-${today}` + } else if (formatType === 'openai') { + // 尝试从OpenAI数据中获取日期 + if (Array.isArray(data) && data.length > 0 && data[0].create_time) { + const chatDate = new Date(data[0].create_time * 1000).toISOString().split('T')[0] + return `OpenAI导入-${chatDate}` + } else if (data.create_time) { + const chatDate = new Date(data.create_time * 1000).toISOString().split('T')[0] + return `OpenAI导入-${chatDate}` + } + return `OpenAI导入-${today}` + } else { + return `外部导入-${today}` + } +} From bccae07948b08655dcc49578e4c169dc5d885d06 Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 11:09:42 +0800 Subject: [PATCH 02/16] refactor: Remove settings component from layout and sidebar, integrate settings page handling in activity bar and tabs area for improved navigation --- src/renderer/src/components/layout/Layout.tsx | 9 +---- .../layout/activitybar/ActivityBar.tsx | 29 +++++++++++---- .../src/components/layout/sidebar/Sidebar.tsx | 12 +----- .../sidebar_items/chat/ChatHistoryTree.tsx | 4 +- .../src/components/layout/tabs/TabsArea.tsx | 22 +++++++---- .../src/components/pages/chat/ChatWindow.tsx | 10 ++--- .../pages/settings/SettingsPage.tsx | 14 +++++++ src/renderer/src/stores/pagesStore.ts | 37 +++++++++++++++++++ src/renderer/src/stores/searchStore.ts | 3 +- src/renderer/src/types/type.ts | 9 ++++- 10 files changed, 106 insertions(+), 43 deletions(-) create mode 100644 src/renderer/src/components/pages/settings/SettingsPage.tsx diff --git a/src/renderer/src/components/layout/Layout.tsx b/src/renderer/src/components/layout/Layout.tsx index da3ee0a..c071085 100644 --- a/src/renderer/src/components/layout/Layout.tsx +++ b/src/renderer/src/components/layout/Layout.tsx @@ -6,7 +6,6 @@ import Sidebar from './sidebar/Sidebar' import ActivityBar, { ActivityBarTab } from './activitybar/ActivityBar' import TabsArea from './tabs/TabsArea' import ResizeHandle from './ResizeHandle' -import Settings from '../settings/Settings' import GlobalSearch from './sidebar_items/search/GlobalSearch' import TitleBar from './titlebar/TitleBar' @@ -17,7 +16,6 @@ export default function Layout() { const { clearSearch } = useSearchStore() const [activeTab, setActiveTab] = useState('explore') const [searchOpen, setSearchOpen] = useState(false) - const [settingsOpen, setSettingsOpen] = useState(false) // 处理键盘快捷键 useEffect(() => { @@ -59,10 +57,6 @@ export default function Layout() { setSearchOpen(true) } - const handleSettingsOpen = () => { - setSettingsOpen(true) - } - return (
{/* 自定义标题栏 */} @@ -91,7 +85,7 @@ export default function Layout() { collapsed={sidebarCollapsed} activeTab={activeTab} onSearchOpen={handleSearchOpen} - onSettingsOpen={handleSettingsOpen} + onSettingsOpen={handleSearchOpen} /> @@ -103,7 +97,6 @@ export default function Layout() { {/* 全局模态框 */} - setSettingsOpen(false)} />
) } diff --git a/src/renderer/src/components/layout/activitybar/ActivityBar.tsx b/src/renderer/src/components/layout/activitybar/ActivityBar.tsx index 614a3d6..4f4e5d4 100644 --- a/src/renderer/src/components/layout/activitybar/ActivityBar.tsx +++ b/src/renderer/src/components/layout/activitybar/ActivityBar.tsx @@ -2,8 +2,10 @@ import React from 'react' import { Button, Badge, Tooltip } from 'antd' import { FolderOutlined, SearchOutlined, MonitorOutlined, SettingOutlined } from '@ant-design/icons' import { useAITasksStore } from '../../../stores/aiTasksStore' +import { usePagesStore } from '../../../stores/pagesStore' +import { useTabsStore } from '../../../stores/tabsStore' -export type ActivityBarTab = 'explore' | 'search' | 'tasks' | 'settings' +export type ActivityBarTab = 'explore' | 'search' | 'tasks' interface ActivityBarProps { activeTab: ActivityBarTab @@ -12,10 +14,18 @@ interface ActivityBarProps { export default function ActivityBar({ activeTab, onTabChange }: ActivityBarProps) { const { getRunningTasksCount } = useAITasksStore() + const { createAndOpenSettingsPage } = usePagesStore() + const { openTab } = useTabsStore() // 计算活跃任务数量 const activeTaskCount = getRunningTasksCount() + // 处理设置按钮点击 + const handleSettingsClick = () => { + const settingsPageId = createAndOpenSettingsPage() + openTab(settingsPageId) + } + const items = [ { key: 'explore' as ActivityBarTab, @@ -35,12 +45,6 @@ export default function ActivityBar({ activeTab, onTabChange }: ActivityBarProps label: '任务监控', tooltip: '任务监控 - AI任务状态', badge: activeTaskCount > 0 ? activeTaskCount : undefined - }, - { - key: 'settings' as ActivityBarTab, - icon: , - label: '设置', - tooltip: '设置 - 应用程序设置' } ] @@ -59,6 +63,17 @@ export default function ActivityBar({ activeTab, onTabChange }: ActivityBarProps ))} + + {/* 设置按钮独立处理 */} + + - - - )} - - ) - if (!open) return null - if (embedded) { - return
{settingsContent}
- } - return ( - - - 应用设置 - - } - open={open} - onCancel={onClose} - width={700} - footer={[ - , - , - - ]} +
- {settingsContent} - +
+ + + +
) } From 973174a67571bad46659f5276c70150a2c1c940e Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 11:24:16 +0800 Subject: [PATCH 05/16] refactor: Reinstate UpdateSettings tab in Settings component and add application information section to enhance user awareness and navigation --- .../src/components/settings/Settings.tsx | 10 ++--- .../components/settings/UpdateSettings.tsx | 45 +++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index 9117dee..2444050 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -44,16 +44,16 @@ export default function Settings({ label: '提示词列表', children: }, - { - key: 'update', - label: '应用更新', - children: - }, { key: 'data', label: '数据管理', children: }, + { + key: 'update', + label: '应用更新', + children: + }, { key: 'debug', label: '持久化状态', diff --git a/src/renderer/src/components/settings/UpdateSettings.tsx b/src/renderer/src/components/settings/UpdateSettings.tsx index 6411cd3..0c498f4 100644 --- a/src/renderer/src/components/settings/UpdateSettings.tsx +++ b/src/renderer/src/components/settings/UpdateSettings.tsx @@ -309,6 +309,51 @@ export default function UpdateSettings() { + + {/* 关于信息 */} + + + 关于应用 + + +
+ 应用名称 + Pointer - AI聊天助手 +
+ +
+ 当前版本 + {currentVersion || '获取中...'} +
+ +
+ 构建框架 + Electron + React + TypeScript +
+ +
+ 许可证 + MIT 开源许可证 +
+ +
+ 项目地址 + +
+ + + 一个探索性的AI聊天应用,提供智能对话、交叉表分析、对象管理等功能, + 致力于提供更好的AI交互体验。 + +
+
) } From 1eeb0891c7de8335d30f00c4270df8378a0bb239 Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 11:27:16 +0800 Subject: [PATCH 06/16] feat: Enhance update handling by introducing startup check state management in UpdateNotification and UpdateSettings components --- .../components/common/UpdateNotification.tsx | 18 ++++++++++++++++++ .../src/components/settings/UpdateSettings.tsx | 2 ++ src/renderer/src/stores/updateStore.ts | 13 ++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/common/UpdateNotification.tsx b/src/renderer/src/components/common/UpdateNotification.tsx index 9b2c05b..e6ccddd 100644 --- a/src/renderer/src/components/common/UpdateNotification.tsx +++ b/src/renderer/src/components/common/UpdateNotification.tsx @@ -49,6 +49,8 @@ export default function UpdateNotification() { const handleUpdateAvailable = (info: UpdateInfo) => { console.log('收到更新可用事件:', info) updateStore.handleUpdateAvailable(info) + // 重置启动检查标识(有更新可用时需要显示通知) + updateStore.setIsStartupCheck(false) showUpdateAvailableNotification(info) } @@ -61,6 +63,8 @@ export default function UpdateNotification() { const handleUpdateError = (error: string) => { console.error('收到更新错误事件:', error) updateStore.handleUpdateError(error) + // 重置启动检查标识 + updateStore.setIsStartupCheck(false) // 静默处理错误,避免过多通知打扰用户 console.warn('Update check failed:', error) } @@ -68,6 +72,15 @@ export default function UpdateNotification() { const handleUpdateNotAvailable = (info: any) => { console.log('收到无更新事件:', info) updateStore.handleUpdateNotAvailable(info) + + // 如果是启动检查且已是最新版本,不显示通知 + if (updateStore.isStartupCheck) { + console.log('启动检查:已是最新版本,不显示通知') + updateStore.setIsStartupCheck(false) // 重置标识 + return + } + + // 手动检查时显示通知 notification.info({ message: '当前已是最新版本', description: `当前版本: ${info.version || '未知'}`, @@ -114,12 +127,17 @@ export default function UpdateNotification() { return } + // 标记为启动检查 + updateStore.setIsStartupCheck(true) + const result = await window.api.updater.checkForUpdates() console.log('启动检查更新结果:', result) } catch (error) { console.error('启动检查更新失败:', error) // 静默失败,不打扰用户 console.warn('Startup update check failed:', error) + // 重置启动检查标识 + updateStore.setIsStartupCheck(false) } } diff --git a/src/renderer/src/components/settings/UpdateSettings.tsx b/src/renderer/src/components/settings/UpdateSettings.tsx index 0c498f4..8a7aae1 100644 --- a/src/renderer/src/components/settings/UpdateSettings.tsx +++ b/src/renderer/src/components/settings/UpdateSettings.tsx @@ -89,6 +89,8 @@ export default function UpdateSettings() { try { updateStore.setCheckingForUpdates(true) updateStore.setError(null) + // 确保手动检查时不是启动检查 + updateStore.setIsStartupCheck(false) console.log('开始检查更新...') diff --git a/src/renderer/src/stores/updateStore.ts b/src/renderer/src/stores/updateStore.ts index 98d97b9..6528ddf 100644 --- a/src/renderer/src/stores/updateStore.ts +++ b/src/renderer/src/stores/updateStore.ts @@ -33,6 +33,9 @@ export interface UpdateState { notificationShown: boolean downloadNotificationKey: string | null downloadNotificationHidden: boolean + + // 检查类型标识(区分启动检查和手动检查) + isStartupCheck: boolean } export interface UpdateActions { @@ -44,6 +47,7 @@ export interface UpdateActions { setUpdateDownloaded: (downloaded: boolean) => void setError: (error: string | null) => void setAutoCheckEnabled: (enabled: boolean) => void + setIsStartupCheck: (isStartup: boolean) => void // 更新信息管理 setUpdateInfo: (info: UpdateInfo | null) => void @@ -75,7 +79,8 @@ const initialState: UpdateState = { downloadProgress: null, notificationShown: false, downloadNotificationKey: null, - downloadNotificationHidden: false + downloadNotificationHidden: false, + isStartupCheck: false } export const useUpdateStore = create()( @@ -136,6 +141,11 @@ export const useUpdateStore = create()( state.autoCheckEnabled = enabled }), + setIsStartupCheck: (isStartup) => + set((state) => { + state.isStartupCheck = isStartup + }), + // 更新信息管理 setUpdateInfo: (info) => set((state) => { @@ -176,6 +186,7 @@ export const useUpdateStore = create()( state.notificationShown = false state.downloadNotificationKey = null state.downloadNotificationHidden = false + state.isStartupCheck = false }), handleUpdateAvailable: (info) => From 1d0be9bde3f1b527ae0a63d41e658d747faef061 Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 11:44:18 +0800 Subject: [PATCH 07/16] refactor: Adjust padding in SettingsPage component for improved layout consistency --- .../src/components/pages/settings/SettingsPage.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/pages/settings/SettingsPage.tsx b/src/renderer/src/components/pages/settings/SettingsPage.tsx index b9e7435..db43522 100644 --- a/src/renderer/src/components/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/components/pages/settings/SettingsPage.tsx @@ -11,13 +11,8 @@ export default function SettingsPage({ defaultActiveTab = 'appearance' }: SettingsPageProps) { return ( -
- {}} - embedded={true} - defaultActiveTab={defaultActiveTab} - /> +
+ {}} defaultActiveTab={defaultActiveTab} />
) } From 3a7e8d507108b779083020fafad519822d1889b5 Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 11:44:27 +0800 Subject: [PATCH 08/16] refactor: Update ChatInput component to remove unnecessary disabled condition and enhance ChatWindow input handling with immediate clear and focus adjustment --- src/renderer/src/components/pages/chat/ChatInput.tsx | 2 +- src/renderer/src/components/pages/chat/ChatWindow.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/pages/chat/ChatInput.tsx b/src/renderer/src/components/pages/chat/ChatInput.tsx index 3a9848b..7d81f59 100644 --- a/src/renderer/src/components/pages/chat/ChatInput.tsx +++ b/src/renderer/src/components/pages/chat/ChatInput.tsx @@ -275,7 +275,7 @@ const ChatInput = forwardRef( onChange={(e) => onChange(e.target.value)} onKeyDown={handleKeyDown} autoSize={{ minRows: 1, maxRows: 10 }} - disabled={disabled || hasNoModels} + disabled={hasNoModels} /> diff --git a/src/renderer/src/components/pages/chat/ChatWindow.tsx b/src/renderer/src/components/pages/chat/ChatWindow.tsx index c3c7ff0..aeb9311 100644 --- a/src/renderer/src/components/pages/chat/ChatWindow.tsx +++ b/src/renderer/src/components/pages/chat/ChatWindow.tsx @@ -240,8 +240,12 @@ const ChatWindow = forwardRef(({ chatId }, ref) onChange={setInputValue} onSend={async () => { if (inputValue.trim()) { + setInputValue('') // 立即清空输入框 await onSendMessage(inputValue) - setInputValue('') + // 在下一个tick中聚焦,确保界面更新完成 + setTimeout(() => { + chatInputRef.current?.focus() + }, 0) } }} onStop={onStopGeneration} From 7920d1045e1c63489b3faf1ee65407124c6e6e5b Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 11:52:51 +0800 Subject: [PATCH 09/16] refactor: Remove disabled condition from delete button in MessageItem component to streamline functionality --- src/renderer/src/components/pages/chat/MessageItem.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/src/components/pages/chat/MessageItem.tsx b/src/renderer/src/components/pages/chat/MessageItem.tsx index b8319e9..cdbf7f4 100644 --- a/src/renderer/src/components/pages/chat/MessageItem.tsx +++ b/src/renderer/src/components/pages/chat/MessageItem.tsx @@ -425,7 +425,6 @@ export default function MessageItem({ size="small" icon={} onClick={handleDelete} - disabled={isCurrentlyStreaming} className="message-delete-btn" /> From 67497d9da579902864c708823a79d897049dfbd7 Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 12:15:06 +0800 Subject: [PATCH 10/16] feat: Implement comprehensive data reset functionality in DataManagement component, including memory state clearance and IndexedDB data removal --- .../components/settings/DataManagement.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/settings/DataManagement.tsx b/src/renderer/src/components/settings/DataManagement.tsx index a0b3c98..df32d28 100644 --- a/src/renderer/src/components/settings/DataManagement.tsx +++ b/src/renderer/src/components/settings/DataManagement.tsx @@ -24,6 +24,9 @@ import { } from '@ant-design/icons' import { useSettingsStore } from '../../stores/settingsStore' import { usePagesStore } from '../../stores/pagesStore' +import { clearAllStores } from '../../stores/useAppStores' +import { clearStoreState } from '../../stores/persistence/storeConfig' +import { useMessagesStore } from '../../stores/messagesStore' import { importExternalChatHistory, @@ -210,7 +213,7 @@ export default function DataManagement() { } updatedFolders = [newFolder] } - + // 保存到存储并更新状态 usePagesStore.getState().importPages([...result.pages]) usePagesStore.getState().importFolders([...updatedFolders]) @@ -290,10 +293,26 @@ export default function DataManagement() { okText: '确定重置', okType: 'danger', cancelText: '取消', - onOk: () => { + onOk: async () => { try { - // 清除localStorage中的所有数据 - usePagesStore.getState().clearAllPages() + // 第一步:清除内存中的所有存储状态 + clearAllStores() + + // 第二步:清除 messagesStore 中的流式消息状态 + useMessagesStore.setState({ streamingMessages: {} }) + + // 第三步:清除 IndexedDB 中的所有持久化数据 + await Promise.all([ + clearStoreState('settings-store'), + clearStoreState('messages-store'), + clearStoreState('search-store'), + clearStoreState('tabs-store'), + clearStoreState('ui-store'), + clearStoreState('object-store'), + clearStoreState('crosstab-store'), + clearStoreState('ai-tasks-store') + // pages 和 folders 已经在 clearAllPages() 中处理了 + ]) message.success('数据已重置') From f0fc424d4594e7cb665fdf98b8cd694d3b94b280 Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 12:26:02 +0800 Subject: [PATCH 11/16] feat: Enhance DataManagement component with new export/import functionalities for settings and chat history, including improved data handling and reset options --- .../components/settings/DataManagement.tsx | 284 +++++++++++++++--- 1 file changed, 240 insertions(+), 44 deletions(-) diff --git a/src/renderer/src/components/settings/DataManagement.tsx b/src/renderer/src/components/settings/DataManagement.tsx index df32d28..4c6681a 100644 --- a/src/renderer/src/components/settings/DataManagement.tsx +++ b/src/renderer/src/components/settings/DataManagement.tsx @@ -40,7 +40,7 @@ const { Text, Paragraph } = Typography export default function DataManagement() { const { importSettings, exportSettings } = useSettingsStore() - const { pages, folders, importPages, clearAllPages } = usePagesStore() + const { pages, folders, importPages, importFolders, clearAllPages } = usePagesStore() const [importing, setImporting] = useState(false) const [importingExternal, setImportingExternal] = useState(false) const [selectiveImportModal, setSelectiveImportModal] = useState(false) @@ -49,26 +49,95 @@ export default function DataManagement() { const [customFolderName, setCustomFolderName] = useState('') const { modal, message } = App.useApp() - const handleExport = () => { + // 生成精确到秒的时间戳用于文件名 + const getTimestamp = () => { + const now = new Date() + return now.toISOString().replace(/[:.]/g, '-').slice(0, -5) // 移除毫秒和Z,替换冒号和点号 + } + + // 导出所有数据 + const handleExportAll = () => { try { - const data = exportSettings() - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) + const settings = exportSettings() + const allData = { + type: 'all-data', + settings, + pages, + folders, + version: '1.0.0', + exportTime: Date.now() + } + const blob = new Blob([JSON.stringify(allData, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url - link.download = `ai-chat-settings-${new Date().toISOString().split('T')[0]}.json` + link.download = `ai-chat-all-data-${getTimestamp()}.json` document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) - message.success('数据导出成功') + message.success('所有数据导出成功') } catch (error) { message.error('导出失败') } } + // 单独导出设置 + const handleExportSettings = () => { + try { + const settings = exportSettings() + const settingsData = { + type: 'settings-only', + settings, + version: '1.0.0', + exportTime: Date.now() + } + const blob = new Blob([JSON.stringify(settingsData, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + + link.href = url + link.download = `ai-chat-settings-${getTimestamp()}.json` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + + message.success('设置数据导出成功') + } catch (error) { + message.error('设置导出失败') + } + } + + // 单独导出聊天记录 + const handleExportChats = () => { + try { + const chatsData = { + type: 'chats-only', + pages, + folders, + version: '1.0.0', + exportTime: Date.now() + } + const blob = new Blob([JSON.stringify(chatsData, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + + link.href = url + link.download = `ai-chat-history-${getTimestamp()}.json` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + + message.success('聊天记录导出成功') + } catch (error) { + message.error('聊天记录导出失败') + } + } + const handleImport = (file: File) => { setImporting(true) @@ -77,8 +146,58 @@ export default function DataManagement() { try { const content = e.target?.result as string const parsedData = JSON.parse(content) - importSettings(parsedData) - message.success('数据导入成功') + + // 根据文件类型进行不同的处理 + if (parsedData.type === 'all-data') { + // 完整数据导出 + importSettings(parsedData.settings) + + if (parsedData.pages && parsedData.pages.length > 0) { + importPages(parsedData.pages) + } + + if (parsedData.folders && parsedData.folders.length > 0) { + importFolders(parsedData.folders) + } + + message.success('所有数据导入成功(包含设置和聊天历史)') + } else if (parsedData.type === 'settings-only') { + // 只有设置数据 + importSettings(parsedData.settings) + message.success('设置数据导入成功') + } else if (parsedData.type === 'chats-only') { + // 只有聊天记录数据 + if (parsedData.pages && parsedData.pages.length > 0) { + importPages(parsedData.pages) + } + + if (parsedData.folders && parsedData.folders.length > 0) { + importFolders(parsedData.folders) + } + + message.success('聊天记录导入成功') + } else if ( + parsedData.settings && + parsedData.pages !== undefined && + parsedData.folders !== undefined + ) { + // 兼容旧版本的完整数据格式(没有type字段) + importSettings(parsedData.settings) + + if (parsedData.pages && parsedData.pages.length > 0) { + importPages(parsedData.pages) + } + + if (parsedData.folders && parsedData.folders.length > 0) { + importFolders(parsedData.folders) + } + + message.success('数据导入成功(包含设置和聊天历史)') + } else { + // 假设是旧格式的纯设置数据 + importSettings(parsedData) + message.success('设置数据导入成功') + } } catch (error) { message.error('导入失败,文件格式错误') } finally { @@ -285,11 +404,12 @@ export default function DataManagement() { } } - const handleReset = () => { + // 重置所有数据 + const handleResetAll = () => { modal.confirm({ - title: '确认重置', + title: '确认重置所有数据', icon: , - content: '这将清除所有设置和数据,此操作不可恢复。确定要继续吗?', + content: '这将清除所有设置和聊天数据,此操作不可恢复。确定要继续吗?', okText: '确定重置', okType: 'danger', cancelText: '取消', @@ -314,10 +434,7 @@ export default function DataManagement() { // pages 和 folders 已经在 clearAllPages() 中处理了 ]) - message.success('数据已重置') - - // 立即重新加载页面以确保状态完全重置 - window.location.reload() + message.success('所有数据已重置') } catch (error) { console.error('重置失败:', error) message.error('重置失败') @@ -326,10 +443,88 @@ export default function DataManagement() { }) } + // 单独清空聊天记录 + const handleResetChats = () => { + modal.confirm({ + title: '确认清空聊天记录', + icon: , + content: '这将清除所有聊天历史和文件夹,但保留您的设置配置。此操作不可恢复,确定要继续吗?', + okText: '确定清空', + okType: 'danger', + cancelText: '取消', + onOk: async () => { + try { + // 清除聊天相关的存储 + clearAllPages() + + // 清除相关的持久化数据 + await Promise.all([ + clearStoreState('messages-store'), + clearStoreState('search-store'), + clearStoreState('tabs-store'), + clearStoreState('ui-store') + ]) + + // 重置消息存储的流式消息状态 + useMessagesStore.setState({ streamingMessages: {} }) + + message.success('聊天记录已清空') + } catch (error) { + console.error('清空聊天记录失败:', error) + message.error('清空聊天记录失败') + } + } + }) + } + + // 单独重置设置 + const handleResetSettings = () => { + modal.confirm({ + title: '确认重置设置', + icon: , + content: + '这将重置所有LLM配置、模型配置和应用设置,但保留您的聊天记录。此操作不可恢复,确定要继续吗?', + okText: '确定重置', + okType: 'danger', + cancelText: '取消', + onOk: async () => { + try { + // 重置设置存储 + const { resetSettings } = useSettingsStore.getState() + resetSettings() + + // 清除设置相关的持久化数据 + await clearStoreState('settings-store') + + message.success('设置已重置') + } catch (error) { + console.error('重置设置失败:', error) + message.error('重置设置失败') + } + } + }) + } + const getCurrentDataSize = () => { try { - const data = exportSettings() - return `${(JSON.stringify(data).length / 1024).toFixed(2)} KB` + const settings = exportSettings() + const allData = { + type: 'all-data', + settings, + pages, + folders, + version: '1.0.0', + exportTime: Date.now() + } + const settingsSize = ( + JSON.stringify({ type: 'settings-only', settings, version: '1.0.0' }).length / 1024 + ).toFixed(2) + const chatsSize = ( + JSON.stringify({ type: 'chats-only', pages, folders, version: '1.0.0' }).length / 1024 + ).toFixed(2) + const totalSize = (JSON.stringify(allData).length / 1024).toFixed(2) + + return `总计: ${totalSize} KB (设置: ${settingsSize} KB, 聊天: ${chatsSize} KB)` } catch { return '计算中...' } @@ -353,18 +548,20 @@ export default function DataManagement() {
导出数据
- + + + + +
- 导出包括设置、LLM配置、聊天记录等所有数据 + 您可以选择导出所有数据,或者分别导出设置和聊天记录
@@ -393,7 +590,7 @@ export default function DataManagement() {
- 导入数据将覆盖当前所有设置,请确保备份重要数据 + 支持导入完整数据文件、设置文件或聊天记录文件,系统会自动识别文件类型进行相应处理
@@ -404,6 +601,11 @@ export default function DataManagement() { 导入外部聊天历史 + + + - - -
@@ -434,21 +631,20 @@ export default function DataManagement() {
重置数据
- - - + + +
- 将应用恢复到初始状态,清除所有用户数据 + 您可以选择重置所有数据,或者分别清空聊天记录、重置设置配置
From 0ae2772b43939b89b0539f3538f1a64160927fbd Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 13:26:34 +0800 Subject: [PATCH 12/16] refactor: Clean up DataManagement component by removing success message notifications after data export actions for a more streamlined user experience --- src/renderer/src/components/settings/DataManagement.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/renderer/src/components/settings/DataManagement.tsx b/src/renderer/src/components/settings/DataManagement.tsx index 4c6681a..d59b98e 100644 --- a/src/renderer/src/components/settings/DataManagement.tsx +++ b/src/renderer/src/components/settings/DataManagement.tsx @@ -7,7 +7,6 @@ import { Upload, Typography, Divider, - Popconfirm, App, Table, Checkbox, @@ -77,8 +76,6 @@ export default function DataManagement() { link.click() document.body.removeChild(link) URL.revokeObjectURL(url) - - message.success('所有数据导出成功') } catch (error) { message.error('导出失败') } @@ -104,8 +101,6 @@ export default function DataManagement() { link.click() document.body.removeChild(link) URL.revokeObjectURL(url) - - message.success('设置数据导出成功') } catch (error) { message.error('设置导出失败') } @@ -131,8 +126,6 @@ export default function DataManagement() { link.click() document.body.removeChild(link) URL.revokeObjectURL(url) - - message.success('聊天记录导出成功') } catch (error) { message.error('聊天记录导出失败') } @@ -601,7 +594,7 @@ export default function DataManagement() { 导入外部聊天历史 - + From 656a5e5e12b75544440415cae5b81c46aea5e467 Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 13:44:00 +0800 Subject: [PATCH 13/16] style: Update chat component styles to improve message alignment during editing and adjust line height for message previews --- .../src/components/pages/chat/MessageItem.tsx | 2 +- src/renderer/src/components/pages/chat/chat.css | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/pages/chat/MessageItem.tsx b/src/renderer/src/components/pages/chat/MessageItem.tsx index cdbf7f4..b78dff6 100644 --- a/src/renderer/src/components/pages/chat/MessageItem.tsx +++ b/src/renderer/src/components/pages/chat/MessageItem.tsx @@ -204,7 +204,7 @@ export default function MessageItem({ }} /> -
+
{message.role === 'user' ? '您' : 'AI助手'} diff --git a/src/renderer/src/components/pages/chat/chat.css b/src/renderer/src/components/pages/chat/chat.css index 7eb3b80..be9200d 100644 --- a/src/renderer/src/components/pages/chat/chat.css +++ b/src/renderer/src/components/pages/chat/chat.css @@ -135,11 +135,14 @@ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); } - .user-message .message-content { align-items: flex-end; } +.user-message .message-content.message-content-editing { + align-items: initial; +} + .user-message .message-card { background: #e6f7ff; border-color: #91d5ff; @@ -149,6 +152,10 @@ align-items: flex-start; } +.assistant-message .message-content.message-content-editing { + align-items: initial; +} + .assistant-message .message-card { background: #fff; border-color: #d9d9d9; @@ -323,7 +330,7 @@ .message-preview-text { font-style: italic; - line-height: 1.4; + line-height: 1.5; word-break: break-word; } From 332c69f7f81702f1ae8f86592dcb2636fbd70698 Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 13:45:48 +0800 Subject: [PATCH 14/16] refactor: Update MessageItem component to prioritize editing state in collapsible message content display and adjust preview visibility accordingly --- src/renderer/src/components/pages/chat/MessageItem.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/pages/chat/MessageItem.tsx b/src/renderer/src/components/pages/chat/MessageItem.tsx index b78dff6..5bad578 100644 --- a/src/renderer/src/components/pages/chat/MessageItem.tsx +++ b/src/renderer/src/components/pages/chat/MessageItem.tsx @@ -249,8 +249,8 @@ export default function MessageItem({
- {/* 消息内容区域 - 可折叠 */} - {!isCollapsed && ( + {/* 消息内容区域 - 可折叠,但编辑状态优先级最高 */} + {(!isCollapsed || isEditing) && ( <> {/* 推理模型思考过程展示 */} {currentReasoningContent && ( @@ -349,8 +349,8 @@ export default function MessageItem({ )} - {/* 折叠状态下的预览 */} - {isCollapsed && ( + {/* 折叠状态下的预览 - 编辑状态时不显示 */} + {isCollapsed && !isEditing && (
From 4d34c80a74a8bbf6dadc4487c0f2cc9bd1d09dfe Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 13:57:07 +0800 Subject: [PATCH 15/16] style: Adjust ActivityBar width in Layout component and import CSS for styling enhancements --- src/renderer/src/components/layout/Layout.tsx | 2 +- .../src/components/layout/activitybar/ActivityBar.tsx | 1 + .../src/components/layout/activitybar/activitybar.css | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/components/layout/activitybar/activitybar.css diff --git a/src/renderer/src/components/layout/Layout.tsx b/src/renderer/src/components/layout/Layout.tsx index c071085..6f6734d 100644 --- a/src/renderer/src/components/layout/Layout.tsx +++ b/src/renderer/src/components/layout/Layout.tsx @@ -68,7 +68,7 @@ export default function Layout() { {/* ActivityBar */} - + diff --git a/src/renderer/src/components/layout/activitybar/ActivityBar.tsx b/src/renderer/src/components/layout/activitybar/ActivityBar.tsx index d331332..93e80ea 100644 --- a/src/renderer/src/components/layout/activitybar/ActivityBar.tsx +++ b/src/renderer/src/components/layout/activitybar/ActivityBar.tsx @@ -4,6 +4,7 @@ import { FolderOutlined, SearchOutlined, MonitorOutlined, SettingOutlined } from import { useAITasksStore } from '../../../stores/aiTasksStore' import { usePagesStore } from '../../../stores/pagesStore' import { useTabsStore } from '../../../stores/tabsStore' +import './activitybar.css' export type ActivityBarTab = 'explore' | 'search' | 'tasks' diff --git a/src/renderer/src/components/layout/activitybar/activitybar.css b/src/renderer/src/components/layout/activitybar/activitybar.css new file mode 100644 index 0000000..7ca2e58 --- /dev/null +++ b/src/renderer/src/components/layout/activitybar/activitybar.css @@ -0,0 +1,9 @@ +.activity-bar { + display: flex; + flex-direction: column; + align-items: center; + padding: 16px 8px; + background: #fafafa; + border-right: 1px solid #f0f0f0; + gap: 4px; +} From 852c1dabef06b944fd6a17c03a1f7c07ed0e4785 Mon Sep 17 00:00:00 2001 From: experdotxie Date: Fri, 1 Aug 2025 16:26:36 +0800 Subject: [PATCH 16/16] style: Change axis data container layout from row to column in crosstab-page.css and update LLMSettings form to destroy on close --- .../src/components/pages/crosstab/crosstab-page.css | 4 ++-- src/renderer/src/components/settings/LLMSettings.tsx | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/pages/crosstab/crosstab-page.css b/src/renderer/src/components/pages/crosstab/crosstab-page.css index c8b909c..71729a1 100644 --- a/src/renderer/src/components/pages/crosstab/crosstab-page.css +++ b/src/renderer/src/components/pages/crosstab/crosstab-page.css @@ -239,7 +239,7 @@ /* 轴数据容器样式 */ .axis-data-container { display: flex; - flex-direction: row; + flex-direction: column; gap: 24px; height: 100%; } @@ -459,7 +459,7 @@ } .axis-data-container { - flex-direction: row; + flex-direction: column; gap: 16px; } diff --git a/src/renderer/src/components/settings/LLMSettings.tsx b/src/renderer/src/components/settings/LLMSettings.tsx index 6748f31..9670583 100644 --- a/src/renderer/src/components/settings/LLMSettings.tsx +++ b/src/renderer/src/components/settings/LLMSettings.tsx @@ -116,7 +116,10 @@ function LLMConfigForm({ open, config, onSave, onCancel }: LLMConfigFormProps) { { + form.resetFields() + onCancel() + }} footer={[