From cbb707238e71b7148586ae97bd01318897d15189 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 3 Apr 2026 14:38:34 +0700 Subject: [PATCH] characters edit, delete and token field update --- frontend/libs/api/src/lib/CharactersApi.ts | 2 + frontend/libs/api/src/lib/models/Character.ts | 1 + .../promptbox/src/lib/CharactersModal.tsx | 285 ++++++++++++++++-- .../promptbox/src/lib/PromptBoxVideo.tsx | 2 +- .../promptbox/src/lib/promptStore.ts | 12 + .../src/lib/enqueue/EnqueueImageToVideo.ts | 10 +- 6 files changed, 278 insertions(+), 34 deletions(-) diff --git a/frontend/libs/api/src/lib/CharactersApi.ts b/frontend/libs/api/src/lib/CharactersApi.ts index 93beac69eb..c02cd00001 100644 --- a/frontend/libs/api/src/lib/CharactersApi.ts +++ b/frontend/libs/api/src/lib/CharactersApi.ts @@ -5,6 +5,8 @@ export interface CreateCharacterRequest { image_media_token: string; model: string; uuid_idempotency_token: string; + character_name: string; + character_description?: string | null; } export interface EditCharacterRequest { diff --git a/frontend/libs/api/src/lib/models/Character.ts b/frontend/libs/api/src/lib/models/Character.ts index cedae7da5a..7a66b4f055 100644 --- a/frontend/libs/api/src/lib/models/Character.ts +++ b/frontend/libs/api/src/lib/models/Character.ts @@ -14,4 +14,5 @@ export interface Character { maybe_avatar?: MediaFileLinks | null; maybe_full_image?: MediaFileLinks | null; models: string[]; + is_user_created?: boolean; } diff --git a/frontend/libs/components/promptbox/src/lib/CharactersModal.tsx b/frontend/libs/components/promptbox/src/lib/CharactersModal.tsx index 82473aae92..112b1ade55 100644 --- a/frontend/libs/components/promptbox/src/lib/CharactersModal.tsx +++ b/frontend/libs/components/promptbox/src/lib/CharactersModal.tsx @@ -9,6 +9,8 @@ import { faSpinnerThird, faImages, faXmark, + faPen, + faTrashAlt, } from "@fortawesome/pro-solid-svg-icons"; import { twMerge } from "tailwind-merge"; import { @@ -31,7 +33,7 @@ interface CharactersModalProps { onSelectCharacter?: (character: Character) => void; } -type ModalView = "list" | "create"; +type ModalView = "list" | "create" | "edit"; interface UploadedImage { file: File; @@ -45,12 +47,28 @@ export const CharactersModal = ({ onSelectCharacter, }: CharactersModalProps) => { const [view, setView] = useState("list"); + const [editingCharacter, setEditingCharacter] = useState( + null, + ); + const [refreshKey, setRefreshKey] = useState(0); const handleClose = () => { setView("list"); + setEditingCharacter(null); onClose(); }; + const handleEdit = (character: Character) => { + setEditingCharacter(character); + setView("edit"); + }; + + const handleEditDone = () => { + setEditingCharacter(null); + setView("list"); + setRefreshKey((k) => k + 1); + }; + return ( {view === "list" ? ( setView("create")} onSelectCharacter={onSelectCharacter} + onEditCharacter={handleEdit} /> - ) : ( + ) : view === "create" ? ( setView("list")} - onCreated={() => setView("list")} + onCreated={() => { + setView("list"); + setRefreshKey((k) => k + 1); + }} /> - )} + ) : editingCharacter ? ( + { + setEditingCharacter(null); + setView("list"); + }} + onSaved={handleEditDone} + /> + ) : null} ); @@ -82,18 +114,22 @@ export const CharactersModal = ({ const CharacterListView = ({ onCreateClick, onSelectCharacter, + onEditCharacter, }: { onCreateClick: () => void; onSelectCharacter?: (character: Character) => void; + onEditCharacter: (character: Character) => void; }) => { const [characters, setCharacters] = useState([]); const [loading, setLoading] = useState(true); const [hasMore, setHasMore] = useState(false); const [cursor, setCursor] = useState(undefined); + const [deletingToken, setDeletingToken] = useState(null); const sentinelRef = useRef(null); const loadingMoreRef = useRef(false); const storeSetCharacters = useCharactersStore((s) => s.setCharacters); const storeSetLoaded = useCharactersStore((s) => s.setLoaded); + const storeRemoveCharacter = useCharactersStore((s) => s.removeCharacter); const fetchCharacters = useCallback( async (nextCursor?: number) => { @@ -154,6 +190,28 @@ const CharacterListView = ({ return () => observer.disconnect(); }, [hasMore, cursor, fetchCharacters]); + const handleDelete = async (character: Character) => { + setDeletingToken(character.token); + try { + const api = new CharactersApi(); + const res = await api.DeleteCharacter({ + characterToken: character.token, + }); + + if (res.success) { + setCharacters((prev) => prev.filter((c) => c.token !== character.token)); + storeRemoveCharacter(character.token); + toast.success(`Character "${character.name}" deleted`); + } else { + toast.error(res.errorMessage || "Failed to delete character"); + } + } catch { + toast.error("Failed to delete character"); + } finally { + setDeletingToken(null); + } + }; + return (
{loading && characters.length === 0 ? ( @@ -198,33 +256,71 @@ const CharacterListView = ({
- {characters.map((character) => ( - + + {/* Edit / Delete overlay buttons (user-created only) */} + {isUserCreated && ( +
+ +
)} -
-

- {character.name} -

-
- - ))} + ); + })} )} @@ -241,6 +337,135 @@ const CharacterListView = ({ ); }; +// --------------------------------------------------------------------------- +// Edit Character View +// --------------------------------------------------------------------------- + +const EditCharacterView = ({ + character, + onBack, + onSaved, +}: { + character: Character; + onBack: () => void; + onSaved: () => void; +}) => { + const updateCharacterInStore = useCharactersStore((s) => s.updateCharacter); + const [name, setName] = useState(character.name); + const [description, setDescription] = useState( + character.maybe_description ?? "", + ); + const [saving, setSaving] = useState(false); + + const hasChanges = + name.trim() !== character.name || + (description.trim() || "") !== (character.maybe_description ?? ""); + + const handleSave = async () => { + if (!name.trim()) { + toast.error("Name cannot be empty"); + return; + } + + setSaving(true); + try { + const api = new CharactersApi(); + const res = await api.EditCharacter({ + token: character.token, + updated_name: + name.trim() !== character.name ? name.trim() : undefined, + updated_description: + (description.trim() || "") !== (character.maybe_description ?? "") + ? description.trim() || null + : undefined, + }); + + if (res.success) { + toast.success("Character updated"); + updateCharacterInStore(character.token, { name: name.trim() }); + onSaved(); + } else { + toast.error(res.errorMessage || "Failed to update character"); + } + } catch { + toast.error("Failed to update character"); + } finally { + setSaving(false); + } + }; + + return ( +
+ {/* Header */} +
+ +

Edit Character

+
+ + {/* Avatar preview */} + {character.maybe_avatar?.cdn_url && ( +
+ {character.name} +
+ )} + + {/* Name input */} +
+ + setName(e.target.value)} + placeholder="Character name" + autoComplete="off" + inputClassName="bg-base-fg/[0.07] hover:border-ui-panel-border" + /> +
+ + {/* Description input */} +
+ +