| import type { ModelState } from '../types'; | |
| interface ModelStatusProps { | |
| models: ModelState[]; | |
| } | |
| const STATUS_COLOR: Record<ModelState['status'], string> = { | |
| pending: '#9e9e9e', | |
| downloading: '#1976d2', | |
| loading: '#f9a825', | |
| ready: '#388e3c', | |
| error: '#d32f2f', | |
| }; | |
| const STATUS_LABEL: Record<ModelState['status'], string> = { | |
| pending: 'Pending', | |
| downloading: 'Downloading', | |
| loading: 'Loading', | |
| ready: 'Ready', | |
| error: 'Error', | |
| }; | |
| function ProgressBar({ progress, color }: { progress: number; color: string }) { | |
| return ( | |
| <div style={{ | |
| height: '4px', | |
| background: 'var(--border)', | |
| borderRadius: '2px', | |
| overflow: 'hidden', | |
| marginTop: '4px', | |
| }}> | |
| <div style={{ | |
| height: '100%', | |
| width: `${Math.round(progress * 100)}%`, | |
| background: color, | |
| borderRadius: '2px', | |
| transition: 'width 0.3s ease', | |
| }} /> | |
| </div> | |
| ); | |
| } | |
| function ModelRow({ model }: { model: ModelState }) { | |
| const color = STATUS_COLOR[model.status]; | |
| const showProgress = model.status === 'downloading' || model.status === 'loading'; | |
| return ( | |
| <div style={{ | |
| padding: '0.5rem 0.75rem', | |
| background: 'var(--bg-card)', | |
| border: '1px solid var(--border)', | |
| borderRadius: '6px', | |
| marginBottom: '0.4rem', | |
| }}> | |
| <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> | |
| <span style={{ | |
| fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace", | |
| fontSize: '0.78rem', | |
| color: 'var(--text)', | |
| }}> | |
| {model.name} | |
| </span> | |
| <span style={{ | |
| fontSize: '0.72rem', | |
| fontFamily: 'system-ui, -apple-system, sans-serif', | |
| fontWeight: 600, | |
| color, | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: '0.3rem', | |
| }}> | |
| {model.status === 'ready' && ( | |
| <span style={{ fontSize: '0.85rem' }}>{'\u2713'}</span> | |
| )} | |
| {model.status === 'error' && ( | |
| <span style={{ fontSize: '0.85rem' }}>{'\u2717'}</span> | |
| )} | |
| {STATUS_LABEL[model.status]} | |
| {showProgress && ( | |
| <span style={{ color: 'var(--text-secondary)', fontWeight: 400 }}> | |
| {Math.round(model.progress * 100)}% | |
| </span> | |
| )} | |
| </span> | |
| </div> | |
| {showProgress && <ProgressBar progress={model.progress} color={color} />} | |
| {model.status === 'error' && model.error && ( | |
| <div style={{ | |
| marginTop: '4px', | |
| fontSize: '0.72rem', | |
| color: '#d32f2f', | |
| fontFamily: 'system-ui, -apple-system, sans-serif', | |
| }}> | |
| {model.error} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| export default function ModelStatus({ models }: ModelStatusProps) { | |
| const coreModels = models.filter((model) => model.name !== 'expansion'); | |
| const expansionModel = models.find((model) => model.name === 'expansion'); | |
| const coreReady = coreModels.length > 0 && coreModels.every((model) => model.status === 'ready'); | |
| const expansionReady = expansionModel?.status === 'ready'; | |
| const expansionUnavailable = expansionModel?.status === 'error'; | |
| return ( | |
| <div style={{ | |
| padding: '1rem', | |
| background: 'var(--bg-section)', | |
| border: '1px solid var(--border)', | |
| borderRadius: '8px', | |
| marginBottom: '1.5rem', | |
| }}> | |
| <div style={{ | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'space-between', | |
| marginBottom: '0.6rem', | |
| }}> | |
| <h3 style={{ | |
| margin: 0, | |
| fontSize: '0.85rem', | |
| fontFamily: 'system-ui, -apple-system, sans-serif', | |
| fontWeight: 600, | |
| color: 'var(--text-secondary)', | |
| textTransform: 'uppercase', | |
| letterSpacing: '0.05em', | |
| }}> | |
| Models | |
| </h3> | |
| {coreReady && ( | |
| <span style={{ | |
| fontSize: '0.75rem', | |
| fontFamily: 'system-ui, -apple-system, sans-serif', | |
| color: '#388e3c', | |
| fontWeight: 600, | |
| }}> | |
| Search ready | |
| </span> | |
| )} | |
| </div> | |
| {!coreReady && ( | |
| <p style={{ | |
| margin: '0 0 0.5rem', | |
| fontSize: '0.75rem', | |
| fontFamily: 'system-ui, -apple-system, sans-serif', | |
| color: 'var(--text-secondary)', | |
| lineHeight: 1.4, | |
| }}> | |
| First load downloads several GB of model weights. Subsequent visits use the browser cache. | |
| </p> | |
| )} | |
| {coreReady && !expansionReady && !expansionUnavailable && ( | |
| <p style={{ | |
| margin: '0 0 0.5rem', | |
| fontSize: '0.75rem', | |
| fontFamily: 'system-ui, -apple-system, sans-serif', | |
| color: 'var(--text-secondary)', | |
| lineHeight: 1.4, | |
| }}> | |
| Embedding and reranker ready. Compact expansion model (single-file download) loading... | |
| </p> | |
| )} | |
| {coreReady && expansionUnavailable && ( | |
| <p style={{ | |
| margin: '0 0 0.5rem', | |
| fontSize: '0.75rem', | |
| fontFamily: 'system-ui, -apple-system, sans-serif', | |
| color: 'var(--text-secondary)', | |
| lineHeight: 1.4, | |
| }}> | |
| Expansion model unavailable. Search uses the original query directly. | |
| </p> | |
| )} | |
| {models.map(m => ( | |
| <ModelRow key={m.name} model={m} /> | |
| ))} | |
| {models.length === 0 && ( | |
| <div style={{ | |
| color: 'var(--text-muted)', | |
| fontSize: '0.85rem', | |
| fontFamily: 'system-ui, -apple-system, sans-serif', | |
| }}> | |
| No models configured. | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |