165 lines
4.5 KiB
QML
165 lines
4.5 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import ZShell.Config
|
|
import ZShell.Llm
|
|
import qs.Components
|
|
import qs.Services
|
|
|
|
MouseArea {
|
|
id: root
|
|
|
|
readonly property ChatGeneration current: modelData.activeGeneration
|
|
required property int index
|
|
readonly property bool isUser: modelData.role === ChatMessage.Role.User
|
|
required property ChatMessage modelData
|
|
property bool reasoningExpanded: false
|
|
property real savedContentY: -1
|
|
|
|
function handleReasoningToggle(expanded: bool): void {
|
|
const view = root.ListView.view;
|
|
if (!view)
|
|
return;
|
|
|
|
if (expanded) {
|
|
root.savedContentY = view.contentY;
|
|
root.reasoningExpanded = true;
|
|
} else {
|
|
root.reasoningExpanded = false;
|
|
if (root.savedContentY !== -1) {
|
|
restoreAnim.to = root.savedContentY;
|
|
restoreAnim.restart();
|
|
root.savedContentY = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: root.isUser ? 0 : Tokens.spacing.extraSmall
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: root.isUser ? Tokens.spacing.extraSmall : 0
|
|
hoverEnabled: true
|
|
// Explicit, synchronous height — sum of children, no Layout polish involved.
|
|
implicitHeight: reasoningLoader.implicitHeight + (reasoningLoader.active ? Tokens.spacing.medium : 0) + bubble.implicitHeight + actionsRow.implicitHeight + actionsRow.anchors.topMargin
|
|
preventStealing: true
|
|
|
|
Binding {
|
|
property: "contentY"
|
|
restoreMode: Binding.RestoreNone
|
|
target: root.ListView.view
|
|
value: root.y - Tokens.padding.large * 2
|
|
when: root.reasoningExpanded && root.ListView.view && ((root.y - Tokens.padding.large * 2) < root.ListView.view.contentY)
|
|
}
|
|
|
|
Anim {
|
|
id: restoreAnim
|
|
|
|
property: "contentY"
|
|
target: root.ListView.view
|
|
type: Anim.DefaultEffects
|
|
}
|
|
|
|
Loader {
|
|
id: reasoningLoader
|
|
|
|
readonly property bool shouldBeActive: root.current.reasoningActive || root.current.content === ""
|
|
|
|
active: !root.isUser
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Tokens.spacing.extraLarge
|
|
anchors.top: parent.top
|
|
|
|
sourceComponent: Reasoning {
|
|
current: root.current
|
|
|
|
onExpandedChanged: {
|
|
root.handleReasoningToggle(expanded);
|
|
}
|
|
}
|
|
}
|
|
|
|
CustomRect {
|
|
id: bubble
|
|
|
|
anchors.left: root.isUser ? undefined : parent.left
|
|
anchors.right: root.isUser ? parent.right : undefined
|
|
anchors.top: reasoningLoader.active ? reasoningLoader.bottom : parent.top
|
|
anchors.topMargin: reasoningLoader.active ? Tokens.spacing.medium : 0
|
|
color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
|
|
implicitHeight: root.current.content !== "" ? msgText.contentHeight + Tokens.padding.medium * 2 : 0
|
|
implicitWidth: Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge)
|
|
radius: Tokens.rounding.medium
|
|
visible: !root.current.reasoningActive
|
|
|
|
TextEditBase {
|
|
id: msgText
|
|
|
|
anchors.left: parent.left
|
|
anchors.margins: Tokens.padding.medium
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
animateCursor: false
|
|
color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
|
cursor.color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary
|
|
font.pointSize: Tokens.font.size.smaller
|
|
readOnly: true
|
|
selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4)
|
|
text: root.current.content
|
|
textFormat: CustomText.MarkdownText
|
|
wrapMode: Text.WordWrap
|
|
|
|
Keys.onPressed: event => {
|
|
if (event.key == Qt.Key_Return) {
|
|
if (!(event.modifiers & Qt.ShiftModifier)) {
|
|
readOnly = true;
|
|
event.accepted = true;
|
|
}
|
|
} else if (event.key == Qt.Key_Escape) {
|
|
text = root.current.content;
|
|
readOnly = true;
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
onEditingFinished: {
|
|
const old = root.current.content;
|
|
if (old !== text) {
|
|
root.modelData.edit(text);
|
|
|
|
if (root.isUser)
|
|
root.modelData.generate();
|
|
}
|
|
|
|
readOnly = true;
|
|
}
|
|
onReadOnlyChanged: {
|
|
if (readOnly) {
|
|
animateCursor = false;
|
|
root.forceActiveFocus();
|
|
textFormat = CustomText.MarkdownText;
|
|
} else {
|
|
var raw = root.current.content;
|
|
textFormat = CustomText.PlainText;
|
|
text = raw;
|
|
forceActiveFocus();
|
|
cursorPosition = text.length;
|
|
animateCursor = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Actions {
|
|
id: actionsRow
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
|
|
anchors.top: bubble.bottom
|
|
anchors.topMargin: Tokens.spacing.medium
|
|
current: root.current
|
|
edit: msgText
|
|
hovered: root.containsMouse
|
|
message: root.modelData
|
|
}
|
|
}
|