diff options
Diffstat (limited to 'desktop/assistant/Assistant.qml')
| -rw-r--r-- | desktop/assistant/Assistant.qml | 84 |
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) { |
