mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 13:02:37 -05:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f767ef655 |
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = 'cancelled_by_system',\n retry_at = NULL,\n internal_debugging_failure_reason = 'reap_stale_fakeyou_jobs'\nWHERE\n job_type != 'fal_queue'\n AND status IN (\n 'pending',\n 'started',\n 'attempt_failed'\n )\n AND created_at > NOW() - INTERVAL 30 MINUTE\nLIMIT 5000\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "19708db99f2b3c6283d2115a511e0eb1c99fab396ad0effd9eb2dd79926e9c93"
|
||||
}
|
||||
@@ -21,8 +21,6 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- The new enum for the type of job, which will eventually replace `inference_category` and `maybe_model_type`.
|
||||
-- This isn't fully supported yet in the inference-job, but we'll start populating it and adding logic around it.
|
||||
-- We'll start this out as nullable, then migrate the old rows with a default column value.
|
||||
--
|
||||
-- Fal jobs have the type `fal_queue`.
|
||||
job_type VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- There is an index on this column.
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
use futures::TryFutureExt;
|
||||
use log::{error, info};
|
||||
use reqwest::Client;
|
||||
use tauri::ipc::Response;
|
||||
|
||||
/// Request a URL and load it without worrying about CORS.
|
||||
#[tauri::command]
|
||||
pub async fn load_without_cors_command(url: String) -> Result<Response, String> {
|
||||
|
||||
info!("load_without_cors_command for URL: {}", url);
|
||||
|
||||
// TODO(bt,2025-07-22): Make sure the URL is valid and safe to fetch. Check an allowlist.
|
||||
|
||||
// TODO(bt,2025-07-22): Should we cache clients so we don't need to SSL handshake every time?
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
error!("Failed to create HTTP client: {:?}", e);
|
||||
format!("Failed to create HTTP client: {:?}", e)
|
||||
})?;
|
||||
|
||||
let response = client.get(url.clone())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to send HTTP request: {:?}", e);
|
||||
format!("Failed to send HTTP request: {:?}", e)
|
||||
})?;
|
||||
|
||||
if response.status() != 200 {
|
||||
return Err(format!("Failed to fetch URL: {}. Status: {}", url, response.status()));
|
||||
}
|
||||
|
||||
let bytes = response.bytes()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to read response bytes: {:?}", e);
|
||||
format!("Failed to read response bytes: {:?}", e)
|
||||
})?;
|
||||
|
||||
Ok(Response::new(bytes.to_vec()))
|
||||
}
|
||||
@@ -2,7 +2,6 @@ pub mod app_preferences;
|
||||
pub mod enqueue;
|
||||
pub mod flip_image;
|
||||
pub mod get_app_info_command;
|
||||
pub mod load_without_cors_command;
|
||||
pub mod platform_info_command;
|
||||
pub mod providers;
|
||||
pub mod response;
|
||||
@@ -12,7 +12,6 @@ use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::
|
||||
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_command;
|
||||
use crate::core::commands::flip_image::flip_image;
|
||||
use crate::core::commands::get_app_info_command::get_app_info_command;
|
||||
use crate::core::commands::load_without_cors_command::load_without_cors_command;
|
||||
use crate::core::commands::platform_info_command::platform_info_command;
|
||||
use crate::core::commands::providers::get_provider_order_command::get_provider_order_command;
|
||||
use crate::core::commands::providers::set_provider_order_command::set_provider_order_command;
|
||||
@@ -155,7 +154,6 @@ pub fn run() {
|
||||
get_app_preferences_command,
|
||||
get_fal_api_key_command,
|
||||
get_provider_order_command,
|
||||
load_without_cors_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
set_fal_api_key_command,
|
||||
|
||||
-14
@@ -1,4 +1,3 @@
|
||||
use log::{debug, warn};
|
||||
use crate::payloads::generic_inference_args::inner_payloads::f5_tts_payload::F5TTSPayload;
|
||||
use crate::payloads::generic_inference_args::inner_payloads::face_fusion_payload::FaceFusionPayload;
|
||||
use crate::payloads::generic_inference_args::inner_payloads::gptsovits_payload::GptSovitsPayload;
|
||||
@@ -181,19 +180,6 @@ impl GenericInferenceArgs {
|
||||
Ok(serde_json::from_str(json)?)
|
||||
}
|
||||
|
||||
pub fn from_json_optional(json: Option<&str>) -> AnyhowResult<Option<Self>> {
|
||||
match json {
|
||||
None => Ok(None),
|
||||
Some("" | "null" | "NULL" | "undefined" | "UNDEFINED") => {
|
||||
// NB: Something inserted a few of these records (only in local dev).
|
||||
// It's better not to stall the job queue than to assert correctness.
|
||||
warn!("Received empty or null JSON `{json:?}` for GenericInferenceArgs, returning None.");
|
||||
Ok(None)
|
||||
},
|
||||
Some(json_str) => Ok(Some(Self::from_json(json_str)?)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> AnyhowResult<String> {
|
||||
Ok(serde_json::to_string(self)?)
|
||||
}
|
||||
|
||||
+4
-3
@@ -2,7 +2,6 @@ use std::collections::BTreeSet;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::Utc;
|
||||
use log::debug;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
@@ -152,8 +151,10 @@ pub async fn list_available_generic_inference_jobs(
|
||||
.map(|token_type| InferenceInputSourceTokenType::from_str(token_type)
|
||||
.map_err(|err| anyhow!("error: {:?}", err)))
|
||||
.transpose()?,
|
||||
maybe_inference_args: GenericInferenceArgs::from_json_optional(
|
||||
record.maybe_inference_args.as_deref())?,
|
||||
maybe_inference_args: record.maybe_inference_args
|
||||
.as_deref()
|
||||
.map(|args| GenericInferenceArgs::from_json(args))
|
||||
.transpose()?,
|
||||
maybe_raw_inference_text: record.maybe_raw_inference_text,
|
||||
status: JobStatusPlus::from_str(&record.status)
|
||||
.map_err(|err| anyhow!("JobStatus failure to parse: {:?}", err))?,
|
||||
|
||||
@@ -5,4 +5,3 @@ pub mod mark_generic_inference_job_completely_failed;
|
||||
pub mod mark_generic_inference_job_failure;
|
||||
pub mod mark_generic_inference_job_pending_and_grab_lock;
|
||||
pub mod mark_generic_inference_job_successfully_done;
|
||||
pub mod reap_stale_fakeyou_jobs;
|
||||
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use log::info;
|
||||
use sqlx;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{MySql, MySqlPool};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// Kill FakeYou jobs that are "stale".
|
||||
///
|
||||
/// - This leaves Fal jobs alone, as they do not use the job runner.
|
||||
/// - This kills jobs that are over a threshold (e.g. an hour) overdue.
|
||||
pub async fn reap_stale_fakeyou_jobs(mysql_pool: &MySqlPool) -> AnyhowResult<()>
|
||||
{
|
||||
let mut connection = mysql_pool.acquire().await?;
|
||||
reap_stale_fakeyou_jobs_using_connection(&mut connection).await
|
||||
}
|
||||
|
||||
pub async fn reap_stale_fakeyou_jobs_using_connection(
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
) -> AnyhowResult<()> {
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE generic_inference_jobs
|
||||
SET
|
||||
status = 'cancelled_by_system',
|
||||
retry_at = NULL,
|
||||
internal_debugging_failure_reason = 'reap_stale_fakeyou_jobs'
|
||||
WHERE
|
||||
job_type != 'fal_queue'
|
||||
AND status IN (
|
||||
'pending',
|
||||
'started',
|
||||
'attempt_failed'
|
||||
)
|
||||
AND created_at > NOW() - INTERVAL 30 MINUTE
|
||||
LIMIT 5000
|
||||
"#
|
||||
)
|
||||
.execute(&mut **mysql_connection)
|
||||
.await;
|
||||
|
||||
match query_result {
|
||||
Ok(result) => {
|
||||
info!("Reaped stale FakeYou jobs: {} rows affected.", result.rows_affected());
|
||||
Ok(())
|
||||
},
|
||||
Err(err) => {
|
||||
Err(anyhow!("error with reaping stale jobs: {:?}", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,6 @@ pub fn run_http_server(args: CreateServerArgs) -> AnyhowResult<Server>
|
||||
// NB: We shouldn't be logging much as the /_status endpoint is all we aim to expose.
|
||||
let log_format = "[%{HOSTNAME}e] IP=[%{X-Forwarded-For}i] \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T";
|
||||
|
||||
info!("HTTP server will bind to: {}", bind_address);
|
||||
|
||||
let handle = HttpServer::new(move || {
|
||||
// NB: app_data being clone()'d below should all be safe (dependencies included)
|
||||
App::new()
|
||||
|
||||
@@ -53,7 +53,6 @@ use crate::state::job_dependencies::{BucketDependencies, ClientDependencies, Dat
|
||||
use crate::state::job_specific_dependencies::JobSpecificDependencies;
|
||||
use crate::state::scoped_job_type_execution::ScopedJobTypeExecution;
|
||||
use crate::state::scoped_model_type_execution::ScopedModelTypeExecution;
|
||||
use crate::threads::reap_jobs_thread::reap_jobs_thread;
|
||||
use crate::util::filesystem::scoped_temp_dir_creator::ScopedTempDirCreator;
|
||||
use crate::util::instrumentation::JobInstruments;
|
||||
use crate::util::instrumentation::{init_otel_metrics_pipeline, JobInstrumentLabels};
|
||||
@@ -62,7 +61,6 @@ use crate::util::model_weights_cache::model_weights_cache_directory::ModelWeight
|
||||
pub mod http_server;
|
||||
pub mod job;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod util;
|
||||
|
||||
// Buckets (shared config)
|
||||
@@ -165,8 +163,6 @@ async fn main() -> AnyhowResult<()> {
|
||||
.connect(&db_connection_string)
|
||||
.await?;
|
||||
|
||||
let mysql_pool_2 = mysql_pool.clone();
|
||||
|
||||
info!("Connected to MySQL.");
|
||||
|
||||
let common_env = CommonEnv::read_from_env()?;
|
||||
@@ -349,14 +345,6 @@ async fn main() -> AnyhowResult<()> {
|
||||
|
||||
set_up_directories(&job_dependencies)?;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let actix_runtime = actix_web::rt::System::new();
|
||||
|
||||
actix_runtime.block_on(reap_jobs_thread(mysql_pool_2));
|
||||
|
||||
warn!("Reap jobs thread terminated");
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let actix_runtime = actix_web::rt::System::new();
|
||||
let http_server_handle = launch_http_server(create_server_args);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod reap_jobs_thread;
|
||||
@@ -1,33 +0,0 @@
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use mysql_queries::queries::generic_inference::job::reap_stale_fakeyou_jobs::reap_stale_fakeyou_jobs;
|
||||
use sqlx::MySqlPool;
|
||||
use std::time::Duration;
|
||||
|
||||
const WAIT_BETWEEN_REAPING_SECONDS: u64 = 60 * 2; // 2 minutes
|
||||
|
||||
pub async fn reap_jobs_thread(
|
||||
mysql_pool: MySqlPool,
|
||||
) -> ! {
|
||||
info!("Database job reaping thread starting...");
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(WAIT_BETWEEN_REAPING_SECONDS)).await;
|
||||
|
||||
info!("Reaping jobs from database...");
|
||||
|
||||
let result = do_reap_jobs(&mysql_pool).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Error reaping jobs: {:?}", err);
|
||||
tokio::time::sleep(Duration::from_secs(300)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn do_reap_jobs(
|
||||
mysql_pool: &MySqlPool,
|
||||
) -> AnyhowResult<()> {
|
||||
reap_stale_fakeyou_jobs(mysql_pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -14,15 +14,6 @@
|
||||
gtag('config', 'G-Y4Y8KMLPQ7');
|
||||
</script>
|
||||
|
||||
<!-- Microsoft Clarity -->
|
||||
<script type="text/javascript">
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "sixp9uu263");
|
||||
</script>
|
||||
|
||||
<base href="/" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Flux</title><path d="M0 20.683L12.01 2.5 24 20.683h-2.233L12.009 5.878 3.471 18.806h12.122l1.239 1.877H0z"></path><path d="M8.069 16.724l2.073-3.115 2.074 3.115H8.069zM18.24 20.683l-5.668-8.707h2.177l5.686 8.707h-2.196zM19.74 11.676l2.13-3.19 2.13 3.19h-4.26z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 438 B |
@@ -1 +0,0 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ByteDance</title><path d="M14.944 18.587l-1.704-.445V10.01l1.824-.462c1-.254 1.84-.461 1.88-.453.032 0 .056 2.235.056 4.972v4.973l-.176-.008c-.104 0-.952-.207-1.88-.446z"></path><path d="M7 16.542c0-2.736.024-4.98.064-4.98.032-.008.872.2 1.88.454l1.816.461-.016 4.05-.024 4.049-1.632.422c-.896.23-1.736.445-1.856.469L7 21.523v-4.98z"></path><path d="M19.24 12.477c0-9.03.008-9.515.144-9.475.072.024.784.207 1.576.406.792.207 1.576.405 1.744.445l.296.08-.016 8.56-.024 8.568-1.624.414c-.888.23-1.728.437-1.856.47l-.24.055v-9.523z"></path><path d="M1 12.509c0-4.678.024-8.505.064-8.505.032 0 .872.207 1.872.454l1.824.461v7.582c0 4.16-.016 7.574-.032 7.574-.024 0-.872.215-1.88.47L1 21.013v-8.505z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 873 B |
@@ -1 +0,0 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 651 B |
@@ -1 +0,0 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kling</title><path clip-rule="evenodd" d="M5.493 21.234c-1.112-1.451-1.109-4.263-.081-7.459l-4.557-2.63a1.683 1.683 0 01-.85-1.304 1.505 1.505 0 01.08-.622 13.18 13.18 0 011.037-2.255c3.476-6.02 10.916-8.23 16.619-4.938.46.266.82.67 1.081 1.184.785 1.545.685 4.096-.234 6.954l4.557 2.631c.339.196.596.492.736.832a1.53 1.53 0 01.034 1.093 13.146 13.146 0 01-1.037 2.255c-3.476 6.02-10.916 8.23-16.619 4.938a2.6 2.6 0 01-.766-.68zm11.096-6.615c-2.073 3.591-5.808 5.316-8.343 3.852-1.267-.731-1.994-2.122-2.145-3.778-.095-1.035.036-2.173.4-3.32.217-.684.517-1.37.902-2.039l.008-.014c2.073-3.59 5.808-5.315 8.343-3.852.633.366 1.13.895 1.49 1.54.986 1.772.922 4.415-.285 6.914-.111.23-.232.457-.362.683l-.008.014z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 888 B |
@@ -1 +0,0 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Midjourney</title><path d="M22.369 17.676c-1.387 1.259-3.17 2.378-5.332 3.417.044.03.086.057.13.083l.018.01.019.012c.216.123.42.184.641.184.222 0 .426-.061.642-.184l.018-.011.019-.011c.14-.084.266-.178.492-.366l.178-.148c.279-.232.426-.342.625-.456.304-.174.612-.266.949-.266.337 0 .645.092.949.266l.023.014c.188.109.334.219.602.442l.178.148c.221.184.346.278.483.36l.028.017.018.01c.21.12.407.181.62.185h.022a.31.31 0 110 .618c-.337 0-.645-.092-.95-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.02-.014a5.356 5.356 0 01-.49-.377l-.159-.132a3.836 3.836 0 00-.483-.36l-.027-.017-.019-.01a1.256 1.256 0 00-.641-.185c-.222 0-.426.061-.641.184l-.02.011-.018.011c-.14.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.51.39l-.022.014-.022.014-.09.054a1.868 1.868 0 01-.95.266c-.337 0-.644-.092-.949-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.026-.017a4.881 4.881 0 01-.425-.325.308.308 0 01-.12-.1l-.098-.081a3.836 3.836 0 00-.483-.36l-.027-.017-.019-.01a1.256 1.256 0 00-.641-.185c-.222 0-.426.061-.642.184l-.018.011-.019.011c-.14.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.51.39l-.023.014-.022.014-.09.054A1.868 1.868 0 0112 22c-.337 0-.645-.092-.949-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.021-.014a5.356 5.356 0 01-.49-.377l-.158-.132a3.836 3.836 0 00-.483-.36l-.028-.017-.018-.01a1.256 1.256 0 00-.642-.185c-.221 0-.425.061-.641.184l-.019.011-.018.011c-.141.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.511.39l-.022.014-.022.014-.09.054a1.868 1.868 0 01-.986.264c-.746-.09-1.319-.38-1.89-.866l-.035-.03c-.047-.041-.118-.106-.192-.174l-.196-.181-.107-.1-.011-.01a1.531 1.531 0 00-.336-.253.313.313 0 00-.095-.03h-.005c-.119.022-.238.059-.361.11a.308.308 0 01-.077.061l-.008.005a.309.309 0 01-.126.034 5.66 5.66 0 00-.774.518l-.416.324-.055.043a6.542 6.542 0 01-.324.236c-.305.207-.552.315-.8.315a.31.31 0 01-.01-.618h.01c.09 0 .235-.062.438-.198l.04-.027c.077-.054.163-.117.27-.199l.385-.301.06-.047c.268-.206.506-.373.73-.505l-.633-1.21a.309.309 0 01.254-.451l20.287-1.305a.309.309 0 01.228.537zm-1.118.14L2.369 19.03l.423.809c.128-.045.256-.078.388-.1a.31.31 0 01.052-.005c.132 0 .26.032.386.093.153.073.294.179.483.35l.016.015.092.086.144.134.097.089c.065.06.125.114.16.144.485.418.948.658 1.554.736h.011a1.25 1.25 0 00.6-.172l.021-.011.019-.011.018-.011c.141-.084.266-.178.492-.366l.178-.148c.279-.232.426-.342.625-.456.305-.174.612-.266.95-.266.336 0 .644.092.948.266l.023.014c.188.109.335.219.603.442l.177.148c.222.184.346.278.484.36l.027.017.019.01c.215.124.42.185.641.185.222 0 .426-.061.641-.184l.019-.011.018-.011c.141-.084.267-.178.493-.366l.177-.148c.28-.232.427-.342.626-.456.304-.174.612-.266.949-.266.337 0 .644.092.949.266l.025.015c.187.109.334.22.603.443 1.867-.878 3.448-1.811 4.73-2.832l.02-.016zM3.653 2.026C6.073 3.06 8.69 4.941 10.8 7.258c2.46 2.7 4.109 5.828 4.637 9.149a.31.31 0 01-.421.335c-2.348-.945-4.54-1.258-6.59-1.02-1.739.2-3.337.792-4.816 1.703-.294.182-.62-.182-.405-.454 1.856-2.355 2.581-4.99 2.343-7.794-.195-2.292-1.031-4.61-2.284-6.709a.31.31 0 01.388-.442zM10.04 4.45c1.778.543 3.892 2.102 5.782 4.243 1.984 2.248 3.552 4.934 4.347 7.582a.31.31 0 01-.401.38l-.022-.01-.386-.154a10.594 10.594 0 00-.291-.112l-.016-.006c-.68-.247-1.199-.291-1.944-.101a.31.31 0 01-.375-.218C15.378 11.123 13.073 7.276 9.775 5c-.291-.201-.072-.653.266-.55zM4.273 2.996l.008.015c1.028 1.94 1.708 4.031 1.885 6.113.213 2.513-.31 4.906-1.673 7.092l-.02.031.003-.001c1.198-.581 2.47-.969 3.825-1.132l.055-.006c1.981-.23 4.083.029 6.309.837l.066.025-.007-.039c-.593-2.95-2.108-5.737-4.31-8.179l-.07-.078c-1.785-1.96-3.944-3.6-6.014-4.65l-.057-.028zm7.92 3.238l.048.048c2.237 2.295 3.885 5.431 4.974 9.191l.038.132.022-.004c.71-.133 1.284-.063 1.963.18l.027.01.066.024.046.018-.025-.073c-.811-2.307-2.208-4.62-3.936-6.594l-.058-.065c-1.02-1.155-2.103-2.132-3.15-2.856l-.015-.011z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,6 +1,8 @@
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { ListDropdown } from "@storyteller/ui-list-dropdown";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
import { Label } from "@storyteller/ui-label";
|
||||
import { FileUploader } from "@storyteller/ui-file-uploader";
|
||||
import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "./utilities";
|
||||
import { upload3DObjects } from "./utilities/upload3DObjects";
|
||||
@@ -46,6 +48,13 @@ export const UploadFiles3D = ({
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const [uploadTitle, setUploadTitle] = useState<{
|
||||
value: string;
|
||||
error?: string;
|
||||
}>({
|
||||
value: "",
|
||||
});
|
||||
|
||||
const [assetFile, setAssetFile] = useState<{
|
||||
value: File | null;
|
||||
error?: string;
|
||||
@@ -61,6 +70,14 @@ export const UploadFiles3D = ({
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!uploadTitle.value) {
|
||||
setUploadTitle((curr) => ({
|
||||
...curr,
|
||||
error: "Please enter a title.",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assetFile.value) {
|
||||
setAssetFile((curr) => ({
|
||||
...curr,
|
||||
@@ -69,11 +86,8 @@ export const UploadFiles3D = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the filename as the title
|
||||
const title = assetFile.value.name.split(".")[0];
|
||||
|
||||
upload3DObjects({
|
||||
title: title,
|
||||
title: uploadTitle.value,
|
||||
assetFile: assetFile.value,
|
||||
thumbnailSnapshot: thumbnailFile,
|
||||
engineCategory: engineCategory,
|
||||
@@ -109,7 +123,18 @@ export const UploadFiles3D = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="mb-4 flex flex-col gap-4">
|
||||
<div className="flex flex-col">
|
||||
<Label required>Asset Name</Label>
|
||||
<Input
|
||||
placeholder="Enter the asset name here"
|
||||
errorMessage={uploadTitle.error}
|
||||
value={uploadTitle.value}
|
||||
onChange={(event) => setUploadTitle({ value: event.target.value })}
|
||||
className={uploadTitle.error ? "mb-3" : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FileUploader
|
||||
fileTypes={fileTypes}
|
||||
file={assetFile.value ?? undefined}
|
||||
@@ -158,7 +183,7 @@ export const UploadFiles3D = ({
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!assetFile.value}
|
||||
disabled={!uploadTitle.value || !assetFile.value}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
|
||||
+89
-15
@@ -1,19 +1,19 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoadingDots } from "@storyteller/ui-loading";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { Label } from "@storyteller/ui-label";
|
||||
import { UploadAssetError, UploadSuccess } from "@storyteller/ui-upload-modal";
|
||||
import { UploadFiles3D } from "./UploadFiles3D";
|
||||
import { initialUploaderState, UploaderState } from "../../../models";
|
||||
import { Select } from "@storyteller/ui-select";
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
UploaderStates,
|
||||
OBJECT_FILE_TYPE,
|
||||
CHARACTER_MIXAMO_FILE_TYPE,
|
||||
IMAGEPLANE_FILE_TYPE,
|
||||
} from "../../../enums";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
galleryModalVisibleViewMode,
|
||||
galleryModalVisibleDuringDrag,
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -26,17 +26,63 @@ interface Props {
|
||||
hasLength?: boolean;
|
||||
hasThumbnailUpload?: boolean;
|
||||
};
|
||||
preselectedCategory?: FilterEngineCategories;
|
||||
isSelectVisible: boolean;
|
||||
}
|
||||
|
||||
const objectFileTypes = Object.values(OBJECT_FILE_TYPE);
|
||||
// Map categories to their allowed file types
|
||||
const categoryFileTypes: Partial<Record<FilterEngineCategories, string[]>> = {
|
||||
[FilterEngineCategories.OBJECT]: Object.values(OBJECT_FILE_TYPE),
|
||||
[FilterEngineCategories.CHARACTER]: [
|
||||
...Object.values(CHARACTER_MIXAMO_FILE_TYPE),
|
||||
],
|
||||
[FilterEngineCategories.IMAGE_PLANE]: Object.values(IMAGEPLANE_FILE_TYPE),
|
||||
[FilterEngineCategories.LOCATION]: Object.values(OBJECT_FILE_TYPE),
|
||||
[FilterEngineCategories.CREATURE]: Object.values(OBJECT_FILE_TYPE),
|
||||
};
|
||||
// Excluded category options for the upload modal
|
||||
const categoryOptions = Object.entries(FilterEngineCategories)
|
||||
.filter(
|
||||
([, value]) =>
|
||||
![
|
||||
"audio",
|
||||
"video_plane",
|
||||
"skybox",
|
||||
"set_dressing",
|
||||
"expression",
|
||||
"animation",
|
||||
"image_plane",
|
||||
"scene",
|
||||
].includes(value),
|
||||
)
|
||||
.sort(([, a], [, b]) => (a === "object" ? -1 : b === "object" ? 1 : 0))
|
||||
.map(([key, value]) => ({
|
||||
// This is because sets is location i guess - BFlat
|
||||
label:
|
||||
key === "LOCATION"
|
||||
? "Set"
|
||||
: key.charAt(0) + key.slice(1).toLowerCase().replace(/_/g, " "),
|
||||
value: value,
|
||||
}));
|
||||
|
||||
export function UploadModal3D(props: Props) {
|
||||
const { isOpen, onClose, onSuccess, title, titleIcon, options } = props;
|
||||
export function UploadModal3D({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
title,
|
||||
titleIcon,
|
||||
options,
|
||||
preselectedCategory,
|
||||
isSelectVisible,
|
||||
}: Props & { preselectedCategory?: FilterEngineCategories }) {
|
||||
const [uploaderState, setUploaderState] =
|
||||
useState<UploaderState>(initialUploaderState);
|
||||
|
||||
// Category fixed to OBJECT
|
||||
const selectedCategory = FilterEngineCategories.OBJECT;
|
||||
const [selectedCategory, setSelectedCategory] =
|
||||
useState<FilterEngineCategories>(
|
||||
preselectedCategory !== undefined
|
||||
? preselectedCategory
|
||||
: FilterEngineCategories.OBJECT,
|
||||
);
|
||||
|
||||
const updateUploaderState = (newState: UploaderState) => {
|
||||
setUploaderState(newState);
|
||||
@@ -53,11 +99,18 @@ export function UploadModal3D(props: Props) {
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploaderState.status === UploaderStates.success) {
|
||||
// Automatically open the global Gallery modal after a successful upload
|
||||
galleryModalVisibleViewMode.value = true;
|
||||
galleryModalVisibleDuringDrag.value = true;
|
||||
if (preselectedCategory !== undefined) {
|
||||
setSelectedCategory(preselectedCategory);
|
||||
}
|
||||
}, [preselectedCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Preselected category:", preselectedCategory);
|
||||
console.log("Initial selected category:", selectedCategory);
|
||||
}, [preselectedCategory, selectedCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploaderState.status === UploaderStates.success) {
|
||||
onSuccess(selectedCategory);
|
||||
onClose();
|
||||
}
|
||||
@@ -69,10 +122,31 @@ export function UploadModal3D(props: Props) {
|
||||
case UploaderStates.ready:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isSelectVisible && (
|
||||
<div className="flex w-full flex-col">
|
||||
<Label className="mb-1">Category</Label>
|
||||
<Select
|
||||
id="category-select"
|
||||
options={categoryOptions}
|
||||
value={selectedCategory}
|
||||
onChange={(value) => {
|
||||
if (
|
||||
typeof value === "string" &&
|
||||
Object.values(FilterEngineCategories).includes(
|
||||
value as FilterEngineCategories,
|
||||
)
|
||||
) {
|
||||
setSelectedCategory(value as FilterEngineCategories);
|
||||
}
|
||||
}}
|
||||
placeholder="Select a category"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<UploadFiles3D
|
||||
title={title}
|
||||
engineCategory={selectedCategory}
|
||||
fileTypes={objectFileTypes}
|
||||
fileTypes={categoryFileTypes[selectedCategory] || []}
|
||||
options={options}
|
||||
onClose={onClose}
|
||||
onUploadProgress={updateUploaderState}
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { FileUploader } from "@storyteller/ui-file-uploader";
|
||||
import { uploadImage } from "./utilities/uploadImage";
|
||||
import { UploaderState } from "../../../models";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
fileTypes: string[];
|
||||
onClose: () => void;
|
||||
onUploadProgress: (newState: UploaderState) => void;
|
||||
}
|
||||
|
||||
export const UploadFilesImage = ({
|
||||
fileTypes,
|
||||
onClose,
|
||||
onUploadProgress,
|
||||
}: Props) => {
|
||||
const [assetFile, setAssetFile] = useState<{
|
||||
value: File | null;
|
||||
error?: string;
|
||||
}>({ value: null });
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!assetFile.value) {
|
||||
setAssetFile((curr) => ({
|
||||
...curr,
|
||||
error: "Please select an image to upload.",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the filename as the title
|
||||
const title = assetFile.value.name.split(".")[0];
|
||||
|
||||
uploadImage({
|
||||
title: title,
|
||||
assetFile: assetFile.value,
|
||||
progressCallback: onUploadProgress,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<FileUploader
|
||||
fileTypes={fileTypes}
|
||||
file={assetFile.value ?? undefined}
|
||||
handleChange={(file: File) => {
|
||||
setAssetFile({
|
||||
value: file,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{assetFile.error && (
|
||||
<h6 className="z-10 text-red">{assetFile.error}</h6>
|
||||
)}
|
||||
|
||||
{assetFile.value && (
|
||||
<div className="relative m-auto flex aspect-square w-full items-center justify-center overflow-hidden rounded-lg bg-brand-secondary">
|
||||
<img
|
||||
alt="Preview"
|
||||
className="m-auto max-h-full max-w-full object-contain"
|
||||
src={URL.createObjectURL(assetFile.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSubmit}>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-134
@@ -1,134 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoadingDots } from "@storyteller/ui-loading";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { UploadAssetError, UploadSuccess } from "@storyteller/ui-upload-modal";
|
||||
import { UploadFilesImage } from "./UploadFilesImage";
|
||||
import { initialUploaderState, UploaderState } from "../../../models";
|
||||
import {
|
||||
FilterEngineCategories,
|
||||
UploaderStates,
|
||||
IMAGEPLANE_FILE_TYPE,
|
||||
} from "../../../enums";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
galleryModalVisibleViewMode,
|
||||
galleryModalVisibleDuringDrag,
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onSuccess: (category: FilterEngineCategories) => void;
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
titleIcon: IconDefinition;
|
||||
}
|
||||
|
||||
const imageFileTypes = Object.values(IMAGEPLANE_FILE_TYPE);
|
||||
|
||||
export function UploadModalImage(props: Props) {
|
||||
const { isOpen, onClose, onSuccess, title, titleIcon } = props;
|
||||
const [uploaderState, setUploaderState] =
|
||||
useState<UploaderState>(initialUploaderState);
|
||||
|
||||
// Category fixed to IMAGE_PLANE
|
||||
const selectedCategory = FilterEngineCategories.IMAGE_PLANE;
|
||||
|
||||
const updateUploaderState = (newState: UploaderState) => {
|
||||
setUploaderState(newState);
|
||||
};
|
||||
|
||||
const resetModalState = () => {
|
||||
setUploaderState(initialUploaderState);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
resetModalState();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploaderState.status === UploaderStates.success) {
|
||||
// Automatically open the global Gallery modal after a successful upload
|
||||
galleryModalVisibleViewMode.value = true;
|
||||
galleryModalVisibleDuringDrag.value = true;
|
||||
|
||||
onSuccess(selectedCategory);
|
||||
onClose();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [uploaderState.status]);
|
||||
|
||||
const UploaderModalContent = () => {
|
||||
switch (uploaderState.status) {
|
||||
case UploaderStates.ready:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<UploadFilesImage
|
||||
title={title}
|
||||
fileTypes={imageFileTypes}
|
||||
onClose={onClose}
|
||||
onUploadProgress={updateUploaderState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case UploaderStates.uploadingAsset:
|
||||
case UploaderStates.uploadingCover:
|
||||
case UploaderStates.settingCover:
|
||||
return (
|
||||
<>
|
||||
<LoadingDots className="mb-1 bg-transparent" />
|
||||
<div className="w-100 text-center opacity-50">Uploading...</div>
|
||||
</>
|
||||
);
|
||||
case UploaderStates.success: {
|
||||
return (
|
||||
<UploadSuccess
|
||||
title="Image"
|
||||
onOk={() => {
|
||||
onClose();
|
||||
onSuccess(selectedCategory);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case UploaderStates.assetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
onRetry={() => {
|
||||
resetModalState();
|
||||
}}
|
||||
type={selectedCategory}
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
case UploaderStates.coverCreateError:
|
||||
case UploaderStates.coverSetError:
|
||||
return (
|
||||
<UploadAssetError
|
||||
onCancel={onClose}
|
||||
onRetry={() => {
|
||||
resetModalState();
|
||||
}}
|
||||
type={"Thumbnail"}
|
||||
errorMessage={uploaderState.errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
titleIcon={titleIcon}
|
||||
title={title}
|
||||
className="max-w-xl"
|
||||
showClose={true}
|
||||
>
|
||||
<UploaderModalContent />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { UploadModalImage } from "./UploadModalImage";
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { MediaUploadApi } from "../../../../Classes/ApiManager";
|
||||
import { UploaderStates } from "../../../../enums";
|
||||
import { UploaderState } from "../../../../models";
|
||||
import { getFileName } from "../../../../utilities";
|
||||
import { EIntermediateFile } from "~/enums/EIntermediateFile";
|
||||
|
||||
export const uploadImage = async ({
|
||||
title,
|
||||
assetFile,
|
||||
progressCallback,
|
||||
}: {
|
||||
title: string;
|
||||
assetFile: File;
|
||||
progressCallback: (newState: UploaderState) => void;
|
||||
}) => {
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
|
||||
console.log("Uploading image plane:", assetFile);
|
||||
|
||||
progressCallback({ status: UploaderStates.uploadingAsset });
|
||||
|
||||
const assetResponse = await mediaUploadApi.UploadImage({
|
||||
blob: assetFile,
|
||||
fileName: getFileName(assetFile),
|
||||
uuid: uuidv4(),
|
||||
maybe_title: title,
|
||||
is_intermediate_system_file: EIntermediateFile.false,
|
||||
});
|
||||
|
||||
console.log("Image plane response:", assetResponse);
|
||||
|
||||
if (assetResponse == undefined) {
|
||||
console.log("Error: Could not upload image plane!");
|
||||
progressCallback({
|
||||
status: UploaderStates.assetError,
|
||||
errorMessage: "Could not upload image plane!",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assetResponse.success || !assetResponse.data) {
|
||||
console.log("Error:", assetResponse.errorMessage);
|
||||
progressCallback({
|
||||
status: UploaderStates.assetError,
|
||||
errorMessage: assetResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Upload successful:", assetResponse.data);
|
||||
progressCallback({
|
||||
status: UploaderStates.success,
|
||||
data: assetResponse.data,
|
||||
});
|
||||
};
|
||||
@@ -37,8 +37,6 @@ import {
|
||||
} from "~/pages/PageEnigma/Editor/editor";
|
||||
// import { usePricingModalStore } from "@storyteller/ui-pricing-modal"; - Uncomment for pricing modal - BFlat
|
||||
import toast from "react-hot-toast";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
|
||||
interface Props {
|
||||
pageName: string;
|
||||
loginSignUpPressed: () => void;
|
||||
@@ -86,7 +84,6 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
const handleOpenGalleryModal = () => {
|
||||
galleryModalVisibleViewMode.value = true;
|
||||
galleryModalVisibleDuringDrag.value = true;
|
||||
gtagEvent("open_gallery_modal", { "tab": tabStore.activeTabId });
|
||||
};
|
||||
|
||||
const tabStore = useTabStore();
|
||||
@@ -141,8 +138,6 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
activeMenu={tabStore.activeTabId}
|
||||
disabled={disableTabSwitcher()}
|
||||
onMenuChange={(tabId) => {
|
||||
gtagEvent("switch_tab", { "tab": tabId });
|
||||
|
||||
// Prevent a second input if the switcher is throttled.
|
||||
if (switcherThrottle.current) {
|
||||
return;
|
||||
@@ -198,10 +193,7 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
|
||||
variant="secondary"
|
||||
icon={faGear}
|
||||
className="h-[38px] w-[38px]"
|
||||
onClick={() => {
|
||||
setIsSettingsModalOpen(true);
|
||||
gtagEvent("open_settings_modal");
|
||||
}}
|
||||
onClick={() => setIsSettingsModalOpen(true)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
|
||||
@@ -15,7 +15,6 @@ import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap";
|
||||
import { captureStageImageBitmap } from "./hooks/useUpdateSnapshot";
|
||||
import { ContextMenuContainer } from "./components/ui/ContextMenu";
|
||||
import { FalBackgroundRemoval } from "@storyteller/tauri-api";
|
||||
import { getCreatorIcon, ModelCreator } from "@storyteller/model-list";
|
||||
|
||||
export const DecodeBase64ToImage = async (
|
||||
base64String: string,
|
||||
@@ -147,18 +146,14 @@ const PageDraw = () => {
|
||||
const modelList: PopoverItem[] = [
|
||||
{
|
||||
label: "GPT-4o",
|
||||
icon: getCreatorIcon(ModelCreator.OpenAi) || (
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
),
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
selected: selectedModel === "GPT-4o",
|
||||
description: "High quality model",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
},
|
||||
{
|
||||
label: "FLUX.1 Kontext",
|
||||
icon: getCreatorIcon(ModelCreator.BlackForestLabs) || (
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
),
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
selected: selectedModel === "FLUX.1 Kontext",
|
||||
description: "Fast and high-quality model",
|
||||
badges: [{ label: "20 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
|
||||
@@ -1035,8 +1035,6 @@ export const useSceneStore = create<SceneState>((set, get) => ({
|
||||
scaleY: node.scaleY || 1,
|
||||
offsetX: node.offsetX || 0,
|
||||
offsetY: node.offsetY || 0,
|
||||
zIndex: node.zIndex || 0,
|
||||
locked: node.locked || false,
|
||||
};
|
||||
|
||||
// Convert imageFile to base64 if it exists
|
||||
|
||||
@@ -11,12 +11,13 @@ import { ChromaKeyMaterial } from "./chromakey";
|
||||
import { TimeLine } from "./timeline";
|
||||
import { ClipGroup, ClipType } from "~/enums";
|
||||
import { ClipUI } from "../clips/clip_ui";
|
||||
import { LoadWithoutCors } from "@storyteller/tauri-api";
|
||||
|
||||
import { GetFrontendEnvironment } from "~/Classes/GetFrontendEnvironment";
|
||||
|
||||
import { gridVisibility } from "../signals/engine";
|
||||
import { InfiniteGridHelper } from "./InfiniteGridHelper";
|
||||
import { cameras, selectedCameraId } from "../signals/camera";
|
||||
import { MediaFilesApi } from "@storyteller/api";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
class Scene {
|
||||
name: string;
|
||||
@@ -624,38 +625,19 @@ class Scene {
|
||||
|
||||
// await this.delay(3000); // artificial delay.
|
||||
|
||||
|
||||
// NB(bt,2025-07-22): This is causing a lot of CORS errors in dev, so I'm starting
|
||||
// to funnel everything through Tauri. Not sure if that'll be more inefficient due
|
||||
// to spinning up new HTTP clients with new SSL handshakes without caching, but at
|
||||
// least it should bypass the cors errors.
|
||||
//
|
||||
// const glb = await this.load_glb_wrapped(url, async (progress) => {
|
||||
// const total_loaded = progress.loaded / progress.total;
|
||||
// //const percent = this.floatToPercent(total_loaded);
|
||||
// //console.log(`GLB Loading: ${percent}`);
|
||||
//
|
||||
// if (total_loaded == 1.0) {
|
||||
// if (this.placeholder_manager === undefined) {
|
||||
// throw Error("Place holder Manager is undefined");
|
||||
// }
|
||||
// //console.log(`GLB Loading: ${total_loaded} === ${key}`);
|
||||
// await this.placeholder_manager.remove(key);
|
||||
// }
|
||||
// }).catch((error: Error) => {
|
||||
// throw error;
|
||||
// });
|
||||
const glb = await this.load_glb_wrapped(url, async (progress) => {
|
||||
const total_loaded = progress.loaded / progress.total;
|
||||
//const percent = this.floatToPercent(total_loaded);
|
||||
//console.log(`GLB Loading: ${percent}`);
|
||||
|
||||
const glb = await this.load_glb_wrapped_no_cors(url, async () => {
|
||||
if (this.placeholder_manager === undefined) {
|
||||
throw Error("Place holder Manager is undefined");
|
||||
}
|
||||
await this.placeholder_manager.remove(key);
|
||||
}).catch(async (error) => {
|
||||
if (this.placeholder_manager !== undefined) {
|
||||
if (total_loaded == 1.0) {
|
||||
if (this.placeholder_manager === undefined) {
|
||||
throw Error("Place holder Manager is undefined");
|
||||
}
|
||||
//console.log(`GLB Loading: ${total_loaded} === ${key}`);
|
||||
await this.placeholder_manager.remove(key);
|
||||
}
|
||||
toast.error("Failed to load 3D asset.");
|
||||
}).catch((error: Error) => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
@@ -753,41 +735,6 @@ class Scene {
|
||||
return child_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a full media_url via Tauri (without browser cors)
|
||||
* @param media_url
|
||||
* @param onComplete
|
||||
* @returns
|
||||
*/
|
||||
private async load_glb_wrapped_no_cors(
|
||||
media_url: string,
|
||||
onComplete: () => void,
|
||||
) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let buffer;
|
||||
try {
|
||||
buffer = await LoadWithoutCors(media_url); // Load via Tauri!
|
||||
} catch (error) {
|
||||
console.error("load GLB from Tauri error:", error)
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
const glbLoader = new GLTFLoader();
|
||||
glbLoader.parse(
|
||||
buffer,
|
||||
"", // NB: No relative path to load additional assets!
|
||||
(gltf) => {
|
||||
resolve(gltf);
|
||||
onComplete();
|
||||
},
|
||||
(error) => {
|
||||
console.error("GLTF loader error:", error)
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Using a constructed media_url load a glb, grab the progress.
|
||||
* @param media_url
|
||||
|
||||
@@ -10,13 +10,14 @@ import {
|
||||
faChevronRight,
|
||||
faMountainCity,
|
||||
faDog,
|
||||
faImage,
|
||||
faUpFromLine,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { CloseButton } from "@storyteller/ui-close-button";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
// import { TabSelector } from "@storyteller/ui-tab-selector";
|
||||
import { TabSelector } from "@storyteller/ui-tab-selector";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
@@ -305,17 +306,17 @@ export const AssetModal = () => {
|
||||
icon: faSun,
|
||||
items: activeLibraryTab === "library" ? demoSkyboxItems.value : [],
|
||||
},
|
||||
// {
|
||||
// id: "image-planes",
|
||||
// label: "Image Planes",
|
||||
// labelSingle: "Image Plane",
|
||||
// icon: faImage,
|
||||
// engineCategory: FilterEngineCategories.IMAGE_PLANE,
|
||||
// items:
|
||||
// activeLibraryTab === "library"
|
||||
// ? (featuredImagePlanes ?? [])
|
||||
// : (userImagePlanes ?? []),
|
||||
// },
|
||||
{
|
||||
id: "image-planes",
|
||||
label: "Image Planes",
|
||||
labelSingle: "Image Plane",
|
||||
icon: faImage,
|
||||
engineCategory: FilterEngineCategories.IMAGE_PLANE,
|
||||
items:
|
||||
activeLibraryTab === "library"
|
||||
? (featuredImagePlanes ?? [])
|
||||
: (userImagePlanes ?? []),
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
@@ -499,7 +500,7 @@ export const AssetModal = () => {
|
||||
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5">
|
||||
<h2 className="text-[18px] font-semibold opacity-80">
|
||||
ArtCraft Presets
|
||||
3D Assets
|
||||
</h2>
|
||||
{/* <Tooltip content="Upload model" position="top" delay={200}>
|
||||
<Button
|
||||
@@ -512,7 +513,7 @@ export const AssetModal = () => {
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<div className="flex h-full flex-col space-y-2">
|
||||
{/* <Button
|
||||
<Button
|
||||
variant="primary"
|
||||
className={twMerge(
|
||||
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
|
||||
@@ -525,7 +526,7 @@ export const AssetModal = () => {
|
||||
className="mr-2 text-lg opacity-70"
|
||||
/>
|
||||
Upload an asset
|
||||
</Button> */}
|
||||
</Button>
|
||||
{assetTabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.id}
|
||||
@@ -580,12 +581,12 @@ export const AssetModal = () => {
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="h-full">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* <TabSelector
|
||||
<TabSelector
|
||||
tabs={libraryTabs}
|
||||
activeTab={activeLibraryTab}
|
||||
onTabChange={setActiveLibraryTab}
|
||||
className="relative z-[51] w-auto"
|
||||
/> */}
|
||||
/>
|
||||
<div className="relative grow">
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
@@ -632,7 +633,7 @@ export const AssetModal = () => {
|
||||
</Button>
|
||||
{currentTab.label}
|
||||
</div>
|
||||
{/* {activeAssetTab !== "skybox" &&
|
||||
{activeAssetTab !== "skybox" &&
|
||||
activeAssetTab !== "all" && (
|
||||
<Button
|
||||
icon={faUpFromLine}
|
||||
@@ -647,7 +648,7 @@ export const AssetModal = () => {
|
||||
Upload {getArticle(currentTab.labelSingle)}{" "}
|
||||
{currentTab.labelSingle}
|
||||
</Button>
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{renderContent()}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
faPlus,
|
||||
faUpRightAndDownLeftFromCenter,
|
||||
faCube,
|
||||
faImages,
|
||||
faArrowUpFromBracket,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { ButtonIconSelect } from "@storyteller/ui-button-icon-select";
|
||||
@@ -34,17 +33,10 @@ import { AssetModal } from "../AssetMenu/AssetModal";
|
||||
import { selectedMode } from "../../signals/selectedMode";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { outlinerState } from "../../signals/outliner/outliner";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { setLogoutStates } from "~/signals/authentication/utilities";
|
||||
import { UploadModal3D } from "../../../../components/reusable/UploadModal3D/UploadModal3D";
|
||||
import { PopoverMenu } from "@storyteller/ui-popover";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
galleryModalVisibleViewMode,
|
||||
galleryModalVisibleDuringDrag,
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { UploadModalImage } from "../../../../components/reusable/UploadModalImage";
|
||||
|
||||
export const Controls3D = () => {
|
||||
useSignals();
|
||||
@@ -53,8 +45,6 @@ export const Controls3D = () => {
|
||||
// Action reminder is now handled through signals
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
const [upload3DIsShowing, setUpload3DIsShowing] = useState(false);
|
||||
const [isAddAssetPopoverOpen, setIsAddAssetPopoverOpen] = useState(false);
|
||||
const [uploadImageIsShowing, setUploadImageIsShowing] = useState(false);
|
||||
|
||||
// Track processed 3D models by their media token to prevent duplicates
|
||||
const processedModelsRef = useRef<Record<string, boolean>>({});
|
||||
@@ -63,12 +53,7 @@ export const Controls3D = () => {
|
||||
// Check if scene is empty and onboarding helper is not visible
|
||||
const checkSceneEmpty = () => {
|
||||
const isSceneEmpty =
|
||||
outlinerState.items.value.length === 0 &&
|
||||
!assetModalVisible.value &&
|
||||
!galleryModalVisibleViewMode.value &&
|
||||
!isAddAssetPopoverOpen &&
|
||||
!upload3DIsShowing &&
|
||||
!uploadImageIsShowing;
|
||||
outlinerState.items.value.length === 0 && !assetModalVisible.value;
|
||||
|
||||
setShowEmptySceneTooltip(isSceneEmpty);
|
||||
};
|
||||
@@ -83,13 +68,7 @@ export const Controls3D = () => {
|
||||
unsubscribe();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
assetModalVisible.value,
|
||||
galleryModalVisibleViewMode.value,
|
||||
isAddAssetPopoverOpen,
|
||||
upload3DIsShowing,
|
||||
uploadImageIsShowing,
|
||||
]);
|
||||
}, [assetModalVisible.value]);
|
||||
|
||||
const handleModeChange = (value: string) => {
|
||||
selectedMode.value = value;
|
||||
@@ -190,29 +169,8 @@ export const Controls3D = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Open global Gallery modal
|
||||
const handleOpenGalleryModal = () => {
|
||||
galleryModalVisibleViewMode.value = true;
|
||||
galleryModalVisibleDuringDrag.value = true;
|
||||
};
|
||||
|
||||
const handleAddAssetAction = (action: string) => {
|
||||
switch (action) {
|
||||
case "presets":
|
||||
handleOpenModal();
|
||||
break;
|
||||
case "library":
|
||||
handleOpenGalleryModal();
|
||||
break;
|
||||
case "upload-3d":
|
||||
setUpload3DIsShowing(true);
|
||||
break;
|
||||
case "upload-image":
|
||||
setUploadImageIsShowing(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const handleOpenUpload3DModal = () => {
|
||||
setUpload3DIsShowing(true);
|
||||
};
|
||||
|
||||
const openSettingsModal = () => {
|
||||
@@ -261,14 +219,14 @@ export const Controls3D = () => {
|
||||
<div className="relative">
|
||||
{showEmptySceneTooltip && (
|
||||
<div className="absolute -bottom-14 left-1/2 -translate-x-1/2 transform whitespace-nowrap">
|
||||
<div className="animate-bounce rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white shadow-lg">
|
||||
<div className="animate-bounce rounded-lg bg-brand-primary px-4 py-2 text-sm font-medium text-white shadow-lg">
|
||||
Click + to add your first 3D asset!
|
||||
<div className="absolute -top-1.5 left-1/2 h-3 w-3 -translate-x-1/2 rotate-45 transform bg-primary" />
|
||||
<div className="absolute -top-1.5 left-1/2 h-3 w-3 -translate-x-1/2 rotate-45 transform bg-brand-primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip
|
||||
content="Add an asset to scene"
|
||||
content="Add 3D asset to scene (B)"
|
||||
position="bottom"
|
||||
delay={300}
|
||||
closeOnClick
|
||||
@@ -276,68 +234,30 @@ export const Controls3D = () => {
|
||||
showEmptySceneTooltip ? "hidden" : "block",
|
||||
)}
|
||||
>
|
||||
<PopoverMenu
|
||||
mode="button"
|
||||
position="bottom"
|
||||
panelTitle="Add an asset to scene"
|
||||
onOpenChange={setIsAddAssetPopoverOpen}
|
||||
items={[
|
||||
{
|
||||
label: "ArtCraft Presets (B)",
|
||||
selected: false,
|
||||
icon: (
|
||||
<FontAwesomeIcon icon={faCube} className="h-4 w-4" />
|
||||
),
|
||||
action: "presets",
|
||||
},
|
||||
{
|
||||
label: "My Library",
|
||||
selected: false,
|
||||
icon: (
|
||||
<FontAwesomeIcon
|
||||
icon={faImages}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
),
|
||||
action: "library",
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: "Upload 3D Model",
|
||||
selected: false,
|
||||
icon: (
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpFromBracket}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
),
|
||||
action: "upload-3d",
|
||||
},
|
||||
{
|
||||
label: "Upload Image",
|
||||
selected: false,
|
||||
icon: (
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpFromBracket}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
),
|
||||
action: "upload-image",
|
||||
},
|
||||
]}
|
||||
onPanelAction={handleAddAssetAction}
|
||||
showIconsInList
|
||||
buttonClassName={`h-9 w-9 rounded-[10px] text-lg ${
|
||||
<Button
|
||||
icon={faPlus}
|
||||
className={`h-9 w-9 rounded-[10px] text-lg ${
|
||||
showEmptySceneTooltip
|
||||
? "bg-primary/90 hover:bg-primary/70"
|
||||
: "border-transparent bg-primary/90 hover:bg-primary/70"
|
||||
? "border-white/80 bg-brand-primary/90"
|
||||
: ""
|
||||
}`}
|
||||
triggerIcon={
|
||||
<FontAwesomeIcon icon={faPlus} className="text-xl" />
|
||||
}
|
||||
onClick={handleOpenModal}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip
|
||||
content="Upload 3D asset"
|
||||
position="bottom"
|
||||
delay={300}
|
||||
closeOnClick
|
||||
>
|
||||
<Button
|
||||
icon={faArrowUpFromBracket}
|
||||
className="text-md h-9 w-9 rounded-[10px] bg-white/15 transition-colors hover:bg-white/25"
|
||||
variant="secondary"
|
||||
onClick={handleOpenUpload3DModal}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content="Create 3D model from image"
|
||||
position="bottom"
|
||||
@@ -378,16 +298,9 @@ export const Controls3D = () => {
|
||||
isOpen={upload3DIsShowing}
|
||||
onClose={() => setUpload3DIsShowing(false)}
|
||||
onSuccess={() => setUpload3DIsShowing(false)}
|
||||
title="Upload a 3D Model"
|
||||
title="Upload 3D Asset"
|
||||
titleIcon={faCube}
|
||||
/>
|
||||
|
||||
<UploadModalImage
|
||||
isOpen={uploadImageIsShowing}
|
||||
onClose={() => setUploadImageIsShowing(false)}
|
||||
onSuccess={() => setUploadImageIsShowing(false)}
|
||||
title="Upload an Image"
|
||||
titleIcon={faImages}
|
||||
isSelectVisible={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+5
-6
@@ -1,5 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { LightboxModal } from "@storyteller/ui-lightbox-modal";
|
||||
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
|
||||
|
||||
@@ -39,9 +41,7 @@ export function CompletedCard({ job }: Props) {
|
||||
<div className="flex aspect-square h-14 w-14 justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
|
||||
<img
|
||||
src={
|
||||
loadError
|
||||
? "/resources/images/movie-placeholder.png"
|
||||
: thumbnailUrl
|
||||
loadError ? "/resources/images/movie-placeholder.png" : thumbnailUrl
|
||||
}
|
||||
className="h-full w-full object-cover"
|
||||
alt={job.maybe_title ?? "unknown"}
|
||||
@@ -57,18 +57,17 @@ export function CompletedCard({ job }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <a
|
||||
<a
|
||||
href={downloadLink}
|
||||
download
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex h-9 w-9 items-center justify-center text-lg text-white/60 transition-all hover:text-white"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownToLine} />
|
||||
</a> */}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<LightboxModal
|
||||
onCloseGallery={() => setIsModalOpen(false)}
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
imageUrl={fullImageUrl}
|
||||
|
||||
@@ -24,8 +24,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
selectedModels[ModelCategory.TextToImage] ||
|
||||
imageGenerationModels[0]?.label;
|
||||
|
||||
const selectedModelInfo: ModelInfo | undefined =
|
||||
IMAGE_MODELS_BY_LABEL[selectedModel];
|
||||
const selectedModelInfo : ModelInfo | undefined = IMAGE_MODELS_BY_LABEL[selectedModel];
|
||||
|
||||
const jobContext: JobContextType = {
|
||||
jobTokens: [],
|
||||
|
||||
@@ -24,8 +24,7 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
selectedModels[ModelCategory.ImageToVideo] ||
|
||||
videoGenerationModels[0]?.label;
|
||||
|
||||
const selectedModelInfo: ModelInfo | undefined =
|
||||
VIDEO_MODELS_BY_LABEL[selectedModel];
|
||||
const selectedModelInfo : ModelInfo | undefined = VIDEO_MODELS_BY_LABEL[selectedModel];
|
||||
|
||||
const jobContext: JobContextType = {
|
||||
jobTokens: [],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { authentication } from "./authentication";
|
||||
import { UsersApi } from "~/Classes/ApiManager/UsersApi";
|
||||
import { BillingApi } from "~/Classes/ApiManager/BillingApi";
|
||||
import { gtagLogin, gtagLogout } from "@storyteller/google-analytics";
|
||||
|
||||
import {
|
||||
updateActiveSubscriptions,
|
||||
@@ -24,7 +23,6 @@ export const logout = async (
|
||||
// if success, nothing
|
||||
// regarldess of success/fail, clear the state and localstorage
|
||||
setLogoutStates();
|
||||
gtagLogout();
|
||||
};
|
||||
|
||||
export const login = async ({
|
||||
@@ -116,11 +114,6 @@ async function getUserInfoAndSubcriptions() {
|
||||
updateAuthStatus(AUTH_STATUS.NO_ACCESS);
|
||||
return;
|
||||
}
|
||||
|
||||
const userToken = sessionResponse.data.user.user_token;
|
||||
if (!!userToken) {
|
||||
gtagLogin(userToken);
|
||||
}
|
||||
|
||||
const billingApi = new BillingApi();
|
||||
const subscriptionsResponse = await billingApi.ListActiveSubscriptions();
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
[build]
|
||||
command = "./apps/editor3d/script/netlify_build.sh"
|
||||
publish = "apps/editor3d/dist/"
|
||||
|
||||
[build.environment]
|
||||
REACT_APP_PUBLIC_POSTHOG_KEY = "phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
|
||||
REACT_APP_PUBLIC_POSTHOG_UI = "https://us.i.posthog.com"
|
||||
|
||||
#[dev]
|
||||
#command = "npm run dev"
|
||||
#framework = "vite"
|
||||
|
||||
# Set immutable caching for static files, because they have fingerprinted filenames
|
||||
|
||||
[[headers]]
|
||||
for = "/build/*"
|
||||
|
||||
[headers.values]
|
||||
"Cache-Control" = "public, max-age=31560000, immutable"
|
||||
|
||||
[[redirects]]
|
||||
from = "/ingest/static/*"
|
||||
to = "https://us-assets.i.posthog.com/static/:splat"
|
||||
host = "us-assets.i.posthog.com"
|
||||
status = 200
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/ingest/*"
|
||||
to = "https://us.i.posthog.com/:splat"
|
||||
host = "us.i.posthog.com"
|
||||
status = 200
|
||||
force = true
|
||||
@@ -144,9 +144,6 @@
|
||||
},
|
||||
{
|
||||
"path": "../../libs/api"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/analytics/google-analytics"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"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,ts,cts,mts}",
|
||||
"{projectRoot}/vite.config.{js,ts,mjs,mts}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# google-analytics
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build google-analytics` to build the library.
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "@storyteller/google-analytics",
|
||||
"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": {}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './lib/gtag-login.js';
|
||||
export * from './lib/gtag-logout.js';
|
||||
export * from './lib/gtag-event.js';
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
// Send an event with name and "args". Args is a string-to-string map.
|
||||
export const gtagEvent = function(eventName: string, args?: { [key: string]: string }) {
|
||||
console.debug("gtagEvent", eventName, args);
|
||||
if (!args) {
|
||||
(window as any).gtag('event', eventName);
|
||||
} else {
|
||||
(window as any).gtag('event', eventName, args);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { TAG_ID } from './tag-id.js';
|
||||
|
||||
export const gtagLogin = function(userToken: string) {
|
||||
console.debug("gtagLogin", TAG_ID, userToken);
|
||||
|
||||
(window as any).gtag(
|
||||
'config', TAG_ID, {
|
||||
'user_id': userToken
|
||||
});
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { TAG_ID } from './tag-id.js';
|
||||
|
||||
export const gtagLogout = function() {
|
||||
console.debug("gtagLogout", TAG_ID);
|
||||
|
||||
(window as any).gtag(
|
||||
'config', TAG_ID, {
|
||||
'user_id': null
|
||||
});
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const TAG_ID = 'G-Y4Y8KMLPQ7';
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/// <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/analytics/google-analytics',
|
||||
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: '@storyteller/google-analytics',
|
||||
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: []
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from "@storyteller/tauri-api";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
import React from "react";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
|
||||
interface LightboxModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -132,7 +131,6 @@ export function LightboxModal({
|
||||
<div className="mt-15 mb-15 flex justify-end gap-2">
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
gtagEvent("image_to_3d_clicked");
|
||||
//let _result = await FalHunyuanImageTo3d({
|
||||
// image_media_token: mediaId,
|
||||
// //base64_image: downloadUrl,
|
||||
@@ -154,7 +152,6 @@ export function LightboxModal({
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("add_to_scene_clicked");
|
||||
await onAddToSceneClicked(downloadUrl, mediaId);
|
||||
onClose(); // close the lightbox
|
||||
onCloseGallery(); // close the gallery
|
||||
@@ -170,7 +167,6 @@ export function LightboxModal({
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("download_clicked");
|
||||
await onDownloadClicked(downloadUrl, mediaClass);
|
||||
}}
|
||||
>
|
||||
@@ -180,10 +176,7 @@ export function LightboxModal({
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("download_clicked");
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="no-underline"
|
||||
>
|
||||
<Button icon={faDownToLine}>Download</Button>
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
{
|
||||
"path": "../../tauri-api"
|
||||
},
|
||||
{
|
||||
"path": "../../analytics/google-analytics"
|
||||
},
|
||||
{
|
||||
"path": "../button"
|
||||
},
|
||||
|
||||
@@ -49,9 +49,6 @@
|
||||
{
|
||||
"path": "../../tauri-api/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../analytics/google-analytics/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Defines the unique categories for each product or context where a model
|
||||
* selection is required. Using an enum prevents typos and ensures type safety.
|
||||
*/
|
||||
export enum ModelCategory {
|
||||
TextToImage = "text-to-image",
|
||||
ImageToVideo = "image-to-video",
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
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,
|
||||
VIDEO_MODELS,
|
||||
getCreatorIcon,
|
||||
ModelCreator,
|
||||
} from "@storyteller/model-list";
|
||||
import { IMAGE_MODELS } from "@storyteller/model-list";
|
||||
|
||||
export type ModelList = Omit<PopoverItem, "selected">[];
|
||||
|
||||
@@ -14,92 +9,67 @@ export const allModels = {
|
||||
video: [
|
||||
{
|
||||
label: "Kling 1.6 Pro",
|
||||
icon: getCreatorIcon(ModelCreator.Kling) || (
|
||||
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
|
||||
),
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "Good quality model",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: VIDEO_MODELS.kling_1_6_pro,
|
||||
},
|
||||
{
|
||||
label: "Kling 2.1 Pro",
|
||||
icon: getCreatorIcon(ModelCreator.Kling) || (
|
||||
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
|
||||
),
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "High quality model",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: VIDEO_MODELS.kling_2_1_pro,
|
||||
},
|
||||
{
|
||||
label: "Kling 2.1 Master",
|
||||
icon: getCreatorIcon(ModelCreator.Kling) || (
|
||||
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
|
||||
),
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "Master quality model ($$)",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: VIDEO_MODELS.kling_2_1_master,
|
||||
},
|
||||
{
|
||||
label: "Seedance 1.0 Lite",
|
||||
icon: getCreatorIcon(ModelCreator.Bytedance) || (
|
||||
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
|
||||
),
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "Fast and high-quality model",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: VIDEO_MODELS.seedance_1_0_lite,
|
||||
},
|
||||
{
|
||||
label: "Google Veo 2",
|
||||
icon: getCreatorIcon(ModelCreator.Google) || (
|
||||
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
|
||||
),
|
||||
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
|
||||
description: "Fast and high-quality model",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: VIDEO_MODELS.veo_2,
|
||||
},
|
||||
],
|
||||
image: [
|
||||
{
|
||||
label: "Flux Pro 1.1 Ultra",
|
||||
icon: getCreatorIcon(ModelCreator.BlackForestLabs) || (
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
),
|
||||
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: getCreatorIcon(ModelCreator.BlackForestLabs) || (
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
),
|
||||
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: getCreatorIcon(ModelCreator.BlackForestLabs) || (
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
),
|
||||
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: getCreatorIcon(ModelCreator.BlackForestLabs) || (
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
),
|
||||
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: getCreatorIcon(ModelCreator.OpenAi) || (
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
),
|
||||
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,
|
||||
@@ -123,18 +93,14 @@ export const allModels = {
|
||||
instructiveImageEdits: [
|
||||
{
|
||||
label: "GPT-4o",
|
||||
icon: getCreatorIcon(ModelCreator.OpenAi) || (
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
),
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
description: "High quality model",
|
||||
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: IMAGE_MODELS.gpt_image_1,
|
||||
},
|
||||
{
|
||||
label: "FLUX.1 Kontext",
|
||||
icon: getCreatorIcon(ModelCreator.BlackForestLabs) || (
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
),
|
||||
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
|
||||
description: "Fast and high-quality model",
|
||||
badges: [{ label: "20 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
|
||||
modelInfo: IMAGE_MODELS.gpt_image_1, // TODO: Wrong metadata!
|
||||
@@ -144,5 +110,4 @@ export const allModels = {
|
||||
|
||||
export const videoGenerationModels: ModelList = allModels.video;
|
||||
export const imageGenerationModels: ModelList = allModels.image;
|
||||
export const instructiveImageEditModels: ModelList =
|
||||
allModels.instructiveImageEdits;
|
||||
export const instructiveImageEditModels: ModelList = allModels.instructiveImageEdits;
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface PopoverItem {
|
||||
icon?: ReactNode;
|
||||
action?: string;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
description?: string;
|
||||
badges?: Array<{
|
||||
label: string;
|
||||
@@ -48,7 +47,6 @@ interface PopoverMenuProps {
|
||||
position?: "top" | "bottom";
|
||||
align?: "start" | "center" | "end";
|
||||
panelActionLabel?: string;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const PopoverMenu = ({
|
||||
@@ -69,7 +67,6 @@ export const PopoverMenu = ({
|
||||
position = "top",
|
||||
align = "start",
|
||||
panelActionLabel,
|
||||
onOpenChange,
|
||||
}: PopoverMenuProps) => {
|
||||
const selectedItem = items.find((item) => item.selected);
|
||||
|
||||
@@ -175,12 +172,6 @@ export const PopoverMenu = ({
|
||||
<Popover>
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
{(() => {
|
||||
useEffect(() => {
|
||||
onOpenChange?.(open);
|
||||
}, [open]);
|
||||
return null;
|
||||
})()}
|
||||
<PopoverButton
|
||||
className={className}
|
||||
onMouseEnter={() =>
|
||||
@@ -273,74 +264,70 @@ export const PopoverMenu = ({
|
||||
) : mode === "hoverSelect" ? (
|
||||
<div className="flex flex-col gap-0 text-sm text-white">
|
||||
{items.map((item, index) => (
|
||||
<div key={index}>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!item.disabled) {
|
||||
handleItemClick(item, close);
|
||||
}
|
||||
}}
|
||||
className={twMerge(
|
||||
"group flex cursor-pointer items-start gap-2 rounded-lg px-2 py-2 transition-all",
|
||||
item.selected
|
||||
? "bg-black/40 border-l-4 border-primary"
|
||||
: "hover:bg-black/20",
|
||||
item.disabled
|
||||
? "!cursor-not-allowed opacity-50"
|
||||
: ""
|
||||
)}
|
||||
style={{ minHeight: 48 }}
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-start gap-2 grow">
|
||||
{showIconsInList && (
|
||||
<span className="mt-1 flex h-5 w-5 items-center justify-center text-lg text-white/80">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="truncate font-semibold text-white text-base">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{item.description && (
|
||||
<div className="truncate text-xs text-white/60 mt-0.5">
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row gap-1 flex-wrap mt-1.5">
|
||||
{item.badges &&
|
||||
Array.isArray(item.badges) &&
|
||||
item.badges.map((badge, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-1 min-w-0"
|
||||
>
|
||||
<span className="inline-flex items-center rounded bg-black/40 px-1.5 py-0.5 text-xs font-medium text-white gap-1">
|
||||
{badge?.icon && (
|
||||
<span>{badge.icon}</span>
|
||||
)}
|
||||
{badge?.label || ""}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.selected && (
|
||||
<span className="text-primary text-xl font-bold bg-white rounded-full p-0 h-4 w-4 flex items-center justify-center mr-1">
|
||||
<FontAwesomeIcon icon={faCircleCheck} />
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
if (!item.disabled) {
|
||||
handleItemClick(item, close);
|
||||
}
|
||||
}}
|
||||
className={twMerge(
|
||||
"group flex cursor-pointer items-start gap-2 rounded-lg px-2 py-2 transition-all",
|
||||
item.selected
|
||||
? "bg-black/40 border-l-4 border-primary"
|
||||
: "hover:bg-black/20",
|
||||
item.disabled
|
||||
? "!cursor-not-allowed opacity-50"
|
||||
: ""
|
||||
)}
|
||||
style={{ minHeight: 48 }}
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-start gap-2 grow">
|
||||
{showIconsInList && (
|
||||
<span className="mt-1 flex h-5 w-5 items-center justify-center text-lg text-white/80">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="truncate font-semibold text-white text-base">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{item.description && (
|
||||
<div className="truncate text-xs text-white/60 mt-0.5">
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row gap-1 flex-wrap mt-1.5">
|
||||
{item.badges &&
|
||||
Array.isArray(item.badges) &&
|
||||
item.badges.map((badge, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-1 min-w-0"
|
||||
>
|
||||
<span className="inline-flex items-center rounded bg-black/40 px-1.5 py-0.5 text-xs font-medium text-white gap-1">
|
||||
{badge?.icon && (
|
||||
<span>{badge.icon}</span>
|
||||
)}
|
||||
{badge?.label || ""}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.selected && (
|
||||
<span className="text-primary text-xl font-bold bg-white rounded-full p-0 h-4 w-4 flex items-center justify-center mr-1">
|
||||
<FontAwesomeIcon icon={faCircleCheck} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.divider && (
|
||||
<div className="my-1 border-t border-white/10" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{showAddButton && onAdd && (
|
||||
@@ -362,63 +349,57 @@ export const PopoverMenu = ({
|
||||
) : (
|
||||
<div className="flex flex-col gap-0 text-sm text-white">
|
||||
{items.map((item, index) => (
|
||||
<div key={index}>
|
||||
<Button
|
||||
className={twMerge(
|
||||
"flex w-full items-center justify-between border-transparent bg-transparent px-1.5",
|
||||
"hover:bg-[#63636B]/60",
|
||||
mode === "toggle" && item.selected
|
||||
? "hover:bg-[#63636B]"
|
||||
: "bg-transparent",
|
||||
item.disabled
|
||||
? "!cursor-not-allowed opacity-50"
|
||||
: ""
|
||||
)}
|
||||
onClick={() =>
|
||||
!item.disabled && handleItemClick(item, close)
|
||||
}
|
||||
variant="secondary"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
{showIconsInList && item.icon}
|
||||
{mode === "toggle" ? (
|
||||
<span
|
||||
className={twMerge(
|
||||
"truncate",
|
||||
item.selected
|
||||
? "text-white"
|
||||
: "text-white/70"
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
) : (
|
||||
<span className="truncate">{item.label}</span>
|
||||
)}
|
||||
</div>
|
||||
{mode === "toggle" && (
|
||||
<Button
|
||||
key={index}
|
||||
className={twMerge(
|
||||
"flex w-full items-center justify-between border-transparent bg-transparent px-1.5",
|
||||
"hover:bg-[#63636B]/60",
|
||||
mode === "toggle" && item.selected
|
||||
? "hover:bg-[#63636B]"
|
||||
: "bg-transparent",
|
||||
item.disabled
|
||||
? "!cursor-not-allowed opacity-50"
|
||||
: ""
|
||||
)}
|
||||
onClick={() =>
|
||||
!item.disabled && handleItemClick(item, close)
|
||||
}
|
||||
variant="secondary"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
{showIconsInList && item.icon}
|
||||
{mode === "toggle" ? (
|
||||
<span
|
||||
className={twMerge(
|
||||
"ml-2 h-5 w-5 rounded-full border flex items-center justify-center transition-colors",
|
||||
item.selected
|
||||
? "border-primary bg-primary"
|
||||
: "border-transparent bg-transparent"
|
||||
"truncate",
|
||||
item.selected ? "text-white" : "text-white/70"
|
||||
)}
|
||||
>
|
||||
{item.selected && (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="text-white text-xs font-bold"
|
||||
/>
|
||||
)}
|
||||
{item.label}
|
||||
</span>
|
||||
) : (
|
||||
<span className="truncate">{item.label}</span>
|
||||
)}
|
||||
</Button>
|
||||
{item.divider && (
|
||||
<div className="my-1 border-t border-white/10" />
|
||||
</div>
|
||||
{mode === "toggle" && (
|
||||
<span
|
||||
className={twMerge(
|
||||
"ml-2 h-5 w-5 rounded-full border flex items-center justify-center transition-colors",
|
||||
item.selected
|
||||
? "border-primary bg-primary"
|
||||
: "border-transparent bg-transparent"
|
||||
)}
|
||||
>
|
||||
{item.selected && (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="text-white text-xs font-bold"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
{showAddButton && onAdd && (
|
||||
<Button
|
||||
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
// import { showActionReminder } from "@storyteller/ui-action-reminder-modal";
|
||||
// import { invoke } from "@tauri-apps/api/core";
|
||||
import { usePrompt2DStore, RefImage } from "./promptStore";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
|
||||
export type AspectRatio = "1:1" | "3:2" | "2:3";
|
||||
|
||||
@@ -398,8 +397,6 @@ export const PromptBox2D = ({
|
||||
}
|
||||
}
|
||||
|
||||
gtagEvent("enqueue_2d");
|
||||
|
||||
if (isEnqueueing) return;
|
||||
setIsEnqueueing(true);
|
||||
if (!prompt.trim()) return;
|
||||
|
||||
@@ -52,7 +52,6 @@ import {
|
||||
} from "@storyteller/tauri-api";
|
||||
// import { showActionReminder } from "@storyteller/ui-action-reminder-modal";
|
||||
import { usePrompt3DStore, RefImage } from "./promptStore";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
|
||||
interface PromptBox3DProps {
|
||||
cameras: Signal<Camera[]>;
|
||||
@@ -317,7 +316,6 @@ export const PromptBox3D = ({
|
||||
};
|
||||
|
||||
const handleEnqueue = async () => {
|
||||
gtagEvent("enqueue_3d");
|
||||
const isDesktop = IsDesktopApp();
|
||||
console.log("Is Desktop?", isDesktop);
|
||||
if (isDesktop) {
|
||||
|
||||
@@ -29,7 +29,6 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
|
||||
import { ModelInfo } from "@storyteller/model-list";
|
||||
import { usePromptImageStore, RefImage } from "./promptStore";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
|
||||
interface PromptBoxImageProps {
|
||||
useJobContext: () => JobContextType;
|
||||
@@ -199,8 +198,6 @@ export const PromptBoxImage = ({
|
||||
|
||||
console.log("PromptBoxImage - Prompting with model", modelInfo);
|
||||
|
||||
gtagEvent("enqueue_image");
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
|
||||
// the backend hangs on a given request.
|
||||
|
||||
@@ -23,7 +23,6 @@ import { IsDesktopApp } from "@storyteller/tauri-utils";
|
||||
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
|
||||
import { ModelInfo } from "@storyteller/model-list";
|
||||
import { usePromptVideoStore, RefImage } from "./promptStore";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
|
||||
interface PromptBoxVideoProps {
|
||||
useJobContext: () => JobContextType;
|
||||
@@ -188,8 +187,6 @@ export const PromptBoxVideo = ({
|
||||
|
||||
console.log("PromptBoxVideo - Prompting with model", modelInfo);
|
||||
|
||||
gtagEvent("enqueue_video");
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO(bt,2025-05-08): This is a hack so we don't accidentally wind up with a permanently disabled prompt box if
|
||||
// the backend hangs on a given request.
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
{
|
||||
"path": "../camera-settings-modal"
|
||||
},
|
||||
{
|
||||
"path": "../../soundboard"
|
||||
},
|
||||
{
|
||||
"path": "../action-reminder-modal"
|
||||
},
|
||||
{
|
||||
"path": "../../tauri-api"
|
||||
},
|
||||
@@ -38,9 +44,6 @@
|
||||
{
|
||||
"path": "../toaster"
|
||||
},
|
||||
{
|
||||
"path": "../../analytics/google-analytics"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
{
|
||||
"path": "../camera-settings-modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../soundboard/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../action-reminder-modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../tauri-api/tsconfig.lib.json"
|
||||
},
|
||||
@@ -76,9 +82,6 @@
|
||||
{
|
||||
"path": "../../common/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../analytics/google-analytics/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../toaster/tsconfig.lib.json"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { AudioSettingsPane } from "./panes/AudioSettingsPane";
|
||||
import { AccountSettingsPane } from "./panes/AccountSettings/AccountSettingsPane";
|
||||
import { AboutSettingsPane } from "./panes/AboutSettingsPane";
|
||||
import { ProviderPrioritySettingsPane } from "./panes/ProviderPrioritySettingsPane";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -94,10 +93,7 @@ export const SettingsModal = ({
|
||||
"h-9 w-full rounded-lg p-2 text-left transition-colors duration-100 hover:bg-[#63636B]/40",
|
||||
section.id === selectedSection ? "bg-[#63636B]/40" : ""
|
||||
)}
|
||||
onClick={() => {
|
||||
gtagEvent("switch_settings_section", { "section": section.id });
|
||||
setSelectedSection(section.id);
|
||||
}}
|
||||
onClick={() => setSelectedSection(section.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5 text-sm">
|
||||
<FontAwesomeIcon icon={section.icon} />
|
||||
|
||||
@@ -32,9 +32,6 @@
|
||||
{
|
||||
"path": "../../tauri-api"
|
||||
},
|
||||
{
|
||||
"path": "../../analytics/google-analytics"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -61,9 +61,6 @@
|
||||
{
|
||||
"path": "../../api/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../analytics/google-analytics/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../input/tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -1,6 +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";
|
||||
export * from "./lib/ModelCreatorIcons.js";
|
||||
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';
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ export const IMAGE_MODELS: Record<string, ModelInfo> = {
|
||||
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
|
||||
// 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,
|
||||
@@ -37,4 +37,5 @@ export const IMAGE_MODELS_BY_LABEL: Record<string, ModelInfo> = {
|
||||
"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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { ModelCreator } from "./ModelCreator.js";
|
||||
|
||||
export const CREATOR_ICON_PATHS: Partial<Record<ModelCreator, string>> = {
|
||||
[ModelCreator.BlackForestLabs]:
|
||||
"/resources/images/services/blackforestlabs.svg",
|
||||
[ModelCreator.Kling]: "/resources/images/services/kling.svg",
|
||||
[ModelCreator.Midjourney]: "/resources/images/services/midjourney.svg",
|
||||
[ModelCreator.OpenAi]: "/resources/images/services/openai.svg",
|
||||
[ModelCreator.Bytedance]: "/resources/images/services/bytedance.svg",
|
||||
[ModelCreator.Google]: "/resources/images/services/google.svg",
|
||||
};
|
||||
|
||||
export const getCreatorIconPath = (creator: ModelCreator): string | undefined =>
|
||||
CREATOR_ICON_PATHS[creator];
|
||||
|
||||
export const getCreatorIcon = (
|
||||
creator: ModelCreator,
|
||||
className = "h-4 w-4 invert"
|
||||
): ReactNode | null => {
|
||||
const path = getCreatorIconPath(creator);
|
||||
if (!path) return null;
|
||||
return React.createElement("img", {
|
||||
src: path,
|
||||
alt: `${creator} logo`,
|
||||
className,
|
||||
});
|
||||
};
|
||||
@@ -3,11 +3,11 @@ 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;
|
||||
name: string,
|
||||
|
||||
// The identifier that Tauri uses (it may send different signals downstream)
|
||||
tauri_id: string;
|
||||
tauri_id: string,
|
||||
|
||||
// Creator of the model type
|
||||
creator: ModelCreator;
|
||||
creator: ModelCreator,
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ export const VIDEO_MODELS: Record<string, ModelInfo> = {
|
||||
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
|
||||
// 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,
|
||||
@@ -37,4 +37,5 @@ export const VIDEO_MODELS_BY_LABEL: Record<string, ModelInfo> = {
|
||||
"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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export function modelList(): string {
|
||||
return "model-list";
|
||||
return 'model-list';
|
||||
}
|
||||
|
||||
@@ -21,4 +21,4 @@ export * from "./lib/system/GetAppInfo.js";
|
||||
export * from "./lib/providers/SetProviderOrder.js";
|
||||
export * from "./lib/providers/Provider.js";
|
||||
export * from "./lib/providers/GetProviderOrder.js";
|
||||
export * from "./lib/util/LoadWithoutCors.js";
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export const LoadWithoutCors = async (url: string) : Promise<ArrayBuffer> => {
|
||||
try {
|
||||
// Tauri Result<Success, Error> - success case.
|
||||
let result = await invoke("load_without_cors_command", { url });
|
||||
return result as ArrayBuffer;
|
||||
} catch (error) {
|
||||
// Tauri Result<Success, Error> - error case.
|
||||
console.error("LoadWithoutCors error", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Generated
-9
@@ -76,7 +76,6 @@
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"ajv": "^8.0.0",
|
||||
"autoprefixer": "10.4.13",
|
||||
"eslint": "~8.57.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
@@ -1148,10 +1147,6 @@
|
||||
"version": "0.0.1",
|
||||
"extraneous": true
|
||||
},
|
||||
"libs/analytics/google-analytics": {
|
||||
"name": "@storyteller/google-analytics",
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"libs/api": {
|
||||
"name": "@storyteller/api",
|
||||
"version": "0.0.1",
|
||||
@@ -9418,10 +9413,6 @@
|
||||
"resolved": "libs/common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@storyteller/google-analytics": {
|
||||
"resolved": "libs/analytics/google-analytics",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@storyteller/model-list": {
|
||||
"resolved": "libs/model-list",
|
||||
"link": true
|
||||
|
||||
+20
-21
@@ -8,34 +8,34 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@preact/signals-react": "^3.0.1",
|
||||
"@remix-run/serve": "^2.15.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"detect-gpu": "^5.0.38",
|
||||
"framer-motion": "^12.11.0",
|
||||
"konva": "^9.3.20",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.512.0",
|
||||
"pnpm": "^10.8.1",
|
||||
"posthog-js": "^1.141.0",
|
||||
"react": "18.3.1",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "18.3.1",
|
||||
"react-drag-drop-files": "^2.3.10",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-router-dom": "6.29.0",
|
||||
"zustand": "^5.0.5",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"three": "^0.162.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
|
||||
"react-drag-drop-files": "^2.3.10",
|
||||
"uuid": "^9.0.1",
|
||||
"zustand": "^5.0.5"
|
||||
"dayjs": "^1.11.10",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"posthog-js": "^1.141.0",
|
||||
"lodash": "^4.17.21",
|
||||
"detect-gpu": "^5.0.38",
|
||||
"three": "^0.162.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"konva": "^9.3.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.5",
|
||||
@@ -58,18 +58,17 @@
|
||||
"@testing-library/jest-dom": "6.4.2",
|
||||
"@testing-library/react": "16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "19.0.0",
|
||||
"@types/react-dom": "19.0.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"ajv": "^8.0.0",
|
||||
"autoprefixer": "10.4.13",
|
||||
"postcss-import": "^16.0.1",
|
||||
"postcss-nesting": "^12.1.0",
|
||||
"eslint": "~8.57.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
@@ -82,8 +81,6 @@
|
||||
"jsonc-eslint-parser": "^2.1.0",
|
||||
"nx": "21.2.3",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-import": "^16.0.1",
|
||||
"postcss-nesting": "^12.1.0",
|
||||
"tailwindcss": "3.4.3",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "5.8.3",
|
||||
@@ -92,7 +89,9 @@
|
||||
"vite-plugin-dts": "~4.5.0",
|
||||
"vite-plugin-static-copy": "^2.3.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.0"
|
||||
"vitest": "^3.0.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/lodash": "^4.14.202"
|
||||
},
|
||||
"workspaces": [
|
||||
"apps/**/*",
|
||||
|
||||
@@ -146,9 +146,6 @@
|
||||
},
|
||||
{
|
||||
"path": "./libs/model-list"
|
||||
},
|
||||
{
|
||||
"path": "./libs/analytics/google-analytics"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user