ai chat: action buttons

copy, edit, toggle markdown rendering, delete
This commit is contained in:
end-4 2025-05-06 23:57:17 +02:00
parent 38efbb0d21
commit db173152c3
4 changed files with 137 additions and 16 deletions

View file

@ -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<br/><sup>(hyprland_global_shortcuts_v1)</sup> | ✅ | ❌ |\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 <bits/stdc++.h>\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

View file

@ -5,7 +5,6 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Notifications
Button {
id: button

View file

@ -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
}
}
}

View file

@ -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
}
}