aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant/Assistant.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/assistant/Assistant.qml')
-rw-r--r--desktop/assistant/Assistant.qml84
1 files changed, 84 insertions, 0 deletions
diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml
index 0139ef9..b1329b1 100644
--- a/desktop/assistant/Assistant.qml
+++ b/desktop/assistant/Assistant.qml
@@ -41,6 +41,22 @@ Scope {
// entry: one line. file: files staged, nothing asked yet. chat: turns.
readonly property string mode: turns.count || root.chatting ? "chat"
: root.staged.length ? "file" : "entry"
+ // The Do row's operation: one at a time, either fileops (a Process) or
+ // the daemon's write (To .md). Its result stays on the card.
+ property string opLabel: ""
+ property bool opRunning: false
+ property bool opDaemon: false
+ property int opChars: 0
+ property var opOut: []
+ property string opError: ""
+ // Results folder: $(xdg-user-dir DOCUMENTS)/Assistant, read once.
+ property string outDir: (Quickshell.env("HOME") || "") + "/Documents/Assistant"
+ readonly property string fileops:
+ decodeURIComponent(Qt.resolvedUrl("fileops").toString().replace(/^file:\/\//, ""))
+ readonly property string mdPrompt: "Rewrite this document as Markdown: keep its wording; "
+ + "use headings, lists and tables where the original has them; transcribe scanned "
+ + "pages. Output only the Markdown."
+
// A staged kind chat does not take holds the question back.
readonly property bool blocked: root.staged.some(f => !Files.chats(f.kind))
@@ -102,6 +118,7 @@ Scope {
const files = paths.filter(p => p !== "").map(Files.file);
if (!files.length) return;
root.fresh();
+ root.clearOp();
root.staged = files;
root.historyOpen = root.pickerOpen = false;
root.open = true;
@@ -114,6 +131,64 @@ Scope {
root.refine();
}
+ function clearOp() {
+ if (root.opRunning) return;
+ root.opLabel = root.opError = "";
+ root.opOut = [];
+ root.opChars = 0;
+ }
+
+ function runOp(o) {
+ if (root.opRunning || !root.staged.length) return;
+ root.clearOp();
+ root.opLabel = o.label;
+ root.opRunning = true;
+ const paths = root.staged.map(f => f.path);
+ if (o.op === "md") {
+ if (!root.connected) { root.opRunning = false; root.opError = "assistant not running"; return; }
+ root.opDaemon = true;
+ const name = root.staged[0].name.replace(/\.[^.]*$/, "") + ".md";
+ root.send({ type: "write", files: paths, prompt: root.mdPrompt, dir: root.outDir, name: name });
+ } else {
+ root.opDaemon = false;
+ ops.command = [root.fileops, o.op, root.outDir].concat(paths);
+ ops.running = false;
+ ops.running = true;
+ }
+ }
+
+ function stopOp() {
+ if (!root.opRunning) return;
+ if (root.opDaemon) root.send({ type: "stop" });
+ else ops.running = false; // SIGTERM: fileops kills its tool, removes the partial
+ root.opRunning = false;
+ root.opError = "stopped";
+ }
+
+ Process {
+ id: ops
+ stdout: StdioCollector {
+ onStreamFinished: {
+ if (!root.opRunning || root.opDaemon) return;
+ root.opRunning = false;
+ try {
+ const r = JSON.parse(text.trim().split("\n").pop());
+ if (r.ok) root.opOut = r.out; else root.opError = r.error;
+ } catch (e) {
+ root.opError = "fileops gave no answer";
+ }
+ }
+ }
+ }
+
+ Process {
+ running: true
+ command: ["xdg-user-dir", "DOCUMENTS"]
+ stdout: StdioCollector {
+ onStreamFinished: if (text.trim().startsWith("/")) root.outDir = text.trim() + "/Assistant"
+ }
+ }
+
// A file whose name says nothing (a script with no suffix) is "other"
// until file(1) has read it. One run for all of them; the daemon makes
// its own call on the content when the question is sent.
@@ -244,7 +319,16 @@ Scope {
if (!e.stopped) root.convId = e.id;
root.lastActive = Date.now();
break;
+ case "progress": if (root.opDaemon) root.opChars = e.chars; break;
+ case "written":
+ if (root.opDaemon && root.opRunning) { root.opRunning = false; root.opOut = [e.path]; }
+ break;
case "error":
+ if (e.turn && root.opDaemon && root.opRunning) {
+ root.opRunning = false;
+ root.opError = e.message;
+ break;
+ }
// Only an error marked turn ended the reply. A command error,
// perhaps another client's, leaves a streaming reply alone.
if (e.turn && i >= 0 && turns.get(i).live) {