diff --git a/src/api.js b/src/api.js index 938b6a24..946f4e09 100644 --- a/src/api.js +++ b/src/api.js @@ -12,9 +12,10 @@ const { generate, stop, serve, + list, } = require("./service/ollama/ollama.js"); -let model = "mistral"; +let model = "mistral:latest"; function debugLog(msg) { if (global.debug) { @@ -36,7 +37,7 @@ async function runOllamaModel(event, msg) { await run(model, (json) => { // status will be set if the model is downloading if (json.status) { - if (json.status.includes("downloading")) { + if (json.status.includes("downloading") || json.status.includes("pulling")) { const percent = Math.round((json.completed / json.total) * 100); const content = isNaN(percent) ? "Downloading AI model..." @@ -151,6 +152,15 @@ async function serveOllama(event) { } } +async function listLocalModels(event) { + try { + modelList= await list() + event.reply("ollama:list", { success: true, content: modelList }); + } catch (err) { + event.reply("ollama:list", { success: false, content: err.message }); + } +} + function stopOllama(event) { stop(); } @@ -164,4 +174,5 @@ module.exports = { serveOllama, runOllamaModel, stopOllama, + listLocalModels, }; diff --git a/src/client.js b/src/client.js index ad462f25..38900eb7 100644 --- a/src/client.js +++ b/src/client.js @@ -18,17 +18,21 @@ const settingsView = document.getElementById("settings-view"); const settingsCancelBtn = document.getElementById("cancel-btn"); const settingsCloseBtn = document.getElementById("settings-close-btn"); const settingsSaveBtn = document.getElementById("save-btn"); +const settingsDownloadBtn = document.getElementById("download-btn"); const modelSelectInput = document.getElementById("model-select"); +const modelSelectDownloadInput = document.getElementById("model-select-download"); +const downloadEmptyWarning = document.getElementById("download-empty-warning"); let responseElem; /** * This is the initial chain of events that must run on start-up. - * 1. Start the Ollama server. + * 1. Start the Ollama server * 2. Run the model. This will load the model into memory so that first chat is not slow. * This step will also download the model if it is not already downloaded. * 3. Monitor the run status * 4. Load the chat + * 5. List locally avaliable models */ // 1. Start the Ollama server @@ -66,6 +70,21 @@ window.electronAPI.onOllamaRun((event, data) => { statusMsg.textContent = data.content; }); +// 5. List avaliable models +window.electronAPI.listLocalModels((event, data) => { + + let models=data.content.models + for(let i = 0; i < models.length; i++) { + var opt = document.createElement('option'); + opt.value = models[i].name; + opt.innerHTML = models[i].name; + modelSelectInput.appendChild(opt); + } + + +} + ); + // Update the display when a document is loaded window.electronAPI.onDocumentLoaded((event, data) => { document.getElementById("file-spinner").style.display = "none"; @@ -190,7 +209,6 @@ window.electronAPI.onChatReply((event, data) => { historyContainer.scrollTop = historyContainer.scrollHeight; } }); - // Open file dialog openFileButton.addEventListener("click", () => { document.getElementById("file-open-icon").style.display = "none"; @@ -253,3 +271,27 @@ userInput.addEventListener("input", function () { this.style.height = "auto"; this.style.height = this.scrollHeight + "px"; }); +//Download button in the settings menu +settingsDownloadBtn.addEventListener("click", () => { + let selectedModel=modelSelectDownloadInput.value + + if (selectedModel.length==0){ + alert("Input the model name from ollama.com/library to download a new model!") + return; + } + if (!selectedModel.includes(":")){ + //select latest model version if none is specified + selectedModel=selectedModel.concat(":latest") + } + var opt = document.createElement('option'); + opt.value = selectedModel; + opt.innerHTML = selectedModel; + modelSelectInput.appendChild(opt); + window.electronAPI.setModel(selectedModel); + window.electronAPI.runOllama(); + modelSelectDownloadInput.value="" + chatView.style.display = "none"; + settingsView.style.display = "none"; + document.getElementById("initial-view").style.display = "flex"; + +}); \ No newline at end of file diff --git a/src/index.css b/src/index.css index b86adcfd..13096267 100644 --- a/src/index.css +++ b/src/index.css @@ -219,6 +219,15 @@ button:focus { font-size: 1rem; width: 100%; } +#model-select-download { + padding: 10px; + background-color: var(--response-background); + border: 1px solid var(--primary-text); + color: var(--primary-text); + border-radius: 4px; + font-size: 1rem; + width: 100%; +} #model-select:focus { outline: none; @@ -460,4 +469,7 @@ button:focus { #user-input-text { flex-grow: 1; margin-right: 10px; +} +#download-empty-warning{ + display: none; } \ No newline at end of file diff --git a/src/index.html b/src/index.html index b3985b76..09247e8e 100644 --- a/src/index.html +++ b/src/index.html @@ -89,12 +89,18 @@

Settings

- - + +
+ + +
+ +
diff --git a/src/index.js b/src/index.js index c37087b8..ba71f894 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ const { stopOllama, loadDocument, runOllamaModel, + listLocalModels, } = require("./api.js"); // When debug is set to true, the app will log debug messages to the console @@ -74,6 +75,7 @@ app.on("ready", () => { ipcMain.on("ollama:serve", serveOllama); ipcMain.on("ollama:run", runOllamaModel); ipcMain.on("ollama:stop", stopOllama); + ipcMain.on("ollama:list", listLocalModels); if (app.isPackaged) { // Check app location diff --git a/src/preload.js b/src/preload.js index cdda5bfd..1e4e2595 100644 --- a/src/preload.js +++ b/src/preload.js @@ -37,5 +37,11 @@ contextBridge.exposeInMainWorld("electronAPI", { callback(event, data); }); }, + listLocalModels: (callback) => { + ipcRenderer.send("ollama:list") + ipcRenderer.on("ollama:list", (event, data) => { + callback(event, data); + }); + }, setModel: (model) => ipcRenderer.send("model:set", model), }); diff --git a/src/service/ollama/ollama.js b/src/service/ollama/ollama.js index 9da80444..64496b5b 100644 --- a/src/service/ollama/ollama.js +++ b/src/service/ollama/ollama.js @@ -155,8 +155,8 @@ class Ollama { if (done) { // We break before reaching here - // This means the prompt is not finished (maybe crashed?) - throw new Error("Failed to fulfill prompt"); + // This means the downloading of the model failed + throw new Error("Failed to download model: "+model); } // Parse responses are they are received from the Ollama server @@ -173,6 +173,43 @@ class Ollama { } } + async listModels() { + + const response = await fetch(this.host + "/api/tags", { + method: "GET", + cache: "no-store", + }); + + if (response.status !== 200) { + let err = `HTTP Error (${response.status}): `; + err += await response.text(); + + throw new Error(err); + } + + const reader = response.body.getReader(); + + //Reads the stream until list of models is returned + while (true) { + const { done, value } = await reader.read(); + + if (done) { + // We break before reaching here + // This means the prompt is not finished (maybe crashed?) + throw new Error("Failed to fulfill prompt"); + } + + // Parse responses are they are received from the Ollama server + for (const buffer of this.parse(value)) { + const json = JSON.parse(buffer); + + // done + return json; + + } + } + } + async run(model, fn) { await this.pull(model, fn); await this.generate(model, "", fn); @@ -359,6 +396,10 @@ function serve() { const ollama = Ollama.getOllama(); return ollama.serve(); } +function list() { + const ollama = Ollama.getOllama(); + return ollama.listModels(); +} module.exports = { run, @@ -368,4 +409,5 @@ module.exports = { clearHistory, stop, serve, + list, };