diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs index 09056a554f..ac1d09dd23 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs @@ -31,6 +31,7 @@ use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs}; use tauri::{AppHandle, State}; +use enums::tauri::ux::tauri_command_caller::TauriCommandCaller; #[derive(Deserialize)] pub struct EnqueueTextToImageRequest { @@ -45,6 +46,21 @@ pub struct EnqueueTextToImageRequest { /// The number of images to generate. pub number_images: Option, + + /// OPTIONAL. + /// Name of the frontend caller. + /// We'll use this to selectively trigger events. + pub frontend_caller: Option, + + /// OPTIONAL. + /// A frontend-defined identifier that we'll send back to the frontend + /// as a Tauri event on task completion. + pub frontend_subscriber_id: Option, + + /// OPTIONAL. + /// A frontend-defined payload that we'll send back to the frontend + /// as a Tauri event on task completion. + pub frontend_subscriber_payload: Option, } // TODO(bt,2025-07-14): Support other aspect ratios / resolutions - @@ -186,7 +202,7 @@ pub async fn handle_request( ) -> Result { let result = dispatch_request( - request, + &request, &app, &app_data_root, &provider_priority_store, @@ -205,7 +221,12 @@ pub async fn handle_request( }; let result = success_event - .insert_into_task_database(task_database) + .insert_into_task_database_with_frontend_payload( + task_database, + request.frontend_caller, + request.frontend_subscriber_id.as_deref(), + request.frontend_subscriber_payload.as_deref(), + ) .await; if let Err(err) = result { @@ -217,7 +238,7 @@ pub async fn handle_request( } pub async fn dispatch_request( - request: EnqueueTextToImageRequest, + request: &EnqueueTextToImageRequest, app: &AppHandle, app_data_root: &AppDataRoot, provider_priority_store: &ProviderPriorityStore, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/generic/handle_image_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/generic/handle_image_artcraft.rs index 6052dcb71b..821859168b 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/generic/handle_image_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/generic/handle_image_artcraft.rs @@ -25,7 +25,7 @@ use storyteller_client::generate::image::generate_flux_pro_11_ultra_text_to_imag use tauri::AppHandle; pub async fn handle_image_artcraft( - request: EnqueueTextToImageRequest, + request: &EnqueueTextToImageRequest, app: &AppHandle, app_env_configs: &AppEnvConfigs, app_data_root: &AppDataRoot, @@ -71,7 +71,7 @@ pub async fn handle_image_artcraft( selected_model = GenerationModel::Flux1Dev; let request = GenerateFlux1DevTextToImageRequest { uuid_idempotency_token, - prompt: request.prompt, + prompt: request.prompt.clone(), aspect_ratio: request.aspect_ratio .map(|aspect| match aspect { // TODO(bt,2025-07-14): Support other aspect ratios. @@ -110,7 +110,7 @@ pub async fn handle_image_artcraft( selected_model = GenerationModel::Flux1Schnell; let request = GenerateFlux1SchnellTextToImageRequest { uuid_idempotency_token, - prompt: request.prompt, + prompt: request.prompt.clone(), aspect_ratio: request.aspect_ratio .map(|aspect| match aspect { // TODO(bt,2025-07-14): Support other aspect ratios. @@ -149,7 +149,7 @@ pub async fn handle_image_artcraft( selected_model = GenerationModel::FluxPro11; let request = GenerateFluxPro11TextToImageRequest { uuid_idempotency_token, - prompt: request.prompt, + prompt: request.prompt.clone(), aspect_ratio: request.aspect_ratio .map(|aspect| match aspect { // TODO(bt,2025-07-14): Support other aspect ratios. @@ -188,7 +188,7 @@ pub async fn handle_image_artcraft( selected_model = GenerationModel::FluxPro11Ultra; let request = GenerateFluxPro11UltraTextToImageRequest { uuid_idempotency_token, - prompt: request.prompt, + prompt: request.prompt.clone(), aspect_ratio: request.aspect_ratio .map(|aspect| match aspect { // TODO(bt,2025-07-14): Support other aspect ratios. diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/generic/handle_image_fal.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/generic/handle_image_fal.rs index 8ca27f1b76..197e9ae52d 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/generic/handle_image_fal.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/generic/handle_image_fal.rs @@ -17,7 +17,7 @@ use tauri::AppHandle; pub async fn handle_image_fal( app: &AppHandle, - request: EnqueueTextToImageRequest, + request: &EnqueueTextToImageRequest, fal_creds_manager: &FalCredentialManager, fal_task_queue: &FalTaskQueue, ) -> Result { diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1.rs index 6e08d84863..aca7626d7f 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1.rs @@ -27,7 +27,7 @@ use std::time::Duration; use tauri::AppHandle; pub async fn handle_gpt_image_1( - request: EnqueueTextToImageRequest, + request: &EnqueueTextToImageRequest, app: &AppHandle, app_data_root: &AppDataRoot, app_env_configs: &AppEnvConfigs, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1_artcraft.rs index 482e0348d3..852779fc71 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1_artcraft.rs @@ -28,7 +28,7 @@ use storyteller_client::generate::image::generate_gpt_image_1_text_to_image::gen use tauri::AppHandle; pub async fn handle_gpt_image_1_artcraft( - request: EnqueueTextToImageRequest, + request: &EnqueueTextToImageRequest, app_env_configs: &AppEnvConfigs, storyteller_creds_manager: &StorytellerCredentialManager, ) -> Result { diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1_sora.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1_sora.rs index 149a737ef0..7d4625570f 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1_sora.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/gpt_image_1/handle_gpt_image_1_sora.rs @@ -22,7 +22,7 @@ use tauri::AppHandle; const SORA_SIMPLE_IMAGE_GEN_TIMEOUT : Duration = Duration::from_millis(1000 * 30); // 30 seconds pub async fn handle_gpt_image_1_sora( - request: EnqueueTextToImageRequest, + request: &EnqueueTextToImageRequest, app: &AppHandle, sora_creds_manager: &SoraCredentialManager, sora_task_queue: &SoraTaskQueue, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/midjourney/handle_midjourney.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/midjourney/handle_midjourney.rs index 045db64954..3d9c2f86f5 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/midjourney/handle_midjourney.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/midjourney/handle_midjourney.rs @@ -39,7 +39,7 @@ use tokens::tokens::media_files::MediaFileToken; pub async fn handle_midjourney( app: &AppHandle, - request: EnqueueTextToImageRequest, + request: &EnqueueTextToImageRequest, app_env_configs: &AppEnvConfigs, mj_creds_manager: &MidjourneyCredentialManager, ) -> Result { diff --git a/crates/desktop/artcraft/src/core/events/functional_events/mod.rs b/crates/desktop/artcraft/src/core/events/functional_events/mod.rs index 8d14d823ad..723590a391 100644 --- a/crates/desktop/artcraft/src/core/events/functional_events/mod.rs +++ b/crates/desktop/artcraft/src/core/events/functional_events/mod.rs @@ -1,4 +1,5 @@ pub mod canvas_background_removal_complete_event; pub mod image_edit_complete_event; pub mod refresh_account_state_event; -pub mod show_provider_login_modal_event; \ No newline at end of file +pub mod show_provider_login_modal_event; +pub mod text_to_image_generation_complete_event; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/events/functional_events/text_to_image_generation_complete_event.rs b/crates/desktop/artcraft/src/core/events/functional_events/text_to_image_generation_complete_event.rs new file mode 100644 index 0000000000..31736a94bd --- /dev/null +++ b/crates/desktop/artcraft/src/core/events/functional_events/text_to_image_generation_complete_event.rs @@ -0,0 +1,26 @@ +use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent}; +use enums::tauri::ux::tauri_event_name::TauriEventName; +use serde_derive::Serialize; +use tokens::tokens::media_files::MediaFileToken; +use url::Url; + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct TextToImageGenerationCompleteEvent { + pub generated_images: Vec, + pub maybe_frontend_subscriber_id: Option, + pub maybe_frontend_subscriber_payload: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct GeneratedImage { + pub media_token: MediaFileToken, + pub cdn_url: Url, + pub maybe_thumbnail_template: Option, +} + +impl BasicSendableEvent for TextToImageGenerationCompleteEvent { + const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::TextToImageGenerationCompleteEvent; + const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success; +} diff --git a/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_inpainting_complete_event.rs b/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_inpainting_complete_event.rs index 085002fecc..d769555dba 100644 --- a/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_inpainting_complete_event.rs +++ b/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_inpainting_complete_event.rs @@ -37,7 +37,7 @@ pub async fn maybe_handle_inpainting_complete_event( let job_result = match job.maybe_result { Some(ref res) => res, None => { - error!("Job result is None for task: {:?}", task); + warn!("Job result is None for task: {:?}", task); return Ok(()); // No result, nothing to do }, }; diff --git a/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_text_to_image_complete_event.rs b/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_text_to_image_complete_event.rs new file mode 100644 index 0000000000..56952610bb --- /dev/null +++ b/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_text_to_image_complete_event.rs @@ -0,0 +1,123 @@ +use crate::core::events::basic_sendable_event_trait::BasicSendableEvent; +use crate::core::events::functional_events::text_to_image_generation_complete_event::{GeneratedImage, TextToImageGenerationCompleteEvent}; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; +use anyhow::anyhow; +use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsItem, ListSessionResultDetailsResponse}; +use enums::tauri::tasks::task_type::TaskType; +use enums::tauri::ux::tauri_command_caller::TauriCommandCaller; +use errors::AnyhowResult; +use log::{error, warn}; +use sqlite_tasks::queries::list_tasks_by_provider_and_tokens::{list_tasks_by_provider_and_tokens, ListTasksArgs, Task}; +use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet; +use storyteller_client::media_files::list_batch_generated_redux_media_files::list_batch_generated_redux_media_files; +use tauri::AppHandle; +use tokens::tokens::batch_generations::BatchGenerationToken; +use tokens::tokens::media_files::MediaFileToken; + +pub async fn maybe_handle_text_to_image_complete_event( + app: &AppHandle, + app_env_configs: &AppEnvConfigs, + maybe_creds: Option<&StorytellerCredentialSet>, + task: &Task, + job: &ListSessionJobsItem, +) -> AnyhowResult<()> { + + match task.task_type { + TaskType::ImageGeneration => {} // NB: Fall-through + _ => return Ok(()), + } + + match task.frontend_caller { + Some(TauriCommandCaller::TextToImage) => {} // NB: Fall-through + _ => return Ok(()), + } + + let job_result = match job.maybe_result { + Some(ref res) => res, + None => { + warn!("Job result is None for task: {:?}", task); + return Ok(()); // No result, nothing to do + }, + }; + + let event = match job_result.maybe_batch_token.as_ref() { + Some(batch_token) => { + handle_batch( + app, + app_env_configs, + maybe_creds, + task, + job, + job_result, + batch_token + ).await? + } + None => { + handle_single( + app, + task, + job, + job_result, + ).await? + } + }; + + if let Err(err) = event.send(&app) { + error!("Failed to send TextToImageGenerationCompleteEvent: {:?}", err); // Fail open + } + + Ok(()) +} + +pub async fn handle_single( + app: &AppHandle, + task: &Task, + job: &ListSessionJobsItem, + job_result: &ListSessionResultDetailsResponse, +) -> AnyhowResult { + Ok(TextToImageGenerationCompleteEvent { + generated_images: vec![GeneratedImage { + media_token: MediaFileToken::new_from_str(&job_result.entity_token), + cdn_url: job_result.media_links.cdn_url.clone(), + maybe_thumbnail_template: job_result.media_links.maybe_thumbnail_template.clone(), + }], + maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(), + maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(), + }) +} + +pub async fn handle_batch( + app: &AppHandle, + app_env_configs: &AppEnvConfigs, + maybe_creds: Option<&StorytellerCredentialSet>, + task: &Task, + job: &ListSessionJobsItem, + job_result: &ListSessionResultDetailsResponse, + batch_token: &BatchGenerationToken, +) -> AnyhowResult { + + let result = list_batch_generated_redux_media_files( + &app_env_configs.storyteller_host, + maybe_creds, + batch_token, + ).await?; + + if result.media_files.is_empty() { + return Err(anyhow!("No media files found for batch token: {}", batch_token)); + } + + let media_files = result.media_files + .into_iter() + .map(|file| GeneratedImage { + media_token: file.token, + cdn_url: file.media_links.cdn_url, + maybe_thumbnail_template: file.media_links.maybe_thumbnail_template, + }) + .collect(); + + Ok(TextToImageGenerationCompleteEvent { + generated_images: media_files, + maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(), + maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(), + }) +} diff --git a/crates/desktop/artcraft/src/services/storyteller/threads/events/mod.rs b/crates/desktop/artcraft/src/services/storyteller/threads/events/mod.rs index c0c376ea6d..ca60c55645 100644 --- a/crates/desktop/artcraft/src/services/storyteller/threads/events/mod.rs +++ b/crates/desktop/artcraft/src/services/storyteller/threads/events/mod.rs @@ -1,2 +1,3 @@ pub mod maybe_handle_inpainting_complete_event; +pub mod maybe_handle_text_to_image_complete_event; pub mod maybe_send_background_removal_complete_event; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_task_polling_thread.rs b/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_task_polling_thread.rs index c1a44fd07b..0b07586255 100644 --- a/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_task_polling_thread.rs +++ b/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_task_polling_thread.rs @@ -12,8 +12,11 @@ use crate::core::utils::enum_conversion::task_type::to_generation_action; use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; use crate::services::sora::state::sora_task_queue::SoraTaskQueue; use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use crate::services::storyteller::threads::events::maybe_handle_inpainting_complete_event::maybe_handle_inpainting_complete_event; +use crate::services::storyteller::threads::events::maybe_handle_text_to_image_complete_event::maybe_handle_text_to_image_complete_event; use crate::services::storyteller::threads::events::maybe_send_background_removal_complete_event::maybe_send_background_removal_complete_event; use anyhow::anyhow; +use artcraft_api_defs::jobs::list_session_jobs::ListSessionJobsItem; use enums::common::generation_provider::GenerationProvider; use enums::common::job_status_plus::JobStatusPlus; use enums::tauri::tasks::task_status::TaskStatus; @@ -27,15 +30,13 @@ use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::path::PathBuf; +use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet; use storyteller_client::error::api_error::ApiError; use storyteller_client::error::storyteller_error::StorytellerError; use storyteller_client::jobs::list_session_jobs::{list_session_jobs, States}; use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file; use tauri::AppHandle; use tempdir::TempDir; -use artcraft_api_defs::jobs::list_session_jobs::ListSessionJobsItem; -use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet; -use crate::services::storyteller::threads::events::maybe_handle_inpainting_complete_event::maybe_handle_inpainting_complete_event; pub async fn storyteller_task_polling_thread( app_handle: AppHandle, @@ -177,6 +178,18 @@ async fn send_additional_events( ) { info!("Attempting to dispatch events for completed Storyteller : {:?}", task); + let result = maybe_handle_text_to_image_complete_event( + app_handle, + app_env_configs, + creds, + task, + job, + ).await; + + if let Err(err) = result { + error!("Failed to send text-to-image complete event: {:?}", err); + } + let result = maybe_handle_inpainting_complete_event( app_handle, app_env_configs, diff --git a/crates/schema/public/enums/src/tauri/ux/tauri_command_caller.rs b/crates/schema/public/enums/src/tauri/ux/tauri_command_caller.rs index bc1296b491..7545f9e6b4 100644 --- a/crates/schema/public/enums/src/tauri/ux/tauri_command_caller.rs +++ b/crates/schema/public/enums/src/tauri/ux/tauri_command_caller.rs @@ -16,6 +16,10 @@ pub enum TauriCommandCaller { Canvas, /// The inpainting editor ImageEditor, + /// The text-to-image page + TextToImage, + /// The image-to-video page + ImageToVideo, } impl_enum_display_and_debug_using_to_str!(TauriCommandCaller); @@ -29,6 +33,8 @@ impl TauriCommandCaller { match self { Self::Canvas => "canvas", Self::ImageEditor => "image_editor", + Self::TextToImage => "text_to_image", + Self::ImageToVideo => "image_to_video", } } @@ -36,6 +42,8 @@ impl TauriCommandCaller { match job_status { "canvas" => Ok(Self::Canvas), "image_editor" => Ok(Self::ImageEditor), + "text_to_image" => Ok(Self::TextToImage), + "image_to_video" => Ok(Self::ImageToVideo), _ => Err(format!("invalid tauri_command_caller: {:?}", job_status)), } } @@ -46,6 +54,8 @@ impl TauriCommandCaller { BTreeSet::from([ Self::Canvas, Self::ImageEditor, + Self::TextToImage, + Self::ImageToVideo, ]) } } @@ -62,26 +72,34 @@ mod tests { fn test_serialization() { assert_serialization(TauriCommandCaller::Canvas, "canvas"); assert_serialization(TauriCommandCaller::ImageEditor, "image_editor"); + assert_serialization(TauriCommandCaller::TextToImage, "text_to_image"); + assert_serialization(TauriCommandCaller::ImageToVideo, "image_to_video"); } #[test] fn to_str() { assert_eq!(TauriCommandCaller::Canvas.to_str(), "canvas"); assert_eq!(TauriCommandCaller::ImageEditor.to_str(), "image_editor"); + assert_eq!(TauriCommandCaller::TextToImage.to_str(), "text_to_image"); + assert_eq!(TauriCommandCaller::ImageToVideo.to_str(), "image_to_video"); } #[test] fn from_str() { assert_eq!(TauriCommandCaller::from_str("canvas").unwrap(), TauriCommandCaller::Canvas); assert_eq!(TauriCommandCaller::from_str("image_editor").unwrap(), TauriCommandCaller::ImageEditor); + assert_eq!(TauriCommandCaller::from_str("text_to_image").unwrap(), TauriCommandCaller::TextToImage); + assert_eq!(TauriCommandCaller::from_str("image_to_video").unwrap(), TauriCommandCaller::ImageToVideo); } #[test] fn all_variants() { let mut variants = TauriCommandCaller::all_variants(); - assert_eq!(variants.len(), 2); + assert_eq!(variants.len(), 4); assert_eq!(variants.pop_first(), Some(TauriCommandCaller::Canvas)); assert_eq!(variants.pop_first(), Some(TauriCommandCaller::ImageEditor)); + assert_eq!(variants.pop_first(), Some(TauriCommandCaller::TextToImage)); + assert_eq!(variants.pop_first(), Some(TauriCommandCaller::ImageToVideo)); assert_eq!(variants.pop_first(), None); } } diff --git a/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs b/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs index c3a4c73131..dc2877b90d 100644 --- a/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs +++ b/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs @@ -43,6 +43,11 @@ pub enum TauriEventName { #[serde(rename = "canvas_bg_removed_event")] CanvasBgRemovedEvent, + /// Special event: + /// Image edit is complete + #[serde(rename = "text_to_image_generation_complete_event")] + TextToImageGenerationCompleteEvent, + /// Special event: /// Image edit is complete #[serde(rename = "image_edit_complete_event")] @@ -78,6 +83,7 @@ impl TauriEventName { Self::GenerationCompleteEvent => "generation-complete-event", Self::GenerationFailedEvent => "generation-failed-event", Self::CanvasBgRemovedEvent => "canvas_bg_removed_event", + Self::TextToImageGenerationCompleteEvent => "text_to_image_generation_complete_event", Self::ImageEditCompleteEvent => "image_edit_complete_event", Self::RefreshAccountStateEvent => "refresh_account_state_event", Self::ShowProviderLoginModalEvent => "show_provider_login_modal_event", @@ -92,6 +98,7 @@ impl TauriEventName { "generation-complete-event" => Ok(Self::GenerationCompleteEvent), "generation-failed-event" => Ok(Self::GenerationFailedEvent), "canvas_bg_removed_event" => Ok(Self::CanvasBgRemovedEvent), + "text_to_image_generation_complete_event" => Ok(Self::TextToImageGenerationCompleteEvent), "image_edit_complete_event" => Ok(Self::ImageEditCompleteEvent), "refresh_account_state_event" => Ok(Self::RefreshAccountStateEvent), "show_provider_login_modal_event" => Ok(Self::ShowProviderLoginModalEvent), @@ -109,6 +116,7 @@ impl TauriEventName { Self::GenerationCompleteEvent, Self::GenerationFailedEvent, Self::CanvasBgRemovedEvent, + Self::TextToImageGenerationCompleteEvent, Self::ImageEditCompleteEvent, Self::RefreshAccountStateEvent, Self::ShowProviderLoginModalEvent, @@ -132,6 +140,7 @@ mod tests { assert_serialization(TauriEventName::GenerationCompleteEvent, "generation-complete-event"); assert_serialization(TauriEventName::GenerationFailedEvent, "generation-failed-event"); assert_serialization(TauriEventName::CanvasBgRemovedEvent, "canvas_bg_removed_event"); + assert_serialization(TauriEventName::TextToImageGenerationCompleteEvent, "text_to_image_generation_complete_event"); assert_serialization(TauriEventName::ImageEditCompleteEvent, "image_edit_complete_event"); assert_serialization(TauriEventName::RefreshAccountStateEvent, "refresh_account_state_event"); assert_serialization(TauriEventName::ShowProviderLoginModalEvent, "show_provider_login_modal_event"); @@ -145,6 +154,7 @@ mod tests { assert_eq!(TauriEventName::GenerationCompleteEvent.to_str(), "generation-complete-event"); assert_eq!(TauriEventName::GenerationFailedEvent.to_str(), "generation-failed-event"); assert_eq!(TauriEventName::CanvasBgRemovedEvent.to_str(), "canvas_bg_removed_event"); + assert_eq!(TauriEventName::TextToImageGenerationCompleteEvent.to_str(), "text_to_image_generation_complete_event"); assert_eq!(TauriEventName::ImageEditCompleteEvent.to_str(), "image_edit_complete_event"); assert_eq!(TauriEventName::RefreshAccountStateEvent.to_str(), "refresh_account_state_event"); assert_eq!(TauriEventName::ShowProviderLoginModalEvent.to_str(), "show_provider_login_modal_event"); @@ -158,6 +168,7 @@ mod tests { assert_eq!(TauriEventName::from_str("generation-complete-event").unwrap(), TauriEventName::GenerationCompleteEvent); assert_eq!(TauriEventName::from_str("generation-failed-event").unwrap(), TauriEventName::GenerationFailedEvent); assert_eq!(TauriEventName::from_str("canvas_bg_removed_event").unwrap(), TauriEventName::CanvasBgRemovedEvent); + assert_eq!(TauriEventName::from_str("text_to_image_generation_complete_event").unwrap(), TauriEventName::TextToImageGenerationCompleteEvent); assert_eq!(TauriEventName::from_str("image_edit_complete_event").unwrap(), TauriEventName::ImageEditCompleteEvent); assert_eq!(TauriEventName::from_str("refresh_account_state_event").unwrap(), TauriEventName::RefreshAccountStateEvent); assert_eq!(TauriEventName::from_str("show_provider_login_modal_event").unwrap(), TauriEventName::ShowProviderLoginModalEvent); @@ -167,12 +178,13 @@ mod tests { #[test] fn all_variants() { let mut variants = TauriEventName::all_variants(); - assert_eq!(variants.len(), 9); + assert_eq!(variants.len(), 10); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueSuccessEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueFailureEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationCompleteEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationFailedEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::CanvasBgRemovedEvent)); + assert_eq!(variants.pop_first(), Some(TauriEventName::TextToImageGenerationCompleteEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::ImageEditCompleteEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::RefreshAccountStateEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::ShowProviderLoginModalEvent)); diff --git a/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx b/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx index d29de8745a..b04fd95ef1 100644 --- a/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx @@ -14,6 +14,7 @@ interface TextToImageProps { imageUrl?: string; } import { ModelInfo } from "@storyteller/model-list"; +import { useTextToImageGenerationCompleteEvent } from "@storyteller/tauri-events"; const PAGE_ID : ModelPage = ModelPage.TextToImage; @@ -36,6 +37,20 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => { clearJobTokens: () => {}, }; + useTextToImageGenerationCompleteEvent(async (event) => { + console.log("Text to image generation complete:", event); + + // TODO: Logic to show edit history + for (const generatedImage of event.generated_images) { + console.log("Generated image:", generatedImage.media_token); + console.log("Generated image url:", generatedImage.cdn_url); + console.log( + "Generated image thumbnail template:", + generatedImage.maybe_thumbnail_template, + ); + } + }); + return (
Promise) => { + useEffect(() => { + let isUnmounted = false; + let unlisten: Promise; + + const setup = async () => { + unlisten = listen>(EVENT_NAME, async (wrappedEvent) => { + await asyncCallback(wrappedEvent.payload.data); + }); + + if (isUnmounted) { + unlisten.then(f => f()); // Unsubscribe if unmounted early. + } + }; + + setup(); + + return () => { + isUnmounted = true; + unlisten.then(f => f()); + }; + + }, []); +} diff --git a/frontend/libs/tauri-events/src/lib/events/FlashUserInputErrorEvent.ts b/frontend/libs/tauri-events/src/lib/events/warning/FlashUserInputErrorEvent.ts similarity index 92% rename from frontend/libs/tauri-events/src/lib/events/FlashUserInputErrorEvent.ts rename to frontend/libs/tauri-events/src/lib/events/warning/FlashUserInputErrorEvent.ts index 90e4161c6b..ac083bb2c8 100644 --- a/frontend/libs/tauri-events/src/lib/events/FlashUserInputErrorEvent.ts +++ b/frontend/libs/tauri-events/src/lib/events/warning/FlashUserInputErrorEvent.ts @@ -1,5 +1,5 @@ import { listen, UnlistenFn } from '@tauri-apps/api/event'; -import { BasicEventWrapper } from '../common/BasicEventWrapper'; +import { BasicEventWrapper } from '../../common/BasicEventWrapper'; import { useEffect } from 'react'; const EVENT_NAME : string = 'flash_user_input_error_event'; diff --git a/frontend/libs/tauri-events/src/lib/tauri-events.module.css b/frontend/libs/tauri-events/src/lib/tauri-events.module.css deleted file mode 100644 index d0246c74b9..0000000000 --- a/frontend/libs/tauri-events/src/lib/tauri-events.module.css +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Replace this with your own classes - * - * e.g. - * .container { - * } -*/ \ No newline at end of file diff --git a/frontend/libs/tauri-events/src/lib/tauri-events.spec.tsx b/frontend/libs/tauri-events/src/lib/tauri-events.spec.tsx deleted file mode 100644 index d108baff9c..0000000000 --- a/frontend/libs/tauri-events/src/lib/tauri-events.spec.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { render } from '@testing-library/react'; - -import FrontendTauriEvents from './tauri-events'; - -describe('FrontendTauriEvents', () => { - - it('should render successfully', () => { - const { baseElement } = render(); - expect(baseElement).toBeTruthy(); - }); - -}); diff --git a/frontend/libs/tauri-events/src/lib/tauri-events.tsx b/frontend/libs/tauri-events/src/lib/tauri-events.tsx deleted file mode 100644 index 437284f6c0..0000000000 --- a/frontend/libs/tauri-events/src/lib/tauri-events.tsx +++ /dev/null @@ -1,15 +0,0 @@ - -import styles from './tauri-events.module.css'; - -export function FrontendTauriEvents() { - return ( -
-

Welcome to FrontendTauriEvents!

-
- ); -} - -export default FrontendTauriEvents; - - -