🐛 | Fixed widget window size and settings tab formating.
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
Kirigami.Heading {
|
||||||
|
text: "System Prompt"
|
||||||
|
level: 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
QQC2.TextArea {
|
QQC2.TextArea {
|
||||||
id: systemPrompt
|
id: systemPrompt
|
||||||
text: page.cfg_systemPrompt
|
placeholderText: "Enter the system prompt here..."
|
||||||
placeholderText: page.cfg_systemPrompt.default
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 240
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
clip: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user