Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
use MediaField to render the output file in the reference widget, add…
… a save button in MediaField

Signed-off-by: Julien Veyssier <[email protected]>
  • Loading branch information
julien-nc committed Apr 29, 2025
commit 4f7e40f4d9edcd51632020f427f74e2bb1acebf6
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
['name' => 'assistantApi#displayUserFile', 'url' => '/api/{apiVersion}/file/{fileId}/display', 'verb' => 'GET', 'requirements' => $requirements],
['name' => 'assistantApi#getUserFileInfo', 'url' => '/api/{apiVersion}/file/{fileId}/info', 'verb' => 'GET', 'requirements' => $requirements],
['name' => 'assistantApi#shareOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/file/{fileId}/share', 'verb' => 'POST', 'requirements' => $requirements],
['name' => 'assistantApi#saveOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/file/{fileId}/save', 'verb' => 'POST', 'requirements' => $requirements],
['name' => 'assistantApi#getOutputFilePreview', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/preview', 'verb' => 'GET', 'requirements' => $requirements],
['name' => 'assistantApi#getOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/download', 'verb' => 'GET', 'requirements' => $requirements],

Expand Down
27 changes: 25 additions & 2 deletions lib/Controller/AssistantApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,13 @@ public function getUserFileInfo(int $fileId): DataResponse {
/**
* Share an output file
*
* Share a file that was produced by a task
* Save and share a file that was produced by a task
*
* @param int $ocpTaskId The task ID
* @param int $fileId The file ID
* @return DataResponse<Http::STATUS_OK, array{shareToken: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: The file was shared
* 200: The file was saved and shared
* 404: The file was not found
*/
#[NoAdminRequired]
Expand All @@ -254,6 +254,29 @@ public function shareOutputFile(int $ocpTaskId, int $fileId): DataResponse {
}
}

/**
* Save an output file
*
* Save a file that was produced by a task
*
* @param int $ocpTaskId The task ID
* @param int $fileId The file ID
* @return DataResponse<Http::STATUS_OK, array{shareToken: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: The file was saved
* 404: The file was not found
*/
#[NoAdminRequired]
public function saveOutputFile(int $ocpTaskId, int $fileId): DataResponse {
try {
$info = $this->assistantService->saveOutputFile($this->userId, $ocpTaskId, $fileId);
return new DataResponse($info);
} catch (\Exception $e) {
$this->logger->debug('Failed to save assistant output file', ['exception' => $e]);
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND);
}
}

/**
* Get task output file preview
*
Expand Down
39 changes: 32 additions & 7 deletions lib/Service/AssistantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ public function getTaskOutputFile(string $userId, int $ocpTaskId, int $fileId):
* @param string $userId
* @param int $ocpTaskId
* @param int $fileId
* @return string
* @return File
* @throws Exception
* @throws InvalidPathException
* @throws LockedException
Expand All @@ -402,34 +402,59 @@ public function getTaskOutputFile(string $userId, int $ocpTaskId, int $fileId):
* @throws TaskProcessingException
* @throws \OCP\Files\NotFoundException
*/
public function shareOutputFile(string $userId, int $ocpTaskId, int $fileId): string {
private function saveFile(string $userId, int $ocpTaskId, int $fileId): File {
$taskOutputFile = $this->getTaskOutputFile($userId, $ocpTaskId, $fileId);
$assistantDataFolder = $this->getAssistantDataFolder($userId);
$targetFileName = $this->getTargetFileName($taskOutputFile);
if ($assistantDataFolder->nodeExists($targetFileName)) {
$existingTarget = $assistantDataFolder->get($targetFileName);
if ($existingTarget instanceof File) {
if ($existingTarget->getSize() === $taskOutputFile->getSize()) {
$fileCopy = $existingTarget;
return $existingTarget;
} else {
$fileCopy = $assistantDataFolder->newFile($targetFileName, $taskOutputFile->fopen('rb'));
return $assistantDataFolder->newFile($targetFileName, $taskOutputFile->fopen('rb'));
}
} else {
throw new Exception('Impossible to copy output file, a directory with this name already exists', Http::STATUS_UNAUTHORIZED);
}
} else {
$fileCopy = $assistantDataFolder->newFile($targetFileName, $taskOutputFile->fopen('rb'));
return $assistantDataFolder->newFile($targetFileName, $taskOutputFile->fopen('rb'));
}
}

/**
* @param string $userId
* @param int $ocpTaskId
* @param int $fileId
* @return string
* @throws Exception
* @throws InvalidPathException
* @throws LockedException
* @throws NoUserException
* @throws NotFoundException
* @throws NotPermittedException
* @throws PreConditionNotMetException
* @throws TaskProcessingException
* @throws \OCP\Files\NotFoundException
*/
public function shareOutputFile(string $userId, int $ocpTaskId, int $fileId): string {
$fileCopy = $this->saveFile($userId, $ocpTaskId, $fileId);
$share = $this->shareManager->newShare();
$share->setNode($fileCopy);
$share->setPermissions(Constants::PERMISSION_READ);
$share->setShareType(IShare::TYPE_LINK);
$share->setSharedBy($userId);
$share->setLabel('Assistant share');
$share = $this->shareManager->createShare($share);
$shareToken = $share->getToken();
return $share->getToken();
}

return $shareToken;
public function saveOutputFile(string $userId, int $ocpTaskId, int $fileId): array {
$fileCopy = $this->saveFile($userId, $ocpTaskId, $fileId);
return [
'fileId' => $fileCopy->getId(),
'path' => $fileCopy->getInternalPath(),
];
}

/**
Expand Down
59 changes: 45 additions & 14 deletions src/components/fields/MediaField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@
</template>
</NcButton>
</a>
<NcButton
:title="t('assistant', 'Save this media')"
@click="onSave">
<template #icon>
<ContentSaveIcon />
</template>
</NcButton>
<NcButton
:title="t('assistant', 'Share this media')"
@click="onShare">
Expand Down Expand Up @@ -76,6 +83,7 @@
import CloseIcon from 'vue-material-design-icons/Close.vue'
import DownloadIcon from 'vue-material-design-icons/Download.vue'
import ShareVariantIcon from 'vue-material-design-icons/ShareVariant.vue'
import ContentSaveIcon from 'vue-material-design-icons/ContentSave.vue'

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'

Expand Down Expand Up @@ -107,6 +115,7 @@ export default {
DownloadIcon,
ShareVariantIcon,
CloseIcon,
ContentSaveIcon,
NcButton,
},

Expand Down Expand Up @@ -220,21 +229,43 @@ export default {
})
},
onShare() {
if (this.value !== null) {
const url = generateOcsUrl('/apps/assistant/api/v1/task/{taskId}/file/{fileId}/share', {
taskId: this.providedCurrentTaskId(),
fileId: this.value,
})
axios.post(url).then(response => {
const shareToken = response.data.ocs.data.shareToken
const shareUrl = window.location.protocol + '//' + window.location.host + generateUrl('/s/{shareToken}', { shareToken })
console.debug('[assistant] generated share link', shareUrl)
const message = t('assistant', 'Output file share link copied to clipboard')
this.copyString(shareUrl, message)
}).catch(error => {
console.error(error)
})
if (this.value === null) {
return
}

const url = generateOcsUrl('/apps/assistant/api/v1/task/{taskId}/file/{fileId}/share', {
taskId: this.providedCurrentTaskId(),
fileId: this.value,
})
axios.post(url).then(response => {
const shareToken = response.data.ocs.data.shareToken
const shareUrl = window.location.protocol + '//' + window.location.host + generateUrl('/s/{shareToken}', { shareToken })
console.debug('[assistant] generated share link', shareUrl)
const message = t('assistant', 'Output file share link copied to clipboard')
this.copyString(shareUrl, message)
}).catch(error => {
console.error(error)
})
},
onSave() {
if (this.value === null) {
return
}

const url = generateOcsUrl('/apps/assistant/api/v1/task/{taskId}/file/{fileId}/save', {
taskId: this.providedCurrentTaskId(),
fileId: this.value,
})
axios.post(url).then(response => {
const savedPath = response.data.ocs.data.path
const savedFileId = response.data.ocs.data.fileId
console.debug('[assistant] save output file', savedPath)
const directUrl = window.location.protocol + '//' + window.location.host + generateUrl('/f/{savedFileId}', { savedFileId })
const message = t('assistant', 'This output file has been saved in {path}', { path: savedPath }) + '\n' + directUrl
this.copyString(directUrl, message)
}).catch(error => {
console.error(error)
})
},
async copyString(content, message) {
try {
Expand Down
23 changes: 20 additions & 3 deletions src/views/TaskOutputFileReferenceWidget.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
<template>
<div class="task-output-widget">
<strong>
{{ t('assistant', 'Output file for task {taskId} ({taskTypeName})', { taskId: richObject.taskId, taskTypeName: richObject.taskTypeName }) }}
</strong>
<MediaField :field="field"
field-key="dummyKey"
:value="richObject.fileId"
:is-output="true" />
</div>
</template>

<script>

import MediaField from '../components/fields/MediaField.vue'

import { SHAPE_TYPE_NAMES } from '../constants.js'

export default {
name: 'TaskOutputFileReferenceWidget',

components: {
MediaField,
},

provide() {
return {
providedCurrentTaskId: () => this.richObject.taskId,
}
},

props: {
Expand All @@ -31,6 +43,11 @@ export default {

data() {
return {
field: {
description: 'field desc',
name: t('assistant', 'Output file for task {taskId} ({taskTypeName})', { taskId: this.richObject.taskId, taskTypeName: this.richObject.taskTypeName }),
type: SHAPE_TYPE_NAMES.File,
},
}
},

Expand Down