add removal of chat sessions, auto title + icon, proper stick-to-bottom as response is streamed
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import ZShell.Llm
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
property alias layout: layout
|
||||
required property ChatSession modelData
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.bottomMargin
|
||||
|
||||
MaterialIcon {
|
||||
id: chatIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
text: root.modelData.icon || "check"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Tokens.padding.small
|
||||
anchors.left: chatIcon.right
|
||||
anchors.leftMargin: Tokens.padding.medium
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.medium
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Tokens.padding.small
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
CustomRect {
|
||||
id: titleContainer
|
||||
|
||||
readonly property bool enoughSpace: width > title.implicitWidth + timeSep.implicitWidth + timeSep.anchors.leftMargin + timestamp.implicitWidth + timestamp.anchors.leftMargin
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: enoughSpace ? title.implicitHeight : title.implicitHeight + timestamp.implicitHeight + timestamp.anchors.topMargin
|
||||
|
||||
CustomText {
|
||||
id: title
|
||||
|
||||
text: root.modelData.title
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: timeSep
|
||||
|
||||
anchors.left: title.right
|
||||
anchors.leftMargin: Tokens.spacing.small
|
||||
anchors.verticalCenter: title.verticalCenter
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: "•"
|
||||
visible: titleContainer.enoughSpace
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: timestamp
|
||||
|
||||
anchors.left: titleContainer.enoughSpace ? timeSep.right : parent.left
|
||||
anchors.leftMargin: titleContainer.enoughSpace ? Tokens.spacing.small : 0
|
||||
anchors.top: titleContainer.enoughSpace ? undefined : title.bottom
|
||||
anchors.topMargin: Tokens.spacing.extraSmall
|
||||
anchors.verticalCenter: titleContainer.enoughSpace ? title.verticalCenter : undefined
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: root.modelData.updatedAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP")
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.small
|
||||
maximumLineCount: 1
|
||||
text: root.modelData.messages[root.modelData.messages.length - 1]?.content.replace(/\s+/g, " ").trim() ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,55 +13,165 @@ Item {
|
||||
|
||||
property alias model: list.model
|
||||
|
||||
signal deleteChatRequest(content: ChatSession)
|
||||
signal loadChatRequest(content: ChatSession)
|
||||
signal newChatRequest
|
||||
|
||||
CustomText {
|
||||
id: header
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.extraSmall
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: Colors.palette.m3outline
|
||||
font.family: "CaskaydiaCove NF"
|
||||
font.pointSize: 13
|
||||
font.weight: 500
|
||||
text: qsTr("%1 Chat%2").arg(list.count).arg(list.count > 1 ? "s" : "")
|
||||
}
|
||||
|
||||
CustomListView {
|
||||
id: list
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
cacheBuffer: height * 2
|
||||
clip: true
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
delegate: CustomRect {
|
||||
id: chatItem
|
||||
delegate: Component {
|
||||
Item {
|
||||
id: chatItem
|
||||
|
||||
required property var modelData
|
||||
property bool closing: false
|
||||
property bool editVisible: false
|
||||
required property var modelData
|
||||
property real startY: 0.0
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.bottomMargin
|
||||
implicitWidth: ListView.view.width
|
||||
radius: Tokens.rounding.medium
|
||||
anchors.fill: undefined
|
||||
implicitHeight: chat.layout.implicitHeight + chat.layout.anchors.topMargin + chat.layout.anchors.bottomMargin
|
||||
implicitWidth: ListView.view.width
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
onEditVisibleChanged: if (!editVisible)
|
||||
chat.x = 0
|
||||
|
||||
anchors.bottomMargin: Tokens.padding.small
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Tokens.padding.medium
|
||||
anchors.rightMargin: Tokens.padding.medium
|
||||
anchors.topMargin: Tokens.padding.small
|
||||
spacing: 0
|
||||
ParallelAnimation {
|
||||
id: removeAnim
|
||||
|
||||
CustomText {
|
||||
id: title
|
||||
onFinished: root.deleteChatRequest(chatItem.modelData)
|
||||
onStarted: chatItem.closing = true
|
||||
|
||||
text: chatItem.modelData.title
|
||||
Anim {
|
||||
property: "implicitHeight"
|
||||
target: chatItem
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "x"
|
||||
target: chat
|
||||
to: -chat.implicitWidth
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
target: chatItem
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: timestamp
|
||||
ChatDelegate {
|
||||
id: chat
|
||||
|
||||
color: Colors.palette.m3outline
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: chatItem.modelData.updatedAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP")
|
||||
implicitWidth: chatItem.implicitWidth
|
||||
modelData: chatItem.modelData
|
||||
radius: Tokens.rounding.medium
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.PointingHandCursor
|
||||
drag.axis: Drag.XAxis
|
||||
drag.target: chat
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
|
||||
onClicked: {
|
||||
if (chatItem.editVisible)
|
||||
chatItem.editVisible = false;
|
||||
else
|
||||
root.loadChatRequest(chatItem.modelData);
|
||||
}
|
||||
onPressed: event => {
|
||||
chatItem.startY = event.y;
|
||||
}
|
||||
onReleased: event => {
|
||||
if (chat.x > -(editTools.implicitWidth + Tokens.padding.extraSmall)) {
|
||||
chat.x = 0;
|
||||
} else {
|
||||
chatItem.editVisible = true;
|
||||
chat.x = -(editTools.implicitWidth + Tokens.spacing.medium + Tokens.padding.extraSmall);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
onClicked: {
|
||||
root.loadChatRequest(chatItem.modelData);
|
||||
CustomClippingRect {
|
||||
anchors.left: chat.right
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.extraSmall
|
||||
anchors.verticalCenter: chat.verticalCenter
|
||||
implicitHeight: editTools.implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: editTools
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
IconButton {
|
||||
id: deleteChatBtn
|
||||
|
||||
font.pointSize: Tokens.font.size.large
|
||||
icon: "close"
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
transform: [
|
||||
Scale {
|
||||
property bool shouldBeActive: (chat.x < -deleteChatBtn.implicitWidth) && !chatItem.closing
|
||||
|
||||
origin.x: deleteChatBtn.implicitWidth / 2
|
||||
origin.y: deleteChatBtn.implicitWidth / 2
|
||||
xScale: shouldBeActive ? 1 : 0
|
||||
yScale: shouldBeActive ? 1 : 0
|
||||
|
||||
Behavior on xScale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on yScale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
onClicked: {
|
||||
removeAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ Item {
|
||||
values: Chat.chats.values
|
||||
}
|
||||
|
||||
onDeleteChatRequest: chat => {
|
||||
Chat.chats.remove(chat);
|
||||
}
|
||||
onLoadChatRequest: chat => {
|
||||
stack.push(chatContent, {
|
||||
"chatData": chat
|
||||
|
||||
Reference in New Issue
Block a user