feat: Add message navigator

This commit is contained in:
Petr Mironychev
2026-05-27 22:06:41 +02:00
parent dc016ce533
commit fcc651fd75
5 changed files with 138 additions and 1 deletions

View File

@@ -27,6 +27,7 @@ qt_add_qml_module(QodeAssistChatView
qml/controls/Toast.qml qml/controls/Toast.qml
qml/controls/TopBar.qml qml/controls/TopBar.qml
qml/controls/SplitDropZone.qml qml/controls/SplitDropZone.qml
qml/controls/MessageNavigator.qml
RESOURCES RESOURCES
icons/attach-file-light.svg icons/attach-file-light.svg

View File

@@ -335,6 +335,16 @@ void ChatModel::resetModelTo(int index)
} }
} }
QVariantList ChatModel::userMessageIndices() const
{
QVariantList result;
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].role == ChatRole::User)
result.append(i);
}
return result;
}
void ChatModel::addToolExecutionStatus( void ChatModel::addToolExecutionStatus(
const QString &requestId, const QString &requestId,
const QString &toolId, const QString &toolId,

View File

@@ -94,6 +94,7 @@ public:
QString lastMessageId() const; QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index); Q_INVOKABLE void resetModelTo(int index);
Q_INVOKABLE QVariantList userMessageIndices() const;
void addToolExecutionStatus( void addToolExecutionStatus(
const QString &requestId, const QString &requestId,

View File

@@ -175,6 +175,27 @@ ChatRootView {
} }
} }
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 2
MessageNavigator {
id: messageNavigator
Layout.preferredWidth: 16
Layout.fillHeight: true
Layout.topMargin: 4
Layout.bottomMargin: 4
chatModel: root.chatModel
onMessageClicked: function(messageIndex) {
chatListView.userScrolledUp = true
chatListView.positionViewAtIndex(messageIndex, ListView.Beginning)
}
}
ListView { ListView {
id: chatListView id: chatListView
@@ -182,7 +203,7 @@ ChatRootView {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
leftMargin: 5 leftMargin: 3
model: root.chatModel model: root.chatModel
clip: true clip: true
spacing: 0 spacing: 0
@@ -370,6 +391,7 @@ ChatRootView {
} }
} }
} }
}
ScrollView { ScrollView {
id: view id: view

View File

@@ -0,0 +1,103 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import ChatView
Item {
id: nav
property var chatModel
property var userIndices: []
property int hoveredDotIndex: -1
property color dotColor: "#92BD6C"
signal messageClicked(int messageIndex)
implicitWidth: 16
function rebuild() {
if (chatModel)
userIndices = chatModel.userMessageIndices()
else
userIndices = []
}
onChatModelChanged: rebuild()
Component.onCompleted: rebuild()
Connections {
target: nav.chatModel
ignoreUnknownSignals: true
function onRowsInserted() { nav.rebuild() }
function onRowsRemoved() { nav.rebuild() }
function onModelReset() { nav.rebuild() }
function onModelReseted() { nav.rebuild() }
function onLayoutChanged() { nav.rebuild() }
}
Rectangle {
id: spine
visible: nav.userIndices.length > 1
x: nav.width / 2 - width / 2
y: 14
width: 1
height: Math.max(0, nav.height - 28)
color: palette.mid
opacity: 0.4
}
Repeater {
model: nav.userIndices
delegate: Item {
id: dotItem
required property var modelData
required property int index
width: nav.width
height: 14
x: 0
y: {
const count = nav.userIndices.length
const dotH = height
if (count <= 1)
return (nav.height - dotH) / 2
const top = 7
const bottom = nav.height - dotH - 7
return top + (bottom - top) * index / (count - 1)
}
Rectangle {
id: dot
anchors.centerIn: parent
width: dotArea.containsMouse ? 10 : 7
height: width
radius: width / 2
color: dotArea.containsMouse ? Qt.lighter(nav.dotColor, 1.15) : nav.dotColor
border.color: Qt.darker(nav.dotColor, 1.4)
border.width: 1
opacity: dotArea.containsMouse ? 1.0 : 0.9
Behavior on width { NumberAnimation { duration: 120 } }
Behavior on color { ColorAnimation { duration: 120 } }
}
MouseArea {
id: dotArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: nav.messageClicked(dotItem.modelData)
ToolTip.visible: containsMouse
ToolTip.delay: 400
ToolTip.text: qsTr("Jump to message #%1").arg(dotItem.index + 1)
}
}
}
}