Files
illogical-impulse/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml
T
end-4 db173152c3 ai chat: action buttons
copy, edit, toggle markdown rendering, delete
2025-05-06 23:57:17 +02:00

235 lines
9.0 KiB
QML

import "root:/"
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
import Quickshell.Io
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
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
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
ColumnLayout {
id: columnLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: messagePadding
spacing: root.contentSpacing
RowLayout { // Header
spacing: 15
Rectangle { // Name
id: nameWrapper
color: Appearance.m3colors.m3secondaryContainer
radius: Appearance.rounding.small
implicitWidth: nameRowLayout.implicitWidth + 10 * 2
implicitHeight: Math.max(nameRowLayout.implicitHeight + 5 * 2, 30)
Layout.alignment: Qt.AlignVCenter
RowLayout {
id: nameRowLayout
anchors.centerIn: parent
spacing: 5
Item {
Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
implicitWidth: messageData.role == 'assistant' ? modelIcon.width : roleIcon.implicitWidth
implicitHeight: messageData.role == 'assistant' ? modelIcon.height : roleIcon.implicitHeight
CustomIcon {
id: modelIcon
anchors.centerIn: parent
visible: messageData.role == 'assistant' && Ai.models[messageData.model].icon
width: Appearance.font.pixelSize.large
height: Appearance.font.pixelSize.large
source: messageData.role == 'assistant' ? Ai.models[messageData.model].icon :
messageData.role == 'user' ? 'linux-symbolic' : 'desktop-symbolic'
}
ColorOverlay {
visible: modelIcon.visible
anchors.fill: modelIcon
source: modelIcon
color: Appearance.m3colors.m3onSecondaryContainer
}
MaterialSymbol {
id: roleIcon
anchors.centerIn: parent
visible: !modelIcon.visible
iconSize: Appearance.font.pixelSize.larger
color: Appearance.m3colors.m3onSecondaryContainer
text: messageData.role == 'user' ? 'person' :
messageData.role == 'interface' ? 'settings' :
messageData.role == 'assistant' ? 'neurology' :
'computer'
}
}
StyledText {
id: providerName
Layout.alignment: Qt.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.normal
font.weight: Font.DemiBold
color: Appearance.m3colors.m3onSecondaryContainer
text: messageData.role == 'assistant' ? Ai.models[messageData.model].name :
(messageData.role == 'user' && SystemInfo.username) ? SystemInfo.username :
(messageData.role == 'interface') ? qsTr("Interface") : qsTr("Unknown")
}
}
}
Item { Layout.fillWidth: true }
Button { // Not visible to model
id: modelVisibilityIndicator
visible: messageData.role == 'interface'
implicitWidth: 16
implicitHeight: 30
Layout.alignment: Qt.AlignVCenter
background: Item
MaterialSymbol {
id: notVisibleToModelText
anchors.centerIn: parent
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colSubtext
text: "visibility_off"
}
StyledToolTip {
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: !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: root.renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
text: messageData.thinking ? qsTr("Waiting for response...") : root.messageData.content
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Control) { // Prevent de-select
event.accepted = true
}
if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
messageText.copy()
event.accepted = true
}
}
onLinkActivated: (link) => {
Qt.openUrlExternally(link)
Hyprland.dispatch("global quickshell:sidebarLeftClose")
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton // Only for hover
hoverEnabled: true
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor
}
}
}
}