Skip to content

Commit a5d4304

Browse files
authored
Merge pull request #632 from bakiburakogun/feat/configurable-data-folder
feat: allow the data folder name to be set server-wide
2 parents fa3c7ba + d3dcaa7 commit a5d4304

5 files changed

Lines changed: 76 additions & 8 deletions

File tree

lib/Service/AssistantService.php

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use OCP\Files\InvalidPathException;
2525
use OCP\Files\IRootFolder;
2626
use OCP\Files\NotPermittedException;
27+
use OCP\IAppConfig;
2728
use OCP\IConfig;
2829
use OCP\IL10N;
2930
use OCP\ITempManager;
@@ -91,6 +92,7 @@ public function __construct(
9192
private IL10N $l10n,
9293
private ITempManager $tempManager,
9394
private IConfig $config,
95+
private IAppConfig $appConfig,
9496
private IShareManager $shareManager,
9597
private SystemTagService $systemTagService,
9698
) {
@@ -522,41 +524,54 @@ public function storeInputFile(string $userId, string $tempFileLocation, ?string
522524
public function getAssistantDataFolder(string $userId): Folder {
523525
$userFolder = $this->rootFolder->getUserFolder($userId);
524526

525-
$dataFolderName = $this->config->getUserValue($userId, Application::APP_ID, 'data_folder', Application::ASSISTANT_DATA_FOLDER_NAME) ?: Application::ASSISTANT_DATA_FOLDER_NAME;
527+
$defaultFolderName = $this->appConfig->getValueString(Application::APP_ID, 'default_data_folder', Application::ASSISTANT_DATA_FOLDER_NAME, lazy: true);
528+
if ($defaultFolderName === '') {
529+
$defaultFolderName = Application::ASSISTANT_DATA_FOLDER_NAME;
530+
}
531+
$dataFolderName = $this->config->getUserValue($userId, Application::APP_ID, 'data_folder', '');
532+
if ($dataFolderName === '') {
533+
// No folder stored for this user. If they already have one under the built-in
534+
// name, from before an administrator set a default, keep using it: applying the
535+
// default here would start a second folder and leave their existing output behind.
536+
$dataFolderName = $userFolder->nodeExists(Application::ASSISTANT_DATA_FOLDER_NAME)
537+
? Application::ASSISTANT_DATA_FOLDER_NAME
538+
: $defaultFolderName;
539+
}
526540
if ($userFolder->nodeExists($dataFolderName)) {
527541
$dataFolderNode = $userFolder->get($dataFolderName);
528542
if ($dataFolderNode instanceof Folder && $dataFolderNode->isCreatable()) {
529543
return $dataFolderNode;
530544
}
531545
}
532546
// it does not exist or is not a folder or does not have write permissions: we create one
533-
$dataFolder = $this->createAssistantDataFolder($userId);
547+
$dataFolder = $this->createAssistantDataFolder($userId, $dataFolderName);
534548
$dataFolderName = $dataFolder->getName();
535549
$this->config->setUserValue($userId, Application::APP_ID, 'data_folder', $dataFolderName);
536550
return $dataFolder;
537551
}
538552

539553
/**
540554
* @param string $userId
555+
* @param string $baseName
541556
* @param int $try
542557
* @return Folder
543558
* @throws NoUserException
544559
* @throws NotPermittedException
545560
*/
546-
private function createAssistantDataFolder(string $userId, int $try = 0): Folder {
561+
private function createAssistantDataFolder(string $userId, string $baseName, int $try = 0): Folder {
547562
$userFolder = $this->rootFolder->getUserFolder($userId);
548563
if ($try === 0) {
549-
$folderPath = Application::ASSISTANT_DATA_FOLDER_NAME;
564+
$folderPath = $baseName;
550565
} else {
551-
$folderPath = Application::ASSISTANT_DATA_FOLDER_NAME . ' ' . $try;
566+
$folderPath = $baseName . ' ' . $try;
552567
}
553568

554569
if ($userFolder->nodeExists($folderPath)) {
555570
if ($try > 3) {
556571
// give up
557572
throw new RuntimeException('Could not create the assistant data folder');
558573
}
559-
return $this->createAssistantDataFolder($userId, $try + 1);
574+
return $this->createAssistantDataFolder($userId, $baseName, $try + 1);
560575
}
561576

562577
return $userFolder->newFolder($folderPath);

lib/Settings/Admin.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public function getForm(): TemplateResponse {
5656
$chattyLLMUserInstructions = $this->appConfig->getValueString(Application::APP_ID, 'chat_user_instructions', Application::CHAT_USER_INSTRUCTIONS, lazy: true) ?: Application::CHAT_USER_INSTRUCTIONS;
5757
$chattyLLMUserInstructionsTitle = $this->appConfig->getValueString(Application::APP_ID, 'chat_user_instructions_title', Application::CHAT_USER_INSTRUCTIONS_TITLE, lazy: true) ?: Application::CHAT_USER_INSTRUCTIONS_TITLE;
5858
$chattyLLMLastNMessages = (int)$this->appConfig->getValueString(Application::APP_ID, 'chat_last_n_messages', '10', lazy: true);
59+
$defaultDataFolder = $this->appConfig->getValueString(Application::APP_ID, 'default_data_folder', Application::ASSISTANT_DATA_FOLDER_NAME, lazy: true);
60+
if ($defaultDataFolder === '') {
61+
$defaultDataFolder = Application::ASSISTANT_DATA_FOLDER_NAME;
62+
}
5963

6064
$globalSkillsConfig = $this->agentSkillsService->getGlobalSkillsConfig();
6165

@@ -73,6 +77,7 @@ public function getForm(): TemplateResponse {
7377
'chat_user_instructions' => $chattyLLMUserInstructions,
7478
'chat_user_instructions_title' => $chattyLLMUserInstructionsTitle,
7579
'chat_last_n_messages' => $chattyLLMLastNMessages,
80+
'default_data_folder' => $defaultDataFolder,
7681
'context_agent_available' => $contextAgentAvailable,
7782
'global_skills_admin_uid' => $globalSkillsConfig['admin_uid'],
7883
'global_skills_path' => $globalSkillsConfig['path'],

lib/Settings/Personal.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public function getForm(): TemplateResponse {
4747
$audioChatAvailable = (class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat') && array_key_exists(\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID, $availableTaskTypes))
4848
|| (class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentAudioInteraction') && array_key_exists(\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID, $availableTaskTypes));
4949
$autoplayAudioChat = $this->config->getUserValue($this->userId, Application::APP_ID, 'autoplay_audio_chat', '1') === '1';
50+
$dataFolder = $this->config->getUserValue($this->userId, Application::APP_ID, 'data_folder', '');
51+
$defaultDataFolder = $this->appConfig->getValueString(Application::APP_ID, 'default_data_folder', Application::ASSISTANT_DATA_FOLDER_NAME, lazy: true);
52+
if ($defaultDataFolder === '') {
53+
$defaultDataFolder = Application::ASSISTANT_DATA_FOLDER_NAME;
54+
}
5055

5156
$assistantAvailable = $taskProcessingAvailable && $this->appConfig->getValueString(Application::APP_ID, 'assistant_enabled', '1', lazy: true) === '1';
5257
$assistantEnabled = $this->config->getUserValue($this->userId, Application::APP_ID, 'assistant_enabled', '1') === '1';
@@ -77,6 +82,8 @@ public function getForm(): TemplateResponse {
7782
'speech_to_text_picker_enabled' => $speechToTextPickerEnabled,
7883
'audio_chat_available' => $audioChatAvailable,
7984
'autoplay_audio_chat' => $autoplayAudioChat,
85+
'data_folder' => $dataFolder,
86+
'default_data_folder' => $defaultDataFolder,
8087
];
8188
$this->initialStateService->provideInitialState('config', $userConfig);
8289

src/components/AdminSettings.vue

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,19 @@
118118
</div>
119119
</NcNoteCard>
120120
</div>
121+
<div class="data-folder">
122+
<h4>
123+
{{ t('assistant', 'Data folder') }}
124+
</h4>
125+
<NcNoteCard type="info">
126+
{{ t('assistant', 'Name of the folder the assistant creates in the files of each user to store generated content. Users can override this in their personal settings. Changing it does not rename folders that already exist.') }}
127+
</NcNoteCard>
128+
<NcTextField id="default_data_folder"
129+
v-model="state.default_data_folder"
130+
class="text-field"
131+
:label="t('assistant', 'Default data folder name')"
132+
@update:model-value="delayedValueUpdate(state.default_data_folder, 'default_data_folder')" />
133+
</div>
121134
<div class="chat-with-ai">
122135
<h4>
123136
{{ t('assistant', 'Chat with AI') }}
@@ -279,9 +292,13 @@ export default {
279292
this.saveOptions({ [key]: this.state[key] ? '1' : '0' })
280293
},
281294
delayedValueUpdate(newValue, key) {
295+
// delay() shares one timer, so a second edit within the delay cancels the
296+
// first callback: queue the value now rather than when the timer fires
297+
this.optionsToSave[key] = newValue
282298
delay(() => {
283-
this.optionsToSave[key] = newValue
284-
this.saveOptions(this.optionsToSave)
299+
const values = { ...this.optionsToSave }
300+
this.optionsToSave = {}
301+
this.saveOptions(values)
285302
}, 2000)
286303
},
287304
async onPickGlobalSkillsFolder() {

src/components/PersonalSettings.vue

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@
5858
{{ taskNames.join(', ') }}
5959
</div>
6060
</div>
61+
<div class="data-folder">
62+
<h3>{{ t('assistant', 'Data folder') }}</h3>
63+
<p>{{ 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.') }}</p>
64+
<NcTextField id="data_folder"
65+
v-model="state.data_folder"
66+
class="data-folder__field"
67+
:label="t('assistant', 'Folder name')"
68+
:placeholder="state.default_data_folder"
69+
@update:model-value="delayedValueUpdate(state.data_folder, 'data_folder')" />
70+
</div>
6171
<div v-if="rememberedConversations.length > 0">
6272
<h3>{{ t('assistant', 'Remembered conversations') }}</h3>
6373
<p>{{ t('assistant', 'The following conversations are remembered by the Assistant Chat and will be taken into account for every new conversation:') }}</p>
@@ -84,10 +94,12 @@ import NcFormBox from '@nextcloud/vue/components/NcFormBox'
8494
import NcFormBoxSwitch from '@nextcloud/vue/components/NcFormBoxSwitch'
8595
import NcFormBoxButton from '@nextcloud/vue/components/NcFormBoxButton'
8696
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
97+
import NcTextField from '@nextcloud/vue/components/NcTextField'
8798
8899
import MemoryIcon from 'vue-material-design-icons/Memory.vue'
89100
90101
import { loadState } from '@nextcloud/initial-state'
102+
import { delay } from '../utils.js'
91103
import { generateUrl } from '@nextcloud/router'
92104
import axios from '@nextcloud/axios'
93105
import { showSuccess, showError } from '@nextcloud/dialogs'
@@ -102,6 +114,7 @@ export default {
102114
NcFormBoxSwitch,
103115
NcFormBoxButton,
104116
NcNoteCard,
117+
NcTextField,
105118
MemoryIcon,
106119
},
107120
@@ -112,6 +125,7 @@ export default {
112125
state: loadState('assistant', 'config'),
113126
providers: loadState('assistant', 'availableProviders'),
114127
rememberedConversations: loadState('assistant', 'rememberedSessions'),
128+
optionsToSave: {},
115129
}
116130
},
117131
@@ -130,6 +144,16 @@ export default {
130144
},
131145
132146
methods: {
147+
delayedValueUpdate(newValue, key) {
148+
// delay() shares one timer, so a second edit within the delay cancels the
149+
// first callback: queue the value now rather than when the timer fires
150+
this.optionsToSave[key] = newValue
151+
delay(() => {
152+
const values = { ...this.optionsToSave }
153+
this.optionsToSave = {}
154+
this.saveOptions(values)
155+
}, 2000)
156+
},
133157
onCheckboxChanged(newValue, key) {
134158
this.state[key] = newValue
135159
this.saveOptions({ [key]: this.state[key] ? '1' : '0' })

0 commit comments

Comments
 (0)