| 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> |
| ); |
| } |
|
|