mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
Bug fix: created characters token (#1813)
- Create flow stored `inference_job_token` as the `character_token`, so mentioning a new character sent a bogus token - backend drops it and name-matches the raw `@name` against stale characters. - Don't add pending characters to the mention store; they aren't generatable yet. - Keep the pending poll running after the modal closes and sync real tokens into the store once the character is active. - Remove unused `addCharacter` store action. - Same fix in all three copies: webapp, website, shared `promptbox` lib.
This commit is contained in:
@@ -89,11 +89,13 @@ export const CharactersModal = ({
|
||||
setPendingCharacters((prev) => prev.filter((p) => p.name !== name));
|
||||
}, []);
|
||||
|
||||
// Poll the server while the modal is open and any creation is pending, so
|
||||
// a creation that finishes while the user is on the create/edit view still
|
||||
// gets cleaned up — preventing the duplicate (real + pending) card.
|
||||
// Poll the server while any creation is pending (even with the modal
|
||||
// closed), so the pending card gets cleaned up and — once the character is
|
||||
// active — the mention store learns its real character token. The create
|
||||
// response only carries an inference job token, so @-mentions must wait for
|
||||
// the server list to include the character before it can be referenced.
|
||||
useEffect(() => {
|
||||
if (!isOpen || pendingCharacters.length === 0) return;
|
||||
if (pendingCharacters.length === 0) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
@@ -112,14 +114,25 @@ export const CharactersModal = ({
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
if (resolved) setRefreshKey((k) => k + 1);
|
||||
if (resolved) {
|
||||
const store = useCharactersStore.getState();
|
||||
store.setCharacters(
|
||||
res.data.map((c) => ({
|
||||
character_token: c.token,
|
||||
name: c.name,
|
||||
avatar_image_url: c.maybe_avatar?.cdn_url,
|
||||
})),
|
||||
);
|
||||
store.setLoaded(true);
|
||||
setRefreshKey((k) => k + 1);
|
||||
}
|
||||
} catch {
|
||||
// retry next tick
|
||||
}
|
||||
}, POLL_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isOpen, pendingCharacters.length]);
|
||||
}, [pendingCharacters.length]);
|
||||
|
||||
// Time out failed creations so the "Creating..." card never sticks forever.
|
||||
useEffect(() => {
|
||||
@@ -610,7 +623,6 @@ const NewCharacterView = ({
|
||||
onBack: () => void;
|
||||
onCreated: (pending: { name: string; previewUrl?: string }) => void;
|
||||
}) => {
|
||||
const addCharacterToStore = useCharactersStore((s) => s.addCharacter);
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [images, setImages] = useState<UploadedImage[]>([]);
|
||||
@@ -753,11 +765,6 @@ const NewCharacterView = ({
|
||||
|
||||
if (res.success && res.data) {
|
||||
toast.success(`Character "${name.trim()}" is being created`);
|
||||
addCharacterToStore({
|
||||
character_token: res.data.inference_job_token,
|
||||
name: name.trim(),
|
||||
avatar_image_url: uploadedImages[0]!.url,
|
||||
});
|
||||
onCreated({
|
||||
name: name.trim(),
|
||||
previewUrl: uploadedImages[0]!.url,
|
||||
|
||||
@@ -10,7 +10,6 @@ interface CharactersStore {
|
||||
characters: StoredCharacter[];
|
||||
loaded: boolean;
|
||||
setCharacters: (characters: StoredCharacter[]) => void;
|
||||
addCharacter: (character: StoredCharacter) => void;
|
||||
updateCharacter: (token: string, updates: Partial<StoredCharacter>) => void;
|
||||
removeCharacter: (token: string) => void;
|
||||
setLoaded: (loaded: boolean) => void;
|
||||
@@ -20,8 +19,6 @@ export const useCharactersStore = create<CharactersStore>()((set) => ({
|
||||
characters: [],
|
||||
loaded: false,
|
||||
setCharacters: (characters) => set({ characters }),
|
||||
addCharacter: (character) =>
|
||||
set((state) => ({ characters: [...state.characters, character] })),
|
||||
updateCharacter: (token, updates) =>
|
||||
set((state) => ({
|
||||
characters: state.characters.map((c) =>
|
||||
|
||||
@@ -89,11 +89,13 @@ export const CharactersModal = ({
|
||||
setPendingCharacters((prev) => prev.filter((p) => p.name !== name));
|
||||
}, []);
|
||||
|
||||
// Poll the server while the modal is open and any creation is pending, so
|
||||
// a creation that finishes while the user is on the create/edit view still
|
||||
// gets cleaned up — preventing the duplicate (real + pending) card.
|
||||
// Poll the server while any creation is pending (even with the modal
|
||||
// closed), so the pending card gets cleaned up and — once the character is
|
||||
// active — the mention store learns its real character token. The create
|
||||
// response only carries an inference job token, so @-mentions must wait for
|
||||
// the server list to include the character before it can be referenced.
|
||||
useEffect(() => {
|
||||
if (!isOpen || pendingCharacters.length === 0) return;
|
||||
if (pendingCharacters.length === 0) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
@@ -112,14 +114,25 @@ export const CharactersModal = ({
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
if (resolved) setRefreshKey((k) => k + 1);
|
||||
if (resolved) {
|
||||
const store = useCharactersStore.getState();
|
||||
store.setCharacters(
|
||||
res.data.map((c) => ({
|
||||
character_token: c.token,
|
||||
name: c.name,
|
||||
avatar_image_url: c.maybe_avatar?.cdn_url,
|
||||
})),
|
||||
);
|
||||
store.setLoaded(true);
|
||||
setRefreshKey((k) => k + 1);
|
||||
}
|
||||
} catch {
|
||||
// retry next tick
|
||||
}
|
||||
}, POLL_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isOpen, pendingCharacters.length]);
|
||||
}, [pendingCharacters.length]);
|
||||
|
||||
// Time out failed creations so the "Creating..." card never sticks forever.
|
||||
useEffect(() => {
|
||||
@@ -610,7 +623,6 @@ const NewCharacterView = ({
|
||||
onBack: () => void;
|
||||
onCreated: (pending: { name: string; previewUrl?: string }) => void;
|
||||
}) => {
|
||||
const addCharacterToStore = useCharactersStore((s) => s.addCharacter);
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [images, setImages] = useState<UploadedImage[]>([]);
|
||||
@@ -753,11 +765,6 @@ const NewCharacterView = ({
|
||||
|
||||
if (res.success && res.data) {
|
||||
toast.success(`Character "${name.trim()}" is being created`);
|
||||
addCharacterToStore({
|
||||
character_token: res.data.inference_job_token,
|
||||
name: name.trim(),
|
||||
avatar_image_url: uploadedImages[0]!.url,
|
||||
});
|
||||
onCreated({
|
||||
name: name.trim(),
|
||||
previewUrl: uploadedImages[0]!.url,
|
||||
|
||||
@@ -10,7 +10,6 @@ interface CharactersStore {
|
||||
characters: StoredCharacter[];
|
||||
loaded: boolean;
|
||||
setCharacters: (characters: StoredCharacter[]) => void;
|
||||
addCharacter: (character: StoredCharacter) => void;
|
||||
updateCharacter: (token: string, updates: Partial<StoredCharacter>) => void;
|
||||
removeCharacter: (token: string) => void;
|
||||
setLoaded: (loaded: boolean) => void;
|
||||
@@ -20,8 +19,6 @@ export const useCharactersStore = create<CharactersStore>()((set) => ({
|
||||
characters: [],
|
||||
loaded: false,
|
||||
setCharacters: (characters) => set({ characters }),
|
||||
addCharacter: (character) =>
|
||||
set((state) => ({ characters: [...state.characters, character] })),
|
||||
updateCharacter: (token, updates) =>
|
||||
set((state) => ({
|
||||
characters: state.characters.map((c) =>
|
||||
|
||||
@@ -99,11 +99,13 @@ export const CharactersModal = ({
|
||||
setPendingCharacters((prev) => prev.filter((p) => p.name !== name));
|
||||
}, []);
|
||||
|
||||
// Poll the server while the modal is open and any creation is pending, so
|
||||
// a creation that finishes while the user is on the create/edit view still
|
||||
// gets cleaned up — preventing the duplicate (real + pending) card.
|
||||
// Poll the server while any creation is pending (even with the modal
|
||||
// closed), so the pending card gets cleaned up and — once the character is
|
||||
// active — the mention store learns its real character token. The create
|
||||
// response only carries an inference job token, so @-mentions must wait for
|
||||
// the server list to include the character before it can be referenced.
|
||||
useEffect(() => {
|
||||
if (!isOpen || pendingCharacters.length === 0) return;
|
||||
if (pendingCharacters.length === 0) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
@@ -122,14 +124,25 @@ export const CharactersModal = ({
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
if (resolved) setRefreshKey((k) => k + 1);
|
||||
if (resolved) {
|
||||
const store = useCharactersStore.getState();
|
||||
store.setCharacters(
|
||||
res.data.map((c) => ({
|
||||
character_token: c.token,
|
||||
name: c.name,
|
||||
avatar_image_url: c.maybe_avatar?.cdn_url,
|
||||
})),
|
||||
);
|
||||
store.setLoaded(true);
|
||||
setRefreshKey((k) => k + 1);
|
||||
}
|
||||
} catch {
|
||||
// retry next tick
|
||||
}
|
||||
}, POLL_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isOpen, pendingCharacters.length]);
|
||||
}, [pendingCharacters.length]);
|
||||
|
||||
// Time out failed creations so the "Creating..." card never sticks forever.
|
||||
useEffect(() => {
|
||||
@@ -604,7 +617,6 @@ const NewCharacterView = ({
|
||||
onBack: () => void;
|
||||
onCreated: (pending: { name: string; previewUrl?: string }) => void;
|
||||
}) => {
|
||||
const addCharacterToStore = useCharactersStore((s) => s.addCharacter);
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [images, setImages] = useState<UploadedImage[]>([]);
|
||||
@@ -754,12 +766,6 @@ const NewCharacterView = ({
|
||||
|
||||
if (res.success && res.data) {
|
||||
toast.success(`Character "${name.trim()}" is being created`);
|
||||
// Add to global store so it appears in @-mentions immediately
|
||||
addCharacterToStore({
|
||||
character_token: res.data.inference_job_token,
|
||||
name: name.trim(),
|
||||
avatar_image_url: uploadedImages[0]!.url,
|
||||
});
|
||||
// Pass pending info up so the list shows an optimistic card
|
||||
onCreated({
|
||||
name: name.trim(),
|
||||
|
||||
@@ -346,7 +346,6 @@ interface CharactersStore {
|
||||
characters: StoredCharacter[];
|
||||
loaded: boolean;
|
||||
setCharacters: (characters: StoredCharacter[]) => void;
|
||||
addCharacter: (character: StoredCharacter) => void;
|
||||
updateCharacter: (token: string, updates: Partial<StoredCharacter>) => void;
|
||||
removeCharacter: (token: string) => void;
|
||||
setLoaded: (loaded: boolean) => void;
|
||||
@@ -356,8 +355,6 @@ export const useCharactersStore = create<CharactersStore>()((set) => ({
|
||||
characters: [],
|
||||
loaded: false,
|
||||
setCharacters: (characters) => set({ characters }),
|
||||
addCharacter: (character) =>
|
||||
set((state) => ({ characters: [...state.characters, character] })),
|
||||
updateCharacter: (token, updates) =>
|
||||
set((state) => ({
|
||||
characters: state.characters.map((c) =>
|
||||
|
||||
Reference in New Issue
Block a user