feat(ai-tags): add reset flow
Add a reset_ai_tags backend command that removes AI-generated tag rows, clears AI tagging metadata, cancels active tagging jobs, and drops queued or failed tagging jobs in the selected scope. Expose reset actions in AI Workspace and the Explore tag manager, refresh gallery/progress/tag state after reset, and add subtle AI source indicators to tag manager rows.
This commit is contained in:
@@ -106,6 +106,38 @@ function searchTagsAutocomplete(payload: unknown): ExploreTagEntry[] {
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function rebuildExploreTags(): ExploreTagEntry[] {
|
||||
const entries = new Map<
|
||||
string,
|
||||
{ imageIds: Set<number>; representativeImageId: number; hasAiSource: boolean; hasUserSource: boolean }
|
||||
>();
|
||||
for (const [imageIdString, imageTags] of Object.entries(db.tagsByImageId)) {
|
||||
const imageId = Number(imageIdString);
|
||||
for (const imageTag of imageTags) {
|
||||
const entry =
|
||||
entries.get(imageTag.tag) ??
|
||||
{ imageIds: new Set<number>(), representativeImageId: imageId, hasAiSource: false, hasUserSource: false };
|
||||
entry.imageIds.add(imageId);
|
||||
if (imageTag.source === "ai") entry.hasAiSource = true;
|
||||
if (imageTag.source === "user") entry.hasUserSource = true;
|
||||
entries.set(imageTag.tag, entry);
|
||||
}
|
||||
}
|
||||
return [...entries.entries()]
|
||||
.map(([tag, entry]) => {
|
||||
const representative = db.images.find((image) => image.id === entry.representativeImageId);
|
||||
return {
|
||||
tag,
|
||||
count: entry.imageIds.size,
|
||||
representative_image_id: entry.representativeImageId,
|
||||
thumbnail_path: representative?.thumbnail_path ?? null,
|
||||
has_ai_source: entry.hasAiSource,
|
||||
has_user_source: entry.hasUserSource,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => right.count - left.count || left.tag.localeCompare(right.tag));
|
||||
}
|
||||
|
||||
function semanticSearch(payload: unknown): ImageRecord[] {
|
||||
const p = params(payload);
|
||||
const query = String(p.query ?? "").toLowerCase();
|
||||
@@ -400,6 +432,32 @@ export async function handleMockCommand(cmd: string, payload?: unknown): Promise
|
||||
case "queue_tagging_jobs":
|
||||
case "clear_tagging_jobs":
|
||||
return 12;
|
||||
case "reset_ai_tags": {
|
||||
const folderIds = p.folder_ids as number[] | undefined;
|
||||
const folderId = p.folder_id as number | null | undefined;
|
||||
const scopedImages = db.images.filter((image) =>
|
||||
folderIds && folderIds.length > 0
|
||||
? folderIds.includes(image.folder_id)
|
||||
: folderId != null
|
||||
? image.folder_id === folderId
|
||||
: true,
|
||||
);
|
||||
let reset = 0;
|
||||
for (const image of scopedImages) {
|
||||
const tags = db.tagsByImageId[image.id] ?? [];
|
||||
const aiTags = tags.filter((tag) => tag.source === "ai");
|
||||
if (aiTags.length > 0 || image.ai_tagged_at || image.ai_tagger_error || image.ai_tagger_model || image.ai_rating) {
|
||||
reset += 1;
|
||||
}
|
||||
db.tagsByImageId[image.id] = tags.filter((tag) => tag.source !== "ai");
|
||||
image.ai_rating = null;
|
||||
image.ai_tagger_model = null;
|
||||
image.ai_tagged_at = null;
|
||||
image.ai_tagger_error = null;
|
||||
}
|
||||
db.exploreTags = rebuildExploreTags();
|
||||
return reset;
|
||||
}
|
||||
case "get_tagger_model_status":
|
||||
return { model_id: "SmilingWolf/wd-vit-tagger-v3", model_name: "WD ViT Tagger", local_dir: "mock://models/tagger", ready: true, missing_files: [] };
|
||||
case "get_tagger_model":
|
||||
|
||||
@@ -280,11 +280,14 @@ function makeExploreTags(images: ImageRecord[], scenario: MockScenario): Explore
|
||||
return vocabulary.map((tag, index) => {
|
||||
const representative = images[index % Math.max(images.length, 1)];
|
||||
const divisor = scenario === "huge" ? 2 + Math.pow(index + 1, 0.58) : index + 3;
|
||||
const sourceCycle = index % 3;
|
||||
return {
|
||||
tag,
|
||||
count: Math.max(1, Math.round(images.length / divisor)),
|
||||
representative_image_id: representative?.id ?? index + 1,
|
||||
thumbnail_path: representative?.thumbnail_path ?? null,
|
||||
has_ai_source: sourceCycle !== 1,
|
||||
has_user_source: sourceCycle !== 0,
|
||||
};
|
||||
}).sort((left, right) => right.count - left.count || left.tag.localeCompare(right.tag));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user