From 5c8a8f305d86a6e22f17135c5279faec585bd962 Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:00:53 +0100 Subject: [PATCH] Fix chat scrolling (#288) * fix: Change chat scrolling behavior * feat: Add compact mode for chat blocks --- ChatView/qml/RootItem.qml | 86 +++++++---- ChatView/qml/chatparts/CodeBlock.qml | 139 ++++++++++++----- ChatView/qml/chatparts/ThinkingBlock.qml | 187 ++++++++++++++--------- ChatView/qml/chatparts/ToolBlock.qml | 140 ++++++++++++----- 4 files changed, 382 insertions(+), 170 deletions(-) diff --git a/ChatView/qml/RootItem.qml b/ChatView/qml/RootItem.qml index 27bc114..ad32c42 100644 --- a/ChatView/qml/RootItem.qml +++ b/ChatView/qml/RootItem.qml @@ -148,7 +148,7 @@ ChatRootView { ListView { id: chatListView - signal hideServiceComponents(int itemIndex) + property bool userScrolledUp: false Layout.fillWidth: true Layout.fillHeight: true @@ -159,6 +159,18 @@ ChatRootView { boundsBehavior: Flickable.StopAtBounds cacheBuffer: 2000 + onMovingChanged: { + if (moving) { + userScrolledUp = !atYEnd + } + } + + onAtYEndChanged: { + if (atYEnd) { + userScrolledUp = false + } + } + delegate: Loader { id: componentLoader @@ -179,11 +191,6 @@ ChatRootView { } } - onLoaded: { - if (componentLoader.sourceComponent == chatItemComponent) { - chatListView.hideServiceComponents(index) - } - } } header: Item { @@ -195,12 +202,53 @@ ChatRootView { id: scroll } + Rectangle { + id: scrollToBottomButton + + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + bottomMargin: 10 + } + width: 36 + height: 36 + radius: 18 + color: palette.button + border.color: palette.mid + border.width: 1 + visible: chatListView.userScrolledUp + opacity: 0.9 + z: 100 + + Text { + anchors.centerIn: parent + text: "▼" + font.pixelSize: 14 + color: palette.buttonText + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + chatListView.userScrolledUp = false + root.scrollToBottom() + } + } + + Behavior on visible { + enabled: false + } + } + onCountChanged: { - root.scrollToBottom() + if (!userScrolledUp) { + root.scrollToBottom() + } } onContentHeightChanged: { - if (atYEnd) { + if (!userScrolledUp && atYEnd) { root.scrollToBottom() } } @@ -236,19 +284,8 @@ ChatRootView { id: toolMessageComponent ToolBlock { - id: toolsItem - width: parent.width toolContent: model.content - - Connections { - target: chatListView - function onHideServiceComponents(itemIndex) { - if (index !== itemIndex) { - toolsItem.headerOpacity = 0.5 - } - } - } } } @@ -281,8 +318,6 @@ ChatRootView { id: thinkingMessageComponent ThinkingBlock { - id: thinking - width: parent.width thinkingContent: { let content = model.content @@ -293,15 +328,6 @@ ChatRootView { return content } isRedacted: model.isRedacted !== undefined ? model.isRedacted : false - - Connections { - target: chatListView - function onHideServiceComponents(itemIndex) { - if (index !== itemIndex) { - thinking.headerOpacity = 0.5 - } - } - } } } } diff --git a/ChatView/qml/chatparts/CodeBlock.qml b/ChatView/qml/chatparts/CodeBlock.qml index 135abc8..3510c7f 100644 --- a/ChatView/qml/chatparts/CodeBlock.qml +++ b/ChatView/qml/chatparts/CodeBlock.qml @@ -28,11 +28,14 @@ Rectangle { property string code: "" property string language: "" - property bool expanded: false + + enum DisplayMode { Collapsed, Compact, Expanded } + property int displayMode: CodeBlock.DisplayMode.Compact + property int compactHeight: 150 property alias codeFontFamily: codeText.font.family property alias codeFontSize: codeText.font.pointSize - readonly property real collapsedHeight: copyButton.height + 10 + readonly property real headerHeight: copyButton.height + 10 color: palette.alternateBase border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3) @@ -46,6 +49,17 @@ Rectangle { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + implicitHeight: { + if (displayMode === CodeBlock.DisplayMode.Collapsed) { + return headerHeight + } else if (displayMode === CodeBlock.DisplayMode.Compact) { + let fullHeight = headerHeight + codeText.implicitHeight + 20 + return Math.min(fullHeight, headerHeight + compactHeight) + } else { + return headerHeight + codeText.implicitHeight + 20 + } + } + ChatUtils { id: utils } @@ -59,9 +73,17 @@ Rectangle { id: header width: parent.width - height: root.collapsedHeight + height: root.headerHeight cursorShape: Qt.PointingHandCursor - onClicked: root.expanded = !root.expanded + onClicked: { + if (root.displayMode === CodeBlock.DisplayMode.Collapsed) { + root.displayMode = CodeBlock.DisplayMode.Compact + } else if (root.displayMode === CodeBlock.DisplayMode.Compact) { + root.displayMode = CodeBlock.DisplayMode.Collapsed + } else { + root.displayMode = CodeBlock.DisplayMode.Compact + } + } Row { id: headerRow @@ -83,33 +105,81 @@ Rectangle { Text { anchors.verticalCenter: parent.verticalCenter - text: root.expanded ? "▼" : "▶" + text: root.displayMode === CodeBlock.DisplayMode.Collapsed ? "▶" : "▼" font.pixelSize: 10 color: palette.mid } } } - TextEdit { - id: codeText + Item { + id: codeWrapper anchors { left: parent.left right: parent.right top: header.bottom + bottom: parent.bottom margins: 10 + bottomMargin: expandButton.visible ? expandButton.height + 15 : 10 + } + clip: true + visible: root.displayMode !== CodeBlock.DisplayMode.Collapsed + + TextEdit { + id: codeText + + width: parent.width + text: root.code + readOnly: true + selectByMouse: true + color: root.color.hslLightness > 0.5 ? "black" : "white" + wrapMode: Text.WordWrap + selectionColor: palette.highlight + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: contextMenu.open() + } + } + } + + Rectangle { + id: expandButton + + property bool needsExpand: codeText.implicitHeight > compactHeight - 20 + + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + bottomMargin: 5 + leftMargin: 10 + rightMargin: 10 + } + height: 24 + radius: 4 + color: palette.button + visible: needsExpand && root.displayMode !== CodeBlock.DisplayMode.Collapsed + + Text { + anchors.centerIn: parent + text: root.displayMode === CodeBlock.DisplayMode.Expanded ? qsTr("▲ Show less") : qsTr("▼ Show more") + font.pixelSize: 11 + color: palette.buttonText } - text: root.code - readOnly: true - selectByMouse: true - color: parent.color.hslLightness > 0.5 ? "black" : "white" - wrapMode: Text.WordWrap - selectionColor: palette.highlight MouseArea { anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: contextMenu.open() + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.displayMode === CodeBlock.DisplayMode.Expanded) { + root.displayMode = CodeBlock.DisplayMode.Compact + } else { + root.displayMode = CodeBlock.DisplayMode.Expanded + } + } } } @@ -127,8 +197,26 @@ Rectangle { Platform.MenuSeparator {} Platform.MenuItem { - text: root.expanded ? qsTr("Collapse") : qsTr("Expand") - onTriggered: root.expanded = !root.expanded + text: root.displayMode === CodeBlock.DisplayMode.Collapsed ? qsTr("Expand") : qsTr("Collapse") + onTriggered: { + if (root.displayMode === CodeBlock.DisplayMode.Collapsed) { + root.displayMode = CodeBlock.DisplayMode.Compact + } else { + root.displayMode = CodeBlock.DisplayMode.Collapsed + } + } + } + + Platform.MenuItem { + text: root.displayMode === CodeBlock.DisplayMode.Expanded ? qsTr("Compact view") : qsTr("Full view") + enabled: root.displayMode !== CodeBlock.DisplayMode.Collapsed + onTriggered: { + if (root.displayMode === CodeBlock.DisplayMode.Expanded) { + root.displayMode = CodeBlock.DisplayMode.Compact + } else { + root.displayMode = CodeBlock.DisplayMode.Expanded + } + } } } @@ -153,21 +241,4 @@ Rectangle { onTriggered: parent.text = qsTr("Copy") } } - - states: [ - State { - when: !root.expanded - PropertyChanges { - target: root - implicitHeight: root.collapsedHeight - } - }, - State { - when: root.expanded - PropertyChanges { - target: root - implicitHeight: header.height + codeText.implicitHeight + 10 - } - } - ] } diff --git a/ChatView/qml/chatparts/ThinkingBlock.qml b/ChatView/qml/chatparts/ThinkingBlock.qml index 6d8a2bf..0a82b02 100644 --- a/ChatView/qml/chatparts/ThinkingBlock.qml +++ b/ChatView/qml/chatparts/ThinkingBlock.qml @@ -24,10 +24,12 @@ Rectangle { id: root property string thinkingContent: "" - // property string signature: "" property bool isRedacted: false - property bool expanded: false - + + enum DisplayMode { Collapsed, Compact, Expanded } + property int displayMode: ThinkingBlock.DisplayMode.Compact + property int compactHeight: 120 + property alias headerOpacity: headerRow.opacity radius: 6 @@ -38,13 +40,32 @@ Rectangle { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + implicitHeight: { + if (displayMode === ThinkingBlock.DisplayMode.Collapsed) { + return header.height + } else if (displayMode === ThinkingBlock.DisplayMode.Compact) { + let fullHeight = header.height + contentColumn.height + 20 + return Math.min(fullHeight, header.height + compactHeight) + } else { + return header.height + contentColumn.height + 20 + } + } + MouseArea { id: header width: parent.width height: headerRow.height + 10 cursorShape: Qt.PointingHandCursor - onClicked: root.expanded = !root.expanded + onClicked: { + if (root.displayMode === ThinkingBlock.DisplayMode.Collapsed) { + root.displayMode = ThinkingBlock.DisplayMode.Compact + } else if (root.displayMode === ThinkingBlock.DisplayMode.Compact) { + root.displayMode = ThinkingBlock.DisplayMode.Collapsed + } else { + root.displayMode = ThinkingBlock.DisplayMode.Compact + } + } Row { id: headerRow @@ -67,72 +88,96 @@ Rectangle { Text { anchors.verticalCenter: parent.verticalCenter - text: root.expanded ? "▼" : "▶" + text: root.displayMode === ThinkingBlock.DisplayMode.Collapsed ? "▶" : "▼" font.pixelSize: 10 color: palette.mid } } } - Column { - id: contentColumn + Item { + id: contentWrapper anchors { left: parent.left right: parent.right top: header.bottom + bottom: parent.bottom margins: 10 + bottomMargin: expandButton.visible ? expandButton.height + 15 : 10 } - spacing: 8 + clip: true + visible: root.displayMode !== ThinkingBlock.DisplayMode.Collapsed + + Column { + id: contentColumn + + width: parent.width + spacing: 8 + + Text { + visible: root.isRedacted + width: parent.width + text: qsTr("Thinking content was redacted by safety systems") + font.pixelSize: 11 + font.italic: true + color: Qt.rgba(0.8, 0.4, 0.4, 1.0) + wrapMode: Text.WordWrap + } + + TextEdit { + id: thinkingText + + visible: !root.isRedacted + width: parent.width + text: root.thinkingContent + readOnly: true + selectByMouse: true + color: palette.text + wrapMode: Text.WordWrap + font.family: "monospace" + font.pixelSize: 11 + selectionColor: palette.highlight + } + } + } + + Rectangle { + id: expandButton + + property bool needsExpand: contentColumn.height > compactHeight - 20 + + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + bottomMargin: 5 + leftMargin: 10 + rightMargin: 10 + } + height: 24 + radius: 4 + color: palette.button + visible: needsExpand && root.displayMode !== ThinkingBlock.DisplayMode.Collapsed Text { - visible: root.isRedacted - width: parent.width - text: qsTr("Thinking content was redacted by safety systems") + anchors.centerIn: parent + text: root.displayMode === ThinkingBlock.DisplayMode.Expanded ? qsTr("▲ Show less") : qsTr("▼ Show more") font.pixelSize: 11 - font.italic: true - color: Qt.rgba(0.8, 0.4, 0.4, 1.0) - wrapMode: Text.WordWrap + color: palette.buttonText } - TextEdit { - id: thinkingText - - visible: !root.isRedacted - width: parent.width - text: root.thinkingContent - readOnly: true - selectByMouse: true - color: palette.text - wrapMode: Text.WordWrap - font.family: "monospace" - font.pixelSize: 11 - selectionColor: palette.highlight + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.displayMode === ThinkingBlock.DisplayMode.Expanded) { + root.displayMode = ThinkingBlock.DisplayMode.Compact + } else { + root.displayMode = ThinkingBlock.DisplayMode.Expanded + } + } } - - // Rectangle { - // visible: root.signature.length > 0 && root.expanded - // width: parent.width - // height: signatureText.height + 10 - // color: palette.alternateBase - // radius: 4 - - // Text { - // id: signatureText - - // anchors { - // left: parent.left - // right: parent.right - // verticalCenter: parent.verticalCenter - // margins: 5 - // } - // text: qsTr("Signature: %1").arg(root.signature.substring(0, Math.min(40, root.signature.length)) + "...") - // font.pixelSize: 9 - // font.family: "monospace" - // color: palette.mid - // elide: Text.ElideRight - // } - // } } MouseArea { @@ -146,8 +191,26 @@ Rectangle { id: contextMenu Platform.MenuItem { - text: root.expanded ? qsTr("Collapse") : qsTr("Expand") - onTriggered: root.expanded = !root.expanded + text: root.displayMode === ThinkingBlock.DisplayMode.Collapsed ? qsTr("Expand") : qsTr("Collapse") + onTriggered: { + if (root.displayMode === ThinkingBlock.DisplayMode.Collapsed) { + root.displayMode = ThinkingBlock.DisplayMode.Compact + } else { + root.displayMode = ThinkingBlock.DisplayMode.Collapsed + } + } + } + + Platform.MenuItem { + text: root.displayMode === ThinkingBlock.DisplayMode.Expanded ? qsTr("Compact view") : qsTr("Full view") + enabled: root.displayMode !== ThinkingBlock.DisplayMode.Collapsed + onTriggered: { + if (root.displayMode === ThinkingBlock.DisplayMode.Expanded) { + root.displayMode = ThinkingBlock.DisplayMode.Compact + } else { + root.displayMode = ThinkingBlock.DisplayMode.Expanded + } + } } } @@ -162,22 +225,4 @@ Rectangle { : Qt.lighter(palette.alternateBase, 1.3)) radius: root.radius } - - states: [ - State { - when: !root.expanded - PropertyChanges { - target: root - implicitHeight: header.height - } - }, - State { - when: root.expanded - PropertyChanges { - target: root - implicitHeight: header.height + contentColumn.height + 20 - } - } - ] } - diff --git a/ChatView/qml/chatparts/ToolBlock.qml b/ChatView/qml/chatparts/ToolBlock.qml index c9feb23..060b6fd 100644 --- a/ChatView/qml/chatparts/ToolBlock.qml +++ b/ChatView/qml/chatparts/ToolBlock.qml @@ -24,7 +24,10 @@ Rectangle { id: root property string toolContent: "" - property bool expanded: false + + enum DisplayMode { Collapsed, Compact, Expanded } + property int displayMode: ToolBlock.DisplayMode.Compact + property int compactHeight: 120 property alias headerOpacity: headerRow.opacity @@ -40,13 +43,32 @@ Rectangle { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + implicitHeight: { + if (displayMode === ToolBlock.DisplayMode.Collapsed) { + return header.height + } else if (displayMode === ToolBlock.DisplayMode.Compact) { + let fullHeight = header.height + contentColumn.height + 20 + return Math.min(fullHeight, header.height + compactHeight) + } else { + return header.height + contentColumn.height + 20 + } + } + MouseArea { id: header width: parent.width height: headerRow.height + 10 cursorShape: Qt.PointingHandCursor - onClicked: root.expanded = !root.expanded + onClicked: { + if (root.displayMode === ToolBlock.DisplayMode.Collapsed) { + root.displayMode = ToolBlock.DisplayMode.Compact + } else if (root.displayMode === ToolBlock.DisplayMode.Compact) { + root.displayMode = ToolBlock.DisplayMode.Collapsed + } else { + root.displayMode = ToolBlock.DisplayMode.Compact + } + } Row { id: headerRow @@ -68,36 +90,84 @@ Rectangle { Text { anchors.verticalCenter: parent.verticalCenter - text: root.expanded ? "▼" : "▶" + text: root.displayMode === ToolBlock.DisplayMode.Collapsed ? "▶" : "▼" font.pixelSize: 10 color: palette.mid } } } - Column { - id: contentColumn + Item { + id: contentWrapper anchors { left: parent.left right: parent.right top: header.bottom + bottom: parent.bottom margins: 10 + bottomMargin: expandButton.visible ? expandButton.height + 15 : 10 } - spacing: 8 + clip: true + visible: root.displayMode !== ToolBlock.DisplayMode.Collapsed - TextEdit { - id: resultText + Column { + id: contentColumn width: parent.width - text: root.toolResult - readOnly: true - selectByMouse: true - color: palette.text - wrapMode: Text.WordWrap - font.family: "monospace" + spacing: 8 + + TextEdit { + id: resultText + + width: parent.width + text: root.toolResult + readOnly: true + selectByMouse: true + color: palette.text + wrapMode: Text.WordWrap + font.family: "monospace" + font.pixelSize: 11 + selectionColor: palette.highlight + } + } + } + + Rectangle { + id: expandButton + + property bool needsExpand: contentColumn.height > compactHeight - 20 + + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + bottomMargin: 5 + leftMargin: 10 + rightMargin: 10 + } + height: 24 + radius: 4 + color: palette.button + visible: needsExpand && root.displayMode !== ToolBlock.DisplayMode.Collapsed + + Text { + anchors.centerIn: parent + text: root.displayMode === ToolBlock.DisplayMode.Expanded ? qsTr("▲ Show less") : qsTr("▼ Show more") font.pixelSize: 11 - selectionColor: palette.highlight + color: palette.buttonText + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.displayMode === ToolBlock.DisplayMode.Expanded) { + root.displayMode = ToolBlock.DisplayMode.Compact + } else { + root.displayMode = ToolBlock.DisplayMode.Expanded + } + } } } @@ -126,8 +196,26 @@ Rectangle { Platform.MenuSeparator {} Platform.MenuItem { - text: root.expanded ? qsTr("Collapse") : qsTr("Expand") - onTriggered: root.expanded = !root.expanded + text: root.displayMode === ToolBlock.DisplayMode.Collapsed ? qsTr("Expand") : qsTr("Collapse") + onTriggered: { + if (root.displayMode === ToolBlock.DisplayMode.Collapsed) { + root.displayMode = ToolBlock.DisplayMode.Compact + } else { + root.displayMode = ToolBlock.DisplayMode.Collapsed + } + } + } + + Platform.MenuItem { + text: root.displayMode === ToolBlock.DisplayMode.Expanded ? qsTr("Compact view") : qsTr("Full view") + enabled: root.displayMode !== ToolBlock.DisplayMode.Collapsed + onTriggered: { + if (root.displayMode === ToolBlock.DisplayMode.Expanded) { + root.displayMode = ToolBlock.DisplayMode.Compact + } else { + root.displayMode = ToolBlock.DisplayMode.Expanded + } + } } } @@ -141,22 +229,4 @@ Rectangle { : Qt.lighter(palette.alternateBase, 1.3) radius: root.radius } - - - states: [ - State { - when: !root.expanded - PropertyChanges { - target: root - implicitHeight: header.height - } - }, - State { - when: root.expanded - PropertyChanges { - target: root - implicitHeight: header.height + contentColumn.height + 20 - } - } - ] }