Fix chat rendering and improve scroll behavior
- Emit SegmentsRole in dataChanged signals so QML updates properly - Add onContentHeightChanged to scroll when content loads after session switch - Add floating scroll-to-bottom button that appears when scrolled up - Wrap chat ScrollView in Item to allow button overlay
This commit is contained in:
parent
07232b94df
commit
0ac03a9931
|
|
@ -223,7 +223,7 @@ void ChatBlockModel::appendData(const QString &text)
|
||||||
|
|
||||||
// But emit with the rendered HTML that includes partial content
|
// But emit with the rendered HTML that includes partial content
|
||||||
QModelIndex idx = index(m_blocks.size() - 1);
|
QModelIndex idx = index(m_blocks.size() - 1);
|
||||||
emit dataChanged(idx, idx, {RenderedHtmlRole});
|
emit dataChanged(idx, idx, {RenderedHtmlRole, SegmentsRole});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,7 +483,7 @@ void ChatBlockModel::appendContent(const QString &line)
|
||||||
rebuildBlockHtml(m_blocks.size() - 1);
|
rebuildBlockHtml(m_blocks.size() - 1);
|
||||||
|
|
||||||
QModelIndex idx = index(m_blocks.size() - 1);
|
QModelIndex idx = index(m_blocks.size() - 1);
|
||||||
emit dataChanged(idx, idx, {ContentRole, RenderedHtmlRole});
|
emit dataChanged(idx, idx, {ContentRole, RenderedHtmlRole, SegmentsRole});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatBlockModel::processLine(const QString &line)
|
void ChatBlockModel::processLine(const QString &line)
|
||||||
|
|
|
||||||
138
gui/main.qml
138
gui/main.qml
|
|
@ -1220,64 +1220,74 @@ ApplicationWindow {
|
||||||
|
|
||||||
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
||||||
|
|
||||||
// Chat display — block-based ListView
|
// Chat display — block-based ListView with scroll-to-bottom button
|
||||||
ScrollView {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
clip: true
|
|
||||||
|
|
||||||
// Placeholder when no agent selected or disconnected
|
ScrollView {
|
||||||
Label {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: {
|
|
||||||
if (!ollie.daemonConnected) return "Not connected"
|
|
||||||
if (!ollie.activeSessionId) return "Select a session"
|
|
||||||
return "Select an agent to view chat"
|
|
||||||
}
|
|
||||||
color: theme.midColor
|
|
||||||
font.italic: true
|
|
||||||
font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
||||||
visible: !ollie.activeAgentId || !ollie.daemonConnected
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: chatView
|
|
||||||
model: chatModel
|
|
||||||
spacing: 4
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 4
|
clip: true
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
visible: ollie.daemonConnected && ollie.activeAgentId !== ""
|
|
||||||
|
|
||||||
// Auto-scroll when near bottom (within ~50px or at end)
|
// Placeholder when no agent selected or disconnected
|
||||||
// Reset to true on new blocks, user can scroll up to disable
|
Label {
|
||||||
property bool shouldAutoScroll: true
|
anchors.centerIn: parent
|
||||||
readonly property bool nearBottom: contentHeight - contentY - height < 50
|
text: {
|
||||||
|
if (!ollie.daemonConnected) return "Not connected"
|
||||||
onContentYChanged: {
|
if (!ollie.activeSessionId) return "Select a session"
|
||||||
// User scrolled up - disable auto-scroll
|
return "Select an agent to view chat"
|
||||||
// User scrolled to bottom - re-enable
|
|
||||||
if (dragging || flicking) {
|
|
||||||
shouldAutoScroll = nearBottom || atYEnd
|
|
||||||
}
|
}
|
||||||
|
color: theme.midColor
|
||||||
|
font.italic: true
|
||||||
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12
|
||||||
|
visible: !ollie.activeAgentId || !ollie.daemonConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
onCountChanged: {
|
ListView {
|
||||||
// New block added - always scroll if we were following
|
id: chatView
|
||||||
if (count > 0) {
|
model: chatModel
|
||||||
if (shouldAutoScroll)
|
spacing: 4
|
||||||
scrollTimer.start()
|
anchors.fill: parent
|
||||||
}
|
anchors.margins: 4
|
||||||
}
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
visible: ollie.daemonConnected && ollie.activeAgentId !== ""
|
||||||
|
|
||||||
Connections {
|
// Auto-scroll when near bottom (within ~50px or at end)
|
||||||
target: chatModel
|
// Reset to true on new blocks, user can scroll up to disable
|
||||||
function onDataChanged() {
|
property bool shouldAutoScroll: true
|
||||||
// Content updated (streaming) - scroll if following
|
readonly property bool nearBottom: contentHeight - contentY - height < 50
|
||||||
if (chatView.shouldAutoScroll && chatView.count > 0)
|
|
||||||
scrollTimer.start()
|
onContentYChanged: {
|
||||||
|
// User scrolled up - disable auto-scroll
|
||||||
|
// User scrolled to bottom - re-enable
|
||||||
|
if (dragging || flicking) {
|
||||||
|
shouldAutoScroll = nearBottom || atYEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentHeightChanged: {
|
||||||
|
// Content height changed (e.g., after loading) - scroll if following
|
||||||
|
if (shouldAutoScroll && count > 0) {
|
||||||
|
positionViewAtEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCountChanged: {
|
||||||
|
// New block added - always scroll if we were following
|
||||||
|
if (count > 0) {
|
||||||
|
if (shouldAutoScroll)
|
||||||
|
scrollTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: chatModel
|
||||||
|
function onDataChanged() {
|
||||||
|
// Content updated (streaming) - scroll if following
|
||||||
|
if (chatView.shouldAutoScroll && chatView.count > 0)
|
||||||
|
scrollTimer.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
delegate: ColumnLayout {
|
||||||
id: blockItem
|
id: blockItem
|
||||||
|
|
@ -1585,6 +1595,38 @@ property bool blockExpanded: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll-to-bottom floating button
|
||||||
|
Rectangle {
|
||||||
|
id: scrollToBottomBtn
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: 20
|
||||||
|
color: theme.highlightColor
|
||||||
|
opacity: 0.9
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: 16
|
||||||
|
visible: !chatView.shouldAutoScroll && chatView.count > 0 && chatView.visible
|
||||||
|
|
||||||
|
Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "↓"
|
||||||
|
font.pixelSize: 20
|
||||||
|
font.bold: true
|
||||||
|
color: theme.highlightedTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
chatView.shouldAutoScroll = true
|
||||||
|
chatView.positionViewAtEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
||||||
|
|
|
||||||
Reference in New Issue