diff --git a/.config/quickshell/modules/sidebarLeft/AiChat.qml b/.config/quickshell/modules/sidebarLeft/AiChat.qml index d587e2db..689a464e 100644 --- a/.config/quickshell/modules/sidebarLeft/AiChat.qml +++ b/.config/quickshell/modules/sidebarLeft/AiChat.qml @@ -82,16 +82,24 @@ Item { Ai.addMessage("## ✏️ Markdown test\n" + "- **Bold**, *Italic*, `Monospace`, [Link](https://example.com)\n\n" + "- Table:\n\n" - + "| | Quickshell | AGS/Astal |\n" - + "|:-----------|:----------:|:---------:|\n" - + "| UI Toolkit | Qt | Gtk3/Gtk4 |\n" - + "| Language | QML | Js/Ts/Lua |\n" - + "| Reactivity | Implied | Needs declaration |\n" - + "| Widget placement | Mildly difficult | More intuitive |\n" - + "| Bluetooth & Wifi support | ❌ | ✅ |\n" - + "| No-delay keybinds
(hyprland_global_shortcuts_v1) | ✅ | ❌ |\n" - + "| Development | New APIs | New syntax |\n" - + "- Code block" + + "| | Quickshell | AGS/Astal |\n" + + "|:-------------------------|:----------------:|:-----------------:|\n" + + "| UI Toolkit | Qt | Gtk3/Gtk4 |\n" + + "| Language | QML | Js/Ts/Lua |\n" + + "| Reactivity | Implied | Needs declaration |\n" + + "| Widget placement | Mildly difficult | More intuitive |\n" + + "| Bluetooth & Wifi support | ❌ | ✅ |\n" + + "| No-delay keybinds | ✅ | ❌ |\n" + + "| Development | New APIs | New syntax |\n" + + "- Code block\n" + + "```cpp\n" + + "#include \n" + + "const std::string GREETING = \"UwU\";\n" + + "int main(int argc, char* argv[]) {\n" + + " std::cout << GREETING;\n" + + "}\n" + + "```\n" + , Ai.interfaceRole); } @@ -157,6 +165,15 @@ Item { easing.bezierCurve: Appearance.animation.elementMoveEnter.bezierCurve } } + remove: Transition { + NumberAnimation { + property: "opacity" + from: 1; to: 0 + duration: Appearance.animation.elementMoveEnter.duration + easing.type: Appearance.animation.elementMoveEnter.type + easing.bezierCurve: Appearance.animation.elementMoveEnter.bezierCurve + } + } model: ScriptModel { values: root.messages diff --git a/.config/quickshell/modules/sidebarLeft/ApiCommandButton.qml b/.config/quickshell/modules/sidebarLeft/ApiCommandButton.qml index 25bfdbb8..cd54e8a3 100644 --- a/.config/quickshell/modules/sidebarLeft/ApiCommandButton.qml +++ b/.config/quickshell/modules/sidebarLeft/ApiCommandButton.qml @@ -5,7 +5,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import Quickshell.Services.Notifications Button { id: button diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml index bb16cc38..35a10237 100644 --- a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml +++ b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml @@ -3,6 +3,7 @@ import "root:/services" import "root:/modules/common" import "root:/modules/common/widgets" import "../" +import "root:/modules/common/functions/string_utils.js" as StringUtils import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -15,12 +16,16 @@ import Qt5Compat.GraphicalEffects Rectangle { id: root + property int messageIndex property var messageData property var messageInputField property real messagePadding: 7 property real contentSpacing: 3 + property bool renderMarkdown: true + property bool editing: false + anchors.left: parent?.left anchors.right: parent?.right implicitHeight: columnLayout.implicitHeight + root.messagePadding * 2 @@ -38,6 +43,8 @@ Rectangle { spacing: root.contentSpacing RowLayout { // Header + spacing: 15 + Rectangle { // Name id: nameWrapper color: Appearance.m3colors.m3secondaryContainer @@ -102,9 +109,10 @@ Rectangle { Item { Layout.fillWidth: true } Button { // Not visible to model + id: modelVisibilityIndicator visible: messageData.role == 'interface' - implicitWidth: Math.max(notVisibleToModelText.implicitWidth + 10 * 2, 30) - implicitHeight: notVisibleToModelText.implicitHeight + 5 * 2 + implicitWidth: 16 + implicitHeight: 30 Layout.alignment: Qt.AlignVCenter background: Item @@ -120,21 +128,84 @@ Rectangle { content: qsTr("Not visible to model") } } + + StyledText { + visible: modelVisibilityIndicator.visible + font.pixelSize: Appearance.font.pixelSize.larger + color: Appearance.colors.colOnLayer1 + text: "•" + } + + RowLayout { + spacing: 5 + + AiMessageControlButton { + id: copyButton + buttonIcon: "content_copy" + onClicked: { + Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(root.messageData.content)}'`) + } + StyledToolTip { + content: qsTr("Copy") + } + } + AiMessageControlButton { + id: editButton + activated: root.editing + buttonIcon: "edit" + onClicked: { + root.editing = !root.editing + if (!root.editing) { // Save changes + root.messageData.content = messageText.text + } + } + StyledToolTip { + content: root.editing ? qsTr("Save") : qsTr("Edit") + } + } + AiMessageControlButton { + id: toggleMarkdownButton + activated: !root.renderMarkdown + buttonIcon: root.renderMarkdown ? "wysiwyg" : "code" + onClicked: { + root.renderMarkdown = !root.renderMarkdown + if (root.renderMarkdown && messageData.finished) { + messageText.text = root.messageData.content + } + } + StyledToolTip { + content: qsTr("Toggle Markdown rendering") + } + } + AiMessageControlButton { + id: deleteButton + buttonIcon: "close" + onClicked: { + Ai.removeMessage(root.messageIndex) + } + StyledToolTip { + content: qsTr("Delete") + } + } + } } TextEdit { // Message id: messageText Layout.fillWidth: true Layout.margins: messagePadding - readOnly: true + readOnly: !root.editing selectByMouse: true + renderType: Text.NativeRendering font.family: Appearance.font.family.reading font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text font.pixelSize: Appearance.font.pixelSize.small + selectedTextColor: Appearance.m3colors.m3onPrimary + selectionColor: Appearance.m3colors.m3primary wrapMode: Text.WordWrap color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1 - textFormat: Text.MarkdownText + textFormat: root.renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText text: messageData.thinking ? qsTr("Waiting for response...") : root.messageData.content Keys.onPressed: (event) => { @@ -155,7 +226,7 @@ Rectangle { anchors.fill: parent acceptedButtons: Qt.NoButton // Only for hover hoverEnabled: true - cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor + cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor } } } diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessageControlButton.qml b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessageControlButton.qml new file mode 100644 index 00000000..003cc1ba --- /dev/null +++ b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessageControlButton.qml @@ -0,0 +1,34 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/services" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell + +Button { + id: button + property string buttonIcon + property bool activated: false + + implicitHeight: 30 + implicitWidth: 30 + + PointingHandInteraction {} + + background: Rectangle { + radius: Appearance.rounding.small + color: button.activated ? Appearance.m3colors.m3primary : + button.down ? Appearance.colors.colSurfaceContainerHighestActive : + button.hovered ? Appearance.colors.colSurfaceContainerHighestHover : + Appearance.m3colors.m3surfaceContainerHighest + } + + contentItem: MaterialSymbol { + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.large + color: button.activated ? Appearance.m3colors.m3onPrimary : + Appearance.m3colors.m3onSurface + text: buttonIcon + } +}