diff --git a/public/script.js b/public/script.js index 19f8099..15bc4ea 100644 --- a/public/script.js +++ b/public/script.js @@ -368,6 +368,20 @@ async function saveGenerationSettings() { setNotice("Generation settings saved.", "success"); } +async function saveSelectedModel() { + if (!activeChatId || !modelSelect.value) return; + const chatId = activeChatId; + const selectedModel = modelSelect.value; + const res = await put(`/chats/${chatId}/model`, {model: selectedModel}); + if (res.error || !res.chat) return setNotice(res.error || "Unable to switch models.", "error"); + const chat = getChatById(chatId); + if (chat) Object.assign(chat, res.chat); + if (chatId === activeChatId && modelSelect.value === selectedModel) { + renderModelSection(); + setNotice(`Model switched to ${modelSelect.selectedOptions[0]?.textContent || selectedModel}.`, "success"); + } +} + function resizeComposerInput() { msgInput.style.height = "auto"; const nextHeight = Math.min(Math.max(msgInput.scrollHeight, 46), 96); @@ -1978,6 +1992,7 @@ if (importWorkspaceFileInput) importWorkspaceFileInput.addEventListener("change" chatSearchInput.addEventListener("input", () => { void loadChatSessions(); }); modelSelect.addEventListener("change", () => { renderModelSection(); + void saveSelectedModel(); }); saveGenerationSettingsBtn.addEventListener("click", () => { void saveGenerationSettings(); }); roleplayNewPersonaBtn.addEventListener("click", () => openPersonaForm(null, "assistant")); diff --git a/src/db/repositories.js b/src/db/repositories.js index a86a2e3..615186f 100644 --- a/src/db/repositories.js +++ b/src/db/repositories.js @@ -163,6 +163,7 @@ export function createRepositories(db, config) { SET preferred_model = ?, temperature = ?, context_length = ?, response_length = ?, system_instruction = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ? `), + updateChatPreferredModel: db.prepare("UPDATE chat_sessions SET preferred_model = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"), updateChatActiveLeaf: db.prepare("UPDATE chat_sessions SET active_leaf_message_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"), updateChatBranchSource: db.prepare("UPDATE chat_sessions SET source_chat_id = ?, branched_from_message_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"), setChatActiveLeafById: db.prepare("UPDATE chat_sessions SET active_leaf_message_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"), @@ -1027,6 +1028,7 @@ export function createRepositories(db, config) { chatId, username ), + updateChatPreferredModel: (chatId, username, model) => statements.updateChatPreferredModel.run(model || null, chatId, username), updateChatMessage: (chatId, messageId, content) => statements.updateChatMessage.run(content, messageId, chatId), updateChatMessageRetryState: (chatId, messageId, payload) => statements.updateChatMessageWithRetryState.run( payload.content, diff --git a/src/routes/chats.js b/src/routes/chats.js index 14230fc..bbd63ed 100644 --- a/src/routes/chats.js +++ b/src/routes/chats.js @@ -91,6 +91,13 @@ const generationSettingsValidator = (body) => { }}; }; +const preferredModelValidator = (body) => { + const model = String(body?.model || "").trim(); + if (!model) return {error: "Model is required"}; + if (model.length > 200) return {error: "Model must be 200 characters or fewer"}; + return {value: {model}}; +}; + export function createChatsRouter({repositories, chatService, modelService, config, chatRateLimiters}) { const router = express.Router(); router.use(requireLogin); @@ -180,6 +187,13 @@ export function createChatsRouter({repositories, chatService, modelService, conf return res.json({chat: repositories.getChat(chatId, req.session.user)}); }); + router.put("/chats/:id/model", validateBody(preferredModelValidator), (req, res) => { + const chatId = Number(req.params.id); + if (!repositories.getChat(chatId, req.session.user)) return res.status(404).json({error: "Chat not found"}); + repositories.updateChatPreferredModel(chatId, req.session.user, req.validatedBody.model); + return res.json({chat: repositories.getChat(chatId, req.session.user)}); + }); + router.post("/chats/:id/branches/:messageId/activate", (req, res) => { const result = chatService.activateBranch({ user: req.session.user, diff --git a/src/services/chatService.js b/src/services/chatService.js index 6e65a02..dc51511 100644 --- a/src/services/chatService.js +++ b/src/services/chatService.js @@ -65,7 +65,7 @@ export function createChatService(repositories, modelService, config) { }; const getGenerationSettings = (session, requestedModel) => ({ - model: session.preferred_model || requestedModel || "mistral:latest", + model: requestedModel || session.preferred_model || "mistral:latest", generation: { temperature: session.temperature ?? undefined, contextLength: session.context_length ?? undefined, diff --git a/test/app.test.js b/test/app.test.js index aae3999..9f01568 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -434,11 +434,17 @@ test("conversation branches, edit-resend, and generation settings work", async ( body: {preferredModel: "preferred:model", temperature: 0.4, contextLength: 4096, responseLength: 300, systemInstruction: "Be exact."}, jar }); - await request(server.baseUrl, "/chat", {method: "POST", body: {chatId, message: "first", model: "ignored:model"}, jar}); - assert.equal(calls[0].model, "preferred:model"); + await request(server.baseUrl, "/chat", {method: "POST", body: {chatId, message: "first", model: "selected:model"}, jar}); + assert.equal(calls[0].model, "selected:model"); assert.deepEqual(calls[0].options.generation, {temperature: 0.4, contextLength: 4096, responseLength: 300}); assert.match(calls[0].messages[0].content, /Be exact/); + const switchedModel = await request(server.baseUrl, `/chats/${chatId}/model`, { + method: "PUT", body: {model: "selected:model"}, jar + }); + assert.equal(switchedModel.status, 200); + assert.equal(switchedModel.json.chat.preferred_model, "selected:model"); + await request(server.baseUrl, "/chat", {method: "POST", body: {chatId, message: "second", model: "ignored:model"}, jar}); const original = await request(server.baseUrl, `/chats/${chatId}/messages`, {jar}); const firstAssistant = original.json.messages[1]; @@ -470,7 +476,7 @@ test("conversation branches, edit-resend, and generation settings work", async ( assert.equal(branched.json.chat.branched_from_message_id, firstAssistant.id); assert.match(branched.json.chat.title, /\(branch\)$/); assert.deepEqual(branched.json.messages.map((message) => message.content), ["first", "reply-1"]); - assert.equal(branched.json.chat.preferred_model, "preferred:model"); + assert.equal(branched.json.chat.preferred_model, "selected:model"); assert.equal(branched.json.chat.temperature, 0.4); const edited = await request(server.baseUrl, `/chats/${chatId}/messages/${active.json.messages[0].id}/edit-resend`, {