clinicpal / src /components /layout /header.tsx
Vrda's picture
Deploy ClinIcPal frontend
9bc2f29 verified
'use client';
import { useRef, useEffect } from 'react';
import Image from 'next/image';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import { useConnectionStatus, useMedgemmaStatus, useLocalStatus, useAppStore, useSettings } from '@/store/app-store';
import { GlassTooltip } from '@/components/ui';
import { TabNavigation } from './tab-navigation';
export function Header() {
const connectionStatus = useConnectionStatus();
const medgemmaStatus = useMedgemmaStatus();
const localStatus = useLocalStatus();
const settings = useSettings();
const setConnectionStatus = useAppStore((state) => state.setConnectionStatus);
const setAvailableModels = useAppStore((state) => state.setAvailableModels);
const updateSettings = useAppStore((state) => state.updateSettings);
// Determine the active provider's status
const activeStatus =
settings.provider === 'local'
? localStatus
: settings.provider === 'medgemma'
? medgemmaStatus
: connectionStatus;
const providerLabel =
settings.provider === 'local'
? 'Local'
: settings.provider === 'medgemma'
? 'MedGemma'
: 'Ollama';
const abortControllerRef = useRef<AbortController | null>(null);
// Cleanup on unmount
useEffect(() => {
return () => {
abortControllerRef.current?.abort();
};
}, []);
const handleRetryConnection = async () => {
// Abort any in-flight request
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
setConnectionStatus('checking');
try {
const response = await fetch('/api/ollama', {
signal: abortControllerRef.current.signal,
});
const data = await response.json();
if (data.success) {
setConnectionStatus('connected');
setAvailableModels(data.data.models);
// Auto-select first model if none selected
if (data.data.models.length > 0) {
updateSettings({ selectedModel: data.data.models[0] });
}
} else {
setConnectionStatus('disconnected');
}
} catch (error) {
// Ignore abort errors
if (error instanceof Error && error.name !== 'AbortError') {
setConnectionStatus('disconnected');
}
}
};
return (
<motion.header
className={cn(
'fixed top-0 left-0 right-0 z-50',
'glass-elevated',
'border-b border-[var(--glass-border-subtle)]'
)}
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.4, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo / Title */}
<div className="flex items-center gap-3">
<Image
src="/clinicpal-iOS-Default-1024x1024@1x.png"
alt="ClinicPal"
width={36}
height={36}
/>
<h1 className="text-lg font-semibold text-[var(--foreground)]">
Clinipal - Clinical Error Detector
</h1>
</div>
{/* Right side: Tab Navigation + Connection Status */}
<div className="flex items-center gap-4">
{/* Tab Navigation */}
<div className="hidden md:block">
<TabNavigation />
</div>
{/* Connection Status */}
<GlassTooltip
content={
activeStatus === 'connected'
? `${providerLabel} is connected and ready`
: activeStatus === 'disconnected'
? `Cannot connect to ${providerLabel}. Click to retry.`
: `Checking connection to ${providerLabel}...`
}
side="bottom"
>
<button
onClick={
activeStatus === 'disconnected'
? handleRetryConnection
: undefined
}
className={cn(
'flex items-center gap-2 px-3 py-1.5 rounded-full',
'glass text-sm font-medium',
'transition-all duration-200',
activeStatus === 'disconnected' &&
'hover:bg-[var(--glass-bg-elevated)] cursor-pointer'
)}
>
<span
className={cn(
'w-2 h-2 rounded-full',
activeStatus === 'connected' && 'bg-green-500 pulse-glow-success',
activeStatus === 'disconnected' && 'bg-red-500',
activeStatus === 'checking' && 'bg-amber-500 animate-pulse'
)}
/>
<span className="hidden sm:inline">
{activeStatus === 'connected' && `${providerLabel}`}
{activeStatus === 'disconnected' && `${providerLabel} Off`}
{activeStatus === 'checking' && 'Checking...'}
</span>
</button>
</GlassTooltip>
</div>
</div>
</div>
</motion.header>
);
}