add removal of chat sessions, auto title + icon, proper stick-to-bottom as response is streamed

This commit is contained in:
2026-08-20 17:40:33 +02:00
parent 47bab21e22
commit 9657e5092a
12 changed files with 776 additions and 273 deletions
@@ -17,10 +17,6 @@ Item {
signal requestClose
function atBottom(): bool {
return list.contentHeight - list.height - list.contentY < 48;
}
function send(): void {
if (Chat.busy || input.text.trim() === "")
return;
@@ -29,30 +25,62 @@ Item {
input.text = "";
}
RowLayout {
id: header
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Tokens.spacing.large
IconButton {
icon: "arrow_back"
inactiveColor: Colors.tPalette.m3surfaceContainerHigh
inactiveOnColor: Colors.palette.m3onSurfaceVariant
isRound: true
type: IconButton.Tonal
onClicked: root.requestClose()
}
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Tokens.font.size.larger
text: qsTr(root.chatData.title)
}
}
VerticalFadeListView {
id: list
readonly property real bottomThreshold: Tokens.spacing.extraSmall
property bool stickToBottom: true
function scrollToEnd(animated: bool): void {
const target = Math.max(0, list.contentHeight - list.height);
if (!animated) {
scrollAnim.stop();
list.contentY = target;
return;
}
scrollAnim.to = target;
scrollAnim.restart();
}
anchors.bottom: inputRow.top
anchors.bottomMargin: Tokens.spacing.medium
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.top: header.bottom
cacheBuffer: height * 20
clip: true
currentIndex: messages.values.length - 1
fadeAmount: 0.1
highlightFollowsCurrentItem: true
highlightRangeMode: ListView.ApplyRange
preferredHighlightEnd: list.height
spacing: Tokens.spacing.medium
CustomScrollBar.vertical: CustomScrollBar {
flickable: list
}
Behavior on contentY {
Anim {
}
}
delegate: ColumnLayout {
id: messageRow
@@ -176,6 +204,7 @@ Item {
anchors.top: parent.top
color: messageRow.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
text: messageRow.modelData.content
textFormat: CustomText.MarkdownText
wrapMode: Text.WordWrap
}
@@ -204,7 +233,23 @@ Item {
values: root.chatData.messages
}
Component.onCompleted: positionViewAtEnd()
Component.onCompleted: scrollToEnd(false)
onContentHeightChanged: {
if (list.stickToBottom)
list.scrollToEnd(false);
}
onMovementEnded: {
list.stickToBottom = (list.contentY >= list.contentHeight - list.height - list.bottomThreshold);
}
onMovementStarted: list.stickToBottom = false
Anim {
id: scrollAnim
property: "contentY"
target: list
type: Anim.DefaultEffects
}
}
ColumnLayout {
@@ -236,6 +281,31 @@ Item {
}
}
IconButton {
id: scrollToBottom
anchors.bottom: inputRow.top
anchors.bottomMargin: Tokens.spacing.extraLarge
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: Tokens.font.size.large
icon: "arrow_downward"
isRound: true
padding: Tokens.padding.extraSmall
scale: list.visibleArea.yPosition + list.visibleArea.heightRatio < 1 ? 1 : 0
type: IconButton.Filled
visible: scale > 0
Behavior on scale {
Anim {
}
}
onClicked: {
list.stickToBottom = true;
list.scrollToEnd(true);
}
}
RowLayout {
id: inputRow
@@ -258,23 +328,12 @@ Item {
id: actionBtn
enabled: Chat.busy || input.text.trim() !== ""
font.pointSize: 20
font.pointSize: Tokens.font.size.normal * 2
icon: Chat.busy ? "stop" : "send"
isRound: true
type: IconButton.Filled
onClicked: Chat.busy ? Chat.stop() : root.send()
}
}
IconButton {
anchors.left: parent.left
anchors.top: parent.top
icon: "arrow_back"
inactiveColor: Colors.tPalette.m3surfaceContainerHigh
inactiveOnColor: Colors.palette.m3onSurfaceVariant
isRound: true
type: IconButton.Tonal
onClicked: root.requestClose()
}
}