From f1116c6c26192787673335884f45cfa84422dc8c Mon Sep 17 00:00:00 2001 From: LyAhn Date: Fri, 3 Jul 2026 08:07:42 +0100 Subject: [PATCH] feat(onboarding): choose AI tagger model Add WD and JoyTag selection to the Welcome Tour AI step so users can choose the model before downloading it. Share tagger model metadata with Settings and keep the Settings close button anchored to the modal chrome. --- src/components/SettingsModal.tsx | 39 +++++--------- src/components/onboarding/StepAiFeatures.tsx | 53 +++++++++++++++++++- src/taggerModels.ts | 16 ++++++ 3 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 src/taggerModels.ts diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index f6ea576..cee18b0 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -4,6 +4,7 @@ import { FfmpegStatusRow } from "./onboarding/StepWelcome"; import { ThemedDropdown } from "./ThemedDropdown"; import { getChangelogForVersion } from "../changelog"; import { Tooltip } from "./Tooltip"; +import { TAGGER_MODELS } from "../taggerModels"; type SettingsSection = "workspace" | "general"; @@ -101,22 +102,6 @@ function ScopeButton({ scope, current, onSelect, children }: { ); } -// Display metadata for each selectable tagging model. -const TAGGER_MODELS: Record = { - wd: { - name: "WD SwinV2 Tagger v3", - tab: "WD (anime)", - description: - "Anime-focused vision model by SmilingWolf. Generates booru-style tags with configurable confidence thresholds.", - }, - joytag: { - name: "JoyTag", - tab: "JoyTag (general)", - description: - "Booru-schema tagger that also handles photographic content and is strong on NSFW concepts. The explicitness rating is derived from its tags.", - }, -}; - function TaggerModelButton({ model, current, onSelect, children }: { model: TaggerModel; current: TaggerModel; @@ -445,16 +430,18 @@ export function SettingsModal() { - - - +
+ + + +
diff --git a/src/components/onboarding/StepAiFeatures.tsx b/src/components/onboarding/StepAiFeatures.tsx index 049bf25..c32b679 100644 --- a/src/components/onboarding/StepAiFeatures.tsx +++ b/src/components/onboarding/StepAiFeatures.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; -import { TaggerModelProgress, useGalleryStore } from "../../store"; +import { TaggerModel, TaggerModelProgress, useGalleryStore } from "../../store"; +import { TAGGER_MODELS } from "../../taggerModels"; import { FakeProgressBar, formatBytes } from "./fakes"; const FAKE_TAGS = ["landscape", "sunset", "outdoors", "no_humans", "ocean", "cloudy_sky"]; @@ -15,19 +16,47 @@ function taggerDownloadFraction(progress: TaggerModelProgress | null): number | return null; } +function TaggerModelChoice({ model, current, disabled, onSelect }: { + model: TaggerModel; + current: TaggerModel; + disabled: boolean; + onSelect: (model: TaggerModel) => void; +}) { + const active = model === current; + return ( + + ); +} + export function StepAiFeatures() { const taggerModelStatus = useGalleryStore((s) => s.taggerModelStatus); const taggerModelPreparing = useGalleryStore((s) => s.taggerModelPreparing); const taggerModelProgress = useGalleryStore((s) => s.taggerModelProgress); const taggerModelError = useGalleryStore((s) => s.taggerModelError); + const taggerModel = useGalleryStore((s) => s.taggerModel); const prepareTaggerModel = useGalleryStore((s) => s.prepareTaggerModel); + const loadTaggerModel = useGalleryStore((s) => s.loadTaggerModel); const loadTaggerModelStatus = useGalleryStore((s) => s.loadTaggerModelStatus); + const setTaggerModel = useGalleryStore((s) => s.setTaggerModel); useEffect(() => { + void loadTaggerModel(); void loadTaggerModelStatus(); - }, [loadTaggerModelStatus]); + }, [loadTaggerModel, loadTaggerModelStatus]); const taggerReady = taggerModelStatus?.ready ?? false; + const taggerStatusLabel = taggerReady ? "Installed" : taggerModelPreparing ? "Preparing" : "Not installed"; return (
@@ -46,6 +75,26 @@ export function StepAiFeatures() { The AI tagger model labels images so you can search with{" "} /t — tags look like:

+
+ {(["wd", "joytag"] as const).map((model) => ( + { + if (nextModel === taggerModel) return; + void setTaggerModel(nextModel); + }} + /> + ))} +
+

+ Current: {TAGGER_MODELS[taggerModel].name} · {taggerStatusLabel} +

+

+ {TAGGER_MODELS[taggerModel].description} +

{FAKE_TAGS.map((tag) => ( diff --git a/src/taggerModels.ts b/src/taggerModels.ts new file mode 100644 index 0000000..e3ac289 --- /dev/null +++ b/src/taggerModels.ts @@ -0,0 +1,16 @@ +import type { TaggerModel } from "./store"; + +export const TAGGER_MODELS: Record = { + wd: { + name: "WD SwinV2 Tagger v3", + tab: "WD (anime)", + description: + "Anime-focused vision model by SmilingWolf. Generates booru-style tags with configurable confidence thresholds.", + }, + joytag: { + name: "JoyTag", + tab: "JoyTag (general)", + description: + "Booru-schema tagger that also handles photographic content and is strong on NSFW concepts. The explicitness rating is derived from its tags.", + }, +};