feat: model switching shows loading spinner, disables input until ready

This commit is contained in:
Jon Saad-Falcon
2026-03-14 12:48:41 -07:00
parent bc7ef2dd51
commit fb993fc6a1
3 changed files with 46 additions and 14 deletions
+3 -2
View File
@@ -23,6 +23,7 @@ export function InputArea() {
const updateLastAssistant = useAppStore((s) => s.updateLastAssistant);
const setStreamState = useAppStore((s) => s.setStreamState);
const resetStream = useAppStore((s) => s.resetStream);
const modelLoading = useAppStore((s) => s.modelLoading);
const { state: speechState, available: speechAvailable, startRecording, stopRecording } = useSpeech();
@@ -272,7 +273,7 @@ export function InputArea() {
rows={1}
className="flex-1 bg-transparent outline-none resize-none text-sm leading-relaxed"
style={{ color: 'var(--color-text)', maxHeight: '200px' }}
disabled={streamState.isStreaming}
disabled={streamState.isStreaming || modelLoading}
/>
{streamState.isStreaming ? (
<button
@@ -293,7 +294,7 @@ export function InputArea() {
/>
<button
onClick={sendMessage}
disabled={!input.trim()}
disabled={!input.trim() || modelLoading}
className="p-2 rounded-xl transition-colors shrink-0 cursor-pointer disabled:opacity-30 disabled:cursor-default"
style={{
background: input.trim() ? 'var(--color-accent)' : 'var(--color-bg-tertiary)',
+18 -2
View File
@@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react';
import { Search, Cpu, X, Download, Loader2, Trash2, Check } from 'lucide-react';
import { useAppStore } from '../lib/store';
import { pullModel, deleteModel, fetchModels } from '../lib/api';
import { pullModel, deleteModel, fetchModels, preloadModel } from '../lib/api';
/** Popular models that users can download from the catalogue. */
const CATALOGUE_MODELS = [
@@ -65,9 +65,25 @@ export function CommandPalette() {
}
}, [pullSuccess]);
const handleSelect = (modelId: string) => {
const handleSelect = async (modelId: string) => {
const previousModel = selectedModel;
setSelectedModel(modelId);
setCommandPaletteOpen(false);
// Preload the model if switching to a different one
if (modelId !== previousModel) {
const { setModelLoading, addLogEntry } = useAppStore.getState();
setModelLoading(true);
addLogEntry({ timestamp: Date.now(), level: 'info', category: 'model', message: `Switching to ${modelId}...` });
try {
await preloadModel(modelId);
addLogEntry({ timestamp: Date.now(), level: 'info', category: 'model', message: `${modelId} loaded` });
} catch (e: any) {
addLogEntry({ timestamp: Date.now(), level: 'error', category: 'model', message: `Failed to load ${modelId}: ${e.message}` });
} finally {
setModelLoading(false);
}
}
};
const refreshModels = async () => {
+25 -10
View File
@@ -14,6 +14,7 @@ import {
Sun,
Moon,
Monitor,
Loader2,
} from 'lucide-react';
import { ConversationList } from './ConversationList';
import { useAppStore } from '../../lib/store';
@@ -29,6 +30,7 @@ export function Sidebar() {
const selectedModel = useAppStore((s) => s.selectedModel);
const serverInfo = useAppStore((s) => s.serverInfo);
const setCommandPaletteOpen = useAppStore((s) => s.setCommandPaletteOpen);
const modelLoading = useAppStore((s) => s.modelLoading);
const settings = useAppStore((s) => s.settings);
const updateSettings = useAppStore((s) => s.updateSettings);
@@ -120,16 +122,29 @@ export function Sidebar() {
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--color-bg-tertiary)')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'var(--color-bg-secondary)')}
>
<Cpu size={14} />
<span className="truncate flex-1 text-left" style={{ color: 'var(--color-text)' }}>
{selectedModel || serverInfo?.model || 'Select model'}
</span>
<kbd
className="text-[10px] px-1.5 py-0.5 rounded font-mono"
style={{ background: 'var(--color-bg-tertiary)', color: 'var(--color-text-tertiary)' }}
>
K
</kbd>
{modelLoading ? (
<Loader2 size={14} className="animate-spin" style={{ color: 'var(--color-accent)' }} />
) : (
<Cpu size={14} />
)}
<div className="flex-1 min-w-0">
<span className="truncate block text-left" style={{ color: 'var(--color-text)' }}>
{selectedModel || serverInfo?.model || 'Select model'}
</span>
{modelLoading && (
<span className="text-[10px] block" style={{ color: 'var(--color-accent)' }}>
Loading model...
</span>
)}
</div>
{!modelLoading && (
<kbd
className="text-[10px] px-1.5 py-0.5 rounded font-mono"
style={{ background: 'var(--color-bg-tertiary)', color: 'var(--color-text-tertiary)' }}
>
K
</kbd>
)}
</button>
{/* Search */}