feat: export conversation as Markdown file

Add a download button in the header that exports the current session's
messages as a well-formatted Markdown file. Includes:
- User/assistant/system event labels with timestamps
- Thinking blocks as collapsible <details>
- Tool calls with JSON parameters
- Tool results as collapsible sections
- Image placeholders
- Session label and export date in header

i18n: EN + FR translations for the export tooltip.
This commit is contained in:
Nicolas Varrot
2026-02-12 18:38:54 +00:00
parent d6449773c3
commit 8d4b606482
4 changed files with 109 additions and 4 deletions

View File

@@ -1,8 +1,10 @@
import { Menu, Sparkles, LogOut, Volume2, VolumeOff, Cpu, Bot } from 'lucide-react';
import type { ConnectionStatus, Session } from '../types';
import { useCallback } from 'react';
import { Menu, Sparkles, LogOut, Volume2, VolumeOff, Cpu, Bot, Download } from 'lucide-react';
import type { ConnectionStatus, Session, ChatMessage } from '../types';
import { useT } from '../hooks/useLocale';
import { LanguageSelector } from './LanguageSelector';
import { sessionDisplayName } from '../lib/sessionName';
import { messagesToMarkdown, downloadFile } from '../lib/exportChat';
interface Props {
status: ConnectionStatus;
@@ -12,12 +14,21 @@ interface Props {
onLogout?: () => void;
soundEnabled?: boolean;
onToggleSound?: () => void;
messages?: ChatMessage[];
}
export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound }: Props) {
export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound, messages }: Props) {
const t = useT();
const sessionLabel = activeSessionData ? sessionDisplayName(activeSessionData) : (sessionKey.split(':').pop() || sessionKey);
const handleExport = useCallback(() => {
if (!messages || messages.length === 0) return;
const md = messagesToMarkdown(messages, sessionLabel);
const safeLabel = sessionLabel.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50);
const date = new Date().toISOString().slice(0, 10);
downloadFile(md, `${safeLabel}_${date}.md`);
}, [messages, sessionLabel]);
return (
<>
<header className="h-14 border-b border-white/8 bg-[#232329]/90 backdrop-blur-xl flex items-center px-4 gap-3 shrink-0" role="banner">
@@ -54,6 +65,16 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
{soundEnabled ? <Volume2 size={16} /> : <VolumeOff size={16} />}
</button>
)}
{messages && messages.length > 0 && (
<button
onClick={handleExport}
aria-label={t('header.export')}
className="p-2 rounded-2xl hover:bg-white/5 text-zinc-500 hover:text-zinc-300 transition-colors"
title={t('header.export')}
>
<Download size={16} />
</button>
)}
<LanguageSelector />
{status === 'connected' ? (
<div className="flex items-center gap-2 rounded-2xl border border-white/8 bg-zinc-800/30 px-3 py-1.5">