diff --git a/contents/config/config.qml b/contents/config/config.qml index 8503c30..01fa2d3 100644 --- a/contents/config/config.qml +++ b/contents/config/config.qml @@ -3,7 +3,7 @@ import org.kde.plasma.configuration ConfigModel { ConfigCategory { - name: i18n("General") + name: "General" icon: "configure" source: "configGeneral.qml" } diff --git a/contents/config/main.xml b/contents/config/main.xml index cb1793a..f06100d 100644 --- a/contents/config/main.xml +++ b/contents/config/main.xml @@ -1,8 +1,10 @@ - + - You are a helpful assistant. Your answers are compact. + + You are a helpful assistant. Your answers are compact. + diff --git a/contents/ui/configGeneral.qml b/contents/ui/configGeneral.qml index 75038e0..86702c3 100644 --- a/contents/ui/configGeneral.qml +++ b/contents/ui/configGeneral.qml @@ -1,14 +1,26 @@ import QtQuick +import QtQuick.Layouts import QtQuick.Controls as QQC2 import org.kde.kirigami as Kirigami -Kirigami.FormLayout { +Kirigami.Page { id: page property alias cfg_systemPrompt: systemPrompt.text - QQC2.TextArea { - id: systemPrompt - text: page.cfg_systemPrompt - placeholderText: page.cfg_systemPrompt.default + contentItem: ColumnLayout { + Kirigami.Heading { + text: "System Prompt" + level: 2 + Layout.fillWidth: true + } + + QQC2.TextArea { + id: systemPrompt + placeholderText: "Enter the system prompt here..." + Layout.fillWidth: true + Layout.preferredHeight: 240 + wrapMode: TextEdit.Wrap + clip: true + } } } diff --git a/contents/ui/main.qml b/contents/ui/main.qml index aa4b97e..4a90270 100644 --- a/contents/ui/main.qml +++ b/contents/ui/main.qml @@ -9,18 +9,17 @@ import org.kde.plasma.plasma5support as Plasma5Support PlasmoidItem { id: root - width: 500 - height: 750 property string system_prompt: plasmoid.configuration.systemPrompt Connections { target: plasmoid.configuration function onSystemPromptChanged() { - root.system_prompt = plasmoid.configuration.systemPrompt - root.clearChat() + root.system_prompt = plasmoid.configuration.systemPrompt; + root.clearChat(); } } + property string ollamaUrl: "http://localhost:11434" property var models: [] property string currentModel: "" @@ -28,55 +27,59 @@ PlasmoidItem { property string pendingMessageText: "" property var chatHistory: [] property string chatText: "" - property bool isWaiting: false // New: Loading state + property bool isWaiting: false function clearChat() { - chatHistory = [] - chatText = "" - selectedImagePath = "" - isWaiting = false - appendMessage("system", system_prompt) + chatHistory = []; + chatText = ""; + selectedImagePath = ""; + isWaiting = false; + appendMessage("system", system_prompt); } function appendMessage(role, content, imageSource) { - var msgObj = { role: role, content: content } - chatHistory.push(msgObj) + var msgObj = { + role: role, + content: content + }; + chatHistory.push(msgObj); - let userColor = Kirigami.Theme.highlightColor - let assistantColor = Kirigami.Theme.positiveTextColor - let systemColor = Kirigami.Theme.disabledTextColor + let userColor = Kirigami.Theme.highlightColor; + let assistantColor = Kirigami.Theme.positiveTextColor; + let systemColor = Kirigami.Theme.disabledTextColor; if (chatText !== "") { - chatText += "
" + chatText += "
"; } if (role === "user") { - chatText += `You: ${content}` + chatText += `You: ${content}`; if (imageSource) { - chatText += `
` + chatText += `
`; } } else if (role === "assistant") { - chatText += `Ollama: ${content}` + chatText += `Ollama: ${content}`; } else if (role === "system") { - chatText += `System: ${content}` + chatText += `System: ${content}`; } } function fetchModels() { - let xhr = new XMLHttpRequest() - xhr.open("GET", ollamaUrl + "/api/tags") + let xhr = new XMLHttpRequest(); + xhr.open("GET", ollamaUrl + "/api/tags"); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { - let res = JSON.parse(xhr.responseText) - let list = [] + let res = JSON.parse(xhr.responseText); + let list = []; if (res.models) { - res.models.forEach(m => list.push(m.name)) - models = list - if (list.length > 0 && currentModel === "") currentModel = list[0] + res.models.forEach(m => list.push(m.name)); + models = list; + if (list.length > 0 && currentModel === "") + currentModel = list[0]; } } - } - xhr.send() + }; + xhr.send(); } Plasma5Support.DataSource { @@ -85,68 +88,71 @@ PlasmoidItem { connectedSources: [] onNewData: (sourceName, data) => { if (data["stdout"]) { - var base64Str = data["stdout"].toString().trim() - root.sendMessage(root.pendingMessageText, base64Str) - disconnectSource(sourceName) + var base64Str = data["stdout"].toString().trim(); + root.sendMessage(root.pendingMessageText, base64Str); + disconnectSource(sourceName); } } } function processAndSendMessage(msg) { - if (!currentModel || !msg || isWaiting) return - - isWaiting = true - if (selectedImagePath !== "") { - root.pendingMessageText = msg - // Robust path cleaning for Linux - let cleanPath = decodeURIComponent(selectedImagePath.toString().replace("file://", "")) - executableSource.connectSource("base64 -w 0 \"" + cleanPath + "\"") - } else { - sendMessage(msg, null) - } + if (!currentModel || !msg || isWaiting) + return; + isWaiting = true; + if (selectedImagePath !== "") { + root.pendingMessageText = msg; + // Robust path cleaning for Linux + let cleanPath = decodeURIComponent(selectedImagePath.toString().replace("file://", "")); + executableSource.connectSource("base64 -w 0 \"" + cleanPath + "\""); + } else { + sendMessage(msg, null); + } } function sendMessage(msg, base64Image) { if (chatHistory.length === 0) { - appendMessage("system", system_prompt) + appendMessage("system", system_prompt); } // 1. Add to local UI history (this handles the visual part) - appendMessage("user", msg, base64Image ? selectedImagePath : null) + appendMessage("user", msg, base64Image ? selectedImagePath : null); // Reset image selection immediately after UI update - selectedImagePath = "" + selectedImagePath = ""; // 2. Prepare the messages for the API call // We map the existing history first - let requestMessages = chatHistory.map(m => ({ role: m.role, content: m.content })) + let requestMessages = chatHistory.map(m => ({ + role: m.role, + content: m.content + })); // 3. IMPORTANT: Add the image to the LAST message in the request list if (base64Image && requestMessages.length > 0) { - requestMessages[requestMessages.length - 1].images = [base64Image] + requestMessages[requestMessages.length - 1].images = [base64Image]; } - let xhr = new XMLHttpRequest() - xhr.open("POST", ollamaUrl + "/api/chat") - xhr.setRequestHeader("Content-Type", "application/json") + let xhr = new XMLHttpRequest(); + xhr.open("POST", ollamaUrl + "/api/chat"); + xhr.setRequestHeader("Content-Type", "application/json"); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { - isWaiting = false + isWaiting = false; if (xhr.status === 200) { - let res = JSON.parse(xhr.responseText) - appendMessage("assistant", res.message.content) + let res = JSON.parse(xhr.responseText); + appendMessage("assistant", res.message.content); } else { - chatText += `
Error: Check if Ollama is running.` + chatText += `
Error: Check if Ollama is running.`; } } - } + }; // Now requestMessages contains the image data in the last 'user' object xhr.send(JSON.stringify({ model: currentModel, messages: requestMessages, stream: false - })) + })); } // Hacky workaround the Kirigami theme loading bug... @@ -157,29 +163,32 @@ PlasmoidItem { repeat: false onTriggered: { if (root.chatHistory.length === 0) { - root.appendMessage("system", root.system_prompt) + root.appendMessage("system", root.system_prompt); } } } Component.onCompleted: { - fetchModels() - initTimer.start() - plasmoid.configurationRequired = true + fetchModels(); + initTimer.start(); + plasmoid.configurationRequired = true; } FileDialog { id: fileDialog title: "Select an Image" - nameFilters: [ "Images (*.png *.jpg *.jpeg *.webp)" ] + nameFilters: ["Images (*.png *.jpg *.jpeg *.webp)"] onAccepted: root.selectedImagePath = selectedFile } fullRepresentation: ColumnLayout { anchors.fill: parent - anchors.margins: 10 + anchors.margins: 8 spacing: 8 + Layout.minimumWidth: Math.max(Screen.width * .20, 400) + Layout.minimumHeight: Math.max(Screen.height * .20, 200) + // New Title Bar RowLayout { Layout.fillWidth: true @@ -198,10 +207,12 @@ PlasmoidItem { onActivated: root.currentModel = currentText } PlasmaComponents.Button { - icon.name: "view-refresh"; onClicked: root.fetchModels() + icon.name: "view-refresh" + onClicked: root.fetchModels() } PlasmaComponents.Button { - icon.name: "edit-clear-all"; onClicked: root.clearChat() + icon.name: "edit-clear-all" + onClicked: root.clearChat() } PlasmaComponents.Button { @@ -212,13 +223,17 @@ PlasmoidItem { onClicked: root.hideOnWindowDeactivate = !checked // Tooltip to explain the button - PlasmaComponents.ToolTip { text: "Keep Open" } + PlasmaComponents.ToolTip { + text: "Keep Open" + } } } } // Horizontal Line separator - Kirigami.Separator { Layout.fillWidth: true } + Kirigami.Separator { + Layout.fillWidth: true + } // Chat View PlasmaComponents.ScrollView { @@ -233,7 +248,7 @@ PlasmoidItem { wrapMode: TextEdit.WordWrap onTextChanged: { // Force scroll to bottom - cursorPosition = length + cursorPosition = length; } background: Rectangle { color: Kirigami.Theme.backgroundColor @@ -268,7 +283,8 @@ PlasmoidItem { fillMode: Image.PreserveAspectFit } PlasmaComponents.Label { - text: "Image attached"; Layout.fillWidth: true + text: "Image attached" + Layout.fillWidth: true } PlasmaComponents.Button { icon.name: "edit-delete" @@ -290,16 +306,16 @@ PlasmoidItem { placeholderText: "Type message..." enabled: !root.isWaiting onAccepted: { - root.processAndSendMessage(text) - text = "" + root.processAndSendMessage(text); + text = ""; } } PlasmaComponents.Button { icon.name: "mail-send" enabled: !root.isWaiting && inputField.text !== "" onClicked: { - root.processAndSendMessage(inputField.text) - inputField.text = "" + root.processAndSendMessage(inputField.text); + inputField.text = ""; } } }