diff --git a/lib/Service/AssistantService.php b/lib/Service/AssistantService.php index b7a69fa4..bb188542 100644 --- a/lib/Service/AssistantService.php +++ b/lib/Service/AssistantService.php @@ -24,6 +24,7 @@ use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; use OCP\Files\NotPermittedException; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; use OCP\ITempManager; @@ -91,6 +92,7 @@ public function __construct( private IL10N $l10n, private ITempManager $tempManager, private IConfig $config, + private IAppConfig $appConfig, private IShareManager $shareManager, private SystemTagService $systemTagService, ) { @@ -522,7 +524,19 @@ public function storeInputFile(string $userId, string $tempFileLocation, ?string public function getAssistantDataFolder(string $userId): Folder { $userFolder = $this->rootFolder->getUserFolder($userId); - $dataFolderName = $this->config->getUserValue($userId, Application::APP_ID, 'data_folder', Application::ASSISTANT_DATA_FOLDER_NAME) ?: Application::ASSISTANT_DATA_FOLDER_NAME; + $defaultFolderName = $this->appConfig->getValueString(Application::APP_ID, 'default_data_folder', Application::ASSISTANT_DATA_FOLDER_NAME, lazy: true); + if ($defaultFolderName === '') { + $defaultFolderName = Application::ASSISTANT_DATA_FOLDER_NAME; + } + $dataFolderName = $this->config->getUserValue($userId, Application::APP_ID, 'data_folder', ''); + if ($dataFolderName === '') { + // No folder stored for this user. If they already have one under the built-in + // name, from before an administrator set a default, keep using it: applying the + // default here would start a second folder and leave their existing output behind. + $dataFolderName = $userFolder->nodeExists(Application::ASSISTANT_DATA_FOLDER_NAME) + ? Application::ASSISTANT_DATA_FOLDER_NAME + : $defaultFolderName; + } if ($userFolder->nodeExists($dataFolderName)) { $dataFolderNode = $userFolder->get($dataFolderName); if ($dataFolderNode instanceof Folder && $dataFolderNode->isCreatable()) { @@ -530,7 +544,7 @@ public function getAssistantDataFolder(string $userId): Folder { } } // it does not exist or is not a folder or does not have write permissions: we create one - $dataFolder = $this->createAssistantDataFolder($userId); + $dataFolder = $this->createAssistantDataFolder($userId, $dataFolderName); $dataFolderName = $dataFolder->getName(); $this->config->setUserValue($userId, Application::APP_ID, 'data_folder', $dataFolderName); return $dataFolder; @@ -538,17 +552,18 @@ public function getAssistantDataFolder(string $userId): Folder { /** * @param string $userId + * @param string $baseName * @param int $try * @return Folder * @throws NoUserException * @throws NotPermittedException */ - private function createAssistantDataFolder(string $userId, int $try = 0): Folder { + private function createAssistantDataFolder(string $userId, string $baseName, int $try = 0): Folder { $userFolder = $this->rootFolder->getUserFolder($userId); if ($try === 0) { - $folderPath = Application::ASSISTANT_DATA_FOLDER_NAME; + $folderPath = $baseName; } else { - $folderPath = Application::ASSISTANT_DATA_FOLDER_NAME . ' ' . $try; + $folderPath = $baseName . ' ' . $try; } if ($userFolder->nodeExists($folderPath)) { @@ -556,7 +571,7 @@ private function createAssistantDataFolder(string $userId, int $try = 0): Folder // give up throw new RuntimeException('Could not create the assistant data folder'); } - return $this->createAssistantDataFolder($userId, $try + 1); + return $this->createAssistantDataFolder($userId, $baseName, $try + 1); } return $userFolder->newFolder($folderPath); diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 052b135c..01647053 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -56,6 +56,10 @@ public function getForm(): TemplateResponse { $chattyLLMUserInstructions = $this->appConfig->getValueString(Application::APP_ID, 'chat_user_instructions', Application::CHAT_USER_INSTRUCTIONS, lazy: true) ?: Application::CHAT_USER_INSTRUCTIONS; $chattyLLMUserInstructionsTitle = $this->appConfig->getValueString(Application::APP_ID, 'chat_user_instructions_title', Application::CHAT_USER_INSTRUCTIONS_TITLE, lazy: true) ?: Application::CHAT_USER_INSTRUCTIONS_TITLE; $chattyLLMLastNMessages = (int)$this->appConfig->getValueString(Application::APP_ID, 'chat_last_n_messages', '10', lazy: true); + $defaultDataFolder = $this->appConfig->getValueString(Application::APP_ID, 'default_data_folder', Application::ASSISTANT_DATA_FOLDER_NAME, lazy: true); + if ($defaultDataFolder === '') { + $defaultDataFolder = Application::ASSISTANT_DATA_FOLDER_NAME; + } $globalSkillsConfig = $this->agentSkillsService->getGlobalSkillsConfig(); @@ -73,6 +77,7 @@ public function getForm(): TemplateResponse { 'chat_user_instructions' => $chattyLLMUserInstructions, 'chat_user_instructions_title' => $chattyLLMUserInstructionsTitle, 'chat_last_n_messages' => $chattyLLMLastNMessages, + 'default_data_folder' => $defaultDataFolder, 'context_agent_available' => $contextAgentAvailable, 'global_skills_admin_uid' => $globalSkillsConfig['admin_uid'], 'global_skills_path' => $globalSkillsConfig['path'], diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index 87e087cd..fc8f9f12 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -47,6 +47,11 @@ public function getForm(): TemplateResponse { $audioChatAvailable = (class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat') && array_key_exists(\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID, $availableTaskTypes)) || (class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentAudioInteraction') && array_key_exists(\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID, $availableTaskTypes)); $autoplayAudioChat = $this->config->getUserValue($this->userId, Application::APP_ID, 'autoplay_audio_chat', '1') === '1'; + $dataFolder = $this->config->getUserValue($this->userId, Application::APP_ID, 'data_folder', ''); + $defaultDataFolder = $this->appConfig->getValueString(Application::APP_ID, 'default_data_folder', Application::ASSISTANT_DATA_FOLDER_NAME, lazy: true); + if ($defaultDataFolder === '') { + $defaultDataFolder = Application::ASSISTANT_DATA_FOLDER_NAME; + } $assistantAvailable = $taskProcessingAvailable && $this->appConfig->getValueString(Application::APP_ID, 'assistant_enabled', '1', lazy: true) === '1'; $assistantEnabled = $this->config->getUserValue($this->userId, Application::APP_ID, 'assistant_enabled', '1') === '1'; @@ -77,6 +82,8 @@ public function getForm(): TemplateResponse { 'speech_to_text_picker_enabled' => $speechToTextPickerEnabled, 'audio_chat_available' => $audioChatAvailable, 'autoplay_audio_chat' => $autoplayAudioChat, + 'data_folder' => $dataFolder, + 'default_data_folder' => $defaultDataFolder, ]; $this->initialStateService->provideInitialState('config', $userConfig); diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 9e41d70d..dea8caa6 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -118,6 +118,19 @@ +
{{ t('assistant', 'Where the assistant stores content it generates for you. Leave empty to use the name your administrator has set. Changing it does not move files that are already there.') }}
+{{ t('assistant', 'The following conversations are remembered by the Assistant Chat and will be taken into account for every new conversation:') }}
@@ -84,10 +94,12 @@ import NcFormBox from '@nextcloud/vue/components/NcFormBox' import NcFormBoxSwitch from '@nextcloud/vue/components/NcFormBoxSwitch' import NcFormBoxButton from '@nextcloud/vue/components/NcFormBoxButton' import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import NcTextField from '@nextcloud/vue/components/NcTextField' import MemoryIcon from 'vue-material-design-icons/Memory.vue' import { loadState } from '@nextcloud/initial-state' +import { delay } from '../utils.js' import { generateUrl } from '@nextcloud/router' import axios from '@nextcloud/axios' import { showSuccess, showError } from '@nextcloud/dialogs' @@ -102,6 +114,7 @@ export default { NcFormBoxSwitch, NcFormBoxButton, NcNoteCard, + NcTextField, MemoryIcon, }, @@ -112,6 +125,7 @@ export default { state: loadState('assistant', 'config'), providers: loadState('assistant', 'availableProviders'), rememberedConversations: loadState('assistant', 'rememberedSessions'), + optionsToSave: {}, } }, @@ -130,6 +144,16 @@ export default { }, methods: { + delayedValueUpdate(newValue, key) { + // delay() shares one timer, so a second edit within the delay cancels the + // first callback: queue the value now rather than when the timer fires + this.optionsToSave[key] = newValue + delay(() => { + const values = { ...this.optionsToSave } + this.optionsToSave = {} + this.saveOptions(values) + }, 2000) + }, onCheckboxChanged(newValue, key) { this.state[key] = newValue this.saveOptions({ [key]: this.state[key] ? '1' : '0' })