From 901ad3cc69004b1df9b8b24d5debac611daa944b Mon Sep 17 00:00:00 2001 From: RafaUC Date: Tue, 26 Aug 2025 00:57:11 -0600 Subject: [PATCH 1/6] Fixes: Dispose clientfiles when clearing fileList. - Delete files of inexistent locations in database. --- src/frontend/stores/FileStore.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/frontend/stores/FileStore.ts b/src/frontend/stores/FileStore.ts index d4de18e4..21cab28d 100644 --- a/src/frontend/stores/FileStore.ts +++ b/src/frontend/stores/FileStore.ts @@ -698,6 +698,9 @@ class FileStore { // Removes all items from fileList @action.bound clearFileList(): void { + for (const file of this.fileList) { + file?.dispose(); + } this.numLoadedFiles = 0; this.fileDimensions.clear(); this.fileList.clear(); @@ -761,6 +764,7 @@ class FileStore { getLocation(location: ID): ClientLocation { const loc = this.rootStore.locationStore.get(location); if (!loc) { + this.backend.removeLocation(location); throw new Error( `Location of file was not found! This should never happen! Location ${location}`, ); From 063115212a77943d353916969693d995f4717a53 Mon Sep 17 00:00:00 2001 From: RafaUC Date: Tue, 26 Aug 2025 17:15:33 -0600 Subject: [PATCH 2/6] Feature: Allow automatic tagging of files using a local AI tagging service. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added the "Tag Selected Using AI Tagging Service" to the files contextual menu. - Added its endpoint configuration to Settings > Background Processes. Custom and minimalistic implementation inspired/based on cmeka's implementation: https://github.com/cmeka/OneFolder/commit/b0d7e12 --- common/promise.ts | 1 + resources/style/controls/input.scss | 25 +++ resources/style/controls/notifications.scss | 2 + .../containers/ContentView/menu-items.tsx | 6 + .../Settings/BackgroundProcesses.tsx | 60 ++++++- src/frontend/entities/File.ts | 4 + src/frontend/stores/FileStore.ts | 169 +++++++++++++++++- src/frontend/stores/UiStore.ts | 10 ++ 8 files changed, 273 insertions(+), 4 deletions(-) diff --git a/common/promise.ts b/common/promise.ts index 383ce083..b6dea6d3 100644 --- a/common/promise.ts +++ b/common/promise.ts @@ -56,6 +56,7 @@ export function promiseAllLimit( if (cancel?.()) { rejected = true; console.log('CANCELLING!'); + reject(new Error('Promise chain cancelled')); return; } diff --git a/resources/style/controls/input.scss b/resources/style/controls/input.scss index b9275dc2..89e962b4 100644 --- a/resources/style/controls/input.scss +++ b/resources/style/controls/input.scss @@ -26,6 +26,7 @@ } } +.split-input-wrapper, .input { height: 28px; background: var(--input-color); @@ -101,3 +102,27 @@ textarea.input { } } } + +.split-input-wrapper { + display: flex; + flex-grow: 1; + flex-wrap: wrap; + overflow-x: hidden; + align-items: center; + user-select: text; + padding-left: 0.25rem; + + > * { + user-select: text; + padding: 0; + margin: 0; + line-height: 1.75em; + } + + input { + flex-grow: 1; + border: none; + background: transparent; + box-sizing: content-box; + } +} diff --git a/resources/style/controls/notifications.scss b/resources/style/controls/notifications.scss index 51aec407..e37b0b22 100644 --- a/resources/style/controls/notifications.scss +++ b/resources/style/controls/notifications.scss @@ -55,10 +55,12 @@ min-height: 1rem; max-height: 5rem; margin: 0.25rem; + word-break: break-word; } button { color: white; + min-width: auto; &:hover { background: rgba(255, 255, 255, 0.15); diff --git a/src/frontend/containers/ContentView/menu-items.tsx b/src/frontend/containers/ContentView/menu-items.tsx index 5801aad1..63ca5d29 100644 --- a/src/frontend/containers/ContentView/menu-items.tsx +++ b/src/frontend/containers/ContentView/menu-items.tsx @@ -97,6 +97,12 @@ export const FileViewerMenuItems = ({ file }: { file: ClientFile }) => { text="Open Tag Selector" icon={IconSet.TAG} /> + { const { uiStore, locationStore } = useStore(); @@ -36,9 +37,7 @@ export const BackgroundProcesses = observer(() => { return ( <> - - Run in background - +

Browser Extension

You need to install the browser extension before either in the{' '} @@ -63,6 +62,11 @@ export const BackgroundProcesses = observer(() => { > Run browser extension +
+
+ + Run in background +
{

Download Directory

{uiStore.importDirectory || 'Not set'}
+
+
+ + ); +}); + +const TaggingServiceConfig = observer(() => { + const { taggingServiceURL, setTaggingServiceURL } = useStore().uiStore; + const prehost = 'http://localhost'; + + const posthost = taggingServiceURL.startsWith(prehost) + ? taggingServiceURL.slice(prehost.length) + : ''; + + const handleKeyDown = useGalleryInputKeydownHandler(); + + const handleChange = (e: React.ChangeEvent) => { + //use URL for validations + let newPosthost = e.target.value.replace(prehost, ''); + const url = new URL(newPosthost, prehost); + //Remove any hostname if present when pasting the full URL. + newPosthost = (url.pathname + url.search + url.hash).replace('/', ''); + if (newPosthost && !newPosthost.startsWith(':') && !newPosthost.startsWith('/')) { + newPosthost = '/' + newPosthost; + } + setTaggingServiceURL(prehost + newPosthost); + }; + + // Custom and minimalistic implementation inspired/based on cmeka's implementation: https://github.com/cmeka/OneFolder/commit/b0d7e12 + return ( + <> +

Local AI Tagging API URL

+ + A tagging service such as{' '} + + media-tag-service + {' '} + must be running. + + +
+ {prehost} + +
); }); diff --git a/src/frontend/entities/File.ts b/src/frontend/entities/File.ts index ad06ea33..1ab4f09e 100644 --- a/src/frontend/entities/File.ts +++ b/src/frontend/entities/File.ts @@ -121,6 +121,10 @@ export class ClientFile { makeObservable(this); } + get isAutoSaveEnabled(): boolean { + return this.autoSave; + } + /** * Gets his tags and all inherithed tags from parent and implied tags from his tags. */ diff --git a/src/frontend/stores/FileStore.ts b/src/frontend/stores/FileStore.ts index 21cab28d..8b562b13 100644 --- a/src/frontend/stores/FileStore.ts +++ b/src/frontend/stores/FileStore.ts @@ -27,6 +27,7 @@ import { } from 'src/api/extraProperty'; import { InheritedTagsVisibilityModeType } from './UiStore'; import { clamp } from 'common/core'; +import { RendererMessenger } from 'src/ipc/renderer'; export const FILE_STORAGE_KEY = 'Allusion_File'; @@ -74,7 +75,7 @@ class FileStore { private filesToSave: Map = new Map(); private pendingSaves: number = 0; @observable isSaving: boolean = false; - + isTaggingWithService: boolean = false; /** The origin of the current files that are shown */ @observable private content: Content = Content.All; @observable orderDirection: OrderDirection = OrderDirection.Desc; @@ -274,6 +275,172 @@ class FileStore { } } + @action.bound async tagFileUsingNamesOrAliases(file: ClientFile, tags: string[]): Promise { + const tagStore = this.rootStore.tagStore; + const root = tagStore.root; + const matches: ClientTag[] = []; + for (const tag of tags) { + let match = tagStore.findByNameOrAlias(tag); + if (match === undefined) { + match = await tagStore.create(root, tag); + } + // First collect all matches in an array instead of directly adding them to + // the file, to avoid unnecessary backend saves while awaiting the creation of tags. + matches.push(match); + } + file.addTags(matches); + } + + @action.bound private async getTagNamesUsingTaggingService(file: ClientFile) { + const taggingServiceURL = this.rootStore.uiStore.taggingServiceURL; + const response = await fetch(taggingServiceURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ file: file.absolutePath }), + }).catch((error) => { + throw new Error(`Error while performing the request to the tagging service: ${error}`); + }); + + // Process the response. If there are errors, show them in the console. + // but do not throw to allow the rest of the files to be processed. + if (response.ok) { + // successful request + try { + const responseData = await response.json(); + if (responseData.tags && Array.isArray(responseData.tags)) { + const tagNames: string[] = responseData.tags + .map((tag: any) => tag?.name) + .filter((name: string | undefined): name is string => name !== undefined); + if (tagNames.length === 0) { + console.warn('Possible invalid tag data format: no "name" found.'); + return; + } + return tagNames; + } else if (responseData.error) { + console.error('Tagging service response error: ', responseData.error); + } else { + console.error( + 'Tagging service error: no tags found or invalid tag data format. ' + + 'The response must contain a "tags" key, containing a an array of objects, each with a "name" key.', + ); + } + } catch (error) { + // catch .json() errors + console.error( + 'Tagging service error: The response must be JSON containing' + + ' a "tags" key, containing a an array of objects, each with a "name" key.', + error, + ); + } + } else { + let errorBody; + try { + errorBody = await response.clone().json(); + } catch { + errorBody = await response.text(); + } + console.error('Response error:', response.status, errorBody); + } + } + + @action.bound async tagSelectedFilesUsingTaggingService(): Promise { + if (this.isTaggingWithService) { + return; + } + this.isTaggingWithService = true; + const taggingServiceURL = this.rootStore.uiStore.taggingServiceURL; + const files = Array.from(this.rootStore.uiStore.fileSelection); + const numFiles = files.length; + const isMulti = numFiles > 1; + const toastKey = 'tagging-using-service'; + let isCancelled = false; + const clickAction = { label: 'Open DevTools', onClick: RendererMessenger.toggleDevTools }; + const showProgressToaster = (progress: number) => + !isCancelled && + AppToaster.show( + { + message: `Tagging ${numFiles} file${isMulti ? 's ' : ''}${ + isMulti ? (progress * 100).toFixed(1) : '' + }${isMulti ? '%...' : '...'}`, + timeout: 0, + clickAction: { + label: 'Cancel', + onClick: () => { + isCancelled = true; + }, + }, + }, + toastKey, + ); + + showProgressToaster(0); + + let successCount = 0; + let isServiceActive = false; + // Process files with only N jobs in parallel and a progress + cancel callback + const N = 4; + await promiseAllLimit( + files.map((file) => async () => { + const generatedTagNames = await this.getTagNamesUsingTaggingService(file); + if (!isCancelled && generatedTagNames !== undefined && generatedTagNames.length > 0) { + await this.tagFileUsingNamesOrAliases(file, generatedTagNames); + // Add a common tag to indicate that the file was auto-tagged, allowing the user to filter them. + await this.tagFileUsingNamesOrAliases(file, ['auto-tagged']); + // Save the file even if it has been disposed after a change of content view + if (!file.isAutoSaveEnabled) { + this.save(runInAction(() => file.serialize())); + } + successCount++; + // else show toasts, allways for the first one but do not spam if all are fails. + } else if (!isServiceActive || successCount > 0) { + AppToaster.show( + { + type: 'error', + message: `Failed to get the tags for "${file.name}" from the tagging service`, + timeout: 8000, + clickAction: clickAction, + }, + `${toastKey}-failed-${file.id}`, + ); + } + isServiceActive = true; + }), + N, + showProgressToaster, + () => isCancelled, + ).catch((e) => console.error(e)); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (successCount === 0) { + const message = taggingServiceURL + ? 'Could not get tags from the tagging service: is it not running, or is the API/URL misconfigured?' + : 'No Local Tagging Service API configured, go to: Settings > Background Processes > Local AI Tagging API URL'; + AppToaster.show( + { + type: 'error', + message: message, + timeout: 10000, + clickAction: taggingServiceURL ? clickAction : undefined, + }, + toastKey, + ); + } else { + const isSuccess = successCount === numFiles; + AppToaster.show( + { + type: isSuccess ? 'success' : 'warning', + message: `Successfully tagged ${successCount} of ${numFiles} files.`, + timeout: 10000, + clickAction: !isSuccess ? clickAction : undefined, + }, + toastKey, + ); + } + this.isTaggingWithService = false; + } + get InheritedTagsVisibilityMode(): InheritedTagsVisibilityModeType { return this.rootStore.uiStore.inheritedTagsVisibilityMode; } diff --git a/src/frontend/stores/UiStore.ts b/src/frontend/stores/UiStore.ts index 89ced40a..eb88ccf7 100644 --- a/src/frontend/stores/UiStore.ts +++ b/src/frontend/stores/UiStore.ts @@ -136,6 +136,7 @@ type PersistentPreferenceFields = | 'isFileTagsEditorOpen' | 'isFileExtraPropertiesEditorOpen' | 'thumbnailDirectory' + | 'taggingServiceURL' | 'importDirectory' | 'method' | 'thumbnailSize' @@ -249,6 +250,7 @@ class UiStore { @observable thumbnailDirectory: string = ''; @observable importDirectory: string = ''; // for browser extension. Must be a (sub-folder of a) Location + @observable taggingServiceURL: string = ''; @observable readonly hotkeyMap: IHotkeyMap = observable(defaultHotkeyMap); @@ -714,6 +716,10 @@ class UiStore { this.thumbnailDirectory = dir; } + @action.bound setTaggingServiceURL(url: string = ''): void { + this.taggingServiceURL = encodeURI(url); + } + @action.bound setImportDirectory(dir: string): void { this.importDirectory = dir; } @@ -1320,6 +1326,9 @@ class UiStore { if (prefs.thumbnailDirectory) { this.setThumbnailDirectory(prefs.thumbnailDirectory); } + if (prefs.taggingServiceURL) { + this.setTaggingServiceURL(prefs.taggingServiceURL); + } if (prefs.importDirectory) { this.setImportDirectory(prefs.importDirectory); } @@ -1421,6 +1430,7 @@ class UiStore { isFileTagsEditorOpen: this.isFileTagsEditorOpen, isFileExtraPropertiesEditorOpen: this.isFileExtraPropertiesEditorOpen, thumbnailDirectory: this.thumbnailDirectory, + taggingServiceURL: this.taggingServiceURL, importDirectory: this.importDirectory, method: this.method, thumbnailSize: this.thumbnailSize, From 7b1521254114e90464f99fb22144e7e769449256 Mon Sep 17 00:00:00 2001 From: RafaUC Date: Wed, 27 Aug 2025 20:00:48 -0600 Subject: [PATCH 3/6] Feature: Configure the number of requests in parallel to the tagging service. --- .../Settings/BackgroundProcesses.tsx | 27 +++++++++++++++++++ src/frontend/stores/FileStore.ts | 2 +- src/frontend/stores/UiStore.ts | 16 +++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/frontend/containers/Settings/BackgroundProcesses.tsx b/src/frontend/containers/Settings/BackgroundProcesses.tsx index 09d75c8d..70c3f6d7 100644 --- a/src/frontend/containers/Settings/BackgroundProcesses.tsx +++ b/src/frontend/containers/Settings/BackgroundProcesses.tsx @@ -8,6 +8,7 @@ import { Callout } from 'widgets/notifications'; import { useStore } from '../../contexts/StoreContext'; import FileInput from 'src/frontend/components/FileInput'; import { useGalleryInputKeydownHandler } from 'src/frontend/hooks/useHandleInputKeydown'; +import UiStore from 'src/frontend/stores/UiStore'; export const BackgroundProcesses = observer(() => { const { uiStore, locationStore } = useStore(); @@ -131,6 +132,32 @@ const TaggingServiceConfig = observer(() => { onChange={handleChange} /> +
+ +
+
); }); + +const TaggingServiceParallelRequests = observer(() => { + const { uiStore } = useStore(); + + const handleChange = (event: React.ChangeEvent) => { + const value = Number(event.target.value); + uiStore.setTaggingServiceParallelRequests(value); + }; + + return ( + + ); +}); diff --git a/src/frontend/stores/FileStore.ts b/src/frontend/stores/FileStore.ts index 8b562b13..d7c5408a 100644 --- a/src/frontend/stores/FileStore.ts +++ b/src/frontend/stores/FileStore.ts @@ -380,7 +380,7 @@ class FileStore { let successCount = 0; let isServiceActive = false; // Process files with only N jobs in parallel and a progress + cancel callback - const N = 4; + const N = this.rootStore.uiStore.taggingServiceParallelRequests; await promiseAllLimit( files.map((file) => async () => { const generatedTagNames = await this.getTagNamesUsingTaggingService(file); diff --git a/src/frontend/stores/UiStore.ts b/src/frontend/stores/UiStore.ts index eb88ccf7..94f5328c 100644 --- a/src/frontend/stores/UiStore.ts +++ b/src/frontend/stores/UiStore.ts @@ -137,6 +137,7 @@ type PersistentPreferenceFields = | 'isFileExtraPropertiesEditorOpen' | 'thumbnailDirectory' | 'taggingServiceURL' + | 'taggingServiceParallelRequests' | 'importDirectory' | 'method' | 'thumbnailSize' @@ -169,6 +170,7 @@ class UiStore { static MIN_OUTLINER_WIDTH = 192; // default of 12 rem static MIN_INSPECTOR_WIDTH = 288; // default of 18 rem static MAX_RECENTLY_USED_TAGS = 40; + static MAX_TAGGING_SERVICE_PARALLEL_REQUESTS = 10; private readonly rootStore: RootStore; @@ -250,7 +252,9 @@ class UiStore { @observable thumbnailDirectory: string = ''; @observable importDirectory: string = ''; // for browser extension. Must be a (sub-folder of a) Location + @observable taggingServiceURL: string = ''; + @observable taggingServiceParallelRequests: number = 4; @observable readonly hotkeyMap: IHotkeyMap = observable(defaultHotkeyMap); @@ -720,6 +724,14 @@ class UiStore { this.taggingServiceURL = encodeURI(url); } + @action.bound setTaggingServiceParallelRequests(value: number = 1): void { + this.taggingServiceParallelRequests = clamp( + value, + 1, + UiStore.MAX_TAGGING_SERVICE_PARALLEL_REQUESTS, + ); + } + @action.bound setImportDirectory(dir: string): void { this.importDirectory = dir; } @@ -1329,6 +1341,9 @@ class UiStore { if (prefs.taggingServiceURL) { this.setTaggingServiceURL(prefs.taggingServiceURL); } + if ('taggingServiceParallelRequests' in prefs) { + this.setTaggingServiceParallelRequests(prefs.taggingServiceParallelRequests); + } if (prefs.importDirectory) { this.setImportDirectory(prefs.importDirectory); } @@ -1431,6 +1446,7 @@ class UiStore { isFileExtraPropertiesEditorOpen: this.isFileExtraPropertiesEditorOpen, thumbnailDirectory: this.thumbnailDirectory, taggingServiceURL: this.taggingServiceURL, + taggingServiceParallelRequests: this.taggingServiceParallelRequests, importDirectory: this.importDirectory, method: this.method, thumbnailSize: this.thumbnailSize, From 10d5364f04ce8dab8748ecd74171b96432197523 Mon Sep 17 00:00:00 2001 From: RafaUC Date: Sat, 6 Sep 2025 15:20:01 -0600 Subject: [PATCH 4/6] Show better tagging progress feedback. --- src/frontend/stores/FileStore.ts | 70 +++++++++++++++++++------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/src/frontend/stores/FileStore.ts b/src/frontend/stores/FileStore.ts index d7c5408a..8994363c 100644 --- a/src/frontend/stores/FileStore.ts +++ b/src/frontend/stores/FileStore.ts @@ -354,47 +354,57 @@ class FileStore { const files = Array.from(this.rootStore.uiStore.fileSelection); const numFiles = files.length; const isMulti = numFiles > 1; + let successCount = 0; + let isServiceActive = false; const toastKey = 'tagging-using-service'; let isCancelled = false; const clickAction = { label: 'Open DevTools', onClick: RendererMessenger.toggleDevTools }; - const showProgressToaster = (progress: number) => - !isCancelled && - AppToaster.show( - { - message: `Tagging ${numFiles} file${isMulti ? 's ' : ''}${ - isMulti ? (progress * 100).toFixed(1) : '' - }${isMulti ? '%...' : '...'}`, - timeout: 0, - clickAction: { - label: 'Cancel', - onClick: () => { - isCancelled = true; + const showProgressToaster = (progress: number) => { + if (!isCancelled) { + const progressCount = Math.round(numFiles * progress); + AppToaster.show( + { + message: `Tagging ${numFiles} file${isMulti ? 's ' : ''}${ + isMulti ? (progress * 100).toFixed(1) : '' + }${isMulti ? '%...' : '...'} ${ + successCount !== progressCount ? `(${progressCount - successCount} failures)` : '' + }`, + timeout: 0, + clickAction: { + label: 'Cancel', + onClick: () => { + isCancelled = true; + }, }, }, - }, - toastKey, - ); + toastKey, + ); + } + }; showProgressToaster(0); - - let successCount = 0; - let isServiceActive = false; // Process files with only N jobs in parallel and a progress + cancel callback const N = this.rootStore.uiStore.taggingServiceParallelRequests; await promiseAllLimit( files.map((file) => async () => { + const currentSuccessCount = successCount; const generatedTagNames = await this.getTagNamesUsingTaggingService(file); if (!isCancelled && generatedTagNames !== undefined && generatedTagNames.length > 0) { - await this.tagFileUsingNamesOrAliases(file, generatedTagNames); - // Add a common tag to indicate that the file was auto-tagged, allowing the user to filter them. - await this.tagFileUsingNamesOrAliases(file, ['auto-tagged']); - // Save the file even if it has been disposed after a change of content view - if (!file.isAutoSaveEnabled) { - this.save(runInAction(() => file.serialize())); + try { + await this.tagFileUsingNamesOrAliases(file, generatedTagNames); + // Add a common tag to indicate that the file was auto-tagged, allowing the user to filter them. + await this.tagFileUsingNamesOrAliases(file, ['auto-tagged']); + // Save the file even if it has been disposed after a change of content view + if (!file.isAutoSaveEnabled) { + this.save(runInAction(() => file.serialize())); + } + successCount++; + } catch (error) { + console.error(error); } - successCount++; - // else show toasts, allways for the first one but do not spam if all are fails. - } else if (!isServiceActive || successCount > 0) { + } + // if not success, allways for the first one but do not spam if all are fails. + if (successCount === currentSuccessCount && (!isServiceActive || successCount > 0)) { AppToaster.show( { type: 'error', @@ -431,8 +441,10 @@ class FileStore { AppToaster.show( { type: isSuccess ? 'success' : 'warning', - message: `Successfully tagged ${successCount} of ${numFiles} files.`, - timeout: 10000, + message: `Successfully tagged ${successCount} of ${numFiles} files. ${ + !isSuccess ? `(${numFiles - successCount} failures)` : '' + }`, + timeout: 0, clickAction: !isSuccess ? clickAction : undefined, }, toastKey, From 1fdeffc04c6dd0a3a95a34e0ea344506a0dac679 Mon Sep 17 00:00:00 2001 From: RafaUC Date: Sat, 13 Sep 2025 00:12:23 -0600 Subject: [PATCH 5/6] update HelpCenter and tagging service config callout --- src/frontend/containers/HelpCenter.tsx | 22 +++++++++++++++++++ .../Settings/BackgroundProcesses.tsx | 6 +++++ 2 files changed, 28 insertions(+) diff --git a/src/frontend/containers/HelpCenter.tsx b/src/frontend/containers/HelpCenter.tsx index e131899b..0c0a7ea4 100644 --- a/src/frontend/containers/HelpCenter.tsx +++ b/src/frontend/containers/HelpCenter.tsx @@ -313,6 +313,24 @@ const PAGE_DATA: () => IPageData[] = () => [ ), }, + { + title: 'Automatic Tagging', + content: ( + <> +

+ You can set an endpoint to a locally hosted AI tagging service of your preference, + allowing the app to send requests and automatically tag files. You can also configure + the number of concurrent requests made to the service simultaneously. For more + information, see the "Background Processes" section in the settings window. +

+

+ To automatically tag selected files, use the + {' "Tagging... > Tag Selected Using AI Tagging Service" '} + option in the file context menu. +

+ + ), + }, { title: 'Tag Import/Export', content: ( @@ -326,6 +344,10 @@ const PAGE_DATA: () => IPageData[] = () => [
Note that only the images shown in the gallery are affected by these operations!

+

+ You can also import/export tags from selected files through the "Tagging" options in + the file context menu. +

), }, diff --git a/src/frontend/containers/Settings/BackgroundProcesses.tsx b/src/frontend/containers/Settings/BackgroundProcesses.tsx index 70c3f6d7..545fe9d7 100644 --- a/src/frontend/containers/Settings/BackgroundProcesses.tsx +++ b/src/frontend/containers/Settings/BackgroundProcesses.tsx @@ -121,6 +121,12 @@ const TaggingServiceConfig = observer(() => { {' '} must be running.
+ + {'The endpoint must accept a JSON request with the format:'}
+ {'{ file: } '}
+ {'and respond with a JSON in the format:'}
+ {'{ tags: [{ name: }, { name: }, ...] }'} +
{prehost} From 2f2cee64bd26d6e0ce4949cfb1495729c49142c2 Mon Sep 17 00:00:00 2001 From: RafaUC Date: Sat, 13 Sep 2025 02:37:03 -0600 Subject: [PATCH 6/6] Change the nomenclature for tagging service related texts to avoid assuming it is AI based, since more implementations are possible. --- .../containers/ContentView/menu-items.tsx | 2 +- src/frontend/containers/HelpCenter.tsx | 11 ++++---- .../Settings/BackgroundProcesses.tsx | 28 +++++++++++++++---- src/frontend/stores/FileStore.ts | 2 +- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/frontend/containers/ContentView/menu-items.tsx b/src/frontend/containers/ContentView/menu-items.tsx index 63ca5d29..4e069fc8 100644 --- a/src/frontend/containers/ContentView/menu-items.tsx +++ b/src/frontend/containers/ContentView/menu-items.tsx @@ -99,7 +99,7 @@ export const FileViewerMenuItems = ({ file }: { file: ClientFile }) => { /> diff --git a/src/frontend/containers/HelpCenter.tsx b/src/frontend/containers/HelpCenter.tsx index 0c0a7ea4..dcf37cc1 100644 --- a/src/frontend/containers/HelpCenter.tsx +++ b/src/frontend/containers/HelpCenter.tsx @@ -318,14 +318,15 @@ const PAGE_DATA: () => IPageData[] = () => [ content: ( <>

- You can set an endpoint to a locally hosted AI tagging service of your preference, - allowing the app to send requests and automatically tag files. You can also configure - the number of concurrent requests made to the service simultaneously. For more - information, see the "Background Processes" section in the settings window. + You can set an endpoint to a locally hosted AI tagging service or any custom tagging + implementation, allowing the app to send requests and automatically tag files with the + service response. You can also configure the number of concurrent requests made to the + service simultaneously. For more information, see the "Background Processes" section + in the settings window.

To automatically tag selected files, use the - {' "Tagging... > Tag Selected Using AI Tagging Service" '} + {' "Tagging... > Auto Tag Selected Using Tagging Service" '} option in the file context menu.

diff --git a/src/frontend/containers/Settings/BackgroundProcesses.tsx b/src/frontend/containers/Settings/BackgroundProcesses.tsx index 545fe9d7..4954289a 100644 --- a/src/frontend/containers/Settings/BackgroundProcesses.tsx +++ b/src/frontend/containers/Settings/BackgroundProcesses.tsx @@ -113,19 +113,35 @@ const TaggingServiceConfig = observer(() => { // Custom and minimalistic implementation inspired/based on cmeka's implementation: https://github.com/cmeka/OneFolder/commit/b0d7e12 return ( <> -

Local AI Tagging API URL

+

Local Tagging Service API URL

A tagging service such as{' '} media-tag-service {' '} - must be running. + or any custom tagging endpoint must be running. - {'The endpoint must accept a JSON request with the format:'}
- {'{ file: } '}
- {'and respond with a JSON in the format:'}
- {'{ tags: [{ name: }, { name: }, ...] }'} +
+ {'The endpoint must accept a JSON request with the format:'} +
{'{ "file": "" }'}
+ {'and respond with a JSON in the format:'} +
+            {'{'}
+            
+ {' "tags": ['} +
+ {' { "name": "" },'} +
+ {' { "name": "" },'} +
+ {' ... etc.'} +
+ {' ]'} +
+ {'}'} +
+
diff --git a/src/frontend/stores/FileStore.ts b/src/frontend/stores/FileStore.ts index 8994363c..5c7af178 100644 --- a/src/frontend/stores/FileStore.ts +++ b/src/frontend/stores/FileStore.ts @@ -426,7 +426,7 @@ class FileStore { if (successCount === 0) { const message = taggingServiceURL ? 'Could not get tags from the tagging service: is it not running, or is the API/URL misconfigured?' - : 'No Local Tagging Service API configured, go to: Settings > Background Processes > Local AI Tagging API URL'; + : 'No Local Tagging Service API configured, go to: Settings > Background Processes > Local Tagging Service API URL'; AppToaster.show( { type: 'error',