From 4d839d3b8b3ea91fd40cb8ea7c740a67b6e513e4 Mon Sep 17 00:00:00 2001 From: BFlat Date: Sat, 8 Aug 2026 15:41:13 +0700 Subject: [PATCH] Library/video promptbox: bulk "Add tags" action + default video mode to Seedance 2.5 (#1864) - Select multiple items in the library and add tags to all of them at once - Tag suggestions pop up as you type, based on your existing tags - Fixed the selection bar looking squished (buttons no longer wrap onto two lines) - Video generation now defaults to Seedance 2.5 (was 2.0) - Omni Reference is the default video input mode for everyone --- .../pages/create-video/create-video-store.ts | 19 +++- .../src/pages/create-video/create-video.tsx | 6 +- .../src/pages/library/library-tags-store.ts | 38 +++++++ .../src/pages/library/library.tsx | 105 ++++++++++++++++-- .../src/lib/tags/TagChipInput.tsx | 10 +- .../src/lib/classes/metadata/ModelMapping.ts | 1 + 6 files changed, 163 insertions(+), 16 deletions(-) diff --git a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video-store.ts b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video-store.ts index 1f906de532..e740dcde5d 100644 --- a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video-store.ts +++ b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video-store.ts @@ -187,10 +187,11 @@ export const useCreateVideoStore = create()( }), { name: "artcraft-video-batches", - // Bumped to 1 when reference became the default input mode: the - // migration runs once for pre-existing persisted state and resets the - // stored mode so everyone lands on the new default. - version: 1, + // Bumped to 1 when reference became the default input mode, and to 2 + // when Seedance 2.5 replaced 2.0 as the default model: each migration + // runs once for pre-existing persisted state so everyone lands on the + // new defaults. + version: 2, migrate: (persisted, version) => { const p = (persisted ?? {}) as { batches?: VideoBatch[]; @@ -200,6 +201,16 @@ export const useCreateVideoStore = create()( if (version < 1) { ui.inputMode = "reference"; } + if (version < 2) { + // Seedance 2.5 replaced 2.0 as the default: users still on the old + // default follow it (null resolves to DEFAULT_MODEL_ID at read + // time); explicit picks of other models are left alone. Everyone + // lands back on Omni Reference as the default input mode. + if (ui.selectedModelId === "seedance_2p0") { + ui.selectedModelId = null; + } + ui.inputMode = "reference"; + } return { batches: p.batches ?? [], ui }; }, // Persist prompt + lightweight settings alongside pending batches so a diff --git a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx index 408e218d17..68f2594b12 100644 --- a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx @@ -88,7 +88,7 @@ import { toast } from "../../components/toast/toast"; // ── Constants ──────────────────────────────────────────────────────────── -const DEFAULT_MODEL_ID = "seedance_2p0"; +const DEFAULT_MODEL_ID = "seedance_2p5"; // Models where character @-mentions are intentionally held back in the UI for now, // even though the API reports character_references_supported. Remove an entry to enable. @@ -637,10 +637,10 @@ export default function CreateVideo() { activeCharacters, ]); - // Seedance 2.0 models support @-mention references in the prompt — once at + // Seedance 2.x models support @-mention references in the prompt — once at // least one ref is attached, surface that in the placeholder. const isSeedance2Model = (selectedModel?.model ?? "").startsWith( - "seedance_2p0", + "seedance_2", ); const hasUploadedRefs = referenceImages.length > 0 || diff --git a/frontend/apps/artcraft-webapp/src/pages/library/library-tags-store.ts b/frontend/apps/artcraft-webapp/src/pages/library/library-tags-store.ts index 6099af122b..77db2c27f9 100644 --- a/frontend/apps/artcraft-webapp/src/pages/library/library-tags-store.ts +++ b/frontend/apps/artcraft-webapp/src/pages/library/library-tags-store.ts @@ -36,6 +36,10 @@ interface LibraryTagsState { loadTagMedia: (tagToken: string, reset?: boolean) => Promise; renameTag: (tagToken: string, newValue: string) => Promise; deleteTag: (tagToken: string) => Promise; + /** Add tags to many media files at once (bulk selection bar). */ + bulkAddTags: (mediaFileTokens: string[], tags: string[]) => Promise; + /** Drop cached media for tags whose contents changed; the open tag reloads. */ + invalidateTagMedia: (tagTokens: string[]) => void; /** Merge canonical tags returned by add/set calls (fresh use counts). */ upsertTags: (tags: TagDetails[]) => void; setRenameTarget: (tagToken: string | null) => void; @@ -193,6 +197,40 @@ export const useLibraryTagsStore = create((set, get) => ({ } }, + bulkAddTags: async (mediaFileTokens, tags) => { + try { + const res = await tagsApi.BulkAddTags({ mediaFileTokens, tags }); + if (!res.success || !res.data) { + toast.error(res.errorMessage || "Failed to add tags."); + return false; + } + // Fresh use counts for the sidebar; tagged listings must refetch. + get().upsertTags(res.data.tags); + get().invalidateTagMedia(res.data.tags.map((t) => t.tag_token)); + const count = res.data.accepted_media_file_tokens.length; + toast.success(`Tagged ${count} ${count === 1 ? "item" : "items"}`); + return true; + } catch (err) { + toast.error(`Failed to add tags: ${errMsg(err)}`); + return false; + } + }, + + invalidateTagMedia: (tagTokens) => { + if (tagTokens.length === 0) return; + set((s) => { + const nextMedia = { ...s.tagMediaItems }; + const nextHasMore = { ...s.tagHasMore }; + for (const token of tagTokens) { + delete nextMedia[token]; + delete nextHasMore[token]; + } + return { tagMediaItems: nextMedia, tagHasMore: nextHasMore }; + }); + const active = get().activeTagToken; + if (active && tagTokens.includes(active)) get().loadTagMedia(active, true); + }, + upsertTags: (incoming) => { if (incoming.length === 0) return; set((s) => { diff --git a/frontend/apps/artcraft-webapp/src/pages/library/library.tsx b/frontend/apps/artcraft-webapp/src/pages/library/library.tsx index 76f932b31c..c206d30912 100644 --- a/frontend/apps/artcraft-webapp/src/pages/library/library.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/library/library.tsx @@ -25,6 +25,10 @@ import { FOLDER_DROP_EVENT, type GalleryItem, } from "@storyteller/ui-gallery-modal"; +import { + TagChipInput, + type TagSuggestion, +} from "@storyteller/ui-lightbox-modal"; import { PLACEHOLDER_IMAGES, is3DModelUrl } from "@storyteller/common"; import { is3DMediaClass } from "@storyteller/ui-generation-list"; import { @@ -2077,6 +2081,11 @@ const SEND_TO_TARGETS: { { destination: "video", label: "Create video", icon: faVideo }, ]; +// Shared styling for the bar's action pills. `whitespace-nowrap` keeps labels +// on a single line — the bar grows instead of squishing buttons into two rows. +const BAR_BUTTON_CLASS = + "flex items-center gap-2 whitespace-nowrap rounded-full bg-ui-controls/60 px-3 py-1.5 text-sm font-medium text-white hover:bg-ui-controls/90 transition-colors"; + interface BulkSelectionBarProps { allItems: GalleryItem[]; folders: UiFolder[]; @@ -2100,6 +2109,9 @@ function BulkSelectionBar({ const navigate = useNavigate(); const [popoverOpen, setPopoverOpen] = useState(false); const [sendToOpen, setSendToOpen] = useState(false); + const [tagsOpen, setTagsOpen] = useState(false); + const [tagChips, setTagChips] = useState([]); + const [addingTags, setAddingTags] = useState(false); const [downloadProgress, setDownloadProgress] = useState<{ done: number; total: number; @@ -2113,11 +2125,22 @@ function BulkSelectionBar({ [folders], ); - // Close the popover when navigating so it doesn't dangle over the new view. + // Close the popovers when navigating so they don't dangle over the new view. useEffect(() => { setPopoverOpen(false); + setTagsOpen(false); }, [activeFolderId]); + // Autocomplete for the Add-tags popover: the user's tags, most-used first. + const storeTags = useLibraryTagsStore((s) => s.tags); + const tagSuggestions = useMemo( + (): TagSuggestion[] => + [...storeTags] + .sort(compareTagsByUseCount) + .map((t) => ({ value: t.value, useCount: t.useCount })), + [storeTags], + ); + // Resolve selected items from everything loaded (root library + folder/tag // caches) — the selection survives navigation, so selected items may not be // part of the currently displayed view. @@ -2173,6 +2196,20 @@ function BulkSelectionBar({ } }; + const handleAddTags = async () => { + if (addingTags || tagChips.length === 0) return; + setAddingTags(true); + const ok = await useLibraryTagsStore + .getState() + .bulkAddTags(Array.from(ids), tagChips); + setAddingTags(false); + if (ok) { + setTagsOpen(false); + setTagChips([]); + clear(); + } + }; + return (
)}
- + {ids.size} selected @@ -2206,7 +2243,7 @@ function BulkSelectionBar({ type="button" title="Send to prompt" onClick={() => setSendToOpen((v) => !v)} - className="flex items-center gap-2 rounded-full bg-ui-controls/60 px-3 py-1.5 text-sm font-medium text-white hover:bg-ui-controls/90 transition-colors" + className={BAR_BUTTON_CLASS} > Extract frames @@ -2270,11 +2307,13 @@ function BulkSelectionBar({
{popoverOpen && ( <> @@ -2334,11 +2373,61 @@ function BulkSelectionBar({ )}
+ {/* Add tags */} +
+ + {tagsOpen && ( + <> +
setTagsOpen(false)} + /> +
+
+ Add tags +
+ setTagChips((prev) => [...prev, ...values])} + onRemove={(value) => + setTagChips((prev) => prev.filter((c) => c !== value)) + } + /> + +
+ + )} +
+
{dropdownOpen && ( -
+
{filtered.map((suggestion, index) => (