From 8cb290806375fa719ec512a6fb981ab74c0d326a Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 13 May 2026 11:43:37 +0200 Subject: [PATCH 01/22] feat(streaming): update task output while polling, display form if the task is running and has outputs Signed-off-by: Julien Veyssier --- src/assistant.js | 10 ++++++++-- src/components/AssistantTextProcessingForm.vue | 10 ++++++++-- src/views/AssistantPage.vue | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/assistant.js b/src/assistant.js index d5b10180..6b24af7f 100644 --- a/src/assistant.js +++ b/src/assistant.js @@ -136,6 +136,7 @@ export async function openAssistantForm({ view.progress = null view.expectedRuntime = null view.inputs = inputs + view.outputs = null view.selectedTaskTypeId = taskTypeId scheduleTask(appId, newTaskCustomId, taskTypeId, inputs) @@ -323,11 +324,12 @@ function updateTask(task, object) { } object.taskStatus = task?.status object.scheduledAt = task?.scheduledAt + object.outputs = task?.output } export async function pollTask(taskId, obj, callback = updateTask) { return new Promise((resolve, reject) => { - window.assistantPollTimerId = setInterval(() => { + const pollOnce = () => { getTask(taskId).then(response => { const task = response.data?.ocs?.data?.task if (window.assistantPollTimerId === null) { @@ -353,7 +355,10 @@ export async function pollTask(taskId, obj, callback = updateTask) { } reject(new Error('pollTask request failed')) }) - }, 2000) + } + // start polling immediately + // pollOnce() + window.assistantPollTimerId = setInterval(pollOnce, 2000) }) } @@ -606,6 +611,7 @@ export async function openAssistantTask( view.isNotifyEnabled = false view.expectedRuntime = null view.inputs = inputs + view.outputs = null view.selectedTaskTypeId = taskTypeId scheduleTask('assistant', newTaskCustomId, taskTypeId, inputs) diff --git a/src/components/AssistantTextProcessingForm.vue b/src/components/AssistantTextProcessingForm.vue index 7f35482b..17177333 100644 --- a/src/components/AssistantTextProcessingForm.vue +++ b/src/components/AssistantTextProcessingForm.vue @@ -39,7 +39,7 @@ { From 21e41db0aabb31badba0ca4aa4df41054ad12e8f Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 13 May 2026 12:25:58 +0200 Subject: [PATCH 02/22] feat(streaming): show notify and cancel buttons in the task header when displaying intermediate results (running + has output) Signed-off-by: Julien Veyssier --- src/assistant.js | 4 ++ .../AssistantTextProcessingForm.vue | 41 ++++++++++++++++++- src/views/AssistantPage.vue | 2 + 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/assistant.js b/src/assistant.js index 6b24af7f..2c365d5e 100644 --- a/src/assistant.js +++ b/src/assistant.js @@ -304,6 +304,8 @@ export async function openAssistantForm({ view.loading = false view.showSyncTaskRunning = false view.selectedTaskId = null + view.outputs = null + view.taskStatus = null lastTask = null }) }) @@ -769,6 +771,8 @@ export async function openAssistantTask( view.loading = false view.showSyncTaskRunning = false view.selectedTaskId = null + view.outputs = null + view.taskStatus = null lastTask = null }) }) diff --git a/src/components/AssistantTextProcessingForm.vue b/src/components/AssistantTextProcessingForm.vue index 17177333..8b89e0bd 100644 --- a/src/components/AssistantTextProcessingForm.vue +++ b/src/components/AssistantTextProcessingForm.vue @@ -71,6 +71,25 @@ +
+ {{ t('assistant', 'Getting results…') }} + + + {{ t('assistant', 'Get notified when the task finishes') }} + + + + {{ t('assistant', 'Cancel task') }} + +
@@ -148,6 +167,9 @@ import PlusIcon from 'vue-material-design-icons/Plus.vue' import UnfoldLessHorizontalIcon from 'vue-material-design-icons/UnfoldLessHorizontal.vue' import UnfoldMoreHorizontalIcon from 'vue-material-design-icons/UnfoldMoreHorizontal.vue' import InformationBoxIcon from 'vue-material-design-icons/InformationBox.vue' +import BellOutlineIcon from 'vue-material-design-icons/BellOutline.vue' +import BellRingOutlineIcon from 'vue-material-design-icons/BellRingOutline.vue' +import CloseIcon from 'vue-material-design-icons/Close.vue' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcActions from '@nextcloud/vue/components/NcActions' @@ -207,6 +229,9 @@ export default { UnfoldLessHorizontalIcon, UnfoldMoreHorizontalIcon, InformationBoxIcon, + BellOutlineIcon, + BellRingOutlineIcon, + CloseIcon, AssistantFormInputs, AssistantFormOutputs, ChattyLLMInputForm, @@ -412,6 +437,9 @@ export default { showRunningEmptyContent() { return this.showSyncTaskRunning && this.myOutputs === null }, + showSubtitle() { + return this.showSyncTaskRunning && this.myOutputs !== null + }, }, watch: { outputs(newVal) { @@ -828,12 +856,12 @@ export default { &__top-bar { display: flex; + flex-direction: column; justify-content: space-between; align-items: center; - gap: 4px; position: sticky; top: 0; - height: calc(var(--default-clickable-area) + var(--default-grid-baseline) * 2); + // height: calc(var(--default-clickable-area) + var(--default-grid-baseline) * 2); box-sizing: border-box; border-bottom: 1px solid var(--color-border); padding-left: 52px; @@ -851,6 +879,15 @@ export default { white-space: nowrap; } + &__subtitle { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + width: 100%; + padding: 4px 0 4px 10px; + } + &__provider { font-weight: normal; font-size: 0.9em; diff --git a/src/views/AssistantPage.vue b/src/views/AssistantPage.vue index bc4ac2e1..2c4be9da 100644 --- a/src/views/AssistantPage.vue +++ b/src/views/AssistantPage.vue @@ -107,6 +107,8 @@ export default { this.loading = false this.showSyncTaskRunning = false this.task.id = null + this.task.output = null + this.task.status = null }) }, syncSubmit(inputs, taskTypeId, newTaskIdentifier = '') { From 9cbb8e02977d826ab61624ff0d2ca3039cf6d17a Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 13 May 2026 15:34:00 +0200 Subject: [PATCH 03/22] feat(streaming): adjust chat UI to display intermediate/streaming message Signed-off-by: Julien Veyssier --- lib/Controller/ChattyLLMController.php | 9 ++++++- .../ChattyLLM/ChattyLLMInputForm.vue | 24 +++++++++++++++++++ src/components/ChattyLLM/ConversationBox.vue | 12 +++++++++- src/components/ChattyLLM/Message.vue | 9 +++++-- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index 8a6aac36..98b9f5f5 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -599,7 +599,14 @@ public function checkMessageGenerationTask(int $taskId, int $sessionId): JSONRes } elseif ($task->getstatus() === Task::STATUS_RUNNING || $task->getstatus() === Task::STATUS_SCHEDULED) { $startTime = $task->getStartedAt() ?? time(); $slowPickup = ($task->getScheduledAt() + (60 * 5)) < $startTime; - return new JSONResponse(['task_status' => $task->getstatus(), 'slow_pickup' => $slowPickup], Http::STATUS_EXPECTATION_FAILED); + $responsePayload = [ + 'task_status' => $task->getstatus(), + 'slow_pickup' => $slowPickup, + ]; + if ($task->getstatus() === Task::STATUS_RUNNING) { + $responsePayload['task_output'] = $task->getOutput(); + } + return new JSONResponse($responsePayload, Http::STATUS_EXPECTATION_FAILED); } elseif ($task->getstatus() === Task::STATUS_FAILED || $task->getstatus() === Task::STATUS_CANCELLED) { return new JSONResponse(['error' => 'task_failed_or_canceled', 'task_status' => $task->getstatus()], Http::STATUS_BAD_REQUEST); } diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index 7c11ef18..801685d4 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -118,6 +118,7 @@
{ const lastIdx = this.messages.length - 1 document.querySelector('#message' + lastIdx)?.scrollIntoView() + document.querySelector('#message-streaming')?.scrollIntoView() + document.querySelector('#message-placeholder')?.scrollIntoView() this.$refs.inputComponent.focus() }) }, @@ -716,6 +722,7 @@ export default { async runGenerationTask(sessionId, agencyConfirm = null) { try { + this.scrollToBottom() this.slowPickup = false this.loading.llmGeneration = true const params = { @@ -737,6 +744,7 @@ export default { showError(t('assistant', 'Error generating a response')) } finally { this.loading.llmGeneration = false + this.streamingMessage = null } }, @@ -756,6 +764,7 @@ export default { showError(t('assistant', 'Error regenerating a response')) } finally { this.loading.llmGeneration = false + this.streamingMessage = null } }, @@ -801,6 +810,21 @@ export default { } else { console.debug('checkTaskPolling, task is still scheduled or running') this.slowPickup = error.response.data.slow_pickup + if (error.response.data.task_output?.output) { + if (this.streamingMessage) { + this.streamingMessage.content = error.response.data.task_output.output + } else { + this.streamingMessage = { + role: Roles.ASSISTANT, + content: error.response.data.task_output.output, + attachments: [], + sources: '', + session_id: sessionId, + id: 0, + timestamp: moment().unix(), + } + } + } } }) }, 2000) diff --git a/src/components/ChattyLLM/ConversationBox.vue b/src/components/ChattyLLM/ConversationBox.vue index c73e0308..20e8f7fc 100644 --- a/src/components/ChattyLLM/ConversationBox.vue +++ b/src/components/ChattyLLM/ConversationBox.vue @@ -31,7 +31,13 @@ :information-source-names="informationSourceNames" @regenerate="regenerate(message.id)" @delete="deleteMessage(message.id)" /> - + + @@ -83,6 +89,10 @@ export default { type: Boolean, default: false, }, + streamingMessage: { + type: Object, + default: null, + }, }, emits: ['delete', 'regenerate'], diff --git a/src/components/ChattyLLM/Message.vue b/src/components/ChattyLLM/Message.vue index 23616495..b9f09763 100644 --- a/src/components/ChattyLLM/Message.vue +++ b/src/components/ChattyLLM/Message.vue @@ -17,13 +17,14 @@ @delete="$emit('delete')" />
+ @@ -135,6 +136,10 @@ export default { type: Boolean, default: false, }, + streaming: { + type: Boolean, + default: false, + }, informationSourceNames: { type: Object, default: null, From 1feb53bcb2a86f39aa83e9a9fbd37a22c26f55ad Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 13 May 2026 15:48:36 +0200 Subject: [PATCH 04/22] make the dialog initial width 70% Signed-off-by: Julien Veyssier --- src/components/AssistantTextProcessingModal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AssistantTextProcessingModal.vue b/src/components/AssistantTextProcessingModal.vue index 58a370e3..3edd6b20 100644 --- a/src/components/AssistantTextProcessingModal.vue +++ b/src/components/AssistantTextProcessingModal.vue @@ -217,7 +217,7 @@ export default { height: calc(100vh - 32px); max-height: calc(100vh - 32px); height: 80%; - width: 50%; + width: 70%; resize: both; overflow: hidden; filter: drop-shadow(0 0 15px rgba(77, 77, 77, 0.5)); From 6f4626a8351ffed47b5649a1202fcf1a4a6369f0 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 13 May 2026 15:56:57 +0200 Subject: [PATCH 05/22] add a loading icon in the output form while streaming Signed-off-by: Julien Veyssier --- src/components/AssistantTextProcessingForm.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/AssistantTextProcessingForm.vue b/src/components/AssistantTextProcessingForm.vue index 8b89e0bd..523840fa 100644 --- a/src/components/AssistantTextProcessingForm.vue +++ b/src/components/AssistantTextProcessingForm.vue @@ -58,7 +58,8 @@ @@ -71,7 +72,7 @@
-
{{ t('assistant', 'Getting results…') }} Date: Wed, 13 May 2026 16:53:02 +0200 Subject: [PATCH 06/22] add @nextcloud/notify_push Signed-off-by: Julien Veyssier --- package-lock.json | 144 +++++++++++++--------------------------------- package.json | 1 + 2 files changed, 40 insertions(+), 105 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d4f9c7c..ffcbcf1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "assistant", - "version": "3.2.0", + "version": "3.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "assistant", - "version": "3.2.0", + "version": "3.4.1", "license": "AGPL-3.0", "dependencies": { "@breezystack/lamejs": "^1.2.7", @@ -21,6 +21,7 @@ "@nextcloud/initial-state": "^3.0.0", "@nextcloud/l10n": "^3.4.0", "@nextcloud/moment": "^1.3.1", + "@nextcloud/notify_push": "^1.4.0", "@nextcloud/router": "^3.0.0", "@nextcloud/vue": "^9.0.0-rc.5", "@primeuix/themes": "^2.0.3", @@ -2870,27 +2871,27 @@ } }, "node_modules/@nextcloud/auth": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.5.3.tgz", - "integrity": "sha512-KIhWLk0BKcP4hvypE4o11YqKOPeFMfEFjRrhUUF+h7Fry+dhTBIEIxuQPVCKXMIpjTDd8791y8V6UdRZ2feKAQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.6.0.tgz", + "integrity": "sha512-VkT87+9UqpPi7O36bVEE4/MxWF8d90VQcuMlvKltsZyLSLkEGrPXgowtD75Y54k60/8SR6mXbeqBwapi8dDUbA==", "license": "GPL-3.0-or-later", "dependencies": { "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/event-bus": "^3.3.2" + "@nextcloud/event-bus": "^3.3.3", + "@nextcloud/router": "^3.1.0" }, "engines": { "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, "node_modules/@nextcloud/axios": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-2.5.2.tgz", - "integrity": "sha512-8frJb77jNMbz00TjsSqs1PymY0nIEbNM4mVmwen2tXY7wNgRai6uXilIlXKOYB9jR/F/HKRj6B4vUwVwZbhdbw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-2.6.0.tgz", + "integrity": "sha512-ehcIgyora8DAJ+STG6iFI4e+ufPVFrIA6o0FgMKeKdfyaxRJ9UM7L+n7V+rc/qv8sDiWC/hWIKwFtLw2W5yE4Q==", "license": "GPL-3.0-or-later", "dependencies": { - "@nextcloud/auth": "^2.5.1", - "@nextcloud/router": "^3.0.1", - "axios": "^1.12.2" + "@nextcloud/auth": "^2.6.0", + "axios": "^1.15.0" }, "engines": { "node": "^20.0.0 || ^22.0.0 || ^24.0.0" @@ -3172,6 +3173,17 @@ "npm": "^10.0.0" } }, + "node_modules/@nextcloud/notify_push": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.4.0.tgz", + "integrity": "sha512-07UDgz1xLG9XABP8+mwQ2CsNWZu6lKzz0ErUA2HfE1ZfxXKiwVpo60t30y34UExGB9+Ok1nFaYU8fyJHncz9aQ==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@nextcloud/axios": "^2.6.0", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/event-bus": "^3.3.3" + } + }, "node_modules/@nextcloud/paths": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-3.1.0.tgz", @@ -3520,9 +3532,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3545,9 +3554,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3570,9 +3576,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3595,9 +3598,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3620,9 +3620,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3645,9 +3642,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3969,9 +3963,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3987,9 +3978,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4005,9 +3993,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4023,9 +4008,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4041,9 +4023,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4059,9 +4038,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4077,9 +4053,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4095,9 +4068,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4113,9 +4083,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4131,9 +4098,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4149,9 +4113,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4167,9 +4128,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4185,9 +4143,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4991,9 +4946,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5009,9 +4961,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5027,9 +4976,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5045,9 +4991,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5063,9 +5006,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5081,9 +5021,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5099,9 +5036,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5117,9 +5051,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5939,14 +5870,14 @@ } }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/babel-plugin-polyfill-corejs2": { @@ -8747,9 +8678,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -12377,10 +12308,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/public-encrypt": { "version": "4.0.3", diff --git a/package.json b/package.json index e5fa2556..723af200 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@nextcloud/initial-state": "^3.0.0", "@nextcloud/l10n": "^3.4.0", "@nextcloud/moment": "^1.3.1", + "@nextcloud/notify_push": "^1.4.0", "@nextcloud/router": "^3.0.0", "@nextcloud/vue": "^9.0.0-rc.5", "@primeuix/themes": "^2.0.3", From 0523281395f43b49ad69dfec36311106586bef37 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 13 May 2026 16:53:26 +0200 Subject: [PATCH 07/22] feat(streaming): use notify_push to get the polled task's output Signed-off-by: Julien Veyssier --- src/assistant.js | 53 ++++++++++++++++++++++++++++++++----- src/views/AssistantPage.vue | 19 +++++++++++-- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/assistant.js b/src/assistant.js index 2c365d5e..9b92cfa8 100644 --- a/src/assistant.js +++ b/src/assistant.js @@ -8,6 +8,7 @@ import { showError } from '@nextcloud/dialogs' import { emit } from '@nextcloud/event-bus' import PrimeVue from 'primevue/config' import Aura from '@primeuix/themes/aura' +import { listen } from '@nextcloud/notify_push' window.assistantPollTimerId = null @@ -146,7 +147,21 @@ export async function openAssistantForm({ view.selectedTaskId = lastTask?.id view.expectedRuntime = (lastTask?.completionExpectedAt - lastTask?.scheduledAt) || null - pollTask(task.id, view).then(finishedTask => { + // attempt to listen to push notifications to get the intermediate output + const pushTaskId = task.id + const pushChannel = 'task_' + pushTaskId + const hasPush = listen(pushChannel, (type, body) => { + console.debug('[assistant] received push notification', type, body) + if (pushTaskId === view.selectedTaskId) { + view.outputs = body + } else { + console.debug('[assistant] ignoring push notification for task', pushTaskId, 'the selected one is', view.selectedTaskId) + } + }) + console.debug('[assistant] HAS PUSH', hasPush) + + // no need to update the task output with polling if we have push notifications + pollTask(task.id, view, !hasPush).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { if (closeOnResult) { @@ -320,16 +335,28 @@ export async function openAssistantForm({ }) } -function updateTask(task, object) { +function updateTask(task, object, updateOutput = true) { if (task?.status === TASK_STATUS_STRING.running) { object.progress = task?.progress * 100 } object.taskStatus = task?.status object.scheduledAt = task?.scheduledAt - object.outputs = task?.output + if (updateOutput) { + console.debug('[assistant] polling update output') + object.outputs = task?.output + } } -export async function pollTask(taskId, obj, callback = updateTask) { +/** + * Poll the task to update its status + * + * @param {number} taskId the task ID + * @param {object} obj the object to update + * @param {boolean} updateOutput whether to update the task output from the polling data or not + * @param {Function} callback the function to call to update the object + * @return {Promise<*>} + */ +export async function pollTask(taskId, obj, updateOutput = true, callback = updateTask) { return new Promise((resolve, reject) => { const pollOnce = () => { getTask(taskId).then(response => { @@ -339,7 +366,7 @@ export async function pollTask(taskId, obj, callback = updateTask) { return } if (obj) { - callback(task, obj) + callback(task, obj, updateOutput) } if (![TASK_STATUS_STRING.scheduled, TASK_STATUS_STRING.running].includes(task?.status)) { // stop polling @@ -622,7 +649,21 @@ export async function openAssistantTask( lastTask = task view.selectedTaskId = lastTask?.id view.expectedRuntime = (lastTask?.completionExpectedAt - lastTask?.scheduledAt) || null - pollTask(task.id, view).then(finishedTask => { + + // attempt to listen to push notifications to get the intermediate output + const pushTaskId = task.id + const pushChannel = 'task_' + pushTaskId + const hasPush = listen(pushChannel, (type, body) => { + console.debug('[assistant] received push notification', type, body) + if (pushTaskId === view.selectedTaskId) { + view.outputs = body + } else { + console.debug('[assistant] ignoring push notification for task', pushTaskId, 'the selected one is', view.selectedTaskId) + } + }) + console.debug('[assistant] HAS PUSH', hasPush) + + pollTask(task.id, view, !hasPush).then(finishedTask => { if (finishedTask.status === TASK_STATUS_STRING.successful) { view.outputs = finishedTask?.output } else if (finishedTask.status === TASK_STATUS_STRING.failed) { diff --git a/src/views/AssistantPage.vue b/src/views/AssistantPage.vue index 2c4be9da..0798d582 100644 --- a/src/views/AssistantPage.vue +++ b/src/views/AssistantPage.vue @@ -40,6 +40,7 @@ import AssistantTextProcessingForm from '../components/AssistantTextProcessingFo import { showError } from '@nextcloud/dialogs' import { emit } from '@nextcloud/event-bus' import { loadState } from '@nextcloud/initial-state' +import { listen } from '@nextcloud/notify_push' import { cancelTask, cancelTaskPolling, @@ -128,7 +129,21 @@ export default { this.task.id = task.id this.task.completionExpectedAt = task.completionExpectedAt this.task.scheduledAt = task.scheduledAt - pollTask(task.id, this, this.updateTask).then(finishedTask => { + + // attempt to listen to push notifications to get the intermediate output + const pushTaskId = task.id + const pushChannel = 'task_' + pushTaskId + const hasPush = listen(pushChannel, (type, body) => { + console.debug('[assistant] received push notification', type, body) + if (pushTaskId === this.task.id) { + this.task.output = body + } else { + console.debug('[assistant] ignoring push notification for task', pushTaskId, 'the selected one is', this.task.id) + } + }) + console.debug('[assistant] HAS PUSH', hasPush) + + pollTask(task.id, this, !hasPush, this.updateTask).then(finishedTask => { if (finishedTask.status === TASK_STATUS_STRING.successful) { this.task.output = finishedTask?.output } else if (finishedTask.status === TASK_STATUS_STRING.failed) { @@ -202,7 +217,7 @@ export default { this.task.completionExpectedAt = updatedTask.completionExpectedAt this.task.scheduledAt = updatedTask.scheduledAt - pollTask(updatedTask.id, this, this.updateTask).then(finishedTask => { + pollTask(updatedTask.id, this, true, this.updateTask).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { this.task.output = finishedTask?.output From 7c6cde0cdf74d7bfed65444ac4bcf78c7ac43cbc Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 13 May 2026 17:16:34 +0200 Subject: [PATCH 08/22] feat(streaming): use notify_push to get the polled chat message generation task's output Signed-off-by: Julien Veyssier --- .../ChattyLLM/ChattyLLMInputForm.vue | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index 801685d4..73d1d763 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -216,6 +216,7 @@ import axios, { isCancel } from '@nextcloud/axios' import { showError } from '@nextcloud/dialogs' import { generateUrl, generateOcsUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' +import { listen } from '@nextcloud/notify_push' import moment from 'moment' import { SHAPE_TYPE_NAMES } from '../../constants.js' @@ -769,6 +770,27 @@ export default { }, async pollGenerationTask(taskId, sessionId) { + // attempt to listen to push notifications to get the intermediate output + const pushTaskId = taskId + const pushChannel = 'task_' + pushTaskId + const pushSessionId = this.active.id + const hasPush = listen(pushChannel, (type, body) => { + console.debug('[assistant] received push notification', type, body) + if (pushSessionId === this.active.id) { + this.updateStreamingMessage(body.output, sessionId) + } else { + console.debug( + '[assistant] ignoring push notification for task', + pushTaskId, + 'in session', + pushSessionId, + 'the selected session is', + this.active.id, + ) + } + }) + console.debug('[assistant] HAS PUSH', hasPush) + return new Promise((resolve, reject) => { this.pollMessageGenerationTimerId = setInterval(() => { if (this.active === null || sessionId !== this.active.id) { @@ -811,19 +833,7 @@ export default { console.debug('checkTaskPolling, task is still scheduled or running') this.slowPickup = error.response.data.slow_pickup if (error.response.data.task_output?.output) { - if (this.streamingMessage) { - this.streamingMessage.content = error.response.data.task_output.output - } else { - this.streamingMessage = { - role: Roles.ASSISTANT, - content: error.response.data.task_output.output, - attachments: [], - sources: '', - session_id: sessionId, - id: 0, - timestamp: moment().unix(), - } - } + this.updateStreamingMessage(error.response.data.task_output.output, sessionId) } } }) @@ -831,6 +841,22 @@ export default { }) }, + updateStreamingMessage(content, sessionId) { + if (this.streamingMessage) { + this.streamingMessage.content = content + } else { + this.streamingMessage = { + role: Roles.ASSISTANT, + content, + attachments: [], + sources: '', + session_id: sessionId, + id: 0, + timestamp: moment().unix(), + } + } + }, + getLastHumanMessage() { return this.messages .filter(m => m.role === Roles.HUMAN) From ed20d39ccf64e65405ac38eb49085e0f3d86ddee Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 18 May 2026 15:17:28 +0200 Subject: [PATCH 09/22] start listening to notify_push messages when loading a task in the generic form Signed-off-by: Julien Veyssier --- src/assistant.js | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/assistant.js b/src/assistant.js index 9b92cfa8..aa0be253 100644 --- a/src/assistant.js +++ b/src/assistant.js @@ -124,6 +124,31 @@ export async function openAssistantForm({ const view = app.mount(modalMountPoint) let lastTask = null + // notify push stuff + const isListeningTo = {} + // listen only if needed + // return true if notify_push is available + // we can't cleanup isListeningTo because there is no way to remove a handler with @nextcloud/notify_push + const listenToTaskNotifications = (pushTaskId) => { + if (isListeningTo[pushTaskId]) { + return true + } + // attempt to listen to push notifications to get the intermediate output + const pushChannel = 'task_' + pushTaskId + const hasPush = listen(pushChannel, (type, body) => { + console.debug('[assistant] received push notification', type, body) + if (pushTaskId === view.selectedTaskId) { + view.outputs = body + } else { + console.debug('[assistant] ignoring push notification for task', pushTaskId, 'the selected one is', view.selectedTaskId) + } + }) + if (hasPush) { + isListeningTo[pushTaskId] = true + } + return hasPush + } + modalMountPoint.addEventListener('cancel', () => { cancelTaskPolling() app.unmount() @@ -147,17 +172,7 @@ export async function openAssistantForm({ view.selectedTaskId = lastTask?.id view.expectedRuntime = (lastTask?.completionExpectedAt - lastTask?.scheduledAt) || null - // attempt to listen to push notifications to get the intermediate output - const pushTaskId = task.id - const pushChannel = 'task_' + pushTaskId - const hasPush = listen(pushChannel, (type, body) => { - console.debug('[assistant] received push notification', type, body) - if (pushTaskId === view.selectedTaskId) { - view.outputs = body - } else { - console.debug('[assistant] ignoring push notification for task', pushTaskId, 'the selected one is', view.selectedTaskId) - } - }) + const hasPush = listenToTaskNotifications(task.id) console.debug('[assistant] HAS PUSH', hasPush) // no need to update the task output with polling if we have push notifications @@ -256,7 +271,10 @@ export async function openAssistantForm({ view.progress = null view.expectedRuntime = (updatedTask?.completionExpectedAt - updatedTask?.scheduledAt) || null - pollTask(updatedTask.id, view).then(finishedTask => { + const hasPush = listenToTaskNotifications(task.id) + console.debug('[assistant] HAS PUSH', hasPush) + + pollTask(updatedTask.id, view, !hasPush).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { view.outputs = finishedTask?.output From c8ab215c4f978e3f68170a575927a4fcb9b51595 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 18 May 2026 15:32:52 +0200 Subject: [PATCH 10/22] prevent listening notify push msgs twice for the same task after switching chat sessions Signed-off-by: Julien Veyssier --- src/components/ChattyLLM/ChattyLLMInputForm.vue | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index 73d1d763..dfddd81d 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -269,6 +269,7 @@ export default { // [{ id: number, session_id: number, role: string, content: string, timestamp: number, sources:string }] messages: [], // null when failed to fetch streamingMessage: null, + isListeningTo: {}, messagesAxiosController: null, // for request cancellation allMessagesLoaded: false, loading: { @@ -769,15 +770,16 @@ export default { } }, - async pollGenerationTask(taskId, sessionId) { + listenToTaskNotifications(pushTaskId, pushSessionId) { // attempt to listen to push notifications to get the intermediate output - const pushTaskId = taskId + if (this.isListeningTo[pushTaskId]) { + return true + } const pushChannel = 'task_' + pushTaskId - const pushSessionId = this.active.id const hasPush = listen(pushChannel, (type, body) => { console.debug('[assistant] received push notification', type, body) if (pushSessionId === this.active.id) { - this.updateStreamingMessage(body.output, sessionId) + this.updateStreamingMessage(body.output, pushSessionId) } else { console.debug( '[assistant] ignoring push notification for task', @@ -789,6 +791,12 @@ export default { ) } }) + this.isListeningTo[pushTaskId] = true + return hasPush + }, + + async pollGenerationTask(taskId, sessionId) { + const hasPush = this.listenToTaskNotifications(taskId, this.active.id) console.debug('[assistant] HAS PUSH', hasPush) return new Promise((resolve, reject) => { From 57a13b8b33c19c2c7156ee3a32b7a3b216d09b70 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 18 May 2026 15:41:53 +0200 Subject: [PATCH 11/22] regenerate openapi specs, fix psalm issue Signed-off-by: Julien Veyssier --- lib/Controller/ChattyLLMController.php | 2 +- openapi.json | 27 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index 98b9f5f5..6804e8a0 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -548,7 +548,7 @@ public function regenerateForSession(int $sessionId, int $messageId): JSONRespon * * @param int $taskId The message generation task ID * @param int $sessionId The chat session ID - * @return JSONResponse|JSONResponse|JSONResponse + * @return JSONResponse|JSONResponse|numeric|string>|null}, array{}>|JSONResponse * @throws MultipleObjectsReturnedException * @throws \OCP\DB\Exception * diff --git a/openapi.json b/openapi.json index 7bd328db..a081df19 100644 --- a/openapi.json +++ b/openapi.json @@ -4852,6 +4852,33 @@ }, "slow_pickup": { "type": "boolean" + }, + "task_output": { + "type": "object", + "nullable": true, + "additionalProperties": { + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + } } } } From 1f43eb621eed264a0203a568a5f7ba0af77e02ce Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 18 May 2026 18:11:06 +0200 Subject: [PATCH 12/22] add simple pulse animation to output fields when streaming Signed-off-by: Julien Veyssier --- .../AssistantTextProcessingForm.vue | 1 + src/components/fields/TextInput.vue | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/AssistantTextProcessingForm.vue b/src/components/AssistantTextProcessingForm.vue index 523840fa..d03f5a0b 100644 --- a/src/components/AssistantTextProcessingForm.vue +++ b/src/components/AssistantTextProcessingForm.vue @@ -241,6 +241,7 @@ export default { provide() { return { providedCurrentTaskId: () => this.selectedTaskId, + streaming: () => this.streaming, } }, props: { diff --git a/src/components/fields/TextInput.vue b/src/components/fields/TextInput.vue index c88045ee..a86cf476 100644 --- a/src/components/fields/TextInput.vue +++ b/src/components/fields/TextInput.vue @@ -17,7 +17,7 @@ :multiline="isMobile" :maxlength="maxLength" class="editable-input" - :class="{ shadowed: isOutput }" + :class="{ shadowed: isOutput, streaming: isOutput && streaming() }" :placeholder="placeholder" :title="title" @submit="hasValue && $emit('submit', $event)" @@ -87,6 +87,10 @@ export default { isMobile, ], + inject: [ + 'streaming', + ], + props: { id: { type: String, @@ -232,8 +236,23 @@ body[dir="rtl"] .choose-file-button { padding-bottom: 4px !important; } .shadowed .rich-contenteditable__input { - border: 2px solid var(--color-primary-element) !important; + border: 2px solid var(--color-primary-element); padding-bottom: 38px !important; } + .shadowed.streaming .rich-contenteditable__input { + animation: pulse 2s infinite; + } +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0px rgba(128, 128, 128, 0.7); + } + 70% { + box-shadow: 0 0 0 14px rgba(128, 128, 128, 0); + } + 100% { + box-shadow: 0 0 0 0px rgba(128, 128, 128, 0); + } } From fdc9e633b269ebe6f40ea09e176197044abe477c Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Tue, 19 May 2026 10:48:57 +0200 Subject: [PATCH 13/22] start listening to notify_push messages when loading a task from a notification or in the standalone page Signed-off-by: Julien Veyssier --- src/assistant.js | 41 ++++++++++++++++++++++++++----------- src/views/AssistantPage.vue | 36 +++++++++++++++++++++----------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/assistant.js b/src/assistant.js index aa0be253..ff1c4428 100644 --- a/src/assistant.js +++ b/src/assistant.js @@ -632,6 +632,31 @@ export async function openAssistantTask( const view = app.mount(modalMountPoint) let lastTask = task + // notify push stuff + const isListeningTo = {} + // listen only if needed + // return true if notify_push is available + // we can't cleanup isListeningTo because there is no way to remove a handler with @nextcloud/notify_push + const listenToTaskNotifications = (pushTaskId) => { + if (isListeningTo[pushTaskId]) { + return true + } + // attempt to listen to push notifications to get the intermediate output + const pushChannel = 'task_' + pushTaskId + const hasPush = listen(pushChannel, (type, body) => { + console.debug('[assistant] received push notification', type, body) + if (pushTaskId === view.selectedTaskId) { + view.outputs = body + } else { + console.debug('[assistant] ignoring push notification for task', pushTaskId, 'the selected one is', view.selectedTaskId) + } + }) + if (hasPush) { + isListeningTo[pushTaskId] = true + } + return hasPush + } + modalMountPoint.addEventListener('cancel', () => { cancelTaskPolling() app.unmount() @@ -668,17 +693,7 @@ export async function openAssistantTask( view.selectedTaskId = lastTask?.id view.expectedRuntime = (lastTask?.completionExpectedAt - lastTask?.scheduledAt) || null - // attempt to listen to push notifications to get the intermediate output - const pushTaskId = task.id - const pushChannel = 'task_' + pushTaskId - const hasPush = listen(pushChannel, (type, body) => { - console.debug('[assistant] received push notification', type, body) - if (pushTaskId === view.selectedTaskId) { - view.outputs = body - } else { - console.debug('[assistant] ignoring push notification for task', pushTaskId, 'the selected one is', view.selectedTaskId) - } - }) + const hasPush = listenToTaskNotifications(task.id) console.debug('[assistant] HAS PUSH', hasPush) pollTask(task.id, view, !hasPush).then(finishedTask => { @@ -767,7 +782,9 @@ export async function openAssistantTask( view.progress = null view.expectedRuntime = (updatedTask?.completionExpectedAt - updatedTask?.scheduledAt) || null - pollTask(updatedTask.id, view).then(finishedTask => { + const hasPush = listenToTaskNotifications(task.id) + + pollTask(updatedTask.id, view, !hasPush).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { view.outputs = finishedTask?.output diff --git a/src/views/AssistantPage.vue b/src/views/AssistantPage.vue index 0798d582..9ae333f9 100644 --- a/src/views/AssistantPage.vue +++ b/src/views/AssistantPage.vue @@ -70,6 +70,7 @@ export default { progress: null, loading: false, isNotifyEnabled: false, + isListeningTo: {}, } }, @@ -112,6 +113,25 @@ export default { this.task.status = null }) }, + listenToTaskNotifications(pushTaskId) { + if (this.isListeningTo[pushTaskId]) { + return true + } + // attempt to listen to push notifications to get the intermediate output + const pushChannel = 'task_' + pushTaskId + const hasPush = listen(pushChannel, (type, body) => { + console.debug('[assistant] received push notification', type, body) + if (pushTaskId === this.task.id) { + this.task.output = body + } else { + console.debug('[assistant] ignoring push notification for task', pushTaskId, 'the selected one is', this.task.id) + } + }) + if (hasPush) { + this.isListeningTo[pushTaskId] = true + } + return hasPush + }, syncSubmit(inputs, taskTypeId, newTaskIdentifier = '') { this.loading = true this.showSyncTaskRunning = true @@ -130,17 +150,7 @@ export default { this.task.completionExpectedAt = task.completionExpectedAt this.task.scheduledAt = task.scheduledAt - // attempt to listen to push notifications to get the intermediate output - const pushTaskId = task.id - const pushChannel = 'task_' + pushTaskId - const hasPush = listen(pushChannel, (type, body) => { - console.debug('[assistant] received push notification', type, body) - if (pushTaskId === this.task.id) { - this.task.output = body - } else { - console.debug('[assistant] ignoring push notification for task', pushTaskId, 'the selected one is', this.task.id) - } - }) + const hasPush = this.listenToTaskNotifications(task.id) console.debug('[assistant] HAS PUSH', hasPush) pollTask(task.id, this, !hasPush, this.updateTask).then(finishedTask => { @@ -217,7 +227,9 @@ export default { this.task.completionExpectedAt = updatedTask.completionExpectedAt this.task.scheduledAt = updatedTask.scheduledAt - pollTask(updatedTask.id, this, true, this.updateTask).then(finishedTask => { + const hasPush = this.listenToTaskNotifications(task.id) + + pollTask(updatedTask.id, this, !hasPush, this.updateTask).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { this.task.output = finishedTask?.output From 6a9709b7c82b669ea131bb4bf9b34507582cf9a5 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Tue, 19 May 2026 13:41:26 +0200 Subject: [PATCH 14/22] add pulse animation to 'getting results...' label Signed-off-by: Julien Veyssier --- .../AssistantTextProcessingForm.vue | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/AssistantTextProcessingForm.vue b/src/components/AssistantTextProcessingForm.vue index d03f5a0b..bbd5585f 100644 --- a/src/components/AssistantTextProcessingForm.vue +++ b/src/components/AssistantTextProcessingForm.vue @@ -58,8 +58,7 @@ @@ -74,7 +73,9 @@
- {{ t('assistant', 'Getting results…') }} +