From d607da691d42a750123098675b33bdb0c6117256 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Fri, 20 Feb 2026 21:02:19 +0000 Subject: [PATCH] refactor: remove duplicate exportConversation module, consolidate into exportChat --- src/components/Chat.tsx | 4 +- src/lib/__tests__/exportConversation.test.ts | 87 -------------------- src/lib/exportConversation.ts | 79 ------------------ 3 files changed, 2 insertions(+), 168 deletions(-) delete mode 100644 src/lib/__tests__/exportConversation.test.ts delete mode 100644 src/lib/exportConversation.ts diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 51227ff..c369652 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -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]); diff --git a/src/lib/__tests__/exportConversation.test.ts b/src/lib/__tests__/exportConversation.test.ts deleted file mode 100644 index 243cbcb..0000000 --- a/src/lib/__tests__/exportConversation.test.ts +++ /dev/null @@ -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('
'); - expect(md).toContain('💭 Thinking'); - expect(md).toContain('Deep thought'); - expect(md).toContain('
'); - }); - - 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'); - }); -}); diff --git a/src/lib/exportConversation.ts b/src/lib/exportConversation.ts deleted file mode 100644 index 1df5a16..0000000 --- a/src/lib/exportConversation.ts +++ /dev/null @@ -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('
'); - lines.push('💭 Thinking'); - lines.push(''); - lines.push(block.text); - lines.push('
'); - 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); -}