diff --git a/.config/quickshell/services/Ai.qml b/.config/quickshell/services/Ai.qml index 385f66c9..f6cbd2aa 100644 --- a/.config/quickshell/services/Ai.qml +++ b/.config/quickshell/services/Ai.qml @@ -25,15 +25,18 @@ Singleton { // - model: Model name of the model // - requires_key: Whether the model requires an API key // - key_id: The identifier of the API key. Use the same identifier for models that can be accessed with the same key. + // - key_get_link: Link to get the API key property var models: { "gemini-2.0-flash": { "name": "Gemini 2.0 Flash", "icon": "google-gemini-symbolic", - "description": "Online Gemini 2.0 Flash", + "description": "Online | Google's model", + "homepage": "https://aistudio.google.com", "endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", "model": "gemini-2.0-flash", "requires_key": true, "key_id": "gemini", + "key_get_link": "https://aistudio.google.com/app/apikey", }, } property var modelList: Object.keys(root.models) @@ -54,11 +57,14 @@ Singleton { function guessModelName(model) { const replaced = model.replace(/-/g, ' ').replace(/:/g, ' '); - const words = replaced.split(' '); + let words = replaced.split(' '); words[words.length - 1] = words[words.length - 1].replace(/(\d+)b$/, (_, num) => `${num}B`) + words = words.map((word) => { + return (word.charAt(0).toUpperCase() + word.slice(1)) + }); words[words.length - 1] = `[${words[words.length - 1]}]`; // Surround the last word with square brackets const result = words.join(' '); - return result.charAt(0).toUpperCase() + result.slice(1); // Capitalize the first letter + return result; } Process { @@ -75,6 +81,7 @@ Singleton { "name": guessModelName(model), "icon": guessModelLogo(model), "description": `Local Ollama model: ${model}`, + "homepage": `https://ollama.com/library/${model}`, "endpoint": "http://localhost:11434/v1/chat/completions", "model": model, } @@ -115,17 +122,21 @@ Singleton { } function setApiKey(key) { - if (!key || key.length === 0) { - root.addMessage("Please enter an API key with the command", Ai.interfaceRole); + const model = models[currentModel]; + if (!model.requires_key) { + root.addMessage(`${model.name} does not require an API key`, Ai.interfaceRole); return; } - const model = models[currentModel]; - if (model.requires_key) { - KeyringStorage.setNestedField(["apiKeys", model.key_id], key); - root.addMessage("API key set for " + model.name, Ai.interfaceRole); - } else { - root.addMessage(`This model (${model.name}) does not require an API key`, Ai.interfaceRole); + if (!key || key.length === 0) { + root.addMessage( + StringUtils.format(qsTr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command

For {0}, you can grab one at:\n\n{1}'), + models[currentModel].name, models[currentModel].key_get_link), + Ai.interfaceRole + ); + return; } + KeyringStorage.setNestedField(["apiKeys", model.key_id], key); + root.addMessage("API key set for " + model.name, Ai.interfaceRole); } function printApiKey() { @@ -133,9 +144,9 @@ Singleton { if (model.requires_key) { const key = root.apiKeys[model.key_id]; if (key) { - root.addMessage("API key: \n\n`" + key + "`", Ai.interfaceRole); + root.addMessage(StringUtils.format(qsTr("API key:\n\n`{0}`"), key), Ai.interfaceRole); } else { - root.addMessage("No API key set for " + model.name, Ai.interfaceRole); + root.addMessage(StringUtils.format(qsTr("No API key set for {0}"), model.name), Ai.interfaceRole); } } else { root.addMessage(`This model (${model.name}) does not require an API key`, Ai.interfaceRole); @@ -225,11 +236,13 @@ Singleton { return; } const dataJson = JSON.parse(cleanData); - requester.message.content += + const newContent = (dataJson.message?.content) ?? // Ollama (dataJson.choices[0]?.delta?.content) ?? // Normal (dataJson.choices[0]?.delta?.reasoning_content) // Deepseek thinking + requester.message.content += newContent; + if (dataJson.done) requester.message.done = true; } catch (e) { requester.message.content += cleanData; diff --git a/.config/quickshell/services/KeyringStorage.qml b/.config/quickshell/services/KeyringStorage.qml index d356a1a0..ef51c3ac 100644 --- a/.config/quickshell/services/KeyringStorage.qml +++ b/.config/quickshell/services/KeyringStorage.qml @@ -28,17 +28,34 @@ Singleton { function setNestedField(path, value) { if (!root.keyringData) root.keyringData = {}; - let keys = path + let keys = path; let obj = root.keyringData; + let parents = [obj]; + + // Traverse and collect parent objects for (let i = 0; i < keys.length - 1; ++i) { if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") { obj[keys[i]] = {}; } obj = obj[keys[i]]; + parents.push(obj); } + + // Set the value at the innermost key obj[keys[keys.length - 1]] = value; - // console.log("[KeyringStorage] Updated keyring data:", JSON.stringify(root.keyringData)); - saveKeyringData() + + // Reassign each parent object from the bottom up to trigger change notifications + for (let i = keys.length - 2; i >= 0; --i) { + let parent = parents[i]; + let key = keys[i]; + // Shallow clone to change object identity (spread replaced with Object.assign) + parent[key] = Object.assign({}, parent[key]); + } + + // Finally, reassign root.keyringData to trigger top-level change + root.keyringData = Object.assign({}, root.keyringData); + + saveKeyringData(); } function fetchKeyringData() {