MultiMindChat / components /discussion-utils.ts
samlax12's picture
Upload 19 files
7be508e verified
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 };
}
}
}