better support for opening chat in floating window
This commit is contained in:
@@ -12,7 +12,7 @@ ScrollBar {
|
|||||||
readonly property bool isHorizontal: flickable && flickable.ScrollBar.horizontal === root
|
readonly property bool isHorizontal: flickable && flickable.ScrollBar.horizontal === root
|
||||||
readonly property real axisSize: isHorizontal ? flickable.width : flickable.height
|
readonly property real axisSize: isHorizontal ? flickable.width : flickable.height
|
||||||
readonly property real axisContentSize: isHorizontal ? flickable.contentWidth : flickable.contentHeight
|
readonly property real axisContentSize: isHorizontal ? flickable.contentWidth : flickable.contentHeight
|
||||||
readonly property real axisContentPos: isHorizontal ? flickable.contentX : flickable.contentY
|
readonly property real axisContentPos: isHorizontal ? (flickable.contentX - flickable.originX) : (flickable.contentY - flickable.originY)
|
||||||
readonly property real axisLength: isHorizontal ? root.width : root.height
|
readonly property real axisLength: isHorizontal ? root.width : root.height
|
||||||
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
|
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
|
||||||
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
|
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
|
||||||
@@ -102,9 +102,9 @@ ScrollBar {
|
|||||||
|
|
||||||
var newPos = contentPosFromThumbStart(thumbStart);
|
var newPos = contentPosFromThumbStart(thumbStart);
|
||||||
if (root.isHorizontal)
|
if (root.isHorizontal)
|
||||||
root.flickable.contentX = newPos;
|
root.flickable.contentX = newPos + root.flickable.originX;
|
||||||
else
|
else
|
||||||
root.flickable.contentY = newPos;
|
root.flickable.contentY = newPos + root.flickable.originY;
|
||||||
}
|
}
|
||||||
|
|
||||||
function visualThumbStart() {
|
function visualThumbStart() {
|
||||||
|
|||||||
@@ -87,10 +87,6 @@ Item {
|
|||||||
|
|
||||||
signal requestClose
|
signal requestClose
|
||||||
|
|
||||||
function scrollToBottom(): void {
|
|
||||||
Qt.callLater(() => listLoader.item?.positionViewAtBeginning());
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(text: string): void {
|
function send(text: string): void {
|
||||||
if (text.trim() === "")
|
if (text.trim() === "")
|
||||||
return;
|
return;
|
||||||
@@ -129,11 +125,15 @@ Item {
|
|||||||
isRound: true
|
isRound: true
|
||||||
type: IconButton.Tonal
|
type: IconButton.Tonal
|
||||||
|
|
||||||
|
enabled: !ChatState.isWindow
|
||||||
|
visible: enabled
|
||||||
|
|
||||||
onClicked: root.requestClose()
|
onClicked: root.requestClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: ChatState.isWindow ? false : true
|
||||||
|
Layout.alignment: ChatState.isWindow ? Qt.AlignCenter : Qt.AlignLeft | Qt.AlignVCenter
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
font.pointSize: Tokens.font.size.larger
|
font.pointSize: Tokens.font.size.larger
|
||||||
text: qsTr(root.chatData.title)
|
text: qsTr(root.chatData.title)
|
||||||
@@ -151,21 +151,20 @@ Item {
|
|||||||
anchors.topMargin: Tokens.spacing.medium
|
anchors.topMargin: Tokens.spacing.medium
|
||||||
active: root.chatData && root.chatData.loaded
|
active: root.chatData && root.chatData.loaded
|
||||||
|
|
||||||
sourceComponent: ListView {
|
sourceComponent: VerticalFadeListView {
|
||||||
id: list
|
id: list
|
||||||
|
|
||||||
property bool userScrolledUp: false
|
property bool userScrolledUp: false
|
||||||
property real lastCHeight: 0.0
|
property real lastCHeight: 0.0
|
||||||
|
|
||||||
function scrollToBottom(): void {
|
function scrollToBottom(): void {
|
||||||
scrollAnim.to = 0;
|
|
||||||
scrollAnim.start();
|
scrollAnim.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheBuffer: height * 20
|
cacheBuffer: Math.max(height * 20, 0)
|
||||||
clip: true
|
clip: true
|
||||||
// fadeAmount: 0.05
|
fadeAmount: 0.05
|
||||||
// fadeThreshold: Tokens.padding.medium
|
fadeThreshold: Tokens.padding.medium
|
||||||
model: root.chatData.messagesModel
|
model: root.chatData.messagesModel
|
||||||
spacing: 0
|
spacing: 0
|
||||||
rotation: 180
|
rotation: 180
|
||||||
@@ -238,6 +237,7 @@ Item {
|
|||||||
property: "contentY"
|
property: "contentY"
|
||||||
target: list
|
target: list
|
||||||
type: Anim.DefaultEffects
|
type: Anim.DefaultEffects
|
||||||
|
to: list.originY
|
||||||
}
|
}
|
||||||
|
|
||||||
WheelInverter {
|
WheelInverter {
|
||||||
@@ -255,7 +255,7 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: Tokens.spacing.small
|
spacing: Tokens.spacing.small
|
||||||
visible: !root.chatData
|
visible: root.chatData.messageCount < 1
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick
|
|||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import ZShell.Llm
|
import ZShell.Llm
|
||||||
|
import qs.Effects
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
@@ -9,12 +10,13 @@ CustomClippingRect {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool expanded: false
|
property bool expanded: false
|
||||||
|
property bool highlighted: false
|
||||||
required property ChatSession modelData
|
required property ChatSession modelData
|
||||||
|
|
||||||
signal clicked(content: ChatSession)
|
signal clicked(content: ChatSession)
|
||||||
signal remove(content: ChatSession)
|
signal remove(content: ChatSession)
|
||||||
|
|
||||||
color: Colors.tPalette.m3surfaceContainer
|
color: highlighted ? Qt.alpha(Colors.palette.m3primaryContainer, 0.3) : Qt.alpha(Colors.tPalette.m3surfaceContainer, 0.2)
|
||||||
implicitHeight: {
|
implicitHeight: {
|
||||||
let h = 0;
|
let h = 0;
|
||||||
|
|
||||||
@@ -33,6 +35,20 @@ CustomClippingRect {
|
|||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
radius: Tokens.rounding.large
|
radius: Tokens.rounding.large
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: borderHighlight
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
border.width: root.highlighted ? 2 : 0
|
||||||
|
border.color: Colors.palette.m3secondary
|
||||||
|
radius: root.radius - border.width + 1
|
||||||
|
|
||||||
|
Behavior on border.width {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
enabled: titleText.readOnly
|
enabled: titleText.readOnly
|
||||||
@@ -49,6 +65,7 @@ CustomClippingRect {
|
|||||||
anchors.topMargin: Tokens.padding.large
|
anchors.topMargin: Tokens.padding.large
|
||||||
font.pointSize: Tokens.font.size.extraLarge
|
font.pointSize: Tokens.font.size.extraLarge
|
||||||
text: root.modelData.icon || "check"
|
text: root.modelData.icon || "check"
|
||||||
|
color: Colors.palette.m3onSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -85,31 +102,73 @@ CustomClippingRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditBase {
|
Item {
|
||||||
id: titleText
|
id: titleWrapper
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
|
anchors.right: parent.right
|
||||||
font.pointSize: Tokens.font.size.normal
|
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
|
||||||
text: root.modelData.title
|
|
||||||
readOnly: true
|
|
||||||
|
|
||||||
Behavior on anchors.leftMargin {
|
layer.enabled: true
|
||||||
Anim {}
|
layer.effect: OpacityMask {
|
||||||
}
|
maskSource: fadeMask
|
||||||
Behavior on anchors.topMargin {
|
|
||||||
Anim {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditingFinished: root.modelData.title = text
|
TextFieldBase {
|
||||||
onReadOnlyChanged: {
|
id: titleText
|
||||||
if (!readOnly) {
|
|
||||||
this.forceActiveFocus();
|
anchors.left: parent.left
|
||||||
cursorPosition = text.length;
|
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
|
||||||
} else {
|
anchors.top: parent.top
|
||||||
root.forceActiveFocus();
|
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
|
||||||
|
font.pointSize: Tokens.font.size.normal
|
||||||
|
text: root.modelData.title || qsTr("New chat")
|
||||||
|
readOnly: true
|
||||||
|
color: Colors.palette.m3onSurface
|
||||||
|
|
||||||
|
Behavior on anchors.leftMargin {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
Behavior on anchors.topMargin {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditingFinished: root.modelData.title = text
|
||||||
|
onReadOnlyChanged: {
|
||||||
|
if (!readOnly) {
|
||||||
|
this.forceActiveFocus();
|
||||||
|
cursorPosition = text.length;
|
||||||
|
} else {
|
||||||
|
root.forceActiveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: fadeMask
|
||||||
|
|
||||||
|
// anchors.top: titleText.top
|
||||||
|
// anchors.bottom: titleText.bottom
|
||||||
|
// anchors.left: titleText.left
|
||||||
|
// anchors.right: parent.right
|
||||||
|
anchors.fill: titleWrapper
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
color: Qt.rgba(1, 1, 1, 1.0)
|
||||||
|
position: 0.85
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
color: Qt.rgba(1, 1, 1, 0)
|
||||||
|
position: 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +210,7 @@ CustomClippingRect {
|
|||||||
id: timestamp
|
id: timestamp
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.top: titleText.bottom
|
anchors.top: title.bottom
|
||||||
anchors.topMargin: Tokens.spacing.extraSmall
|
anchors.topMargin: Tokens.spacing.extraSmall
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
font.pointSize: Tokens.font.size.small
|
font.pointSize: Tokens.font.size.small
|
||||||
@@ -188,7 +247,7 @@ CustomClippingRect {
|
|||||||
font.pointSize: Tokens.font.size.small
|
font.pointSize: Tokens.font.size.small
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
opacity: !root.expanded ? 1 : 0
|
opacity: !root.expanded ? 1 : 0
|
||||||
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? ""
|
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
|
||||||
|
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
Anim {}
|
Anim {}
|
||||||
@@ -213,7 +272,7 @@ CustomClippingRect {
|
|||||||
color: Colors.palette.m3onSurfaceVariant
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
maximumLineCount: 5
|
maximumLineCount: 5
|
||||||
opacity: root.expanded ? 1 : 0
|
opacity: root.expanded ? 1 : 0
|
||||||
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content ?? ""
|
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content ?? qsTr("No messages yet")
|
||||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
@@ -286,7 +345,7 @@ CustomClippingRect {
|
|||||||
implicitHeight: expandIcon.implicitHeight
|
implicitHeight: expandIcon.implicitHeight
|
||||||
implicitWidth: expandIcon.implicitHeight
|
implicitWidth: expandIcon.implicitHeight
|
||||||
radius: Tokens.rounding.full
|
radius: Tokens.rounding.full
|
||||||
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 3)
|
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
onClicked: root.expanded = !root.expanded
|
onClicked: root.expanded = !root.expanded
|
||||||
@@ -298,6 +357,7 @@ CustomClippingRect {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
anchors.verticalCenterOffset: root.expanded ? -1 : 1
|
anchors.verticalCenterOffset: root.expanded ? -1 : 1
|
||||||
rotation: root.expanded ? 180 : 0
|
rotation: root.expanded ? 180 : 0
|
||||||
|
color: Colors.palette.m3onSurface
|
||||||
text: "expand_more"
|
text: "expand_more"
|
||||||
|
|
||||||
Behavior on anchors.verticalCenterOffset {
|
Behavior on anchors.verticalCenterOffset {
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ Item {
|
|||||||
|
|
||||||
property alias list: list
|
property alias list: list
|
||||||
property alias model: list.model
|
property alias model: list.model
|
||||||
|
property bool highlight: false
|
||||||
|
|
||||||
signal deleteChatRequest(content: ChatSession)
|
signal deleteChatRequest(content: ChatSession)
|
||||||
signal loadChatRequest(content: ChatSession)
|
signal loadChatRequest(content: ChatSession, index: int)
|
||||||
signal newChatRequest
|
signal newChatRequest
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
@@ -40,16 +41,48 @@ Item {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: header.bottom
|
anchors.top: header.bottom
|
||||||
anchors.topMargin: Tokens.spacing.medium
|
anchors.topMargin: Tokens.spacing.medium
|
||||||
cacheBuffer: height * 2
|
|
||||||
clip: true
|
clip: true
|
||||||
spacing: Tokens.spacing.medium
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
add: Transition {
|
||||||
|
Anim {
|
||||||
|
from: list.width
|
||||||
|
property: "x"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remove: Transition {
|
||||||
|
Anim {
|
||||||
|
to: list.width
|
||||||
|
property: "x"
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
to: 0
|
||||||
|
property: "opacity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
displaced: Transition {
|
||||||
|
Anim {
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
move: Transition {
|
||||||
|
Anim {
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: ChatDelegate {
|
delegate: ChatDelegate {
|
||||||
id: chat
|
id: chat
|
||||||
|
|
||||||
implicitWidth: ListView.view.width
|
required property int index
|
||||||
|
|
||||||
onClicked: content => root.loadChatRequest(content)
|
implicitWidth: ListView.view.width
|
||||||
|
highlighted: root.highlight && ChatState.chatSession === modelData
|
||||||
|
|
||||||
|
onHighlightedChanged: if (highlighted)
|
||||||
|
list.currentIndex = index
|
||||||
|
onClicked: content => root.loadChatRequest(content, index)
|
||||||
onRemove: content => root.deleteChatRequest(content)
|
onRemove: content => root.deleteChatRequest(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,12 +91,12 @@ Item {
|
|||||||
id: newChatBtn
|
id: newChatBtn
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: Tokens.padding.small
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Tokens.padding.small
|
||||||
|
padding: 8
|
||||||
font.pointSize: Math.round(18 * 1.2)
|
font.pointSize: Math.round(18 * 1.2)
|
||||||
icon: "add"
|
icon: "add"
|
||||||
padding: 8
|
isRound: true
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.newChatRequest();
|
root.newChatRequest();
|
||||||
|
|||||||
@@ -18,45 +18,35 @@ Item {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (ChatState.inChat) {
|
if (ChatState.inChat) {
|
||||||
stackLoader.item?.push(chatList);
|
stack.push(chatList);
|
||||||
stackLoader.item?.push(chatContent, {
|
stack.push(chatContent, {
|
||||||
"chatData": ChatState.chatSession
|
"chatData": ChatState.chatSession
|
||||||
});
|
});
|
||||||
} else
|
} else
|
||||||
stackLoader.item?.push(chatList);
|
stack.push(chatList);
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
StackView {
|
||||||
id: stackLoader
|
id: stack
|
||||||
|
|
||||||
active: !ChatState.isWindow || root.width <= (ChatState.screen.width / 4)
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
sourceComponent: StackView {
|
IconButton {
|
||||||
id: stack
|
anchors.left: parent.left
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
icon: "open_in_new"
|
||||||
|
anchors.margins: Tokens.padding.small
|
||||||
|
isRound: true
|
||||||
|
padding: 8
|
||||||
|
font.pointSize: Math.round(18 * 1.2)
|
||||||
|
visible: !ChatState.isWindow
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Detach.create();
|
||||||
|
Visibilities.getForActive().sidebar = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: sidebarViewLoader
|
|
||||||
|
|
||||||
active: ChatState.isWindow && root.width > (ChatState.screen.width / 4)
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
sourceComponent: SidebarView {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IconButton {
|
|
||||||
// anchors.right: parent.right
|
|
||||||
// icon: "close"
|
|
||||||
// visible: !ChatState.isWindow
|
|
||||||
//
|
|
||||||
// onClicked: {
|
|
||||||
// Detach.create();
|
|
||||||
// Visibilities.getForActive().sidebar = false;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: chatList
|
id: chatList
|
||||||
|
|
||||||
@@ -68,8 +58,8 @@ Item {
|
|||||||
onDeleteChatRequest: chat => {
|
onDeleteChatRequest: chat => {
|
||||||
Chat.chats.remove(chat);
|
Chat.chats.remove(chat);
|
||||||
}
|
}
|
||||||
onLoadChatRequest: chat => {
|
onLoadChatRequest: (chat, index) => {
|
||||||
stackLoader.item?.push(chatContent, {
|
stack.push(chatContent, {
|
||||||
"chatData": chat
|
"chatData": chat
|
||||||
});
|
});
|
||||||
ChatState.inChat = true;
|
ChatState.inChat = true;
|
||||||
@@ -77,7 +67,7 @@ Item {
|
|||||||
}
|
}
|
||||||
onNewChatRequest: {
|
onNewChatRequest: {
|
||||||
const data = Chat.chats.insert();
|
const data = Chat.chats.insert();
|
||||||
stackLoader.item?.push(chatContent, {
|
stack.push(chatContent, {
|
||||||
"chatData": data
|
"chatData": data
|
||||||
});
|
});
|
||||||
ChatState.inChat = true;
|
ChatState.inChat = true;
|
||||||
@@ -91,7 +81,7 @@ Item {
|
|||||||
|
|
||||||
ChatContent {
|
ChatContent {
|
||||||
onRequestClose: {
|
onRequestClose: {
|
||||||
stackLoader.item?.pop();
|
stack.pop();
|
||||||
ChatState.inChat = false;
|
ChatState.inChat = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ChatSession chatSession
|
property ChatSession chatSession
|
||||||
|
readonly property string title: chatSession ? chatSession.title : "Chat"
|
||||||
|
property int currentIdx: -1
|
||||||
property bool inChat: false
|
property bool inChat: false
|
||||||
property bool isWindow: false
|
property bool isWindow: false
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import QtQuick
|
||||||
|
import ZShell.Llm
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
import qs.Modules.Notifications.Sidebar.Chat
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int animOff
|
||||||
|
property Item currentItem
|
||||||
|
property bool loading: false
|
||||||
|
property int lastIdx: -1
|
||||||
|
|
||||||
|
readonly property Component conversationComp: ChatContent {}
|
||||||
|
|
||||||
|
function loadConversation(chat): void {
|
||||||
|
if (currentItem) {
|
||||||
|
currentItem.destroy();
|
||||||
|
currentItem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chat)
|
||||||
|
return;
|
||||||
|
|
||||||
|
root.loading = true;
|
||||||
|
|
||||||
|
const incubator = root.conversationComp.incubateObject(container, {
|
||||||
|
chatData: chat
|
||||||
|
});
|
||||||
|
|
||||||
|
const attach = () => {
|
||||||
|
incubator.object.anchors.fill = container;
|
||||||
|
currentItem = incubator.object;
|
||||||
|
root.loading = false;
|
||||||
|
enterAnim.start();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (incubator.status === Component.Ready)
|
||||||
|
attach();
|
||||||
|
else
|
||||||
|
incubator.onStatusChanged = status => {
|
||||||
|
if (status === Component.Ready)
|
||||||
|
attach();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: container
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
layer.enabled: opacity < 1
|
||||||
|
objectName: "ConversationContainer"
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (ChatState.chatSession)
|
||||||
|
root.loadConversation(ChatState.chatSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingIndicator {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitSize: Tokens.font.size.extraLarge * 4
|
||||||
|
opacity: root.loading ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onChatSessionChanged(): void {
|
||||||
|
exitAnim.complete();
|
||||||
|
enterAnim.complete();
|
||||||
|
root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
|
||||||
|
root.lastIdx = ChatState.currentIdx;
|
||||||
|
exitAnim.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: ChatState
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: exitAnim
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
property: "opacity"
|
||||||
|
target: container
|
||||||
|
to: 0
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptAction {
|
||||||
|
script: root.loadConversation(ChatState.chatSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: enterAnim
|
||||||
|
|
||||||
|
PropertyAction {
|
||||||
|
property: "topMargin"
|
||||||
|
target: container.anchors
|
||||||
|
value: root.animOff
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyAction {
|
||||||
|
property: "bottomMargin"
|
||||||
|
target: container.anchors
|
||||||
|
value: -root.animOff
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
Anim {
|
||||||
|
from: 0
|
||||||
|
property: "opacity"
|
||||||
|
target: container
|
||||||
|
to: 1
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
properties: "topMargin,bottomMargin"
|
||||||
|
target: container.anchors
|
||||||
|
to: 0
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,28 +5,28 @@ import qs.Components
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
spacing: Tokens.spacing.small
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
color: Colors.tPalette.m3outlineVariant
|
color: Colors.tPalette.m3outlineVariant
|
||||||
font.pointSize: 36
|
font.pointSize: 36
|
||||||
text: "chat"
|
text: "chat"
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
color: Colors.tPalette.m3onSurfaceVariant
|
color: Colors.tPalette.m3onSurfaceVariant
|
||||||
text: qsTr("No messages yet")
|
text: qsTr("Send a message to get started")
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ Item {
|
|||||||
property color mathColor: Colors.palette.m3onSurface
|
property color mathColor: Colors.palette.m3onSurface
|
||||||
property real fontSize: Tokens.font.size.large
|
property real fontSize: Tokens.font.size.large
|
||||||
|
|
||||||
implicitWidth: Math.max(fallbackText.implicitWidth, root.width)
|
|
||||||
implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2
|
implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2
|
||||||
|
|
||||||
LlmMathText {
|
LlmMathText {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ MouseArea {
|
|||||||
required property ChatMessage modelData
|
required property ChatMessage modelData
|
||||||
property bool reasoningExpanded: false
|
property bool reasoningExpanded: false
|
||||||
readonly property var blocks: blockify(current.segments)
|
readonly property var blocks: blockify(current.segments)
|
||||||
property real savedContentY: -1
|
property real savedOffset: -1
|
||||||
|
property bool stickActive: false
|
||||||
|
|
||||||
function handleReasoningToggle(expanded: bool): void {
|
function handleReasoningToggle(expanded: bool): void {
|
||||||
const view = root.ListView.view;
|
const view = root.ListView.view;
|
||||||
@@ -25,14 +26,16 @@ MouseArea {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
root.savedContentY = view.contentY;
|
root.savedOffset = view.contentY - root.y;
|
||||||
root.reasoningExpanded = true;
|
root.reasoningExpanded = true;
|
||||||
} else {
|
} else {
|
||||||
root.reasoningExpanded = false;
|
root.reasoningExpanded = false;
|
||||||
if (root.savedContentY !== -1) {
|
root.stickActive = false;
|
||||||
restoreAnim.to = root.savedContentY;
|
settleTimer.stop();
|
||||||
|
if (!isNaN(root.savedOffset)) {
|
||||||
|
restoreAnim.to = root.y + root.savedOffset;
|
||||||
restoreAnim.restart();
|
restoreAnim.restart();
|
||||||
root.savedContentY = -1;
|
root.savedOffset = NaN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,6 +72,30 @@ MouseArea {
|
|||||||
preventStealing: false
|
preventStealing: false
|
||||||
implicitHeight: layout.implicitHeight + Tokens.padding.medium
|
implicitHeight: layout.implicitHeight + Tokens.padding.medium
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: settleTimer
|
||||||
|
interval: 16
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
root.stickActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onImplicitHeightChanged: {
|
||||||
|
if (root.reasoningExpanded) {
|
||||||
|
root.stickActive = true;
|
||||||
|
settleTimer.restart();
|
||||||
|
|
||||||
|
const view = root.ListView.view;
|
||||||
|
if (!view)
|
||||||
|
return;
|
||||||
|
const bottomEdge = root.y + root.height + Tokens.padding.large * 2;
|
||||||
|
const viewportBottom = view.contentY + view.height;
|
||||||
|
if (bottomEdge > viewportBottom)
|
||||||
|
view.contentY = bottomEdge - view.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on current {
|
Behavior on current {
|
||||||
id: changeAnim
|
id: changeAnim
|
||||||
|
|
||||||
@@ -119,14 +146,6 @@ MouseArea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Binding {
|
|
||||||
property: "contentY"
|
|
||||||
restoreMode: Binding.RestoreNone
|
|
||||||
target: root.ListView.view
|
|
||||||
value: root.y + root.height + Tokens.padding.large * 2 - root.ListView.view.height
|
|
||||||
when: root.reasoningExpanded && root.ListView.view && (root.y + root.height + Tokens.padding.large * 2 > root.ListView.view.contentY + root.ListView.view.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
id: restoreAnim
|
id: restoreAnim
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Singleton {
|
|||||||
minimumSize.height: Config.sidebar.sizes.width
|
minimumSize.height: Config.sidebar.sizes.width
|
||||||
minimumSize.width: Config.sidebar.sizes.width
|
minimumSize.width: Config.sidebar.sizes.width
|
||||||
surfaceFormat.opaque: false
|
surfaceFormat.opaque: false
|
||||||
title: qsTr("ZShell - Chat")
|
title: qsTr("ZShell - %1").arg(ChatState.title)
|
||||||
|
|
||||||
Component.onCompleted: ChatState.screen = screen
|
Component.onCompleted: ChatState.screen = screen
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
@@ -41,7 +41,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatPanel {
|
SidebarView {
|
||||||
id: chat
|
id: chat
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -2,90 +2,109 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import ZShell.Llm
|
import ZShell.Llm
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Item {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property int breakpoint: 700
|
property int breakpoint: 700
|
||||||
property alias conversationModel: sidebar.model
|
property alias conversationModel: sidebar.model
|
||||||
property var currentConversation: null // whatever model item is "open"
|
|
||||||
|
|
||||||
readonly property bool isWide: root.width >= root.breakpoint
|
readonly property bool isWide: root.width >= root.breakpoint
|
||||||
property bool narrowShowsSidebar: true
|
property bool narrowShowsSidebar: true
|
||||||
|
|
||||||
function closeConversation() {
|
function openConversation(conv: ChatSession, index: int): void {
|
||||||
narrowShowsSidebar = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openConversation(conv) {
|
|
||||||
currentConversation = conv;
|
|
||||||
narrowShowsSidebar = false; // in narrow mode, jump to the chat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatList {
|
ChatList {
|
||||||
id: sidebar
|
id: sidebar
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
Layout.fillHeight: true
|
||||||
anchors.left: parent.left
|
Layout.margins: Tokens.padding.medium
|
||||||
anchors.right: root.isWide ? undefined : parent.right
|
Layout.rightMargin: Tokens.spacing.medium
|
||||||
anchors.top: parent.top
|
Layout.maximumWidth: Config.sidebar.sizes.width
|
||||||
implicitWidth: 20
|
Layout.preferredWidth: root.width / 4
|
||||||
list.currentIndex: -1
|
Layout.minimumWidth: Config.sidebar.sizes.width / 2
|
||||||
list.highlightFollowsCurrentItem: false
|
highlight: true
|
||||||
|
|
||||||
Behavior on anchors.right {
|
|
||||||
AnchorAnim {}
|
|
||||||
}
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
list.highlight: CustomRect {
|
|
||||||
color: Colors.palette.m3primary
|
|
||||||
implicitHeight: sidebar.list.currentItem?.implicitHeight ?? 0
|
|
||||||
implicitWidth: sidebar.list.width
|
|
||||||
radius: Tokens.rounding.medium
|
|
||||||
x: sidebar.list.currentItem?.chat.x ?? 0
|
|
||||||
y: sidebar.list.currentItem?.y ?? 0
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
Anim {
|
|
||||||
duration: Tokens.anim.durations.small
|
|
||||||
easing: Tokens.anim.expressiveDefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: Chat.chats.values
|
values: Chat.chats.values
|
||||||
}
|
}
|
||||||
|
|
||||||
anchors.onRightChanged: {
|
|
||||||
if (anchors.right === undefined)
|
|
||||||
implicitWidth = 20;
|
|
||||||
}
|
|
||||||
onLoadChatRequest: (chat, index) => {
|
onLoadChatRequest: (chat, index) => {
|
||||||
sidebar.list.currentIndex = index;
|
ChatState.currentIdx = index;
|
||||||
root.openConversation(chat);
|
ChatState.chatSession = chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteChatRequest: chat => {
|
||||||
|
Chat.chats.remove(chat);
|
||||||
|
ChatState.currentIdx = -1;
|
||||||
|
ChatState.chatSession = null;
|
||||||
|
}
|
||||||
|
onNewChatRequest: {
|
||||||
|
const data = Chat.chats.insert();
|
||||||
|
ChatState.currentIdx = 0;
|
||||||
|
ChatState.chatSession = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomClippingWrapperRect {
|
Item {
|
||||||
anchors.bottom: parent.bottom
|
Layout.margins: Tokens.padding.extraLarge
|
||||||
anchors.left: sidebar.right
|
Layout.leftMargin: Tokens.spacing.extraLarge
|
||||||
anchors.right: parent.right
|
Layout.topMargin: Tokens.padding.large
|
||||||
anchors.top: parent.top
|
Layout.preferredWidth: Config.sidebar.sizes.width * 2
|
||||||
child: conversationView
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
ChatHost {
|
||||||
|
id: convHost
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
implicitWidth: Math.min(parent.width, 800)
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
onImplicitWidthChanged: console.log(implicitWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: noChatLayout
|
||||||
|
anchors.centerIn: parent
|
||||||
|
opacity: ChatState.chatSession ? 0 : 1
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
text: "chat_bubble_off"
|
||||||
|
font.pointSize: Tokens.font.size.extraLarge
|
||||||
|
color: Colors.tPalette.m3onSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
text: qsTr("Start or create a chat")
|
||||||
|
color: Colors.tPalette.m3onSurfaceVariant
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatContent {
|
Component {
|
||||||
id: conversationView
|
id: conversationView
|
||||||
|
|
||||||
anchors.fill: root
|
ChatContent {}
|
||||||
anchors.leftMargin: root.isWide ? Config.sidebar.sizes.width / 2 : 0
|
|
||||||
chatData: root.currentConversation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,9 +178,6 @@ Item {
|
|||||||
sourceComponent: MouseArea {
|
sourceComponent: MouseArea {
|
||||||
onWheel: event => event.accepted = true
|
onWheel: event => event.accepted = true
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
cursorShape: undefined
|
|
||||||
enabled: dialogContent.opacity > 0
|
|
||||||
clip: true
|
clip: true
|
||||||
implicitWidth: innerLayout.implicitWidth + innerLayout.anchors.margins * 2
|
implicitWidth: innerLayout.implicitWidth + innerLayout.anchors.margins * 2
|
||||||
implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins + innerLayout.anchors.bottomMargin
|
implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins + innerLayout.anchors.bottomMargin
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ ConnectedRect {
|
|||||||
property string iconText: "open_in_new"
|
property string iconText: "open_in_new"
|
||||||
property int verticalPadding: Tokens.padding.small
|
property int verticalPadding: Tokens.padding.small
|
||||||
|
|
||||||
signal clicked
|
signal clicked(event: MouseEvent)
|
||||||
signal toggled(checked: bool)
|
signal toggled(checked: bool)
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -56,7 +56,7 @@ ConnectedRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
onClicked: root.clicked()
|
onClicked: e => root.clicked(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomSwitch {
|
CustomSwitch {
|
||||||
|
|||||||
Reference in New Issue
Block a user