🐛 | Fixed widget window size and settings tab formating.

This commit is contained in:
2026-01-26 17:42:14 +02:00
parent 8219e5c1a2
commit 55917b9d1a
4 changed files with 111 additions and 81 deletions

View File

@@ -3,7 +3,7 @@ import org.kde.plasma.configuration
ConfigModel { ConfigModel {
ConfigCategory { ConfigCategory {
name: i18n("General") name: "General"
icon: "configure" icon: "configure"
source: "configGeneral.qml" source: "configGeneral.qml"
} }

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" ?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"> <kcfg xmlns="http://www.kde.org/standards/kcfg/1.0">
<group name="General"> <group name="General">
<entry name="systemPrompt" type="String"> <entry name="systemPrompt" type="String">
<default>You are a helpful assistant. Your answers are compact.</default> <default>
You are a helpful assistant. Your answers are compact.
</default>
</entry> </entry>
</group> </group>
</kcfg> </kcfg>

View File

@@ -1,14 +1,26 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
Kirigami.FormLayout { Kirigami.Page {
id: page id: page
property alias cfg_systemPrompt: systemPrompt.text property alias cfg_systemPrompt: systemPrompt.text
QQC2.TextArea { contentItem: ColumnLayout {
id: systemPrompt Kirigami.Heading {
text: page.cfg_systemPrompt text: "System Prompt"
placeholderText: page.cfg_systemPrompt.default 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
}
} }
} }

View File

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