// 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 and the file kinds it is // offered for. Every change edits the module's draft; the header's Save // hands the list to Prefs. Reset fills the draft with the built-in list. Column { id: page spacing: 10 // The module, holding the draft. Not named pm, its id (it would bind to // itself, AGENTS.md). required property QtObject owner readonly property var list: owner.shown // 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 = ""; if (fn === null || out.length !== page.list.length) page.confirming = -1; page.owner.edit(out); } function add() { page.owner.edit(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 } } // Reports every keystroke into the draft. Not bound to value, which // would reset the cursor; it follows the value only when the draft moves // under it (Discard, Reset). Empty is outlined, since Save refuses it. component Field: Rectangle { id: field property string value: "" property string placeholder: "" signal edited(string text) height: 32 radius: 8 color: Qt.alpha(Theme.base, 0.6) border.width: ti.text.trim() === "" ? 1 : 0 border.color: Theme.red onValueChanged: if (ti.text !== field.value) ti.text = field.value TextInput { id: ti 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 } Component.onCompleted: text = field.value onTextEdited: field.edited(text) 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. Save applies the " + "changes, which the canvas picks up 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 { // A count, not the list: a new list on every keystroke would rebuild // every card and drop the focus of the field being typed in. model: page.list.length delegate: Rectangle { id: card required property int index readonly property var modelData: page.list[index] ?? { label: "", prompt: "", kinds: [], icon: "" } 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" onEdited: t => page.edit(card.index, c => Object.assign(c, { label: t })) } 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" onEdited: t => page.edit(card.index, c => Object.assign(c, { prompt: t })) } // 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: !page.owner.resetting && (Prefs.presets !== null || page.owner.dirty) onClicked: page.owner.reset() } } }