diff --git a/src/assistant.js b/src/assistant.js index cf0273f3..32d0ca2e 100644 --- a/src/assistant.js +++ b/src/assistant.js @@ -10,7 +10,13 @@ import PrimeVue from 'primevue/config' import Aura from '@primeuix/themes/aura' import { listen } from '@nextcloud/notify_push' +window.assistantPollAbortController = null window.assistantPollTimerId = null +window.assistantPollTaskId = null +window.assistantPollPositionTimerId = null +window.assistantPollPositionTaskId = null +window.assistantPollPositionAbortController = null +window.assistantSchedulingAbortController = null listen('taskprocessing:task_update', (type, body) => { console.debug('[assistant] received task update push notification', type, body) @@ -167,7 +173,9 @@ export async function openAssistantForm({ } modalMountPoint.addEventListener('cancel', () => { + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() app.unmount() OCA.Assistant.isAssistantDialogOpen = false reject(new Error('User cancellation')) @@ -175,6 +183,7 @@ export async function openAssistantForm({ const syncSubmit = (inputs, taskTypeId, newTaskCustomId = '') => { view.loading = true view.showSyncTaskRunning = true + view.taskPosition = null view.isNotifyEnabled = false view.progress = null view.expectedRuntime = null @@ -184,18 +193,27 @@ export async function openAssistantForm({ view.outputs = null view.selectedTaskTypeId = taskTypeId - scheduleTask(appId, newTaskCustomId, taskTypeId, inputs) + const controller = new AbortController() + window.assistantSchedulingAbortController = controller + scheduleTask(appId, newTaskCustomId, taskTypeId, inputs, controller.signal) .then((response) => { + if (window.assistantSchedulingAbortController === controller) { + cancelScheduling() + } const task = response.data?.ocs?.data?.task lastTask = task view.selectedTaskId = lastTask?.id view.expectedRuntime = (lastTask?.completionExpectedAt - lastTask?.scheduledAt) || null view.startedAt = lastTask?.startedAt || null view.completionExpectedAt = lastTask?.completionExpectedAt || null - const hasPush = listenToTaskNotifications(task.id) console.debug('[assistant] HAS PUSH', hasPush) + pollTaskPosition(task.id, view).then(() => { + console.debug('[assistant] pollTaskPosition: the task is not scheduled anymore ', task.id) + }).catch(error => { + console.debug('[assistant] pollPosition error', task.id, error.message) + }) // 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) @@ -224,9 +242,13 @@ export async function openAssistantForm({ resolve(finishedTask) view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null + cancelTaskPositionPolling() emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error.message) + view.taskPosition = null + cancelTaskPositionPolling() if (error.message === 'task-not-found') { view.loading = false view.showSyncTaskRunning = false @@ -239,8 +261,15 @@ export async function openAssistantForm({ }) }) .catch(error => { + if (controller.signal.aborted) { + return + } + if (window.assistantSchedulingAbortController === controller) { + cancelScheduling() + } view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null console.error('Assistant scheduling error', error?.response?.data?.ocs?.data?.message) showError(t('assistant', 'Assistant error') + ': ' + t('assistant', 'Something went wrong when scheduling the task')) }) @@ -250,6 +279,7 @@ export async function openAssistantForm({ syncSubmit(data.detail.inputs, data.detail.selectedTaskTypeId, customId || identifier) }) modalMountPoint.addEventListener('try-again', (data) => { + cancelScheduling() const task = data.detail console.debug('[assistant] try again', task) syncSubmit(task.input, task.type) @@ -257,8 +287,11 @@ export async function openAssistantForm({ modalMountPoint.addEventListener('load-task', (data) => { const task = data.detail console.debug('[assistant] loading task', task) + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.loading = false view.taskStatus = task.status @@ -271,6 +304,10 @@ export async function openAssistantForm({ if ([TASK_STATUS_STRING.scheduled, TASK_STATUS_STRING.running].includes(task?.status)) { getTask(task.id).then(response => { + if (task.id !== view.selectedTaskId) { + console.debug('[assistant] ignoring stale getTask response for task', task.id, 'selected is', view.selectedTaskId) + return + } const updatedTask = response.data?.ocs?.data?.task if (![TASK_STATUS_STRING.scheduled, TASK_STATUS_STRING.running].includes(updatedTask?.status)) { @@ -284,6 +321,9 @@ export async function openAssistantForm({ } getNotifyReady(task.id).then(response => { + if (task.id !== view.selectedTaskId) { + return + } view.isNotifyEnabled = !!response.data?.ocs?.data?.id }).catch(error => { console.error('[assistant] get task notification status error', error) @@ -291,6 +331,7 @@ export async function openAssistantForm({ view.loading = true view.showSyncTaskRunning = true + view.taskPosition = null view.progress = null view.expectedRuntime = (updatedTask?.completionExpectedAt - updatedTask?.scheduledAt) || null view.startedAt = lastTask?.startedAt || null @@ -299,6 +340,11 @@ export async function openAssistantForm({ const hasPush = listenToTaskNotifications(task.id) console.debug('[assistant] HAS PUSH', hasPush) + pollTaskPosition(updatedTask.id, view).then(() => { + console.debug('[assistant] pollTaskPosition: the task is not scheduled anymore', updatedTask.id) + }).catch(error => { + console.debug('[assistant] pollPosition error', updatedTask.id, error.message) + }) pollTask(updatedTask.id, view, !hasPush).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { @@ -322,9 +368,13 @@ export async function openAssistantForm({ // resolve(finishedTask) view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null + cancelTaskPositionPolling() emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error) + view.taskPosition = null + cancelTaskPositionPolling() if (error.message === 'task-not-found') { view.loading = false view.showSyncTaskRunning = false @@ -342,9 +392,12 @@ export async function openAssistantForm({ }) modalMountPoint.addEventListener('new-task', () => { console.debug('[assistant] new task') + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.outputs = null view.selectedTaskId = null @@ -357,11 +410,14 @@ export async function openAssistantForm({ }) }) modalMountPoint.addEventListener('cancel-task', () => { + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() setNotifyReady(lastTask.id, false) cancelTask(lastTask.id).then(res => { view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.selectedTaskId = null view.outputs = null view.taskStatus = null @@ -393,6 +449,71 @@ function updateTask(task, object, updateOutput = true) { object.completionExpectedAt = task?.completionExpectedAt } +function updateTaskPosition(position, object) { + object.taskPosition = position +} + +/** + * Poll the task position + * + * @param {number} taskId the task ID + * @param {object} obj the object to update + * @param {(position: number, obj: object) => void} callback the function to call to update the object + * @return {Promise} + */ +export async function pollTaskPosition(taskId, obj, callback = updateTaskPosition) { + const { isCancel } = await import('@nextcloud/axios') + return new Promise((resolve, reject) => { + const pollPositionOnce = () => { + if (window.assistantPollPositionTaskId !== taskId || window.assistantPollPositionTimerId === null) { + reject(new Error('pollTaskPosition cancelled')) + return + } + getTaskPosition(taskId, window.assistantPollPositionAbortController.signal).then(response => { + const taskPosition = response.data?.ocs?.data + if (window.assistantPollPositionTaskId !== taskId || window.assistantPollPositionTimerId === null) { + reject(new Error('pollTaskPosition cancelled')) + return + } + if (obj) { + callback(taskPosition, obj) + } + }).catch(error => { + const stopPolling = () => { + if (window.assistantPollPositionTaskId === taskId) { + clearInterval(window.assistantPollPositionTimerId) + window.assistantPollPositionTimerId = null + window.assistantPollPositionTaskId = null + } + } + if (isCancel(error)) { + stopPolling() + console.debug('[assistant] pollPosition request cancelled', error) + reject(new Error('pollTaskPosition request cancelled')) + return + } + console.debug('[assistant] pollPosition request failed', error) + if (error.status === 404) { + stopPolling() + reject(new Error('task-not-found')) + return + } else if (error.status === 412) { + stopPolling() + resolve() + return + } + console.warn('[assistant] pollPosition temporary failure, will retry', error) + }) + } + cancelTaskPositionPolling() + window.assistantPollPositionTaskId = taskId + window.assistantPollPositionAbortController = new AbortController() + window.assistantPollPositionTimerId = setInterval(pollPositionOnce, 5000) + // start polling immediately + pollPositionOnce() + }) +} + /** * Poll the task to update its status * @@ -405,9 +526,9 @@ function updateTask(task, object, updateOutput = true) { export async function pollTask(taskId, obj, updateOutput = true, callback = updateTask) { return new Promise((resolve, reject) => { const pollOnce = () => { - getTask(taskId).then(response => { + getTask(taskId, window.assistantPollAbortController.signal).then(response => { const task = response.data?.ocs?.data?.task - if (window.assistantPollTimerId === null) { + if (window.assistantPollTaskId !== taskId || window.assistantPollTimerId === null) { reject(new Error('pollTask cancelled')) return } @@ -423,14 +544,20 @@ export async function pollTask(taskId, obj, updateOutput = true, callback = upda }).catch(error => { console.debug('[assistant] poll request failed', error) if (error.status === 404) { - clearInterval(window.assistantPollTimerId) - window.assistantPollTimerId = null + if (window.assistantPollTaskId === taskId) { + clearInterval(window.assistantPollTimerId) + window.assistantPollTimerId = null + window.assistantPollTaskId = null + } reject(new Error('task-not-found')) return } - reject(new Error('pollTask request failed')) + console.warn('[assistant] poll temporary failure, will retry', error) }) } + cancelTaskPolling() + window.assistantPollTaskId = taskId + window.assistantPollAbortController = new AbortController() // start polling immediately // pollOnce() window.assistantPollTimerId = setInterval(pollOnce, 2000) @@ -438,17 +565,38 @@ export async function pollTask(taskId, obj, updateOutput = true, callback = upda } export async function cancelTaskPolling() { - window.assistantAbortController?.abort() + window.assistantPollAbortController?.abort() clearInterval(window.assistantPollTimerId) window.assistantPollTimerId = null + window.assistantPollTaskId = null } -export async function getTask(taskId) { - window.assistantAbortController = new AbortController() +export async function cancelTaskPositionPolling() { + window.assistantPollPositionAbortController?.abort() + clearInterval(window.assistantPollPositionTimerId) + window.assistantPollPositionTimerId = null + window.assistantPollPositionTaskId = null +} + +export async function cancelScheduling() { + window.assistantSchedulingAbortController?.abort() + window.assistantSchedulingAbortController = null +} + +export async function getTask(taskId, signal = null) { const { default: axios } = await import('@nextcloud/axios') const { generateOcsUrl } = await import('@nextcloud/router') const url = generateOcsUrl('taskprocessing/task/{taskId}', { taskId }) - return axios.get(url, { signal: window.assistantAbortController.signal }) + const config = signal ? { signal } : {} + return axios.get(url, config) +} + +export async function getTaskPosition(taskId, signal = null) { + const { default: axios } = await import('@nextcloud/axios') + const { generateOcsUrl } = await import('@nextcloud/router') + const url = generateOcsUrl('taskprocessing/tasks/{taskId}/queue_position', { taskId }) + const config = signal ? { signal } : {} + return axios.get(url, config) } export async function getNotifyReady(taskId) { @@ -481,10 +629,10 @@ export async function cancelTask(taskId) { * @param {string} customId the task custom ID * @param {string} taskType the task type class * @param {Array} inputs the task input texts as an array + * @param {AbortSignal} signal optional abort signal for cancellation * @return {Promise} */ -export async function scheduleTask(appId, customId, taskType, inputs) { - window.assistantAbortController = new AbortController() +export async function scheduleTask(appId, customId, taskType, inputs, signal = null) { const { default: axios } = await import('@nextcloud/axios') const { generateOcsUrl } = await import('@nextcloud/router') if (taskType === 'core:text2text:translate') { @@ -498,7 +646,8 @@ export async function scheduleTask(appId, customId, taskType, inputs) { customId, preferStreaming: true, } - return axios.post(url, params, { signal: window.assistantAbortController.signal }) + const config = signal ? { signal } : {} + return axios.post(url, params, config) } export async function saveLastSelectedTaskType(taskType) { @@ -687,16 +836,29 @@ export async function openAssistantTask( } modalMountPoint.addEventListener('cancel', () => { + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() app.unmount() OCA.Assistant.isAssistantDialogOpen = false }) modalMountPoint.addEventListener('submit', (data) => { - scheduleTask(task.appId, task.identifier ?? '', data.detail.selectedTaskTypeId, data.detail.inputs) + const controller = new AbortController() + window.assistantSchedulingAbortController = controller + scheduleTask(task.appId, task.identifier ?? '', data.detail.selectedTaskTypeId, data.detail.inputs, controller.signal) .then((response) => { + if (window.assistantSchedulingAbortController === controller) { + cancelScheduling() + } console.debug('scheduled task', response.data?.ocs?.data?.task) }) .catch(error => { + if (controller.signal.aborted) { + return + } + if (window.assistantSchedulingAbortController === controller) { + cancelScheduling() + } app.unmount() OCA.Assistant.isAssistantDialogOpen = false console.error('Assistant scheduling error', error) @@ -709,6 +871,7 @@ export async function openAssistantTask( const syncSubmit = (inputs, taskTypeId, newTaskCustomId = '') => { view.loading = true view.showSyncTaskRunning = true + view.taskPosition = null view.isNotifyEnabled = false view.expectedRuntime = null view.startedAt = null @@ -717,8 +880,13 @@ export async function openAssistantTask( view.outputs = null view.selectedTaskTypeId = taskTypeId - scheduleTask('assistant', newTaskCustomId, taskTypeId, inputs) + const controller = new AbortController() + window.assistantSchedulingAbortController = controller + scheduleTask('assistant', newTaskCustomId, taskTypeId, inputs, controller.signal) .then((response) => { + if (window.assistantSchedulingAbortController === controller) { + cancelScheduling() + } const task = response.data?.ocs?.data?.task lastTask = task view.selectedTaskId = lastTask?.id @@ -728,6 +896,11 @@ export async function openAssistantTask( const hasPush = listenToTaskNotifications(task.id) console.debug('[assistant] HAS PUSH', hasPush) + pollTaskPosition(task.id, view).then(() => { + console.debug('[assistant] pollTaskPosition: the task is not scheduled anymore', task.id) + }).catch(error => { + console.debug('[assistant] pollPosition error', task.id, error.message) + }) pollTask(task.id, view, !hasPush).then(finishedTask => { if (finishedTask.status === TASK_STATUS_STRING.successful) { view.outputs = finishedTask?.output @@ -749,10 +922,14 @@ export async function openAssistantTask( // resolve(finishedTask) view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null + cancelTaskPositionPolling() emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error) view.outputs = null + view.taskPosition = null + cancelTaskPositionPolling() if (error.message === 'task-not-found') { view.loading = false view.showSyncTaskRunning = false @@ -764,8 +941,15 @@ export async function openAssistantTask( }) }) .catch(error => { + if (controller.signal.aborted) { + return + } + if (window.assistantSchedulingAbortController === controller) { + cancelScheduling() + } view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null console.error('Assistant scheduling error', error?.response?.data?.ocs?.data?.message) showError(t('assistant', 'Assistant error') + ': ' + t('assistant', 'Something went wrong when scheduling the task')) }) @@ -774,13 +958,17 @@ export async function openAssistantTask( syncSubmit(data.detail.inputs, data.detail.selectedTaskTypeId, task.identifier ?? '') }) modalMountPoint.addEventListener('try-again', (data) => { + cancelScheduling() const task = data.detail syncSubmit(task.input, task.type) }) modalMountPoint.addEventListener('load-task', (data) => { const task = data.detail + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.loading = false view.taskStatus = task.status @@ -793,6 +981,10 @@ export async function openAssistantTask( if ([TASK_STATUS_STRING.scheduled, TASK_STATUS_STRING.running].includes(task?.status)) { getTask(task.id).then(response => { + if (task.id !== view.selectedTaskId) { + console.debug('[assistant] ignoring stale getTask response for task', task.id, 'selected is', view.selectedTaskId) + return + } const updatedTask = response.data?.ocs?.data?.task if (![TASK_STATUS_STRING.scheduled, TASK_STATUS_STRING.running].includes(updatedTask?.status)) { @@ -806,6 +998,9 @@ export async function openAssistantTask( } getNotifyReady(task.id).then(response => { + if (task.id !== view.selectedTaskId) { + return + } view.isNotifyEnabled = !!response.data?.ocs?.data?.id }).catch(error => { console.error('[assistant] get task notification status error', error) @@ -813,6 +1008,7 @@ export async function openAssistantTask( view.loading = true view.showSyncTaskRunning = true + view.taskPosition = null view.progress = null view.expectedRuntime = (updatedTask?.completionExpectedAt - updatedTask?.scheduledAt) || null view.startedAt = lastTask?.startedAt || null @@ -820,6 +1016,11 @@ export async function openAssistantTask( const hasPush = listenToTaskNotifications(task.id) + pollTaskPosition(updatedTask.id, view).then(() => { + console.debug('[assistant] pollTaskPosition: the task is not scheduled anymore', updatedTask.id) + }).catch(error => { + console.debug('[assistant] pollPosition error', updatedTask.id, error.message) + }) pollTask(updatedTask.id, view, !hasPush).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { @@ -843,9 +1044,13 @@ export async function openAssistantTask( // resolve(finishedTask) view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null + cancelTaskPositionPolling() emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error) + view.taskPosition = null + cancelTaskPositionPolling() if (error.message === 'task-not-found') { view.loading = false view.showSyncTaskRunning = false @@ -863,9 +1068,12 @@ export async function openAssistantTask( }) modalMountPoint.addEventListener('new-task', () => { console.debug('[assistant] new task') + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.outputs = null view.selectedTaskId = null @@ -878,11 +1086,14 @@ export async function openAssistantTask( }) }) modalMountPoint.addEventListener('cancel-task', () => { + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() setNotifyReady(lastTask.id, false) cancelTask(lastTask.id).then(res => { view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.selectedTaskId = null view.outputs = null view.taskStatus = null diff --git a/src/components/AssistantTextProcessingForm.vue b/src/components/AssistantTextProcessingForm.vue index da3c374c..b3433ff8 100644 --- a/src/components/AssistantTextProcessingForm.vue +++ b/src/components/AssistantTextProcessingForm.vue @@ -46,6 +46,7 @@ class="running-area" :description="shortInput" :progress="progress" + :task-position="taskPosition" :expected-runtime="expectedRuntime" :started-at="startedAt" :completion-expected-at="completionExpectedAt" @@ -286,6 +287,10 @@ export default { type: Boolean, default: false, }, + taskPosition: { + type: [Number, null], + default: null, + }, shortInput: { type: String, required: true, diff --git a/src/components/AssistantTextProcessingModal.vue b/src/components/AssistantTextProcessingModal.vue index 77a6fc06..5d03194d 100644 --- a/src/components/AssistantTextProcessingModal.vue +++ b/src/components/AssistantTextProcessingModal.vue @@ -34,6 +34,7 @@ :loading="loading" :action-buttons="actionButtons" :show-sync-task-running="showSyncTaskRunning" + :task-position="taskPosition" :short-input="shortInput" :progress="progress" :expected-runtime="expectedRuntime" @@ -135,6 +136,7 @@ export default { completionExpectedAt: null, isNotifyEnabled: false, showSyncTaskRunning: false, + taskPosition: null, showScheduleConfirmation: false, // from props selectedTaskId: this.initSelectedTaskId, diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index 4225308a..dc238c9e 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -259,6 +259,7 @@ import { SHAPE_TYPE_NAMES, TASK_STATUS_INT } from '../../constants.js' import ICAL from 'ical.js' import formatRecurrenceRule from './recurrenceRule.js' import { getLanguage } from '@nextcloud/l10n' +import { getTaskPosition } from '../../assistant.js' import navAutoCollapse from '../../mixins/navAutoCollapse.js' @@ -343,6 +344,7 @@ export default { newSession: false, messageDelete: false, sessionDelete: false, + taskPosition: null, }, msgCursor: 0, msgLimit: 20, @@ -443,6 +445,7 @@ export default { this.allMessagesLoaded = false this.loading.llmGeneration = false this.loading.llmRunning = false + this.loading.taskPosition = null this.loading.titleGeneration = false this.streamingMessage = null this.chatContent = '' @@ -562,6 +565,7 @@ export default { } finally { this.loading.llmGeneration = false this.loading.llmRunning = false + this.loading.taskPosition = null this.loading.titleGeneration = false if (isAssignment) { this.pollCheckSessionTimeout = setTimeout(() => { this.checkSession(sessionId, isAssignment) }, 5000) @@ -931,6 +935,7 @@ export default { this.slowPickup = false this.loading.llmGeneration = true this.loading.llmRunning = false + this.loading.taskPosition = null this.userScrolled = false const params = { sessionId, @@ -956,6 +961,7 @@ export default { } finally { this.loading.llmGeneration = false this.loading.llmRunning = false + this.loading.taskPosition = null this.streamingMessage = null this.userScrolled = false } @@ -966,6 +972,7 @@ export default { const sessionId = this.active.id this.loading.llmGeneration = true this.loading.llmRunning = false + this.loading.taskPosition = null this.userScrolled = false const regenerationResponse = await axios.get(getChatURL('/regenerate'), { params: { messageId, sessionId } }) const regenerationResponseData = regenerationResponse.data @@ -984,6 +991,7 @@ export default { } finally { this.loading.llmGeneration = false this.loading.llmRunning = false + this.loading.taskPosition = null this.streamingMessage = null this.userScrolled = false } @@ -1064,6 +1072,19 @@ export default { this.slowPickup = error.response.data.slow_pickup if (error.response.data.task_status === TASK_STATUS_INT.running) { this.loading.llmRunning = true + } else if (error.response.data.task_status === TASK_STATUS_INT.scheduled) { + getTaskPosition(taskId) + .then(response => { + if (sessionId !== this.active?.id) { + return + } + const taskPosition = response.data?.ocs?.data + this.loading.taskPosition = taskPosition + console.debug('Task position:', taskPosition) + }) + .catch(error => { + console.error('Failed to get task position', error) + }) } if (!hasPush && typeof error.response.data.task_output !== 'undefined' && error.response.data.task_output !== null) { this.updateStreamingMessage(error.response.data.task_output || {}, sessionId) diff --git a/src/components/ChattyLLM/InputArea.vue b/src/components/ChattyLLM/InputArea.vue index baa88850..53e97633 100644 --- a/src/components/ChattyLLM/InputArea.vue +++ b/src/components/ChattyLLM/InputArea.vue @@ -141,6 +141,7 @@ export default { newSession: false, messageDelete: false, sessionDelete: false, + taskPosition: null, }), }, }, @@ -190,6 +191,9 @@ export default { ? this.loading.llmRunning ? this.thinkingText : this.scheduledText + + ((this.loading.taskPosition !== null && this.loading.taskPosition !== undefined) + ? ' ' + t('assistant', 'Task position: {position}', { position: this.loading.taskPosition }) + : '') : this.placeholderText }, }, diff --git a/src/components/RunningEmptyContent.vue b/src/components/RunningEmptyContent.vue index 9379fce2..d3c1c004 100644 --- a/src/components/RunningEmptyContent.vue +++ b/src/components/RunningEmptyContent.vue @@ -14,8 +14,13 @@ -
- {{ formattedRuntime }} +
+ + {{ formattedRuntime }} + + + {{ formattedPosition }} +
{{ t('assistant', 'This task is running in the background.') }} @@ -83,6 +88,10 @@ export default { type: [Number, null], default: null, }, + taskPosition: { + type: [Number, null], + default: null, + }, expectedRuntime: { type: [Number, null], default: null, @@ -144,6 +153,12 @@ export default { } return t('assistant', 'This may take a few minutes…') }, + formattedPosition() { + if (this.taskPosition === null || this.taskStatus !== TASK_STATUS_STRING.scheduled) { + return '' + } + return t('assistant', 'Task position: {position}', { position: this.taskPosition }) + }, progressMessage() { if (this.taskStatus === TASK_STATUS_STRING.scheduled || this.taskStatus === null) { return t('assistant', 'Waiting…') @@ -208,5 +223,10 @@ export default { .info-text-block { text-align: center; } + + .inline { + display: flex; + gap: 4px; + } } diff --git a/src/views/AssistantPage.vue b/src/views/AssistantPage.vue index 62fa0c77..92dcea77 100644 --- a/src/views/AssistantPage.vue +++ b/src/views/AssistantPage.vue @@ -14,6 +14,7 @@ :selected-task-type-id="task.type" :loading="loading" :show-sync-task-running="showSyncTaskRunning" + :task-position="taskPosition" :short-input="shortInput" :task-status="task.status" :scheduled-at="task.scheduledAt" @@ -44,10 +45,13 @@ import { emit } from '@nextcloud/event-bus' import { loadState } from '@nextcloud/initial-state' import { listen } from '@nextcloud/notify_push' import { + cancelScheduling, cancelTask, cancelTaskPolling, + cancelTaskPositionPolling, getTask, pollTask, + pollTaskPosition, scheduleTask, setNotifyReady, } from '../assistant.js' @@ -69,6 +73,7 @@ export default { return { task: loadState('assistant', 'task'), showSyncTaskRunning: false, + taskPosition: null, progress: null, loading: false, isNotifyEnabled: false, @@ -113,12 +118,15 @@ export default { } }, onCancel() { + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() if (this.task?.id) { setNotifyReady(this.task.id, false) cancelTask(this.task.id).then(res => { this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null this.task.id = null this.task.output = null this.task.status = null @@ -127,6 +135,7 @@ export default { // if we ever end up in this state, this helps to recover this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null this.task.id = null this.task.output = null this.task.status = null @@ -154,6 +163,7 @@ export default { syncSubmit(inputs, taskTypeId, newTaskIdentifier = '') { this.loading = true this.showSyncTaskRunning = true + this.taskPosition = null this.isNotifyEnabled = false this.progress = null this.task.completionExpectedAt = null @@ -162,8 +172,13 @@ export default { this.task.input = inputs this.task.output = null this.task.type = taskTypeId - scheduleTask('assistant', this.task.identifier, taskTypeId, inputs) + const controller = new AbortController() + window.assistantSchedulingAbortController = controller + scheduleTask('assistant', this.task.identifier, taskTypeId, inputs, controller.signal) .then((response) => { + if (window.assistantSchedulingAbortController === controller) { + cancelScheduling() + } console.debug('Assistant SYNC result', response.data?.ocs?.data) const task = response.data?.ocs?.data?.task this.task.id = task.id @@ -174,6 +189,11 @@ export default { const hasPush = this.listenToTaskNotifications(task.id) console.debug('[assistant] HAS PUSH', hasPush) + pollTaskPosition(task.id, this).then(() => { + console.debug('[assistant] pollTaskPosition finished', task.id) + }).catch(error => { + console.debug('[assistant] pollPosition error', task.id, error.message) + }) pollTask(task.id, this, !hasPush, this.updateTask).then(finishedTask => { if (finishedTask.status === TASK_STATUS_STRING.successful) { this.task.output = finishedTask?.output @@ -184,9 +204,14 @@ export default { } this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null + // the position polling would stop on the next request but why not stopping it right now + cancelTaskPositionPolling() emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error) + this.taskPosition = null + cancelTaskPositionPolling() if (error.message === 'task-not-found') { this.loading = false this.showSyncTaskRunning = false @@ -199,8 +224,15 @@ export default { }) }) .catch(error => { + if (controller.signal.aborted) { + return + } + if (window.assistantSchedulingAbortController === controller) { + cancelScheduling() + } this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null console.error('Assistant scheduling error', error?.response?.data?.ocs?.data?.message) showError(t('assistant', 'Assistant error') + ': ' + t('assistant', 'Something went wrong when scheduling the task')) }) @@ -222,11 +254,15 @@ export default { this.syncSubmit(data.inputs, data.selectedTaskTypeId, this.task.identifier) }, onTryAgain(task) { + cancelScheduling() this.syncSubmit(task.input, task.type) }, onLoadTask(task) { + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() this.showSyncTaskRunning = false + this.taskPosition = null this.loading = false this.task.type = task.type @@ -237,6 +273,10 @@ export default { if ([TASK_STATUS_STRING.scheduled, TASK_STATUS_STRING.running].includes(task?.status)) { getTask(task.id).then(response => { + if (task.id !== this.task.id) { + console.debug('[assistant] ignoring stale getTask response for task', task.id, 'selected is', this.task.id) + return + } const updatedTask = response.data?.ocs?.data?.task if (![TASK_STATUS_STRING.scheduled, TASK_STATUS_STRING.running].includes(updatedTask?.status)) { @@ -249,6 +289,7 @@ export default { this.loading = true this.showSyncTaskRunning = true + this.taskPosition = null this.progress = null this.task.completionExpectedAt = updatedTask.completionExpectedAt this.task.startedAt = updatedTask.startedAt @@ -256,6 +297,11 @@ export default { const hasPush = this.listenToTaskNotifications(task.id) + pollTaskPosition(updatedTask.id, this).then(() => { + console.debug('[assistant] pollTaskPosition finished', updatedTask.id) + }).catch(error => { + console.debug('[assistant] pollPosition error', updatedTask.id, error.message) + }) pollTask(updatedTask.id, this, !hasPush, this.updateTask).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { @@ -269,9 +315,13 @@ export default { // resolve(finishedTask) this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null + cancelTaskPositionPolling() emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('Assistant poll error', error) + this.taskPosition = null + cancelTaskPositionPolling() if (error.message === 'task-not-found') { this.loading = false this.showSyncTaskRunning = false @@ -288,9 +338,12 @@ export default { } }, onNewTask() { + cancelScheduling() cancelTaskPolling() + cancelTaskPositionPolling() this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null this.isNotifyEnabled = false this.task.status = TASK_STATUS_STRING.unknown this.task.output = null