aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/assistant/PresetsPage.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/assistant/PresetsPage.qml')
-rw-r--r--desktop/modules/assistant/PresetsPage.qml215
1 files changed, 215 insertions, 0 deletions
diff --git a/desktop/modules/assistant/PresetsPage.qml b/desktop/modules/assistant/PresetsPage.qml
new file mode 100644
index 0000000..ec103cf
--- /dev/null
+++ b/desktop/modules/assistant/PresetsPage.qml
@@ -0,0 +1,215 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import QtQuick
+import "../.."
+import "../../assistant/Presets.js" as Presets
+
+// One card per preset: its label and question, saved on Enter, and the file
+// kinds it is offered for. Every change saves the whole list to Prefs; Reset
+// drops the saved list, which brings the built-in one back.
+Column {
+ id: page
+ spacing: 10
+
+ readonly property var list: Presets.list(Prefs.presets)
+ // The kinds a preset can name: the four the canvas chats about, and
+ // "many", offered whenever two or more files are staged.
+ readonly property var kinds: ["pdf", "text", "code", "image", "many"]
+ property string error: ""
+ property int confirming: -1
+
+ // Saves a copy of the list with entry i replaced by fn(entry), or
+ // removed when fn returns null.
+ function edit(i, fn) {
+ const out = [];
+ page.list.forEach((p, j) => {
+ const c = { label: p.label, icon: p.icon, kinds: p.kinds.slice(), prompt: p.prompt };
+ if (j !== i) { out.push(c); return; }
+ const r = fn(c);
+ if (r) out.push(r);
+ });
+ page.error = "";
+ page.confirming = -1;
+ Prefs.setPresets(out);
+ }
+
+ function add() {
+ Prefs.setPresets(page.list.concat([{ label: "New preset", icon: Presets.FALLBACK,
+ kinds: ["text"], prompt: "Summarize this." }]));
+ }
+
+ component Small: Text {
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+
+ component Field: Rectangle {
+ id: field
+ property string value: ""
+ property string placeholder: ""
+ signal commit(string text)
+ height: 32
+ radius: 8
+ color: Qt.alpha(Theme.base, 0.6)
+ TextInput {
+ anchors { fill: parent; leftMargin: 10; rightMargin: 10 }
+ verticalAlignment: TextInput.AlignVCenter
+ clip: true
+ selectByMouse: true
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ text: field.value
+ onAccepted: if (text !== field.value) field.commit(text)
+ Keys.onEscapePressed: event => {
+ if (text !== field.value) text = field.value;
+ else event.accepted = false;
+ }
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ visible: parent.text === ""
+ text: field.placeholder
+ color: Theme.overlay
+ font: parent.font
+ }
+ }
+ }
+
+ Small {
+ width: page.width
+ wrapMode: Text.Wrap
+ text: "Offered on the assistant canvas for the staged files: a preset shows when every staged file is "
+ + "one of its kinds, or, with \"many\", when two or more are staged. Enter saves a field; "
+ + "changes apply at once."
+ }
+
+ Text {
+ width: page.width
+ visible: page.error !== ""
+ wrapMode: Text.Wrap
+ text: page.error
+ color: Theme.red
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+
+ Repeater {
+ model: page.list
+
+ delegate: Rectangle {
+ id: card
+ required property var modelData
+ required property int index
+ objectName: "preset:" + index
+ width: page.width
+ height: body.implicitHeight + 20
+ radius: 10
+ color: Qt.alpha(Theme.surface, 0.45)
+
+ Column {
+ id: body
+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: 10 }
+ spacing: 8
+
+ Item {
+ width: parent.width
+ height: 32
+ Text {
+ id: glyph
+ anchors { left: parent.left; verticalCenter: parent.verticalCenter }
+ width: 24
+ text: card.modelData.icon ?? Presets.FALLBACK
+ font { family: Theme.iconFamily; pixelSize: Theme.fontSize }
+ color: Theme.text
+ }
+ Field {
+ anchors { left: glyph.right; leftMargin: 6; right: del.left; rightMargin: 8; top: parent.top; bottom: parent.bottom }
+ value: card.modelData.label
+ placeholder: "label"
+ onCommit: t => {
+ if (t.trim() === "") { page.error = "A preset needs a label."; return; }
+ page.edit(card.index, c => Object.assign(c, { label: t.trim() }));
+ }
+ }
+ Button {
+ id: del
+ anchors { right: parent.right; verticalCenter: parent.verticalCenter }
+ danger: true
+ text: page.confirming === card.index ? "Confirm" : "Remove"
+ onClicked: {
+ if (page.confirming === card.index) page.edit(card.index, c => null);
+ else page.confirming = card.index;
+ }
+ }
+ }
+
+ Field {
+ width: parent.width
+ value: card.modelData.prompt
+ placeholder: "the question asked"
+ onCommit: t => {
+ if (t.trim() === "") { page.error = "A preset needs a question."; return; }
+ page.edit(card.index, c => Object.assign(c, { prompt: t.trim() }));
+ }
+ }
+
+ // A kind toggles on click; the last one cannot be cleared,
+ // since a preset with no kinds would never be offered.
+ Flow {
+ width: parent.width
+ spacing: 6
+ Repeater {
+ model: page.kinds
+ Rectangle {
+ id: chip
+ required property string modelData
+ readonly property bool on: card.modelData.kinds.includes(modelData)
+ implicitWidth: chipText.implicitWidth + 20
+ implicitHeight: 26
+ radius: 13
+ color: on ? Qt.alpha(Theme.accent, 0.85)
+ : chipArea.containsMouse ? Qt.alpha(Theme.accent, 0.22) : Qt.alpha(Theme.base, 0.6)
+ Text {
+ id: chipText
+ anchors.centerIn: parent
+ text: chip.modelData
+ color: chip.on ? Theme.base : Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+ MouseArea {
+ id: chipArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ const ks = card.modelData.kinds;
+ if (chip.on && ks.length === 1) { page.error = "A preset needs at least one kind."; return; }
+ page.edit(card.index, c => Object.assign(c, {
+ kinds: chip.on ? ks.filter(k => k !== chip.modelData) : ks.concat([chip.modelData]) }));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Row {
+ spacing: 8
+ Button { text: "Add preset"; onClicked: page.add() }
+ Button {
+ text: "Reset to defaults"
+ danger: true
+ enabled: Prefs.presets !== null
+ onClicked: Prefs.resetPresets()
+ }
+ }
+}