// Copyright (C) 2026 Danilo M. // // 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() } } }