File size: 5,098 Bytes
9bc2f29 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | '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>
);
}
|