From 30634e8be9727009e6051d7553ce253ad26c52b8 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 27 Aug 2025 17:17:13 -0400 Subject: [PATCH 1/3] fix the model list, wrong tauri ids, new feature predicate (#798) --- .../src/lib/model-selectors-for-pages.tsx | 11 ++++++----- .../libs/model-list/src/lib/classes/ImageModel.ts | 8 ++++++++ frontend/libs/model-list/src/lib/lists/ImageModels.ts | 7 +++++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/frontend/libs/components/model-selector/src/lib/model-selectors-for-pages.tsx b/frontend/libs/components/model-selector/src/lib/model-selectors-for-pages.tsx index c6a0698c69..5e7d76494e 100644 --- a/frontend/libs/components/model-selector/src/lib/model-selectors-for-pages.tsx +++ b/frontend/libs/components/model-selector/src/lib/model-selectors-for-pages.tsx @@ -48,11 +48,12 @@ export const STAGE_3D_PAGE_MODEL_LIST : ModelList = buildItems2( ); export const IMAGE_EDITOR_PAGE_MODEL_LIST : ModelList = buildItems2( - [ - ALL_MODELS_BY_ID.get("flux_pro_inpaint")!, - ALL_MODELS_BY_ID.get("flux_dev_juggernaut_inpaint")!, - ALL_MODELS_BY_ID.get("flux_pro_kontext_max")!, - ], + //[ + // ALL_MODELS_BY_ID.get("flux_pro_inpaint")!, + // ALL_MODELS_BY_ID.get("flux_dev_juggernaut_inpaint")!, + // ALL_MODELS_BY_ID.get("flux_pro_kontext_max")!, + //], + IMAGE_MODELS.filter((m) => m.canEditImages), ); diff --git a/frontend/libs/model-list/src/lib/classes/ImageModel.ts b/frontend/libs/model-list/src/lib/classes/ImageModel.ts index 0da9528f90..44eb26db19 100644 --- a/frontend/libs/model-list/src/lib/classes/ImageModel.ts +++ b/frontend/libs/model-list/src/lib/classes/ImageModel.ts @@ -4,9 +4,15 @@ import { ModelCategory } from "../legacy/ModelConfig.js"; import { ModelTag } from "./metadata/ModelTag.js"; export class ImageModel extends Model { + // Maximum number of images that can be generated at once readonly maxGenerationCount: number; + + // Default number of images that can be generated at once readonly defaultGenerationCount: number; + // Signals image editing models that focus on editing a single image. + readonly canEditImages: boolean; + // For inpainting models, does it require sending a mask? readonly usesInpaintingMask: boolean; @@ -21,6 +27,7 @@ export class ImageModel extends Model { selectorBadges: string[]; maxGenerationCount: number; defaultGenerationCount: number; + canEditImages?: boolean; usesInpaintingMask?: boolean; tags?: ModelTag[]; }) { @@ -36,6 +43,7 @@ export class ImageModel extends Model { super(args); this.maxGenerationCount = args.maxGenerationCount; this.defaultGenerationCount = args.defaultGenerationCount; + this.canEditImages = args.canEditImages ?? false; this.usesInpaintingMask = args.usesInpaintingMask ?? false; } } diff --git a/frontend/libs/model-list/src/lib/lists/ImageModels.ts b/frontend/libs/model-list/src/lib/lists/ImageModels.ts index 42376b056f..ad48147864 100644 --- a/frontend/libs/model-list/src/lib/lists/ImageModels.ts +++ b/frontend/libs/model-list/src/lib/lists/ImageModels.ts @@ -87,11 +87,12 @@ export const IMAGE_MODELS: ImageModel[] = [ selectorBadges: ["20 sec."], maxGenerationCount: 4, defaultGenerationCount: 4, + canEditImages: true, tags: [ModelTag.InstructiveEdit], }), new ImageModel({ id: "flux_pro_inpaint", - tauriId: "flux_pro_inpaint", + tauriId: "flux_pro_1", fullName: "Flux Pro Inpaint", category: "image", creator: ModelCreator.BlackForestLabs, @@ -100,11 +101,12 @@ export const IMAGE_MODELS: ImageModel[] = [ selectorBadges: ["30 sec."], maxGenerationCount: 1, // NB: Fal only allows one image for some reason! defaultGenerationCount: 1, // NB: Fal only allows one image for some reason! + canEditImages: true, usesInpaintingMask: true, }), new ImageModel({ id: "flux_dev_juggernaut_inpaint", - tauriId: "flux_dev_juggernaut_inpaint", + tauriId: "flux_dev_juggernaut", fullName: "Flux Dev Juggernaut Inpaint", category: "image", creator: ModelCreator.BlackForestLabs, @@ -113,6 +115,7 @@ export const IMAGE_MODELS: ImageModel[] = [ selectorBadges: ["15 sec."], maxGenerationCount: 4, defaultGenerationCount: 4, + canEditImages: true, usesInpaintingMask: true, }), ]; From 7dae90f1e25654c088b2a017bf1036610849ef22 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 27 Aug 2025 17:41:43 -0400 Subject: [PATCH 2/3] start work on consolidating internal errors in http server --- .../web/storyteller_web/src/api_doc_cli.rs | 1 + .../web/storyteller_web/src/api_doc_server.rs | 1 + .../src/error/internal_error.rs | 37 +++++++++++++++++++ .../web/storyteller_web/src/error/mod.rs | 1 + .../avt_cookie_manager.rs | 8 ++-- .../avt_cookie_payload.rs | 17 ++++----- .../service/web/storyteller_web/src/main.rs | 1 + 7 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 crates/service/web/storyteller_web/src/error/internal_error.rs create mode 100644 crates/service/web/storyteller_web/src/error/mod.rs diff --git a/crates/service/web/storyteller_web/src/api_doc_cli.rs b/crates/service/web/storyteller_web/src/api_doc_cli.rs index fc62536c7c..3d6aebd27e 100644 --- a/crates/service/web/storyteller_web/src/api_doc_cli.rs +++ b/crates/service/web/storyteller_web/src/api_doc_cli.rs @@ -27,6 +27,7 @@ use errors::AnyhowResult; pub mod billing; pub mod configs; +pub mod error; pub mod http_server; pub mod state; pub mod threads; diff --git a/crates/service/web/storyteller_web/src/api_doc_server.rs b/crates/service/web/storyteller_web/src/api_doc_server.rs index 1924c42725..de5c546b18 100644 --- a/crates/service/web/storyteller_web/src/api_doc_server.rs +++ b/crates/service/web/storyteller_web/src/api_doc_server.rs @@ -30,6 +30,7 @@ use crate::configs::static_api_tokens::StaticApiTokenSet; pub mod billing; pub mod configs; +pub mod error; pub mod http_server; pub mod state; pub mod threads; diff --git a/crates/service/web/storyteller_web/src/error/internal_error.rs b/crates/service/web/storyteller_web/src/error/internal_error.rs new file mode 100644 index 0000000000..0c479310bd --- /dev/null +++ b/crates/service/web/storyteller_web/src/error/internal_error.rs @@ -0,0 +1,37 @@ +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; + +/// Any generic errors arising internally. +/// An error class that we do not expose via the API. +/// This should be massaged before we send it to the user. +#[derive(Debug)] +pub enum InternalError { + /// Issue with HMAC decoding + /// This is typically when using development cookies against production, + /// or vice versa. + VisitorCookieError(String), + + /// Decode error with AVT cookie missing a required field. + VisitorCookieMissingField(&'static str), + + /// Catch instances of anyhow::Error. + AnyhowError(anyhow::Error), +} + +impl Error for InternalError {} + +impl Display for InternalError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + InternalError::VisitorCookieError(reason) => write!(f, "Visitor cookie error: {:?}", reason), + InternalError::VisitorCookieMissingField(field) => write!(f, "Visitor cookie missing field: {}", field), + InternalError::AnyhowError(err) => write!(f, "Anyhow error: {}", err), + } + } +} + +impl From for InternalError { + fn from(err: anyhow::Error) -> Self { + InternalError::AnyhowError(err) + } +} diff --git a/crates/service/web/storyteller_web/src/error/mod.rs b/crates/service/web/storyteller_web/src/error/mod.rs new file mode 100644 index 0000000000..3e6d672990 --- /dev/null +++ b/crates/service/web/storyteller_web/src/error/mod.rs @@ -0,0 +1 @@ +pub mod internal_error; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/cookies/anonymous_visitor_tracking/avt_cookie_manager.rs b/crates/service/web/storyteller_web/src/http_server/cookies/anonymous_visitor_tracking/avt_cookie_manager.rs index c5a009796e..93b1f906a2 100644 --- a/crates/service/web/storyteller_web/src/http_server/cookies/anonymous_visitor_tracking/avt_cookie_manager.rs +++ b/crates/service/web/storyteller_web/src/http_server/cookies/anonymous_visitor_tracking/avt_cookie_manager.rs @@ -4,12 +4,12 @@ use actix_web::HttpRequest; use anyhow::anyhow; use log::warn; +use crate::error::internal_error::InternalError; +use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_payload::AvtCookiePayload; use cookies::jwt_signer::JwtSigner; use errors::AnyhowResult; use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; -use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_payload::AvtCookiePayload; - const VISITOR_COOKIE_NAME : &str = "visitor"; /// Handle "anonymous visitor tracking" cookies. @@ -70,7 +70,7 @@ impl AvtCookieManager { Ok(payload) } - pub fn decode_cookie_payload_from_request(&self, request: &HttpRequest) -> AnyhowResult> { + pub fn decode_cookie_payload_from_request(&self, request: &HttpRequest) -> Result, InternalError> { let cookie = match request.cookie(VISITOR_COOKIE_NAME) { None => return Ok(None), Some(cookie) => cookie, @@ -79,7 +79,7 @@ impl AvtCookieManager { match self.decode_cookie_payload(&cookie) { Err(e) => { warn!("Visitor cookie decode error: {:?}", e); - Err(anyhow!("Could not decode visitor cookie: {:?}", e)) + Err(InternalError::VisitorCookieError(e.to_string())) }, Ok(payload) => Ok(Some(payload)), } diff --git a/crates/service/web/storyteller_web/src/http_server/cookies/anonymous_visitor_tracking/avt_cookie_payload.rs b/crates/service/web/storyteller_web/src/http_server/cookies/anonymous_visitor_tracking/avt_cookie_payload.rs index 49d19c1f6d..0706d90590 100644 --- a/crates/service/web/storyteller_web/src/http_server/cookies/anonymous_visitor_tracking/avt_cookie_payload.rs +++ b/crates/service/web/storyteller_web/src/http_server/cookies/anonymous_visitor_tracking/avt_cookie_payload.rs @@ -1,9 +1,7 @@ use std::collections::BTreeMap; use std::str::FromStr; -use anyhow::anyhow; - -use errors::AnyhowResult; +use crate::error::internal_error::InternalError; use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; /** @@ -38,18 +36,19 @@ impl AvtCookiePayload { } } - pub fn from_map(map: BTreeMap) -> AnyhowResult { + pub fn from_map(map: BTreeMap) -> Result { let avt_token = map.get("avt_token") - .ok_or(anyhow!("avt_token not present"))? - .to_string(); + .ok_or_else(|| InternalError::VisitorCookieMissingField("avt_token"))?; let cookie_version = map .get("cookie_version") - .ok_or(anyhow!("avt_token not present"))?; + .ok_or_else(|| InternalError::VisitorCookieMissingField("cookie_version"))?; - let avt_token = AnonymousVisitorTrackingToken::new_from_str(&avt_token); + let avt_token = AnonymousVisitorTrackingToken::new_from_str(avt_token); - let cookie_version = u32::from_str(cookie_version)?; + let cookie_version = u32::from_str(cookie_version) + .map_err(|e| InternalError::VisitorCookieError( + format!("invalid integer for cookie_version: {:?}, version: {}", e, cookie_version)))?; Ok(AvtCookiePayload { avt_token, diff --git a/crates/service/web/storyteller_web/src/main.rs b/crates/service/web/storyteller_web/src/main.rs index bc12ed599d..73a9a72dc6 100644 --- a/crates/service/web/storyteller_web/src/main.rs +++ b/crates/service/web/storyteller_web/src/main.rs @@ -92,6 +92,7 @@ use crate::util::troll_user_bans::troll_user_ban_list::TrollUserBanList; pub mod billing; pub mod configs; +pub mod error; pub mod http_server; pub mod state; pub mod threads; From c39651415df0d2aff055f7ab7926f289ff332b99 Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Wed, 27 Aug 2025 14:43:07 -0700 Subject: [PATCH 3/3] Fix lossy History Stack (#796) --- .../app/src/pages/PageEdit/HistoryStack.tsx | 43 +++++++------------ .../app/src/pages/PageEdit/PageEdit.tsx | 34 +++++++-------- .../src/pages/PageEdit/stores/EditState.ts | 19 +++++++- 3 files changed, 49 insertions(+), 47 deletions(-) diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx index f0bd918e88..38a7a14c2e 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx @@ -6,7 +6,7 @@ import { faTrashXmark, faXmark, } from "@fortawesome/pro-solid-svg-icons"; -import { useRef, useState } from "react"; +import { Fragment, useRef, useState } from "react"; import { twMerge } from "tailwind-merge"; import { BaseSelectorImage } from "./BaseImageSelector"; import { Tooltip } from "@storyteller/ui-tooltip"; @@ -23,21 +23,18 @@ export interface ImageBundle { interface HistoryStackProps { onClear: () => void; onImageSelect?: (image: BaseSelectorImage) => void; - startingBundles: ImageBundle[]; + imageBundles: ImageBundle[]; + onNewImageBundle?: (newBundle: ImageBundle) => void; + selectedImageToken?: string; } export const HistoryStack = ({ onClear, - onImageSelect = () => {}, - startingBundles = [], + onImageSelect = () => { }, + imageBundles, + onNewImageBundle = () => { }, + selectedImageToken }: HistoryStackProps) => { - const [imageBundles, setImageBundles] = - useState(startingBundles); - const [selectedImageToken, setSelectedImageToken] = useState( - startingBundles.length > 0 && startingBundles[0].images.length > 0 - ? startingBundles[0].images[0].mediaToken - : null, - ); useImageEditCompleteEvent(async (event) => { const newBundle: ImageBundle = { images: event.edited_images.map( @@ -49,12 +46,7 @@ export const HistoryStack = ({ ), }; - setImageBundles([...imageBundles, newBundle]); - const newlySelected = newBundle.images[0]; - if (newlySelected) { - setSelectedImageToken(newlySelected.mediaToken); - onImageSelect(newlySelected); - } + onNewImageBundle(newBundle); }); // This is used to force image reloads in different sessions @@ -62,26 +54,23 @@ export const HistoryStack = ({ const sessionRandBuster = useRef(Math.random()); const handleClear = () => { - setImageBundles(startingBundles); - setSelectedImageToken(null); onClear(); }; return (
-
+
{imageBundles.map((bundle, index) => ( - <> - {bundle.images.map((image, imgIndex) => ( + + {bundle.images.map((image) => ( ))} {index < imageBundles.length - 1 && ( -
+
)} - +
))} { //useStateSceneLoader(); - const selectedImageModel : ImageModel | undefined = getSelectedImageModel(PAGE_ID); + const selectedImageModel: ImageModel | undefined = getSelectedImageModel(PAGE_ID); // State for canvas dimensions const canvasWidth = useRef(1024); @@ -46,6 +46,8 @@ const PageEdit = () => { // Use the Zustand store const store = useEditStore(); + const addHistoryImageBundle = useEditStore((state) => state.addHistoryImageBundle); + const historyImageBundles = useEditStore((state) => state.historyImageBundles) // Pass store actions directly as callbacks useDeleteHotkeys({ onDelete: store.deleteSelectedItems }); @@ -154,18 +156,6 @@ const PageEdit = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Auto-fit the canvas when base image is loaded - useEffect(() => { - if (!store.baseImageBitmap) { - console.log("No base image bitmap available, skipping auto-fit"); - return; - } - - // When baseImageBitmap changes from null to an actual image, call onFitPressed - // Use a slight delay to ensure the image dimensions are properly loaded - onFitPressed(); - }, [onFitPressed, store.baseImageBitmap]); - // Create a function to use the left layer ref and download the bitmap from it const getMaskArrayBuffer = async (): Promise => { if ( @@ -247,11 +237,14 @@ const PageEdit = () => { //const supportsMaskedInpainting = modelConfig?.tags?.includes(ModelTag.MaskedInpainting) ?? false; const supportsMaskedInpainting = selectedImageModel?.usesInpaintingMask ?? false; - if (!supportsMaskedInpainting && (store.activeTool !== "select" || store.lineNodes.length > 0)) { - // TODO: Implement a new mode for unsupported masking and hide the nodes layer instead of clearing - store.setActiveTool("select"); - store.clearLineNodes(); - } + useEffect(() => { + if (!supportsMaskedInpainting && (store.activeTool !== "select" || store.lineNodes.length > 0)) { + // TODO: Implement a new mode for unsupported masking and hide the nodes layer instead of clearing + store.setActiveTool("select"); + store.clearLineNodes(); + } + }, [store, supportsMaskedInpainting]); + /* * @@ -273,6 +266,7 @@ const PageEdit = () => { > { + addHistoryImageBundle({ images: [image] }); store.setBaseImageInfo(image); }} showLoading={ @@ -305,11 +299,13 @@ const PageEdit = () => { onClear={() => { store.RESET(); }} - startingBundles={[{ images: [store.baseImageInfo] } as ImageBundle]} + imageBundles={historyImageBundles} onImageSelect={(baseImage) => { store.clearLineNodes(); store.setBaseImageInfo(baseImage); }} + onNewImageBundle={addHistoryImageBundle} + selectedImageToken={store.baseImageInfo?.mediaToken} />
void; // clearHistory: () => void; RESET: () => void; + + // For the history stack + historyImageBundles: ImageBundle[]; + clearHistoryImages: () => void; + addHistoryImageBundle: (bundle: ImageBundle) => void; } // Add a flag to prevent saving state during restoration @@ -172,6 +178,9 @@ export const useEditStore = create((set, get, store) => ({ baseImageInfo: null, baseImageBitmap: null, + // History stack state + historyImageBundles: [], + // Actions addNode: (node: Node) => { set((state) => { @@ -1009,5 +1018,13 @@ export const useEditStore = create((set, get, store) => ({ RESET: () => { set(store.getInitialState()); - } + }, + + clearHistoryImages: () => { + set({ historyImageBundles: [] }); + }, + + addHistoryImageBundle(bundle) { + set((state) => ({ historyImageBundles: [...state.historyImageBundles, bundle] })); + }, }));