| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import useAppStore from '../store/appStore' |
|
|
| |
| |
| |
| function displayName(id) { |
| if (!id) return '—' |
| |
| const noProvider = id.includes('/') ? id.split('/').pop() : id |
| return noProvider |
| } |
|
|
| export default function ModelPicker() { |
| const model = useAppStore((s) => s.model) |
| const supportedModels = useAppStore((s) => s.supportedModels) |
| const setModel = useAppStore((s) => s.setModel) |
| const isStreaming = useAppStore((s) => s.isStreaming) |
| const setCurrentUser = useAppStore((s) => s.setCurrentUser) |
| const showToast = useAppStore((s) => s.showToast) |
|
|
| const list = supportedModels && supportedModels.length > 0 |
| ? supportedModels |
| : (model ? [model] : []) |
|
|
| if (list.length === 0) return null |
|
|
| async function onChange(next) { |
| if (!next || next === model) return |
| try { |
| const r = await fetch('/api/auth/me/preferred-model', { |
| method: 'PATCH', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ model: next }), |
| }) |
| if (!r.ok) throw new Error(`HTTP ${r.status}`) |
| const user = await r.json() |
| setCurrentUser(user) |
| setModel(next) |
| showToast?.(`Model switched to ${next}`) |
| } catch (e) { |
| showToast?.(`Model switch failed: ${e.message}`) |
| } |
| } |
|
|
| const disabled = isStreaming |
| const current = model || list[0] |
|
|
| return ( |
| <span |
| title={disabled |
| ? 'Wait for the current turn to finish before switching models' |
| : `Inference model — ${current} · saved to your profile` |
| } |
| style={{ |
| position: 'relative', |
| display: 'inline-flex', |
| alignItems: 'center', |
| gap: 6, |
| height: 28, // matches h-7 chrome of sibling TopBar controls |
| padding: '0 10px', |
| borderRadius: 6, |
| background: 'var(--surface-2)', |
| border: '1px solid var(--border-2)', |
| fontSize: 11, |
| color: 'var(--text-dim)', |
| cursor: disabled ? 'not-allowed' : 'pointer', |
| opacity: disabled ? 0.55 : 1, |
| transition: 'border-color 0.15s, background 0.15s', |
| }} |
| onMouseEnter={(e) => { |
| if (disabled) return |
| e.currentTarget.style.borderColor = 'var(--accent)' |
| e.currentTarget.style.background = 'var(--surface-3)' |
| }} |
| onMouseLeave={(e) => { |
| e.currentTarget.style.borderColor = 'var(--border-2)' |
| e.currentTarget.style.background = 'var(--surface-2)' |
| }} |
| > |
| <span |
| style={{ |
| color: 'var(--accent-hi)', |
| fontWeight: 600, |
| letterSpacing: '0.08em', |
| textTransform: 'uppercase', |
| fontSize: 9.5, |
| pointerEvents: 'none', |
| }} |
| > |
| Model |
| </span> |
| <span |
| style={{ |
| fontFamily: 'ui-monospace, "JetBrains Mono", monospace', |
| color: 'var(--text)', |
| fontSize: 11, |
| pointerEvents: 'none', |
| maxWidth: 180, |
| overflow: 'hidden', |
| textOverflow: 'ellipsis', |
| whiteSpace: 'nowrap', |
| }} |
| > |
| {displayName(current)} |
| </span> |
| <span style={{ color: 'var(--text-faint)', pointerEvents: 'none', fontSize: 9 }}>▾</span> |
| <select |
| value={current} |
| disabled={disabled} |
| onChange={(e) => onChange(e.target.value)} |
| aria-label="Inference model" |
| style={{ |
| position: 'absolute', |
| inset: 0, |
| width: '100%', |
| height: '100%', |
| opacity: 0, |
| cursor: disabled ? 'not-allowed' : 'pointer', |
| appearance: 'none', |
| border: 'none', |
| padding: 0, |
| font: 'inherit', |
| }} |
| > |
| {list.map((m) => ( |
| <option key={m} value={m}>{m}</option> |
| ))} |
| </select> |
| </span> |
| ) |
| } |
|
|