From 78421bf8de5a1b7c1a21edf2c744023d76c18301 Mon Sep 17 00:00:00 2001 From: Lukas Schaefer Date: Thu, 9 Jul 2026 17:32:23 -0400 Subject: [PATCH 1/6] feat: support multimodal chat Signed-off-by: Lukas Schaefer --- .../BeforeTemplateRenderedListener.php | 2 + lib/Service/ChatService.php | 69 ++++++- .../ChattyLLM/ChattyLLMInputForm.vue | 10 +- src/components/ChattyLLM/DocumentPreviews.vue | 82 ++++++++ src/components/ChattyLLM/InputArea.vue | 180 ++++++++++++++---- src/components/ChattyLLM/Message.vue | 11 ++ src/constants.js | 1 + 7 files changed, 316 insertions(+), 39 deletions(-) create mode 100644 src/components/ChattyLLM/DocumentPreviews.vue diff --git a/lib/Listener/BeforeTemplateRenderedListener.php b/lib/Listener/BeforeTemplateRenderedListener.php index 2c4fd6f0d..209f31d9f 100644 --- a/lib/Listener/BeforeTemplateRenderedListener.php +++ b/lib/Listener/BeforeTemplateRenderedListener.php @@ -72,6 +72,8 @@ public function handle(Event $event): void { $this->initialStateService->provideInitialState('contextChatIndexingComplete', $indexingComplete); $this->initialStateService->provideInitialState('contextAgentToolSources', $this->assistantService->informationSources); $this->initialStateService->provideInitialState('audio_chat_available', $this->assistantService->isAudioChatAvailable()); + $multimodalChatAvailable = class_exists('OCP\\TaskProcessing\\TaskTypes\\MultimodalChatWithTools') && array_key_exists(\OCP\TaskProcessing\TaskTypes\MultimodalChatWithTools::ID, $this->taskProcessingManager->getAvailableTaskTypes()); + $this->initialStateService->provideInitialState('multimodal_chat_available', $multimodalChatAvailable); $autoplayAudioChat = $this->config->getUserValue($this->userId, Application::APP_ID, 'autoplay_audio_chat', '1') === '1'; $this->initialStateService->provideInitialState('autoplay_audio_chat', $autoplayAudioChat); $agencyAvailable = class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentInteraction') && array_key_exists(\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID, $this->taskProcessingManager->getAvailableTaskTypes()); diff --git a/lib/Service/ChatService.php b/lib/Service/ChatService.php index 0a5986a5a..fd7014e3b 100644 --- a/lib/Service/ChatService.php +++ b/lib/Service/ChatService.php @@ -447,13 +447,27 @@ public function scheduleMessageGeneration(?string $userId, int $sessionId, int $ $taskId = $this->scheduleAudioChatTask($userId, $fileId, $systemPrompt, $history, $sessionId, $lastUserMessage->getId()); } else { // for a text chat task, let's only use text in the history - $history = array_map(static function (Message $message) { + $historyMessages = array_map(static function (Message $message) { return json_encode([ 'role' => $message->getRole(), 'content' => $message->getContent(), ]); }, $history); - $taskId = $this->scheduleLLMChatTask($userId, $lastUserMessage->getContent(), $systemPrompt, $history, $sessionId); + if (count($lastAttachments) > 0 && $this->isMultimodalChatAvailable()) { + // for a multimodal chat task, let's use the attachments in the history as we don't know the structure in history + $attachmentsHistory = array_map(static function (Message $message) { + return $message->jsonSerialize()['attachments']; + }, $history); + // Just add all the attachments in the history as attachments for the latest message as we can't know the structure in history + $attachmentsHistory = array_merge($lastAttachments, ...$attachmentsHistory); + $attachmentsHistory = array_map(static function (array $attachment) { + return $attachment['file_id']; + }, $attachmentsHistory); + + $taskId = $this->scheduleMultimodalChatTask($userId, $lastUserMessage->getContent(), $systemPrompt, $historyMessages, $sessionId, $attachmentsHistory); + } else { + $taskId = $this->scheduleLLMChatTask($userId, $lastUserMessage->getContent(), $systemPrompt, $historyMessages, $sessionId); + } } } return $taskId; @@ -567,6 +581,14 @@ public function isContextAgentAudioAvailable(): bool { return in_array(\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID, $this->taskProcessingManager->getAvailableTaskTypeIds()); } + public function isMultimodalChatAvailable(): bool { + if (!class_exists('OCP\\TaskProcessing\\TaskTypes\\MultimodalChatWithTools')) { + return false; + } + return in_array(\OCP\TaskProcessing\TaskTypes\MultimodalChatWithTools::ID, $this->taskProcessingManager->getAvailableTaskTypeIds()); + } + + private function getAudioHistory(array $history): array { // history is a list of JSON strings // The content is the remote audio ID (or the transcription as fallback) @@ -682,6 +704,49 @@ private function scheduleLLMChatTask( return $task->getId() ?? 0; } + /** + * Schedule a Multimodal Chat task + * + * @throws BadRequestException + * @throws InternalException + */ + private function scheduleMultimodalChatTask( + ?string $userId, + string $content, + string $systemPrompt, + array $history, + int $sessionId, + array $attachmentsHistory, + ): int { + $customId = 'chatty-llm:' . $sessionId; + $this->checkIfSessionIsThinking($userId, $customId); + $input = [ + 'input' => $content, + 'system_prompt' => $systemPrompt, + 'history' => $history, + 'input_attachments' => $attachmentsHistory, + 'tools' => '[]', // Empty tools as there is not a non tools version + 'tool_message' => '', + ]; + /** @psalm-suppress UndefinedClass */ + $task = new Task(\OCP\TaskProcessing\TaskTypes\MultimodalChatWithTools::ID, $input, Application::APP_ID . ':chatty-llm', $userId, $customId); + /** @psalm-suppress UndefinedMethod */ + $task->setPreferStreaming(true); + try { + $this->taskProcessingManager->scheduleTask($task); + } catch (PreConditionNotMetException $e) { + throw new BadRequestException('pre_condition_not_met', previous: $e); + } catch (\OCP\TaskProcessing\Exception\UnauthorizedException $e) { + throw new BadRequestException('unauthorized', previous: $e); + } catch (ValidationException $e) { + throw new BadRequestException('validation_failed', previous: $e); + } catch (\OCP\TaskProcessing\Exception\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalException(previous: $e); + } + return $task->getId() ?? 0; + } + /** * Schedule an agency chat task * diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index da2b310c7..0d93d4514 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -665,16 +665,20 @@ export default { return session.timestamp ? (' ' + moment(session.timestamp * 1000).format('LLL')) : t('assistant', 'Untitled conversation') }, - async handleSubmit(event) { + async handleSubmit(attachedFileIds) { if (this.chatContent.trim() === '') { console.debug('empty message') return } + const attachments = attachedFileIds.map(fileId => ({ type: SHAPE_TYPE_NAMES.File, file_id: fileId })) const role = Roles.HUMAN const content = this.chatContent.trim() const timestamp = +new Date() / 1000 | 0 console.debug('[Assistant] submit text', content) + if (attachedFileIds.length > 0) { + console.debug('[Assistant] submit attachments', attachments) + } if (this.active === null) { await this.newSession() @@ -686,10 +690,10 @@ export default { this.active.agencyAnswered = true } - this.messages.push({ role, content, timestamp, session_id: this.active.id }) + this.messages.push({ role, content, timestamp, session_id: this.active.id, attachments }) this.chatContent = '' this.scrollToLastMessage() - await this.newMessage(role, content, timestamp, this.active.id) + await this.newMessage(role, content, timestamp, this.active.id, attachments) }, async handleSubmitAudio(fileId) { diff --git a/src/components/ChattyLLM/DocumentPreviews.vue b/src/components/ChattyLLM/DocumentPreviews.vue new file mode 100644 index 000000000..06e5192a3 --- /dev/null +++ b/src/components/ChattyLLM/DocumentPreviews.vue @@ -0,0 +1,82 @@ + + + + + + diff --git a/src/components/ChattyLLM/InputArea.vue b/src/components/ChattyLLM/InputArea.vue index a40c24c47..36ab20347 100644 --- a/src/components/ChattyLLM/InputArea.vue +++ b/src/components/ChattyLLM/InputArea.vue @@ -4,53 +4,97 @@ --> @@ -187,11 +294,16 @@ export default { .input-area { display: flex; - flex-direction: row; - justify-content: space-between; - align-items: end; + flex-direction: column; gap: 4px; + &__row { + display: flex; + flex-direction: row; + align-items: end; + gap: 4px; + } + :deep(&__thinking > div) { font-style: italic; animation: breathing 2s linear infinite normal; diff --git a/src/components/ChattyLLM/Message.vue b/src/components/ChattyLLM/Message.vue index fa05f8019..fb46a4104 100644 --- a/src/components/ChattyLLM/Message.vue +++ b/src/components/ChattyLLM/Message.vue @@ -103,6 +103,12 @@ :file-id="a.file_id" :task-id="message.role === 'human' ? undefined : (a.ocp_task_id ?? message.ocp_task_id)" :is-output="message.role === 'assistant'" /> + @@ -126,6 +132,7 @@ import { showSuccess } from '@nextcloud/dialogs' import { generateOcsUrl } from '@nextcloud/router' import axios from '@nextcloud/axios' import { SHAPE_TYPE_NAMES } from '../../constants.js' +import FileDisplay from '../fields/FileDisplay.vue' const PLAIN_URL_PATTERN = /(?:\s|^|\()((?:https?:\/\/)(?:[-A-Z0-9+_.]+(?::[0-9]+)?(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*))(?:\s|$|\))/ig const MARKDOWN_LINK_PATTERN = /\[[-A-Z0-9+&@#%?=~_|!:,.;()]+\]\(((?:https?:\/\/)(?:[-A-Z0-9+_.]+(?::[0-9]+)?(?:\/[-A-Z0-9+&@#%?=~_|!:,.;]*)*))\)/ig @@ -135,6 +142,7 @@ export default { components: { AudioDisplay, + FileDisplay, AssistantIcon, NcAvatar, @@ -213,6 +221,9 @@ export default { audioAttachments() { return this.message.attachments?.filter(a => a.type === SHAPE_TYPE_NAMES.Audio) ?? [] }, + fileAttachments() { + return this.message.attachments?.filter(a => a.type === SHAPE_TYPE_NAMES.File) ?? [] + }, }, watch: { diff --git a/src/constants.js b/src/constants.js index 750349551..d71a33b7e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,6 +4,7 @@ */ export const MAX_TEXT_INPUT_LENGTH = 64_000 +export const MAX_ATTACHED_FILES = 10 export const TASK_STATUS_INT = { cancelled: 5, From 0f45478fdc5125a93a612e429bcefe149f4399cf Mon Sep 17 00:00:00 2001 From: Lukas Schaefer Date: Fri, 10 Jul 2026 14:59:20 -0400 Subject: [PATCH 2/6] feat: support multimodal chat outputs Signed-off-by: Lukas Schaefer --- lib/Listener/ChattyLLMTaskListener.php | 12 ++++++++++++ lib/Service/AssistantService.php | 2 +- lib/Service/ChatService.php | 20 ++++++++++++++++--- src/components/ChattyLLM/Message.vue | 27 ++++++++++++++++++++++++-- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/lib/Listener/ChattyLLMTaskListener.php b/lib/Listener/ChattyLLMTaskListener.php index 9f8609487..264bc85e0 100644 --- a/lib/Listener/ChattyLLMTaskListener.php +++ b/lib/Listener/ChattyLLMTaskListener.php @@ -82,6 +82,8 @@ public function handle(Event $event): void { && $taskTypeId === \OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID; $isAgencyAudioChat = class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentAudioInteraction') && $taskTypeId === \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID; + $isMultimodalChat = class_exists('OCP\\TaskProcessing\\TaskTypes\\MultimodalChatWithTools') + && $taskTypeId === \OCP\TaskProcessing\TaskTypes\MultimodalChatWithTools::ID; $taskOutput = $task->getOutput(); @@ -128,6 +130,16 @@ public function handle(Event $event): void { // the task is not an audio one, but we might still need to Tts the answer // if it is a response to a ContextAgentInteraction confirmation that was asked about an audio message $this->runTtsIfNeeded($sessionId, $message, $taskTypeId, $task->getUserId()); + if ($isMultimodalChat) { + $attachments = $taskOutput['output_attachments'] ?? []; + $attachments = array_map(function($attachment) { + return [ + 'type' => 'File', + 'file_id' => $attachment, + ]; + }, $attachments); + $message->setAttachments(json_encode($attachments)); + } } try { $this->messageMapper->insert($message); diff --git a/lib/Service/AssistantService.php b/lib/Service/AssistantService.php index 28330bd14..fa7e78eb8 100644 --- a/lib/Service/AssistantService.php +++ b/lib/Service/AssistantService.php @@ -784,7 +784,7 @@ private function extractFileIdsFromTask(Task $task): array { /** @var int|list $outputSlot */ $outputSlot = $task->getOutput()[$key]; if (is_array($outputSlot)) { - $ids += $outputSlot; + $ids = array_merge($ids, $outputSlot); } else { $ids[] = $outputSlot; } diff --git a/lib/Service/ChatService.php b/lib/Service/ChatService.php index fd7014e3b..dc0e83aea 100644 --- a/lib/Service/ChatService.php +++ b/lib/Service/ChatService.php @@ -40,6 +40,7 @@ public function __construct( private readonly IManager $taskProcessingManager, private readonly LoggerInterface $logger, private readonly ITimeFactory $timeFactory, + private readonly AssistantService $assistantService, ) { } @@ -453,9 +454,23 @@ public function scheduleMessageGeneration(?string $userId, int $sessionId, int $ 'content' => $message->getContent(), ]); }, $history); - if (count($lastAttachments) > 0 && $this->isMultimodalChatAvailable()) { + if ($this->isMultimodalChatAvailable()) { // for a multimodal chat task, let's use the attachments in the history as we don't know the structure in history - $attachmentsHistory = array_map(static function (Message $message) { + $assistantService = $this->assistantService; + $attachmentsHistory = array_map(static function (Message $message) use ($userId, $assistantService) { + // Make sure that the user has access to the output files + if ($message->getRole() === Message::ROLE_ASSISTANT) { + $attachments = $message->jsonSerialize()['attachments']; + $result = []; + foreach ($attachments as $attachment) { + $info = $assistantService->saveOutputFile($userId, $message->getOcpTaskId(), $attachment['file_id']); + $result[] = [ + 'type' => 'File', + 'file_id' => $info['fileId'], + ]; + } + return $result; + } return $message->jsonSerialize()['attachments']; }, $history); // Just add all the attachments in the history as attachments for the latest message as we can't know the structure in history @@ -463,7 +478,6 @@ public function scheduleMessageGeneration(?string $userId, int $sessionId, int $ $attachmentsHistory = array_map(static function (array $attachment) { return $attachment['file_id']; }, $attachmentsHistory); - $taskId = $this->scheduleMultimodalChatTask($userId, $lastUserMessage->getContent(), $systemPrompt, $historyMessages, $sessionId, $attachmentsHistory); } else { $taskId = $this->scheduleLLMChatTask($userId, $lastUserMessage->getContent(), $systemPrompt, $historyMessages, $sessionId); diff --git a/src/components/ChattyLLM/Message.vue b/src/components/ChattyLLM/Message.vue index fb46a4104..a17e5bef7 100644 --- a/src/components/ChattyLLM/Message.vue +++ b/src/components/ChattyLLM/Message.vue @@ -102,13 +102,15 @@ :autoplay="message.autoPlay" :file-id="a.file_id" :task-id="message.role === 'human' ? undefined : (a.ocp_task_id ?? message.ocp_task_id)" - :is-output="message.role === 'assistant'" /> + :is-output="isOutput" /> + :is-output="isOutput" + :clickable="isOutput" + @click.native="onPreviewClick(f)" /> @@ -224,6 +226,9 @@ export default { fileAttachments() { return this.message.attachments?.filter(a => a.type === SHAPE_TYPE_NAMES.File) ?? [] }, + isOutput() { + return this.message.role === 'assistant' + }, }, watch: { @@ -284,6 +289,24 @@ export default { } return this.informationSourceNames[source] ? this.informationSourceNames[source] : source }, + onPreviewClick(file) { + // do not open input media files in the viewer + if (file.file_id === null || !this.isOutput) { + return + } + + const url = generateOcsUrl('/apps/assistant/api/v1/task/{taskId}/file/{fileId}/save', { + taskId: file.ocp_task_id ?? this.message.ocp_task_id, + fileId: file.file_id, + }) + return axios.post(url).then(response => { + const savedPath = response.data.ocs.data.path + console.debug('[assistant] view output file', savedPath) + OCA.Viewer.open({ path: savedPath }) + }).catch(error => { + console.error(error) + }) + }, }, } From 783534d64e823fd9bbef6be0564e3ada509f5e2c Mon Sep 17 00:00:00 2001 From: Lukas Schaefer Date: Mon, 13 Jul 2026 16:35:09 -0400 Subject: [PATCH 3/6] Handle history for attachments correctly Signed-off-by: Lukas Schaefer --- lib/Listener/ChattyLLMTaskListener.php | 4 +- lib/Service/ChatService.php | 52 ++++++++++++++------------ 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/lib/Listener/ChattyLLMTaskListener.php b/lib/Listener/ChattyLLMTaskListener.php index 264bc85e0..29d5089da 100644 --- a/lib/Listener/ChattyLLMTaskListener.php +++ b/lib/Listener/ChattyLLMTaskListener.php @@ -84,6 +84,8 @@ public function handle(Event $event): void { && $taskTypeId === \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID; $isMultimodalChat = class_exists('OCP\\TaskProcessing\\TaskTypes\\MultimodalChatWithTools') && $taskTypeId === \OCP\TaskProcessing\TaskTypes\MultimodalChatWithTools::ID; + $isMultimodalAgencyChat = class_exists('OCP\\TaskProcessing\\TaskTypes\\MultimodalContextAgentInteraction') + && $taskTypeId === \OCP\TaskProcessing\TaskTypes\MultimodalContextAgentInteraction::ID; $taskOutput = $task->getOutput(); @@ -150,7 +152,7 @@ public function handle(Event $event): void { $session = $this->sessionMapper->getUserSession($task->getUserId(), $sessionId); // store the conversation token and the actions if we are using the agency feature - if ($isAgency || $isAgencyAudioChat) { + if ($isAgency || $isAgencyAudioChat || $isMultimodalAgencyChat) { $conversationToken = ($taskOutput['conversation_token'] ?? null) ?: null; $pendingActions = ($taskOutput['actions'] ?? null) ?: null; $session->setAgencyConversationToken($conversationToken); diff --git a/lib/Service/ChatService.php b/lib/Service/ChatService.php index dc0e83aea..6e168f858 100644 --- a/lib/Service/ChatService.php +++ b/lib/Service/ChatService.php @@ -447,39 +447,43 @@ public function scheduleMessageGeneration(?string $userId, int $sessionId, int $ $fileId = $audioAttachment['file_id']; $taskId = $this->scheduleAudioChatTask($userId, $fileId, $systemPrompt, $history, $sessionId, $lastUserMessage->getId()); } else { - // for a text chat task, let's only use text in the history - $historyMessages = array_map(static function (Message $message) { - return json_encode([ - 'role' => $message->getRole(), - 'content' => $message->getContent(), - ]); - }, $history); if ($this->isMultimodalChatAvailable()) { - // for a multimodal chat task, let's use the attachments in the history as we don't know the structure in history + // for a multimodal chat also attachments need to be added to the history $assistantService = $this->assistantService; - $attachmentsHistory = array_map(static function (Message $message) use ($userId, $assistantService) { - // Make sure that the user has access to the output files - if ($message->getRole() === Message::ROLE_ASSISTANT) { - $attachments = $message->jsonSerialize()['attachments']; - $result = []; - foreach ($attachments as $attachment) { + $historyMessages = array_map(static function (Message $message) use ($userId, $assistantService) { + $attachments = $message->jsonSerialize()['attachments']; + // Attachments that were generated need to be saved in the user's files so they are ac + $content = array_map(static function (array $attachment) use ($userId, $assistantService, $message) { + if ($message->getRole() === Message::ROLE_ASSISTANT) { $info = $assistantService->saveOutputFile($userId, $message->getOcpTaskId(), $attachment['file_id']); - $result[] = [ - 'type' => 'File', + return [ + 'type' => 'file', 'file_id' => $info['fileId'], ]; } - return $result; - } - return $message->jsonSerialize()['attachments']; + return ['type' => 'file', 'file_id' => $attachment['file_id']]; + }, $attachments); + $content[] = [ + 'type' => 'text', + 'text' => $message->getContent(), + ]; + return json_encode([ + 'role' => $message->getRole(), + 'content' => $content, + ]); }, $history); - // Just add all the attachments in the history as attachments for the latest message as we can't know the structure in history - $attachmentsHistory = array_merge($lastAttachments, ...$attachmentsHistory); - $attachmentsHistory = array_map(static function (array $attachment) { + $lastAttachments = array_map(static function (array $attachment) { return $attachment['file_id']; - }, $attachmentsHistory); - $taskId = $this->scheduleMultimodalChatTask($userId, $lastUserMessage->getContent(), $systemPrompt, $historyMessages, $sessionId, $attachmentsHistory); + }, $lastAttachments); + $taskId = $this->scheduleMultimodalChatTask($userId, $lastUserMessage->getContent(), $systemPrompt, $historyMessages, $sessionId, $lastAttachments); } else { + // for a text chat task, let's only use text in the history + $historyMessages = array_map(static function (Message $message) { + return json_encode([ + 'role' => $message->getRole(), + 'content' => $message->getContent(), + ]); + }, $history); $taskId = $this->scheduleLLMChatTask($userId, $lastUserMessage->getContent(), $systemPrompt, $historyMessages, $sessionId); } } From e2723064fc8258e8d831110ddc7ea5a31e8bd816 Mon Sep 17 00:00:00 2001 From: Lukas Schaefer Date: Mon, 13 Jul 2026 16:53:16 -0400 Subject: [PATCH 4/6] Handle paste event Signed-off-by: Lukas Schaefer --- src/components/ChattyLLM/InputArea.vue | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/ChattyLLM/InputArea.vue b/src/components/ChattyLLM/InputArea.vue index 36ab20347..c5d107ee4 100644 --- a/src/components/ChattyLLM/InputArea.vue +++ b/src/components/ChattyLLM/InputArea.vue @@ -50,6 +50,7 @@ :maxlength="64_000" :multiline="isMobile" dir="auto" + @paste="onPaste" @update:model-value="$emit('update:chatContent', $event)" @submit="onSubmitText" />
@@ -198,6 +199,13 @@ export default { }, methods: { + onPaste(e) { + if (!this.multimodalChatAvailable || e.clipboardData.files.length === 0) { + return + } + e.preventDefault() + this.uploadFiles(e.clipboardData.files) + }, focus() { this.$nextTick(() => { this.$refs.richContenteditable.focus() @@ -228,12 +236,17 @@ export default { this.$refs.fileInput.click() }, onUploadFileSelected() { - const files = this.$refs.fileInput.files + this.uploadFiles(this.$refs.fileInput.files) + .finally(() => { + this.$refs.fileInput.value = '' + }) + }, + uploadFiles(files) { if (!files || files.length === 0) { - return + return Promise.resolve() } this.isUploading = true - Promise.all(Array.from(files).map(f => uploadInputFile(f))) + return Promise.all(Array.from(files).map(f => uploadInputFile(f))) .then((responses) => { const fileIds = responses.map(response => response.data.ocs.data.fileId) this.attachedFileIds = [...this.attachedFileIds, ...fileIds] @@ -244,7 +257,6 @@ export default { }) .finally(() => { this.isUploading = false - this.$refs.fileInput.value = '' }) }, async onChooseFromNextcloud() { From 4746488136d98977534ae2ebc88e3b79aed19f3b Mon Sep 17 00:00:00 2001 From: Lukas Schaefer Date: Tue, 14 Jul 2026 12:18:39 -0400 Subject: [PATCH 5/6] Disable submit when uploading Signed-off-by: Lukas Schaefer Co-authored-by: Cursor --- lib/Listener/ChattyLLMTaskListener.php | 2 +- lib/Service/ChatService.php | 2 +- src/components/ChattyLLM/InputArea.vue | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Listener/ChattyLLMTaskListener.php b/lib/Listener/ChattyLLMTaskListener.php index 29d5089da..5bf45ad73 100644 --- a/lib/Listener/ChattyLLMTaskListener.php +++ b/lib/Listener/ChattyLLMTaskListener.php @@ -134,7 +134,7 @@ public function handle(Event $event): void { $this->runTtsIfNeeded($sessionId, $message, $taskTypeId, $task->getUserId()); if ($isMultimodalChat) { $attachments = $taskOutput['output_attachments'] ?? []; - $attachments = array_map(function($attachment) { + $attachments = array_map(function ($attachment) { return [ 'type' => 'File', 'file_id' => $attachment, diff --git a/lib/Service/ChatService.php b/lib/Service/ChatService.php index 6e168f858..42efaeac8 100644 --- a/lib/Service/ChatService.php +++ b/lib/Service/ChatService.php @@ -452,7 +452,7 @@ public function scheduleMessageGeneration(?string $userId, int $sessionId, int $ $assistantService = $this->assistantService; $historyMessages = array_map(static function (Message $message) use ($userId, $assistantService) { $attachments = $message->jsonSerialize()['attachments']; - // Attachments that were generated need to be saved in the user's files so they are ac + // Attachments that were generated need to be saved in the user's files so they are accessible to provider $content = array_map(static function (array $attachment) use ($userId, $assistantService, $message) { if ($message->getRole() === Message::ROLE_ASSISTANT) { $info = $assistantService->saveOutputFile($userId, $message->getOcpTaskId(), $attachment['file_id']); diff --git a/src/components/ChattyLLM/InputArea.vue b/src/components/ChattyLLM/InputArea.vue index c5d107ee4..8855e476c 100644 --- a/src/components/ChattyLLM/InputArea.vue +++ b/src/components/ChattyLLM/InputArea.vue @@ -57,7 +57,7 @@