From c644e378fb0791aa4956151d4a9ec668c06a6e34 Mon Sep 17 00:00:00 2001 From: BFlat Date: Wed, 1 Apr 2026 19:56:27 +0700 Subject: [PATCH] characters update to use real api --- frontend/libs/api/src/lib/CharactersApi.ts | 128 +++++++++--------- frontend/libs/api/src/lib/models/Character.ts | 21 ++- .../promptbox/src/lib/CharactersModal.tsx | 105 +++++++------- .../promptbox/src/lib/PromptBoxVideo.tsx | 14 +- .../src/lib/enqueue/EnqueueImageToVideo.ts | 8 ++ 5 files changed, 143 insertions(+), 133 deletions(-) diff --git a/frontend/libs/api/src/lib/CharactersApi.ts b/frontend/libs/api/src/lib/CharactersApi.ts index d8550ef530..93beac69eb 100644 --- a/frontend/libs/api/src/lib/CharactersApi.ts +++ b/frontend/libs/api/src/lib/CharactersApi.ts @@ -1,38 +1,38 @@ import { ApiManager, ApiResponse } from "./ApiManager.js"; import { Character } from "./models/Character.js"; -import { PaginationInfinite } from "./models/Pagination.js"; export interface CreateCharacterRequest { - name: string; - description?: string; - image_media_file_token: string; + image_media_token: string; + model: string; + uuid_idempotency_token: string; } -export interface UpdateCharacterRequest { - name?: string; - description?: string; - image_media_file_token?: string; +export interface EditCharacterRequest { + token: string; + updated_name?: string | null; + updated_description?: string | null; } export interface ListCharactersRequest { - cursor?: string; - cursorIsReversed?: boolean; - pageSize?: number; + cursor?: number; } export class CharactersApi extends ApiManager { public CreateCharacter( params: CreateCharacterRequest, - ): Promise> { - const endpoint = `${this.getApiSchemeAndHost()}/v1/characters/create`; + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/character/create`; - return this.post({ + return this.post< + CreateCharacterRequest, + { success: boolean; inference_job_token: string } + >({ endpoint, body: params, }) - .then(({ success, ...character }) => ({ + .then(({ success, inference_job_token }) => ({ success, - data: character, + data: { inference_job_token }, })) .catch((err) => ({ success: false, @@ -40,24 +40,21 @@ export class CharactersApi extends ApiManager { })); } - public ListCharacters({ - ...params - }: ListCharactersRequest): Promise< - ApiResponse - > { - const endpoint = `${this.getApiSchemeAndHost()}/v1/characters/list`; + public EditCharacter( + params: EditCharacterRequest, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/character/edit`; - const query = this.parseQueryValues(params); - - return this.get<{ - success: boolean; - results: Character[]; - pagination: PaginationInfinite; - }>({ endpoint, query }) - .then((response) => ({ - success: true, - data: response.results, - pagination: response.pagination, + return this.post< + EditCharacterRequest, + { success: boolean; BadInput?: string } + >({ + endpoint, + body: params, + }) + .then(({ success, BadInput }) => ({ + success: success ?? false, + errorMessage: BadInput, })) .catch((err) => ({ success: false, @@ -70,10 +67,10 @@ export class CharactersApi extends ApiManager { }: { characterToken: string; }): Promise> { - const endpoint = `${this.getApiSchemeAndHost()}/v1/characters/character/${characterToken}`; + const endpoint = `${this.getApiSchemeAndHost()}/v1/character/${characterToken}`; - return this.get<{ success: boolean } & Character>({ endpoint }) - .then(({ success, ...character }) => ({ + return this.get<{ success: boolean; character: Character }>({ endpoint }) + .then(({ success, character }) => ({ success, data: character, })) @@ -83,46 +80,43 @@ export class CharactersApi extends ApiManager { })); } - public UpdateCharacter({ - characterToken, - ...params - }: UpdateCharacterRequest & { - characterToken: string; - }): Promise> { - const endpoint = `${this.getApiSchemeAndHost()}/v1/characters/character/${characterToken}`; - - const body = this.parseBodyValues< - UpdateCharacterRequest, - Record - >(params); - - return this.post, { success?: boolean; BadInput?: string }>({ - endpoint, - body, - }) - .then(({ success, BadInput }) => ({ - success: success ?? false, - errorMessage: BadInput, - })) - .catch((err) => ({ - success: false, - errorMessage: err.message, - })); - } - public DeleteCharacter({ characterToken, }: { characterToken: string; }): Promise> { - const endpoint = `${this.getApiSchemeAndHost()}/v1/characters/character/${characterToken}`; + const endpoint = `${this.getApiSchemeAndHost()}/v1/character/${characterToken}`; - return this.delete({ + return this.delete({ endpoint, }) - .then(({ success, BadInput }) => ({ + .then(({ success }) => ({ success: success ?? false, - errorMessage: BadInput, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } + + public ListCharacters( + params?: ListCharactersRequest, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/characters/session`; + + const query = params?.cursor + ? { cursor: params.cursor.toString() } + : undefined; + + return this.get<{ + success: boolean; + characters: Character[]; + next_cursor?: number | null; + }>({ endpoint, query }) + .then((response) => ({ + success: response.success, + data: response.characters, + pagination: { next_cursor: response.next_cursor }, })) .catch((err) => ({ success: false, diff --git a/frontend/libs/api/src/lib/models/Character.ts b/frontend/libs/api/src/lib/models/Character.ts index 33a4e934f8..cedae7da5a 100644 --- a/frontend/libs/api/src/lib/models/Character.ts +++ b/frontend/libs/api/src/lib/models/Character.ts @@ -1,10 +1,17 @@ +export interface MediaFileLinks { + cdn_url: string; + maybe_thumbnail_template?: string | null; + maybe_video_previews?: { + cdn_url: string; + maybe_thumbnail_template?: string | null; + } | null; +} + export interface Character { - character_token: string; + token: string; name: string; - description?: string; - avatar_image_url?: string; - full_image_url?: string; - image_media_file_token?: string; - created_at: string; - updated_at: string; + maybe_description?: string | null; + maybe_avatar?: MediaFileLinks | null; + maybe_full_image?: MediaFileLinks | null; + models: string[]; } diff --git a/frontend/libs/components/promptbox/src/lib/CharactersModal.tsx b/frontend/libs/components/promptbox/src/lib/CharactersModal.tsx index 9f12933b02..0f1b6928b7 100644 --- a/frontend/libs/components/promptbox/src/lib/CharactersModal.tsx +++ b/frontend/libs/components/promptbox/src/lib/CharactersModal.tsx @@ -12,9 +12,13 @@ import { faXmark, } from "@fortawesome/pro-solid-svg-icons"; import { twMerge } from "tailwind-merge"; -import { CharactersApi, Character, downloadFileFromUrl } from "@storyteller/api"; +import { + CharactersApi, + Character, + MediaUploadApi, + downloadFileFromUrl, +} from "@storyteller/api"; import { toast } from "@storyteller/ui-toaster"; -import { MediaUploadApi } from "@storyteller/api"; import { v4 as uuidv4 } from "uuid"; import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal"; import { useCharactersStore } from "./promptStore"; @@ -50,20 +54,22 @@ export const CharactersModal = ({ isOpen={isOpen} onClose={handleClose} title={view === "list" ? "Characters" : undefined} - width={520} - className="max-h-[80vh]" + width={680} + className="max-h-[80vh] flex flex-col overflow-hidden" > - {view === "list" ? ( - setView("create")} - onSelectCharacter={onSelectCharacter} - /> - ) : ( - setView("list")} - onCreated={() => setView("list")} - /> - )} +
+ {view === "list" ? ( + setView("create")} + onSelectCharacter={onSelectCharacter} + /> + ) : ( + setView("list")} + onCreated={() => setView("list")} + /> + )} +
); }; @@ -82,22 +88,20 @@ const CharacterListView = ({ const [characters, setCharacters] = useState([]); const [loading, setLoading] = useState(true); const [hasMore, setHasMore] = useState(false); - const [cursor, setCursor] = useState(undefined); - const scrollContainerRef = useRef(null); + const [cursor, setCursor] = useState(undefined); const sentinelRef = useRef(null); const loadingMoreRef = useRef(false); const storeSetCharacters = useCharactersStore((s) => s.setCharacters); const storeSetLoaded = useCharactersStore((s) => s.setLoaded); const fetchCharacters = useCallback( - async (nextCursor?: string) => { + async (nextCursor?: number) => { if (loadingMoreRef.current) return; loadingMoreRef.current = true; try { const api = new CharactersApi(); const res = await api.ListCharacters({ - pageSize: 20, cursor: nextCursor, }); @@ -107,16 +111,16 @@ const CharacterListView = ({ // Sync to global store for @-mention system storeSetCharacters( updated.map((c) => ({ - character_token: c.character_token, + character_token: c.token, name: c.name, - avatar_image_url: c.avatar_image_url, + avatar_image_url: c.maybe_avatar?.cdn_url, })), ); storeSetLoaded(true); return updated; }); - const nextPage = res.pagination?.maybe_next; - setCursor(nextPage); + const nextPage = res.pagination?.next_cursor; + setCursor(nextPage ?? undefined); setHasMore(!!nextPage); } } catch { @@ -143,27 +147,14 @@ const CharacterListView = ({ fetchCharacters(cursor); } }, - { root: scrollContainerRef.current, threshold: 0.1 }, + { threshold: 0.1 }, ); observer.observe(sentinelRef.current); return () => observer.disconnect(); }, [hasMore, cursor, fetchCharacters]); return ( -
-
- {/* Create New button */} - - +
{loading && characters.length === 0 ? (

Loading characters...

- ) : characters.length === 0 ? ( -
- -

No characters yet

-
) : ( -
+
+ {/* Create New card */} + + {characters.map((character) => (
); }; @@ -377,16 +370,16 @@ const NewCharacterView = ({ try { const api = new CharactersApi(); const res = await api.CreateCharacter({ - name: name.trim(), - description: description.trim() || undefined, - image_media_file_token: uploadedImages[0]!.mediaToken!, + image_media_token: uploadedImages[0]!.mediaToken!, + model: "seedance_2p0", + uuid_idempotency_token: uuidv4(), }); - if (res.success) { + if (res.success && res.data) { toast.success(`Character "${name.trim()}" created!`); // Add to global store so it appears in @-mentions immediately addCharacterToStore({ - character_token: res.data?.character_token ?? "", + character_token: res.data.inference_job_token, name: name.trim(), avatar_image_url: uploadedImages[0]!.url, }); diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx index e274d96129..0fd05d4814 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx @@ -153,14 +153,14 @@ export const PromptBoxVideo = ({ if (charactersLoaded) return; const api = new CharactersApi(); api - .ListCharacters({ pageSize: 100 }) + .ListCharacters() .then((res) => { if (res.success && res.data) { storeSetCharacters( res.data.map((c) => ({ - character_token: c.character_token, + character_token: c.token, name: c.name, - avatar_image_url: c.avatar_image_url, + avatar_image_url: c.maybe_avatar?.cdn_url, })), ); } @@ -690,6 +690,14 @@ export const PromptBoxVideo = ({ ); } + // Extract character token from @-mention in prompt + const mentionedChar = storedCharacters.find((c) => + prompt.includes(`@${c.name}`), + ); + if (mentionedChar) { + request.character_token = mentionedChar.character_token; + } + // Pass duration if model supports it if (selectedModel.durationOptions && duration !== null) { request.duration_seconds = duration; diff --git a/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageToVideo.ts b/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageToVideo.ts index c12a032dbe..32e3758e67 100644 --- a/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageToVideo.ts +++ b/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageToVideo.ts @@ -60,6 +60,9 @@ export interface EnqueueImageToVideoRequest { // Optional. Reference audio media tokens (for audio reference). reference_audio_media_tokens?: string[]; + + // Optional. Character token for character consistency. + character_token?: string; } interface RawEnqueueImageToVideoRequest { @@ -78,6 +81,7 @@ interface RawEnqueueImageToVideoRequest { reference_image_media_tokens?: string[]; reference_video_media_tokens?: string[]; reference_audio_media_tokens?: string[]; + character_token?: string; } export interface EnqueueImageToVideoError extends CommandResult { @@ -168,6 +172,10 @@ export const EnqueueImageToVideo = async ( request.reference_audio_media_tokens; } + if (request.character_token) { + mutableRequest.character_token = request.character_token; + } + const result = await invoke("enqueue_image_to_video_command", { request: mutableRequest, });