🎉 | Project released.
This commit is contained in:
10
contents/config/config.qml
Normal file
10
contents/config/config.qml
Normal file
@@ -0,0 +1,10 @@
|
||||
import QtQuick
|
||||
import org.kde.plasma.configuration
|
||||
|
||||
ConfigModel {
|
||||
ConfigCategory {
|
||||
name: i18n("General")
|
||||
icon: "configure"
|
||||
source: "configGeneral.qml"
|
||||
}
|
||||
}
|
||||
8
contents/config/main.xml
Normal file
8
contents/config/main.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0">
|
||||
<group name="General">
|
||||
<entry name="systemPrompt" type="String">
|
||||
<default>You are a helpful assistant. Your answers are compact.</default>
|
||||
</entry>
|
||||
</group>
|
||||
</kcfg>
|
||||
14
contents/ui/configGeneral.qml
Normal file
14
contents/ui/configGeneral.qml
Normal file
@@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Kirigami.FormLayout {
|
||||
id: page
|
||||
property alias cfg_systemPrompt: systemPrompt.text
|
||||
|
||||
QQC2.TextArea {
|
||||
id: systemPrompt
|
||||
text: page.cfg_systemPrompt
|
||||
placeholderText: page.cfg_systemPrompt.default
|
||||
}
|
||||
}
|
||||
307
contents/ui/main.qml
Normal file
307
contents/ui/main.qml
Normal file
@@ -0,0 +1,307 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
import org.kde.plasma.plasmoid
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.kirigami as Kirigami
|
||||
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()
|
||||
}
|
||||
}
|
||||
property string ollamaUrl: "http://localhost:11434"
|
||||
property var models: []
|
||||
property string currentModel: ""
|
||||
property string selectedImagePath: ""
|
||||
property string pendingMessageText: ""
|
||||
property var chatHistory: []
|
||||
property string chatText: ""
|
||||
property bool isWaiting: false // New: Loading state
|
||||
|
||||
function clearChat() {
|
||||
chatHistory = []
|
||||
chatText = ""
|
||||
selectedImagePath = ""
|
||||
isWaiting = false
|
||||
appendMessage("system", system_prompt)
|
||||
}
|
||||
|
||||
function appendMessage(role, content, imageSource) {
|
||||
var msgObj = { role: role, content: content }
|
||||
chatHistory.push(msgObj)
|
||||
|
||||
let userColor = Kirigami.Theme.highlightColor
|
||||
let assistantColor = Kirigami.Theme.positiveTextColor
|
||||
let systemColor = Kirigami.Theme.disabledTextColor
|
||||
|
||||
if (chatText !== "") {
|
||||
chatText += "<br>"
|
||||
}
|
||||
|
||||
if (role === "user") {
|
||||
chatText += `<b style="color:${userColor}">You:</b> ${content}`
|
||||
if (imageSource) {
|
||||
chatText += `<br><img src="${imageSource}" width="150" style="border-radius:4px;">`
|
||||
}
|
||||
} else if (role === "assistant") {
|
||||
chatText += `<b style="color:${assistantColor}">Ollama:</b> ${content}`
|
||||
} else if (role === "system") {
|
||||
chatText += `<i style="color:${systemColor}">System: ${content}</i>`
|
||||
}
|
||||
}
|
||||
|
||||
function fetchModels() {
|
||||
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 = []
|
||||
if (res.models) {
|
||||
res.models.forEach(m => list.push(m.name))
|
||||
models = list
|
||||
if (list.length > 0 && currentModel === "") currentModel = list[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
Plasma5Support.DataSource {
|
||||
id: executableSource
|
||||
engine: "executable"
|
||||
connectedSources: []
|
||||
onNewData: (sourceName, data) => {
|
||||
if (data["stdout"]) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage(msg, base64Image) {
|
||||
if (chatHistory.length === 0) {
|
||||
appendMessage("system", system_prompt)
|
||||
}
|
||||
|
||||
// 1. Add to local UI history (this handles the visual part)
|
||||
appendMessage("user", msg, base64Image ? selectedImagePath : null)
|
||||
|
||||
// Reset image selection immediately after UI update
|
||||
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 }))
|
||||
|
||||
// 3. IMPORTANT: Add the image to the LAST message in the request list
|
||||
if (base64Image && requestMessages.length > 0) {
|
||||
requestMessages[requestMessages.length - 1].images = [base64Image]
|
||||
}
|
||||
|
||||
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
|
||||
if (xhr.status === 200) {
|
||||
let res = JSON.parse(xhr.responseText)
|
||||
appendMessage("assistant", res.message.content)
|
||||
} else {
|
||||
chatText += `<br><font color="orange">Error: Check if Ollama is running.</font>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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...
|
||||
Timer {
|
||||
id: initTimer
|
||||
interval: 50
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (root.chatHistory.length === 0) {
|
||||
root.appendMessage("system", root.system_prompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
fetchModels()
|
||||
initTimer.start()
|
||||
plasmoid.configurationRequired = true
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
title: "Select an Image"
|
||||
nameFilters: [ "Images (*.png *.jpg *.jpeg *.webp)" ]
|
||||
onAccepted: root.selectedImagePath = selectedFile
|
||||
}
|
||||
|
||||
fullRepresentation: ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 8
|
||||
|
||||
// New Title Bar
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Kirigami.Heading {
|
||||
text: "AI Chat"
|
||||
level: 2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
PlasmaComponents.ComboBox {
|
||||
id: modelCombo
|
||||
Layout.fillWidth: true
|
||||
model: root.models
|
||||
onActivated: root.currentModel = currentText
|
||||
}
|
||||
PlasmaComponents.Button {
|
||||
icon.name: "view-refresh"; onClicked: root.fetchModels()
|
||||
}
|
||||
PlasmaComponents.Button {
|
||||
icon.name: "edit-clear-all"; onClicked: root.clearChat()
|
||||
}
|
||||
|
||||
PlasmaComponents.Button {
|
||||
id: pinButton
|
||||
icon.name: root.hideOnWindowDeactivate ? "window-pin" : "window-unpin"
|
||||
checkable: true
|
||||
checked: !root.hideOnWindowDeactivate
|
||||
onClicked: root.hideOnWindowDeactivate = !checked
|
||||
|
||||
// Tooltip to explain the button
|
||||
PlasmaComponents.ToolTip { text: "Keep Open" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal Line separator
|
||||
Kirigami.Separator { Layout.fillWidth: true }
|
||||
|
||||
// Chat View
|
||||
PlasmaComponents.ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
PlasmaComponents.TextArea {
|
||||
id: chatArea
|
||||
text: root.chatText
|
||||
readOnly: true
|
||||
textFormat: TextEdit.RichText
|
||||
wrapMode: TextEdit.WordWrap
|
||||
onTextChanged: {
|
||||
// Force scroll to bottom
|
||||
cursorPosition = length
|
||||
}
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
opacity: 0.3
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading Indicator
|
||||
PlasmaComponents.ProgressBar {
|
||||
Layout.fillWidth: true
|
||||
indeterminate: true
|
||||
visible: root.isWaiting
|
||||
}
|
||||
|
||||
// Image Preview
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 80
|
||||
visible: root.selectedImagePath !== ""
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
opacity: 0.8
|
||||
radius: 4
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
Image {
|
||||
source: root.selectedImagePath
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 70
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
PlasmaComponents.Label {
|
||||
text: "Image attached"; Layout.fillWidth: true
|
||||
}
|
||||
PlasmaComponents.Button {
|
||||
icon.name: "edit-delete"
|
||||
onClicked: root.selectedImagePath = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input Area
|
||||
RowLayout {
|
||||
PlasmaComponents.Button {
|
||||
icon.name: "mail-attachment"
|
||||
onClicked: fileDialog.open()
|
||||
enabled: !root.isWaiting
|
||||
}
|
||||
PlasmaComponents.TextField {
|
||||
id: inputField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Type message..."
|
||||
enabled: !root.isWaiting
|
||||
onAccepted: {
|
||||
root.processAndSendMessage(text)
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
PlasmaComponents.Button {
|
||||
icon.name: "mail-send"
|
||||
enabled: !root.isWaiting && inputField.text !== ""
|
||||
onClicked: {
|
||||
root.processAndSendMessage(inputField.text)
|
||||
inputField.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user