From a6e360c1db056bc9a17cb32517657654c2f148db Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 12 Oct 2025 12:58:03 +0200 Subject: [PATCH] ai: fade in response --- .../quickshell/ii/modules/common/Config.qml | 3 + .../sidebarLeft/aiChat/MessageTextBlock.qml | 113 +++++++++++++----- 2 files changed, 84 insertions(+), 32 deletions(-) diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml index 58ed95e37..6b872d79d 100644 --- a/.config/quickshell/ii/modules/common/Config.qml +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -348,6 +348,9 @@ Singleton { property JsonObject translator: JsonObject { property int delay: 300 // Delay before sending request. Reduces (potential) rate limits and lag. } + property JsonObject ai: JsonObject { + property bool textFadeIn: true + } property JsonObject booru: JsonObject { property bool allowNsfw: false property string defaultProvider: "yandere" diff --git a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageTextBlock.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageTextBlock.qml index d0d9d640d..9f5d20d4e 100644 --- a/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageTextBlock.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageTextBlock.qml @@ -8,6 +8,7 @@ import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell import Quickshell.Hyprland ColumnLayout { @@ -22,6 +23,8 @@ ColumnLayout { property list renderedLatexHashes: [] property string renderedSegmentContent: "" + property string shownText: "" + property bool fadeChunkSplitting: !editing && !/\n\|/.test(shownText) && Config.options.sidebar.ai.textFadeIn Layout.fillWidth: true @@ -73,7 +76,7 @@ ColumnLayout { renderLatex() } else { // console.log("Editing mode enabled", segmentContent) - textArea.text = segmentContent + root.shownText = segmentContent } } @@ -88,7 +91,7 @@ ColumnLayout { onRenderedSegmentContentChanged: { // console.log("Rendered segment content changed: " + renderedSegmentContent); if (renderedSegmentContent) { - textArea.text = renderedSegmentContent; + root.shownText = renderedSegmentContent; } } @@ -104,39 +107,85 @@ ColumnLayout { } } - TextArea { - id: textArea - - Layout.fillWidth: true - readOnly: !editing - selectByMouse: enableMouseSelection || editing - 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.m3onSecondaryContainer - selectionColor: Appearance.colors.colSecondaryContainer - wrapMode: TextEdit.Wrap - color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1 - textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText - text: Translation.tr("Waiting for response...") - - onTextChanged: { - if (!root.editing) return - segmentContent = text + spacing: 0 + Repeater { + id: textLinesRepeater + property list textLineOpacities: [] + model: ScriptModel { + // Split by either double newlines or single newlines in a list + values: root.fadeChunkSplitting ? root.shownText.split(/\n\n|\n(?= {0,2}[-\*])/g).filter(line => line.trim() !== "") : [root.shownText] + onValuesChanged: { + while (textLinesRepeater.textLineOpacities.length < values.length) { + textLinesRepeater.textLineOpacities.push(root.messageData.done ? 1 : 0); + } + } } + delegate: TextArea { + id: textArea + required property int index + required property string modelData - onLinkActivated: (link) => { - Qt.openUrlExternally(link) - GlobalStates.sidebarLeftOpen = false - } + // Fade in animation + visible: opacity > 0 + opacity: fadeChunkSplitting ? (textLinesRepeater.textLineOpacities[index] ?? (root.messageData.done ? 1 : 0)) : 1 + Connections { + target: root.messageData + function onDoneChanged() { + if (root.messageData.done) { + textLinesRepeater.textLineOpacities[textArea.index] = 1 + } + } + } + Connections { + target: textLinesRepeater.model + function onValuesChanged() { + if (textLinesRepeater.model.values.length > textArea.index + 1) { + textLinesRepeater.textLineOpacities[textArea.index] = 1 + } + } + } + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } - MouseArea { // Pointing hand for links - anchors.fill: parent - acceptedButtons: Qt.NoButton // Only for hover - hoverEnabled: true - cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : - (enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor + Layout.fillWidth: true + readOnly: !editing + selectByMouse: enableMouseSelection || editing + 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.m3onSecondaryContainer + selectionColor: Appearance.colors.colSecondaryContainer + wrapMode: TextEdit.Wrap + color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1 + textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText + text: modelData + + onTextChanged: { + if (!root.editing) return + segmentContent = text + } + + onLinkActivated: (link) => { + Qt.openUrlExternally(link) + GlobalStates.sidebarLeftOpen = false + } + + MouseArea { // Pointing hand for links + anchors.fill: parent + acceptedButtons: Qt.NoButton // Only for hover + hoverEnabled: true + cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : + (enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor + } + + // Rectangle { + // anchors.fill: parent + // color: "#22786378" + // border.width: 1 + // border.color: "#7E7E7E" + // } } } }