import { Clock, Edit, Share, Trash, FileText, FileDown } from 'lucide-react'; import { Message } from './ChatWindow'; import { useEffect, useState, Fragment } from 'react'; import { formatTimeDifference, formatRelativeTime, formatDate, } from '@/lib/utils'; import { useLocale, useTranslations } from 'next-intl'; import DeleteChat from './DeleteChat'; import { Popover, PopoverButton, PopoverPanel, Transition, } from '@headlessui/react'; import jsPDF from 'jspdf'; import { ensureNotoSansTC } from '@/lib/pdfFont'; type ExportLabels = { chatExportTitle: (p: { title: string }) => string; exportedOn: string; user: string; assistant: string; citations: string; }; const downloadFile = (filename: string, content: string, type: string) => { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0); }; const exportAsMarkdown = ( messages: Message[], title: string, labels: ExportLabels, locale: string, ) => { const date = formatDate(messages[0]?.createdAt || Date.now(), locale); let md = `# 💬 ${labels.chatExportTitle({ title })}\n\n`; md += `*${labels.exportedOn} ${date}*\n\n---\n`; messages.forEach((msg) => { md += `\n---\n`; md += `**${msg.role === 'user' ? `🧑 ${labels.user}` : `🤖 ${labels.assistant}`}** \n`; md += `*${formatDate(msg.createdAt, locale)}*\n\n`; md += `> ${msg.content.replace(/\n/g, '\n> ')}\n`; if (msg.sources && msg.sources.length > 0) { md += `\n**${labels.citations}**\n`; msg.sources.forEach((src: any, i: number) => { const url = src.metadata?.url || ''; md += `- [${i + 1}] [${url}](${url})\n`; }); } }); md += '\n---\n'; downloadFile(`${title || 'chat'}.md`, md, 'text/markdown'); }; const exportAsPDF = async ( messages: Message[], title: string, labels: ExportLabels, locale: string, ) => { const doc = new jsPDF(); // Ensure CJK-capable font is available, then set fonts try { await ensureNotoSansTC(doc); doc.setFont('NotoSansTC', 'normal'); } catch (e) { // If network fails, fallback to default font (may garble CJK) } const date = formatDate(messages[0]?.createdAt || Date.now(), locale); let y = 15; const pageHeight = doc.internal.pageSize.height; doc.setFontSize(18); doc.text(labels.chatExportTitle({ title }), 10, y); y += 8; doc.setFontSize(11); doc.setTextColor(100); doc.text(`${labels.exportedOn} ${date}`, 10, y); y += 8; doc.setDrawColor(200); doc.line(10, y, 200, y); y += 6; doc.setTextColor(30); messages.forEach((msg) => { if (y > pageHeight - 30) { doc.addPage(); y = 15; } doc.setFont('NotoSansTC', 'bold'); doc.text(`${msg.role === 'user' ? labels.user : labels.assistant}`, 10, y); doc.setFont('NotoSansTC', 'normal'); doc.setFontSize(10); doc.setTextColor(120); doc.text(`${formatDate(msg.createdAt, locale)}`, 40, y); y += 6; doc.setTextColor(30); doc.setFontSize(12); const lines = doc.splitTextToSize(msg.content, 180); for (let i = 0; i < lines.length; i++) { if (y > pageHeight - 20) { doc.addPage(); y = 15; } doc.text(lines[i], 12, y); y += 6; } if (msg.sources && msg.sources.length > 0) { doc.setFontSize(11); doc.setTextColor(80); if (y > pageHeight - 20) { doc.addPage(); y = 15; } doc.text(labels.citations, 12, y); y += 5; msg.sources.forEach((src: any, i: number) => { const url = src.metadata?.url || ''; if (y > pageHeight - 15) { doc.addPage(); y = 15; } doc.text(`- [${i + 1}] ${url}`, 15, y); y += 5; }); doc.setTextColor(30); } y += 6; doc.setDrawColor(230); if (y > pageHeight - 10) { doc.addPage(); y = 15; } doc.line(10, y, 200, y); y += 4; }); doc.save(`${title || 'chat'}.pdf`); }; const Navbar = ({ chatId, messages, }: { messages: Message[]; chatId: string; }) => { const [title, setTitle] = useState(''); const tCommon = useTranslations('common'); const tNavbar = useTranslations('navbar'); const tExport = useTranslations('export'); const locale = useLocale(); useEffect(() => { if (messages.length > 0) { const newTitle = messages[0].content.length > 20 ? `${messages[0].content.substring(0, 20).trim()}...` : messages[0].content; setTitle(newTitle); // title already set above } }, [messages]); // Removed per-locale relative time uses render-time computation return (

{formatRelativeTime( new Date(), messages[0]?.createdAt || new Date(), locale, )}

{title}

{}} />
); }; export default Navbar;