mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 02:49:12 -04:00
feat: Add preview to message navigator
This commit is contained in:
@@ -335,12 +335,24 @@ void ChatModel::resetModelTo(int index)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList ChatModel::userMessageIndices() const
|
QVariantList ChatModel::userMessagePreviews(int maxLength) const
|
||||||
{
|
{
|
||||||
QVariantList result;
|
QVariantList result;
|
||||||
|
const int limit = maxLength > 4 ? maxLength : 80;
|
||||||
for (int i = 0; i < m_messages.size(); ++i) {
|
for (int i = 0; i < m_messages.size(); ++i) {
|
||||||
if (m_messages[i].role == ChatRole::User)
|
if (m_messages[i].role != ChatRole::User)
|
||||||
result.append(i);
|
continue;
|
||||||
|
QString preview = m_messages[i].content;
|
||||||
|
preview.replace(QLatin1Char('\n'), QLatin1Char(' '));
|
||||||
|
preview.replace(QLatin1Char('\r'), QLatin1Char(' '));
|
||||||
|
preview.replace(QLatin1Char('\t'), QLatin1Char(' '));
|
||||||
|
preview = preview.simplified();
|
||||||
|
if (preview.size() > limit)
|
||||||
|
preview = preview.left(limit - 1).trimmed() + QChar(0x2026);
|
||||||
|
QVariantMap entry;
|
||||||
|
entry[QStringLiteral("messageIndex")] = i;
|
||||||
|
entry[QStringLiteral("preview")] = preview;
|
||||||
|
result.append(entry);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +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;
|
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
|
||||||
|
|
||||||
void addToolExecutionStatus(
|
void addToolExecutionStatus(
|
||||||
const QString &requestId,
|
const QString &requestId,
|
||||||
|
|||||||
@@ -201,6 +201,11 @@ ChatRootView {
|
|||||||
|
|
||||||
property bool userScrolledUp: false
|
property bool userScrolledUp: false
|
||||||
|
|
||||||
|
function syncNavigatorCurrent() {
|
||||||
|
const top = indexAt(10, contentY + 4)
|
||||||
|
messageNavigator.updateCurrentFromModelIndex(top)
|
||||||
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
leftMargin: 3
|
leftMargin: 3
|
||||||
@@ -210,6 +215,8 @@ ChatRootView {
|
|||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
cacheBuffer: 2000
|
cacheBuffer: 2000
|
||||||
|
|
||||||
|
onContentYChanged: Qt.callLater(syncNavigatorCurrent)
|
||||||
|
|
||||||
onMovingChanged: {
|
onMovingChanged: {
|
||||||
if (moving) {
|
if (moving) {
|
||||||
userScrolledUp = !atYEnd
|
userScrolledUp = !atYEnd
|
||||||
@@ -296,6 +303,7 @@ ChatRootView {
|
|||||||
if (!userScrolledUp) {
|
if (!userScrolledUp) {
|
||||||
root.scrollToBottom()
|
root.scrollToBottom()
|
||||||
}
|
}
|
||||||
|
Qt.callLater(syncNavigatorCurrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentHeightChanged: {
|
onContentHeightChanged: {
|
||||||
|
|||||||
@@ -4,27 +4,91 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import ChatView
|
import ChatView
|
||||||
|
import UIControls
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: nav
|
id: nav
|
||||||
|
|
||||||
property var chatModel
|
property var chatModel
|
||||||
property var userIndices: []
|
property var entries: []
|
||||||
property int hoveredDotIndex: -1
|
|
||||||
property color dotColor: "#92BD6C"
|
property color dotColor: "#92BD6C"
|
||||||
|
property int currentMessageIndex: -1
|
||||||
|
|
||||||
|
readonly property int dotCount: entries.length
|
||||||
|
readonly property int verticalPadding: 8
|
||||||
|
readonly property int minDotSpacing: 18
|
||||||
|
readonly property real availableHeight: Math.max(0, height - 2 * verticalPadding)
|
||||||
|
readonly property real naturalHeight: dotCount > 1 ? (dotCount - 1) * minDotSpacing : 0
|
||||||
|
readonly property bool needsScrolling: naturalHeight > availableHeight
|
||||||
|
readonly property real contentHeight: needsScrolling
|
||||||
|
? naturalHeight + 2 * verticalPadding
|
||||||
|
: Math.max(height, 2 * verticalPadding)
|
||||||
|
|
||||||
signal messageClicked(int messageIndex)
|
signal messageClicked(int messageIndex)
|
||||||
|
|
||||||
implicitWidth: 16
|
implicitWidth: 16
|
||||||
|
|
||||||
function rebuild() {
|
function rebuild() {
|
||||||
if (chatModel)
|
entries = chatModel ? chatModel.userMessagePreviews(80) : []
|
||||||
userIndices = chatModel.userMessageIndices()
|
Qt.callLater(scrollCurrentIntoView)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentFromModelIndex(modelIdx) {
|
||||||
|
if (modelIdx < 0) {
|
||||||
|
currentMessageIndex = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let best = -1
|
||||||
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
|
const e = entries[i]
|
||||||
|
if (!e)
|
||||||
|
continue
|
||||||
|
const mi = e.messageIndex
|
||||||
|
if (mi <= modelIdx)
|
||||||
|
best = mi
|
||||||
else
|
else
|
||||||
userIndices = []
|
break
|
||||||
|
}
|
||||||
|
currentMessageIndex = best
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiIndexOf(messageIndex) {
|
||||||
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
|
const e = entries[i]
|
||||||
|
if (e && e.messageIndex === messageIndex)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function dotCenterY(uiIndex) {
|
||||||
|
const count = dotCount
|
||||||
|
if (count <= 1)
|
||||||
|
return contentHeight / 2
|
||||||
|
const spacing = needsScrolling
|
||||||
|
? minDotSpacing
|
||||||
|
: availableHeight / (count - 1)
|
||||||
|
return verticalPadding + spacing * uiIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollCurrentIntoView() {
|
||||||
|
if (!needsScrolling || currentMessageIndex < 0)
|
||||||
|
return
|
||||||
|
const ui = uiIndexOf(currentMessageIndex)
|
||||||
|
if (ui < 0)
|
||||||
|
return
|
||||||
|
const y = dotCenterY(ui)
|
||||||
|
const margin = 24
|
||||||
|
if (y < flick.contentY + margin)
|
||||||
|
flick.contentY = Math.max(0, y - margin)
|
||||||
|
else if (y > flick.contentY + flick.height - margin)
|
||||||
|
flick.contentY = Math.min(
|
||||||
|
Math.max(0, flick.contentHeight - flick.height),
|
||||||
|
y - flick.height + margin)
|
||||||
}
|
}
|
||||||
|
|
||||||
onChatModelChanged: rebuild()
|
onChatModelChanged: rebuild()
|
||||||
|
onCurrentMessageIndexChanged: scrollCurrentIntoView()
|
||||||
Component.onCompleted: rebuild()
|
Component.onCompleted: rebuild()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -34,23 +98,33 @@ Item {
|
|||||||
function onRowsRemoved() { nav.rebuild() }
|
function onRowsRemoved() { nav.rebuild() }
|
||||||
function onModelReset() { nav.rebuild() }
|
function onModelReset() { nav.rebuild() }
|
||||||
function onModelReseted() { nav.rebuild() }
|
function onModelReseted() { nav.rebuild() }
|
||||||
function onLayoutChanged() { nav.rebuild() }
|
function onDataChanged() { nav.rebuild() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: flick
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: nav.contentHeight
|
||||||
|
interactive: nav.needsScrolling
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: spine
|
id: spine
|
||||||
|
|
||||||
visible: nav.userIndices.length > 1
|
visible: nav.dotCount > 1
|
||||||
x: nav.width / 2 - width / 2
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
y: 14
|
y: nav.verticalPadding
|
||||||
width: 1
|
width: 1
|
||||||
height: Math.max(0, nav.height - 28)
|
height: Math.max(0, flick.contentHeight - 2 * nav.verticalPadding)
|
||||||
color: palette.mid
|
color: palette.mid
|
||||||
opacity: 0.4
|
opacity: 0.4
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: nav.userIndices
|
model: nav.entries
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: dotItem
|
id: dotItem
|
||||||
@@ -58,31 +132,35 @@ Item {
|
|||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
width: nav.width
|
readonly property int msgIndex: modelData && modelData.messageIndex !== undefined
|
||||||
|
? modelData.messageIndex : -1
|
||||||
|
readonly property string preview: modelData && modelData.preview !== undefined
|
||||||
|
? modelData.preview : ""
|
||||||
|
readonly property bool isCurrent: nav.currentMessageIndex === msgIndex
|
||||||
|
|
||||||
|
width: 16
|
||||||
height: 14
|
height: 14
|
||||||
x: 0
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
y: {
|
y: nav.dotCenterY(index) - height / 2
|
||||||
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 {
|
Rectangle {
|
||||||
id: dot
|
id: dot
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: dotArea.containsMouse ? 10 : 7
|
width: dotItem.isCurrent ? 11 : (dotArea.containsMouse ? 10 : 7)
|
||||||
height: width
|
height: width
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: dotArea.containsMouse ? Qt.lighter(nav.dotColor, 1.15) : nav.dotColor
|
color: dotArea.containsMouse
|
||||||
border.color: Qt.darker(nav.dotColor, 1.4)
|
? Qt.lighter(nav.dotColor, 1.2)
|
||||||
border.width: 1
|
: nav.dotColor
|
||||||
opacity: dotArea.containsMouse ? 1.0 : 0.9
|
border.color: dotItem.isCurrent
|
||||||
|
? Qt.darker(nav.dotColor, 1.7)
|
||||||
|
: Qt.darker(nav.dotColor, 1.4)
|
||||||
|
border.width: dotItem.isCurrent ? 2 : 1
|
||||||
|
opacity: dotItem.isCurrent || dotArea.containsMouse ? 1.0 : 0.55
|
||||||
|
|
||||||
Behavior on width { NumberAnimation { duration: 120 } }
|
Behavior on width { NumberAnimation { duration: 120 } }
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 120 } }
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,12 +170,18 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: nav.messageClicked(dotItem.modelData)
|
onClicked: nav.messageClicked(dotItem.msgIndex)
|
||||||
|
|
||||||
ToolTip.visible: containsMouse
|
QoAToolTip {
|
||||||
ToolTip.delay: 400
|
visible: dotArea.containsMouse
|
||||||
ToolTip.text: qsTr("Jump to message #%1").arg(dotItem.index + 1)
|
delay: 350
|
||||||
|
text: dotItem.preview.length > 0
|
||||||
|
? qsTr("#%1 · %2").arg(dotItem.index + 1).arg(dotItem.preview)
|
||||||
|
: qsTr("Jump to message #%1").arg(dotItem.index + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user