aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant/Card.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/assistant/Card.qml')
-rw-r--r--desktop/assistant/Card.qml170
1 files changed, 153 insertions, 17 deletions
diff --git a/desktop/assistant/Card.qml b/desktop/assistant/Card.qml
index ab80d1b..477f430 100644
--- a/desktop/assistant/Card.qml
+++ b/desktop/assistant/Card.qml
@@ -11,14 +11,29 @@
import QtQuick
import ".."
+import "Files.js" as Files
-// The card: header, transcript, input, and the history panel over the
-// transcript. All state lives in the canvas (Assistant.qml), which outlives
-// this card, so closing and reopening loses nothing.
+// The card in its three states (Assistant.qml, mode): entry is the input
+// line alone; file adds the staged chips and what can be done with them;
+// chat adds the header and the transcript. A panel (history, picker) takes
+// the full height whatever the state. All state lives in the canvas, which
+// outlives this card, so closing and reopening loses nothing.
Rectangle {
id: card
property var canvas
property var turnModel
+ property real fullHeight: 600 // the chat state's height, set by the window
+
+ readonly property string mode: card.canvas.mode
+ readonly property bool panel: card.canvas.historyOpen || card.canvas.pickerOpen
+ readonly property bool tall: card.mode === "chat" || card.panel
+
+ // Bottom-up: the input, then the actions (file state), then the chips.
+ height: card.tall ? card.fullHeight
+ : footer.height + 28 + (chips.visible ? chips.height + 10 : 0)
+ + (actions.visible ? actions.height + 10 : 0)
+ Behavior on height { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
+ clip: true
color: Qt.alpha(Theme.base, 0.82)
radius: 14
@@ -26,7 +41,7 @@ Rectangle {
border.color: Qt.alpha(Theme.text, 0.12)
// Keys reach a focused item, never the window (AGENTS.md); the card is
- // the ancestor of both text fields, so their Escape bubbles here.
+ // the ancestor of every text field, so their Escape bubbles here.
Keys.onEscapePressed: card.canvas.back()
// Clicks on the card must not reach the close-on-click catcher behind it.
@@ -34,7 +49,7 @@ Rectangle {
Component.onCompleted: input.forceActiveFocus()
- // Focus falls out of the card when its holder goes: the history field
+ // Focus falls out of the card when its holder goes: a panel's field
// hiding, or a streaming reply's text rebuilt under a click. With the
// keyboard grabbed, nothing would then reach Escape. Give it back.
Connections {
@@ -47,12 +62,15 @@ Rectangle {
Connections {
target: card.canvas
function onHistoryOpenChanged() { if (!card.canvas.historyOpen) input.forceActiveFocus(); }
+ function onPickerOpenChanged() { if (!card.canvas.pickerOpen) input.forceActiveFocus(); }
+ function onChattingChanged() { input.forceActiveFocus(); }
}
- // --- header ---
+ // --- header (chat) ---
Item {
id: header
+ visible: card.mode === "chat"
anchors { top: parent.top; left: parent.left; right: parent.right; margins: 14 }
height: 32
@@ -85,17 +103,19 @@ Rectangle {
Button {
text: "New"
enabled: card.canvas.connected
- onClicked: { card.canvas.send({ type: "new" }); card.canvas.historyOpen = false; input.forceActiveFocus(); }
+ onClicked: { card.canvas.fresh(); card.canvas.historyOpen = false; input.forceActiveFocus(); }
}
}
}
- // --- transcript ---
+ // --- transcript (chat) ---
ListView {
id: list
+ visible: card.mode === "chat"
anchors {
- top: header.bottom; topMargin: 8; bottom: footer.top; bottomMargin: 8
+ top: header.bottom; topMargin: 8
+ bottom: chips.visible ? chips.top : footer.top; bottomMargin: 8
left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14
}
clip: true
@@ -125,6 +145,78 @@ Rectangle {
}
}
+ // --- staged files (file, chat) ---
+
+ Flow {
+ id: chips
+ visible: card.canvas.staged.length > 0
+ anchors {
+ bottom: actions.visible ? actions.top : footer.top; bottomMargin: 10
+ left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14
+ }
+ spacing: 6
+
+ Repeater {
+ model: card.canvas.staged
+ Rectangle {
+ id: chip
+ required property var modelData
+ required property int index
+ readonly property bool ok: Files.chats(modelData.kind)
+ width: label.implicitWidth + cross.implicitWidth + 24
+ height: label.implicitHeight + 10
+ radius: height / 2
+ color: Qt.alpha(Theme.surface, 0.9)
+ border.width: 1
+ border.color: Qt.alpha(chip.ok ? Theme.text : Theme.red, 0.25)
+
+ Text {
+ id: label
+ anchors { left: parent.left; leftMargin: 10; verticalCenter: parent.verticalCenter }
+ text: `${chip.modelData.kind} · ${chip.modelData.name}`
+ color: chip.ok ? Theme.text : Theme.red
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+ Text {
+ id: cross
+ anchors { right: parent.right; rightMargin: 9; verticalCenter: parent.verticalCenter }
+ text: "×"
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
+ MouseArea {
+ anchors { fill: parent; margins: -4 }
+ cursorShape: Qt.PointingHandCursor
+ onClicked: card.canvas.unstage(chip.index)
+ }
+ }
+ }
+ }
+ }
+
+ // --- what to do with them (file) ---
+ // Piece 1 offers Chat about it; the Ask and Do rows (model prompts and
+ // local operations) are their own specs and land here.
+
+ Row {
+ id: actions
+ visible: card.mode === "file"
+ anchors { bottom: footer.top; bottomMargin: 10; left: parent.left; leftMargin: 14 }
+ spacing: 10
+
+ Button {
+ text: "Chat about it"
+ enabled: card.canvas.connected && !card.canvas.blocked
+ onClicked: card.canvas.chatting = true
+ }
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ visible: card.canvas.blocked
+ text: "chat supports text, images and PDF"
+ color: Theme.red
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+ }
+
// --- input ---
Item {
@@ -143,9 +235,24 @@ Rectangle {
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
}
+ // The paperclip, nf-fa-paperclip, checked in Inconsolata Nerd Font.
+ Button {
+ id: attach
+ visible: card.mode !== "entry"
+ width: visible ? implicitWidth : 0
+ anchors { bottom: parent.bottom; left: parent.left }
+ height: box.height
+ text: ""
+ font.family: Theme.iconFamily
+ onClicked: card.canvas.pickerOpen = !card.canvas.pickerOpen
+ }
+
Rectangle {
id: box
- anchors { bottom: parent.bottom; left: parent.left; right: send.left; rightMargin: 8 }
+ anchors {
+ bottom: parent.bottom; left: attach.right; leftMargin: attach.visible ? 8 : 0
+ right: history.left; rightMargin: 8
+ }
// Grows with the text up to six lines, then scrolls.
height: Math.min(input.contentHeight, 6 * input.font.pixelSize * 1.4) + 16
radius: 8
@@ -188,21 +295,35 @@ Rectangle {
}
Text {
- anchors { left: parent.left; top: parent.top; margins: 8 }
+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: 8 }
visible: input.text === ""
- text: "Message the assistant"
+ elide: Text.ElideRight
+ text: card.mode === "entry" ? "How can I help you?"
+ : card.canvas.staged.length ? `Ask about ${card.canvas.staged[0].name}…`
+ : "Message the assistant"
color: Theme.overlay
font: input.font
}
}
+ // History from every state; in chat the header has it too.
+ Button {
+ id: history
+ visible: card.mode !== "chat"
+ width: visible ? implicitWidth : 0
+ anchors { bottom: parent.bottom; right: send.left; rightMargin: visible ? 8 : 0 }
+ height: box.height
+ text: "History"
+ onClicked: card.canvas.historyOpen ? card.canvas.back() : card.canvas.showHistory()
+ }
+
Button {
id: send
anchors { bottom: parent.bottom; right: parent.right }
height: box.height
text: card.canvas.busy ? "Stop" : "Send"
danger: card.canvas.busy
- enabled: card.canvas.connected
+ enabled: card.canvas.connected && (card.canvas.busy || !card.canvas.blocked)
onClicked: {
if (card.canvas.busy) card.canvas.send({ type: "stop" });
else if (card.canvas.ask(input.text)) input.text = "";
@@ -211,9 +332,24 @@ Rectangle {
}
}
- History {
- anchors.fill: list
- visible: card.canvas.historyOpen
- canvas: card.canvas
+ // --- panels, over everything above the input ---
+
+ Item {
+ id: panelArea
+ anchors {
+ top: parent.top; topMargin: 14; bottom: chips.visible ? chips.top : footer.top
+ bottomMargin: 8; left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14
+ }
+
+ History {
+ anchors.fill: parent
+ visible: card.canvas.historyOpen
+ canvas: card.canvas
+ }
+ Picker {
+ anchors.fill: parent
+ visible: card.canvas.pickerOpen
+ canvas: card.canvas
+ }
}
}