Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions lib/Service/AssistantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
) {
Expand Down Expand Up @@ -522,41 +524,54 @@ 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()) {
return $dataFolderNode;
}
}
// 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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* @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)) {
if ($try > 3) {
// 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);
Expand Down
5 changes: 5 additions & 0 deletions lib/Settings/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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'],
Expand Down
7 changes: 7 additions & 0 deletions lib/Settings/Personal.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand Down
21 changes: 19 additions & 2 deletions src/components/AdminSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@
</div>
</NcNoteCard>
</div>
<div class="data-folder">
<h4>
{{ t('assistant', 'Data folder') }}
</h4>
<NcNoteCard type="info">
{{ 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.') }}
</NcNoteCard>
<NcTextField id="default_data_folder"
v-model="state.default_data_folder"
class="text-field"
:label="t('assistant', 'Default data folder name')"
@update:model-value="delayedValueUpdate(state.default_data_folder, 'default_data_folder')" />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
<div class="chat-with-ai">
<h4>
{{ t('assistant', 'Chat with AI') }}
Expand Down Expand Up @@ -279,9 +292,13 @@ export default {
this.saveOptions({ [key]: this.state[key] ? '1' : '0' })
},
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(() => {
this.optionsToSave[key] = newValue
this.saveOptions(this.optionsToSave)
const values = { ...this.optionsToSave }
this.optionsToSave = {}
this.saveOptions(values)
}, 2000)
},
async onPickGlobalSkillsFolder() {
Expand Down
24 changes: 24 additions & 0 deletions src/components/PersonalSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@
{{ taskNames.join(', ') }}
</div>
</div>
<div class="data-folder">
<h3>{{ t('assistant', 'Data folder') }}</h3>
<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>
<NcTextField id="data_folder"
v-model="state.data_folder"
class="data-folder__field"
:label="t('assistant', 'Folder name')"
:placeholder="state.default_data_folder"
@update:model-value="delayedValueUpdate(state.data_folder, 'data_folder')" />
</div>
<div v-if="rememberedConversations.length > 0">
<h3>{{ t('assistant', 'Remembered conversations') }}</h3>
<p>{{ t('assistant', 'The following conversations are remembered by the Assistant Chat and will be taken into account for every new conversation:') }}</p>
Expand All @@ -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'
Expand All @@ -102,6 +114,7 @@ export default {
NcFormBoxSwitch,
NcFormBoxButton,
NcNoteCard,
NcTextField,
MemoryIcon,
},

Expand All @@ -112,6 +125,7 @@ export default {
state: loadState('assistant', 'config'),
providers: loadState('assistant', 'availableProviders'),
rememberedConversations: loadState('assistant', 'rememberedSessions'),
optionsToSave: {},
}
},

Expand All @@ -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' })
Expand Down
Loading