Skip to content

Commit b0d7e12

Browse files
committed
feat(tags): AI tag files
Adds the ability to tag files using AI models via local API service such as https://github.com/cmeka/media-tag-service
1 parent 247e51d commit b0d7e12

11 files changed

Lines changed: 378 additions & 8 deletions

File tree

src/api/tag.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export type TagDTO = {
1111
subTags: ID[];
1212
/** Whether any files with this tag should be hidden */
1313
isHidden: boolean;
14+
isAi: boolean;
1415
};

src/backend/backend.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default class Backend implements DataStorage {
5656
subTags: [],
5757
color: '',
5858
isHidden: false,
59+
isAi: false,
5960
});
6061
}
6162
});

src/frontend/containers/ContentView/menu-items.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ export const FileViewerMenuItems = ({ file }: { file: ClientFile }) => {
8181
text="Open Tag Selector (T)"
8282
icon={IconSet.TAG}
8383
/>
84+
<MenuItem
85+
onClick={uiStore.aiTagSelection}
86+
text="AI Tag Selection"
87+
icon={IconSet.TAG}
88+
/>
8489
<MenuItem
8590
onClick={handleRemoveAllTags}
8691
text="Remove All Tags"

src/frontend/containers/Outliner/LocationsPanel/index.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ const DirectoryMenu = observer(
142142
location: ClientLocation | ClientSubLocation;
143143
onExclude: (subLocation: ClientSubLocation) => void;
144144
}) => {
145-
const { uiStore } = useStore();
145+
const { uiStore, fileStore } = useStore();
146146

147147
const path = location instanceof ClientLocation ? location.path : location.path;
148148

@@ -158,6 +158,11 @@ const DirectoryMenu = observer(
158158
[path, uiStore],
159159
);
160160

161+
const handleAiTagDirectory = useCallback(
162+
() => fileStore.aiTagDirectory(path),
163+
[path, fileStore],
164+
);
165+
161166
return (
162167
<>
163168
<MenuItem onClick={handleAddToSearch} text="Add to Search Query" icon={IconSet.SEARCH} />
@@ -167,6 +172,12 @@ const DirectoryMenu = observer(
167172
icon={IconSet.REPLACE}
168173
/>
169174
<MenuDivider />
175+
<MenuItem
176+
onClick={handleAiTagDirectory}
177+
text="AI Tag this Directory"
178+
icon={IconSet.TAG_GROUP}
179+
/>
180+
<MenuDivider />
170181
{location instanceof ClientSubLocation && (
171182
<MenuItem
172183
// Only show alert when excluding, not when re-including

src/frontend/containers/Settings/Advanced.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ExternalLink from 'src/frontend/components/ExternalLink';
1414
export const Advanced = observer(() => {
1515
const { uiStore, fileStore } = useStore();
1616
const thumbnailDirectory = uiStore.thumbnailDirectory;
17+
const aiTagUrl: string = fileStore.aiTagUrl;
1718

1819
const [defaultThumbnailDir, setDefaultThumbnailDir] = useState('');
1920
useEffect(() => {
@@ -74,6 +75,12 @@ export const Advanced = observer(() => {
7475
<div className="filepicker-path">{thumbnailDirectory}</div>
7576
</div>
7677

78+
<h3>AI Tagging API</h3>
79+
<Callout icon={IconSet.INFO}>
80+
A tagging service such as <ExternalLink url="https://github.com/cmeka/media-tag-service">media-tag-service</ExternalLink> must be running.
81+
</Callout>
82+
<input name="aiTagUrl" defaultValue={aiTagUrl} size={40} onChange={fileStore.setAiTagUrl}></input>
83+
7784
<h3>Development</h3>
7885
<ButtonGroup>
7986
<ClearDbButton />

src/frontend/entities/File.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,16 @@ export class ClientFile {
117117
return this.annotations;
118118
}
119119

120+
@computed get hasAiTags(): boolean {
121+
if (this.tags.size === 0) return false;
122+
for (const obj of this.tags) {
123+
if (obj.isAi === true) {
124+
return true;
125+
}
126+
}
127+
return false;
128+
}
129+
120130
@action.bound setThumbnailPath(thumbnailPath: string): void {
121131
this.thumbnailPath = thumbnailPath;
122132
}

src/frontend/entities/Tag.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class ClientTag {
1919
@observable name: string;
2020
@observable color: string;
2121
@observable isHidden: boolean;
22+
@observable isAi: boolean;
2223
@observable private _parent: ClientTag | undefined;
2324
readonly subTags = observable<ClientTag>([]);
2425
// icon, (fileCount?)
@@ -37,6 +38,7 @@ export class ClientTag {
3738
dateAdded: Date,
3839
color: string,
3940
isHidden: boolean,
41+
isAi: boolean,
4042
) {
4143
this.store = store;
4244
this.id = id;
@@ -45,6 +47,7 @@ export class ClientTag {
4547
this.color = color;
4648
this.fileCount = 0;
4749
this.isHidden = isHidden;
50+
this.isAi = isAi;
4851

4952
// observe all changes to observable fields
5053
this.saveHandler = reaction(
@@ -199,6 +202,7 @@ export class ClientTag {
199202
color: this.color,
200203
subTags: this.subTags.map((subTag) => subTag.id),
201204
isHidden: this.isHidden,
205+
isAi: this.isAi,
202206
};
203207
}
204208

0 commit comments

Comments
 (0)