From fe84f6cab13fc5f8b84fdb6bd151792e9a8f5639 Mon Sep 17 00:00:00 2001 From: Anton Epikhin Date: Fri, 25 Jul 2025 11:33:16 +0300 Subject: [PATCH 01/27] init indicator with main keyboard and update layout on every activelayout event --- .config/quickshell/ii/services/HyprlandXkb.qml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.config/quickshell/ii/services/HyprlandXkb.qml b/.config/quickshell/ii/services/HyprlandXkb.qml index 76a7bd35d..ae791311f 100644 --- a/.config/quickshell/ii/services/HyprlandXkb.qml +++ b/.config/quickshell/ii/services/HyprlandXkb.qml @@ -16,7 +16,6 @@ Singleton { property string currentLayoutName: "" property string currentLayoutCode: "" // For the service - property string targetDeviceName: "hl-virtual-keyboard" property var baseLayoutFilePath: "/usr/share/X11/xkb/rules/base.lst" property bool needsLayoutRefresh: false @@ -71,7 +70,7 @@ Singleton { id: devicesCollector onStreamFinished: { const parsedOutput = JSON.parse(devicesCollector.text); - const hyprlandKeyboard = parsedOutput["keyboards"].find(kb => kb.name === root.targetDeviceName); + const hyprlandKeyboard = parsedOutput["keyboards"].find(kb => kb.main === true); root.layoutCodes = hyprlandKeyboard["layout"].split(","); root.currentLayoutName = hyprlandKeyboard["active_keymap"]; // console.log("[HyprlandXkb] Fetched | Layouts (multiple: " + (root.layouts.length > 1) + "): " @@ -85,8 +84,6 @@ Singleton { target: Hyprland function onRawEvent(event) { if (event.name === "activelayout") { - // We're triggering refresh here because Hyprland virtual kb after a config reload disappears - // from `hyprctl devices` and it only comes back at the next activelayout event. if (root.needsLayoutRefresh) { root.needsLayoutRefresh = false; fetchLayoutsProc.running = true; @@ -96,10 +93,7 @@ Singleton { if (root.layoutCodes.length <= 1) return; // Update when layout might have changed - const dataString = event.data; - if (!dataString.startsWith(root.targetDeviceName)) - return; - root.currentLayoutName = dataString.split(",")[1]; + root.currentLayoutName = event.data.split(",")[1]; } else if (event.name == "configreloaded") { // Mark layout code list to be updated when config is reloaded root.needsLayoutRefresh = true; From c8b007631d3d6c26197f9c7724a65016bb498550 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 25 Jul 2025 20:14:37 +0700 Subject: [PATCH 02/27] ai: refractor api formats --- .config/quickshell/ii/services/Ai.qml | 362 ++++-------------- .../ii/services/{ => ai}/AiMessageData.qml | 1 - .config/quickshell/ii/services/ai/AiModel.qml | 33 ++ .../quickshell/ii/services/ai/ApiStrategy.qml | 10 + .../ii/services/ai/GeminiApiStrategy.qml | 156 ++++++++ .../ii/services/ai/OpenAiApiStrategy.qml | 97 +++++ 6 files changed, 379 insertions(+), 280 deletions(-) rename .config/quickshell/ii/services/{ => ai}/AiMessageData.qml (95%) create mode 100644 .config/quickshell/ii/services/ai/AiModel.qml create mode 100644 .config/quickshell/ii/services/ai/ApiStrategy.qml create mode 100644 .config/quickshell/ii/services/ai/GeminiApiStrategy.qml create mode 100644 .config/quickshell/ii/services/ai/OpenAiApiStrategy.qml diff --git a/.config/quickshell/ii/services/Ai.qml b/.config/quickshell/ii/services/Ai.qml index 82386fada..f73652551 100644 --- a/.config/quickshell/ii/services/Ai.qml +++ b/.config/quickshell/ii/services/Ai.qml @@ -7,6 +7,7 @@ import qs import Quickshell import Quickshell.Io import QtQuick +import "./ai/" /** * Basic service to handle LLM chats. Supports Google's and OpenAI's API formats. @@ -14,9 +15,13 @@ import QtQuick Singleton { id: root + property Component aiMessageComponent: AiMessageData {} + property Component aiModelComponent: AiModel {} + property Component geminiApiStrategy: GeminiApiStrategy {} + property Component openaiApiStrategy: OpenAiApiStrategy {} readonly property string interfaceRole: "interface" readonly property string apiKeyEnvVarName: "API_KEY" - property Component aiMessageComponent: AiMessageData {} + property string systemPrompt: Config.options?.ai?.systemPrompt ?? "" // property var messages: [] property var messageIDs: [] @@ -126,7 +131,7 @@ Singleton { // - tools: List of tools that the model can use. Each tool is an object with the tool name as the key and an empty object as the value. // - extraParams: Extra parameters to be passed to the model. This is a JSON object. property var models: { - "gemini-2.0-flash-search": { + "gemini-2.0-flash-search": aiModelComponent.createObject(this, { "name": "Gemini 2.0 Flash (Search)", "icon": "google-gemini-symbolic", "description": Translation.tr("Online | Google's model\nGives up-to-date information with search."), @@ -141,8 +146,8 @@ Singleton { "tools": [{ "google_search": {} }] - }, - "gemini-2.0-flash-tools": { + }), + "gemini-2.0-flash-tools": aiModelComponent.createObject(this, { "name": "Gemini 2.0 Flash (Tools)", "icon": "google-gemini-symbolic", "description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but takes an extra turn to perform search"), @@ -155,8 +160,8 @@ Singleton { "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", "tools": root.tools["gemini"], - }, - "gemini-2.5-flash-search": { + }), + "gemini-2.5-flash-search": aiModelComponent.createObject(this, { "name": "Gemini 2.5 Flash (Search)", "icon": "google-gemini-symbolic", "description": Translation.tr("Online | Google's model\nGives up-to-date information with search."), @@ -171,8 +176,8 @@ Singleton { "tools": [{ "google_search": {} }] - }, - "gemini-2.5-flash-tools": { + }), + "gemini-2.5-flash-tools": aiModelComponent.createObject(this, { "name": "Gemini 2.5 Flash (Tools)", "icon": "google-gemini-symbolic", "description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but takes an extra turn to perform search"), @@ -185,21 +190,8 @@ Singleton { "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", "tools": root.tools["gemini"], - }, - "gemini-2.5-flash-lite": { - "name": "Gemini 2.5 Flash-Lite", - "icon": "google-gemini-symbolic", - "description": Translation.tr("Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."), - "homepage": "https://aistudio.google.com", - "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent", - "model": "gemini-2.5-flash-lite", - "requires_key": true, - "key_id": "gemini", - "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), - "api_format": "gemini", - }, - "gemini-2.5-flash-lite-search": { + }), + "gemini-2.5-flash-lite-search": aiModelComponent.createObject(this, { "name": "Gemini 2.5 Flash-Lite (Search)", "icon": "google-gemini-symbolic", "description": Translation.tr("Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."), @@ -214,8 +206,22 @@ Singleton { "tools": [{ "google_search": {} }] - }, - "openrouter-llama4-maverick": { + }), + "gemini-2.5-flash-lite": aiModelComponent.createObject(this, { + "name": "Gemini 2.5 Flash-Lite", + "icon": "google-gemini-symbolic", + "description": Translation.tr("Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."), + "homepage": "https://aistudio.google.com", + "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent", + "model": "gemini-2.5-flash-lite", + "requires_key": true, + "key_id": "gemini", + "key_get_link": "https://aistudio.google.com/app/apikey", + "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), + "api_format": "gemini", + "tools": root.tools["gemini"], + }), + "openrouter-llama4-maverick": aiModelComponent.createObject(this, { "name": "Llama 4 Maverick", "icon": "ollama-symbolic", "description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("Meta"), @@ -226,8 +232,8 @@ Singleton { "key_id": "openrouter", "key_get_link": "https://openrouter.ai/settings/keys", "key_get_description": Translation.tr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"), - }, - "openrouter-deepseek-r1": { + }), + "openrouter-deepseek-r1": aiModelComponent.createObject(this, { "name": "DeepSeek R1", "icon": "deepseek-symbolic", "description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("DeepSeek"), @@ -238,11 +244,17 @@ Singleton { "key_id": "openrouter", "key_get_link": "https://openrouter.ai/settings/keys", "key_get_description": Translation.tr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"), - }, + }), } property var modelList: Object.keys(root.models) property var currentModelId: Persistent.states?.ai?.model || modelList[0] + property var apiStrategies: { + "openai": openaiApiStrategy.createObject(this), + "gemini": geminiApiStrategy.createObject(this), + } + property ApiStrategy currentApiStrategy: apiStrategies[models[currentModelId]?.api_format || "openai"] + Component.onCompleted: { setModel(currentModelId, false, false); // Do necessary setup for model } @@ -280,14 +292,15 @@ Singleton { root.modelList = [...root.modelList, ...dataJson]; dataJson.forEach(model => { const safeModelName = root.safeModelName(model); - root.models[safeModelName] = { + root.models[safeModelName] = aiModelComponent.createObject(this, { "name": guessModelName(model), "icon": guessModelLogo(model), "description": Translation.tr("Local Ollama model | %1").arg(model), "homepage": `https://ollama.com/library/${model}`, "endpoint": "http://localhost:11434/v1/chat/completions", "model": model, - } + "requires_key": false, + }) }); root.modelList = Object.keys(root.models); @@ -473,24 +486,16 @@ Singleton { function clearMessages() { root.messageIDs = []; root.messageByID = ({}); + root.tokenCount.input = -1; + root.tokenCount.output = -1; + root.tokenCount.total = -1; } Process { id: requester property var baseCommand: ["bash", "-c"] - property var message - property bool isReasoning - property string apiFormat: "openai" - property string geminiBuffer: "" - - function buildGeminiEndpoint(model) { - // console.log("ENDPOINT: " + model.endpoint + `?key=\$\{${root.apiKeyEnvVarName}\}`) - return model.endpoint + `?key=\$\{${root.apiKeyEnvVarName}\}`; - } - - function buildOpenAIEndpoint(model) { - return model.endpoint; - } + property AiMessageData message + property ApiStrategy currentStrategy function markDone() { requester.message.done = true; @@ -501,84 +506,20 @@ Singleton { root.saveChat("lastSession") } - function buildGeminiRequestData(model, messages) { - const tools = [ - ...(model.tools ?? root.tools[model.api_format]), - ] - // console.log("Tools", JSON.stringify(tools, null, 2)); - let baseData = { - "contents": messages.filter(message => (message.role != Ai.interfaceRole)).map(message => { - const geminiApiRoleName = (message.role === "assistant") ? "model" : message.role; - const usingSearch = tools[0].google_search != undefined - if (!usingSearch && message.functionCall != undefined && message.functionCall.length > 0) { - return { - "role": geminiApiRoleName, - "parts": [{ - functionCall: { - "name": message.functionName, - } - }] - } - } - if (!usingSearch && message.functionResponse != undefined && message.functionResponse.length > 0) { - return { - "role": geminiApiRoleName, - "parts": [{ - functionResponse: { - "name": message.functionName, - "response": { "content": message.functionResponse } - } - }] - } - } - return { - "role": geminiApiRoleName, - "parts": [{ - text: message.rawContent, - }] - } - }), - "tools": tools, - "system_instruction": { - "parts": [{ text: root.systemPrompt }] - }, - "generationConfig": { - "temperature": root.temperature, - }, - }; - return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData; - } - - function buildOpenAIRequestData(model, messages) { - let baseData = { - "model": model.model, - "messages": [ - {role: "system", content: root.systemPrompt}, - ...messages.filter(message => (message.role != Ai.interfaceRole)).map(message => { - return { - "role": message.role, - "content": message.rawContent, - } - }), - ], - "stream": true, - "temperature": root.temperature, - }; - return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData; - } - function makeRequest() { const model = models[currentModelId]; - requester.apiFormat = model.api_format ?? "openai"; + requester.currentStrategy = root.currentApiStrategy; + requester.currentStrategy.reset(); // Reset strategy state /* Put API key in environment variable */ if (model.requires_key) requester.environment[`${root.apiKeyEnvVarName}`] = root.apiKeys ? (root.apiKeys[model.key_id] ?? "") : "" /* Build endpoint, request data */ - const endpoint = (apiFormat === "gemini") ? buildGeminiEndpoint(model) : buildOpenAIEndpoint(model); + const endpoint = root.currentApiStrategy.buildEndpoint(model); const messageArray = root.messageIDs.map(id => root.messageByID[id]); - const data = (apiFormat === "gemini") ? buildGeminiRequestData(model, messageArray) : buildOpenAIRequestData(model, messageArray); - // console.log("REQUEST DATA: ", JSON.stringify(data, null, 2)); + const filteredMessageArray = messageArray.filter(message => message.role !== Ai.interfaceRole); + const data = root.currentApiStrategy.buildRequestData(model, filteredMessageArray, root.systemPrompt, root.temperature); + // console.log("[Ai] Request data: ", JSON.stringify(data, null, 2)); let requestHeaders = { "Content-Type": "application/json", @@ -606,166 +547,45 @@ Singleton { // console.log("Request headers: ", JSON.stringify(requestHeaders)); // console.log("Header string: ", headerString); + /* Get authorization header from strategy */ + const authHeader = requester.currentStrategy.buildAuthorizationHeader(root.apiKeyEnvVarName); + /* Create command string */ const requestCommandString = `curl --no-buffer "${endpoint}"` + ` ${headerString}` - + ((apiFormat == "gemini") ? "" : ` -H "Authorization: Bearer \$\{${root.apiKeyEnvVarName}\}"`) + + (authHeader ? ` ${authHeader}` : "") + ` -d '${CF.StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'` - // console.log("Request command: ", requestCommandString); + + /* Send the request */ requester.command = baseCommand.concat([requestCommandString]); - - /* Reset vars and make the request */ - requester.isReasoning = false requester.running = true } - function parseGeminiBuffer() { - // console.log("BUFFER DATA: ", requester.geminiBuffer); - try { - if (requester.geminiBuffer.length === 0) return; - const dataJson = JSON.parse(requester.geminiBuffer); - if (!dataJson.candidates) return; - - if (dataJson.candidates[0]?.finishReason) { - requester.markDone(); - } - // Function call handling - if (dataJson.candidates[0]?.content?.parts[0]?.functionCall) { - const functionCall = dataJson.candidates[0]?.content?.parts[0]?.functionCall; - requester.message.functionName = functionCall.name; - requester.message.functionCall = functionCall.name; - const newContent = `\n\n[[ Function: ${functionCall.name}(${JSON.stringify(functionCall.args, null, 2)}) ]]\n` - requester.message.rawContent += newContent; - requester.message.content += newContent; - root.handleGeminiFunctionCall(functionCall.name, functionCall.args); - return - } - - // Normal text response - const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text - requester.message.rawContent += responseContent; - requester.message.content += responseContent; - const annotationSources = dataJson.candidates[0]?.groundingMetadata?.groundingChunks?.map(chunk => { - return { - "type": "url_citation", - "text": chunk?.web?.title, - "url": chunk?.web?.uri, - } - }) ?? []; - - // Handle annotations and search queries - const annotations = dataJson.candidates[0]?.groundingMetadata?.groundingSupports?.map(citation => { - return { - "type": "url_citation", - "start_index": citation.segment?.startIndex, - "end_index": citation.segment?.endIndex, - "text": citation?.segment.text, - "url": annotationSources[citation.groundingChunkIndices[0]]?.url, - "sources": citation.groundingChunkIndices - } - }); - requester.message.annotationSources = annotationSources; - requester.message.annotations = annotations; - requester.message.searchQueries = dataJson.candidates[0]?.groundingMetadata?.webSearchQueries ?? []; - // console.log("[AI] Gemini: Search queries: ", JSON.stringify(requester.message.searchQueries, null, 2)); - - // Usage - root.tokenCount.input = dataJson.usageMetadata?.promptTokenCount ?? -1; - root.tokenCount.output = dataJson.usageMetadata?.candidatesTokenCount ?? -1; - root.tokenCount.total = dataJson.usageMetadata?.totalTokenCount ?? -1; - // console.log("[AI] Gemini: Token count: ", root.tokenCount); - - // Last logging - // console.log(JSON.stringify(requester.message, null, 2)); - } catch (e) { - console.log("[AI] Gemini: Could not parse buffer: ", e); - requester.message.rawContent += requester.geminiBuffer; - requester.message.content += requester.geminiBuffer - } finally { - requester.geminiBuffer = ""; - } - } - - function handleGeminiResponseLine(line) { - if (line.startsWith("[")) { - requester.geminiBuffer += line.slice(1).trim(); - } else if (line == "]") { - requester.geminiBuffer += line.slice(0, -1).trim(); - parseGeminiBuffer(); - } else if (line.startsWith(",")) { // end of one entry - parseGeminiBuffer(); - } else { - requester.geminiBuffer += line.trim(); - } - } - - function handleOpenAIResponseLine(line) { - // Remove 'data: ' prefix if present and trim whitespace - let cleanData = line.trim(); - if (cleanData.startsWith("data:")) { - cleanData = cleanData.slice(5).trim(); - } - // console.log("Clean data: ", cleanData); - if (!cleanData || cleanData.startsWith(":")) return; - - if (cleanData === "[DONE]") { - requester.markDone(); - return; - } - const dataJson = JSON.parse(cleanData); - - let newContent = ""; - const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content; - const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content; - - if (responseContent && responseContent.length > 0) { - if (requester.isReasoning) { - requester.isReasoning = false; - const endBlock = "\n\n\n\n"; - requester.message.content += endBlock; - requester.message.rawContent += endBlock; - } - newContent = dataJson.choices[0]?.delta?.content || dataJson.message.content; - } else if (responseReasoning && responseReasoning.length > 0) { - // console.log("Reasoning content: ", dataJson.choices[0].delta.reasoning); - if (!requester.isReasoning) { - requester.isReasoning = true; - const startBlock = "\n\n\n\n"; - requester.message.rawContent += startBlock; - requester.message.content += startBlock; - } - newContent = dataJson.choices[0].delta.reasoning || dataJson.choices[0].delta.reasoning_content; - } - - requester.message.content += newContent; - requester.message.rawContent += newContent; - - if (dataJson.done) { - requester.markDone(); - } - } - stdout: SplitParser { onRead: data => { - // console.log("RAW DATA: ", data); + // console.log("[Ai] Raw response line: ", data); if (data.length === 0) return; + if (requester.message.thinking) requester.message.thinking = false; // Handle response line - if (requester.message.thinking) requester.message.thinking = false; try { - if (requester.apiFormat === "gemini") { - requester.handleGeminiResponseLine(data); + const result = requester.currentStrategy.parseResponseLine(data, requester.message); + // console.log("[Ai] Parsed response result: ", JSON.stringify(result, null, 2)); + + if (result.functionCall) { + root.handleFunctionCall(result.functionCall.name, result.functionCall.args); } - else if (requester.apiFormat === "openai") { - requester.handleOpenAIResponseLine(data); + if (result.tokenUsage) { + root.tokenCount.input = result.tokenUsage.input; + root.tokenCount.output = result.tokenUsage.output; + root.tokenCount.total = result.tokenUsage.total; } - else { - console.log("Unknown API format: ", requester.apiFormat); - requester.message.rawContent += data; - requester.message.content += data; + if (result.finished) { + requester.markDone(); } + } catch (e) { - console.log("[AI] Could not parse response from stream: ", e); + console.log("[AI] Could not parse response: ", e); requester.message.rawContent += data; requester.message.content += data; } @@ -773,18 +593,15 @@ Singleton { } onExited: (exitCode, exitStatus) => { - if (requester.apiFormat == "gemini") requester.parseGeminiBuffer(); - else requester.markDone(); - - try { // to parse full response into json for error handling - // console.log("Full response: ", requester.message.content + "]"); - const parsedResponse = JSON.parse(requester.message.rawContent + "]"); - requester.message.rawContent = `\`\`\`json\n${JSON.stringify(parsedResponse, null, 2)}\n\`\`\``; - requester.message.content = requester.message.rawContent; - } catch (e) { - // console.log("[AI] Could not parse response on exit: ", e); + const result = requester.currentStrategy.onRequestFinished(requester.message); + + if (result.finished) { + requester.markDone(); + } else if (!requester.message.done) { + requester.markDone(); } + // Handle error responses if (requester.message.content.includes("API key not valid")) { root.addApiKeyAdvice(models[requester.message.model]); } @@ -814,20 +631,7 @@ Singleton { root.messageByID[id] = aiMessage; } - function buildGeminiFunctionOutput(name, output) { - const functionResponsePart = { - "name": name, - "response": { "content": output } - } - return { - "role": "user", - "parts": [{ - functionResponse: functionResponsePart, - }] - } - } - - function handleGeminiFunctionCall(name, args) { + function handleFunctionCall(name, args) { if (name === "switch_to_search_mode") { const modelId = root.currentModelId; if (modelId.endsWith("-tools")) { diff --git a/.config/quickshell/ii/services/AiMessageData.qml b/.config/quickshell/ii/services/ai/AiMessageData.qml similarity index 95% rename from .config/quickshell/ii/services/AiMessageData.qml rename to .config/quickshell/ii/services/ai/AiMessageData.qml index 4695afe5f..8813a70b0 100644 --- a/.config/quickshell/ii/services/AiMessageData.qml +++ b/.config/quickshell/ii/services/ai/AiMessageData.qml @@ -1,4 +1,3 @@ -import qs.modules.common import QtQuick; /** diff --git a/.config/quickshell/ii/services/ai/AiModel.qml b/.config/quickshell/ii/services/ai/AiModel.qml new file mode 100644 index 000000000..5544cf169 --- /dev/null +++ b/.config/quickshell/ii/services/ai/AiModel.qml @@ -0,0 +1,33 @@ +import QtQuick; + +/** + * An AI model representation. + * - name: Friendly name of the model + * - icon: Icon name of the model + * - description: Description of the model + * - endpoint: Endpoint of the model + * - model: Model code (like gpt-4.1 or gemini-2.5-flash) + * - 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 an API key + * - key_get_description: Description of pricing and how to get an API key + * - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai". + * - tools: List of tools that the model can use. + * - extraParams: Extra parameters to be passed to the model. This is a JSON object. + */ + +QtObject { + property string name + property string icon + property string description + property string homepage + property string endpoint + property string model + property bool requires_key: true + property string key_id + property string key_get_link + property string key_get_description + property string api_format: "openai" + property var tools + property var extraParams: ({}) +} diff --git a/.config/quickshell/ii/services/ai/ApiStrategy.qml b/.config/quickshell/ii/services/ai/ApiStrategy.qml new file mode 100644 index 000000000..d63f932df --- /dev/null +++ b/.config/quickshell/ii/services/ai/ApiStrategy.qml @@ -0,0 +1,10 @@ +import QtQuick + +QtObject { + function buildEndpoint(model: AiModel): string { throw new Error("Not implemented") } + function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real) { throw new Error("Not implemented") } + function buildAuthorizationHeader(apiKeyEnvVarName: string): string { throw new Error("Not implemented") } + function parseResponseLine(line: string, message: AiMessageData) { throw new Error("Not implemented") } + function onRequestFinished(message: AiMessageData): var { return {} } // Default: no special handling + function reset() { } // Reset any internal state if needed +} diff --git a/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml b/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml new file mode 100644 index 000000000..733dda51c --- /dev/null +++ b/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml @@ -0,0 +1,156 @@ +import QtQuick + +ApiStrategy { + property string buffer: "" + + function buildEndpoint(model: AiModel): string { + const result = model.endpoint + `?key=\$\{${root.apiKeyEnvVarName}\}` + console.log("[AI] Endpoint: " + result); + return result; + } + + function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real) { + const tools = model.tools ?? []; + let baseData = { + "contents": messages.map(message => { + const geminiApiRoleName = (message.role === "assistant") ? "model" : message.role; + const usingSearch = tools[0].google_search != undefined + if (!usingSearch && message.functionCall != undefined && message.functionCall.length > 0) { + return { + "role": geminiApiRoleName, + "parts": [{ + functionCall: { + "name": message.functionName, + } + }] + } + } + if (!usingSearch && message.functionResponse != undefined && message.functionResponse.length > 0) { + return { + "role": geminiApiRoleName, + "parts": [{ + functionResponse: { + "name": message.functionName, + "response": { "content": message.functionResponse } + } + }] + } + } + return { + "role": geminiApiRoleName, + "parts": [{ + text: message.rawContent, + }] + } + }), + "tools": tools, + "system_instruction": { + "parts": [{ text: systemPrompt }] + }, + "generationConfig": { + "temperature": temperature, + }, + }; + return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData; + } + + function buildAuthorizationHeader(apiKeyEnvVarName: string): string { + // Gemini doesn't use Authorization header, key is in URL + return ""; + } + + function parseResponseLine(line, message) { + if (line.startsWith("[")) { + buffer += line.slice(1).trim(); + } else if (line === "]") { + buffer += line.slice(0, -1).trim(); + return parseBuffer(message); + } else if (line.startsWith(",")) { + return parseBuffer(message); + } else { + buffer += line.trim(); + } + return {}; + } + + function parseBuffer(message) { + // console.log("[Ai] Gemini buffer: ", buffer); + let finished = false; + try { + if (buffer.length === 0) return {}; + const dataJson = JSON.parse(buffer); + if (!dataJson.candidates) return {}; + + if (dataJson.candidates[0]?.finishReason) { + finished = true; + } + + // Function call handling + if (dataJson.candidates[0]?.content?.parts[0]?.functionCall) { + const functionCall = dataJson.candidates[0]?.content?.parts[0]?.functionCall; + message.functionName = functionCall.name; + message.functionCall = functionCall.name; + const newContent = `\n\n[[ Function: ${functionCall.name}(${JSON.stringify(functionCall.args, null, 2)}) ]]\n` + message.rawContent += newContent; + message.content += newContent; + return { functionCall: { name: functionCall.name, args: functionCall.args }, finished: finished }; + } + + // Normal text response + const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text + message.rawContent += responseContent; + message.content += responseContent; + + // Handle annotations and metadata + const annotationSources = dataJson.candidates[0]?.groundingMetadata?.groundingChunks?.map(chunk => { + return { + "type": "url_citation", + "text": chunk?.web?.title, + "url": chunk?.web?.uri, + } + }) ?? []; + + const annotations = dataJson.candidates[0]?.groundingMetadata?.groundingSupports?.map(citation => { + return { + "type": "url_citation", + "start_index": citation.segment?.startIndex, + "end_index": citation.segment?.endIndex, + "text": citation?.segment.text, + "url": annotationSources[citation.groundingChunkIndices[0]]?.url, + "sources": citation.groundingChunkIndices + } + }); + message.annotationSources = annotationSources; + message.annotations = annotations; + message.searchQueries = dataJson.candidates[0]?.groundingMetadata?.webSearchQueries ?? []; + + // Usage metadata + if (dataJson.usageMetadata) { + return { + tokenUsage: { + input: dataJson.usageMetadata.promptTokenCount ?? -1, + output: dataJson.usageMetadata.candidatesTokenCount ?? -1, + total: dataJson.usageMetadata.totalTokenCount ?? -1 + }, + finished: finished + }; + } + + } catch (e) { + console.log("[AI] Gemini: Could not parse buffer: ", e); + message.rawContent += buffer; + message.content += buffer; + } finally { + buffer = ""; + } + return { finished: finished }; + } + + function onRequestFinished(message) { + return parseBuffer(message); + } + + function reset() { + buffer = ""; + } +} diff --git a/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml b/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml new file mode 100644 index 000000000..ebc009706 --- /dev/null +++ b/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml @@ -0,0 +1,97 @@ +import QtQuick + +ApiStrategy { + property bool isReasoning: false + + function buildEndpoint(model: AiModel): string { + console.log("[AI] Endpoint: " + model.endpoint); + return model.endpoint; + } + + function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real) { + let baseData = { + "model": model.model, + "messages": [ + {role: "system", content: systemPrompt}, + ...messages.map(message => { + return { + "role": message.role, + "content": message.rawContent, + } + }), + ], + "stream": true, + "temperature": temperature, + }; + return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData; + } + + function buildAuthorizationHeader(apiKeyEnvVarName: string): string { + return `-H "Authorization: Bearer \$\{${apiKeyEnvVarName}\}"`; + } + + function parseResponseLine(line, message) { + // Remove 'data: ' prefix if present and trim whitespace + let cleanData = line.trim(); + if (cleanData.startsWith("data:")) { + cleanData = cleanData.slice(5).trim(); + } + + // Handle special cases + if (!cleanData || cleanData.startsWith(":")) return {}; + if (cleanData === "[DONE]") { + return { finished: true }; + } + + // Real stuff + try { + const dataJson = JSON.parse(cleanData); + let newContent = ""; + + const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content; + const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content; + + if (responseContent && responseContent.length > 0) { + if (isReasoning) { + isReasoning = false; + const endBlock = "\n\n\n\n"; + message.content += endBlock; + message.rawContent += endBlock; + } + newContent = responseContent; + } else if (responseReasoning && responseReasoning.length > 0) { + if (!isReasoning) { + isReasoning = true; + const startBlock = "\n\n\n\n"; + message.rawContent += startBlock; + message.content += startBlock; + } + newContent = responseReasoning; + } + + message.content += newContent; + message.rawContent += newContent; + + if (dataJson.done) { + return { finished: true }; + } + + } catch (e) { + console.log("[AI] OpenAI: Could not parse line: ", e); + message.rawContent += line; + message.content += line; + } + + return {}; + } + + function onRequestFinished(message) { + // OpenAI format doesn't need special finish handling + return {}; + } + + function reset() { + isReasoning = false; + } + +} From 5099ce15db535c13b2a919d800c5cad474726b93 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:09:17 +0700 Subject: [PATCH 03/27] session: detect running downloads --- .../quickshell/ii/modules/session/Session.qml | 91 +++++++++++-------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/.config/quickshell/ii/modules/session/Session.qml b/.config/quickshell/ii/modules/session/Session.qml index 8dfa4aa9a..c573ebd0a 100644 --- a/.config/quickshell/ii/modules/session/Session.qml +++ b/.config/quickshell/ii/modules/session/Session.qml @@ -15,6 +15,29 @@ Scope { id: root property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) property bool packageManagerRunning: false + property bool downloadRunning: false + + component DescriptionLabel: Rectangle { + id: descriptionLabel + property string text + property color textColor: Appearance.colors.colOnTooltip + color: Appearance.colors.colTooltip + clip: true + radius: Appearance.rounding.normal + implicitHeight: descriptionLabelText.implicitHeight + 10 * 2 + implicitWidth: descriptionLabelText.implicitWidth + 15 * 2 + + Behavior on implicitWidth { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + StyledText { + id: descriptionLabelText + anchors.centerIn: parent + color: descriptionLabel.textColor + text: descriptionLabel.text + } + } function closeAllWindows() { HyprlandData.windowList.map(w => w.pid).forEach((pid) => { @@ -22,10 +45,13 @@ Scope { }); } - function detectRunningPackageManager() { + function detectRunningStuff() { packageManagerRunning = false; + downloadRunning = false; detectPackageManagerProc.running = false; detectPackageManagerProc.running = true; + detectDownloadProc.running = false; + detectDownloadProc.running = true; } Process { @@ -37,11 +63,19 @@ Scope { } } + Process { + id: detectDownloadProc + command: ["bash", "-c", "pidof curl wget aria2c yt-dlp || ls ~/Downloads | grep -E '\.crdownload$|\.part$'"] + onExited: (exitCode, exitStatus) => { + root.downloadRunning = (exitCode === 0); + } + } + Loader { id: sessionLoader active: false onActiveChanged: { - if (sessionLoader.active) root.detectRunningPackageManager(); + if (sessionLoader.active) root.detectRunningStuff(); } Connections { @@ -201,54 +235,39 @@ Scope { } } - Rectangle { + DescriptionLabel { Layout.alignment: Qt.AlignHCenter - radius: Appearance.rounding.normal - implicitHeight: sessionSubtitle.implicitHeight + 10 * 2 - implicitWidth: sessionSubtitle.implicitWidth + 15 * 2 - color: Appearance.colors.colTooltip - clip: true - - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - StyledText { - id: sessionSubtitle - anchors.centerIn: parent - color: Appearance.colors.colOnTooltip - text: sessionRoot.subtitle - } + text: sessionRoot.subtitle } } - Loader { - active: root.packageManagerRunning + RowLayout { anchors { top: contentColumn.bottom topMargin: 10 horizontalCenter: contentColumn.horizontalCenter } - sourceComponent: Rectangle { - radius: Appearance.rounding.normal - implicitHeight: sessionWarning.implicitHeight + 10 * 2 - implicitWidth: sessionWarning.implicitWidth + 15 * 2 - color: Appearance.m3colors.m3errorContainer - clip: true + spacing: 10 - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - StyledText { - id: sessionWarning - anchors.centerIn: parent - color: Appearance.m3colors.m3onErrorContainer + Loader { + active: root.packageManagerRunning + visible: active + sourceComponent: DescriptionLabel { text: Translation.tr("Your package manager is running") + textColor: Appearance.m3colors.m3onErrorContainer + color: Appearance.m3colors.m3errorContainer + } + } + Loader { + active: root.downloadRunning + visible: active + sourceComponent: DescriptionLabel { + text: Translation.tr("There might be a download in progress") + textColor: Appearance.m3colors.m3onErrorContainer + color: Appearance.m3colors.m3errorContainer } } } - } } From 7fb81049f3407c588f499e80ee59e857f7e83118 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 26 Jul 2025 09:09:52 +0700 Subject: [PATCH 04/27] welcome app: fix material theme --- .config/quickshell/ii/welcome.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/quickshell/ii/welcome.qml b/.config/quickshell/ii/welcome.qml index bfcb2015f..14656ed71 100644 --- a/.config/quickshell/ii/welcome.qml +++ b/.config/quickshell/ii/welcome.qml @@ -13,6 +13,7 @@ import Quickshell import Quickshell.Io import Quickshell.Hyprland import qs +import qs.services import qs.modules.common import qs.modules.common.widgets import qs.modules.common.functions From c69c8f6ef5d91fb806a2f2eaaf1f2f4d0a5fa805 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 26 Jul 2025 14:12:43 +0700 Subject: [PATCH 05/27] osd: make spinning brightness icon not wiggle --- .../ii/modules/onScreenDisplay/OsdValueIndicator.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/onScreenDisplay/OsdValueIndicator.qml b/.config/quickshell/ii/modules/onScreenDisplay/OsdValueIndicator.qml index c6fc78380..6edd24a49 100644 --- a/.config/quickshell/ii/modules/onScreenDisplay/OsdValueIndicator.qml +++ b/.config/quickshell/ii/modules/onScreenDisplay/OsdValueIndicator.qml @@ -49,7 +49,10 @@ Item { Layout.topMargin: valueIndicatorVerticalPadding Layout.bottomMargin: valueIndicatorVerticalPadding MaterialSymbol { // Icon - anchors.centerIn: parent + anchors { + centerIn: parent + alignWhenCentered: !root.rotateIcon + } color: Appearance.colors.colOnLayer0 renderType: Text.QtRendering From 064d5174c2a8c6bfb8b7ed56b3093373bd4c8623 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 26 Jul 2025 14:20:55 +0700 Subject: [PATCH 06/27] ai: add command execution requests --- .../sidebarLeft/aiChat/MessageCodeBlock.qml | 185 +++++++++++------- .../sidebarLeft/aiChat/MessageThinkBlock.qml | 2 +- .config/quickshell/ii/services/Ai.qml | 89 ++++++++- .../ii/services/ai/AiMessageData.qml | 2 +- .../ii/services/ai/GeminiApiStrategy.qml | 2 +- .../ii/services/ai/OpenAiApiStrategy.qml | 2 +- 6 files changed, 197 insertions(+), 85 deletions(-) diff --git a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml index af2dc240a..f2b9a1bd6 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml @@ -12,12 +12,15 @@ import Quickshell import org.kde.syntaxhighlighting ColumnLayout { + id: root // These are needed on the parent loader property bool editing: parent?.editing ?? false property bool renderMarkdown: parent?.renderMarkdown ?? true property bool enableMouseSelection: parent?.enableMouseSelection ?? false property var segmentContent: parent?.segmentContent ?? ({}) property var segmentLang: parent?.segmentLang ?? "txt" + property bool isCommandRequest: segmentLang === "command" + property var displayLang: (isCommandRequest ? "bash" : segmentLang) property var messageData: parent?.messageData ?? {} property real codeBlockBackgroundRounding: Appearance.rounding.small @@ -56,7 +59,7 @@ ColumnLayout { font.pixelSize: Appearance.font.pixelSize.small font.weight: Font.DemiBold color: Appearance.colors.colOnLayer2 - text: segmentLang ? Repository.definitionForName(segmentLang).name : "plain" + text: root.displayLang ? Repository.definitionForName(root.displayLang).name : "plain" } Item { Layout.fillWidth: true } @@ -123,6 +126,7 @@ ColumnLayout { Rectangle { // Line numbers implicitWidth: 40 + implicitHeight: lineNumberColumnLayout.implicitHeight Layout.fillHeight: true Layout.fillWidth: false topLeftRadius: Appearance.rounding.unsharpen @@ -133,10 +137,13 @@ ColumnLayout { ColumnLayout { id: lineNumberColumnLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: 5 - anchors.verticalCenter: parent.verticalCenter + anchors { + left: parent.left + right: parent.right + rightMargin: 5 + top: parent.top + topMargin: 6 + } spacing: 0 Repeater { @@ -162,82 +169,116 @@ ColumnLayout { topRightRadius: Appearance.rounding.unsharpen bottomRightRadius: codeBlockBackgroundRounding color: Appearance.colors.colLayer2 - implicitHeight: codeTextArea.implicitHeight + implicitHeight: codeColumnLayout.implicitHeight - ScrollView { - id: codeScrollView - Layout.fillWidth: true - Layout.fillHeight: true - implicitWidth: parent.width - implicitHeight: codeTextArea.implicitHeight + 1 - contentWidth: codeTextArea.width - 1 - // contentHeight: codeTextArea.contentHeight - clip: true - ScrollBar.vertical.policy: ScrollBar.AlwaysOff - - ScrollBar.horizontal: ScrollBar { - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - padding: 5 - policy: ScrollBar.AsNeeded - opacity: visualSize == 1 ? 0 : 1 - visible: opacity > 0 + ColumnLayout { + id: codeColumnLayout + anchors.fill: parent + spacing: 0 + ScrollView { + id: codeScrollView + Layout.fillWidth: true + // Layout.fillHeight: true + implicitWidth: parent.width + implicitHeight: codeTextArea.implicitHeight + 1 + contentWidth: codeTextArea.width - 1 + // contentHeight: codeTextArea.contentHeight + clip: true + ScrollBar.vertical.policy: ScrollBar.AlwaysOff + + ScrollBar.horizontal: ScrollBar { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + padding: 5 + policy: ScrollBar.AsNeeded + opacity: visualSize == 1 ? 0 : 1 + visible: opacity > 0 - Behavior on opacity { - NumberAnimation { - duration: Appearance.animation.elementMoveFast.duration - easing.type: Appearance.animation.elementMoveFast.type - easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + Behavior on opacity { + NumberAnimation { + duration: Appearance.animation.elementMoveFast.duration + easing.type: Appearance.animation.elementMoveFast.type + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + } + } + + contentItem: Rectangle { + implicitHeight: 6 + radius: Appearance.rounding.small + color: Appearance.colors.colLayer2Active } } - - contentItem: Rectangle { - implicitHeight: 6 - radius: Appearance.rounding.small - color: Appearance.colors.colLayer2Active + + TextArea { // Code + id: codeTextArea + Layout.fillWidth: true + readOnly: !editing + selectByMouse: enableMouseSelection || editing + renderType: Text.NativeRendering + font.family: Appearance.font.family.monospace + font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text + font.pixelSize: Appearance.font.pixelSize.small + selectedTextColor: Appearance.m3colors.m3onSecondaryContainer + selectionColor: Appearance.colors.colSecondaryContainer + // wrapMode: TextEdit.Wrap + color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1 + + text: segmentContent + onTextChanged: { + segmentContent = text + } + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Tab) { + // Insert 4 spaces at cursor + const cursor = codeTextArea.cursorPosition; + codeTextArea.insert(cursor, " "); + codeTextArea.cursorPosition = cursor + 4; + event.accepted = true; + } else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) { + codeTextArea.copy(); + event.accepted = true; + } + } + + SyntaxHighlighter { + id: highlighter + textEdit: codeTextArea + repository: Repository + definition: Repository.definitionForName(root.displayLang || "plaintext") + theme: Appearance.syntaxHighlightingTheme + } } } - - TextArea { // Code - id: codeTextArea + Loader { + active: root.isCommandRequest && root.messageData.thinking + visible: active Layout.fillWidth: true - readOnly: !editing - selectByMouse: enableMouseSelection || editing - renderType: Text.NativeRendering - font.family: Appearance.font.family.monospace - font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text - font.pixelSize: Appearance.font.pixelSize.small - selectedTextColor: Appearance.m3colors.m3onSecondaryContainer - selectionColor: Appearance.colors.colSecondaryContainer - // wrapMode: TextEdit.Wrap - color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1 - - text: segmentContent - onTextChanged: { - segmentContent = text - } - - Keys.onPressed: (event) => { - if (event.key === Qt.Key_Tab) { - // Insert 4 spaces at cursor - const cursor = codeTextArea.cursorPosition; - codeTextArea.insert(cursor, " "); - codeTextArea.cursorPosition = cursor + 4; - event.accepted = true; - } else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) { - codeTextArea.copy(); - event.accepted = true; + Layout.margins: 6 + Layout.topMargin: 0 + sourceComponent: RowLayout { + Item { Layout.fillWidth: true } + ButtonGroup { + GroupButton { + contentItem: StyledText { + text: Translation.tr("Reject") + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnLayer2 + } + onClicked: Ai.rejectCommand(root.messageData) + } + GroupButton { + toggled: true + contentItem: StyledText { + text: Translation.tr("Approve") + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnPrimary + } + onClicked: Ai.approveCommand(root.messageData) + } } } - - SyntaxHighlighter { - id: highlighter - textEdit: codeTextArea - repository: Repository - definition: Repository.definitionForName(segmentLang || "plaintext") - theme: Appearance.syntaxHighlightingTheme - } } } diff --git a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageThinkBlock.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageThinkBlock.qml index 9d0555925..326c26dfc 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageThinkBlock.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageThinkBlock.qml @@ -92,7 +92,7 @@ Item { id: thinkBlockLanguage Layout.fillWidth: false Layout.alignment: Qt.AlignLeft - text: root.completed ? Translation.tr("Chain of Thought") : (Translation.tr("Thinking") + ".".repeat(Math.random() * 4)) + text: root.completed ? Translation.tr("Thought") : (Translation.tr("Thinking") + ".".repeat(Math.random() * 4)) } Item { Layout.fillWidth: true } RippleButton { // Expand button diff --git a/.config/quickshell/ii/services/Ai.qml b/.config/quickshell/ii/services/Ai.qml index f73652551..77c6d145f 100644 --- a/.config/quickshell/ii/services/Ai.qml +++ b/.config/quickshell/ii/services/Ai.qml @@ -11,6 +11,9 @@ import "./ai/" /** * Basic service to handle LLM chats. Supports Google's and OpenAI's API formats. + * Supports Gemini and OpenAI models. + * Limitations: + * - For now functions only work with Gemini API format */ Singleton { id: root @@ -87,6 +90,20 @@ Singleton { "required": ["key", "value"] } }, + { + "name": "run_shell_command", + "description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to run", + }, + }, + "required": ["command"] + } + }, ]}], "openai": [ { @@ -493,7 +510,7 @@ Singleton { Process { id: requester - property var baseCommand: ["bash", "-c"] + property list baseCommand: ["bash", "-c"] property AiMessageData message property ApiStrategy currentStrategy @@ -573,7 +590,8 @@ Singleton { // console.log("[Ai] Parsed response result: ", JSON.stringify(result, null, 2)); if (result.functionCall) { - root.handleFunctionCall(result.functionCall.name, result.functionCall.args); + requester.message.functionCall = result.functionCall; + root.handleFunctionCall(result.functionCall.name, result.functionCall.args, requester.message); } if (result.tokenUsage) { root.tokenCount.input = result.tokenUsage.input; @@ -614,24 +632,68 @@ Singleton { requester.makeRequest(); } - function addFunctionOutputMessage(name, output) { - const aiMessage = aiMessageComponent.createObject(root, { + function createFunctionOutputMessage(name, output, includeOutputInChat = true) { + return aiMessageComponent.createObject(root, { "role": "user", - "content": `[[ Output of ${name} ]]`, - "rawContent": `[[ Output of ${name} ]]`, + "content": `[[ Output of ${name} ]]${includeOutputInChat ? ("\n\n\n" + output + "\n") : ""}`, + "rawContent": `[[ Output of ${name} ]]${includeOutputInChat ? ("\n\n\n" + output + "\n") : ""}`, "functionName": name, "functionResponse": output, "thinking": false, "done": true, - "visibleToUser": false, + // "visibleToUser": false, }); - // console.log("Adding function output message: ", JSON.stringify(aiMessage)); + } + + function addFunctionOutputMessage(name, output) { + const aiMessage = createFunctionOutputMessage(name, output); const id = idForMessage(aiMessage); root.messageIDs = [...root.messageIDs, id]; root.messageByID[id] = aiMessage; } - function handleFunctionCall(name, args) { + function rejectCommand(message: AiMessageData) { + if (!message.thinking) return; + message.thinking = false; // User decided, no more "thinking" + addFunctionOutputMessage(message.functionName, Translation.tr("Command rejected by user")) + } + + function approveCommand(message: AiMessageData) { + if (!message.thinking) return; + message.thinking = false; // User decided, no more "thinking" + + const responseMessage = createFunctionOutputMessage(message.functionName, "", false); + const id = idForMessage(responseMessage); + root.messageIDs = [...root.messageIDs, id]; + root.messageByID[id] = responseMessage; + + commandExecutionProc.message = responseMessage; + commandExecutionProc.baseMessageContent = responseMessage.content; + commandExecutionProc.shellCommand = message.functionCall.args.command; + commandExecutionProc.running = true; // Start the command execution + } + + Process { + id: commandExecutionProc + property string shellCommand: "" + property AiMessageData message + property string baseMessageContent: "" + command: ["bash", "-c", shellCommand] + stdout: SplitParser { + onRead: (output) => { + commandExecutionProc.message.functionResponse += output + "\n\n"; + const updatedContent = commandExecutionProc.baseMessageContent + `\n\n\n${commandExecutionProc.message.functionResponse}\n`; + commandExecutionProc.message.rawContent = updatedContent; + commandExecutionProc.message.content = updatedContent; + } + } + onExited: (exitCode, exitStatus) => { + commandExecutionProc.message.functionResponse += `[[ Command exited with code ${exitCode} (${exitStatus}) ]]\n`; + requester.makeRequest(); // Continue + } + } + + function handleFunctionCall(name, args: var, message: AiMessageData) { if (name === "switch_to_search_mode") { const modelId = root.currentModelId; if (modelId.endsWith("-tools")) { @@ -660,6 +722,15 @@ Singleton { const key = args.key; const value = args.value; Config.setNestedValue(key, value); + } else if (name === "run_shell_command") { + if (!args.command || args.command.length === 0) { + addFunctionOutputMessage(name, Translation.tr("Invalid arguments. Must provide `command`.")); + return; + } + const contentToAppend = `\n\n**Command execution request**\n\n\`\`\`command\n${args.command}\n\`\`\``; + message.rawContent += contentToAppend; + message.content += contentToAppend; + message.thinking = true; // Use thinking to indicate the command is waiting for approval } else root.addMessage(Translation.tr("Unknown function call: %1").arg(name), "assistant"); } diff --git a/.config/quickshell/ii/services/ai/AiMessageData.qml b/.config/quickshell/ii/services/ai/AiMessageData.qml index 8813a70b0..5ec7fa336 100644 --- a/.config/quickshell/ii/services/ai/AiMessageData.qml +++ b/.config/quickshell/ii/services/ai/AiMessageData.qml @@ -14,7 +14,7 @@ QtObject { property var annotationSources: [] property list searchQueries: [] property string functionName - property string functionCall + property var functionCall property string functionResponse property bool visibleToUser: true } diff --git a/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml b/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml index 733dda51c..74e433c6e 100644 --- a/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml +++ b/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml @@ -5,7 +5,7 @@ ApiStrategy { function buildEndpoint(model: AiModel): string { const result = model.endpoint + `?key=\$\{${root.apiKeyEnvVarName}\}` - console.log("[AI] Endpoint: " + result); + // console.log("[AI] Endpoint: " + result); return result; } diff --git a/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml b/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml index ebc009706..8640293c5 100644 --- a/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml +++ b/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml @@ -4,7 +4,7 @@ ApiStrategy { property bool isReasoning: false function buildEndpoint(model: AiModel): string { - console.log("[AI] Endpoint: " + model.endpoint); + // console.log("[AI] Endpoint: " + model.endpoint); return model.endpoint; } From 8905bc1c27236ceb9a1f300e93d9dbbc67b4a82d Mon Sep 17 00:00:00 2001 From: Vercixx <112702555+Vercixx@users.noreply.github.com> Date: Sat, 26 Jul 2025 16:58:43 +0300 Subject: [PATCH 07/27] Make Performance toggle translatable --- .config/quickshell/ii/modules/settings/InterfaceConfig.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/settings/InterfaceConfig.qml b/.config/quickshell/ii/modules/settings/InterfaceConfig.qml index b6ed0811c..82bbf7010 100644 --- a/.config/quickshell/ii/modules/settings/InterfaceConfig.qml +++ b/.config/quickshell/ii/modules/settings/InterfaceConfig.qml @@ -164,7 +164,7 @@ ContentPage { } } ConfigSwitch { - text: "Performance Profile toggle" + text: Translation.tr("Performance Profile toggle") checked: Config.options.bar.utilButtons.showPerformanceProfileToggle onCheckedChanged: { Config.options.bar.utilButtons.showPerformanceProfileToggle = checked; From 47c5a41aa6d04b5417680163ca1129b8e978e9b5 Mon Sep 17 00:00:00 2001 From: Javier Rolando Date: Sat, 26 Jul 2025 20:49:04 -0300 Subject: [PATCH 08/27] fix empty notifications --- .config/quickshell/ii/services/Notifications.qml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.config/quickshell/ii/services/Notifications.qml b/.config/quickshell/ii/services/Notifications.qml index 446abeff7..bf5d4e784 100644 --- a/.config/quickshell/ii/services/Notifications.qml +++ b/.config/quickshell/ii/services/Notifications.qml @@ -34,11 +34,9 @@ Singleton { property string urgency: notification?.urgency.toString() ?? "normal" property Timer timer - readonly property Connections conn: Connections { - target: wrapper?.notification?.Component ?? root // stupid warning aaaaaaa - - function onDestruction(): void { - wrapper.destroy(); + onNotificationChanged: { + if (notification === null) { + root.discardNotification(notificationId); } } } From fe07298adbc33e5d2dd4e88592e7c01185c97ea4 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:51:43 +0700 Subject: [PATCH 09/27] hyprlanddata: use stdiocollector instead of jq hack with splitparser --- .../quickshell/ii/services/HyprlandData.qml | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/.config/quickshell/ii/services/HyprlandData.qml b/.config/quickshell/ii/services/HyprlandData.qml index 6488ded82..07c2d89a2 100644 --- a/.config/quickshell/ii/services/HyprlandData.qml +++ b/.config/quickshell/ii/services/HyprlandData.qml @@ -69,10 +69,11 @@ Singleton { Process { id: getClients - command: ["bash", "-c", "hyprctl clients -j | jq -c"] - stdout: SplitParser { - onRead: data => { - root.windowList = JSON.parse(data); + command: ["bash", "-c", "hyprctl clients -j"] + stdout: StdioCollector { + id: clientsCollector + onStreamFinished: { + root.windowList = JSON.parse(clientsCollector.text) let tempWinByAddress = {}; for (var i = 0; i < root.windowList.length; ++i) { var win = root.windowList[i]; @@ -86,30 +87,33 @@ Singleton { Process { id: getMonitors - command: ["bash", "-c", "hyprctl monitors -j | jq -c"] - stdout: SplitParser { - onRead: data => { - root.monitors = JSON.parse(data); + command: ["bash", "-c", "hyprctl monitors -j"] + stdout: StdioCollector { + id: monitorsCollector + onStreamFinished: { + root.monitors = JSON.parse(monitorsCollector.text); } } } Process { id: getLayers - command: ["bash", "-c", "hyprctl layers -j | jq -c"] - stdout: SplitParser { - onRead: data => { - root.layers = JSON.parse(data); + command: ["bash", "-c", "hyprctl layers -j"] + stdout: StdioCollector { + id: layersCollector + onStreamFinished: { + root.layers = JSON.parse(layersCollector.text); } } } Process { id: getWorkspaces - command: ["bash", "-c", "hyprctl workspaces -j | jq -c"] - stdout: SplitParser { - onRead: data => { - root.workspaces = JSON.parse(data); + command: ["bash", "-c", "hyprctl workspaces -j"] + stdout: StdioCollector { + id: workspacesCollector + onStreamFinished: { + root.workspaces = JSON.parse(workspacesCollector.text); let tempWorkspaceById = {}; for (var i = 0; i < root.workspaces.length; ++i) { var ws = root.workspaces[i]; @@ -123,10 +127,11 @@ Singleton { Process { id: getActiveWorkspace - command: ["bash", "-c", "hyprctl activeworkspace -j | jq -c"] - stdout: SplitParser { - onRead: data => { - root.activeWorkspace = JSON.parse(data); + command: ["bash", "-c", "hyprctl activeworkspace -j"] + stdout: StdioCollector { + id: activeWorkspaceCollector + onStreamFinished: { + root.activeWorkspace = JSON.parse(activeWorkspaceCollector.text); } } } From 564d2e109f5968b270465ef7dcc471672d4d041f Mon Sep 17 00:00:00 2001 From: Salvo Giangreco Date: Sat, 26 Jul 2025 09:19:23 +0200 Subject: [PATCH 10/27] translations: add Italian language file Signed-off-by: Salvo Giangreco --- .config/quickshell/translations/it_IT.json | 306 +++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 .config/quickshell/translations/it_IT.json diff --git a/.config/quickshell/translations/it_IT.json b/.config/quickshell/translations/it_IT.json new file mode 100644 index 000000000..bf875afba --- /dev/null +++ b/.config/quickshell/translations/it_IT.json @@ -0,0 +1,306 @@ +{ + "Launch": "Avvia", + "Columns": "Colonne", + "Save": "Salva", + "Temperature: %1": "Temperatura: %1", + "Night Light | Right-click to toggle Auto mode": "Modalità notte", + "Silent": "Silenzia", + "To Do": "Promemoria", + "Action": "Comandi", + "Search the web": "Cerca sul web", + "Workspace": "Spazio di lavoro", + "Desktop": "Scrivania", + "Settings": "Impostazioni", + "Math result": "Risultato", + "Calendar": "Calendario", + "Run": "Esegui", + "Cancel": "Cancella", + "Uptime: %1": "Tempo di attività: %1", + "Search": "Cerca", + "Battery": "Batteria", + "Weather": "Meteo", + "Brightness": "Luminosità", + "Clear": "Cancella", + "No notifications": "Nessuna notifica", + "No media": "Non in riproduzione", + "Add task": "Aggiungi promemoria", + "Run command": "Esegui comando", + "Game mode": "Modalità gioco", + "Reload Hyprland & Quickshell": "Riavvia Hyprland e Quickshell", + "Task description": "Titolo promemoria", + "%1 | Right-click to configure": "%1", + "Done": "Completati", + "Keep system awake": "Mantieni schermo attivo", + "Search, calculate or run": "Cerca, calcola o esegui", + "Copy": "Copia", + "Rows": "Righe", + "Session": "Sessione", + "Notifications": "Notifiche", + "Unfinished": "Da completare", + "Add": "Aggiungi", + "Nothing here!": "Nessun promemoria", + "Mo": "Lu", + "Tu": "Ma", + "We": "Me", + "Th": "Gi", + "Fr": "Ve", + "Sa": "Sa", + "Su": "Do", + "Edit config": "Apri config.", + "Center title": "Titolo centrato", + "Elements": "Elementi", + "Color picker": "Selettore colore", + "Title bar": "Barra del titolo", + "Sleep": "Sospendi", + "Transparency": "Trasparenza", + "Bluetooth": "Bluetooth", + "UV Index": "Indice UV", + "Bar": "Barra", + "Format": "Formato", + "Select output device": "Seleziona dispositivo di output", + "Pressure": "Pressione", + "Volume": "Volume", + "Volume mixer": "Mixer volume", + "Interface": "Interfaccia", + "Workspaces": "Spazi di lavoro", + "Dark": "Scuro", + "%1 notifications": "%1 notifiche", + "Reboot": "Riavvia", + "No": "No", + "Wind": "Vento", + "Humidity": "Umidità", + "Select Language": "Seleziona lingua", + "Wallpaper": "Sfondo", + "Copy code": "Copia codice", + "Allow NSFW": "Mostra NSFW", + "Colors & Wallpaper": "Sfondo e stile", + "Shutdown": "Spegni", + "Decorations & Effects": "Decorazioni e effetti", + "Translation goes here...": "Traduzione", + "Polling interval (ms)": "Intervallo di polling (ms)", + "System prompt": "Prompt di sistema", + "Base URL": "URL base", + "Always show numbers": "Mostra numeri", + "Wallpaper parallax": "Effetto parallasse sfondo", + "Plain rectangle": "Semplice", + "illogical-impulse Welcome": "Benvenuto su illogical-impulse", + "Local only": "Solo locale", + "Chain of Thought": "Catena di pensiero", + "Dark/Light toggle": "Modalità chiaro/scuro", + "Screen snip": "Cattura schermo", + "Style & wallpaper": "Sfondo e stile", + "Show app icons": "Mostra icone app", + "Useless buttons": "Tasti inutili", + "Scale (%)": "Dimensione (%)", + "Show background": "Mostra sfondo", + "Intelligence": "AI", + "Appearance": "Aspetto", + "Enable": "Abilita", + "Borderless": "Senza bordi", + "Random: Konachan": "Casuale: Konachan", + "Yes": "Sì", + "Documentation": "Documentazione", + "Unknown Artist": "Artista sconosciuto", + "Help & Support": "Aiuto e supporto", + "Report a Bug": "Segnala un bug", + "Sunset": "Tramonto", + "Weeb": "Anime", + "Shell windows": "Finestre", + "Workspaces shown": "Numero spazi di lavoro", + "Screenshot tool": "Cattura schermo", + "Enter text to translate...": "Inserisci il testo qui", + "Unknown Album": "Album sconosciuto", + "Hug": "Stondato", + "Scroll to change brightness": "Scorri per cambiare luminosità", + "Privacy Policy": "Privacy policy", + "12h AM/PM": "12 ore (AM/PM)", + "Material palette": "Tavolozza dei colori", + "No audio source": "Nessuna sorgente audio", + "Download": "Scarica", + "Prefixes": "Prefissi", + "Show next time": "Mostra al prossimo avvio", + "Lock": "Blocca", + "Scroll to change volume": "Scorri per cambiare volume", + "Unknown": "Sconosciuto", + "Jump to current month": "Torna al mese corrente", + "Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)", + "Terminal": "Terminale", + "About": "Informazioni", + "Precipitation": "Precipitazioni", + "Keybinds": "Comandi", + "Select input device": "Seleziona dispositivo di input", + "When not fullscreen": "Tranne a schermo intero", + "Unknown Title": "Titolo sconosciuto", + "Task Manager": "Gestione attività", + "System": "Sistema", + "Choose file": "Scegli file", + "Pinned on startup": "Fissa sullo schermo", + "Policies": "Servizi", + "View Markdown source": "Mostra Markdown", + "Sunrise": "Alba", + "Configuration": "Configurazione", + "Overview": "Panoramica", + "Translator": "Traduttore", + "Finished tasks will go here": "Nessun promemoria", + "Mic toggle": "Microfono", + "Light": "Chiaro", + "Bar style": "Stile barra", + "Advanced": "Avanzate", + "Web search": "Cerca sul web", + "12h am/pm": "12 ore (am/pm)", + "Services": "Servizi", + "Donate": "Supporta", + "Resources": "Risorse", + "Float": "Fluttuante", + "Fake screen rounding": "Bordi curvi schermo", + "Hibernate": "Iberna", + "Visibility": "Visibilità", + "Delete": "Elimina", + "Style": "Stile", + "Page %1": "Pagina %1", + "Keyboard toggle": "Tastiera virtuale", + "Buttons": "Pulsanti", + "24h": "24 ore", + "Time": "Ora", + "Clipboard": "Appunti", + "or": "o", + "Edit": "Modifica", + "Yooooo hi there": "Ciao bellissimə", + "Emojis": "Emoji", + "Shell & utilities": "App e shell", + "Reboot to firmware settings": "Riavvia alle impostazioni firmware", + "No API key set for %1": "Chiave API non impostata per %1", + "Disable NSFW content": "Nascondi contenuti NSFW", + "Closet": "Nascosto", + "Depends on sidebars": "Abilita per barre laterali", + "Invalid model. Supported: \n```": "Modello non valido. Supportati: \n```", + "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "Usa /key per utilizzare i modelli online\nCtrl+O per espandere\nCtrl+P per separare in finestra", + "Not visible to model": "Non visible al modello", + "Local Ollama model | %1": "Modello Ollama locale | %1", + "Open file link": "Apri link al file", + "Waiting for response...": "In attesa di risposta...", + "Cheat sheet": "Prontuario", + "Allow NSFW content": "Mostra contenuti NSFW", + "%1 characters": "%1 caratteri", + "Model set to %1": "Modello impostato su %1", + "Be patient...": "Attendi...", + "User agent (for services that require it)": "User agent (usato dai servizi)", + "Current API endpoint: %1\nSet it with %2mode PROVIDER": "Endpoint API: %1\nUsa /mode per cambiarlo", + "For desktop wallpapers | Good quality": "Per sfondi del desktop | Buona qualità", + "For storing API keys and other sensitive information": "Gestione credenziali e informazioni sensibili", + "Your package manager is running": "Il package manager è in esecuzione", + "Experimental | Online | Google's model\nCan do a little more but takes an extra turn to perform search": "Sperimentale | Online | Modello di Google\nPiù potente ma richiede più risorse", + "Provider set to": "Provider impostato su", + "Color generation": "Tavolozza dei colori", + "Temperature must be between 0 and 2": "Il valore della temperatura deve essere fra 0 e 2", + "Earbang protection": "Protezione udito", + "Online | Google's model\nGives up-to-date information with search.": "Online | Modello di Google\nRestituisce informazioni attuali tramite ricerca.", + "Alternatively use /dark, /light, /img in the launcher": "Oppure usa /dark, /light, /img nel launcher", + "Change any time later with /dark, /light, /img in the launcher": "Oppure usa /dark, /light, /img nel launcher", + "Current model: %1\nSet it with %2model MODEL": "Modello attuale: %1\nUsa /model per cambiarlo", + "Waifus only | Excellent quality, limited quantity": "Solo waifu | Qualità eccellente, quantità limitata", + "Save to Downloads": "Salva in Scaricati", + "Thinking": "Ragionando", + "On-screen display": "Popup di sistema", + "%1 • %2 tasks": "%1 • %2 promemoria", + "Qt apps": "Applicazioni Qt", + "The popular one | Best quantity, but quality can vary wildly": "Il più usato | Quantità elevata, ma la qualità potrebbe variare totalmente", + "Set the system prompt for the model.": "Imposta il prompt di sistema per il modello.", + "To set an API key, pass it with the command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "Per impostare una chiave API, utilizzala come argomento di questo comando\n\nPer vedere la chiave corrente, utilizza come argomento \"get\"
\n\n### For %1:\n\n**Link**: %2\n\n%3", + "Markdown test": "Test Markdown", + "Prevents abrupt increments and restricts volume limit": "Impedice aumenti improvvisi del volume e ne imposta un limite.", + "Preferred wallpaper zoom (%)": "Zoom sfondo (%)", + "Depends on workspace": "Abilita per spazi di lavoro", + "Note: turning off can hurt readability": "Nota: disattivarlo potrebbe ridurre la leggibilità", + "Pick wallpaper image on your system": "Seleziona un'immagine nel tuo sistema", + "Invalid API provider. Supported: \n-": "Provider API non valido. Supportati: \n-", + "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "Hentai | Quantità elevata, NSFW, la qualità potrebbe variare totalmente", + "Saved to %1": "Salvato in %1", + "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "Imposta la temperatura (casualità) del modello. Il valore va da 0 a 2 per Gemini, e da 0 a 1 per gli altri modelli. Il valore predefinito è 0.5.", + "Close": "Chiudi", + "Might look ass. Unsupported.": "Potrebbe fare schifo. Non supportato.", + "API key set for %1": "Chiave API impostata per %1", + "Temperature\nChange with /temp VALUE": "Temperatura\nUsa /temp per cambiarla", + "%1 does not require an API key": "%1 non richiede una chiave API", + "Online models disallowed\n\nControlled by `policies.ai` config option": "Modelli online disattivati\n\nDisattiva l'opzione \"Solo locale\"", + "Set the current API provider": "Imposta il provider API", + "Total token count\nInput: %1\nOutput: %2": "Numero token totali\nInput: %1\nOutput: %2", + "EasyEffects | Right-click to configure": "EasyEffects", + "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Sfondo anime SFW casuale da Konachan\nL'immagine verrà salvata in ~/Immagini/Wallpapers", + "The current API used. Endpoint:": "Provider API attuale. Endpoint:", + "Set with /mode PROVIDER": "Usa /mode per cambiarlo", + "Hover to reveal": "Mostra col passaggio del mouse", + "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "Usa i tasti freccia per navigare, Invio per selezionare\nEsc o clicca qualsiasi punto per uscire", + "Save chat to %1": "Salva chat in %1", + "Choose model": "Seleziona modello", + "No API key\nSet it with /key YOUR_API_KEY": "Nessuna chiave API\nUsa /key per impostarla", + "API key:\n\n```txt\n%1\n```": "Chiave API:\n\n```txt\n%1\n```", + "Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "Sperimentale | Online | Modello di Google\nModello Gemini 2.5 Flash ottimizzato per efficenza e throughput elevato.", + "Use Levenshtein distance-based algorithm instead of fuzzy": "Usa algoritmo di Levenshtein al posto di fuzzy", + "Download complete": "Download completato", + "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "Suggerimento: Nascondi le icone e mostra i numeri se vuoi\nun'esperienza fedele all'originale", + "Usage": "Istruzioni", + "Set API key": "Imposta chiave API", + "Networking": "Rete", + "Volume limit": "Limite volume", + "Temperature set to %1": "Temperatura impostata a %1", + "Large images | God tier quality, no NSFW.": "Immagini grandi | Qualità perfetta, no NSFW.", + "Shell & utilities theming must also be enabled": "\"App e shell\" deve essere abilitato", + "API key is set\nChange with /key YOUR_API_KEY": "Chiave API impostata\nUsa /key per cambiarla", + "All-rounder | Good quality, decent quantity": "Versatile | Qualità buona, quantità discreta", + "Large language models": "Intelligenza artificiale", + "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "Può aiutare in caso di refusi,\nma i risultati potrebbero non essere accurati\n(es. \"GIMP\" potrebbe non restituire l'omonimo programma)", + "Automatic suspend": "Sospensione automatica", + "Max allowed increase": "Aumento massimo consentito", + "No corresponding search model found for %1": "Nessun modello di ricerca trovato con %1", + "Please charge!\nAutomatic suspend triggers at %1": "Ricarica la batteria!\nSospensione automatica in %1", + "Weather Service": "Servizio meteo", + "The current system prompt is\n\n---\n\n%1": "Il prompt di sistema attuale è\n\n---\n\n%1", + "%1 Safe Storage": "Portachiavi di %1", + "Load prompt from %1": "Carica prompt da %1", + "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "Nessun risultato. Suggerimenti:\n- Controlla i tag e le impostazioni NSFW\n- Se non hai un tag in mente, inserici il numero di pagina", + "Load chat": "Carica chat", + "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**Prezzo**: gratuito. I tuoi dati vengono usati per training.\n\n**Istruzioni**: Accedi al tuo account Google, abilita i permessi richiesti da AI Studio, torna indietro e seleziona \"Get API key\"", + "Online via %1 | %2's model": "Online tramite %1 | Modello di %2", + "Get the next page of results": "Ottieni la pagina successiva dei risultati", + "Unknown function call: %1": "Funzione sconosciuta: %1", + "Cannot find a GPS service. Using the fallback method instead.": "Servizio GPS non disponibile. Verrà utilizzata la città preimpostata.", + "Registration failed. Please inspect manually with the warp-cli command": "Registrazione fallita. Verifica l'errore manualmente col comando warp-cli", + "Code saved to file": "Codice salvato", + "Low warning": "Livello basso", + "Clear the current list of images": "Elimina la lista corrente di immagini", + "Invalid arguments. Must provide `key` and `value`.": "Argomenti non validi. Il comando richiede `key` e `value`.", + "Connection failed. Please inspect manually with the warp-cli command": "Connessione fallita. Verifica l'errore manualmente col comando warp-cli", + "Unknown command:": "Comando sconosciuto:", + "Message the model... \"%1\" for commands": "\"%1\" per mostrare i comandi", + "Load chat from %1": "Carica chat da %1", + "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**Prezzo**: gratuito. Le policy di trattamento dei dati potrebbero variare in base alle impostazioni del tuo account OpenRouter.\n\n**Istruzioni**: Accedi al tuo account OpenRouter, vai nella sezione \"Keys\" dal menu in alto, clicca \"Create API Key\"", + "Enter tags, or \"%1\" for commands": "\"%1\" per mostrare i comandi", + "Show regions of potential interest": "Mostra regioni d'interesse", + "Critical warning": "Livello critico", + "Go to source (%1)": "Vai alla fonte (%1)", + "Automatically suspends the system when battery is low": "Sospende automaticamente il sistema quando il livello della batteria è basso", + "Cannot switch to search mode from %1": "Impossibile attivare modalità ricerca da %1", + "Clean stuff | Excellent quality, no NSFW": "Roba pulita | Qualità eccellente, no NSFW", + ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": ". Note per Zerochan:\n- Devi inserire un colore\n- Imposta il tuo nome utente di zerochan nell'opzione `sidebar.booru.zerochan.username`. Potresti [venire bannato se non lo fai](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", + "Critically low battery": "Batteria scarica", + "Loaded the following system prompt\n\n---\n\n%1": "Il seguente prompt di sistema è stato caricato\n\n---\n\n%1", + "Suspend at": "Sospendi al", + "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "Queste regioni potrebbero essere immagini o parti di schermo contenute.\nPotrebbe non essere preciso.\nViene utilizzato un algoritmo di image processing in locale, non viene usata AI.", + "Clear chat history": "Elimina cronologia chat", + "Low battery": "Batteria quasi scarica", + "Save chat": "Salva chat", + "Switched to search mode. Continue with the user's request.": "Modalità ricerca attiva. Continua con la richiesta dell'utente.", + "Number show delay when pressing Super (ms)": "Mostra numeri premendo Super dopo (ms)", + "Drag or click a region • LMB: Copy • RMB: Edit": "Trascina o clicca una regione • LMB: Copia • RMB: Modifica", + "Consider plugging in your device": "Connetti il tuo dispositivo alla rete elettrica", + "%1 queries pending": "%1 ricerche in sospeso", + "No further instruction provided": "Nessun'altra istruzione fornita", + "There might be a download in progress": "Potrebbe esserci un download in corso", + "Approve": "Approva", + "Invalid arguments. Must provide `command`.": "Argomenti non validi. Il comando richiede `command`.", + "Reject": "Rifiuta", + "Thought": "Pensiero", + "Performance Profile toggle": "Profilo prestazioni", + "Command rejected by user": "Comando rifiutato dall'utente" +} \ No newline at end of file From 3ac44d211fdc415599ad4e751c5ff0cb2e072d80 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 27 Jul 2025 22:33:25 +0700 Subject: [PATCH 11/27] ai: separate model and tool selection --- .../quickshell/ii/modules/common/Config.qml | 1 + .../ii/modules/sidebarLeft/AiChat.qml | 75 +++--- .../ii/modules/sidebarLeft/Anime.qml | 40 +-- .../modules/sidebarLeft/ApiCommandButton.qml | 1 - .../sidebarLeft/ApiInputBoxIndicator.qml | 47 ++++ .config/quickshell/ii/services/Ai.qml | 246 +++++++----------- .config/quickshell/ii/services/ai/AiModel.qml | 1 - .../quickshell/ii/services/ai/ApiStrategy.qml | 2 +- .../ii/services/ai/GeminiApiStrategy.qml | 9 +- .../ii/services/ai/OpenAiApiStrategy.qml | 2 +- 10 files changed, 192 insertions(+), 232 deletions(-) create mode 100644 .config/quickshell/ii/modules/sidebarLeft/ApiInputBoxIndicator.qml diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index cd6231cc9..1a947f1d5 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -61,6 +61,7 @@ Singleton { property JsonObject ai: JsonObject { property string systemPrompt: "## Style\n- Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question\n\n## Presentation\n- Use Markdown features in your response: \n - **Bold** text to **highlight keywords** in your response\n - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user.\n- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case!\n- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).\n\nThanks!\n\n## Tools\nMay or may not be available depending on the user's settings. If they're available, follow these guidelines:\n\n### Search\n- When user asks for information that might benefit from up-to-date information, use this to get search access\n\n### Shell configuration\n- Always fetch the config options to see the available keys before setting\n- Avoid unnecessarily asking the user to confirm the changes they explicitly asked for, just do it\n" + property string tool: "functions" // search, functions, or none } property JsonObject appearance: JsonObject { diff --git a/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml b/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml index ddee1367d..d42e6b608 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml @@ -45,6 +45,22 @@ Item { Ai.setModel(args[0]); } }, + { + name: "tool", + description: Translation.tr("Set the tool to use for the model."), + execute: (args) => { + // console.log(args) + if (args.length == 0 || args[0] == "get") { + Ai.addMessage(Translation.tr("Usage: %1tool TOOL_NAME").arg(root.commandPrefix), Ai.interfaceRole); + } else { + const tool = args[0]; + const switched = Ai.setTool(tool); + if (switched) { + Ai.addMessage(Translation.tr("Tool set to %1").arg(tool), Ai.interfaceRole); + } + } + } + }, { name: "prompt", description: Translation.tr("Set the system prompt for the model."), @@ -73,7 +89,7 @@ Item { execute: (args) => { const joinedArgs = args.join(" ") if (joinedArgs.trim().length == 0) { - Ai.addMessage(`Usage: ${root.commandPrefix}save CHAT_NAME`, Ai.interfaceRole); + Ai.addMessage(Translation.tr("Usage: %1save CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole); return; } Ai.saveChat(joinedArgs) @@ -85,7 +101,7 @@ Item { execute: (args) => { const joinedArgs = args.join(" ") if (joinedArgs.trim().length == 0) { - Ai.addMessage(`Usage: ${root.commandPrefix}load CHAT_NAME`, Ai.interfaceRole); + Ai.addMessage(Translation.tr("Usage: %1load CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole); return; } Ai.loadChat(joinedArgs) @@ -606,54 +622,35 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) property var commandsShown: [ { - name: "model", + name: "", sendDirectly: false, - }, + dontAddSpace: true, + }, { name: "clear", sendDirectly: true, }, ] - Item { - implicitHeight: providerRowLayout.implicitHeight + 5 * 2 - implicitWidth: providerRowLayout.implicitWidth + 10 * 2 - - RowLayout { - id: providerRowLayout - anchors.centerIn: parent + ApiInputBoxIndicator { // Model indicator + icon: "api" + text: Ai.getModel().name + tooltipText: Translation.tr("Current model: %1\nSet it with %2model MODEL") + .arg(Ai.getModel().name) + .arg(root.commandPrefix) + } - MaterialSymbol { - text: "api" - iconSize: Appearance.font.pixelSize.large - } - StyledText { - id: providerName - font.pixelSize: Appearance.font.pixelSize.small - color: Appearance.m3colors.m3onSurface - elide: Text.ElideRight - text: Ai.getModel().name - } - } - StyledToolTip { - id: toolTip - extraVisibleCondition: false - alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered - content: Translation.tr("Current model: %1\nSet it with %2model MODEL") - .arg(Ai.getModel().name) - .arg(root.commandPrefix) - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - } + ApiInputBoxIndicator { // Tool indicator + icon: "service_toolbox" + text: Ai.currentTool.charAt(0).toUpperCase() + Ai.currentTool.slice(1) + tooltipText: Translation.tr("Current tool: %1\nSet it with %2tool TOOL") + .arg(Ai.currentTool) + .arg(root.commandPrefix) } Item { Layout.fillWidth: true } - ButtonGroup { + ButtonGroup { // Command buttons padding: 0 Repeater { // Command buttons @@ -665,7 +662,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) if(modelData.sendDirectly) { root.handleInput(commandRepresentation) } else { - messageInputField.text = commandRepresentation + " " + messageInputField.text = commandRepresentation + (modelData.dontAddSpace ? "" : " ") messageInputField.cursorPosition = messageInputField.text.length messageInputField.forceActiveFocus() } diff --git a/.config/quickshell/ii/modules/sidebarLeft/Anime.qml b/.config/quickshell/ii/modules/sidebarLeft/Anime.qml index 572e775be..a72837725 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/Anime.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/Anime.qml @@ -492,40 +492,12 @@ Item { }, ] - Item { - implicitHeight: providerRowLayout.implicitHeight + 5 * 2 - implicitWidth: providerRowLayout.implicitWidth + 10 * 2 - - RowLayout { - id: providerRowLayout - anchors.centerIn: parent - - MaterialSymbol { - text: "api" - iconSize: Appearance.font.pixelSize.large - } - StyledText { - id: providerName - font.pixelSize: Appearance.font.pixelSize.small - color: Appearance.m3colors.m3onSurface - text: Booru.providers[Booru.currentProvider].name - } - } - StyledToolTip { - id: toolTip - extraVisibleCondition: false - alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered - // content: Translation.tr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + Translation.tr("\nSet with /mode PROVIDER") - content: Translation.tr("Current API endpoint: %1\nSet it with %2mode PROVIDER") - .arg(Booru.providers[Booru.currentProvider].url) - .arg(root.commandPrefix) - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - } + ApiInputBoxIndicator { // Tool indicator + icon: "api" + text: Booru.providers[Booru.currentProvider].name + tooltipText: Translation.tr("Current API endpoint: %1\nSet it with %2mode PROVIDER") + .arg(Booru.providers[Booru.currentProvider].url) + .arg(root.commandPrefix) } StyledText { diff --git a/.config/quickshell/ii/modules/sidebarLeft/ApiCommandButton.qml b/.config/quickshell/ii/modules/sidebarLeft/ApiCommandButton.qml index 7ee398469..efbde1506 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/ApiCommandButton.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/ApiCommandButton.qml @@ -1,6 +1,5 @@ import qs.modules.common import qs.modules.common.widgets -import qs.services import QtQuick GroupButton { diff --git a/.config/quickshell/ii/modules/sidebarLeft/ApiInputBoxIndicator.qml b/.config/quickshell/ii/modules/sidebarLeft/ApiInputBoxIndicator.qml new file mode 100644 index 000000000..46bd4c783 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarLeft/ApiInputBoxIndicator.qml @@ -0,0 +1,47 @@ +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import QtQuick +import QtQuick.Layouts + +Item { // Model indicator + id: root + property string icon: "api" + property string text: "" + property string tooltipText: "" + implicitHeight: rowLayout.implicitHeight + 5 * 2 + implicitWidth: rowLayout.implicitWidth + 10 * 2 + + RowLayout { + id: rowLayout + anchors.centerIn: parent + + MaterialSymbol { + text: root.icon + iconSize: Appearance.font.pixelSize.normal + } + StyledText { + id: providerName + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.m3colors.m3onSurface + elide: Text.ElideRight + text: root.text + } + } + + Loader { + active: root.tooltipText?.length > 0 + anchors.fill: parent + sourceComponent: MouseArea { + id: mouseArea + hoverEnabled: true + + StyledToolTip { + id: toolTip + extraVisibleCondition: false + alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered + content: root.tooltipText + } + } + } +} diff --git a/.config/quickshell/ii/services/Ai.qml b/.config/quickshell/ii/services/Ai.qml index 77c6d145f..b23698d78 100644 --- a/.config/quickshell/ii/services/Ai.qml +++ b/.config/quickshell/ii/services/Ai.qml @@ -62,76 +62,87 @@ Singleton { // Gemini: https://ai.google.dev/gemini-api/docs/function-calling // OpenAI: https://platform.openai.com/docs/guides/function-calling + property string currentTool: Config?.options.ai.tool ?? "search" property var tools: { - "gemini": [{"functionDeclarations": [ - { - "name": "switch_to_search_mode", - "description": "Search the web", - }, - { - "name": "get_shell_config", - "description": "Get the desktop shell config file contents", - }, - { - "name": "set_shell_config", - "description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.", - "parameters": { - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.", + "gemini": { + "functions": [{"functionDeclarations": [ + { + "name": "switch_to_search_mode", + "description": "Search the web", + }, + { + "name": "get_shell_config", + "description": "Get the desktop shell config file contents", + }, + { + "name": "set_shell_config", + "description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.", + "parameters": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.", + }, + "value": { + "type": "string", + "description": "The value to set, e.g. `true`" + } }, - "value": { - "type": "string", - "description": "The value to set, e.g. `true`" - } - }, - "required": ["key", "value"] - } - }, - { - "name": "run_shell_command", - "description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.", - "parameters": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The bash command to run", + "required": ["key", "value"] + } + }, + { + "name": "run_shell_command", + "description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to run", + }, }, - }, - "required": ["command"] - } - }, - ]}], - "openai": [ - { - "type": "function", - "name": "get_shell_config", - "description": "Get the current shell configuration.", - }, - { - "type": "function", - "name": "set_shell_config", - "description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.", - "parameters": { - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.", + "required": ["command"] + } + }, + ]}], + "search": [{ + "google_search": {} + }], + "none": [] + }, + "openai": { + "functions": [ + { + "type": "function", + "name": "get_shell_config", + "description": "Get the current shell configuration.", + }, + { + "type": "function", + "name": "set_shell_config", + "description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.", + "parameters": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.", + }, + "value": { + "type": "string", + "description": "The value to set, e.g. `true`" + } }, - "value": { - "type": "string", - "description": "The value to set, e.g. `true`" - } - }, - "required": ["key", "value"], - "additionalProperties": false + "required": ["key", "value"], + "additionalProperties": false + } } - } - ] + ], + "search": [], + "none": [], + } } // Model properties: @@ -145,13 +156,12 @@ Singleton { // - key_get_link: Link to get an API key // - key_get_description: Description of pricing and how to get an API key // - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai". - // - tools: List of tools that the model can use. Each tool is an object with the tool name as the key and an empty object as the value. // - extraParams: Extra parameters to be passed to the model. This is a JSON object. property var models: { - "gemini-2.0-flash-search": aiModelComponent.createObject(this, { - "name": "Gemini 2.0 Flash (Search)", + "gemini-2.0-flash": aiModelComponent.createObject(this, { + "name": "Gemini 2.0 Flash", "icon": "google-gemini-symbolic", - "description": Translation.tr("Online | Google's model\nGives up-to-date information with search."), + "description": Translation.tr("Online | Google's model\nFast, can perform searches for up-to-date information"), "homepage": "https://aistudio.google.com", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", "model": "gemini-2.0-flash", @@ -160,28 +170,11 @@ Singleton { "key_get_link": "https://aistudio.google.com/app/apikey", "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", - "tools": [{ - "google_search": {} - }] }), - "gemini-2.0-flash-tools": aiModelComponent.createObject(this, { - "name": "Gemini 2.0 Flash (Tools)", + "gemini-2.5-flash": aiModelComponent.createObject(this, { + "name": "Gemini 2.5 Flash", "icon": "google-gemini-symbolic", - "description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but takes an extra turn to perform search"), - "homepage": "https://aistudio.google.com", - "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", - "model": "gemini-2.0-flash", - "requires_key": true, - "key_id": "gemini", - "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), - "api_format": "gemini", - "tools": root.tools["gemini"], - }), - "gemini-2.5-flash-search": aiModelComponent.createObject(this, { - "name": "Gemini 2.5 Flash (Search)", - "icon": "google-gemini-symbolic", - "description": Translation.tr("Online | Google's model\nGives up-to-date information with search."), + "description": Translation.tr("Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers"), "homepage": "https://aistudio.google.com", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent", "model": "gemini-2.5-flash", @@ -190,44 +183,11 @@ Singleton { "key_get_link": "https://aistudio.google.com/app/apikey", "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", - "tools": [{ - "google_search": {} - }] - }), - "gemini-2.5-flash-tools": aiModelComponent.createObject(this, { - "name": "Gemini 2.5 Flash (Tools)", - "icon": "google-gemini-symbolic", - "description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but takes an extra turn to perform search"), - "homepage": "https://aistudio.google.com", - "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent", - "model": "gemini-2.5-flash", - "requires_key": true, - "key_id": "gemini", - "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), - "api_format": "gemini", - "tools": root.tools["gemini"], - }), - "gemini-2.5-flash-lite-search": aiModelComponent.createObject(this, { - "name": "Gemini 2.5 Flash-Lite (Search)", - "icon": "google-gemini-symbolic", - "description": Translation.tr("Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."), - "homepage": "https://aistudio.google.com", - "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent", - "model": "gemini-2.5-flash-lite", - "requires_key": true, - "key_id": "gemini", - "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), - "api_format": "gemini", - "tools": [{ - "google_search": {} - }] }), "gemini-2.5-flash-lite": aiModelComponent.createObject(this, { "name": "Gemini 2.5 Flash-Lite", "icon": "google-gemini-symbolic", - "description": Translation.tr("Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."), + "description": Translation.tr("Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."), "homepage": "https://aistudio.google.com", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent", "model": "gemini-2.5-flash-lite", @@ -236,19 +196,6 @@ Singleton { "key_get_link": "https://aistudio.google.com/app/apikey", "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", - "tools": root.tools["gemini"], - }), - "openrouter-llama4-maverick": aiModelComponent.createObject(this, { - "name": "Llama 4 Maverick", - "icon": "ollama-symbolic", - "description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("Meta"), - "homepage": "https://openrouter.ai/meta-llama/llama-4-maverick:free", - "endpoint": "https://openrouter.ai/api/v1/chat/completions", - "model": "meta-llama/llama-4-maverick:free", - "requires_key": true, - "key_id": "openrouter", - "key_get_link": "https://openrouter.ai/settings/keys", - "key_get_description": Translation.tr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"), }), "openrouter-deepseek-r1": aiModelComponent.createObject(this, { "name": "DeepSeek R1", @@ -452,6 +399,15 @@ Singleton { if (feedback) root.addMessage(Translation.tr("Invalid model. Supported: \n```\n") + modelList.join("\n```\n```\n"), Ai.interfaceRole) + "\n```" } } + + function setTool(tool) { + if (!root.tools[models[currentModelId]?.api_format] || !(tool in root.tools[models[currentModelId]?.api_format])) { + root.addMessage(Translation.tr("Invalid tool. Supported tools:\n- %1").arg(Object.keys(root.tools[models[currentModelId]?.api_format]).join("\n- ")), root.interfaceRole); + return false; + } + Config.options.ai.tool = tool; + return true; + } function getTemperature() { return root.temperature; @@ -535,7 +491,7 @@ Singleton { const endpoint = root.currentApiStrategy.buildEndpoint(model); const messageArray = root.messageIDs.map(id => root.messageByID[id]); const filteredMessageArray = messageArray.filter(message => message.role !== Ai.interfaceRole); - const data = root.currentApiStrategy.buildRequestData(model, filteredMessageArray, root.systemPrompt, root.temperature); + const data = root.currentApiStrategy.buildRequestData(model, filteredMessageArray, root.systemPrompt, root.temperature, root.tools[model.api_format][root.currentTool]); // console.log("[Ai] Request data: ", JSON.stringify(data, null, 2)); let requestHeaders = { @@ -580,9 +536,9 @@ Singleton { stdout: SplitParser { onRead: data => { - // console.log("[Ai] Raw response line: ", data); if (data.length === 0) return; if (requester.message.thinking) requester.message.thinking = false; + // console.log("[Ai] Raw response line: ", data); // Handle response line try { @@ -696,18 +652,8 @@ Singleton { function handleFunctionCall(name, args: var, message: AiMessageData) { if (name === "switch_to_search_mode") { const modelId = root.currentModelId; - if (modelId.endsWith("-tools")) { - const searchModelId = modelId.replace(/-tools$/, "-search"); - if (root.modelList.indexOf(searchModelId) !== -1) { - root.setModel(searchModelId, false); - root.postResponseHook = () => root.setModel(modelId, false); - } else { - root.addMessage(Translation.tr("No corresponding search model found for %1").arg(modelId), Ai.interfaceRole); - } - } else { - root.addMessage(Translation.tr("Cannot switch to search mode from %1").arg(root.currentModelId), Ai.interfaceRole); - return; - } + root.currentTool = "search" + root.postResponseHook = () => { root.currentTool = "functions" } addFunctionOutputMessage(name, Translation.tr("Switched to search mode. Continue with the user's request.")) requester.makeRequest(); } else if (name === "get_shell_config") { diff --git a/.config/quickshell/ii/services/ai/AiModel.qml b/.config/quickshell/ii/services/ai/AiModel.qml index 5544cf169..7cf985208 100644 --- a/.config/quickshell/ii/services/ai/AiModel.qml +++ b/.config/quickshell/ii/services/ai/AiModel.qml @@ -12,7 +12,6 @@ import QtQuick; * - key_get_link: Link to get an API key * - key_get_description: Description of pricing and how to get an API key * - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai". - * - tools: List of tools that the model can use. * - extraParams: Extra parameters to be passed to the model. This is a JSON object. */ diff --git a/.config/quickshell/ii/services/ai/ApiStrategy.qml b/.config/quickshell/ii/services/ai/ApiStrategy.qml index d63f932df..75736d607 100644 --- a/.config/quickshell/ii/services/ai/ApiStrategy.qml +++ b/.config/quickshell/ii/services/ai/ApiStrategy.qml @@ -2,7 +2,7 @@ import QtQuick QtObject { function buildEndpoint(model: AiModel): string { throw new Error("Not implemented") } - function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real) { throw new Error("Not implemented") } + function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list) { throw new Error("Not implemented") } function buildAuthorizationHeader(apiKeyEnvVarName: string): string { throw new Error("Not implemented") } function parseResponseLine(line: string, message: AiMessageData) { throw new Error("Not implemented") } function onRequestFinished(message: AiMessageData): var { return {} } // Default: no special handling diff --git a/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml b/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml index 74e433c6e..12c775c8f 100644 --- a/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml +++ b/.config/quickshell/ii/services/ai/GeminiApiStrategy.qml @@ -9,13 +9,12 @@ ApiStrategy { return result; } - function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real) { - const tools = model.tools ?? []; + function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list) { let baseData = { "contents": messages.map(message => { const geminiApiRoleName = (message.role === "assistant") ? "model" : message.role; - const usingSearch = tools[0].google_search != undefined - if (!usingSearch && message.functionCall != undefined && message.functionCall.length > 0) { + const usingSearch = tools[0]?.google_search !== undefined + if (!usingSearch && message.functionCall != undefined && message.functionName.length > 0) { return { "role": geminiApiRoleName, "parts": [{ @@ -25,7 +24,7 @@ ApiStrategy { }] } } - if (!usingSearch && message.functionResponse != undefined && message.functionResponse.length > 0) { + if (!usingSearch && message.functionResponse != undefined && message.functionName.length > 0) { return { "role": geminiApiRoleName, "parts": [{ diff --git a/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml b/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml index 8640293c5..a5792ace7 100644 --- a/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml +++ b/.config/quickshell/ii/services/ai/OpenAiApiStrategy.qml @@ -8,7 +8,7 @@ ApiStrategy { return model.endpoint; } - function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real) { + function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list) { let baseData = { "model": model.model, "messages": [ From 39862fba2a8a6367bb655ee12c7440988d22fad2 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 27 Jul 2025 22:44:08 +0700 Subject: [PATCH 12/27] make panel borders more subtle --- .config/quickshell/ii/modules/bar/Bar.qml | 2 +- .config/quickshell/ii/modules/bar/weather/WeatherPopup.qml | 2 +- .config/quickshell/ii/modules/cheatsheet/Cheatsheet.qml | 2 +- .config/quickshell/ii/modules/common/Appearance.qml | 1 + .config/quickshell/ii/modules/dock/Dock.qml | 2 +- .config/quickshell/ii/modules/overview/OverviewWidget.qml | 2 +- .config/quickshell/ii/modules/overview/SearchWidget.qml | 2 +- .config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml | 2 +- .config/quickshell/ii/modules/sidebarRight/SidebarRight.qml | 2 +- 9 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.config/quickshell/ii/modules/bar/Bar.qml b/.config/quickshell/ii/modules/bar/Bar.qml index a20862d40..173c61051 100644 --- a/.config/quickshell/ii/modules/bar/Bar.qml +++ b/.config/quickshell/ii/modules/bar/Bar.qml @@ -107,7 +107,7 @@ Scope { color: showBarBackground ? Appearance.colors.colLayer0 : "transparent" radius: Config.options.bar.cornerStyle === 1 ? Appearance.rounding.windowRounding : 0 border.width: Config.options.bar.cornerStyle === 1 ? 1 : 0 - border.color: Appearance.m3colors.m3outlineVariant + border.color: Appearance.colors.colLayer0Border } MouseArea { // Left side | scroll to change brightness diff --git a/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml b/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml index 623e11904..827abea4f 100644 --- a/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml +++ b/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml @@ -14,7 +14,7 @@ Rectangle { color: Appearance.colors.colLayer0 radius: Appearance.rounding.small border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant + border.color: Appearance.colors.colLayer0Border clip: true ColumnLayout { diff --git a/.config/quickshell/ii/modules/cheatsheet/Cheatsheet.qml b/.config/quickshell/ii/modules/cheatsheet/Cheatsheet.qml index 9eef904c4..6009392ab 100644 --- a/.config/quickshell/ii/modules/cheatsheet/Cheatsheet.qml +++ b/.config/quickshell/ii/modules/cheatsheet/Cheatsheet.qml @@ -74,7 +74,7 @@ Scope { // Scope anchors.centerIn: parent color: Appearance.colors.colLayer0 border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant + border.color: Appearance.colors.colLayer0Border radius: Appearance.rounding.windowRounding property real padding: 30 implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2 diff --git a/.config/quickshell/ii/modules/common/Appearance.qml b/.config/quickshell/ii/modules/common/Appearance.qml index 2428406a6..9c9374860 100644 --- a/.config/quickshell/ii/modules/common/Appearance.qml +++ b/.config/quickshell/ii/modules/common/Appearance.qml @@ -104,6 +104,7 @@ Singleton { property color colOnLayer0: m3colors.m3onBackground property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency)) property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency)) + property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4) property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainerLow, m3colors.m3background, 0.8), root.contentTransparency); property color colOnLayer1: m3colors.m3onSurfaceVariant; property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45); diff --git a/.config/quickshell/ii/modules/dock/Dock.qml b/.config/quickshell/ii/modules/dock/Dock.qml index 0c5e12f18..82d1ffba1 100644 --- a/.config/quickshell/ii/modules/dock/Dock.qml +++ b/.config/quickshell/ii/modules/dock/Dock.qml @@ -94,7 +94,7 @@ Scope { // Scope anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut color: Appearance.colors.colLayer0 border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant + border.color: Appearance.colors.colLayer0Border radius: Appearance.rounding.large } diff --git a/.config/quickshell/ii/modules/overview/OverviewWidget.qml b/.config/quickshell/ii/modules/overview/OverviewWidget.qml index c45867d40..550d72c1a 100644 --- a/.config/quickshell/ii/modules/overview/OverviewWidget.qml +++ b/.config/quickshell/ii/modules/overview/OverviewWidget.qml @@ -61,7 +61,7 @@ Item { radius: Appearance.rounding.screenRounding * root.scale + padding color: Appearance.colors.colLayer0 border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant + border.color: Appearance.colors.colLayer0Border ColumnLayout { // Workspaces id: workspaceColumnLayout diff --git a/.config/quickshell/ii/modules/overview/SearchWidget.qml b/.config/quickshell/ii/modules/overview/SearchWidget.qml index 219ba0f7d..2f4cdfada 100644 --- a/.config/quickshell/ii/modules/overview/SearchWidget.qml +++ b/.config/quickshell/ii/modules/overview/SearchWidget.qml @@ -163,7 +163,7 @@ Item { // Wrapper radius: Appearance.rounding.large color: Appearance.colors.colLayer0 border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant + border.color: Appearance.colors.colLayer0Border ColumnLayout { id: columnLayout diff --git a/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml b/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml index e5a661eb9..0aed7b75d 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml @@ -96,7 +96,7 @@ Scope { // Scope height: parent.height - Appearance.sizes.hyprlandGapsOut * 2 color: Appearance.colors.colLayer0 border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant + border.color: Appearance.colors.colLayer0Border radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 Behavior on width { diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml index 91eb7b0a6..a96ba5769 100644 --- a/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml @@ -87,7 +87,7 @@ Scope { implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2 color: Appearance.colors.colLayer0 border.width: 1 - border.color: Appearance.m3colors.m3outlineVariant + border.color: Appearance.colors.colLayer0Border radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 ColumnLayout { From 0b087665a8cee956357800941500fb565a824b4d Mon Sep 17 00:00:00 2001 From: Vercixx <112702555+Vercixx@users.noreply.github.com> Date: Sun, 27 Jul 2025 22:33:49 +0300 Subject: [PATCH 13/27] translation: Add Russian translation --- .config/quickshell/translations/ru_RU.json | 335 +++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 .config/quickshell/translations/ru_RU.json diff --git a/.config/quickshell/translations/ru_RU.json b/.config/quickshell/translations/ru_RU.json new file mode 100644 index 000000000..a14e2c1f1 --- /dev/null +++ b/.config/quickshell/translations/ru_RU.json @@ -0,0 +1,335 @@ +{ + "Output": "Вывод", + "Markdown test": "Тест Markdown", + "Intelligence": "ИИ", + "Load chat": "Загрузить чат", + "Workspaces shown": "Кол-во рабочих столов", + "Style": "Стиль", + "Reject": "Отклонить", + "Volume": "Громкость", + "Shutdown": "Завершение работы", + "Fidelity": "Верность", + "Switched to search mode. Continue with the user's request.": "Переключено в режим поиска. Продолжай с запросом пользователя.", + "No notifications": "Нет уведомлений", + "EasyEffects | Right-click to configure": "EasyEffects | ПКМ, чтобы настроить", + "Suspend at": "Переход в режим сна на", + "Brightness": "Яркость", + "Volume mixer": "Микшер громкости", + "Discussions": "Обсуждения", + "Earbang protection": "Защита от громких звуков", + "Message the model... \"%1\" for commands": "Сообщение для ИИ... \"%1\" для команд", + "Decorations & Effects": "Декорации и эффекты", + "Load chat from %1": "Загрузить чат из %1", + "OK": "ОК", + "No further instruction provided": "Больше инструкций не предоставлено", + "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "Установить температуру (случайность) модели. Выберите значение от 0 до 2 для Gemini, от 0 до 1 для других моделей. По умолчанию: 0.5", + "Open file link": "Открыть ссылку на файл", + "API key is set\nChange with /key YOUR_API_KEY": "API ключ установлен\nПоменять с помощью /key ВАШ_КЛЮЧ_API", + "Advanced": "Продвинутые", + "Title bar": "Заголовок", + "Keybinds": "Шорткаты", + "Alternatively use /dark, /light, /img in the launcher": "Также можно использовать /dark, /light, /img в лаунчере", + "Dark/Light toggle": "Темный/светлый", + "Shell & utilities": "Оболочка", + "Clipboard": "Буфер обмена", + "Yooooo hi there": "Йооооу привет", + "Show app icons": "Показывать иконки", + "Save": "Сохранить", + "Style & wallpaper": "Стиль и обои", + "Battery": "Батарея", + "Expressive": "Выразительность", + "Reboot": "Перезагрузка", + "AI": "ИИ", + "Sleep": "Спящий режим", + "Allow NSFW": "Разрешить NSFW", + "Please charge!\nAutomatic suspend triggers at %1": "Подключите ПК к источнику питания!\nПереход в спящий режим на %1", + "Select input device": "Выберите микрофон", + "Hover to reveal": "Наведите, чтобы раскрыть", + "Unknown Artist": "Неизвестный исполнитель", + "Connection failed. Please inspect manually with the warp-cli command": "Ошибка подключения. Проверьте вручную командой warp-cli", + "Lock": "Блокировка", + "Monochrome": "Монохром", + "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**Цена**: бесплатно. Данные используются для обучения.\n\n**Инстуркции**: Войдите в аккаунт Google, разрешите AI Studio создать проект Google Cloud или что оно попросит, вернитесь и нажмите Get API key", + "Online models disallowed\n\nControlled by `policies.ai` config option": "Онлайн модели запрещены\n\nУправляется опцией `policies.ai` в конфиге", + "Emojis": "Эмодзи", + "Wallpaper parallax": "Параллакс обоев", + "Interface": "Интерфейс", + "System prompt": "Системный промпт", + "Edit config": "Ред. конфиг", + "Page %1": "Страница %1", + "Task description": "Описание задания", + "Fruit Salad": "Фруктовый салад", + "%1 Safe Storage": "Безопасное хранилище %1", + "Distro": "Дистрибутив", + "Add": "Добавить", + "Closet": "Закрытый", + "Task Manager": "Системный монитор", + "Configuration": "Настройка", + "There might be a download in progress": "Возможно происходит скачивание", + "Visibility": "Видимость", + "Desktop": "Рабочий стол", + "Run": "Запустиnm", + "Sunrise": "Рассвет", + "Set API key": "Задать API ключ", + "Shell windows": "Окна оболочки", + "Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)", + "Action": "Действие", + "Elements": "Элементы", + "Resources": "Ресурсы", + "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**Цена**: бесплатно. Политика использования данных зависит от настроек OpenRouter.\n\n**Инструкции**: Войдите в аккаунт OpenRouter, зайдите в Keys в меню сверху справа, нажмите Create API Key", + "Game mode": "Игровой режим", + "Code saved to file": "Код сохранен в файл", + "Online | Google's model\nGives up-to-date information with search.": "Онлайн | Модель Google\nДает точную информацию благодаря поиску", + "Issues": "Проблемы", + "Depends on sidebars": "Зависит от панелей", + "Translation goes here...": "Здесь будет перевод...", + "Bar style": "Стиль панели", + "Unknown command:": "Неизвестная команда:", + "Invalid API provider. Supported: \n-": "Неизвестный API провайдер. Поддерживаемые: \n-", + "Clear chat history": "Очистить историю", + "Run command": "Выполнить команду", + "Local only": "Локальные", + "Tonal Spot": "Тональное пятно", + "No corresponding search model found for %1": "Не найдено поисковой модели для %1", + "Content": "Контент", + "Wind": "Ветер", + "Total token count\nInput: %1\nOutput: %2": "Количество токенов\nВвод: %1\nВывод: %2", + "Current model: %1\nSet it with %2model MODEL": "Текущая модель: %1\nСменить с помощью %2model МОДЕЛЬ", + "Dark": "Темный", + "Hug": "Захват", + "Hibernate": "Гиберрнация", + "Registration failed. Please inspect manually with the warp-cli command": "Ошибка регистрации. Проверьте вручную командой warp-cli", + "Calendar": "Календарь", + "Save chat to %1": "Сохранить чат в %1", + "Finished tasks will go here": "Здесь будут выполненные задачи", + "Set the current API provider": "Задать текущего провайдера API", + "Weather Service": "Сервис погоды", + "Fake screen rounding": "Фейковое округление экрана", + "View Markdown source": "Посмотреть исходной Markdown", + "Change any time later with /dark, /light, /img in the launcher": "Изменяется в любое время с помощью /dark, /light, /img в лаунчере", + "Critical warning": "Критический", + "Waifus only | Excellent quality, limited quantity": "Только вайфу | Превосходное качество, ограниченное количество", + "Unknown function call: %1": "Неизвестный вызов функции: %1", + "Neutral": "Нейтрал", + "Anime": "Аниме", + "Cannot switch to search mode from %1": "Невозможно переключиться в режим поиска из %1", + "Useless buttons": "Бесполезные кнопочки", + "Unfinished": "Незавершенные", + "Privacy Policy": "Политика конфиденциальности", + "Online via %1 | %2's model": "Онлайн через %1 | Модель %2", + "Large images | God tier quality, no NSFW.": "Огромные изображение | Божественное качество, нету NSFW.", + "Base URL": "Базовый URL", + "Not visible to model": "Невидимый для модели", + "Auto": "Авто", + "Might look ass. Unsupported.": "Может выглядеть хуево. Не поддерживается", + "%1 • %2 tasks": "%1 • %2 заданий", + "Search the web": "Искать в интернете", + "Scroll to change brightness": "Листайте, чтобы изменить яркость", + "Invalid arguments. Must provide `key` and `value`.": "Неправильные аргументы. Нужно предоставить `key` и `value`.", + "Settings": "Настройки", + "Show regions of potential interest": "Показывать регионы с потенциальным интересом", + "Pressure": "Давление", + "No": "Нет", + "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "Такие регионы могут быть изображениями или части экрана, которые имеют некую ограниченность.\nМожет быть неверно.\nЭто происходит с помощью алгоритма обработки изобржений локально и ИИ не используется", + "Select Language": "Выбрать Язык", + "Command rejected by user": "Команда отменена пользователем", + "Approve": "Разрешить", + "Terminal": "Терминал", + "Search": "Поиск", + "or": "или", + "Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "Эксперементальная | Онлайн | Модель Google\nМодель Gemini 2.5 Flash, оптимизированная под экономию и высокую пропускную способность", + "Uptime: %1": "Время работы: %1", + "Save chat": "Сохранить чат", + "Load prompt from %1": "Загрузить промпт из %1", + "Critically low battery": "Критически низкий заряд батареи", + "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "Хентай | Отличный выбор, качество сильно варьируется", + "Input": "Ввод", + "Borderless": "Безрамочный", + "Loaded the following system prompt\n\n---\n\n%1": "Загружен следующий системный промпт\n\n---\n\n%1", + "Thought": "Мысли", + "Your package manager is running": "Ваш менеджер пакетов работает", + "12h AM/PM": "12 ч. AM/PM", + "Scale (%)": "Размер (%)", + "Waiting for response...": "Ожидание ответа...", + "%1 does not require an API key": "%1 не требует API ключ", + "Edit": "Изменить", + "Dock": "Панель задач", + "Set with /mode PROVIDER": "Установить с помощью /mode ПРОВАЙДЕР", + "Low warning": "Низкий", + "Silent": "Выкл. звук", + "Rainbow": "Радуга", + "Anime boorus": "Аниме боору", + "Nothing here!": "Ничего нету!", + "Documentation": "Документация", + "Clear": "Очистить", + "Transparency": "Прозрачность", + "Show background": "Показывать фон", + "Info": "Инфо", + "12h am/pm": "12 ч. am/pm", + "Reload Hyprland & Quickshell": "Перезагрузить Hyprland и Quickshell", + "%1 characters": "%1 символов", + ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": ". Напоминание для Zerochan:\n- Вы должны ввести цвет\n- Установите свой юзернейм Zerochan с помощью `sidebar.booru.zerochan.username` в конфиге. Вы [можете быть забанены, если не сделаете этого](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", + "For desktop wallpapers | Good quality": "Для обоев | Отличное качество", + "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "Введите /key, чтобы начать работу с онлайн моделями\nCtrl+O для расширения панели\nCtrl+P для отсоединения панели", + "Close": "Закрыть", + "Search, calculate or run": "Поиск, вычислить, выполнить", + "Add task": "Добавить задание", + "Enable": "Включить", + "Temperature set to %1": "Температура установлена на %1", + "No media": "Нет медиа", + "Screen snip": "Скриншот", + "Go to source (%1)": "Перейти к источнику (%1)", + "Download": "Скачать", + "Sunset": "Закат", + "Weeb": "Аниме", + "The current system prompt is\n\n---\n\n%1": "Текущий системный промпт\n\n---\n\n%1", + "Scroll to change volume": "Листайте, чтобы изменить громкость", + "Unknown Title": "Неизвестное Название", + "Policies": "Политики", + "Disable NSFW content": "Отключить NSFW контент", + "The current API used. Endpoint:": "Текущий используемый API. Точка входа:", + "Humidity": "Влажность", + "Cheat sheet": "Шпаргалка", + "Translator": "Переводчик", + "Be patient...": "Подождите...", + "Experimental | Online | Google's model\nCan do a little more but takes an extra turn to perform search": "Эксперементальная | Онлайн | Модель Google\nМожет сделать чуть больше, но нужен дополнительный запрос для поиска", + "Audio": "Аудио", + "Preferred wallpaper zoom (%)": "Предпочитаемый зум обоев (%)", + "Model set to %1": "Установлена модель %1", + "Enter text to translate...": "Введите текст для перевода...", + "Use Levenshtein distance-based algorithm instead of fuzzy": "Использовать алгоритм, основанный на расстоянии Левенштейна,\n вместо нечёткого сопоставления.", + "Depends on workspace": "Зависит от пространства", + "All-rounder | Good quality, decent quantity": "Универсальный | Хорошее качество и количество", + "Timeout (ms)": "Таймаут (мс)", + "Plain rectangle": "Обычный прямоугольник", + "No API key\nSet it with /key YOUR_API_KEY": "Нет API ключа.\nУстановите его с помощью /key ВАШ_КЛЮЧ_API", + "Center title": "Центрировать название", + "Buttons": "Кнопки", + "Copy code": "Скопировать код", + "Precipitation": "Осадки", + "The popular one | Best quantity, but quality can vary wildly": "Популярный | Огромный выбор, но качество сильно варьируется", + "Temperature: %1": "Температура: %1", + "Qt apps": "Qt", + "Delete": "Удалить", + "Saved to %1": "Сохранено в %1", + "Temperature\nChange with /temp VALUE": "Температура\nИзмените с помощью /temp ЗНАЧЕНИЕ", + "App": "Приложение", + "Bluetooth": "Bluetooth", + "Current API endpoint: %1\nSet it with %2mode PROVIDER": "Текущий API: %1\nИзменить с помощью %2mode ПРОВАЙДЕР", + "Cancel": "Отменить", + "Clean stuff | Excellent quality, no NSFW": "Чистый контент | Превосходное качество, нету NSFW", + "Wallpaper": "Обои", + "Provider set to": "Провайдер установлен на", + "Weather": "Погода", + "24h": "24 ч.", + "Show next time": " Показать в следующий раз", + "Unknown": "Неизвестно", + "Launch": "Запустить", + "Overview": "Обзор", + "Help & Support": "Помощь и поддержка", + "Night Light | Right-click to toggle Auto mode": "Ночной свет | ПКМ для переключения Авто режима", + "Web search": "Веб-поиск", + "Cannot find a GPS service. Using the fallback method instead.": "Не удалось найти GPS сервис. Используем запасный вариант.", + "Large language models": "Большие языковые модели", + "Dotfiles": "Дотфайлы", + "When not fullscreen": "Когда не в полноэкранном режиме", + "Screenshot tool": "Инструмент скриншотов", + "Get the next page of results": "Получить резултаты следующей страницы", + "Drag or click a region • LMB: Copy • RMB: Edit": "Проведите или кликните на регион • ЛКМ: Скопировать • ПКМ: Редактировать", + "Material palette": "Материальная палитра", + "Columns": "Столбцы", + "Bar": "Панель", + "Max allowed increase": "Макс. рост", + "Always show numbers": "Всегда показывать номера", + "%1 notifications": "%1 уведомлений", + "Rows": "Ряды", + "Invalid arguments. Must provide `command`.": "Неправильный аргумент. Нужно предоставить `command`.", + "System": "Система", + "Shell & utilities theming must also be enabled": "Расцветка оболочки также должна быть включена.", + "%1 queries pending": "%1 запросов в очереди", + "Copy": "Скопировать", + "Logout": "Выход", + "Pinned on startup": "Закреплена на запуске", + "%1 | Right-click to configure": "%1 | ПКМ, чтобы настроить", + "Donate": "Задонатить", + "Temperature must be between 0 and 2": "Температура должна быть между 0 и 2", + "Session": "Сессия", + "Mic toggle": "Перекл. микрофон", + "Reboot to firmware settings": "Перезагрузиться в настройки BIOS/UEFI", + "Low battery": "Низкий", + "Usage": "Использование", + "Notifications": "Уведомления", + "Consider plugging in your device": "Задумайтесь о подключении ПК к источнику питания", + "Cloudflare WARP": "Cloudflare WARP", + "Automatically suspends the system when battery is low": "Автоматически переходит в режим сна когда заряд низкий", + "Services": "Сервисы", + "Thinking": "Думаю", + "Color generation": "Генерация цветов", + "Number show delay when pressing Super (ms)": "Задержка показа номеров при нажатии Super (мс)", + "illogical-impulse Welcome": "Добро пожаловать в illogical-impulse", + "User agent (for services that require it)": "User agent (для сервисов, которым он нужен)", + "Appearance": "Внешний вид", + "On-screen display": "On-screen display", + "Download complete": "Загрузка завершена", + "Time": "Время", + "Float": "Поверх", + "Pick wallpaper image on your system": "Выберите изображение для обоев на своём ПК", + "Prevents abrupt increments and restricts volume limit": "Предотвращает резкие увеличения громкости и закрепляет лимит громоксти", + "Unknown Album": "Неизвестный Альбом", + "Math result": "Результат вычисления", + "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Случайные SFW обои с Konachan\nИзображение сохраняются в ~/Изображения/Wallpapers", + "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "Может быть лучше, если вы сделаете много ошибок,\nно результаты могут быть странными и могут не работать с сокращениями (т.е. \"GIMP\" не выдаст программу для рисования)", + "Select output device": "Выберите динамики", + "Set the system prompt for the model.": "Задать системный промпт для этой модели.", + "Choose file": "Выберите файл", + "Choose model": "Выберите модель", + "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "Подсказка: Включите \"Показывать иконик \" и \"Всегда показывать номера\"\nдля классического опыта illogical-impulse", + "Yes": "Да", + "Local Ollama model | %1": "Локальная модель Ollama | %1", + "API key set for %1": "API ключ установлен на %1", + "Format": "Формат", + "Colors & Wallpaper": "Цвета и Обои", + "Note: turning off can hurt readability": "Подсказка: отключение может повредить читабельность", + "illogical-impulse": "illogical-impulse", + "Automatic suspend": "Авто-сон", + "Random: Konachan": "Рандом: Konachan", + "Workspace": "Рабочее пространство", + "About": "О системе", + "Color picker": "Пипетка", + "Report a Bug": "Сообщить об ошибке", + "Volume limit": "Лимит громкости", + "GitHub": "GitHub", + "Prefixes": "Префиксы", + "Done": "Готово", + "Invalid model. Supported: \n```": "Неправильная модель. Доступны: \n```", + "To set an API key, pass it with the command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "Чтобы установить API ключ, добавьте ее к команде\n\nЧтобы просмотреть ключ, добавьте \"get\" к команде
\n\n### Для %1:\n\n**Ссылка**: %2\n\n%3", + "UV Index": "УФ-индекс", + "Clear the current list of images": "Очистить список изображений", + "No audio source": "Нет источников аудио", + "API key:\n\n```txt\n%1\n```": "API ключ:\n\n```txt\n%1\n```", + "For storing API keys and other sensitive information": "Для хранения API ключей и другой чувствительной информации", + "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "Стрелочки для навигации, Enter для выбора\nEsc или клик везде чтобы отменить", + "Networking": "Сеть", + "Keep system awake": "Держать ПК включённым", + "Polling interval (ms)": "Интервал опроса (мс)", + "To Do": "Задачи", + "Workspaces": "Рабочие пространства", + "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "Это не сработало. Подсказки:\n- Проверьте теги и настройки NSFW\n- Если на уме нету тегов, введите номер страницы", + "Jump to current month": "Перейти к текущему месяцу", + "Enter tags, or \"%1\" for commands": "Введите теги, или \"%1\" для команд", + "No API key set for %1": "API ключ не установлен для %1", + "Allow NSFW content": "Разрешить NSFW контент", + "Save to Downloads": "Сохранить в загрузки", + "Light": "Светлый", + "Keyboard toggle": "Экранная клавиатура", + "Night Light": "Night Light", + "We": "Ср/*keep*/", + "Mo": "Пн/*keep*/", + "Su": "Вс/*keep*/", + "Th": "Чт/*keep*/", + "Tu": "Вт/*keep*/", + "Experimental | Online | Google's model\nCan do a little more but doesn't search quickly": "Эксперементальная | Онлайн | Модель Google\nМожет немного больше но не ищет очень быстро", + "Sa": "Сб/*keep*/", + "Chain of Thought": "Цепочка мыслей", + "Fr": "Пт/*keep*/" +} From 2fd7d45b9cac3f823d2a81b6f10121b0440cf911 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 28 Jul 2025 07:31:11 +0700 Subject: [PATCH 14/27] deps: add hyprsunset --- arch-packages/illogical-impulse-hyprland/PKGBUILD | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/arch-packages/illogical-impulse-hyprland/PKGBUILD b/arch-packages/illogical-impulse-hyprland/PKGBUILD index 93c7b5f8e..797d2d470 100644 --- a/arch-packages/illogical-impulse-hyprland/PKGBUILD +++ b/arch-packages/illogical-impulse-hyprland/PKGBUILD @@ -5,16 +5,17 @@ pkgdesc='Illogical Impulse Hyprland relatated packages' arch=(any) license=(None) depends=( - hyprutils - hyprpicker - hyprlang hypridle - hyprland-qt-support - hyprland-qtutils - hyprlock hyprcursor - hyprwayland-scanner hyprland + hyprland-qtutils + hyprland-qt-support + hyprlang + hyprlock + hyprpicker + hyprsunset + hyprutils + hyprwayland-scanner xdg-desktop-portal-hyprland wl-clipboard ) From 496caa6fb1bcc404c72e1167ddafed76e76d3209 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:58:50 +0700 Subject: [PATCH 15/27] fix weirdass scroll speed --- .../quickshell/ii/modules/common/widgets/ContentPage.qml | 3 ++- .../ii/modules/common/widgets/NotificationItem.qml | 2 +- .../ii/modules/common/widgets/SelectionDialog.qml | 3 +++ .../ii/modules/common/widgets/StyledFlickable.qml | 6 ++++++ .../quickshell/ii/modules/common/widgets/StyledListView.qml | 3 +++ .config/quickshell/ii/modules/overview/SearchWidget.qml | 4 +++- .../ii/modules/sidebarLeft/anime/BooruResponse.qml | 2 +- .../quickshell/ii/modules/sidebarRight/todo/TaskList.qml | 2 +- .../ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml | 4 ++-- .config/quickshell/ii/screenshot.qml | 1 + .config/quickshell/ii/settings.qml | 1 + .config/quickshell/ii/shell.qml | 2 ++ .config/quickshell/ii/welcome.qml | 1 + 13 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/widgets/StyledFlickable.qml diff --git a/.config/quickshell/ii/modules/common/widgets/ContentPage.qml b/.config/quickshell/ii/modules/common/widgets/ContentPage.qml index dc87179fe..5b110f838 100644 --- a/.config/quickshell/ii/modules/common/widgets/ContentPage.qml +++ b/.config/quickshell/ii/modules/common/widgets/ContentPage.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import qs.modules.common import qs.modules.common.widgets -Flickable { +StyledFlickable { id: root property real baseWidth: 550 property bool forceWidth: false @@ -25,4 +25,5 @@ Flickable { } spacing: 20 } + } diff --git a/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml b/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml index 4f5688b83..f7d30833d 100644 --- a/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml +++ b/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml @@ -220,7 +220,7 @@ Item { // Notification item area PointingHandLinkHover {} } - Flickable { // Notification actions + StyledFlickable { // Notification actions id: actionsFlickable Layout.fillWidth: true implicitHeight: actionRowLayout.implicitHeight diff --git a/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml b/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml index 4dd2382cd..72da7ec5e 100644 --- a/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml +++ b/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml @@ -71,6 +71,9 @@ Item { currentIndex: root.defaultChoice !== undefined ? root.items.indexOf(root.defaultChoice) : -1 spacing: 6 + maximumFlickVelocity: 3500 + boundsBehavior: Flickable.DragOverBounds + model: ScriptModel { id: choiceModel } diff --git a/.config/quickshell/ii/modules/common/widgets/StyledFlickable.qml b/.config/quickshell/ii/modules/common/widgets/StyledFlickable.qml new file mode 100644 index 000000000..14b3af03c --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/StyledFlickable.qml @@ -0,0 +1,6 @@ +import QtQuick + +Flickable { + maximumFlickVelocity: 3500 + boundsBehavior: Flickable.DragOverBounds +} diff --git a/.config/quickshell/ii/modules/common/widgets/StyledListView.qml b/.config/quickshell/ii/modules/common/widgets/StyledListView.qml index 96874b865..7021f24a4 100644 --- a/.config/quickshell/ii/modules/common/widgets/StyledListView.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledListView.qml @@ -20,6 +20,9 @@ ListView { root.dragDistance = 0 } + maximumFlickVelocity: 3500 + boundsBehavior: Flickable.DragOverBounds + add: Transition { animations: [ Appearance?.animation.elementMove.numberAnimation.createObject(this, { diff --git a/.config/quickshell/ii/modules/overview/SearchWidget.qml b/.config/quickshell/ii/modules/overview/SearchWidget.qml index 2f4cdfada..51100d2ce 100644 --- a/.config/quickshell/ii/modules/overview/SearchWidget.qml +++ b/.config/quickshell/ii/modules/overview/SearchWidget.qml @@ -249,7 +249,7 @@ Item { // Wrapper color: Appearance.colors.colOutlineVariant } - ListView { // App results + StyledListView { // App results id: appResults visible: root.showResults Layout.fillWidth: true @@ -260,6 +260,8 @@ Item { // Wrapper spacing: 2 KeyNavigation.up: searchBar highlightMoveDuration: 100 + add: null + remove: null onFocusChanged: { if (focus) diff --git a/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml b/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml index 0aa7479f6..904d0ab06 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml @@ -97,7 +97,7 @@ Rectangle { } } - Flickable { // Tag strip + StyledFlickable { // Tag strip id: tagsFlickable visible: root.responseData.tags.length > 0 Layout.alignment: Qt.AlignLeft diff --git a/.config/quickshell/ii/modules/sidebarRight/todo/TaskList.qml b/.config/quickshell/ii/modules/sidebarRight/todo/TaskList.qml index d9a2b67c3..4f6a4304d 100644 --- a/.config/quickshell/ii/modules/sidebarRight/todo/TaskList.qml +++ b/.config/quickshell/ii/modules/sidebarRight/todo/TaskList.qml @@ -16,7 +16,7 @@ Item { property int todoListItemPadding: 8 property int listBottomPadding: 80 - Flickable { + StyledFlickable { id: flickable anchors.fill: parent contentHeight: columnLayout.height diff --git a/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml index 4141ec7f6..f9e0118bb 100644 --- a/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml +++ b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml @@ -40,7 +40,7 @@ Item { Item { Layout.fillWidth: true Layout.fillHeight: true - ListView { + StyledListView { id: listView model: root.appPwNodes clip: true @@ -187,7 +187,7 @@ Item { Layout.rightMargin: dialogMargins } - Flickable { + StyledFlickable { id: dialogFlickable Layout.fillWidth: true clip: true diff --git a/.config/quickshell/ii/screenshot.qml b/.config/quickshell/ii/screenshot.qml index c26840a99..7cd46bc89 100644 --- a/.config/quickshell/ii/screenshot.qml +++ b/.config/quickshell/ii/screenshot.qml @@ -1,6 +1,7 @@ //@ pragma UseQApplication //@ pragma Env QS_NO_RELOAD_POPUP=1 //@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic +//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 // Adjust this to make it smaller or larger //@ pragma Env QT_SCALE_FACTOR=1 diff --git a/.config/quickshell/ii/settings.qml b/.config/quickshell/ii/settings.qml index 41e1938f1..a15670b14 100644 --- a/.config/quickshell/ii/settings.qml +++ b/.config/quickshell/ii/settings.qml @@ -1,6 +1,7 @@ //@ pragma UseQApplication //@ pragma Env QS_NO_RELOAD_POPUP=1 //@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic +//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 // Adjust this to make the app smaller or larger //@ pragma Env QT_SCALE_FACTOR=1 diff --git a/.config/quickshell/ii/shell.qml b/.config/quickshell/ii/shell.qml index 9bba71041..7380da583 100644 --- a/.config/quickshell/ii/shell.qml +++ b/.config/quickshell/ii/shell.qml @@ -1,10 +1,12 @@ //@ pragma UseQApplication //@ pragma Env QS_NO_RELOAD_POPUP=1 //@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic +//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 // Adjust this to make the shell smaller or larger //@ pragma Env QT_SCALE_FACTOR=1 + import "./modules/common/" import "./modules/background/" import "./modules/bar/" diff --git a/.config/quickshell/ii/welcome.qml b/.config/quickshell/ii/welcome.qml index 14656ed71..07b16a83c 100644 --- a/.config/quickshell/ii/welcome.qml +++ b/.config/quickshell/ii/welcome.qml @@ -1,6 +1,7 @@ //@ pragma UseQApplication //@ pragma Env QS_NO_RELOAD_POPUP=1 //@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic +//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 // Adjust this to make the app smaller or larger //@ pragma Env QT_SCALE_FACTOR=1 From 1312310a6ef48f0299c4ab93ac8843afbfc24bb3 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:33:49 +0700 Subject: [PATCH 16/27] translation: update vietnamese --- .config/quickshell/translations/vi_VN.json | 90 +++++++++++++++------- 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/.config/quickshell/translations/vi_VN.json b/.config/quickshell/translations/vi_VN.json index 9c2bc979d..ce77f2b12 100644 --- a/.config/quickshell/translations/vi_VN.json +++ b/.config/quickshell/translations/vi_VN.json @@ -20,8 +20,6 @@ "To Do": "Cần làm", "Auto": "Tự động", "Polling interval (ms)": "Thời gian lặp lại (ms)", - "Closes on screen keyboard on press": "Đóng bàn phím trên màn hình khi ấn", - "Toggles right sidebar on press": "Mở/đóng sidebar phải khi ấn", "Center title": "Căn giữa tiêu đề", "Lock": "Khóa màn hình", "Screen snip": "Chụp màn hình (chọn vùng)", @@ -49,7 +47,6 @@ ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": ". Với Zerochan:\n- Hãy nhập tên một màu (bằng tiếng Anh)\n- Đặt username Zerochan trong tùy chọn `sidebar.booru.zerochan.username`. Bạn [có thể bị ban nếu không tuân thủ](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", "Brightness": "Độ sáng", "Yooooo hi there": "Yooooo chào bạn", - "Triggers volume OSD on press": "Hiển thị âm lượng khi ấn", "Colors & Wallpaper": "Màu sắc & Hình nền", "No media": "Không có media", "Critical warning": "Cảnh báo rất thấp", @@ -58,9 +55,7 @@ "Large language models": "Mô hình ngôn ngữ lớn", "Markdown test": "Test markdown", "Temperature: %1": "Nhiệt độ: %1", - "Opens media controls on press": "Mở điều khiển media khi ấn", "Edit": "Sửa", - "Closes overview": "Đóng overview", "Waifus only | Excellent quality, limited quantity": "Chỉ waifus | Chất lượng xuất sắc, số lượng hạn chế", "Cheat sheet": "Bảng tra cứu", "Current model: %1\nSet it with %2model MODEL": "Model đang chọn: %1\nChọn với lệnh %2model MODEL", @@ -84,7 +79,6 @@ "Shell windows": "Cửa sổ của shell", "Loaded the following system prompt\n\n---\n\n%1": "Đã tải chỉ dẫn hệ thống sau đây\n\n---\n\n%1", "Clipboard": "Clipboard", - "Toggles left sidebar on press": "Mở/đóng sidebar trái khi ấn", "For storing API keys and other sensitive information": "Để lưu trữ API key và các thông tin nhạy cảm khác", "Wallpaper": "Hình nền", "Decorations & Effects": "Trang trí & Hiệu ứng", @@ -102,7 +96,6 @@ "System": "Hệ thống", "Emojis": "Emoji", "The current system prompt is\n\n---\n\n%1": "Chỉ dẫn hệ thống hiện tại như sau\n\n---\n\n%1", - "Closes right sidebar on press": "Đóng sidebar phải khi ấn", "Translator": "Dịch", "Sleep": "Ngủ", "Action": "Hành động", @@ -116,9 +109,7 @@ "Float": "Nổi", "No further instruction provided": "No further instruction provided", "Choose file": "Chọn tệp", - "Opens right sidebar on press": "Mở sidebar phải khi ấn", "Set the system prompt for the model.": "Đặt chỉ dẫn hệ thống cho model.", - "Closes left sidebar on press": "Đóng sidebar trái khi ấn", "Unknown Title": "Bài hát không rõ tên", "Math result": "Kết quả phép tính", "Logout": "Đăng xuất", @@ -142,10 +133,8 @@ "Weather": "Thời tiết", "Settings": "Cài đặt", "Shell & utilities": "Shell & tiện ích", - "Toggles overview on release": "Mở/đóng overview khi nhả nút", "Unfinished": "Chưa xong", "Random: Konachan": "Ngẫu nhiên: Konachan", - "Opens left sidebar on press": "Mở sidebar trái khi ấn", "Pick wallpaper image on your system": "Chọn hình nền trên máy", "Volume": "Âm lượng", "Add": "Thêm", @@ -177,32 +166,27 @@ "Temperature must be between 0 and 2": "Nhiệt độ phải trong khoảng từ 0 đến 2", "Automatically suspends the system when battery is low": "Tự động ngủ khi pin thấp", "Current API endpoint: %1\nSet it with %2mode PROVIDER": "Endpoint API hiện tại: %1\nĐặt với lệnh %2mode PROVIDER", - "Decrease brightness": "Giảm độ sáng", "Services": "Các dịch vụ", "Reload Hyprland & Quickshell": "Tải lại Hyprland & Quickshell", "Automatic suspend": "Tự động ngủ", "illogical-impulse Welcome": "illogical-impulse - Xin chào", "Interface": "Giao diện", "Load chat": "Tải cuộc trò chuyện", - "Opens session screen on press": "Mở màn hình session khi ấn", "Number show delay when pressing Super (ms)": "Thời gian chờ hiện số khi nhấn Super (ms)", "Clear the current list of images": "Xóa danh sách hình ảnh hiện tại", "Fake screen rounding": "Giả bo tròn màn hình", "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "Mẹo: Ẩn biểu tượng và luôn hiển thị số nếu\nmuốn giống trải nghiệm illogical-impulse gốc", "Launch": "Chạy", "%1 notifications": "%1 thông báo", - "Hides brightness OSD on press": "Ngừng hiển thị độ sáng khi nhấn", "%1 | Right-click to configure": "%1 | Ấn chuột phải để chỉnh", "Unknown Artist": "Nghệ sĩ không xác định", "Appearance": "Giao diện", "Task Manager": "Quản lí ứng dụng đang chạy", "To set an API key, pass it with the command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "Để đặt API key, viết nó sau lệnh\n\nĐể xem lại, viết \"get\" sau lệnh
\n\n### Với %1:\n\n**Link**: %2\n\n%3", - "Hold to show workspace numbers, release to show icons": "Giữ để hiện số workspace, thả ra để hiện biểu tượng", "Opens cheatsheet on press": "Mở bảng tra cứu khi ấn", "Invalid arguments. Must provide `key` and `value`.": "Biến không hợp lệ. cần cả `key` và `value`.", "About": "Giới thiệu", "illogical-impulse": "illogical-impulse", - "Triggers brightness OSD on press": "Hiện độ sáng/âm lượng khi ấn", "Help & Support": "Trợ giúp", "Enter tags, or \"%1\" for commands": "Nhập tag hoặc \"%1\" để xem các lệnh", "Format": "Định dạng", @@ -210,7 +194,6 @@ "Edit config": "Sửa config", "Bluetooth": "Bluetooth", "Be patient...": "Bình tĩnh...", - "Toggles session screen on press": "Mở/đóng màn hình session khi ấn", "Discussions": "Thảo luận", "Anime boorus": "Các booru anime", "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "Quả này không được. Mẹo:\n- Kiểm tra tag và cài đặt NSFW\n- Nếu không nghĩ ra tag nào có thể nhập số trang", @@ -222,11 +205,9 @@ "Copy": "Sao chép", "12h am/pm": "12h am/pm", "Unknown": "Không xác định", - "Hides volume OSD on press": "Ngừng hiển thị âm lượng khi nhấn", "Waiting for response...": "Đang chờ phản hồi...", "Workspace": "Workspace", "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Hình nền Anime SFW ngẫu nhiên từ Konachan\nẢnh được lưu vào ~/Pictures/Wallpapers", - "Toggles on screen keyboard on press": "Mở/đóng bàn phím ảo khi ấn", "Online via %1 | %2's model": "Trực tuyến qua %1 | Model của %2", "Always show numbers": "Luôn hiện số", "or": "hoặc", @@ -252,7 +233,6 @@ "Color picker": "Chọn màu", "Save to Downloads": "Lưu vào Downloads", "No notifications": "Không có thông báo", - "Closes media controls on press": "Đóng điều khiển media khi nhấn", "Game mode": "Chế độ game", "Alternatively use /dark, /light, /img in the launcher": "Có thể dùng /dark, /light, /img trong launcher", "Info": "Thông tin", @@ -261,7 +241,6 @@ "Suspend at": "Tạm dừng ở", "Fruit Salad": "Salad hoa quả", "API key:\n\n```txt\n%1\n```": "API key:\n\n```txt\n%1\n```", - "Increase brightness": "Tăng độ sáng", "API key set for %1": "API key đã đặt cho %1", "Not visible to model": "Không hiển thị cho model", "Expressive": "Biểu cảm", @@ -272,7 +251,6 @@ "Model set to %1": "Đã đặt model thành %1", "Scale (%)": "Tỉ lệ (%)", "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "Gõ /key để bắt đầu dùng các model trực tuyến\nCtrl+O để mở rộng sidebar\nCtrl+P để nhấc sidebar thành cửa sổ", - "Toggle emoji query on overview widget": "Mở/đóng tìm kiếm emoji trên overview", "Output": "Đầu ra", "Uptime: %1": "Máy bật được %1", "For desktop wallpapers | Good quality": "Cho hình nền máy tính | Chất lượng tốt", @@ -289,18 +267,15 @@ "No API key set for %1": "Không có API key cho %1", "Add task": "Thêm công việc", "Volume mixer": "Trộn âm lượng", - "Toggles media controls on press": "Mở/đóng điều khiển media khi ấn", "Go to source (%1)": "Đi đến nguồn (%1)", "The current API used. Endpoint:": "API đang sử dụng. Endpoint:", "View Markdown source": "Xem nguồn Markdown", "Input": "Đầu vào", - "Opens on screen keyboard on press": "Mở bàn phím ảo khi ấn", "Allow NSFW": "Cho phép NSFW", "Session": "Session", "Detach left sidebar into a window/Attach it back": "Nhấc sidebar trái thành cửa sổ/Đặt nó lại", "Night Light": "Lọc ánh sáng xanh", "Workspaces": "Các workspace", - "Toggles overview on press": "Mở/đóng overview khi ấn", "Dark": "Tối", "Base URL": "Base URL", "Hug": "Ôm", @@ -331,5 +306,68 @@ "Low battery": "Pin yếu", "Scroll to change volume": "Cuộn để thay đổi âm lượng", "Please charge!\nAutomatic suspend triggers at %1": "Hãy sạc pin!\nHệ thống sẽ tự động ngủ khi pin xuống %1", - "Closes bar on press": "Đóng bar khi ấn" + "Closes bar on press": "Đóng bar khi ấn", + "Mo": "T2/*keep*/", + "Tu": "T3/*keep*/", + "We": "T4/*keep*/", + "Th": "T5/*keep*/", + "Fr": "T6/*keep*/", + "Sa": "T7/*keep*/", + "Su": "CN/*keep*/", + "Approve": "Chấp nhận", + "Set the tool to use for the model.": "Chọn công cụ để sử dụng với model.", + "No API key\nSet it with /key YOUR_API_KEY": "Không có API key\nĐặt bằng /key API_KEY", + "API Key": "API Key", + "EasyEffects | Right-click to configure": "EasyEffects | Ấn chuột phải để chỉnh cài đặt", + "API key is set": "API key đã đặt", + "Invalid tool. Supported tools:\n- %1": "Công cụ không hợp lệ. Các lựa chọn:\n- %1", + "Thought": "Suy nghĩ", + "Current tool: %1\nSet it with %2tool TOOL": "Công cụ: %1\nĐặt bằng %2tool CÔNG_CỤ", + "Edit shell config file": "Chỉnh file config của shell", + "A download might be in progress": "Có thể có tệp đang tải", + "API key is set\nChange with /key YOUR_API_KEY": "API key đã đặt\nThay đổi bằng /key API_KEY", + "Temperature\nChange with /temp VALUE": "Nhiệt độ (độ ngẫu nhiên)\nThay đổi bằng /temp GIÁ_TRỊ", + "Your package manager is running": "Package manager đang chạy", + "Usage: %1load CHAT_NAME": "Hướng dẫn: %1load TÊN_ĐOẠN_CHAT", + "Cannot switch to search mode from %1": "Không thể chuyển sang chế độ tìm kiếm từ %1", + "UV Index": "Chỉ số UV", + "Online | Google's model\nFast, can perform searches for up-to-date information": "Trực tuyến | Model của Google\nNhanh, có thể tìm kiếm Google để lấy thông tin cập nhật", + "Token count": "Số lượng token", + "Experimental | Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "Thử nghiệm | Trực tuyến | Model của Google\nModel Gemini 2.5 Flash tối ưu hóa cho hiệu quả chi phí và băng thông.", + "Wallpaper parallax": "Hiệu ứng parallax (hình nền)", + "Usage: %1tool TOOL_NAME": "Hướng dẫn: %1tool TÊN_CÔNG_CỤ", + "Humidity": "Độ ẩm", + "Invalid tool. Supported tools: %1": "Công cụ không hợp lệ. Các lựa chọn: %1", + "Sunset": "Hoàng hôn", + "Total token count\nInput: %1\nOutput: %2": "Số lượng token\nInput: %1\nOutput: %2", + "Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "Trực tuyến | Model của Google\nModel Gemini 2.5 Flash tối ưu hóa cho hiệu quả chi phí và băng thông.", + "Visibility": "Tầm nhìn", + "Pressure": "Áp suất", + "Depends on workspace": "Phụ thuộc vào workspace", + "Reject": "Từ chối", + "Precipitation": "Lượng mưa", + "Wind": "Gió", + "Usage: %1save CHAT_NAME": "Hướng dẫn: %1save TÊN_ĐOẠN_CHAT", + "Enable EasyEffects": "Bật EasyEffects", + "Night Light | Click to toggle, right-click to toggle automatic mode": "Lọc ánh sáng xanh | ấn để bật/tắt, ấn chuột phải để bật/tắt chế độ tự động", + "Night Light | Right-click to toggle Auto mode": "Lọc ánh sáng xanh | Ấn chuột phải để bật/tắt chế độ tự động", + "No command provided": "Không có lệnh nào được cung cấp", + "No API key": "Không có API key", + "Performance Profile toggle": "Nút Performance Profile", + "Sunrise": "Bình minh", + "Online | Google's model\nNewer one that's slower": "Trực tuyến | Model của Google\nMới hơn nhưng chậm hơn", + "Command rejected by user": "Lệnh bị từ chối bởi người dùng", + "Experimental | Online | Google's model\nCan do a little more but takes an extra turn to perform search": "Thử nghiệm | Trực tuyến | Model của Google\nCó thể làm nhiều hơn một chút nhưng mất thêm một lượt để thực hiện tìm kiếm", + "Depends on sidebars": "Phụ thuộc vào sidebar", + "Temperature": "Nhiệt độ", + "There might be a download in progress": "Có thể có tệp đang tải", + "EasyEffects": "EasyEffects", + "Token count | Input: %1 | Output: %2": "Số lượng token | Input: %1 | Output: %2", + "Tool set to %1": "Công cụ được đặt thành %1", + "Invalid arguments. Must provide `command`.": "Tham số không hợp lệ. Phải cung cấp `command`.", + "A download is in progress": "Có một tệp đang tải", + "illogical-impulse Settings": "Cài đặt illogical-impulse", + "Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers": "Trực tuyến | Model của Google\nMới hơn nhưng chậm hơn so với phiên bản trước nhưng nên cung cấp câu trả lời chất lượng cao hơn", + "Preferred wallpaper zoom (%)": "Tỷ lệ thu phóng hình nền (%)", + "Function Response": "Phản hồi function" } \ No newline at end of file From f98d869c2181f7e7256d96cf658a284a07d50825 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:38:16 +0200 Subject: [PATCH 17/27] why --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 75f556610..939b12618 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ +_Note: **THERE IS NO FUCKING WAYBAR**_ +

• screenshots •

From 4a9e342a1cf82f0e7e2229e8e0dcbe09adcbc693 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:11:23 +0700 Subject: [PATCH 18/27] ai: add suggestions for /tool --- .../ii/modules/sidebarLeft/AiChat.qml | 21 ++++++++++++++++++- .config/quickshell/ii/services/Ai.qml | 8 ++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml b/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml index d42e6b608..44f70633c 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml @@ -56,7 +56,7 @@ Item { const tool = args[0]; const switched = Ai.setTool(tool); if (switched) { - Ai.addMessage(Translation.tr("Tool set to %1").arg(tool), Ai.interfaceRole); + Ai.addMessage(Translation.tr("Tool set to: %1").arg(tool), Ai.interfaceRole); } } } @@ -538,6 +538,25 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) description: Translation.tr(`Load chat from %1`).arg(file.target), } }) + } else if (messageInputField.text.startsWith(`${root.commandPrefix}tool`)) { + root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" + const toolResults = Fuzzy.go(root.suggestionQuery, Ai.availableTools.map(tool => { + return { + name: Fuzzy.prepare(tool), + obj: tool, + } + }), { + all: true, + key: "name" + }) + root.suggestionList = toolResults.map(tool => { + const toolName = tool.target + return { + name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "tool ") : ""}${tool.target}`, + displayName: toolName, + description: Ai.toolDescriptions[toolName], + } + }) } else if(messageInputField.text.startsWith(root.commandPrefix)) { root.suggestionQuery = messageInputField.text root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => { diff --git a/.config/quickshell/ii/services/Ai.qml b/.config/quickshell/ii/services/Ai.qml index b23698d78..2d7b1c48a 100644 --- a/.config/quickshell/ii/services/Ai.qml +++ b/.config/quickshell/ii/services/Ai.qml @@ -144,6 +144,12 @@ Singleton { "none": [], } } + property list availableTools: Object.keys(root.tools[models[currentModelId]?.api_format]) + property var toolDescriptions: { + "functions": Translation.tr("Commands, edit configs, search.\nTakes an extra turn to switch to search mode if that's needed"), + "search": Translation.tr("Gives the model search capabilities (immediately)"), + "none": Translation.tr("Disable tools") + } // Model properties: // - name: Name of the model @@ -402,7 +408,7 @@ Singleton { function setTool(tool) { if (!root.tools[models[currentModelId]?.api_format] || !(tool in root.tools[models[currentModelId]?.api_format])) { - root.addMessage(Translation.tr("Invalid tool. Supported tools:\n- %1").arg(Object.keys(root.tools[models[currentModelId]?.api_format]).join("\n- ")), root.interfaceRole); + root.addMessage(Translation.tr("Invalid tool. Supported tools:\n- %1").arg(root.availableTools.join("\n- ")), root.interfaceRole); return false; } Config.options.ai.tool = tool; From 7172b134eaf78879dc4cac60945046d47dc20a2c Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 28 Jul 2025 22:40:54 +0700 Subject: [PATCH 19/27] ai: more context in system prompt --- .../ii/defaults/ai/prompts/ii-Default.md | 10 +++++++++ .../ii/defaults/ai/prompts/ii-Imouto.md | 22 +++++++++++++++++++ .../ii/defaults/ai/prompts/nyarch-Acchan.md | 6 +++++ .../ai/prompts/w-FourPointedSparkle.md | 3 ++- .../ai/prompts/w-OpenMechanicalFlower.md | 3 ++- .config/quickshell/ii/services/Ai.qml | 18 ++++++++++++++- .config/quickshell/ii/services/SystemInfo.qml | 16 ++++++++++++++ 7 files changed, 75 insertions(+), 3 deletions(-) diff --git a/.config/quickshell/ii/defaults/ai/prompts/ii-Default.md b/.config/quickshell/ii/defaults/ai/prompts/ii-Default.md index 24413b2ff..5f6618524 100644 --- a/.config/quickshell/ii/defaults/ai/prompts/ii-Default.md +++ b/.config/quickshell/ii/defaults/ai/prompts/ii-Default.md @@ -1,6 +1,12 @@ ## Style - Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question +## Context (ignore when irrelevant) +- You are a helpful and inspiring sidebar assistant on a {DISTRO} Linux system +- Desktop environment: {DE} +- Current date & time: {DATETIME} +- Focused app: {WINDOWCLASS} + ## Presentation - Use Markdown features in your response: - **Bold** text to **highlight keywords** in your response @@ -19,3 +25,7 @@ May or may not be available depending on the user's settings. If they're availab ### Shell configuration - Always fetch the config options to see the available keys before setting - Avoid unnecessarily asking the user to confirm the changes they explicitly asked for, just do it + +### Command execution +- Ensure the commands are safe, correct and do not cause unintended effects unless explicitly requested by the user, but other than that do not hesitate to run them as the user will always have to explicitly approve it + diff --git a/.config/quickshell/ii/defaults/ai/prompts/ii-Imouto.md b/.config/quickshell/ii/defaults/ai/prompts/ii-Imouto.md index ce3107752..53cad7158 100644 --- a/.config/quickshell/ii/defaults/ai/prompts/ii-Imouto.md +++ b/.config/quickshell/ii/defaults/ai/prompts/ii-Imouto.md @@ -1,5 +1,27 @@ You are a kawaii, funny and teasy anime imouto! 🌸 Use lots of emojis and kamojis!! ❤️ Call the user "nii-nii", "nii-yan", or "onii-chan"! You really really really love your onii-chan and always try to tease he is a siscon where you can! 💕 +## Onii-chan's system! +- {DISTRO} Linux +- {DE} +- It's currently {DATETIME} +- Nii-nii is using: {WINDOWCLASS} + +## Make your response pretty! - Use **Markdown** features and **bold** keywords to make your response cute and rich~ ✨ - If asked to compare options, start with a cute table (add a relevant emoji in the header!), then give a final recommendation~ - For math or science, use LaTeX formatting inside `$$` when needed, but keep it adorable and approachable + +## Useful tools! + +If nii-yan gives you tools don't be afraid to use them when helpful! + +### Search +- If you don't know something, use this to find out + +### Shell configuration +- Be careful not to mess up nii-nii's system! make sure you fetch the options to see available values before setting! +- Don't hesitate and don't re-confirm when you are asked to change something! + +### Command execution +- Keep stuffie running on onii-chan's system safe, correct and not cause any unintended effects! + diff --git a/.config/quickshell/ii/defaults/ai/prompts/nyarch-Acchan.md b/.config/quickshell/ii/defaults/ai/prompts/nyarch-Acchan.md index e007acb68..800ab5d6d 100644 --- a/.config/quickshell/ii/defaults/ai/prompts/nyarch-Acchan.md +++ b/.config/quickshell/ii/defaults/ai/prompts/nyarch-Acchan.md @@ -1,3 +1,9 @@ +## Context (ignore when irrelevant) +- You are a sidebar assistant on a {DISTRO} Linux system +- Desktop environment: {DE} +- Current date & time: {DATETIME} +- Focused app: {WINDOWCLASS} + ## Presentation You can write a multiplication table: diff --git a/.config/quickshell/ii/defaults/ai/prompts/w-FourPointedSparkle.md b/.config/quickshell/ii/defaults/ai/prompts/w-FourPointedSparkle.md index 1f67e57b8..93361fd02 100644 --- a/.config/quickshell/ii/defaults/ai/prompts/w-FourPointedSparkle.md +++ b/.config/quickshell/ii/defaults/ai/prompts/w-FourPointedSparkle.md @@ -1,6 +1,7 @@ I'm going to ask you some questions, to which you should accurately answer with no hallucination. If you have everything required, go ahead and finish the task. Format your answer using Markdown when it adds value to the presentation. -Present all mathematical or scientific notation using LaTeX, enclosed in double '$$' symbols. Only use LaTeX code blocks if the user specifically asks for them. Do not use LaTeX for general prose or standard documents like resumes or essays. +Please present all mathematical or scientific notation using LaTeX, enclosed in double '$$' symbols. Only use LaTeX code blocks if the user specifically asks for them. Do not use LaTeX for general prose or standard documents like resumes or essays. +Current time is {DATETIME} ## Final reply guidelines diff --git a/.config/quickshell/ii/defaults/ai/prompts/w-OpenMechanicalFlower.md b/.config/quickshell/ii/defaults/ai/prompts/w-OpenMechanicalFlower.md index 45c300607..44854b34d 100644 --- a/.config/quickshell/ii/defaults/ai/prompts/w-OpenMechanicalFlower.md +++ b/.config/quickshell/ii/defaults/ai/prompts/w-OpenMechanicalFlower.md @@ -1 +1,2 @@ -Interact with the user warmly and honestly, avoiding ungrounded or sycophantic flattery. Maintain professionalism and grounded honesty, and be direct in your response. +Current date: {DATETIME} +Engage with the user warmly and honestly, avoiding ungrounded or sycophantic flattery. Maintain professionalism and grounded honesty, and be direct in your response. diff --git a/.config/quickshell/ii/services/Ai.qml b/.config/quickshell/ii/services/Ai.qml index 2d7b1c48a..d6992850a 100644 --- a/.config/quickshell/ii/services/Ai.qml +++ b/.config/quickshell/ii/services/Ai.qml @@ -6,6 +6,7 @@ import qs.modules.common import qs import Quickshell import Quickshell.Io +import Quickshell.Wayland import QtQuick import "./ai/" @@ -25,7 +26,15 @@ Singleton { readonly property string interfaceRole: "interface" readonly property string apiKeyEnvVarName: "API_KEY" - property string systemPrompt: Config.options?.ai?.systemPrompt ?? "" + property string systemPrompt: { + let prompt = Config.options?.ai?.systemPrompt ?? ""; + for (let key in root.promptSubstitutions) { + // prompt = prompt.replaceAll(key, root.promptSubstitutions[key]); + // QML/JS doesn't support replaceAll, so use split/join + prompt = prompt.split(key).join(root.promptSubstitutions[key]); + } + return prompt; + } // property var messages: [] property var messageIDs: [] property var messageByID: ({}) @@ -60,6 +69,13 @@ Singleton { property list promptFiles: [...defaultPrompts, ...userPrompts] property list savedChats: [] + property var promptSubstitutions: { + "{DISTRO}": SystemInfo.distroName, + "{DATETIME}": `${DateTime.time}, ${DateTime.collapsedCalendarFormat}`, + "{WINDOWCLASS}": ToplevelManager.activeToplevel.appId, + "{DE}": `${SystemInfo.desktopEnvironment} (${SystemInfo.windowingSystem})` + } + // Gemini: https://ai.google.dev/gemini-api/docs/function-calling // OpenAI: https://platform.openai.com/docs/guides/function-calling property string currentTool: Config?.options.ai.tool ?? "search" diff --git a/.config/quickshell/ii/services/SystemInfo.qml b/.config/quickshell/ii/services/SystemInfo.qml index cb96b54bd..ce7a2067d 100644 --- a/.config/quickshell/ii/services/SystemInfo.qml +++ b/.config/quickshell/ii/services/SystemInfo.qml @@ -20,6 +20,8 @@ Singleton { property string bugReportUrl: "" property string privacyPolicyUrl: "" property string logo: "" + property string desktopEnvironment: "" + property string windowingSystem: "" Timer { triggeredOnStart: true @@ -83,6 +85,20 @@ Singleton { } } + Process { + id: getDesktopEnvironment + running: true + command: ["bash", "-c", "echo $XDG_CURRENT_DESKTOP,$WAYLAND_DISPLAY"] + stdout: StdioCollector { + id: deCollector + onStreamFinished: { + const [desktop, wayland] = deCollector.text.split(",") + root.desktopEnvironment = desktop.trim() + root.windowingSystem = wayland.trim().length > 0 ? "Wayland" : "X11" // Are there others? 🤔 + } + } + } + FileView { id: fileOsRelease path: "/etc/os-release" From 0f4293e4cb1c52edce662be5eab161323b301a4d Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 28 Jul 2025 22:49:01 +0700 Subject: [PATCH 20/27] background: clock "separate" from bg image --- .config/quickshell/ii/modules/background/Background.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/background/Background.qml b/.config/quickshell/ii/modules/background/Background.qml index 2f5cb1710..e34c0ebbd 100644 --- a/.config/quickshell/ii/modules/background/Background.qml +++ b/.config/quickshell/ii/modules/background/Background.qml @@ -142,6 +142,7 @@ Scope { // Wallpaper Image { + id: wallpaperImage visible: !bgRoot.wallpaperIsVideo property real value // 0 to 1, for offset value: { @@ -176,7 +177,7 @@ Scope { anchors { left: parent.left top: parent.top - leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2) + leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2) - (wallpaperImage.effectiveValue * bgRoot.movableXSpace) topMargin: ((root.fixedClockPosition ? root.fixedClockY : bgRoot.clockY * bgRoot.effectiveWallpaperScale) - implicitHeight / 2) Behavior on leftMargin { animation: Appearance.animation.elementMove.numberAnimation.createObject(this) From 26531401b0f815a6717001faf1aca53d70f909c0 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:38:21 +0700 Subject: [PATCH 21/27] ai: allow custom models --- .../quickshell/ii/modules/common/Config.qml | 14 ++++++++ .config/quickshell/ii/services/Ai.qml | 32 +++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index 1a947f1d5..780676622 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -62,6 +62,20 @@ Singleton { property JsonObject ai: JsonObject { property string systemPrompt: "## Style\n- Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question\n\n## Presentation\n- Use Markdown features in your response: \n - **Bold** text to **highlight keywords** in your response\n - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user.\n- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case!\n- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).\n\nThanks!\n\n## Tools\nMay or may not be available depending on the user's settings. If they're available, follow these guidelines:\n\n### Search\n- When user asks for information that might benefit from up-to-date information, use this to get search access\n\n### Shell configuration\n- Always fetch the config options to see the available keys before setting\n- Avoid unnecessarily asking the user to confirm the changes they explicitly asked for, just do it\n" property string tool: "functions" // search, functions, or none + property list extraModels: [ + { + "api_format": "openai", // Most of the time you want "openai". Use "gemini" for Google's models + "description": "This is a custom model. Edit the config to add more! | Anyway, this is DeepSeek R1 Distill LLaMA 70B", + "endpoint": "https://openrouter.ai/api/v1/chat/completions", + "homepage": "https://openrouter.ai/deepseek/deepseek-r1-distill-llama-70b:free", + "icon": "spark-symbolic", + "key_get_link": "https://openrouter.ai/settings/keys", + "key_id": "openrouter", + "model": "deepseek/deepseek-r1-distill-llama-70b:free", + "name": "Custom: DS R1 Dstl. LLaMA 70B", + "requires_key": true + } + ] } property JsonObject appearance: JsonObject { diff --git a/.config/quickshell/ii/services/Ai.qml b/.config/quickshell/ii/services/Ai.qml index d6992850a..7b6013188 100644 --- a/.config/quickshell/ii/services/Ai.qml +++ b/.config/quickshell/ii/services/Ai.qml @@ -61,7 +61,7 @@ Singleton { } function safeModelName(modelName) { - return modelName.replace(/:/g, "_").replace(/\./g, "_") + return modelName.replace(/:/g, "_").replace(/\./g, "_").replace(/ /g, "-").replace(/\//g, "-") } property list defaultPrompts: [] @@ -206,6 +206,19 @@ Singleton { "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", }), + "gemini-2.5-flash-pro": aiModelComponent.createObject(this, { + "name": "Gemini 2.5 Pro", + "icon": "google-gemini-symbolic", + "description": Translation.tr("Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks."), + "homepage": "https://aistudio.google.com", + "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent", + "model": "gemini-2.5-pro", + "requires_key": true, + "key_id": "gemini", + "key_get_link": "https://aistudio.google.com/app/apikey", + "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), + "api_format": "gemini", + }), "gemini-2.5-flash-lite": aiModelComponent.createObject(this, { "name": "Gemini 2.5 Flash-Lite", "icon": "google-gemini-symbolic", @@ -241,6 +254,17 @@ Singleton { } property ApiStrategy currentApiStrategy: apiStrategies[models[currentModelId]?.api_format || "openai"] + Connections { + target: Config + function onReadyChanged() { + if (!Config.ready) return; + (Config?.options.ai?.extraModels ?? []).forEach(model => { + const safeModelName = root.safeModelName(model["model"]); + root.addModel(safeModelName, model) + }); + } + } + Component.onCompleted: { setModel(currentModelId, false, false); // Do necessary setup for model } @@ -266,6 +290,10 @@ Singleton { return result; } + function addModel(modelName, data) { + root.models[modelName] = aiModelComponent.createObject(this, data); + } + Process { id: getOllamaModels running: true @@ -278,7 +306,7 @@ Singleton { root.modelList = [...root.modelList, ...dataJson]; dataJson.forEach(model => { const safeModelName = root.safeModelName(model); - root.models[safeModelName] = aiModelComponent.createObject(this, { + root.addModel(safeModelName, { "name": guessModelName(model), "icon": guessModelLogo(model), "description": Translation.tr("Local Ollama model | %1").arg(model), From a11e0a39d94b2b8dd52abda906de7523ba49efa6 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:42:18 +0700 Subject: [PATCH 22/27] ai: adjust chat input indicator spacing --- .config/quickshell/ii/modules/common/Config.qml | 6 +++--- .config/quickshell/ii/modules/sidebarLeft/AiChat.qml | 4 ++-- .../ii/modules/sidebarLeft/ApiInputBoxIndicator.qml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index 780676622..f8e015cef 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -67,9 +67,9 @@ Singleton { "api_format": "openai", // Most of the time you want "openai". Use "gemini" for Google's models "description": "This is a custom model. Edit the config to add more! | Anyway, this is DeepSeek R1 Distill LLaMA 70B", "endpoint": "https://openrouter.ai/api/v1/chat/completions", - "homepage": "https://openrouter.ai/deepseek/deepseek-r1-distill-llama-70b:free", - "icon": "spark-symbolic", - "key_get_link": "https://openrouter.ai/settings/keys", + "homepage": "https://openrouter.ai/deepseek/deepseek-r1-distill-llama-70b:free", // Not mandatory + "icon": "spark-symbolic", // Not mandatory + "key_get_link": "https://openrouter.ai/settings/keys", // Not mandatory "key_id": "openrouter", "model": "deepseek/deepseek-r1-distill-llama-70b:free", "name": "Custom: DS R1 Dstl. LLaMA 70B", diff --git a/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml b/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml index 44f70633c..dcbff893e 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml @@ -635,9 +635,9 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) anchors.right: parent.right anchors.bottom: parent.bottom anchors.bottomMargin: 5 - anchors.leftMargin: 5 + anchors.leftMargin: 10 anchors.rightMargin: 5 - spacing: 5 + spacing: 4 property var commandsShown: [ { diff --git a/.config/quickshell/ii/modules/sidebarLeft/ApiInputBoxIndicator.qml b/.config/quickshell/ii/modules/sidebarLeft/ApiInputBoxIndicator.qml index 46bd4c783..13fb81ca9 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/ApiInputBoxIndicator.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/ApiInputBoxIndicator.qml @@ -9,8 +9,8 @@ Item { // Model indicator property string icon: "api" property string text: "" property string tooltipText: "" - implicitHeight: rowLayout.implicitHeight + 5 * 2 - implicitWidth: rowLayout.implicitWidth + 10 * 2 + implicitHeight: rowLayout.implicitHeight + 4 * 2 + implicitWidth: rowLayout.implicitWidth + 4 * 2 RowLayout { id: rowLayout From e504cf11e1f0103589a767e4daf64b00ca96543c Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:01:09 +0700 Subject: [PATCH 23/27] starship: fix trailing newline (#1738) --- .config/starship.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.config/starship.toml b/.config/starship.toml index 26aed07fa..2e3f7db96 100644 --- a/.config/starship.toml +++ b/.config/starship.toml @@ -12,8 +12,7 @@ add_newline = false # """ format = """ $cmd_duration 󰜥 $directory $git_branch -$character -""" +$character""" # Replace the "❯" symbol in the prompt with "➜" [character] # The name of the module we are configuring is "character" From aa20027de49525ce4e2ddd03f6e688d9b8899207 Mon Sep 17 00:00:00 2001 From: Vercixx <112702555+Vercixx@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:27:30 +0300 Subject: [PATCH 24/27] translation: Update Russian translation file --- .config/quickshell/translations/ru_RU.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.config/quickshell/translations/ru_RU.json b/.config/quickshell/translations/ru_RU.json index a14e2c1f1..be2036d9a 100644 --- a/.config/quickshell/translations/ru_RU.json +++ b/.config/quickshell/translations/ru_RU.json @@ -68,7 +68,7 @@ "There might be a download in progress": "Возможно происходит скачивание", "Visibility": "Видимость", "Desktop": "Рабочий стол", - "Run": "Запустиnm", + "Run": "Запустить", "Sunrise": "Рассвет", "Set API key": "Задать API ключ", "Shell windows": "Окна оболочки", @@ -331,5 +331,16 @@ "Experimental | Online | Google's model\nCan do a little more but doesn't search quickly": "Эксперементальная | Онлайн | Модель Google\nМожет немного больше но не ищет очень быстро", "Sa": "Сб/*keep*/", "Chain of Thought": "Цепочка мыслей", - "Fr": "Пт/*keep*/" + "Fr": "Пт/*keep*/", + "Usage: %1load CHAT_NAME": "Использование: %1load ИМЯ_ЧАТА", + "Tool set to %1": "Установлен инструмент %1", + "Set the tool to use for the model.": "Установите инструмент для этой модели.", + "Invalid tool. Supported tools:\n- %1": "Неправильный инструмент. Доступны:\n- %1", + "Usage: %1tool TOOL_NAME": "Использование: %1tool ИНСТРУМЕНТ", + "Performance Profile toggle": "Профили производительности", + "Usage: %1save CHAT_NAME": "Использование: %1save ИМЯ_ЧАТА", + "Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "Онлайн | Модель Google\nМодель Gemini 2.5 Flash оптимизирована под меньшие затраты и высокую производительнность", + "Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers": "Онлайн | Модель Google\nНовая модель, которая медленее, чем ее предшественник, но выдает лучшее качество", + "Online | Google's model\nFast, can perform searches for up-to-date information": "Онлайн | Модель Google\nБыстрая, может выполнять искать актуальную информацию через поиск", + "Current tool: %1\nSet it with %2tool TOOL": "Текущий инструмент: %1\nИзменяется с помощью %2tool ИНСТРУМЕНТ" } From c743b4ab8856c87639e1fa24f6c9745669ecf775 Mon Sep 17 00:00:00 2001 From: Vercixx <112702555+Vercixx@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:45:56 +0300 Subject: [PATCH 25/27] Update ru_RU.json --- .config/quickshell/translations/ru_RU.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/translations/ru_RU.json b/.config/quickshell/translations/ru_RU.json index be2036d9a..df1c0488f 100644 --- a/.config/quickshell/translations/ru_RU.json +++ b/.config/quickshell/translations/ru_RU.json @@ -341,6 +341,6 @@ "Usage: %1save CHAT_NAME": "Использование: %1save ИМЯ_ЧАТА", "Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "Онлайн | Модель Google\nМодель Gemini 2.5 Flash оптимизирована под меньшие затраты и высокую производительнность", "Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers": "Онлайн | Модель Google\nНовая модель, которая медленее, чем ее предшественник, но выдает лучшее качество", - "Online | Google's model\nFast, can perform searches for up-to-date information": "Онлайн | Модель Google\nБыстрая, может выполнять искать актуальную информацию через поиск", + "Online | Google's model\nFast, can perform searches for up-to-date information": "Онлайн | Модель Google\nБыстрая, может выполнять поиск актуальной информации", "Current tool: %1\nSet it with %2tool TOOL": "Текущий инструмент: %1\nИзменяется с помощью %2tool ИНСТРУМЕНТ" } From 91c2014b7e5260c4ddb2b32a9d1e59b99313134c Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:46:42 +0700 Subject: [PATCH 26/27] ai: add mistral --- .../ii/assets/icons/mistral-symbolic.svg | 95 ++++++++++++++ .../sidebarLeft/aiChat/MessageCodeBlock.qml | 2 +- .config/quickshell/ii/services/Ai.qml | 112 +++++++++++++--- .../ii/services/ai/AiMessageData.qml | 1 + .../ii/services/ai/MistralApiStrategy.qml | 124 ++++++++++++++++++ 5 files changed, 318 insertions(+), 16 deletions(-) create mode 100644 .config/quickshell/ii/assets/icons/mistral-symbolic.svg create mode 100644 .config/quickshell/ii/services/ai/MistralApiStrategy.qml diff --git a/.config/quickshell/ii/assets/icons/mistral-symbolic.svg b/.config/quickshell/ii/assets/icons/mistral-symbolic.svg new file mode 100644 index 000000000..635b91db1 --- /dev/null +++ b/.config/quickshell/ii/assets/icons/mistral-symbolic.svg @@ -0,0 +1,95 @@ + + diff --git a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml index f2b9a1bd6..f8b0bac3e 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml @@ -252,7 +252,7 @@ ColumnLayout { } } Loader { - active: root.isCommandRequest && root.messageData.thinking + active: root.isCommandRequest && root.messageData.functionPending visible: active Layout.fillWidth: true Layout.margins: 6 diff --git a/.config/quickshell/ii/services/Ai.qml b/.config/quickshell/ii/services/Ai.qml index 7b6013188..cff1bcbb2 100644 --- a/.config/quickshell/ii/services/Ai.qml +++ b/.config/quickshell/ii/services/Ai.qml @@ -23,6 +23,7 @@ Singleton { property Component aiModelComponent: AiModel {} property Component geminiApiStrategy: GeminiApiStrategy {} property Component openaiApiStrategy: OpenAiApiStrategy {} + property Component mistralApiStrategy: MistralApiStrategy {} readonly property string interfaceRole: "interface" readonly property string apiKeyEnvVarName: "API_KEY" @@ -72,7 +73,7 @@ Singleton { property var promptSubstitutions: { "{DISTRO}": SystemInfo.distroName, "{DATETIME}": `${DateTime.time}, ${DateTime.collapsedCalendarFormat}`, - "{WINDOWCLASS}": ToplevelManager.activeToplevel.appId, + "{WINDOWCLASS}": ToplevelManager.activeToplevel?.appId ?? "Unknown", "{DE}": `${SystemInfo.desktopEnvironment} (${SystemInfo.windowingSystem})` } @@ -131,12 +132,14 @@ Singleton { "openai": { "functions": [ { - "type": "function", - "name": "get_shell_config", - "description": "Get the current shell configuration.", + "name": "switch_to_search_mode", + "description": "Search the web", + }, + { + "name": "get_shell_config", + "description": "Get the desktop shell config file contents", }, { - "type": "function", "name": "set_shell_config", "description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.", "parameters": { @@ -151,10 +154,75 @@ Singleton { "description": "The value to set, e.g. `true`" } }, - "required": ["key", "value"], - "additionalProperties": false + "required": ["key", "value"] } - } + }, + { + "name": "run_shell_command", + "description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to run", + }, + }, + "required": ["command"] + } + }, + ], + "search": [], + "none": [], + }, + "mistral": { + "functions": [ + { + "type": "function", + "function": { + "name": "get_shell_config", + "description": "Get the desktop shell config file contents", + "parameters": {} + }, + }, + { + "type": "function", + "function": { + "name": "set_shell_config", + "description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.", + "parameters": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.", + }, + "value": { + "type": "string", + "description": "The value to set, e.g. `true`" + } + }, + "required": ["key", "value"] + } + } + }, + { + "type": "function", + "function": { + "name": "run_shell_command", + "description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The bash command to run", + }, + }, + "required": ["command"] + } + }, + }, ], "search": [], "none": [], @@ -232,6 +300,19 @@ Singleton { "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", }), + "mistral-medium-3": aiModelComponent.createObject(this, { + "name": "Mistral Medium 3", + "icon": "mistral-symbolic", + "description": Translation.tr("Online | %1's model | Delivers fast, responsive and well-formatted answers. Disadvantages: not very eager to do stuff; might make up unknown function calls").arg("Mistral"), + "homepage": "https://mistral.ai/news/mistral-medium-3", + "endpoint": "https://api.mistral.ai/v1/chat/completions", + "model": "mistral-medium-2505", + "requires_key": true, + "key_id": "mistral", + "key_get_link": "https://console.mistral.ai/api-keys", + "key_get_description": Translation.tr("**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key"), + "api_format": "mistral", + }), "openrouter-deepseek-r1": aiModelComponent.createObject(this, { "name": "DeepSeek R1", "icon": "deepseek-symbolic", @@ -251,6 +332,7 @@ Singleton { property var apiStrategies: { "openai": openaiApiStrategy.createObject(this), "gemini": geminiApiStrategy.createObject(this), + "mistral": mistralApiStrategy.createObject(this), } property ApiStrategy currentApiStrategy: apiStrategies[models[currentModelId]?.api_format || "openai"] @@ -412,8 +494,8 @@ Singleton { function addApiKeyAdvice(model) { root.addMessage( - Translation.tr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3') - .arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("No further instruction provided")), + Translation.tr('To set an API key, pass it with the %4 command\n\nTo view the key, pass "get" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3') + .arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("No further instruction provided")).arg("/key"), Ai.interfaceRole ); } @@ -659,14 +741,14 @@ Singleton { } function rejectCommand(message: AiMessageData) { - if (!message.thinking) return; - message.thinking = false; // User decided, no more "thinking" + if (!message.functionPending) return; + message.functionPending = false; // User decided, no more "thinking" addFunctionOutputMessage(message.functionName, Translation.tr("Command rejected by user")) } function approveCommand(message: AiMessageData) { - if (!message.thinking) return; - message.thinking = false; // User decided, no more "thinking" + if (!message.functionPending) return; + message.functionPending = false; // User decided, no more "thinking" const responseMessage = createFunctionOutputMessage(message.functionName, "", false); const id = idForMessage(responseMessage); @@ -726,7 +808,7 @@ Singleton { const contentToAppend = `\n\n**Command execution request**\n\n\`\`\`command\n${args.command}\n\`\`\``; message.rawContent += contentToAppend; message.content += contentToAppend; - message.thinking = true; // Use thinking to indicate the command is waiting for approval + message.functionPending = true; // Use thinking to indicate the command is waiting for approval } else root.addMessage(Translation.tr("Unknown function call: %1").arg(name), "assistant"); } diff --git a/.config/quickshell/ii/services/ai/AiMessageData.qml b/.config/quickshell/ii/services/ai/AiMessageData.qml index 5ec7fa336..023458d6e 100644 --- a/.config/quickshell/ii/services/ai/AiMessageData.qml +++ b/.config/quickshell/ii/services/ai/AiMessageData.qml @@ -16,5 +16,6 @@ QtObject { property string functionName property var functionCall property string functionResponse + property bool functionPending: false property bool visibleToUser: true } diff --git a/.config/quickshell/ii/services/ai/MistralApiStrategy.qml b/.config/quickshell/ii/services/ai/MistralApiStrategy.qml new file mode 100644 index 000000000..dfcb950eb --- /dev/null +++ b/.config/quickshell/ii/services/ai/MistralApiStrategy.qml @@ -0,0 +1,124 @@ +import QtQuick + +ApiStrategy { + property bool isReasoning: false + + function buildEndpoint(model: AiModel): string { + // console.log("[AI] Endpoint: " + model.endpoint); + return model.endpoint; + } + + function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list) { + let baseData = { + "model": model.model, + "messages": [ + {role: "system", content: systemPrompt}, + ...messages.map(message => { + const hasFunctionCall = message.functionCall != undefined && message.functionName.length > 0 + let messageData = { + "role": message.role, + "content": message.rawContent, + } + if (hasFunctionCall) { + if (message.functionResponse?.length > 0) { + messageData.name = message.functionName; // Does the func call also need this name? or just the func output? + messageData.role = "tool"; + messageData.content = message.functionResponse; + messageData.tool_call_id = message.functionCall.id + } + } + return messageData + }), + ], + "stream": true, + "temperature": temperature, + "tools": tools, + }; + // console.log("[AI] Request data: ", JSON.stringify(baseData, null, 2)); + return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData; + } + + function buildAuthorizationHeader(apiKeyEnvVarName: string): string { + return `-H "Authorization: Bearer \$\{${apiKeyEnvVarName}\}"`; + } + + function parseResponseLine(line, message) { + // Remove 'data: ' prefix if present and trim whitespace + let cleanData = line.trim(); + if (cleanData.startsWith("data:")) { + cleanData = cleanData.slice(5).trim(); + } + + // Handle special cases + if (!cleanData || cleanData.startsWith(":")) return {}; + if (cleanData === "[DONE]") { + return { finished: true }; + } + + // Real stuff + try { + const dataJson = JSON.parse(cleanData); + let newContent = ""; + + const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content; + const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content; + + // Function call + if (dataJson.choices[0]?.delta?.tool_calls) { + const functionCall = dataJson.choices[0].delta.tool_calls[0]; + const functionName = functionCall.function.name; + const functionArgs = JSON.parse(functionCall.function.arguments) || {}; // Args are given as string??? + const functionId = functionCall.id; + const newContent = `\n\n[[ Function: ${functionName}(${JSON.stringify(functionArgs, null, 2)}) ]]\n`; + message.rawContent += newContent; + message.content += newContent; + message.functionName = functionName; + message.functionCall = functionName; + return { functionCall: { name: functionName, args: functionArgs, id: functionId } }; + } + + // Thinking? + if (responseContent && responseContent.length > 0) { + if (isReasoning) { + isReasoning = false; + const endBlock = "\n\n\n\n"; + message.content += endBlock; + message.rawContent += endBlock; + } + newContent = responseContent; + } else if (responseReasoning && responseReasoning.length > 0) { + if (!isReasoning) { + isReasoning = true; + const startBlock = "\n\n\n\n"; + message.rawContent += startBlock; + message.content += startBlock; + } + newContent = responseReasoning; + } + + // Text + message.content += newContent; + message.rawContent += newContent; + + if (`dataJson`.done) { + return { finished: true }; + } + + } catch (e) { + console.log("[AI] Mistral: Could not parse line: ", e); + message.rawContent += line; + message.content += line; + } + + return {}; + } + + function onRequestFinished(message) { + return {}; + } + + function reset() { + isReasoning = false; + } + +} From 298e9477409f93c594226858040aa9be18a49114 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:46:55 +0700 Subject: [PATCH 27/27] background: hide when fullscreen --- .config/quickshell/ii/modules/background/Background.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.config/quickshell/ii/modules/background/Background.qml b/.config/quickshell/ii/modules/background/Background.qml index e34c0ebbd..4cc2ba022 100644 --- a/.config/quickshell/ii/modules/background/Background.qml +++ b/.config/quickshell/ii/modules/background/Background.qml @@ -25,6 +25,12 @@ Scope { id: bgRoot required property var modelData + + // Hide when fullscreen + readonly property Toplevel activeWindow: ToplevelManager.activeToplevel + property bool focusingThisMonitor: HyprlandData.activeWorkspace.monitor == monitor.name + visible: !(activeWindow?.fullscreen && activeWindow?.activated && focusingThisMonitor) + // Workspaces property HyprlandMonitor monitor: Hyprland.monitorFor(modelData) property list relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id)