diff --git a/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/ConversationView.tsx b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/ConversationView.tsx index b156314b..398071a1 100644 --- a/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/ConversationView.tsx +++ b/__LOCAL_LLMs/dashboard/src/app/(workspace)/components/ConversationView.tsx @@ -8,6 +8,8 @@ import { InputBar } from './InputBar'; import { MessageThread } from './MessageThread'; import { ContextBar } from './ContextBar'; import { estimateTokens, getModelContextWindow } from '../../lib/format'; +import { autoDetectDefaults, classifyTask, resolveModel } from '../../lib/router'; +import type { ModelDefaults } from '../../lib/types'; interface ConversationViewProps { conversation: Conversation; @@ -27,9 +29,11 @@ export function ConversationView({ const [title, setTitle] = useState(conversation.title); const [messages, setMessages] = useState(initialMessages); const [streaming, setStreaming] = useState(false); - const [selectedModel, setSelectedModel] = useState(conversation.model); + const [selectedModel, setSelectedModel] = useState(conversation.model || '__auto__'); const [models, setModels] = useState([]); + const [runningModels, setRunningModels] = useState([]); const [showModels, setShowModels] = useState(false); + const [modelDefaults, setModelDefaults] = useState(null); const abortRef = useRef(null); const usedTokens = useMemo(() => { @@ -39,11 +43,28 @@ export function ConversationView({ const maxTokens = getModelContextWindow(selectedModel); const ensureModels = async () => { - if (models.length > 0) return; + if (models.length > 0 && modelDefaults) return; const res = await fetch('/api/ollama'); if (!res.ok) return; const data = (await res.json()) as OllamaData; - setModels(data.models.map(m => m.name)); + const installed = data.models.map(m => m.name); + const running = data.running.map(m => m.name); + setModels(installed); + setRunningModels(running); + + const stored = localStorage.getItem('llm-model-defaults'); + if (stored) { + try { + setModelDefaults(JSON.parse(stored) as ModelDefaults); + return; + } catch { + // fall through + } + } + + const detected = autoDetectDefaults(installed); + setModelDefaults(detected); + localStorage.setItem('llm-model-defaults', JSON.stringify(detected)); }; const persistConversationMeta = async (nextMessages: Message[]) => { @@ -86,6 +107,18 @@ export function ConversationView({ }; const onSend = async (text: string) => { + await ensureModels(); + + const routedModel = (() => { + if (selectedModel !== '__auto__') return selectedModel; + const defaults = modelDefaults || autoDetectDefaults(models); + const taskType = classifyTask(text); + const resolved = resolveModel(taskType, defaults, runningModels, models); + return resolved.model; + })(); + + if (!routedModel) return; + const now = Date.now(); const userMessage: Message = { id: crypto.randomUUID(), @@ -93,7 +126,7 @@ export function ConversationView({ role: 'user', content: text, timestamp: now, - model: selectedModel, + model: routedModel, }; await addMessage(userMessage); @@ -115,7 +148,12 @@ export function ConversationView({ const res = await fetch('/api/ollama/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: selectedModel, messages: chatPayload }), + body: JSON.stringify({ + model: routedModel, + modelDefaults: modelDefaults, + taskType: classifyTask(text), + messages: chatPayload, + }), signal: controller.signal, }); @@ -154,7 +192,7 @@ export function ConversationView({ role: 'assistant', content: assistantContent, timestamp: Date.now(), - model: selectedModel, + model: routedModel, }; setMessages(prev => { @@ -177,7 +215,7 @@ export function ConversationView({ role: 'assistant', content: assistantContent, timestamp: Date.now(), - model: selectedModel, + model: routedModel, metrics: { tokensPerSec, totalTokens, @@ -202,7 +240,7 @@ export function ConversationView({ role: 'assistant', content: `Error: ${String(err)}`, timestamp: Date.now(), - model: selectedModel, + model: routedModel, }; setMessages(prev => { const withoutTemp = prev.filter(m => m.id !== assistantId); @@ -247,11 +285,21 @@ export function ConversationView({ }} className="inline-flex items-center gap-1 rounded border border-white/10 bg-[var(--surface-card)] px-2 py-1 text-xs text-[var(--text-secondary)]" > - {selectedModel || 'Select model'} + {selectedModel === '__auto__' ? 'Auto' : selectedModel || 'Select model'} {showModels && models.length > 0 ? (
+ {models.map(model => ( + + ))} +
+ )} +
+
+ + + +
+ + void readFiles(e.target.files, 'file')} + /> + void readFiles(e.target.files, 'image')} + /> + { + const file = e.target.files?.[0]; + if (file) { + void transcribeAudio(file); + } + e.currentTarget.value = ''; + }} + /> +