mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
577 lines
20 KiB
QML
577 lines
20 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import QtQuick.Window
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.config
|
|
import qs.modules.functions
|
|
import qs.modules.components
|
|
import qs.services
|
|
|
|
FloatingWindow {
|
|
id: appWin
|
|
color: Appearance.m3colors.m3background
|
|
property bool initialChatSelected: false
|
|
property bool chatsInitialized: false
|
|
|
|
function appendMessage(sender, message) {
|
|
messageModel.append({
|
|
"sender": sender,
|
|
"message": message
|
|
});
|
|
scrollToBottom();
|
|
}
|
|
|
|
function updateChatsList(files) {
|
|
let existing = {
|
|
};
|
|
for (let i = 0; i < chatListModel.count; i++) existing[chatListModel.get(i).name] = true
|
|
for (let file of files) {
|
|
let name = file.trim();
|
|
if (!name.length)
|
|
continue;
|
|
|
|
if (name.endsWith(".txt"))
|
|
name = name.slice(0, -4);
|
|
|
|
if (!existing[name])
|
|
chatListModel.append({
|
|
"name": name
|
|
});
|
|
|
|
delete existing[name];
|
|
}
|
|
// remove chats that no longer exist
|
|
for (let name in existing) {
|
|
for (let i = 0; i < chatListModel.count; i++) {
|
|
if (chatListModel.get(i).name === name) {
|
|
chatListModel.remove(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// ensure default exists
|
|
let hasDefault = false;
|
|
for (let i = 0; i < chatListModel.count; i++) if (chatListModel.get(i).name === "default") {
|
|
hasDefault = true;
|
|
}
|
|
if (!hasDefault) {
|
|
chatListModel.insert(0, {
|
|
"name": "default"
|
|
});
|
|
FileUtils.createFile(FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/default.txt");
|
|
}
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
// Always scroll to end after appending
|
|
chatView.forceLayout();
|
|
chatView.positionViewAtEnd();
|
|
}
|
|
|
|
function sendMessage() {
|
|
if (userInput.text === "" || Zenith.loading)
|
|
return ;
|
|
|
|
Zenith.pendingInput = userInput.text;
|
|
appendMessage("You", userInput.text);
|
|
userInput.text = "";
|
|
Zenith.loading = true;
|
|
Zenith.send();
|
|
}
|
|
|
|
function loadChatHistory(chatName) {
|
|
messageModel.clear();
|
|
Zenith.loadChat(chatName);
|
|
}
|
|
|
|
function selectDefaultChat() {
|
|
let defaultIndex = -1;
|
|
for (let i = 0; i < chatListModel.count; i++) {
|
|
if (chatListModel.get(i).name === "default") {
|
|
defaultIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (defaultIndex !== -1) {
|
|
chatSelector.currentIndex = defaultIndex;
|
|
Zenith.currentChat = "default";
|
|
loadChatHistory("default");
|
|
} else if (chatListModel.count > 0) {
|
|
chatSelector.currentIndex = 0;
|
|
Zenith.currentChat = chatListModel.get(0).name;
|
|
loadChatHistory(Zenith.currentChat);
|
|
}
|
|
}
|
|
|
|
visible: Globals.states.intelligenceWindowOpen
|
|
|
|
onVisibleChanged: {
|
|
if (!visible)
|
|
return ;
|
|
|
|
chatsInitialized = false;
|
|
messageModel.clear();
|
|
}
|
|
|
|
IpcHandler {
|
|
function openWindow() {
|
|
Globals.states.intelligenceWindowOpen = true;
|
|
}
|
|
|
|
function closeWindow() {
|
|
Globals.states.intelligenceWindowOpen = false;
|
|
}
|
|
|
|
target: "intelligence"
|
|
}
|
|
|
|
ListModel {
|
|
// { sender: "You" | "AI", message: string }
|
|
|
|
id: messageModel
|
|
}
|
|
|
|
ListModel {
|
|
id: chatListModel
|
|
}
|
|
|
|
ColumnLayout {
|
|
spacing: Metrics.spacing(8)
|
|
anchors.centerIn: parent
|
|
|
|
StyledText {
|
|
visible: !Config.runtime.misc.intelligence.enabled
|
|
text: "Intelligence is disabled!"
|
|
Layout.leftMargin: Metrics.margin(24)
|
|
font.pixelSize: Metrics.fontSize("huge")
|
|
}
|
|
|
|
StyledText {
|
|
visible: !Config.runtime.misc.intelligence.enabled
|
|
text: "Go to the settings to enable intelligence"
|
|
}
|
|
|
|
}
|
|
|
|
StyledRect {
|
|
anchors.fill: parent
|
|
color: "transparent"
|
|
visible: Config.runtime.misc.intelligence.enabled
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Metrics.margin(16)
|
|
spacing: Metrics.spacing(10)
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Metrics.spacing(10)
|
|
|
|
StyledDropDown {
|
|
id: chatSelector
|
|
|
|
Layout.fillWidth: true
|
|
model: chatListModel
|
|
textRole: "name"
|
|
Layout.preferredHeight: 40
|
|
onCurrentIndexChanged: {
|
|
if (currentIndex < 0)
|
|
return ;
|
|
|
|
let name = chatListModel.get(currentIndex).name;
|
|
if (name === Zenith.currentChat)
|
|
return ;
|
|
|
|
Zenith.currentChat = name;
|
|
loadChatHistory(name);
|
|
}
|
|
}
|
|
|
|
StyledButton {
|
|
icon: "add"
|
|
Layout.preferredWidth: 40
|
|
onClicked: {
|
|
let name = "new-chat-" + chatListModel.count;
|
|
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
|
FileUtils.createFile(path, function(success) {
|
|
if (success) {
|
|
chatListModel.append({
|
|
"name": name
|
|
});
|
|
chatSelector.currentIndex = chatListModel.count - 1;
|
|
Zenith.currentChat = name;
|
|
messageModel.clear();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
StyledButton {
|
|
icon: "edit"
|
|
Layout.preferredWidth: 40
|
|
enabled: chatSelector.currentIndex >= 0
|
|
onClicked: renameDialog.open()
|
|
}
|
|
|
|
StyledButton {
|
|
icon: "delete"
|
|
Layout.preferredWidth: 40
|
|
enabled: chatSelector.currentIndex >= 0 && chatSelector.currentText !== "default"
|
|
onClicked: {
|
|
let name = chatSelector.currentText;
|
|
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
|
FileUtils.removeFile(path, function(success) {
|
|
if (success) {
|
|
chatListModel.remove(chatSelector.currentIndex);
|
|
selectDefaultChat();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Metrics.spacing(10)
|
|
|
|
StyledDropDown {
|
|
id: modelSelector
|
|
|
|
Layout.fillWidth: true
|
|
model: ["openai/gpt-4o","openai/gpt-4","openai/gpt-3.5-turbo","openai/gpt-4o-mini","anthropic/claude-3.5-sonnet","anthropic/claude-3-haiku","meta-llama/llama-3.3-70b-instruct:free","deepseek/deepseek-r1-0528:free","qwen/qwen3-coder:free"]
|
|
currentIndex: 0
|
|
Layout.preferredHeight: 40
|
|
onCurrentTextChanged: Zenith.currentModel = currentText
|
|
}
|
|
|
|
StyledButton {
|
|
icon: "close_fullscreen"
|
|
Layout.preferredWidth: 40
|
|
onClicked: {
|
|
Quickshell.execDetached(["nucleus", "ipc", "call", "intelligence", "closeWindow"]);
|
|
Globals.visiblility.sidebarLeft = false;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
StyledRect {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
radius: Metrics.radius("normal")
|
|
color: Appearance.m3colors.m3surfaceContainerLow
|
|
|
|
ScrollView {
|
|
anchors.fill: parent
|
|
clip: true
|
|
|
|
ListView {
|
|
id: chatView
|
|
|
|
model: messageModel
|
|
spacing: Metrics.spacing(8)
|
|
anchors.fill: parent
|
|
anchors.margins: Metrics.margin(12)
|
|
clip: true
|
|
|
|
delegate: Item {
|
|
property bool isCodeBlock: message.split("\n").length > 2 && message.includes("import ") // simple heuristic
|
|
|
|
width: chatView.width
|
|
height: bubble.implicitHeight + 6
|
|
Component.onCompleted: {
|
|
chatView.forceLayout();
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Metrics.spacing(8)
|
|
|
|
Item {
|
|
width: sender === "AI" ? 0 : parent.width * 0.2
|
|
}
|
|
|
|
StyledRect {
|
|
id: bubble
|
|
|
|
radius: Metrics.radius("normal")
|
|
color: sender === "You" ? Appearance.m3colors.m3primaryContainer : Appearance.m3colors.m3surfaceContainerHigh
|
|
implicitWidth: Math.min(textItem.implicitWidth + 20, chatView.width * 0.8)
|
|
implicitHeight: textItem.implicitHeight
|
|
anchors.right: sender === "You" ? parent.right : undefined
|
|
anchors.left: sender === "AI" ? parent.left : undefined
|
|
anchors.topMargin: Metrics.margin(2)
|
|
|
|
TextEdit {
|
|
id: textItem
|
|
|
|
text: StringUtils.markdownToHtml(message)
|
|
wrapMode: TextEdit.Wrap
|
|
textFormat: TextEdit.RichText
|
|
readOnly: true // make it selectable but not editable
|
|
font.pixelSize: Metrics.fontSize(16)
|
|
color: Appearance.syntaxHighlightingTheme
|
|
padding: Metrics.padding(8)
|
|
anchors.fill: parent
|
|
}
|
|
|
|
MouseArea {
|
|
id: ma
|
|
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.RightButton
|
|
onClicked: {
|
|
let p = Qt.createQmlObject('import Quickshell; import Quickshell.Io; Process { command: ["wl-copy", "' + message + '"] }', parent);
|
|
p.running = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
Item {
|
|
width: sender === "You" ? 0 : parent.width * 0.2
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
StyledRect {
|
|
Layout.fillWidth: true
|
|
height: 50
|
|
radius: Metrics.radius("normal")
|
|
color: Appearance.m3colors.m3surfaceContainer
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Metrics.margin(6)
|
|
spacing: Metrics.spacing(10)
|
|
|
|
StyledTextField {
|
|
// Shift+Enter → insert newline
|
|
// Enter → send message
|
|
|
|
id: userInput
|
|
|
|
Layout.fillWidth: true
|
|
placeholderText: "Type your message..."
|
|
font.pixelSize: Metrics.iconSize(14)
|
|
padding: Metrics.spacing(8)
|
|
Keys.onPressed: {
|
|
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
if (event.modifiers & Qt.ShiftModifier)
|
|
insert("\n");
|
|
else
|
|
sendMessage();
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledButton {
|
|
text: "Send"
|
|
enabled: userInput.text.trim().length > 0 && !Zenith.loading
|
|
opacity: enabled ? 1 : 0.5
|
|
onClicked: sendMessage()
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Dialog {
|
|
id: renameDialog
|
|
|
|
title: "Rename Chat"
|
|
modal: true
|
|
visible: false
|
|
standardButtons: Dialog.NoButton
|
|
x: (appWin.width - 360) / 2 // center horizontally
|
|
y: (appWin.height - 160) / 2 // center vertically
|
|
width: 360
|
|
height: 200
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Metrics.margin(16)
|
|
spacing: Metrics.spacing(12)
|
|
|
|
StyledText {
|
|
text: "Enter a new name for the chat"
|
|
font.pixelSize: Metrics.fontSize(18)
|
|
horizontalAlignment: Text.AlignHCenter
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
StyledTextField {
|
|
id: renameInput
|
|
|
|
Layout.fillWidth: true
|
|
placeholderText: "New name"
|
|
filled: false
|
|
highlight: false
|
|
text: chatSelector.currentText
|
|
font.pixelSize: Metrics.fontSize(16)
|
|
Layout.preferredHeight: 45
|
|
padding: Metrics.padding(8)
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Metrics.spacing(12)
|
|
Layout.alignment: Qt.AlignRight
|
|
|
|
StyledButton {
|
|
text: "Cancel"
|
|
Layout.preferredWidth: 80
|
|
onClicked: renameDialog.close()
|
|
}
|
|
|
|
StyledButton {
|
|
text: "Rename"
|
|
Layout.preferredWidth: 100
|
|
enabled: renameInput.text.trim().length > 0 && renameInput.text !== chatSelector.currentText
|
|
onClicked: {
|
|
let oldName = chatSelector.currentText;
|
|
let newName = renameInput.text.trim();
|
|
let oldPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + oldName + ".txt";
|
|
let newPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + newName + ".txt";
|
|
FileUtils.renameFile(oldPath, newPath, function(success) {
|
|
if (success) {
|
|
chatListModel.set(chatSelector.currentIndex, {
|
|
"name": newName
|
|
});
|
|
Zenith.currentChat = newName;
|
|
renameDialog.close();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
background: StyledRect {
|
|
color: Appearance.m3colors.m3surfaceContainer
|
|
radius: Metrics.radius("normal")
|
|
border.color: Appearance.colors.colOutline
|
|
border.width: 1
|
|
}
|
|
|
|
header: StyledRect {
|
|
color: Appearance.m3colors.m3surfaceContainer
|
|
radius: Metrics.radius("normal")
|
|
border.color: Appearance.colors.colOutline
|
|
border.width: 1
|
|
}
|
|
|
|
}
|
|
|
|
StyledText {
|
|
text: "Thinking…"
|
|
visible: Zenith.loading
|
|
color: Appearance.colors.colSubtext
|
|
font.pixelSize: Metrics.fontSize(14)
|
|
|
|
anchors {
|
|
left: parent.left
|
|
bottom: parent.bottom
|
|
leftMargin: Metrics.margin(22)
|
|
bottomMargin: Metrics.margin(76)
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Connections {
|
|
// only auto-select once
|
|
function onChatsListed(text) {
|
|
let lines = text.split(/\r?\n/);
|
|
let previousChat = Zenith.currentChat;
|
|
updateChatsList(lines);
|
|
// select & load once
|
|
if (!chatsInitialized) {
|
|
chatsInitialized = true;
|
|
let index = -1;
|
|
for (let i = 0; i < chatListModel.count; i++) {
|
|
if (chatListModel.get(i).name === previousChat) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
if (index === -1 && chatListModel.count > 0)
|
|
index = 0;
|
|
|
|
if (index !== -1) {
|
|
chatSelector.currentIndex = index;
|
|
Zenith.currentChat = chatListModel.get(index).name;
|
|
loadChatHistory(Zenith.currentChat);
|
|
}
|
|
return ;
|
|
}
|
|
// AFTER init: only react if current chat vanished
|
|
let stillExists = false;
|
|
for (let i = 0; i < chatListModel.count; i++) {
|
|
if (chatListModel.get(i).name === Zenith.currentChat) {
|
|
stillExists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!stillExists && chatListModel.count > 0) {
|
|
chatSelector.currentIndex = 0;
|
|
Zenith.currentChat = chatListModel.get(0).name;
|
|
loadChatHistory(Zenith.currentChat);
|
|
}
|
|
}
|
|
|
|
function onAiReply(text) {
|
|
appendMessage("AI", text.slice(5));
|
|
Zenith.loading = false;
|
|
}
|
|
|
|
function onChatLoaded(text) {
|
|
let lines = text.split(/\r?\n/);
|
|
let batch = [];
|
|
for (let l of lines) {
|
|
let line = l.trim();
|
|
if (!line.length)
|
|
continue;
|
|
|
|
let u = line.match(/^\[\d{4}-.*\] User: (.*)$/);
|
|
let a = line.match(/^\[\d{4}-.*\] AI: (.*)$/);
|
|
if (u)
|
|
batch.push({
|
|
"sender": "You",
|
|
"message": u[1]
|
|
});
|
|
else if (a)
|
|
batch.push({
|
|
"sender": "AI",
|
|
"message": a[1]
|
|
});
|
|
else if (batch.length)
|
|
batch[batch.length - 1].message += "\n" + line;
|
|
}
|
|
messageModel.clear();
|
|
for (let m of batch) messageModel.append(m)
|
|
scrollToBottom();
|
|
}
|
|
|
|
target: Zenith
|
|
}
|
|
|
|
}
|