import React, { useState, useRef, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { Globe, Fingerprint, Wand2, Film, FolderOpen, RefreshCw, Settings2, ChevronRight, ChevronDown, Zap, Building2, Library, FileText, Trash2 } from 'lucide-react'; import { Button, Badge } from '../ui'; import NotificationPanel from './NotificationPanel'; import { useAppStore } from '../store'; const VIEW_META = { launchpad: { label: 'Launchpad', Icon: Globe, accent: '#f3a5b6', kicker: 'Studio' }, clone: { label: 'Voice Clone', Icon: Fingerprint, accent: '#d3869b', kicker: 'Studio' }, design: { label: 'Voice Design', Icon: Wand2, accent: '#8ec07c', kicker: 'Studio' }, dub: { label: 'Dubbing', Icon: Film, accent: '#fe8019', kicker: 'Studio' }, projects: { label: 'OmniDrive', Icon: FolderOpen, accent: '#83a598', kicker: 'Library' }, gallery: { label: 'Gallery', Icon: Library, accent: '#b8bb26', kicker: 'Library' }, transcriptions: { label: 'Transcriptions', Icon: FileText, accent: '#d3869b', kicker: 'Library' }, settings: { label: 'Settings', Icon: Settings2, accent: '#fabd2f', kicker: 'Preferences' }, enterprise: { label: 'Commercial License', Icon: Building2, accent: '#fe8019', kicker: 'Licensing' }, }; function WaveBars({ color = '#f3a5b6', active }) { const heights = [4, 9, 5, 11, 6, 10, 5, 8]; return (
); } export default function Header({ mode, setMode, sysStats, modelStatus, doubleClickMaximize, activeProjectName, onFlushMemory, }) { // Default OFF — chrome shouldn't double as a resource monitor. Power users // flip this on via Settings → Performance. Idle/Ready/Loading badge + // Flush button stay visible regardless (action-relevant). const showLiveStats = useAppStore(s => s.showHeaderLiveStats); const [flushing, setFlushing] = useState(false); const [flushOpen, setFlushOpen] = useState(false); const [loadedModels, setLoadedModels] = useState([]); const [unloading, setUnloading] = useState(null); const flushRef = useRef(null); const flushBtnRef = useRef(null); const [dropdownPos, setDropdownPos] = useState({ top: 0, left: 0 }); // Dynamically compute dropdown position from button rect const computePos = useCallback(() => { if (!flushBtnRef.current) return; const rect = flushBtnRef.current.getBoundingClientRect(); const dropW = 260; const dropH = 220; // approximate max height const pad = 6; // Default: below button, right-aligned let top = rect.bottom + pad; let left = rect.right - dropW; // Flip up if too close to bottom if (top + dropH > window.innerHeight - 10) { top = rect.top - dropH - pad; } // Clamp left so it doesn't go off-screen if (left < 8) left = 8; if (left + dropW > window.innerWidth - 8) left = window.innerWidth - dropW - 8; setDropdownPos({ top, left }); }, []); // Recompute on open, resize, and scroll useEffect(() => { if (!flushOpen) return; computePos(); window.addEventListener('resize', computePos); window.addEventListener('scroll', computePos, true); return () => { window.removeEventListener('resize', computePos); window.removeEventListener('scroll', computePos, true); }; }, [flushOpen, computePos]); const view = VIEW_META[mode] || VIEW_META.launchpad; const ViewIcon = view.Icon; // Fetch loaded models when dropdown opens useEffect(() => { if (!flushOpen) return; const fetchModels = async () => { try { const { API } = await import('../api/client'); const res = await fetch(`${API}/model/loaded`); if (res.ok) { const data = await res.json(); setLoadedModels(data.models || []); } } catch {} }; fetchModels(); }, [flushOpen]); // Click outside to close (must check both the button wrapper AND the portal dropdown) const dropdownRef = useRef(null); useEffect(() => { if (!flushOpen) return; const handler = (e) => { const inBtn = flushRef.current && flushRef.current.contains(e.target); const inDrop = dropdownRef.current && dropdownRef.current.contains(e.target); if (!inBtn && !inDrop) setFlushOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [flushOpen]); const unloadModel = async (modelId) => { setUnloading(modelId); try { const { API } = await import('../api/client'); const res = await fetch(`${API}/model/unload/${modelId}`, { method: 'POST' }); if (res.ok) { setLoadedModels(prev => prev.filter(m => m.id !== modelId)); } } catch {} finally { setUnloading(null); } }; // Dynamic accent color must stay inline — it's driven by the current view. const dotStyle = { background: view.accent, boxShadow: `0 0 10px ${view.accent}90` }; const labelStyle = { color: view.accent }; return (