Compare commits

...
Author SHA1 Message Date
Tanish Manku a13dc6acfd Do only one render pass 2025-07-02 13:43:33 -07:00
Brandon Thomas 760639d071 video model plumbing (#693) 2025-06-25 14:59:11 -04:00
Brandon Thomas 4e68169faf cookie debugging 2025-06-24 22:28:48 -04:00
Brandon Thomas 8090cdb526 call api endpoints from frontend (#692) 2025-06-24 15:41:02 -04:00
bflatastic ada4c77093 modal closable with esc key (for floating panels that's draggable and background interactable) 2025-06-24 08:22:00 +07:00
bflatastic 7bb14e1b17 topbar minor padding fix 2025-06-24 08:11:25 +07:00
bflatastic f9c8343165 library asset downloaded toast 2025-06-24 08:06:30 +07:00
Tanish Manku 53eb46d2b6 Add 500ms of timeout on tab switching (#691) 2025-06-23 13:03:57 -07:00
bflatastic f5a3ff67b3 small updates to router priority pane 2025-06-23 22:04:55 +07:00
BFlat 6b262d9fcb model router priority drag and drop reordering and login fixes (#690)
Model router priority tab in settings:
- drag and drop to update
- function calls on order change

Login refactor:
- fixed login (opens modal on log out)
- fixes bug where login modal opens even before login state is checked
(annoying)
- uses zustand states now
2025-06-21 02:22:32 +07:00
72 changed files with 1229 additions and 661 deletions
@@ -7,6 +7,7 @@ use crate::core::commands::response::failure_response_wrapper::{CommandErrorResp
use crate::core::commands::response::shorthand::Response;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::model::image_models::ImageModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
@@ -58,6 +59,10 @@ use storyteller_client::media_files::upload_image_media_file_from_file::upload_i
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
#[derive(Deserialize)]
pub struct EnqueueTextToImageRequest {
@@ -65,28 +70,9 @@ pub struct EnqueueTextToImageRequest {
pub prompt: Option<String>,
/// The model to use.
pub model: Option<EnqueueTextToImageModel>,
pub model: Option<ImageModel>,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EnqueueTextToImageModel {
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
Flux1Schnell,
#[deprecated(note="use `flux_pro_11_ultra` instead")]
#[serde(rename = "flux_pro_ultra")]
FluxProUltra,
#[serde(rename = "flux_pro_11")]
FluxPro11,
#[serde(rename = "flux_pro_11_ultra")]
FluxPro11Ultra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "recraft_3")]
Recraft3,
}
#[derive(Serialize)]
pub struct EnqueueTextToImageSuccessResponse {
@@ -166,7 +152,17 @@ pub async fn enqueue_text_to_image_command(
error_details: None,
})
}
Ok(()) => {
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateImage,
service: event.service_provider,
model: Some(event.tauri_event_model()),
};
if let Err(err) = event.send(&app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
Ok(EnqueueTextToImageSuccessResponse {}.into())
}
}
@@ -183,15 +179,18 @@ pub async fn handle_request(
fal_task_queue: &FalTaskQueue,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> Result<(), InternalImageError> {
) -> Result<SuccessEvent, InternalImageError> {
match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
}
Some(EnqueueTextToImageModel::GptImage1) => {
Some(ImageModel::GptImage1) => {
handle_image_sora(&app, request, sora_creds_manager, sora_task_queue).await?;
return Ok(());
return Ok(SuccessEvent {
service_provider: GenerationServiceProvider::Sora,
model: ImageModel::GptImage1,
});
}
_ => {
// Fall-through
@@ -199,10 +198,8 @@ pub async fn handle_request(
};
if fal_creds_manager.has_apparent_api_token()? {
handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await?;
handle_image_fal(&app, request, fal_creds_manager, fal_task_queue).await
} else {
handle_image_artcraft(request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await?;
handle_image_artcraft(request, &app, app_env_configs, app_data_root, storyteller_creds_manager).await
}
Ok(())
}
@@ -1,9 +1,11 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationModel, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::model::image_models::ImageModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::utils::download_media_file_to_temp_dir::download_media_file_to_temp_dir;
@@ -41,7 +43,7 @@ pub async fn handle_image_artcraft(
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(), InternalImageError> {
) -> Result<SuccessEvent, InternalImageError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
@@ -62,21 +64,21 @@ pub async fn handle_image_artcraft(
let uuid_idempotency_token = generate_random_uuid();
let mut selected_model = None;
let mut selected_model;
let job_token = match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
}
Some(EnqueueTextToImageModel::GptImage1) => {
Some(ImageModel::GptImage1) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: artcraft is handling sora images")));
}
Some(EnqueueTextToImageModel::Recraft3) => {
Some(ImageModel::Recraft3) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented in Artcraft")));
}
Some(EnqueueTextToImageModel::Flux1Dev) => {
Some(ImageModel::Flux1Dev) => {
info!("enqueue Flux 1 Dev");
selected_model = Some(GenerationModel::Flux1Dev);
selected_model = ImageModel::Flux1Dev;
let request = GenerateFlux1DevTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
@@ -99,9 +101,9 @@ pub async fn handle_image_artcraft(
}
}
}
Some(EnqueueTextToImageModel::Flux1Schnell) => {
Some(ImageModel::Flux1Schnell) => {
info!("enqueue Flux 1 Schnell");
selected_model = Some(GenerationModel::Flux1Schnell);
selected_model = ImageModel::Flux1Schnell;
let request = GenerateFlux1SchnellTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
@@ -124,9 +126,9 @@ pub async fn handle_image_artcraft(
}
}
}
Some(EnqueueTextToImageModel::FluxPro11) => {
Some(ImageModel::FluxPro11) => {
info!("enqueue Flux Pro 1.1");
selected_model = Some(GenerationModel::FluxPro11);
selected_model = ImageModel::FluxPro11;
let request = GenerateFluxPro11TextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
@@ -149,12 +151,9 @@ pub async fn handle_image_artcraft(
}
}
}
Some(
EnqueueTextToImageModel::FluxProUltra |
EnqueueTextToImageModel::FluxPro11Ultra
) => {
Some(ImageModel::FluxPro11Ultra) => {
info!("enqueue Flux Pro 1.1 Ultra");
selected_model = Some(GenerationModel::FluxPro11Ultra);
selected_model = ImageModel::FluxPro11Ultra;
let request = GenerateFluxPro11UltraTextToImageRequest {
uuid_idempotency_token,
prompt: request.prompt,
@@ -179,5 +178,8 @@ pub async fn handle_image_artcraft(
}
};
Ok(())
Ok(SuccessEvent {
service_provider: GenerationServiceProvider::Artcraft,
model:selected_model,
})
}
@@ -1,9 +1,11 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::EnqueueTextToImageRequest;
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::commands::enqueue::image::success_event::SuccessEvent;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::model::image_models::ImageModel;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
@@ -22,7 +24,7 @@ pub async fn handle_image_fal(
request: EnqueueTextToImageRequest,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<(), InternalImageError> {
) -> Result<SuccessEvent, InternalImageError> {
let api_key = match fal_creds_manager.get_key()? {
Some(key) => key,
@@ -47,31 +49,32 @@ pub async fn handle_image_fal(
let prompt = request.prompt.as_deref().unwrap_or("");
let model;
let result = match request.model {
None => {
return Err(InternalImageError::NoModelSpecified);
}
Some(EnqueueTextToImageModel::GptImage1) => {
Some(ImageModel::GptImage1) => {
return Err(InternalImageError::AnyhowError(anyhow!("wrong logic: fal is handling sora images")));
}
Some(
EnqueueTextToImageModel::Flux1Dev |
EnqueueTextToImageModel::Flux1Schnell |
EnqueueTextToImageModel::FluxPro11
ImageModel::Flux1Dev |
ImageModel::Flux1Schnell |
ImageModel::FluxPro11
) => {
return Err(InternalImageError::AnyhowError(anyhow!("not yet implemented: {:?}", request.model)));
}
Some(
EnqueueTextToImageModel::FluxProUltra |
EnqueueTextToImageModel::FluxPro11Ultra
) => {
Some(ImageModel::FluxPro11Ultra) => {
model = ImageModel::FluxPro11Ultra;
info!("enqueue Flux Pro 1.1 Ultra text-to-image with prompt: {}", prompt);
enqueue_flux_pro_11_ultra_text_to_image(FluxPro11UltraTextToImageArgs {
prompt,
api_key: &api_key,
}).await
}
Some(EnqueueTextToImageModel::Recraft3) => {
Some(ImageModel::Recraft3) => {
model = ImageModel::Recraft3;
info!("enqueue Recraft v3 text-to-image with prompt: {}", prompt);
enqueue_recraft3_text_to_image(Recraft3TextToImageArgs {
prompt,
@@ -84,15 +87,15 @@ pub async fn handle_image_fal(
Ok(enqueued) => {
info!("Successfully enqueued text to image");
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Fal,
model: None,
};
//let event = GenerationEnqueueSuccessEvent {
// action: GenerationAction::GenerateImage,
// service: GenerationServiceProvider::Fal,
// model: None,
//};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
//if let Err(err) = event.send(app) {
// error!("Failed to emit event: {:?}", err); // Fail open.
//}
if let Err(err) = fal_task_queue.insert(&enqueued) {
error!("Failed to enqueue task: {:?}", err);
@@ -117,5 +120,8 @@ pub async fn handle_image_fal(
}
}
Ok(())
Ok(SuccessEvent {
service_provider: GenerationServiceProvider::Fal,
model,
})
}
@@ -1,4 +1,4 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageRequest};
use crate::core::commands::enqueue::image::internal_image_error::InternalImageError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
@@ -90,15 +90,5 @@ pub async fn handle_image_sora(
sora_task_queue.insert(&response.task_id)?;
let event = GenerationEnqueueSuccessEvent {
action: GenerationAction::GenerateImage,
service: GenerationServiceProvider::Sora,
model: None,
};
if let Err(err) = event.send(app) {
error!("Failed to emit event: {:?}", err); // Fail open.
}
Ok(())
}
@@ -2,4 +2,5 @@ pub mod enqueue_text_to_image_command;
pub mod handle_image_artcraft;
pub mod handle_image_fal;
pub mod handle_image_sora;
pub mod internal_image_error;
pub mod internal_image_error;
mod success_event;
@@ -0,0 +1,20 @@
use crate::core::events::generation_events::common::{GenerationModel, GenerationServiceProvider};
use crate::core::model::image_models::ImageModel;
pub struct SuccessEvent {
pub service_provider: GenerationServiceProvider,
pub model: ImageModel,
}
impl SuccessEvent {
pub fn tauri_event_model(&self) -> GenerationModel {
match self.model {
ImageModel::Flux1Dev => GenerationModel::Flux1Dev,
ImageModel::Flux1Schnell => GenerationModel::Flux1Schnell,
ImageModel::FluxPro11 => GenerationModel::FluxPro11,
ImageModel::FluxPro11Ultra => GenerationModel::FluxPro11Ultra,
ImageModel::GptImage1 => GenerationModel::GptImage1,
ImageModel::Recraft3 => GenerationModel::Recraft3,
}
}
}
@@ -1,4 +1,3 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -15,6 +14,7 @@ use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::queue::enqueue_hunyuan2_image_to_3d::{enqueue_hunyuan2_image_to_3d, Hunyuan2Args};
use fal_client::requests::queue::image_gen::enqueue_flux_pro_11_ultra_text_to_image::{enqueue_flux_pro_11_ultra_text_to_image, FluxPro11UltraTextToImageArgs};
@@ -22,10 +22,9 @@ use fal_client::requests::queue::image_gen::enqueue_recraft3_text_to_image::{enq
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use storyteller_client::generate::object::generate_hunyuan_3d_2_0_image_to_3d::generate_hunyuan3d_2_0_image_to_3d;
use storyteller_client::generate::object::generate_hunyuan_3d_2_1_image_to_3d::generate_hunyuan3d_2_1_image_to_3d;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, State};
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest;
use storyteller_client::generate::object::generate_hunyuan_3d_2_1_image_to_3d::generate_hunyuan3d_2_1_image_to_3d;
pub async fn handle_object_artcraft(
request: EnqueueImageTo3dObjectRequest,
@@ -1,4 +1,3 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::core::commands::enqueue::object::internal_object_error::InternalObjectError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -1,5 +1,3 @@
use crate::core::commands::enqueue::object::handle_object_artcraft::handle_object_artcraft;
use crate::core::commands::enqueue::object::handle_object_fal::handle_object_fal;
use crate::core::commands::enqueue::video::handle_video_artcraft::handle_video_artcraft;
use crate::core::commands::enqueue::video::handle_video_fal::handle_video_fal;
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
@@ -1,4 +1,3 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::{EnqueueImageTo3dObjectModel, EnqueueImageTo3dObjectRequest};
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
@@ -132,7 +131,6 @@ pub async fn handle_video_artcraft(
}
}
Some(VideoModel::Kling21Pro) => {
info!("enqueue Kling 2.1 Pro with Artcraft API");
selected_model = Some(GenerationModel::Kling21Pro);
let request = GenerateKling21ProImageToVideoRequest {
@@ -1,4 +1,3 @@
use crate::core::commands::enqueue::image::enqueue_text_to_image_command::{EnqueueTextToImageModel, EnqueueTextToImageRequest};
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::video::internal_video_error::InternalVideoError;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
@@ -19,6 +19,8 @@ pub enum GenerationModel {
FluxPro11,
#[serde(rename = "flux_pro_1.1_ultra")]
FluxPro11Ultra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "kling_1.6")]
Kling1_6,
#[serde(rename = "kling_2.0")]
@@ -27,6 +29,9 @@ pub enum GenerationModel {
Kling21Master,
#[serde(rename = "kling_2.1_pro")]
Kling21Pro,
#[serde(rename = "recraft_3")]
Recraft3,
#[serde(rename = "sora")]
Sora,
#[serde(rename = "seedance_1.0_lite")]
Seedance10Lite,
@@ -0,0 +1,20 @@
use serde_derive::Deserialize;
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum ImageModel {
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
Flux1Schnell,
#[serde(rename = "flux_pro_11")]
FluxPro11,
#[serde(rename = "flux_pro_11_ultra")]
FluxPro11Ultra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "recraft_3")]
Recraft3,
}
@@ -2,12 +2,14 @@ use crate::core::state::app_startup_time::AppStartupTime;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::main_window_position::MainWindowPosition;
use crate::core::state::main_window_size::MainWindowSize;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::core::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
use crate::core::threads::main_window_thread::persist_window_position_task::persist_window_position_task;
use crate::core::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
use crate::core::utils::clear_all_webview_cookies::clear_all_webview_cookies;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info};
use log::{error, info, warn};
use memory_store::clone_slot::CloneSlot;
use tauri::{AppHandle, Manager, Webview, Window};
@@ -31,6 +33,8 @@ pub async fn main_window_thread(
.unwrap_or_else(|| "unknown".to_string()));
info!("build timestamp: {:?}", build_metadata::build_timestamp()
.to_string());
// debug_try_clear_all_webview_data(&app);
loop {
for (window_name, window) in app.windows() {
@@ -75,3 +79,15 @@ pub fn log_errors<T>(result: AnyhowResult<T>) {
error!("Error persisting window size: {:?}", err);
}
}
fn debug_try_clear_all_webview_data(app: &AppHandle) {
warn!("[!!!] THIS IS ONLY FOR DEBUGGING PURPOSES: Attempting to clear webview data...");
for (name, webview) in app.webviews() {
if let Err(err) = webview.clear_all_browsing_data() {
error!("Failed to clear cookies for '{}' window webview: {:?}", name, err);
} else {
warn!("Successfully cleared cookies for '{}' window webview", name);
}
}
}
+4 -2
View File
@@ -53,8 +53,10 @@ pub fn run() {
println!("Loading app preferences...");
let app_preferences = load_app_preferences_or_default(&app_data_root);
println!("Attempting to read existing artcraft credentials...");
let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
// NB: tauri-plugin-http stores the credentials on disk, so we can defer to that for now.
// println!("Attempting to read existing artcraft credentials...");
// let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
let storyteller_creds_manager = StorytellerCredentialManager::initialize_empty(&app_data_root);
let storyteller_creds_manager_2 = storyteller_creds_manager.clone();
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
@@ -20,6 +20,13 @@ pub struct StorytellerCredentialManager {
impl StorytellerCredentialManager {
pub fn initialize_empty(app_data_root: &AppDataRoot) -> Self {
Self {
holder: StorytellerCredentialHolder::new(),
app_data_root: app_data_root.clone(),
}
}
pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self {
let holder = StorytellerCredentialHolder::new();
@@ -21,7 +21,10 @@ pub enum ApiError {
TooManyRequests(String),
/// 500. An internal server error occurred.
InternalServerError(String),
InternalServerError {
body: String,
backend_hostname: Option<String>,
},
/// A deserialization error with the response.
DeserializationError(serde_json::Error),
@@ -53,7 +56,8 @@ impl Display for ApiError {
ApiError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
ApiError::TooManyRequests(msg) => write!(f, "Too many requests: {}", msg),
ApiError::InternalServerError(msg) => write!(f, "Internal server error: {}", msg),
ApiError::InternalServerError {body, backend_hostname} =>
write!(f, "Internal Server Error; backend hostname: {:?} ; body: {}; ", backend_hostname, body),
// Server response handling errors
ApiError::DeserializationError(error) => write!(f, "Deserialization error: {}", error),
// Network errors
@@ -1,68 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::{GenerateHunyuan20ImageTo3dRequest, GenerateHunyuan20ImageTo3dResponse, GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_hunyuan3d_2_0_image_to_3d(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateHunyuan20ImageTo3dRequest,
) -> Result<GenerateHunyuan20ImageTo3dResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH)
Ok(basic_json_post_request(
api_host,
GENERATE_HUNYUAN_2_0_IMAGE_TO_3D_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,68 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::{GenerateHunyuan21ImageTo3dRequest, GenerateHunyuan21ImageTo3dResponse, GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_hunyuan3d_2_1_image_to_3d(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateHunyuan21ImageTo3dRequest,
) -> Result<GenerateHunyuan21ImageTo3dResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH)
Ok(basic_json_post_request(
api_host,
GENERATE_HUNYUAN_2_1_IMAGE_TO_3D_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,68 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{GenerateKling16ProImageToVideoRequest, GenerateKling16ProImageToVideoResponse, GENERATE_KLING_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_kling_16_pro_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateKling16ProImageToVideoRequest,
) -> Result<GenerateKling16ProImageToVideoResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_KLING_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH)
Ok(basic_json_post_request(
api_host,
GENERATE_KLING_1_6_PRO_IMAGE_TO_VIDEO_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,68 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{GenerateKling21MasterImageToVideoRequest, GenerateKling21MasterImageToVideoResponse, GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_kling_21_master_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateKling21MasterImageToVideoRequest,
) -> Result<GenerateKling21MasterImageToVideoResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH)
Ok(basic_json_post_request(
api_host,
GENERATE_KLING_2_1_MASTER_IMAGE_TO_VIDEO_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,68 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{GenerateKling21ProImageToVideoRequest, GenerateKling21ProImageToVideoResponse, GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_kling_21_pro_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateKling21ProImageToVideoRequest,
) -> Result<GenerateKling21ProImageToVideoResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH)
Ok(basic_json_post_request(
api_host,
GENERATE_KLING_2_1_PRO_IMAGE_TO_VIDEO_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,68 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::{GenerateSeedance10LiteImageToVideoRequest, GenerateSeedance10LiteImageToVideoResponse, GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_seedance_1_0_lite_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateSeedance10LiteImageToVideoRequest,
) -> Result<GenerateSeedance10LiteImageToVideoResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH)
Ok(basic_json_post_request(
api_host,
GENERATE_SEEDANCE_1_0_LITE_IMAGE_TO_VIDEO_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,68 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::{GenerateVeo2ImageToVideoRequest, GenerateVeo2ImageToVideoResponse, GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH};
use log::{debug, info};
use reqwest::Client;
pub async fn generate_veo_2_image_to_video(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GenerateVeo2ImageToVideoRequest,
) -> Result<GenerateVeo2ImageToVideoResponse, StorytellerError> {
let url = get_route(api_host);
info!("Requesting {:?}", &url);
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON)
.header("Content-Type", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let request_body = serde_json::to_string(&request)
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
debug!("Request body: {:?}", request_body);
let response = request_builder
.body(request_body)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response)
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response_body = &response.text()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
debug!("Response body: {:?}", response_body);
let response = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(response)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}{}", api_hostname, GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH)
Ok(basic_json_post_request(
api_host,
GENERATE_VEO_2_IMAGE_TO_VIDEO_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,6 +1,7 @@
use crate::error::api_error::ApiError;
use crate::utils::status_codes::*;
use anyhow::anyhow;
use log::error;
/// Pass request errors to this method for standard error handling behavior.
/// This function needs to take temporary ownership, but will return it to the caller.
@@ -13,6 +14,15 @@ pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest:
let status = response.status();
if !status.is_success() {
// The host in the fleet that handled the request.
let backend_hostname= response.headers()
.get("x-backend-hostname")
.map(|header| header.to_str().unwrap_or_else(|err| {
error!("Failed to parse x-backend-hostname header: {:?}", err);
"unknown"
}))
.map(|s| s.to_string());
let response_body = match response.text().await {
Ok(text) => text.to_string(),
Err(err) => format!("Could not read response body: {:?}", err),
@@ -23,7 +33,7 @@ pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest:
STATUS_403_FORBIDDEN => Err(ApiError::Forbidden(response_body)),
STATUS_404_NOT_FOUND => Err(ApiError::NotFound(response_body)),
STATUS_429_TOO_MANY_REQUESTS => Err(ApiError::TooManyRequests(response_body)),
STATUS_500_INTERNAL_SERVER_ERROR => Err(ApiError::InternalServerError(response_body)),
STATUS_500_INTERNAL_SERVER_ERROR => Err(ApiError::InternalServerError { body: response_body, backend_hostname }),
_ => Err(ApiError::Other(anyhow!("Bad status code: {}; message: {:?}", status, response_body))),
}
}
+4
View File
@@ -41,6 +41,10 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
std::fs::create_dir_all(&cache_dir)?;
let path = cache_dir.join(COOKIES_FILENAME);
// NB(bt): Let's always log this.
println!("Tauri plugin http: using cookie store at {}", path.display());
let file = File::options()
.create(true)
.append(true)
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useRef, useState } from "react";
import {
faGear,
faImages,
@@ -6,8 +6,7 @@ import {
faFilm,
faPaintbrush,
faImage,
faDiamond,
faGem,
// faGem,
} from "@fortawesome/pro-solid-svg-icons";
import { Button } from "@storyteller/ui-button";
import { AuthButtons } from "./AuthButtons";
@@ -20,7 +19,7 @@ import {
} from "@storyteller/ui-gallery-modal";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { Tooltip } from "@storyteller/ui-tooltip";
import { downloadFileFromUrl } from "@storyteller/api";
import { downloadFileFromUrl, FilterMediaClasses } from "@storyteller/api";
import {
MenuIconSelector,
MenuIconItem,
@@ -36,12 +35,15 @@ import {
is3DSceneLoaded,
set3DPageMounted,
} from "~/pages/PageEnigma/Editor/editor";
import { usePricingModalStore } from "@storyteller/ui-pricing-modal";
// import { usePricingModalStore } from "@storyteller/ui-pricing-modal"; - Uncomment for pricing modal - BFlat
import toast from "react-hot-toast";
interface Props {
pageName: string;
loginSignUpPressed: () => void;
}
const SWITCHER_THROTTLE_TIME = 500; // milliseconds
const appMenuTabs: MenuIconItem[] = [
{
id: "2D",
@@ -74,7 +76,7 @@ const appMenuTabs: MenuIconItem[] = [
export const topNavMediaId = signal<string>("");
export const topNavMediaUrl = signal<string>("");
export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
useSignals();
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
@@ -88,15 +90,32 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
const is3DSceneReady = is3DSceneLoaded.value;
const is3DEditorReady = is3DEditorInitialized.value;
const [disableSwitcher, setDisableSwitcher] = useState(false);
const switcherThrottle = useRef(false);
const disableTabSwitcher = () => {
return (
useTabStore.getState().activeTabId === "3D" &&
!is3DEditorReady &&
!is3DSceneReady
disableSwitcher ||
(useTabStore.getState().activeTabId === "3D" &&
!is3DEditorReady &&
!is3DSceneReady)
);
};
const { toggleModal } = usePricingModalStore();
const downloadFile = async (url: string, mediaClass?: string) => {
try {
await downloadFileFromUrl(url);
if (mediaClass === FilterMediaClasses.DIMENSIONAL) {
toast.success(`Downloaded 3D model`);
} else {
toast.success(`Downloaded ${mediaClass}`);
}
} catch (error) {
toast.error("Failed to download file");
}
};
// const { toggleModal } = usePricingModalStore(); - Uncomment for pricing modal - BFlat
return (
<>
@@ -119,6 +138,13 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
activeMenu={tabStore.activeTabId}
disabled={disableTabSwitcher()}
onMenuChange={(tabId) => {
// Prevent a second input if the switcher is throttled.
if (switcherThrottle.current) {
return;
}
switcherThrottle.current = true;
setDisableSwitcher(true);
// Disable 3d engine to prevent memory leak.
if (tabId === "3D") {
set3DPageMounted(true);
@@ -126,6 +152,12 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
set3DPageMounted(false);
}
useTabStore.getState().setActiveTab(tabId);
setTimeout(() => {
// Clear the throttle
switcherThrottle.current = false;
// Trigger a new re-render (important)
setDisableSwitcher(false);
}, SWITCHER_THROTTLE_TIME);
}}
className="w-fit"
/>
@@ -145,8 +177,9 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
)}
</div>
<div className="flex justify-end gap-3.5">
<div className="flex justify-end gap-3.5 pr-2">
<div className="flex gap-2">
{/* - Uncomment for pricing modal - BFlat */}
{/* <Button
variant="primary"
icon={faGem}
@@ -173,9 +206,7 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
<Activity />
</div>
<div className="flex justify-end gap-2">
<AuthButtons loginSignUpPressed={loginSignUpPressed}/>
</div>
<AuthButtons loginSignUpPressed={loginSignUpPressed} />
</div>
</nav>
</header>
@@ -186,7 +217,7 @@ export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
globalAccountLogoutCallback={() => setLogoutStates()}
/>
<GalleryModal mode="view" onDownloadClicked={downloadFileFromUrl} />
<GalleryModal mode="view" onDownloadClicked={downloadFile} />
</>
);
};
@@ -1216,10 +1216,10 @@ class Editor {
: this.render_camera_aspect_ratio === meraAspectRatio.VERTICAL_9_16
? 576
: this.render_camera_aspect_ratio ===
meraAspectRatio.HORIZONTAL_3_2
meraAspectRatio.HORIZONTAL_3_2
? 900
: this.render_camera_aspect_ratio ===
meraAspectRatio.VERTICAL_2_3
meraAspectRatio.VERTICAL_2_3
? 600
: 1000;
const height =
@@ -1228,10 +1228,10 @@ class Editor {
: this.render_camera_aspect_ratio === meraAspectRatio.VERTICAL_9_16
? 1024
: this.render_camera_aspect_ratio ===
meraAspectRatio.HORIZONTAL_3_2
meraAspectRatio.HORIZONTAL_3_2
? 600
: this.render_camera_aspect_ratio ===
meraAspectRatio.VERTICAL_2_3
meraAspectRatio.VERTICAL_2_3
? 900
: 1000;
@@ -1323,7 +1323,7 @@ class Editor {
) {
this.composer.render();
//this.rawRenderer.render(this.activeScene.scene, this.render_camera!);
this.render_composer.render();
// this.render_composer.render();
} else if (this.renderer && this.render_camera && !this.rendering) {
this.renderer.setSize(this.render_width, this.render_height);
this.renderer.render(this.activeScene.scene, this.render_camera);
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useContext, useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { TopBar } from "~/components";
import { Controls3D } from "./comps/Controls3D";
@@ -73,26 +73,32 @@ import {
// videoGenerationModels,
// useModelSelectorStore,
} from "@storyteller/ui-model-selector";
import { LoginModal } from "@storyteller/ui-login-modal";
import { LoginModal, useLoginModalStore } from "@storyteller/ui-login-modal";
import PageDraw from "../PageDraw/PageDraw";
import { useTabStore } from "../Stores/TabState";
export const PageEditor = () => {
useSignals();
const [isLoginModalOpen, setIsLoginModalOpen] = useState<boolean>(false);
const { triggerRecheck } = useLoginModalStore();
const { status } = authentication;
useEffect(() => {
if (status.value !== AUTH_STATUS.LOGGED_OUT) {
setIsLoginModalOpen(true);
if (status.value === AUTH_STATUS.LOGGED_OUT) {
triggerRecheck();
}
}, [status.value]);
}, [status.value, triggerRecheck]);
const tabStore = useTabStore();
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
};
// TODO: For 3d Promptbox to accept this later on
// const { selectedModels } = useModelSelectorStore();
// const selectedModel =
// selectedModels[ModelCategory.Editor3D] ||
// videoGenerationModels[0]?.label;
useEffect(() => {
timelineHeight.value = 0; //timelineHeight.value = 208;
sidePanelWidth.value = 340;
@@ -409,30 +415,30 @@ export const PageEditor = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tabStore.activeTabId, editorEngine]);
return (
<div className="w-screen">
<TopBar loginSignUpPressed={()=> {
setIsLoginModalOpen(true)
}} pageName="Edit Scene" />
{isLoginModalOpen && (
<LoginModal
onClose={() => setIsLoginModalOpen(false)}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
onOpenChange={(isOpen: boolean) => {
if (isOpen) {
disableHotkeyInput(DomLevels.DIALOGUE);
} else {
enableHotkeyInput(DomLevels.DIALOGUE);
}
}}
onArtCraftAuthSuccess={(userInfo: any) => {
authentication.status.value = AUTH_STATUS.LOGGED_IN;
authentication.userInfo.value = userInfo;
}}
/>
)}
<TopBar
loginSignUpPressed={() => {
console.log("PRESSED");
triggerRecheck();
}}
pageName="Edit Scene"
/>
<LoginModal
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
onOpenChange={(isOpen: boolean) => {
if (isOpen) {
disableHotkeyInput(DomLevels.DIALOGUE);
} else {
enableHotkeyInput(DomLevels.DIALOGUE);
}
}}
onArtCraftAuthSuccess={(userInfo: any) => {
authentication.status.value = AUTH_STATUS.LOGGED_IN;
authentication.userInfo.value = userInfo;
}}
/>
{tabStore.activeTabId == "3D" && (
<div>
<OnboardingHelper />
@@ -565,8 +571,6 @@ export const PageEditor = () => {
/>
</div>
)}
</div>
);
};
@@ -13,14 +13,19 @@ interface TextToImageProps {
imageMediaId?: string;
imageUrl?: string;
}
import { IMAGE_MODELS_BY_LABEL } from "@storyteller/model-list";
import { ModelInfo } from "@storyteller/model-list";
const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const { selectedModels } = useModelSelectorStore();
const selectedModel =
selectedModels[ModelCategory.TextToImage] ||
imageGenerationModels[0]?.label;
const selectedModelInfo : ModelInfo | undefined = IMAGE_MODELS_BY_LABEL[selectedModel];
const jobContext: JobContextType = {
jobTokens: [],
addJobToken: () => {},
@@ -48,6 +53,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
return jobContext;
}}
model={selectedModel}
modelInfo={selectedModelInfo}
imageMediaId={imageMediaId}
url={imageUrl ?? undefined}
onEnqueuePressed={async () => {
@@ -8,6 +8,8 @@ import {
useModelSelectorStore,
videoGenerationModels,
} from "@storyteller/ui-model-selector";
import { VIDEO_MODELS_BY_LABEL } from "@storyteller/model-list";
import { ModelInfo } from "@storyteller/model-list";
interface ImageToVideoProps {
imageMediaId?: string;
@@ -17,13 +19,12 @@ interface ImageToVideoProps {
const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const { selectedModels } = useModelSelectorStore();
const selectedModel =
selectedModels[ModelCategory.ImageToVideo] ||
videoGenerationModels[0]?.label;
console.log("Selected model:", selectedModel)
console.log("Image Media Id:", imageMediaId)
console.log("Image URL:", imageUrl)
const selectedModelInfo : ModelInfo | undefined = VIDEO_MODELS_BY_LABEL[selectedModel];
const jobContext: JobContextType = {
jobTokens: [],
@@ -53,6 +54,7 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
return jobContext;
}}
model={selectedModel}
modelInfo={selectedModelInfo}
imageMediaId={imageMediaId}
url={imageUrl ?? undefined}
onEnqueuePressed={async () => {
+1 -2
View File
@@ -1,3 +1,2 @@
export * from './lib/BuildEnv';
export * from './lib/BuildEnvType';
export * from './lib/build-env';
+27 -29
View File
@@ -1,4 +1,4 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
@@ -7,7 +7,7 @@ import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/build-env',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') }), ],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
@@ -15,35 +15,33 @@ export default defineConfig(() => ({
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@frontend/build-env',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
'emptyOutDir': true,
'transformMixedEsModules': true,
'entry': 'src/index.ts',
'name': '@frontend/build-env',
'fileName': 'index',
'formats': ['es' as const],
'external': ['react','react-dom','react/jsx-runtime'],
'lib': {
'entry': "src/index.ts",
'name': "@frontend/build-env",
'fileName': "index",
'formats': ['es' as const],
},
'rollupOptions': {"external":["'react'","'react-dom'","'react/jsx-runtime'"]},
'outDir': "./dist",
'reportCompressedSize': true,
'commonjsOptions': {"transformMixedEsModules":true},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
'watch': false,
'globals': true,
'environment': "jsdom",
'include': ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
'reporters': ["default"],
'coverage': {
'reportsDirectory': './test-output/vitest/coverage',
'provider': 'v8' as const,
}
},
}));
@@ -62,7 +62,7 @@ interface GalleryModalProps {
onSelectItem?: (id: string) => void;
maxSelections?: number;
onUseSelected?: (selectedItems: GalleryItem[]) => void;
onDownloadClicked?: (url: string) => Promise<void>;
onDownloadClicked?: (url: string, mediaClass?: string) => Promise<void>;
onAddToSceneClicked?: (
url: string,
media_id: string | undefined
@@ -21,7 +21,7 @@ interface LightboxModalProps {
additionalInfo?: React.ReactNode;
downloadUrl?: string;
mediaId?: string;
onDownloadClicked?: (url: string) => Promise<void>;
onDownloadClicked?: (url: string, mediaClass?: string) => Promise<void>;
onAddToSceneClicked?: (
url: string,
media_id: string | undefined
@@ -164,7 +164,7 @@ export function LightboxModal({
icon={faDownToLine}
onClick={async (e) => {
e.stopPropagation();
await onDownloadClicked(downloadUrl);
await onDownloadClicked(downloadUrl, mediaClass);
}}
>
Download
@@ -1,2 +1,2 @@
export * from './lib/login-modal';
export * from "./lib/login-modal";
export * from "./lib/useLoginModalStore";
@@ -1,9 +1,7 @@
import { Button } from "@storyteller/ui-button";
import { Transition, TransitionChild } from "@headlessui/react";
import { useState, useEffect, useRef } from "react";
import {
faArrowRight,
} from "@fortawesome/pro-solid-svg-icons";
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import {
CheckSoraSession,
@@ -12,8 +10,9 @@ import {
} from "@storyteller/tauri-api";
import { ArtCraftSignUp } from "./artcraft-signup";
import { UsersApi } from "@storyteller/api";
import { useLoginModalStore } from "./useLoginModalStore";
interface LoginModalProps {
onClose: () => void;
onClose?: () => void;
videoSrc2D?: string;
videoSrc3D?: string;
onOpenChange?: (isOpen: boolean) => void;
@@ -29,9 +28,9 @@ export function LoginModal({
onArtCraftAuthSuccess,
isSignUp: initialIsSignUp = true,
}: LoginModalProps) {
const { isOpen, recheckTrigger, closeModal } = useLoginModalStore();
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(true);
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
const [isSignUp, setIsSignUp] = useState(initialIsSignUp);
const [errorMessage, setErrorMessage] = useState("");
@@ -61,26 +60,35 @@ export function LoginModal({
setStep(4); // Always go to the final step after Sora login
});
// Check session on component mount
// Check session on component mount and when recheckTrigger changes
useEffect(() => {
checkArtCraftLogin().then((loggedIn) => {
if (loggedIn) {
setIsLoggedInArtCraft(true);
setIsOpen(false);
closeModal();
} else {
setIsOpen(true);
setStep(1); // Always start at step 1
// Reset all modal state to initial values
setStep(1);
setIsLoading(false);
setIsSignUp(initialIsSignUp);
setErrorMessage("");
setShowDiscord(false);
setShowSuccess(false);
setIsLoggedInArtCraft(false);
const { openModal } = useLoginModalStore.getState();
openModal();
}
});
}, []);
}, [recheckTrigger, closeModal, initialIsSignUp]);
useEffect(() => {
if (onOpenChange) onOpenChange(isOpen);
}, [isOpen, onOpenChange]);
const handleClose = () => {
setIsOpen(false);
onClose();
closeModal();
onClose?.();
};
const handleNext = async () => {
@@ -149,7 +157,8 @@ export function LoginModal({
Join Our Community
</h2>
<p className="text-white/70 mb-6 text-center px-24">
Connect with other creators, share your work, and get the latest updates in our Discord community.
Connect with other creators, share your work, and get the latest
updates in our Discord community.
</p>
<div className="flex gap-4">
<Button
@@ -234,15 +243,15 @@ export function LoginModal({
try {
let signupResponse, loginResponse;
if (isSignUp) {
console.log("Sign up!")
console.log("Sign up!");
signupResponse = await usersApi.Signup({
username,
email,
password,
passwordConfirmation,
});
console.log(signupResponse)
console.log(signupResponse);
if (!signupResponse.success) {
setErrorMessage(
signupResponse.errorMessage ||
@@ -251,21 +260,21 @@ export function LoginModal({
setIsLoading(false);
return;
}
console.log("Path 1")
console.log("Path 1");
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
console.log(loginResponse)
console.log(loginResponse);
} else {
console.log("Path 2")
console.log("Path 2");
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
console.log(loginResponse)
console.log(loginResponse);
}
if (!loginResponse.success) {
setErrorMessage(
loginResponse.errorMessage ||
@@ -276,12 +285,12 @@ export function LoginModal({
}
setIsLoggedInArtCraft(true);
console.log("ARTCRAFT LOGIN SUCCeSS")
console.log("ARTCRAFT LOGIN SUCCeSS");
if (onArtCraftAuthSuccess) {
const session = await usersApi.GetSession();
const userInfo = session.data?.user;
console.log("USERINFO")
console.log(userInfo)
console.log("USERINFO");
console.log(userInfo);
if (userInfo) onArtCraftAuthSuccess(userInfo);
}
setShowDiscord(true); // Show Discord step after successful login
@@ -313,11 +322,7 @@ export function LoginModal({
return (
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
{step === 2 && (
<Button
variant="secondary"
onClick={handleBack}
disabled={isLoading}
>
<Button variant="secondary" onClick={handleBack} disabled={isLoading}>
Back
</Button>
)}
@@ -334,7 +339,8 @@ export function LoginModal({
);
};
return <Transition appear show={isOpen}>
return (
<Transition appear show={isOpen}>
<div className="fixed inset-0 z-[100]">
<TransitionChild
enter="ease-out duration-300"
@@ -376,6 +382,7 @@ export function LoginModal({
</div>
</div>
</Transition>
);
}
export default LoginModal;
@@ -0,0 +1,18 @@
import { create } from "zustand";
interface LoginModalStore {
isOpen: boolean;
recheckTrigger: number;
openModal: () => void;
closeModal: () => void;
triggerRecheck: () => void;
}
export const useLoginModalStore = create<LoginModalStore>((set) => ({
isOpen: false,
recheckTrigger: 0,
openModal: () => set({ isOpen: true }),
closeModal: () => set({ isOpen: false }),
triggerRecheck: () =>
set((state) => ({ recheckTrigger: state.recheckTrigger + 1 })),
}));
@@ -25,6 +25,7 @@ import { DomLevels } from "@storyteller/common";
declare global {
interface Window {
__inertStripperInstalled?: boolean;
__modalEscListenerInstalled?: boolean;
}
}
@@ -58,6 +59,56 @@ if (!window.__inertStripperInstalled) {
});
}
// ---------------------------------------------------------------------------
// Modal ESC handling & stack tracking
// If several modals are stacked, Esc will dismiss the one visually on top (highest z-index).
// ---------------------------------------------------------------------------
interface ModalRegistryEntry {
id: number;
zIndex: number;
onClose: () => void;
closeOnEsc: boolean;
}
const modalRegistry: ModalRegistryEntry[] = [];
const registerModal = (entry: ModalRegistryEntry) => {
modalRegistry.push(entry);
};
const unregisterModal = (id: number) => {
const idx = modalRegistry.findIndex((m) => m.id === id);
if (idx !== -1) modalRegistry.splice(idx, 1);
};
const updateModal = (
id: number,
data: Partial<{ zIndex: number; onClose: () => void; closeOnEsc: boolean }>
) => {
const entry = modalRegistry.find((m) => m.id === id);
if (entry) Object.assign(entry, data);
};
const getTopEscapableModal = (): ModalRegistryEntry | undefined => {
return modalRegistry
.filter((m) => m.closeOnEsc)
.sort((a, b) => b.zIndex - a.zIndex)[0];
};
// Install one global ESC listener (once per page load)
if (typeof window !== "undefined" && !window.__modalEscListenerInstalled) {
window.__modalEscListenerInstalled = true;
window.addEventListener("keydown", (e: KeyboardEvent) => {
if (e.key !== "Escape" && e.key !== "Esc") return;
const top = getTopEscapableModal();
if (top) {
e.preventDefault();
top.onClose();
}
});
}
// Simple global z-index tracker for stacked modals
let modalZCounter = 70;
@@ -176,6 +227,7 @@ export const Modal = ({
resizable = false,
initialPosition,
closeOnOutsideClick = true,
closeOnEsc = true,
allowBackgroundInteraction = false,
expandable = false,
}: {
@@ -203,6 +255,10 @@ export const Modal = ({
* If false, clicking the backdrop will NOT close the modal
*/
closeOnOutsideClick?: boolean;
/**
* If true, pressing the Escape key will close the modal
*/
closeOnEsc?: boolean;
/**
* If true, allow interacting with background (removes pointer events from backdrop)
*/
@@ -747,6 +803,31 @@ export const Modal = ({
};
}, [allowBackgroundInteraction]);
// Register this modal in the global registry for ESC handling
const idRef = useRef<number>(Math.floor(Math.random() * 1e9));
useEffect(() => {
if (!isOpen) return;
registerModal({
id: idRef.current,
zIndex,
onClose,
closeOnEsc,
});
return () => {
unregisterModal(idRef.current);
};
// We intentionally want to run this only on mount/unmount when isOpen true
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
// Keep registry entry updated when props change
useEffect(() => {
if (!isOpen) return;
updateModal(idRef.current, { zIndex, onClose, closeOnEsc });
}, [isOpen, zIndex, onClose, closeOnEsc]);
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog
@@ -1,41 +1,37 @@
import type { PopoverItem } from "@storyteller/ui-popover";
import { faClock, faFilm, faImage } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IMAGE_MODELS } from "@storyteller/model-list";
export type ModelList = Omit<PopoverItem, "selected">[];
export const allModels = {
video: [
{
id: "kling_1.6_pro",
label: "Kling 1.6 Pro",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "Good quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "kling_2.1_pro",
label: "Kling 2.1 Pro",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "kling_2.1_master",
label: "Kling 2.1 Master",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "Master quality model ($$)",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "seedance_1.0_lite",
label: "Seedance 1.0 Lite",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "Fast and high-quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "veo_2",
label: "Google Veo 2",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "Fast and high-quality model",
@@ -44,33 +40,54 @@ export const allModels = {
],
image: [
{
id: "todo",
label: "Flux Pro 1.1 Ultra",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.flux_pro_1_1_ultra,
},
{
label: "Flux Pro 1.1",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.flux_pro_1_1,
},
{
label: "Flux 1 Dev",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.flux_1_dev,
},
{
label: "Flux 1 Schnell",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.flux_1_schnell,
},
{
label: "GPT Image 1 (GPT-4o)",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "Slow, ultra instructive model",
badges: [{ label: "45 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
modelInfo: IMAGE_MODELS.gpt_image_1,
},
{
id: "todo",
label: "Flux Pro Ultra",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "todo",
label: "Recraft 3",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "Fast and high-quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
id: "todo",
label: "Flux.1 Kontext",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "New model with advanced context",
badges: [{ label: "25 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
//{
// id: "todo",
// label: "Recraft 3",
// icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
// description: "Fast and high-quality model",
// badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
//},
//{
// id: "todo",
// label: "Flux.1 Kontext",
// icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
// description: "New model with advanced context",
// badges: [{ label: "25 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
//},
],
};
@@ -5,6 +5,9 @@
{
"path": "../popover"
},
{
"path": "../../model-list"
},
{
"path": "./tsconfig.lib.json"
},
@@ -45,6 +45,9 @@
"references": [
{
"path": "../popover/tsconfig.lib.json"
},
{
"path": "../../model-list/tsconfig.lib.json"
}
]
}
@@ -13,6 +13,7 @@ import {
faChevronUp,
faCircleCheck,
} from "@fortawesome/pro-solid-svg-icons";
import { ModelInfo } from "@storyteller/model-list";
export interface PopoverItem {
label: string;
@@ -25,6 +26,7 @@ export interface PopoverItem {
label: string;
icon?: ReactNode;
}>;
modelInfo?: ModelInfo;
}
interface PopoverMenuProps {
@@ -5,6 +5,9 @@
{
"path": "../button"
},
{
"path": "../../model-list"
},
{
"path": "./tsconfig.lib.json"
},
@@ -45,6 +45,9 @@
"references": [
{
"path": "../button/tsconfig.lib.json"
},
{
"path": "../../model-list/tsconfig.lib.json"
}
]
}
@@ -27,6 +27,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import { ModelInfo } from "@storyteller/model-list";
interface ReferenceImage {
id: string;
@@ -39,6 +40,7 @@ interface PromptBoxImageProps {
useJobContext: () => JobContextType;
onEnqueuePressed?: () => void | Promise<void>;
model: string;
modelInfo?: ModelInfo;
imageMediaId?: string;
url?: string;
}
@@ -47,6 +49,7 @@ export const PromptBoxImage = ({
useJobContext,
onEnqueuePressed,
model,
modelInfo,
imageMediaId,
url,
}: PromptBoxImageProps) => {
@@ -65,11 +68,8 @@ export const PromptBoxImage = ({
}
}, [imageMediaId, url]);
console.log("Is this a desktop app?", IsDesktopApp());
const [isModalOpen, setIsModalOpen] = useState(false);
const [content, setContent] = useState("");
//const { jobTokens, addJobToken, removeJobToken, clearJobTokens } =
// useJobContext();
const [prompt, setPrompt] = useState("");
const [isEnqueueing, setIsEnqueueing] = useState(false);
@@ -196,7 +196,7 @@ export const PromptBoxImage = ({
const handleEnqueue = async () => {
setIsEnqueueing(true);
console.log("Prompting");
console.log("PromptBoxImage - Prompting with model", modelInfo);
setTimeout(() => {
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
@@ -205,14 +205,12 @@ export const PromptBoxImage = ({
setIsEnqueueing(false);
}, 10000);
const modelName = getModelByName(model);
const generateResponse = await EnqueueTextToImage({
prompt: prompt,
model: modelName,
model: modelInfo,
});
console.log("generateResponse", generateResponse);
console.log("PromptBoxImage - generateResponse", generateResponse);
onEnqueuePressed?.();
@@ -6,7 +6,7 @@ import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Button, ToggleButton } from "@storyteller/ui-button";
import { Modal } from "@storyteller/ui-modal";
import { FalKlingImageToVideo } from "@storyteller/tauri-api";
import { EnqueueImageToVideo } from "@storyteller/tauri-api";
import {
faMessageXmark,
faMessageCheck,
@@ -20,6 +20,7 @@ import { faRectangle } from "@fortawesome/pro-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import { ModelInfo } from "@storyteller/model-list";
interface ReferenceImage {
id: string;
@@ -32,6 +33,7 @@ interface PromptBoxVideoProps {
useJobContext: () => JobContextType;
onEnqueuePressed?: () => void | Promise<void>;
model: string;
modelInfo?: ModelInfo;
imageMediaId?: string;
url?: string;
}
@@ -40,6 +42,7 @@ export const PromptBoxVideo = ({
useJobContext,
onEnqueuePressed,
model,
modelInfo,
imageMediaId,
url,
}: PromptBoxVideoProps) => {
@@ -183,7 +186,7 @@ export const PromptBoxVideo = ({
const handleEnqueue = async () => {
setIsEnqueueing(true);
console.log("Handle Enqueue");
console.log("PromptBoxVideo - Prompting with model", modelInfo);
setTimeout(() => {
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
@@ -192,8 +195,10 @@ export const PromptBoxVideo = ({
setIsEnqueueing(false);
}, 10000);
const generateResponse = await FalKlingImageToVideo({
const generateResponse = await EnqueueImageToVideo({
model: modelInfo,
image_media_token: referenceImages[0].mediaToken,
//prompt: prompt,
});
console.log("generateResponse", generateResponse);
@@ -5,6 +5,7 @@ import { Button } from "@storyteller/ui-button";
import { UsersApi } from "@storyteller/api";
import { UserInfo } from "@storyteller/api";
import { useLoginModalStore } from "@storyteller/ui-login-modal";
const usersApi = new UsersApi();
@@ -15,6 +16,7 @@ export interface ArtcraftAccountBlockProps {
export const ArtcraftAccountBlock = ({
globalAccountLogoutCallback,
}: ArtcraftAccountBlockProps) => {
const { triggerRecheck } = useLoginModalStore();
const [artcraftSession, setArtcraftSession] = useState<UserInfo | undefined>(
undefined
);
@@ -50,8 +52,9 @@ export const ArtcraftAccountBlock = ({
setIsLoggedIn(false);
setIsCheckingArtcraftSession(false);
globalAccountLogoutCallback(); // TODO: This resets the old global application state
triggerRecheck(); // Trigger modal to recheck session and potentially open
} else {
window.location.href = "/login"; // TODO(bt,2025-05-08): Once we have in-page routing, get rid of this.
triggerRecheck(); // Open login modal instead of redirecting
}
};
@@ -0,0 +1,174 @@
import { useState } from "react";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faGripVertical,
faSpinnerThird,
} from "@fortawesome/pro-solid-svg-icons";
interface RouterItem {
id: string;
name: string;
emoji: string;
}
interface SortableItemProps {
id: string;
name: string;
emoji: string;
isUpdating?: boolean;
}
const SortableItem = ({
id,
name,
emoji,
isUpdating = false,
}: SortableItemProps) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={`
flex items-center justify-between rounded-lg bg-white/5 p-3 transition-colors duration-200
${isUpdating ? "opacity-60" : "cursor-move hover:bg-white/10"}
${isDragging ? "opacity-50 shadow-lg" : ""}
`}
>
<div className="flex items-center gap-3">
<span className="text-lg">{emoji}</span>
<span className="font-medium">{name}</span>
</div>
<div className="flex items-center">
<FontAwesomeIcon
icon={faGripVertical}
className="text-white/40 text-sm"
/>
</div>
</div>
);
};
export const RouterPrioritySettingsPane = () => {
const [items, setItems] = useState<RouterItem[]>([
{ id: "artcraft-3d", name: "ArtCraft 3D", emoji: "🎨" },
{ id: "gpt-image-1", name: "GPT Image 1 (GPT-4o)", emoji: "🤖" },
{ id: "flux-pro-ultra", name: "Flux Pro Ultra", emoji: "⚡" },
{ id: "recraft-3", name: "Recraft 3", emoji: "🌟" },
{ id: "flux-kontext", name: "Flux.1 Kontext", emoji: "🔮" },
]);
const [isUpdating, setIsUpdating] = useState(false);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const updateRouterPriorityOnBackend = async (newOrder: RouterItem[]) => {
try {
setIsUpdating(true);
console.log("Updating router priority on backend:", newOrder);
// Simulate API call delay
await new Promise((resolve) => setTimeout(resolve, 500)); //remove this - BFlat
//api call here
console.log("Router priority updated successfully");
} catch (error) {
console.error("Failed to update router priority:", error);
} finally {
setIsUpdating(false);
}
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
setItems((prevItems) => {
const oldIndex = prevItems.findIndex((item) => item.id === active.id);
const newIndex = prevItems.findIndex((item) => item.id === over?.id);
const newOrder = arrayMove(prevItems, oldIndex, newIndex);
// Send update to backend
updateRouterPriorityOnBackend(newOrder);
return newOrder;
});
}
};
return (
<div className="space-y-4">
<div>
<p className="text-sm text-white/70 mb-4">
Drag and drop to reorder model priority. Higher items will be tried
first.
</p>
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<div className="space-y-2">
{items.map((item) => (
<SortableItem
key={item.id}
id={item.id}
name={item.name}
emoji={item.emoji}
isUpdating={isUpdating}
/>
))}
</div>
</SortableContext>
</DndContext>
{isUpdating && (
<div className="text-xs rounded-full animate-pulse mt-4 flex items-center gap-2">
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
Updating...
</div>
)}
</div>
);
};
@@ -6,12 +6,14 @@ import {
faCog,
faVolumeHigh,
faCircleInfo,
faRoute,
} from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { MiscSettingsPane } from "./panes/MiscSettingsPane";
import { AudioSettingsPane } from "./panes/AudioSettingsPane";
import { AccountSettingsPane } from "./panes/AccountSettings/AccountSettingsPane";
import { AboutSettingsPane } from "./panes/AboutSettingsPane";
import { RouterPrioritySettingsPane } from "./panes/RouterPrioritySettingsPane";
interface SettingsModalProps {
isOpen: boolean;
@@ -20,8 +22,12 @@ interface SettingsModalProps {
initialSection?: SettingsSection;
}
//type SettingsSection = "misc" | "audio" | "accounts" | "video" | "image";
type SettingsSection = "general" | "accounts" | "alerts" | "about";
type SettingsSection =
| "general"
| "accounts"
| "alerts"
| "about"
| "router_priority";
export const SettingsModal = ({
isOpen,
@@ -37,6 +43,11 @@ export const SettingsModal = ({
{ id: "accounts" as const, label: "Accounts", icon: faUser },
{ id: "alerts" as const, label: "Alerts", icon: faVolumeHigh },
{ id: "about" as const, label: "About", icon: faCircleInfo },
{
id: "router_priority" as const,
label: "Model Router Priority",
icon: faRoute,
},
//{ id: "video" as const, label: "Video", icon: faVideo },
//{ id: "image" as const, label: "Image", icon: faImage },
];
@@ -55,6 +66,8 @@ export const SettingsModal = ({
);
case "about":
return <AboutSettingsPane />;
case "router_priority":
return <RouterPrioritySettingsPane />;
}
};
+50
View File
@@ -0,0 +1,50 @@
{
"extends": [
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.json"
],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
"{projectRoot}/vite.config.{js,ts,mjs,mts}"
]
}
]
}
}
]
}
+11
View File
@@ -0,0 +1,11 @@
# model-list
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build model-list` to build the library.
## Running unit tests
Run `nx test model-list` to execute the unit tests via [Vitest](https://vitest.dev/).
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@storyteller/model-list",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"dependencies": {}
}
+6
View File
@@ -0,0 +1,6 @@
export * from './lib/model-list.js';
export * from './lib/ModelInfo.js';
export * from './lib/ModelCreator.js';
export * from './lib/ImageModels.js';
export * from './lib/VideoModels.js';
@@ -0,0 +1,41 @@
import { ModelCreator } from "./ModelCreator.js";
import { ModelInfo } from "./ModelInfo.js";
export const IMAGE_MODELS: Record<string, ModelInfo> = {
flux_pro_1_1_ultra: {
name: "Flux Pro 1.1 Ultra",
tauri_id: "flux_pro_11_ultra",
creator: ModelCreator.BlackForestLabs,
},
flux_pro_1_1: {
name: "Flux Pro 1.1",
tauri_id: "flux_pro_11",
creator: ModelCreator.BlackForestLabs,
},
flux_1_dev: {
name: "Flux 1 Dev",
tauri_id: "flux_1_dev",
creator: ModelCreator.BlackForestLabs,
},
flux_1_schnell: {
name: "Flux 1 Schnell",
tauri_id: "flux_1_schnell",
creator: ModelCreator.BlackForestLabs,
},
gpt_image_1: {
name: "GPT Image 1 (GPT-4o)",
tauri_id: "gpt_image_1",
creator: ModelCreator.OpenAi,
}
}
// This is a hack to get the old model selector code to work since it's keyed off
// of the human-readable label of the model. Long term, that should be fixed.
export const IMAGE_MODELS_BY_LABEL: Record<string, ModelInfo> = {
"Flux Pro 1.1 Ultra": IMAGE_MODELS.flux_pro_1_1_ultra,
"Flux Pro 1.1": IMAGE_MODELS.flux_pro_1_1,
"Flux 1 Dev": IMAGE_MODELS.flux_1_dev,
"Flux 1 Schnell": IMAGE_MODELS.flux_1_schnell,
"GPT Image 1 (GPT-4o)": IMAGE_MODELS.gpt_image_1,
}
@@ -0,0 +1,12 @@
export enum ModelCreator {
BlackForestLabs = "BlackForestLabs",
Bytedance = "Bytedance",
Google = "Google",
Hailuo = "Hailuo",
Kling = "Kling",
Midjourney = "Midjourney",
OpenAi = "OpenAi",
Runway = "Runway",
Stability = "Stability",
}
@@ -0,0 +1,13 @@
import { ModelCreator } from "./ModelCreator.js";
export interface ModelInfo {
// Human-readible name
// Other components may want to rename the model, eg. to fit in smaller width elements
name: string,
// The identifier that Tauri uses (it may send different signals downstream)
tauri_id: string,
// Creator of the model type
creator: ModelCreator,
}
@@ -0,0 +1,41 @@
import { ModelCreator } from "./ModelCreator.js";
import { ModelInfo } from "./ModelInfo.js";
export const VIDEO_MODELS: Record<string, ModelInfo> = {
kling_1_6_pro: {
name: "Kling 1.6 Pro",
tauri_id: "kling_1.6_pro",
creator: ModelCreator.Kling,
},
kling_2_1_pro: {
name: "Kling 2.1 Pro",
tauri_id: "kling_2.1_pro",
creator: ModelCreator.Kling,
},
kling_2_1_master: {
name: "Kling 2.1 Master",
tauri_id: "kling_2.1_master",
creator: ModelCreator.Kling,
},
seedance_1_0_lite: {
name: "Seedance 1.0 Lite",
tauri_id: "seedance_1.0_lite",
creator: ModelCreator.Bytedance,
},
veo_2: {
name: "Google Veo 2",
tauri_id: "veo_2",
creator: ModelCreator.Google,
}
}
// This is a hack to get the old model selector code to work since it's keyed off
// of the human-readable label of the model. Long term, that should be fixed.
export const VIDEO_MODELS_BY_LABEL: Record<string, ModelInfo> = {
"Kling 1.6 Pro": VIDEO_MODELS.kling_1_6_pro,
"Kling 2.1 Pro": VIDEO_MODELS.kling_2_1_pro,
"Kling 2.1 Master": VIDEO_MODELS.kling_2_1_master,
"Seedance 1.0 Lite": VIDEO_MODELS.seedance_1_0_lite,
"Google Veo 2": VIDEO_MODELS.veo_2,
}
@@ -0,0 +1,7 @@
import { modelList } from './model-list.js';
describe('modelList', () => {
it('should work', () => {
expect(modelList()).toEqual('model-list');
})
})
@@ -0,0 +1,3 @@
export function modelList(): string {
return 'model-list';
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
@@ -0,0 +1,35 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"emitDeclarationOnly": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"forceConsistentCasingInFileNames": true,
"types": [
"node",
"vite/client"
]
},
"include": [
"src/**/*.ts"
],
"references": [],
"exclude": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
]
}
@@ -0,0 +1,36 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"module": "nodenext",
"moduleResolution": "nodenext",
"forceConsistentCasingInFileNames": true
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+48
View File
@@ -0,0 +1,48 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/model-list',
plugins: [dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@frontend/model-list',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: []
},
},
test: {
watch: false,
globals: true,
environment: 'node',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+4 -3
View File
@@ -1,18 +1,19 @@
export * from "./lib/common/CommandStatus.js";
export * from "./lib/enqueue/EnqueueTextToImage.js";
export * from "./lib/enqueue/EnqueueImageTo3dObject.js";
export * from "./lib/enqueue/EnqueueTextToImage.js";
export * from "./lib/enqueue/EnqueueImageToVideo.js";
export * from "./lib/fal/FalBackgroundRemoval.js";
export * from "./lib/fal/FalHunyuanImageTo3d.js";
export * from "./lib/fal/FalKlingImageToVideo.js";
export * from "./lib/fal/GetFalApiKey.js";
export * from "./lib/fal/SetFalApiKey.js";
export * from "./lib/openai/SetOpenAIApiKey.js";
export * from "./lib/openai/GetOpenAIApiKey.js";
export * from "./lib/openai/SetOpenAIApiKey.js";
export * from "./lib/settings/GetAppPreferences.js";
export * from "./lib/settings/UpdateAppPreference.js";
export * from "./lib/sora/CheckSoraSession.js";
export * from "./lib/sora/LogoutSoraSession.js";
export * from "./lib/sora/SoraImageRemix.js";
export * from "./lib/sora/useSoraLoginListener.js";
export * from "./lib/system/GetAppInfo.js";
export * from "./lib/sora/waitForSoraLogin.js";
export * from "./lib/system/GetAppInfo.js";
@@ -0,0 +1,49 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
import { ModelInfo } from "@storyteller/model-list";
export enum EnqueueImageToVideoErrorType {
/// Caller didn't specify a model
ModelNotSpecified = "model_not_specified",
/// Generic server error
ServerError = "server_error",
/// No Fal API key available
NeedsFalApiKey = "needs_fal_api_key",
/// Fal had an API error
FalError = "fal_error",
}
export interface EnqueueImageToVideoRequest {
image_media_token?: string;
model?: ModelInfo;
}
export interface EnqueueImageToVideoError extends CommandResult {
error_type: EnqueueImageToVideoErrorType;
error_message?: string;
}
export interface EnqueueImageToVideoPayload {
media_token: string;
cdn_url: string;
base64_bytes: string;
}
export interface EnqueueImageToVideoSuccess extends CommandResult {
payload: EnqueueImageToVideoPayload;
}
export type EnqueueImageToVideoResult = EnqueueImageToVideoSuccess | EnqueueImageToVideoError;
export const EnqueueImageToVideo = async (request: EnqueueImageToVideoRequest) : Promise<EnqueueImageToVideoResult> => {
const modelName = request.model?.tauri_id;
const result = await invoke("enqueue_image_to_video_command", {
request: {
image_media_token: request.image_media_token,
model: modelName,
}
});
return (result as EnqueueImageToVideoResult);
}
@@ -1,5 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
import { ModelInfo } from "@storyteller/model-list";
export enum EnqueueTextToImageErrorType {
/// Caller didn't specify a model
@@ -14,7 +15,7 @@ export enum EnqueueTextToImageErrorType {
export interface EnqueueTextToImageRequest {
prompt?: string;
model?: EnqueueTextToImageModel;
model?: ModelInfo | EnqueueTextToImageModel;
}
export enum EnqueueTextToImageModel {
@@ -41,11 +42,22 @@ export interface EnqueueTextToImageSuccess extends CommandResult {
export type EnqueueTextToImageResult = EnqueueTextToImageSuccess | EnqueueTextToImageError;
export const EnqueueTextToImage = async (request: EnqueueTextToImageRequest) : Promise<EnqueueTextToImageResult> => {
let result = await invoke("enqueue_text_to_image_command", {
let modelName = undefined;
if (!!request.model) {
if (typeof request.model === "string") {
modelName = request.model;
} else {
modelName = request.model.tauri_id;
}
}
const result = await invoke("enqueue_text_to_image_command", {
request: {
prompt: request.prompt,
model: request.model,
model: modelName,
}
});
return (result as EnqueueTextToImageResult);
}
+3
View File
@@ -7,6 +7,9 @@
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "../model-list"
}
],
"extends": "../../tsconfig.base.json"
@@ -44,5 +44,10 @@
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
],
"references": [
{
"path": "../model-list/tsconfig.lib.json"
}
]
}
+60
View File
@@ -13,6 +13,9 @@
"libs/**/*"
],
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^2.2.2",
"@preact/signals-react": "^3.0.1",
"@remix-run/serve": "^2.15.0",
@@ -1522,6 +1525,10 @@
"name": "@frontend/login",
"version": "0.0.1"
},
"libs/model-list": {
"name": "@storyteller/model-list",
"version": "0.0.1"
},
"libs/soundboard": {
"name": "@storyteller/soundboard",
"version": "0.0.1",
@@ -3428,6 +3435,55 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.0.tgz",
@@ -9649,6 +9705,10 @@
"resolved": "libs/common",
"link": true
},
"node_modules/@storyteller/model-list": {
"resolved": "libs/model-list",
"link": true
},
"node_modules/@storyteller/soundboard": {
"resolved": "libs/soundboard",
"link": true
+3
View File
@@ -5,6 +5,9 @@
"scripts": {},
"private": true,
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^2.2.2",
"@preact/signals-react": "^3.0.1",
"@remix-run/serve": "^2.15.0",
+3
View File
@@ -143,6 +143,9 @@
},
{
"path": "./libs/components/pricing-modal"
},
{
"path": "./libs/model-list"
}
]
}