import { useCallback, useState, useRef, useEffect } from 'react'; import { Menu, Sparkles, LogOut, Volume2, VolumeOff, Cpu, Bot, Download, Minimize2, Info, Copy, Check } from 'lucide-react'; import type { ConnectionStatus, Session, ChatMessage } from '../types'; import { useT } from '../hooks/useLocale'; import { LanguageSelector } from './LanguageSelector'; import { ThemeSwitcher } from './ThemeSwitcher'; import { sessionDisplayName } from '../lib/sessionName'; import { messagesToMarkdown, downloadFile } from '../lib/exportChat'; interface Props { status: ConnectionStatus; sessionKey: string; onToggleSidebar: () => void; activeSessionData?: Session; onLogout?: () => void; soundEnabled?: boolean; onToggleSound?: () => void; messages?: ChatMessage[]; agentAvatarUrl?: string; agentName?: string; onCompact?: (sessionKey: string) => Promise; } export function Header({ status, sessionKey, onToggleSidebar, activeSessionData, onLogout, soundEnabled, onToggleSound, messages, agentAvatarUrl, agentName, onCompact }: Props) { const t = useT(); const sessionLabel = activeSessionData ? sessionDisplayName(activeSessionData) : (sessionKey.split(':').pop() || sessionKey); const [showSessionInfo, setShowSessionInfo] = useState(false); const sessionInfoRef = useRef(null); // Close popover on outside click useEffect(() => { if (!showSessionInfo) return; const handler = (e: MouseEvent) => { if (sessionInfoRef.current && !sessionInfoRef.current.contains(e.target as Node)) { setShowSessionInfo(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [showSessionInfo]); 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 ( <>
PinchChat { const img = e.target as HTMLImageElement; if (img.src !== window.location.origin + '/logo.png') { img.src = '/logo.png'; } else { img.style.display = 'none'; } }} /> {showSessionInfo && activeSessionData && ( setShowSessionInfo(false)} /> )}
{onToggleSound && ( )} {messages && messages.length > 0 && ( )} {status === 'connected' ? (
{t('header.connected')}
) : status === 'connecting' ? (
{t('login.connecting')}
) : (
{t('header.disconnected')}
)} {onLogout && ( )}
{(() => { const ctx = activeSessionData?.contextTokens; const total = activeSessionData?.totalTokens || 0; if (!ctx) return null; const pct = Math.min(100, (total / ctx) * 100); const opacity = Math.max(0.35, Math.min(1, pct / 100)); const barStyle = { width: `${pct}%`, backgroundColor: `rgba(var(--pc-accent-rgb), ${opacity})` }; return (
{activeSessionData?.model && ( {activeSessionData.model.replace(/^.*\//, '')} )}
{(total / 1000).toFixed(1)}k / {(ctx / 1000).toFixed(0)}k tokens {onCompact && pct >= 50 && ( )}
); })()} ); } function CopyField({ value }: { value: string }) { const [copied, setCopied] = useState(false); return ( ); } function SessionInfoPopover({ session, sessionKey, messageCount, onClose }: { session: Session; sessionKey: string; messageCount: number; onClose: () => void }) { const t = useT(); const rows: Array<{ label: string; value: string; copyable?: boolean }> = [ { label: t('sessionInfo.sessionKey'), value: sessionKey, copyable: true }, ]; if (session.channel) rows.push({ label: t('sessionInfo.channel'), value: session.channel }); if (session.kind) rows.push({ label: t('sessionInfo.kind'), value: session.kind }); if (session.model) rows.push({ label: t('sessionInfo.model'), value: session.model.replace(/^.*\//, '') }); if (session.agentId) rows.push({ label: t('sessionInfo.agent'), value: session.agentId }); rows.push({ label: t('sessionInfo.messages'), value: String(messageCount) }); if (session.totalTokens) { rows.push({ label: t('sessionInfo.totalTokens'), value: `${(session.totalTokens / 1000).toFixed(1)}k` }); if (session.inputTokens) rows.push({ label: t('sessionInfo.inputTokens'), value: `${(session.inputTokens / 1000).toFixed(1)}k` }); if (session.outputTokens) rows.push({ label: t('sessionInfo.outputTokens'), value: `${(session.outputTokens / 1000).toFixed(1)}k` }); if (session.contextTokens) rows.push({ label: t('sessionInfo.contextWindow'), value: `${(session.contextTokens / 1000).toFixed(0)}k` }); } if (session.updatedAt) { rows.push({ label: t('sessionInfo.lastActive'), value: new Date(session.updatedAt).toLocaleString() }); } return (
{t('header.sessionInfo')}
{rows.map(({ label, value, copyable }) => (
{label} {value} {copyable && }
))}
); } function CompactButton({ sessionKey, onCompact }: { sessionKey: string; onCompact: (key: string) => Promise }) { const [compacting, setCompacting] = useState(false); const t = useT(); const handleCompact = useCallback(async () => { if (compacting) return; setCompacting(true); try { await onCompact(sessionKey); } finally { setCompacting(false); } }, [compacting, sessionKey, onCompact]); return ( ); }