From cf1f0f75d9aa0045f0b52b700dc5f979ba7388dc Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:27:20 +0200 Subject: [PATCH] pin reaction badge on assistant blocks, hide reaction buttons after reacting --- gui/ChatPane.qml | 18 +++++++++++++++--- gui/chatblockmodel.cpp | 22 ++++++++++++++++++++-- gui/chatblockmodel.h | 2 ++ gui/main-kf5.qml | 18 +++++++++++++++--- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/gui/ChatPane.qml b/gui/ChatPane.qml index 98f90fa..159cbf3 100644 --- a/gui/ChatPane.qml +++ b/gui/ChatPane.qml @@ -683,14 +683,26 @@ Item { Behavior on opacity { NumberAnimation { duration: 150 } } } - // Reaction buttons - Row { + // Reaction badge (pinned when reacted) + Label { anchors.bottom: parent.bottom anchors.right: parent.right anchors.margins: 4 z: 10 + text: block.reaction || "" + font.pixelSize: 18 + visible: block.reaction !== "" + } + + // Reaction buttons (hover only, hidden when already reacted) + Row { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.rightMargin: block.reaction !== "" ? 36 : 4 + anchors.margins: 4 + z: 10 spacing: 2 - opacity: assistantHover.hovered ? 0.9 : 0 + opacity: (assistantHover.hovered && block.reaction === "") ? 0.9 : 0 Behavior on opacity { NumberAnimation { duration: 150 } } Repeater { diff --git a/gui/chatblockmodel.cpp b/gui/chatblockmodel.cpp index 50eb5cb..9eb5a28 100644 --- a/gui/chatblockmodel.cpp +++ b/gui/chatblockmodel.cpp @@ -66,6 +66,8 @@ QVariant ChatBlockModel::data(const QModelIndex &index, int role) const return block.content; case ResponseIdRole: return block.responseId; + case ReactionRole: + return block.reaction; case SubBlocksRole: if (block.cachedContent != block.content) { block.cachedSubBlocks = parseSubBlocks(block.content); @@ -105,6 +107,7 @@ QHash ChatBlockModel::roleNames() const {SubBlocksRole, "subBlocks"}, {FormattedContentRole, "formattedContent"}, {ResponseIdRole, "responseId"}, + {ReactionRole, "reaction"}, }; } @@ -236,6 +239,8 @@ QList ChatBlockModel::parseBlocks(const QString &text) const static const QRegularExpression headerRe( R"(^\[(user|assistant|reasoning|call|tool|error|info)(?::([^\]]*))?\](.*)$)", QRegularExpression::MultilineOption); + static const QRegularExpression reactionRe( + R"(^\[reaction to assistant response ([^ ]+): ([^ ]+) \(([^)]+)\)\])"); auto it = headerRe.globalMatch(text); QList> headers; @@ -253,6 +258,15 @@ QList ChatBlockModel::parseBlocks(const QString &text) const return blocks; } + // First pass: collect reaction-to-response mappings. + QHash reactionMap; + for (const auto &[pos, match] : headers) { + auto rm = reactionRe.match(match.captured(0)); + if (rm.hasMatch()) { + reactionMap[rm.captured(1)] = rm.captured(3); + } + } + for (int i = 0; i < headers.size(); ++i) { const auto &[pos, match] = headers[i]; QString typeStr = match.captured(1); @@ -284,10 +298,14 @@ QList ChatBlockModel::parseBlocks(const QString &text) const else if (typeStr == "error") b.type = ChatBlock::Error; else if (typeStr == "info") b.type = ChatBlock::Info; - if (b.type == ChatBlock::Assistant) + if (b.type == ChatBlock::Assistant) { b.responseId = name; - else + auto it = reactionMap.constFind(name); + if (it != reactionMap.constEnd()) + b.reaction = it.value(); + } else { b.name = name; + } b.content = content; blocks.append(b); } diff --git a/gui/chatblockmodel.h b/gui/chatblockmodel.h index 1bd3369..568256e 100644 --- a/gui/chatblockmodel.h +++ b/gui/chatblockmodel.h @@ -20,6 +20,7 @@ struct ChatBlock { Type type; QString name; // tool/call name, empty for others QString responseId; // stable assistant response ID + QString reaction; // emoji reaction text (empty if not reacted) QString content; // raw text content mutable QVariantList cachedSubBlocks; // cached parseSubBlocks result mutable QString cachedContent; // content when subBlocks were cached @@ -53,6 +54,7 @@ public: SubBlocksRole, // QVariantList of sub-block maps FormattedContentRole, // pre-formatted HTML for call/tool expanded view ResponseIdRole, + ReactionRole, }; explicit ChatBlockModel(QObject *parent = nullptr); diff --git a/gui/main-kf5.qml b/gui/main-kf5.qml index f9ba6d4..bd0745a 100644 --- a/gui/main-kf5.qml +++ b/gui/main-kf5.qml @@ -1508,14 +1508,26 @@ ApplicationWindow { Behavior on opacity { NumberAnimation { duration: 150 } } } - // Reaction buttons - Row { + // Reaction badge (pinned when reacted) + Label { anchors.bottom: parent.bottom anchors.right: parent.right anchors.margins: 4 z: 10 + text: block.reaction || "" + font.pixelSize: 18 + visible: block.reaction !== "" + } + + // Reaction buttons (hover only, hidden when already reacted) + Row { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.rightMargin: block.reaction !== "" ? 36 : 4 + anchors.margins: 4 + z: 10 spacing: 2 - opacity: assistantHover.hovered ? 0.9 : 0 + opacity: (assistantHover.hovered && block.reaction === "") ? 0.9 : 0 Behavior on opacity { NumberAnimation { duration: 150 } } Repeater {