diff --git a/src/assistant.js b/src/assistant.js index 80279948..9f72aa9e 100644 --- a/src/assistant.js +++ b/src/assistant.js @@ -11,6 +11,7 @@ import Aura from '@primeuix/themes/aura' import { listen } from '@nextcloud/notify_push' window.assistantPollTimerId = null +window.assistantPollPositionTimerId = null listen('taskprocessing:task_update', (type, body) => { console.debug('[assistant] received task update push notification', type, body) @@ -168,6 +169,7 @@ export async function openAssistantForm({ modalMountPoint.addEventListener('cancel', () => { cancelTaskPolling() + cancelTaskPositionPolling() app.unmount() OCA.Assistant.isAssistantDialogOpen = false reject(new Error('User cancellation')) @@ -175,6 +177,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 @@ -196,6 +199,11 @@ export async function openAssistantForm({ const hasPush = listenToTaskNotifications(task.id) console.debug('[assistant] HAS PUSH', hasPush) + pollTaskPosition(task.id, view).then(() => { + console.debug('[assistant] pollTaskPosition finished') + }).catch(error => { + console.debug('[assistant] pollPosition error', 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,12 +232,14 @@ export async function openAssistantForm({ resolve(finishedTask) view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error.message) if (error.message === 'task-not-found') { view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.outputs = null view.selectedTaskId = null @@ -241,6 +251,7 @@ export async function openAssistantForm({ .catch(error => { 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')) }) @@ -258,7 +269,9 @@ export async function openAssistantForm({ const task = data.detail console.debug('[assistant] loading task', task) cancelTaskPolling() + cancelTaskPositionPolling() view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.loading = false view.taskStatus = task.status @@ -291,6 +304,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 +313,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 finished') + }).catch(error => { + console.debug('[assistant] pollPosition error', error.message) + }) pollTask(updatedTask.id, view, !hasPush).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { @@ -322,12 +341,14 @@ export async function openAssistantForm({ // resolve(finishedTask) view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error) if (error.message === 'task-not-found') { view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.outputs = null view.selectedTaskId = null @@ -343,8 +364,10 @@ export async function openAssistantForm({ modalMountPoint.addEventListener('new-task', () => { console.debug('[assistant] new task') cancelTaskPolling() + cancelTaskPositionPolling() view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.outputs = null view.selectedTaskId = null @@ -358,10 +381,12 @@ export async function openAssistantForm({ }) modalMountPoint.addEventListener('cancel-task', () => { 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 +418,55 @@ 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 {Function} callback the function to call to update the object + * @return {Promise<*>} + */ +export async function pollTaskPosition(taskId, obj, callback = updateTaskPosition) { + return new Promise((resolve, reject) => { + const pollPositionOnce = () => { + if (window.assistantPollPositionTimerId === null) { + reject(new Error('pollTaskPosition cancelled')) + return + } + getTaskPosition(taskId).then(response => { + const taskPosition = response.data?.ocs?.data + if (window.assistantPollPositionTimerId === null) { + reject(new Error('pollTaskPosition cancelled')) + return + } + if (obj) { + callback(taskPosition, obj) + } + }).catch(error => { + console.debug('[assistant] pollPosition request failed', error) + clearInterval(window.assistantPollPositionTimerId) + window.assistantPollPositionTimerId = null + if (error.status === 404) { + reject(new Error('task-not-found')) + return + } else if (error.status === 412) { + // the task is not scheduled anymore + resolve() + return + } + reject(new Error('pollTaskPosition request failed')) + }) + } + // start polling immediately + pollPositionOnce() + window.assistantPollPositionTimerId = setInterval(pollPositionOnce, 5000) + }) +} + /** * Poll the task to update its status * @@ -443,6 +517,11 @@ export async function cancelTaskPolling() { window.assistantPollTimerId = null } +export async function cancelTaskPositionPolling() { + clearInterval(window.assistantPollPositionTimerId) + window.assistantPollPositionTimerId = null +} + export async function getTask(taskId) { window.assistantAbortController = new AbortController() const { default: axios } = await import('@nextcloud/axios') @@ -451,6 +530,13 @@ export async function getTask(taskId) { return axios.get(url, { signal: window.assistantAbortController.signal }) } +export async function getTaskPosition(taskId) { + const { default: axios } = await import('@nextcloud/axios') + const { generateOcsUrl } = await import('@nextcloud/router') + const url = generateOcsUrl('taskprocessing/tasks/{taskId}/queue_position', { taskId }) + return axios.get(url, {}) +} + export async function getNotifyReady(taskId) { const { default: axios } = await import('@nextcloud/axios') const { generateOcsUrl } = await import('@nextcloud/router') @@ -688,6 +774,7 @@ export async function openAssistantTask( modalMountPoint.addEventListener('cancel', () => { cancelTaskPolling() + cancelTaskPositionPolling() app.unmount() OCA.Assistant.isAssistantDialogOpen = false }) @@ -709,6 +796,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 @@ -728,6 +816,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 finished') + }).catch(error => { + console.debug('[assistant] pollPosition error', error.message) + }) pollTask(task.id, view, !hasPush).then(finishedTask => { if (finishedTask.status === TASK_STATUS_STRING.successful) { view.outputs = finishedTask?.output @@ -749,6 +842,7 @@ export async function openAssistantTask( // resolve(finishedTask) view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error) @@ -756,6 +850,7 @@ export async function openAssistantTask( if (error.message === 'task-not-found') { view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.selectedTaskId = null lastTask = null @@ -766,6 +861,7 @@ export async function openAssistantTask( .catch(error => { 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')) }) @@ -780,7 +876,9 @@ export async function openAssistantTask( modalMountPoint.addEventListener('load-task', (data) => { const task = data.detail cancelTaskPolling() + cancelTaskPositionPolling() view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.loading = false view.taskStatus = task.status @@ -813,6 +911,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 +919,11 @@ export async function openAssistantTask( const hasPush = listenToTaskNotifications(task.id) + pollTaskPosition(updatedTask.id, view).then(() => { + console.debug('[assistant] pollTaskPosition finished') + }).catch(error => { + console.debug('[assistant] pollPosition error', error.message) + }) pollTask(updatedTask.id, view, !hasPush).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { @@ -843,12 +947,14 @@ export async function openAssistantTask( // resolve(finishedTask) view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error) if (error.message === 'task-not-found') { view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.outputs = null view.selectedTaskId = null @@ -864,8 +970,10 @@ export async function openAssistantTask( modalMountPoint.addEventListener('new-task', () => { console.debug('[assistant] new task') cancelTaskPolling() + cancelTaskPositionPolling() view.loading = false view.showSyncTaskRunning = false + view.taskPosition = null view.isNotifyEnabled = false view.outputs = null view.selectedTaskId = null @@ -879,10 +987,12 @@ export async function openAssistantTask( }) modalMountPoint.addEventListener('cancel-task', () => { 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 667c26e7..33ebacf7 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" @@ -283,6 +284,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 3b05f661..b4703a8d 100644 --- a/src/components/AssistantTextProcessingModal.vue +++ b/src/components/AssistantTextProcessingModal.vue @@ -33,6 +33,7 @@ :loading="loading" :action-buttons="actionButtons" :show-sync-task-running="showSyncTaskRunning" + :task-position="taskPosition" :short-input="shortInput" :progress="progress" :expected-runtime="expectedRuntime" @@ -130,6 +131,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 51cda77f..530e4a37 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' // future: type (text, image, file, etc), attachments, etc support @@ -339,6 +340,7 @@ export default { newSession: false, messageDelete: false, sessionDelete: false, + taskPosition: null, }, msgCursor: 0, msgLimit: 20, @@ -439,6 +441,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 = '' @@ -559,6 +562,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) @@ -924,6 +928,7 @@ export default { this.slowPickup = false this.loading.llmGeneration = true this.loading.llmRunning = false + this.loading.taskPosition = null this.userScrolled = false const params = { sessionId, @@ -949,6 +954,7 @@ export default { } finally { this.loading.llmGeneration = false this.loading.llmRunning = false + this.loading.taskPosition = null this.streamingMessage = null this.userScrolled = false } @@ -959,6 +965,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 @@ -977,6 +984,7 @@ export default { } finally { this.loading.llmGeneration = false this.loading.llmRunning = false + this.loading.taskPosition = null this.streamingMessage = null this.userScrolled = false } @@ -1058,6 +1066,12 @@ 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 => { + const taskPosition = response.data?.ocs?.data + this.loading.taskPosition = taskPosition + console.debug('Task position:', taskPosition) + }) } 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 a40c24c4..629a27d8 100644 --- a/src/components/ChattyLLM/InputArea.vue +++ b/src/components/ChattyLLM/InputArea.vue @@ -89,6 +89,7 @@ export default { newSession: false, messageDelete: false, sessionDelete: false, + taskPosition: null, }), }, }, @@ -122,6 +123,7 @@ export default { ? this.loading.llmRunning ? this.thinkingText : this.scheduledText + + (this.loading.taskPosition ? ' ' + t('assistant', 'Task position: {position}', { position: this.loading.taskPosition }) : '') : this.placeholderText }, }, diff --git a/src/components/RunningEmptyContent.vue b/src/components/RunningEmptyContent.vue index 2036b8a7..a2cd9b5e 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 bba4a8c0..5d493a61 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" @@ -46,8 +47,10 @@ import { listen } from '@nextcloud/notify_push' import { cancelTask, cancelTaskPolling, + cancelTaskPositionPolling, getTask, pollTask, + pollTaskPosition, scheduleTask, setNotifyReady, } from '../assistant.js' @@ -69,6 +72,7 @@ export default { return { task: loadState('assistant', 'task'), showSyncTaskRunning: false, + taskPosition: null, progress: null, loading: false, isNotifyEnabled: false, @@ -114,11 +118,13 @@ export default { }, onCancel() { 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 +133,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 +161,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 @@ -174,6 +182,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') + }).catch(error => { + console.debug('[assistant] pollPosition error', error.message) + }) pollTask(task.id, this, !hasPush, this.updateTask).then(finishedTask => { if (finishedTask.status === TASK_STATUS_STRING.successful) { this.task.output = finishedTask?.output @@ -184,12 +197,14 @@ export default { } this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('[assistant] poll error', error) if (error.message === 'task-not-found') { this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null this.isNotifyEnabled = false this.task.status = TASK_STATUS_STRING.unknown this.task.output = null @@ -201,6 +216,7 @@ export default { .catch(error => { 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')) }) @@ -226,7 +242,9 @@ export default { }, onLoadTask(task) { cancelTaskPolling() + cancelTaskPositionPolling() this.showSyncTaskRunning = false + this.taskPosition = null this.loading = false this.task.type = task.type @@ -249,6 +267,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 +275,11 @@ export default { const hasPush = this.listenToTaskNotifications(task.id) + pollTaskPosition(updatedTask.id, this).then(() => { + console.debug('[assistant] pollTaskPosition finished') + }).catch(error => { + console.debug('[assistant] pollPosition error', error.message) + }) pollTask(updatedTask.id, this, !hasPush, this.updateTask).then(finishedTask => { console.debug('pollTask.then', finishedTask) if (finishedTask.status === TASK_STATUS_STRING.successful) { @@ -269,12 +293,14 @@ export default { // resolve(finishedTask) this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null emit('assistant:task:updated', finishedTask) }).catch(error => { console.debug('Assistant poll error', error) if (error.message === 'task-not-found') { this.loading = false this.showSyncTaskRunning = false + this.taskPosition = null this.isNotifyEnabled = false this.task.status = TASK_STATUS_STRING.unknown this.task.output = null @@ -289,8 +315,10 @@ export default { }, onNewTask() { 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