Spaces:
Sleeping
Sleeping
| import { DiscussionRecord, DiscussionStats, DiscussionExportData, ExportOptions, ChatMessage, MessagePurpose } from './types'; | |
| export class DiscussionRecordManager { | |
| private static STORAGE_KEY = 'multi-mind-chat-discussion-records'; | |
| private static MAX_RECORDS = 50; // 最多保存50条记录 | |
| // 保存讨论记录到本地存储 | |
| static saveRecord(record: DiscussionRecord): void { | |
| try { | |
| const existingRecords = this.getAllRecords(); | |
| const updatedRecords = [record, ...existingRecords.filter(r => r.id !== record.id)]; | |
| // 保持最大记录数限制 | |
| const trimmedRecords = updatedRecords.slice(0, this.MAX_RECORDS); | |
| localStorage.setItem(this.STORAGE_KEY, JSON.stringify(trimmedRecords)); | |
| } catch (error) { | |
| console.error('保存讨论记录失败:', error); | |
| throw new Error('无法保存讨论记录到本地存储'); | |
| } | |
| } | |
| // 获取所有讨论记录 | |
| static getAllRecords(): DiscussionRecord[] { | |
| try { | |
| const stored = localStorage.getItem(this.STORAGE_KEY); | |
| if (!stored) return []; | |
| const records = JSON.parse(stored); | |
| return records.map((record: any) => ({ | |
| ...record, | |
| timestamp: new Date(record.timestamp), | |
| turns: record.turns.map((turn: any) => ({ | |
| ...turn, | |
| timestamp: new Date(turn.timestamp) | |
| })), | |
| notepadUpdates: record.notepadUpdates.map((update: any) => ({ | |
| ...update, | |
| timestamp: new Date(update.timestamp) | |
| })), | |
| finalAnswer: record.finalAnswer ? { | |
| ...record.finalAnswer, | |
| timestamp: new Date(record.finalAnswer.timestamp) | |
| } : undefined, | |
| interruptedAt: record.interruptedAt ? new Date(record.interruptedAt) : undefined, | |
| metadata: { | |
| ...record.metadata, | |
| exportedAt: record.metadata.exportedAt ? new Date(record.metadata.exportedAt) : undefined | |
| } | |
| })); | |
| } catch (error) { | |
| console.error('加载讨论记录失败:', error); | |
| return []; | |
| } | |
| } | |
| // 根据ID获取单个记录 | |
| static getRecordById(id: string): DiscussionRecord | null { | |
| const records = this.getAllRecords(); | |
| return records.find(record => record.id === id) || null; | |
| } | |
| // 删除指定记录 | |
| static deleteRecord(id: string): void { | |
| try { | |
| const records = this.getAllRecords(); | |
| const filteredRecords = records.filter(record => record.id !== id); | |
| localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredRecords)); | |
| } catch (error) { | |
| console.error('删除讨论记录失败:', error); | |
| throw new Error('无法删除讨论记录'); | |
| } | |
| } | |
| // 清空所有记录 | |
| static clearAllRecords(): void { | |
| try { | |
| localStorage.removeItem(this.STORAGE_KEY); | |
| } catch (error) { | |
| console.error('清空讨论记录失败:', error); | |
| throw new Error('无法清空讨论记录'); | |
| } | |
| } | |
| // 计算讨论统计信息 | |
| static calculateStats(record: DiscussionRecord): DiscussionStats { | |
| const turns = record.turns.filter(turn => turn.durationMs && turn.durationMs > 0); | |
| const responseTimes = turns.map(turn => turn.durationMs!); | |
| const roleParticipation: Record<string, any> = {}; | |
| // 统计每个角色的参与情况 | |
| record.activeRoles.forEach(role => { | |
| const roleTurns = turns.filter(turn => turn.roleId === role.id); | |
| const roleResponseTimes = roleTurns.map(turn => turn.durationMs!); | |
| roleParticipation[role.name] = { | |
| turnCount: roleTurns.length, | |
| totalResponseTime: roleResponseTimes.reduce((sum, time) => sum + time, 0), | |
| averageResponseTime: roleResponseTimes.length > 0 | |
| ? roleResponseTimes.reduce((sum, time) => sum + time, 0) / roleResponseTimes.length | |
| : 0 | |
| }; | |
| }); | |
| return { | |
| totalTurns: turns.length, | |
| averageResponseTime: responseTimes.length > 0 | |
| ? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length | |
| : 0, | |
| longestResponseTime: responseTimes.length > 0 ? Math.max(...responseTimes) : 0, | |
| shortestResponseTime: responseTimes.length > 0 ? Math.min(...responseTimes) : 0, | |
| roleParticipation, | |
| notepadUpdateFrequency: record.notepadUpdates.length / Math.max(turns.length, 1) | |
| }; | |
| } | |
| // 生成完整的讨论文本记录 | |
| static generateTranscript(record: DiscussionRecord, includeMetadata: boolean = true): string { | |
| let transcript = ''; | |
| if (includeMetadata) { | |
| transcript += `=== Multi-Mind Chat 讨论记录 ===\n`; | |
| transcript += `讨论ID: ${record.id}\n`; | |
| transcript += `开始时间: ${record.timestamp.toLocaleString()}\n`; | |
| transcript += `讨论模式: ${record.discussionMode}\n`; | |
| transcript += `参与角色: ${record.activeRoles.map(r => r.name).join(', ')}\n`; | |
| transcript += `总耗时: ${(record.totalDuration / 1000).toFixed(2)}秒\n`; | |
| if (record.wasInterrupted) { | |
| transcript += `状态: 被用户中断 (${record.interruptedAt?.toLocaleString()})\n`; | |
| } else if (record.isCompleted) { | |
| transcript += `状态: 正常完成\n`; | |
| } | |
| transcript += `\n=== 用户查询 ===\n`; | |
| transcript += `${record.userQuery}\n\n`; | |
| } | |
| // 添加讨论过程 | |
| transcript += `=== 讨论过程 ===\n`; | |
| record.turns.forEach((turn, index) => { | |
| const timeStr = turn.timestamp.toLocaleTimeString(); | |
| const durationStr = turn.durationMs ? ` (${(turn.durationMs / 1000).toFixed(2)}s)` : ''; | |
| transcript += `[${timeStr}] ${turn.role}${durationStr}:\n${turn.message}\n\n`; | |
| }); | |
| // 添加记事本更新历史 | |
| if (record.notepadUpdates.length > 0) { | |
| transcript += `=== 记事本更新历史 ===\n`; | |
| record.notepadUpdates.forEach((update, index) => { | |
| const timeStr = update.timestamp.toLocaleTimeString(); | |
| transcript += `[${timeStr}] ${update.updater} 更新了记事本:\n${update.content}\n\n`; | |
| }); | |
| } | |
| // 添加最终答案 | |
| if (record.finalAnswer) { | |
| transcript += `=== 最终答案 ===\n`; | |
| const timeStr = record.finalAnswer.timestamp.toLocaleTimeString(); | |
| const durationStr = record.finalAnswer.durationMs ? ` (${(record.finalAnswer.durationMs / 1000).toFixed(2)}s)` : ''; | |
| transcript += `[${timeStr}] ${record.finalAnswer.provider}${durationStr}:\n${record.finalAnswer.content}\n\n`; | |
| } | |
| return transcript; | |
| } | |
| // 导出讨论记录为指定格式 | |
| static exportRecord(record: DiscussionRecord, options: ExportOptions): DiscussionExportData { | |
| const stats = options.includeStats ? this.calculateStats(record) : {} as DiscussionStats; | |
| const transcript = this.generateTranscript(record, options.includeMetadata); | |
| return { | |
| record: options.includeMetadata ? record : { | |
| ...record, | |
| metadata: { version: record.metadata.version, messageCount: 0, notepadUpdateCount: 0 } | |
| } as DiscussionRecord, | |
| stats, | |
| fullTranscript: transcript, | |
| exportFormat: options.format, | |
| exportedAt: new Date().toISOString(), | |
| version: '1.0' | |
| }; | |
| } | |
| // 生成Markdown格式的导出 | |
| static exportAsMarkdown(record: DiscussionRecord, options: ExportOptions): string { | |
| let markdown = `# Multi-Mind Chat 讨论记录\n\n`; | |
| if (options.includeMetadata) { | |
| markdown += `## 基本信息\n\n`; | |
| markdown += `- **讨论ID**: ${record.id}\n`; | |
| markdown += `- **开始时间**: ${record.timestamp.toLocaleString()}\n`; | |
| markdown += `- **讨论模式**: ${record.discussionMode}\n`; | |
| markdown += `- **参与角色**: ${record.activeRoles.map(r => r.name).join(', ')}\n`; | |
| markdown += `- **总耗时**: ${(record.totalDuration / 1000).toFixed(2)}秒\n`; | |
| if (record.wasInterrupted) { | |
| markdown += `- **状态**: ⚠️ 被用户中断 (${record.interruptedAt?.toLocaleString()})\n`; | |
| } else if (record.isCompleted) { | |
| markdown += `- **状态**: ✅ 正常完成\n`; | |
| } | |
| markdown += `\n`; | |
| } | |
| markdown += `## 用户查询\n\n`; | |
| markdown += `> ${record.userQuery}\n\n`; | |
| if (record.userImage) { | |
| markdown += `*用户还上传了图片: ${record.userImage.name} (${(record.userImage.size / 1024).toFixed(1)} KB)*\n\n`; | |
| } | |
| markdown += `## 讨论过程\n\n`; | |
| record.turns.forEach((turn, index) => { | |
| const timeStr = turn.timestamp.toLocaleTimeString(); | |
| const durationStr = turn.durationMs ? ` *(${(turn.durationMs / 1000).toFixed(2)}s)*` : ''; | |
| markdown += `### ${turn.role} - ${timeStr}${durationStr}\n\n`; | |
| markdown += `${turn.message}\n\n`; | |
| }); | |
| if (record.notepadUpdates.length > 0 && options.includeNotepadHistory) { | |
| markdown += `## 记事本更新历史\n\n`; | |
| record.notepadUpdates.forEach((update, index) => { | |
| const timeStr = update.timestamp.toLocaleTimeString(); | |
| markdown += `### ${update.updater} - ${timeStr}\n\n`; | |
| markdown += `\`\`\`\n${update.content}\n\`\`\`\n\n`; | |
| }); | |
| } | |
| if (record.finalAnswer) { | |
| markdown += `## 最终答案\n\n`; | |
| const timeStr = record.finalAnswer.timestamp.toLocaleTimeString(); | |
| const durationStr = record.finalAnswer.durationMs ? ` *(${(record.finalAnswer.durationMs / 1000).toFixed(2)}s)*` : ''; | |
| markdown += `### ${record.finalAnswer.provider} - ${timeStr}${durationStr}\n\n`; | |
| markdown += `${record.finalAnswer.content}\n\n`; | |
| } | |
| if (options.includeStats) { | |
| const stats = this.calculateStats(record); | |
| markdown += `## 讨论统计\n\n`; | |
| markdown += `- **总轮次**: ${stats.totalTurns}\n`; | |
| markdown += `- **平均响应时间**: ${(stats.averageResponseTime / 1000).toFixed(2)}秒\n`; | |
| markdown += `- **最长响应时间**: ${(stats.longestResponseTime / 1000).toFixed(2)}秒\n`; | |
| markdown += `- **最短响应时间**: ${(stats.shortestResponseTime / 1000).toFixed(2)}秒\n`; | |
| markdown += `- **记事本更新频率**: ${(stats.notepadUpdateFrequency * 100).toFixed(1)}%\n\n`; | |
| markdown += `### 角色参与度\n\n`; | |
| Object.entries(stats.roleParticipation).forEach(([role, data]) => { | |
| markdown += `- **${role}**: ${data.turnCount}轮, 平均${(data.averageResponseTime / 1000).toFixed(2)}秒\n`; | |
| }); | |
| } | |
| markdown += `\n---\n`; | |
| markdown += `*导出时间: ${new Date().toLocaleString()}*\n`; | |
| markdown += `*导出版本: Multi-Mind Chat v1.0*\n`; | |
| return markdown; | |
| } | |
| // 下载文件的工具函数 | |
| static downloadFile(content: string, filename: string, mimeType: string = 'text/plain'): void { | |
| const blob = new Blob([content], { type: mimeType }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = filename; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| URL.revokeObjectURL(url); | |
| } | |
| // 导出并下载讨论记录 | |
| static downloadRecord(record: DiscussionRecord, format: 'json' | 'markdown' | 'txt' = 'json'): void { | |
| const timestamp = record.timestamp.toISOString().split('T')[0]; | |
| const safeQuery = record.userQuery.substring(0, 20).replace(/[^\w\s-]/g, '').trim(); | |
| switch (format) { | |
| case 'json': | |
| const exportData = this.exportRecord(record, { | |
| format: 'json', | |
| includeMetadata: true, | |
| includeStats: true, | |
| includeNotepadHistory: true, | |
| includeSystemMessages: false, | |
| timestampFormat: 'iso', | |
| compressOutput: false | |
| }); | |
| this.downloadFile( | |
| JSON.stringify(exportData, null, 2), | |
| `讨论记录-${timestamp}-${safeQuery}.json`, | |
| 'application/json' | |
| ); | |
| break; | |
| case 'markdown': | |
| const markdownContent = this.exportAsMarkdown(record, { | |
| format: 'markdown', | |
| includeMetadata: true, | |
| includeStats: true, | |
| includeNotepadHistory: true, | |
| includeSystemMessages: false, | |
| timestampFormat: 'local', | |
| compressOutput: false | |
| }); | |
| this.downloadFile( | |
| markdownContent, | |
| `讨论记录-${timestamp}-${safeQuery}.md`, | |
| 'text/markdown' | |
| ); | |
| break; | |
| case 'txt': | |
| const txtContent = this.generateTranscript(record, true); | |
| this.downloadFile( | |
| txtContent, | |
| `讨论记录-${timestamp}-${safeQuery}.txt`, | |
| 'text/plain' | |
| ); | |
| break; | |
| } | |
| } | |
| // 搜索讨论记录 | |
| static searchRecords(query: string, maxResults: number = 10): DiscussionRecord[] { | |
| const records = this.getAllRecords(); | |
| const searchLower = query.toLowerCase(); | |
| return records | |
| .filter(record => | |
| record.userQuery.toLowerCase().includes(searchLower) || | |
| record.turns.some(turn => turn.message.toLowerCase().includes(searchLower)) || | |
| record.finalAnswer?.content.toLowerCase().includes(searchLower) | |
| ) | |
| .slice(0, maxResults); | |
| } | |
| // 获取存储使用情况 | |
| static getStorageInfo(): { used: number; available: number; recordCount: number } { | |
| try { | |
| const records = this.getAllRecords(); | |
| const dataSize = JSON.stringify(records).length; | |
| return { | |
| used: dataSize, | |
| available: 5242880 - dataSize, // 假设5MB存储限制 | |
| recordCount: records.length | |
| }; | |
| } catch (error) { | |
| return { used: 0, available: 0, recordCount: 0 }; | |
| } | |
| } | |
| } |