ir-studio / src /components /Header.tsx
Natnael B. Haile
Cursor
feat: complete IR Studio with Git LFS tracking for all binary assets and NFCorpus index
fbad523
Raw History Blame Contribute Delete
7.05 kB
import React, { useEffect } from 'react';
import {
Sparkles,
Layers,
BookOpen,
FolderOpen,
Database,
Sun,
Moon,
} from 'lucide-react';
import type { CorpusDataset } from '../types/corpus';
interface HeaderProps {
activeTab: string;
setActiveTab: (tab: string) => void;
datasets?: CorpusDataset[];
activeDatasetId?: string;
onSelectDataset?: (id: string) => void;
onOpenExplorer?: () => void;
theme?: 'dark' | 'light';
onToggleTheme?: () => void;
}
export const Header: React.FC<HeaderProps> = ({
activeTab,
setActiveTab,
datasets = [],
activeDatasetId,
onSelectDataset,
onOpenExplorer,
theme = 'light',
onToggleTheme,
}) => {
const tabs = [
{ id: 'playground', label: 'Retrieval Arena', icon: Sparkles },
{ id: 'index', label: 'Index Inspector', icon: Layers },
{ id: 'concepts', label: 'Core Concepts', icon: BookOpen },
];
// Quick hotkeys: 1 -> Arena, 2 -> Inspector, 3 -> Core Concepts (when not in an input/textarea)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (
target &&
(target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'SELECT' ||
target.isContentEditable)
) {
return;
}
if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
setActiveTab('playground');
} else if (e.key === '2' && !e.metaKey && !e.ctrlKey) {
setActiveTab('index');
} else if (e.key === '3' && !e.metaKey && !e.ctrlKey) {
setActiveTab('concepts');
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [setActiveTab]);
return (
<header className="border-b border-zinc-800/80 bg-zinc-950/90 backdrop-blur-md sticky top-0 z-40">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-14 gap-3">
{/* Left: Brand */}
<div className="flex items-center space-x-2.5 shrink-0">
<div className="w-7 h-7 rounded-lg bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-800 flex items-center justify-center text-sm shadow-xs select-none">
🦫
</div>
<span className="font-semibold text-sm tracking-tight text-zinc-900 dark:text-zinc-100">
IR Studio
</span>
</div>
{/* Center: Primary Navigation Tabs (Retrieval Arena vs Index Inspector) */}
<nav className="flex items-center p-1 rounded-xl bg-zinc-950/80 border border-zinc-800 shadow-inner gap-1">
{tabs.map((tab, idx) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
const accentColor =
tab.id === 'playground'
? 'text-sky-400'
: tab.id === 'index'
? 'text-emerald-400'
: 'text-amber-400';
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
title={`Switch to ${tab.label} (Press ${idx + 1})`}
className={`group relative flex items-center space-x-2 px-3.5 py-1.5 rounded-lg text-xs font-medium transition-all duration-150 cursor-pointer ${
isActive
? 'bg-zinc-850 text-zinc-100 font-semibold shadow-xs border border-zinc-750'
: 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-900/60 border border-transparent'
}`}
>
<Icon
className={`w-3.5 h-3.5 ${
isActive ? accentColor : 'text-zinc-500 group-hover:text-zinc-300'
} transition-colors`}
/>
<span>{tab.label}</span>
<span
className={`text-[9px] font-mono px-1 py-0.2 rounded transition-colors ${
isActive
? 'bg-zinc-900 text-zinc-400 border border-zinc-800'
: 'text-zinc-600 group-hover:text-zinc-400'
}`}
>
{idx + 1}
</span>
</button>
);
})}
</nav>
{/* Right: Mounted Dataset Picker & Theme Toggle */}
<div className="flex items-center space-x-2 shrink-0">
{datasets.length > 0 && onSelectDataset && (
<div className="hidden sm:flex items-center space-x-2 border border-zinc-800 bg-zinc-900/80 px-2.5 py-1 rounded-xl text-xs shadow-xs h-9">
<Database className="w-3.5 h-3.5 text-zinc-400 shrink-0" />
<span className="text-zinc-500 font-mono text-[11px]">corpus:</span>
<select
value={activeDatasetId}
onChange={(e) => onSelectDataset(e.target.value)}
className="bg-transparent text-zinc-200 font-medium focus:outline-none max-w-[150px] truncate cursor-pointer text-xs"
>
{datasets.map((d) => (
<option key={d.id} value={d.id} className="bg-zinc-900 text-zinc-200">
{d.name.replace(' (Pedagogical Subset)', '').replace(' (BEIR Pedagogical Subset)', '')}
</option>
))}
</select>
<div className="w-px h-3.5 bg-zinc-800 shrink-0" />
{onOpenExplorer && (
<button
type="button"
onClick={onOpenExplorer}
className="flex items-center space-x-1 text-[11px] font-mono text-zinc-400 hover:text-zinc-100 px-1.5 py-0.5 rounded-md hover:bg-zinc-800 transition cursor-pointer"
title="Open virtual file explorer"
>
<FolderOpen className="w-3 h-3 text-zinc-400" />
<span>files</span>
</button>
)}
</div>
)}
{/* Theme Toggle (Dark / Light) */}
{onToggleTheme && (
<button
type="button"
onClick={onToggleTheme}
className="w-9 h-9 rounded-xl border border-zinc-800 bg-zinc-900/80 hover:bg-zinc-850 text-zinc-400 hover:text-zinc-100 transition shadow-xs flex items-center justify-center cursor-pointer shrink-0"
title={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
aria-label="Toggle theme"
>
{theme === 'dark' ? (
<Sun className="w-4 h-4 text-amber-400" />
) : (
<Moon className="w-4 h-4 text-sky-400" />
)}
</button>
)}
</div>
</div>
</div>
</header>
);
};