refactor: remove duplicate exportConversation module, consolidate into exportChat
This commit is contained in:
@@ -9,7 +9,7 @@ import { useT } from '../hooks/useLocale';
|
||||
import { getLocale, type TranslationKey } from '../lib/i18n';
|
||||
import { useToolCollapse } from '../hooks/useToolCollapse';
|
||||
import { useBookmarks } from '../hooks/useBookmarks';
|
||||
import { exportAsMarkdown, downloadFile } from '../lib/exportConversation';
|
||||
import { messagesToMarkdown, downloadFile } from '../lib/exportChat';
|
||||
|
||||
interface Props {
|
||||
messages: ChatMessage[];
|
||||
@@ -215,7 +215,7 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
const label = sessionKey?.replace(/^agent:[^:]+:/, '') || 'conversation';
|
||||
const md = exportAsMarkdown(messages, label);
|
||||
const md = messagesToMarkdown(messages, label);
|
||||
const safeLabel = label.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 40);
|
||||
downloadFile(md, `${safeLabel}-${new Date().toISOString().slice(0, 10)}.md`);
|
||||
}, [messages, sessionKey]);
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { exportAsMarkdown } from '../exportConversation';
|
||||
import type { ChatMessage } from '../../types';
|
||||
|
||||
function makeMsg(
|
||||
role: 'user' | 'assistant',
|
||||
content: string,
|
||||
blocks: ChatMessage['blocks'] = [],
|
||||
): ChatMessage {
|
||||
return { id: `msg-${Math.random()}`, role, content, timestamp: 1700000000000, blocks };
|
||||
}
|
||||
|
||||
describe('exportAsMarkdown', () => {
|
||||
it('exports a basic user + assistant exchange', () => {
|
||||
const msgs = [
|
||||
makeMsg('user', 'Hello'),
|
||||
makeMsg('assistant', 'Hi there!'),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('# Conversation');
|
||||
expect(md).toContain('### 👤 User');
|
||||
expect(md).toContain('Hello');
|
||||
expect(md).toContain('### 🤖 Assistant');
|
||||
expect(md).toContain('Hi there!');
|
||||
});
|
||||
|
||||
it('uses custom session label as title', () => {
|
||||
const md = exportAsMarkdown([], 'My Session');
|
||||
expect(md).toContain('# My Session');
|
||||
});
|
||||
|
||||
it('renders text blocks', () => {
|
||||
const msgs = [
|
||||
makeMsg('assistant', '', [{ type: 'text', text: 'Block content' }]),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('Block content');
|
||||
});
|
||||
|
||||
it('renders thinking blocks in details tags', () => {
|
||||
const msgs = [
|
||||
makeMsg('assistant', '', [{ type: 'thinking', text: 'Deep thought' }]),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('<details>');
|
||||
expect(md).toContain('💭 Thinking');
|
||||
expect(md).toContain('Deep thought');
|
||||
expect(md).toContain('</details>');
|
||||
});
|
||||
|
||||
it('renders tool_use blocks with JSON', () => {
|
||||
const msgs = [
|
||||
makeMsg('assistant', '', [{ type: 'tool_use', name: 'exec', input: { command: 'ls' }, id: 't1' }]),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('**🔧 Tool: `exec`**');
|
||||
expect(md).toContain('"command": "ls"');
|
||||
});
|
||||
|
||||
it('renders tool_result blocks truncated', () => {
|
||||
const longContent = 'x'.repeat(3000);
|
||||
const msgs = [
|
||||
makeMsg('assistant', '', [{ type: 'tool_result', content: longContent, toolUseId: 't1' }]),
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('**📋 Result:**');
|
||||
expect(md).toContain('...(truncated)');
|
||||
// Should have at most 2000 chars of content + truncation
|
||||
const resultLine = md.split('\n').find(l => l.startsWith('xxx'));
|
||||
expect(resultLine!.length).toBeLessThanOrEqual(2000);
|
||||
});
|
||||
|
||||
it('renders compaction separator', () => {
|
||||
const msgs: ChatMessage[] = [
|
||||
{ id: 'sep', role: 'assistant', content: '', timestamp: 0, blocks: [], isCompactionSeparator: true },
|
||||
];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('---');
|
||||
expect(md).toContain('*Context compacted*');
|
||||
});
|
||||
|
||||
it('falls back to content when no blocks', () => {
|
||||
const msgs = [makeMsg('user', 'Plain text')];
|
||||
const md = exportAsMarkdown(msgs);
|
||||
expect(md).toContain('Plain text');
|
||||
});
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { ChatMessage } from '../types';
|
||||
|
||||
/**
|
||||
* Export conversation messages as a Markdown string.
|
||||
*/
|
||||
export function exportAsMarkdown(messages: ChatMessage[], sessionLabel?: string): string {
|
||||
const lines: string[] = [];
|
||||
const title = sessionLabel || 'Conversation';
|
||||
lines.push(`# ${title}`);
|
||||
lines.push(`> Exported from PinchChat on ${new Date().toLocaleString()}`);
|
||||
lines.push('');
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.isCompactionSeparator) {
|
||||
lines.push('---');
|
||||
lines.push('*Context compacted*');
|
||||
lines.push('---');
|
||||
lines.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
const ts = msg.timestamp ? new Date(msg.timestamp).toLocaleString() : '';
|
||||
const roleLabel = msg.role === 'user' ? '👤 User' : msg.role === 'assistant' ? '🤖 Assistant' : '⚙️ System';
|
||||
const header = ts ? `### ${roleLabel} — ${ts}` : `### ${roleLabel}`;
|
||||
lines.push(header);
|
||||
lines.push('');
|
||||
|
||||
// Render blocks
|
||||
for (const block of msg.blocks) {
|
||||
if (block.type === 'text') {
|
||||
lines.push(block.text);
|
||||
lines.push('');
|
||||
} else if (block.type === 'thinking') {
|
||||
lines.push('<details>');
|
||||
lines.push('<summary>💭 Thinking</summary>');
|
||||
lines.push('');
|
||||
lines.push(block.text);
|
||||
lines.push('</details>');
|
||||
lines.push('');
|
||||
} else if (block.type === 'tool_use') {
|
||||
const input = typeof block.input === 'string' ? block.input : JSON.stringify(block.input, null, 2);
|
||||
lines.push(`**🔧 Tool: \`${block.name}\`**`);
|
||||
lines.push('```json');
|
||||
lines.push(input);
|
||||
lines.push('```');
|
||||
lines.push('');
|
||||
} else if (block.type === 'tool_result') {
|
||||
lines.push('**📋 Result:**');
|
||||
lines.push('```');
|
||||
lines.push(block.content.slice(0, 2000) + (block.content.length > 2000 ? '\n...(truncated)' : ''));
|
||||
lines.push('```');
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to content if no blocks
|
||||
if (msg.blocks.length === 0 && msg.content) {
|
||||
lines.push(msg.content);
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Download text as a file.
|
||||
*/
|
||||
export function downloadFile(content: string, filename: string, mimeType = 'text/markdown') {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
Reference in New Issue
Block a user