Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce60583675 | ||
|
|
496fc46300 |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/*~": true,
|
||||
"frontend_old/**": true
|
||||
}
|
||||
}
|
||||
@@ -802,6 +802,47 @@ dependencies = [
|
||||
"num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "artcraft"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assertor",
|
||||
"base64 0.22.1",
|
||||
"bytes 1.10.0",
|
||||
"chrono",
|
||||
"const_format",
|
||||
"crockford",
|
||||
"directories 6.0.0",
|
||||
"dirs 6.0.0",
|
||||
"errors",
|
||||
"filesys",
|
||||
"futures 0.3.29",
|
||||
"image",
|
||||
"libloading 0.8.6",
|
||||
"log",
|
||||
"memory_caching",
|
||||
"once_cell",
|
||||
"openai_sora_client",
|
||||
"primitives",
|
||||
"rand 0.9.0",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"serial_test",
|
||||
"subprocess_common",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-log",
|
||||
"tempdir",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "0.9.3"
|
||||
@@ -11985,48 +12026,6 @@ dependencies = [
|
||||
"windows 0.60.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-artcraft"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assertor",
|
||||
"base64 0.22.1",
|
||||
"bytes 1.10.0",
|
||||
"chrono",
|
||||
"const_format",
|
||||
"crockford",
|
||||
"directories 6.0.0",
|
||||
"dirs 6.0.0",
|
||||
"errors",
|
||||
"filesys",
|
||||
"futures 0.3.29",
|
||||
"image",
|
||||
"libloading 0.8.6",
|
||||
"log",
|
||||
"memory_caching",
|
||||
"once_cell",
|
||||
"openai_sora_client",
|
||||
"primitives",
|
||||
"rand 0.9.0",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"serial_test",
|
||||
"subprocess_common",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-log",
|
||||
"tempdir",
|
||||
"tempfile",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.1.1"
|
||||
|
||||
@@ -24,7 +24,7 @@ members = [
|
||||
"crates/cli/production/migration-tool",
|
||||
|
||||
# Desktop apps
|
||||
"crates/desktop/tauri-artcraft",
|
||||
"crates/desktop/tauri-4o",
|
||||
"crates/desktop/tauri-realtime",
|
||||
|
||||
# Shared libraries
|
||||
@@ -168,14 +168,12 @@ sha3 = "0.10.8"
|
||||
thiserror = "2"
|
||||
toml = "0.7.4"
|
||||
url = "2.5.4"
|
||||
|
||||
# Internal libs
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
filesys = { path = "crates/lib/files/filesys" }
|
||||
memory_caching = { path = "crates/lib/caching/memory_caching" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
openai_sora_client = { path = "crates/lib/api_clients/openai_sora_client" }
|
||||
primitives = { path = "crates/lib/data/primitives" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
subprocess_common = { path = "crates/lib/subprocess_common" }
|
||||
tokens = { path = "crates/_types/tokens" }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "tauri-artcraft"
|
||||
name = "artcraft"
|
||||
version = "0.1.0"
|
||||
description = "Artcraft"
|
||||
authors = ["you"]
|
||||
@@ -12,7 +12,7 @@ publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "tauri_artcraft_app_lib"
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
@@ -30,7 +30,6 @@ memory_caching = { workspace = true }
|
||||
openai_sora_client = { workspace = true }
|
||||
primitives = { workspace = true }
|
||||
subprocess_common = { workspace = true }
|
||||
tokens = { workspace = true }
|
||||
|
||||
# External
|
||||
#openssl = { version = "0.10", features = ["vendored"] }
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@@ -15,15 +15,18 @@ use openai_sora_client::upload::upload_media_from_file::SoraMediaUploadRequest;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sora_image_generation_command(
|
||||
pub async fn image_generation_command(
|
||||
image: &str,
|
||||
prompt: &str,
|
||||
image: Option<&str>,
|
||||
_app: AppHandle,
|
||||
app: AppHandle,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
) -> Result<String, String> {
|
||||
info!("image_generation_command called; processing image...");
|
||||
|
||||
generate_image(image, prompt, &app_data_root)
|
||||
let bytes = BASE64_STANDARD.decode(image)
|
||||
.map_err(|err| format!("Base64 decode error: {}", err))?;
|
||||
|
||||
generate_image(bytes, prompt, &app_data_root)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("error: {:?}", err);
|
||||
@@ -33,25 +36,19 @@ pub async fn sora_image_generation_command(
|
||||
Ok("success".to_string())
|
||||
}
|
||||
|
||||
pub async fn generate_image(maybe_image: Option<&str>, prompt: &str, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
pub async fn generate_image(file_bytes: Vec<u8>, prompt: &str, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
let sora_credentials = get_credentials(app_data_root)?;
|
||||
|
||||
let mut sora_media_tokens = vec![];
|
||||
let filename = "image.png".to_string();
|
||||
|
||||
if let Some(image) = maybe_image {
|
||||
let image_bytes = BASE64_STANDARD.decode(image)?;
|
||||
let response= sora_media_upload_from_bytes(file_bytes, filename, &sora_credentials)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image to Sora: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
let filename = "image.png".to_string();
|
||||
|
||||
let response = sora_media_upload_from_bytes(image_bytes, filename, &sora_credentials)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image to Sora: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
sora_media_tokens.push(response.id);
|
||||
}
|
||||
let sora_media_tokens = vec![response.id];
|
||||
|
||||
let response = sora_image_gen_remix(SoraImageGenRemixRequest {
|
||||
prompt: prompt.to_string(),
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod flip_image;
|
||||
pub mod image_generation_command;
|
||||
pub mod open_login_command;
|
||||
@@ -4,7 +4,7 @@ use once_cell::sync::Lazy;
|
||||
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowBuilder};
|
||||
use tokio::time::sleep_until;
|
||||
use errors::AnyhowResult;
|
||||
use crate::threads::sora_session_login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
|
||||
use crate::threads::login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
|
||||
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
|
||||
pub const START_URL_STR: &str = "https://storyteller.ai/";
|
||||
@@ -20,7 +20,7 @@ pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_sora_login_command(
|
||||
pub async fn open_login_command(
|
||||
app: AppHandle,
|
||||
) -> Result<String, String> {
|
||||
info!("open_login_command called");
|
||||
@@ -6,11 +6,10 @@ pub mod transfer;
|
||||
pub mod utils;
|
||||
|
||||
use crate::commands::flip_image::flip_image;
|
||||
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
|
||||
use crate::commands::sora::sora_image_generation_command::sora_image_generation_command;
|
||||
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
|
||||
use crate::commands::image_generation_command::image_generation_command;
|
||||
use crate::commands::open_login_command::open_login_command;
|
||||
use crate::state::app_config::AppConfig;
|
||||
use crate::threads::sora_session_login_thread::sora_session_login_thread;
|
||||
use crate::threads::login_thread::login_thread;
|
||||
|
||||
use tauri_plugin_log::Target;
|
||||
use tauri_plugin_log::TargetKind;
|
||||
@@ -50,7 +49,7 @@ pub fn run() {
|
||||
//}
|
||||
let app = app.handle().clone();
|
||||
|
||||
tauri::async_runtime::spawn(sora_session_login_thread(app, app_data_root2));
|
||||
tauri::async_runtime::spawn(login_thread(app, app_data_root2));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -58,9 +57,8 @@ pub fn run() {
|
||||
.manage(app_data_root)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
flip_image,
|
||||
open_sora_login_command,
|
||||
sora_image_generation_command,
|
||||
sora_image_remix_command,
|
||||
image_generation_command,
|
||||
open_login_command,
|
||||
])
|
||||
.run(tauri::generate_context!("tauri.conf.json"))
|
||||
.expect("error while running tauri application");
|
||||
@@ -2,5 +2,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
tauri_artcraft_app_lib::run();
|
||||
app_lib::run();
|
||||
}
|
||||
@@ -23,7 +23,7 @@ pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub async fn sora_session_login_thread(app: AppHandle, app_data_root: AppDataRoot) -> ! {
|
||||
pub async fn login_thread(app: AppHandle, app_data_root: AppDataRoot) -> ! {
|
||||
loop {
|
||||
for (window_name, webview) in app.webviews() {
|
||||
if window_name == LOGIN_WINDOW_NAME {
|
||||
@@ -0,0 +1 @@
|
||||
pub mod login_thread;
|
||||
@@ -2,12 +2,12 @@
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.1.0",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"identifier": "com.unique.value.thing.why",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/editor2d/dist",
|
||||
"devUrl": "http://localhost:5741",
|
||||
"beforeDevCommand": "nx run editor2d:dev",
|
||||
"beforeBuildCommand": "nx run editor2d:build"
|
||||
"frontendDist": "../../../frontend/realtime/dist",
|
||||
"devUrl": "http://localhost:5174",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build2"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_library_loading() {
|
||||
let name = libloading::library_filename("cublas");
|
||||
|
||||
assert_eq!(name, "libcublas.so");
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod flip_image;
|
||||
pub mod sora;
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod open_sora_login_command;
|
||||
pub mod sora_image_generation_command;
|
||||
pub mod sora_image_remix_command;
|
||||
@@ -1,100 +0,0 @@
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use errors::AnyhowResult;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::{DynamicImage, ImageReader};
|
||||
use log::{error, info};
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use openai_sora_client::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use openai_sora_client::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
|
||||
use openai_sora_client::upload::upload_media_from_file::SoraMediaUploadRequest;
|
||||
use serde_derive::Deserialize;
|
||||
use std::fs::read_to_string;
|
||||
use std::io::Cursor;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SoraImageRemixCommand {
|
||||
/// Image media file; the engine or canvas snapshot (screenshot).
|
||||
pub snapshot_media_token: MediaFileToken,
|
||||
|
||||
/// The user's image generation prompt.
|
||||
pub prompt: String,
|
||||
|
||||
/// Turn off the system prompt.
|
||||
pub disable_system_prompt: Option<bool>,
|
||||
|
||||
/// Additional images to include (optional). Up to nine images.
|
||||
pub maybe_additional_images: Option<Vec<MediaFileToken>>,
|
||||
|
||||
pub maybe_number_of_samples: Option<u32>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sora_image_remix_command(
|
||||
_app: AppHandle,
|
||||
request: SoraImageRemixCommand,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
) -> Result<String, String> {
|
||||
info!("image_generation_command called; processing image...");
|
||||
|
||||
generate_image(request, &app_data_root)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("error: {:?}", err);
|
||||
"there was an error".to_string()
|
||||
})?;
|
||||
|
||||
Ok("success".to_string())
|
||||
}
|
||||
|
||||
pub async fn generate_image(request: SoraImageRemixCommand, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
let sora_credentials = get_credentials(app_data_root)?;
|
||||
|
||||
let sora_media_tokens = vec![];
|
||||
|
||||
// TODO(bt,2025-04-21): Download media tokens.
|
||||
// Note: This is incredibly inefficient. We should keep a local cache.
|
||||
// Also, if they've already been uploaded to OpenAI, we shouldn't continue to re-upload.
|
||||
|
||||
let response = sora_image_gen_remix(SoraImageGenRemixRequest {
|
||||
prompt: request.prompt.to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: &sora_credentials,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to call Sora image generation: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
println!(">> TASK ID: {:?} ", response.task_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_credentials(app_data_root: &AppDataRoot) -> AnyhowResult<SoraCredentials> {
|
||||
let cookie_file = app_data_root.get_sora_cookie_file_path();
|
||||
|
||||
let bearer = read_to_string("/Users/bt/dev/storyteller/storyteller-rust/test_data/temp/bearer.txt")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let cookie= read_to_string(cookie_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let sentinel = read_to_string("/Users/bt/dev/storyteller/storyteller-rust/test_data/temp/sentinel.txt")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
Ok(SoraCredentials {
|
||||
bearer_token: bearer,
|
||||
cookie: cookie,
|
||||
sentinel: Some(sentinel),
|
||||
})
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod sora_session_login_thread;
|
||||
@@ -1,5 +0,0 @@
|
||||
# For some reason, we can't pass json files to the tauri dev command
|
||||
[build]
|
||||
frontendDist = "../../../frontend/apps/editor2d/dist"
|
||||
beforeDevCommand = "nx run editor2d:dev"
|
||||
devUrl = "http://localhost:5741"
|
||||
@@ -1,5 +0,0 @@
|
||||
# For some reason, we can't pass json files to the tauri dev command
|
||||
[build]
|
||||
frontendDist = "../../../frontend/apps/editor3d/dist"
|
||||
beforeDevCommand = "nx run editor3d:dev"
|
||||
devUrl = "http://localhost:5173"
|
||||
@@ -83,16 +83,16 @@ pub struct Generation {
|
||||
pub created_at: String,
|
||||
pub deleted_at: Option<String>,
|
||||
pub url: String,
|
||||
pub seed: Option<i64>,
|
||||
pub can_download: Option<bool>,
|
||||
pub download_status: Option<String>,
|
||||
pub is_favorite: Option<bool>,
|
||||
pub is_liked: Option<bool>,
|
||||
pub is_public: Option<bool>,
|
||||
pub is_archived: Option<bool>,
|
||||
pub is_featured: Option<bool>,
|
||||
pub has_feedback: Option<bool>,
|
||||
pub like_count: Option<i32>,
|
||||
pub seed: i64,
|
||||
pub can_download: bool,
|
||||
pub download_status: String,
|
||||
pub is_favorite: bool,
|
||||
pub is_liked: bool,
|
||||
pub is_public: bool,
|
||||
pub is_archived: bool,
|
||||
pub is_featured: bool,
|
||||
pub has_feedback: bool,
|
||||
pub like_count: i32,
|
||||
pub cloudflare_metadata: Option<serde_json::Value>,
|
||||
pub cf_thumbnail_url: Option<String>,
|
||||
pub encodings: Encodings,
|
||||
|
||||
@@ -2,5 +2,4 @@ pub mod credentials;
|
||||
pub mod image_gen;
|
||||
pub mod job;
|
||||
pub mod upload;
|
||||
pub mod sentinel_refresh;
|
||||
mod sora_error;
|
||||
pub mod sentinel_refresh;
|
||||
@@ -1,48 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SoraError {
|
||||
/// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login.
|
||||
///
|
||||
/// Example message, e.g. from the upload endpoint:
|
||||
/// {
|
||||
/// "error": {
|
||||
/// "message": "Your authentication token has expired. Please try signing in again.",
|
||||
/// "type": "invalid_request_error",
|
||||
/// "param": null,
|
||||
/// "code": "token_expired"
|
||||
/// }
|
||||
/// }
|
||||
UnauthorizedCookieOrBearerExpired,
|
||||
|
||||
/// Another error occurred.
|
||||
OtherBadStatus(anyhow::Error),
|
||||
|
||||
/// Reqwest Error
|
||||
ReqwestError(reqwest::Error),
|
||||
}
|
||||
|
||||
impl Error for SoraError {}
|
||||
|
||||
impl Display for SoraError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SoraError::UnauthorizedCookieOrBearerExpired => {
|
||||
write!(f, "Unauthorized: cookie and/or bearer token expired")
|
||||
}
|
||||
SoraError::OtherBadStatus(err) => {
|
||||
write!(f, "Other error: {}", err)
|
||||
}
|
||||
SoraError::ReqwestError(err) => {
|
||||
write!(f, "Reqwest error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for SoraError {
|
||||
fn from(err: reqwest::Error) -> SoraError {
|
||||
SoraError::ReqwestError(err)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ use errors::AnyhowResult;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use crate::sora_error::SoraError;
|
||||
|
||||
const SORA_UPLOAD_MEDIA_URL: &str = "https://sora.com/backend/uploads";
|
||||
|
||||
@@ -26,17 +25,6 @@ Response:
|
||||
"size_bytes": 118501,
|
||||
"thumbnail_url": "https://videos.openai.com/vg-assets/assets%2Fclient_upload%2Fmedia%2Fuser-9wf6JFdgRSJLjvSJ53LNAbV8%2Fmedia_01jqt9vt20erx9zvryf3v1pecx.jpg?st=2025-04-02T02%3A39%3A01Z&se=2025-04-08T03%3A39%3A01Z&sks=b&skt=2025-04-02T02%3A39%3A01Z&ske=2025-04-08T03%3A39%3A01Z&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skoid=3d249c53-07fa-4ba4-9b65-0bf8eb4ea46a&skv=2019-02-02&sv=2018-11-09&sr=b&sp=r&spr=https%2Chttp&sig=9aU9xKI0BkhRtrTb0RD05%2BT6prBnmXk1I7nV2nTsFbY%3D&az=oaivgprodscus"
|
||||
}
|
||||
|
||||
Error response:
|
||||
|
||||
Failed to upload scene media to Sora: Upload failed with status 401 Unauthorized: {
|
||||
"error": {
|
||||
"message": "Your authentication token has expired. Please try signing in again.",
|
||||
"type": "invalid_request_error",
|
||||
"param": null,
|
||||
"code": "token_expired"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -62,13 +50,7 @@ pub (crate) struct SoraMediaUploadRequest<'a> {
|
||||
pub credentials: &'a SoraCredentials,
|
||||
}
|
||||
|
||||
pub (crate) async fn upload_media_http_request(
|
||||
file_bytes: Vec<u8>,
|
||||
filename: String,
|
||||
mime_type: &str,
|
||||
credentials: &SoraCredentials,
|
||||
) -> Result<SoraMediaUploadResponse, SoraError> {
|
||||
|
||||
pub (crate) async fn upload_media_http_request(file_bytes: Vec<u8>, filename: String, mime_type: &str, credentials: &SoraCredentials) -> AnyhowResult<SoraMediaUploadResponse> {
|
||||
// Create multipart form
|
||||
let part = Part::bytes(file_bytes) // NB: Reqwest needs to own the bytes.
|
||||
.file_name(filename) // NB: Reqwest needs to own the bytes
|
||||
@@ -88,29 +70,12 @@ pub (crate) async fn upload_media_http_request(
|
||||
|
||||
// Check response status
|
||||
if !response.status().is_success() {
|
||||
let error = classify_error(response).await;
|
||||
return Err(error);
|
||||
let status = response.status();
|
||||
let text = response.text().await?;
|
||||
return Err(anyhow::anyhow!("Upload failed with status {}: {}", status, text));
|
||||
}
|
||||
|
||||
// Parse response
|
||||
let upload_response = response.json::<SoraMediaUploadResponse>().await?;
|
||||
Ok(upload_response)
|
||||
}
|
||||
|
||||
async fn classify_error(response: reqwest::Response) -> SoraError {
|
||||
let status = response.status();
|
||||
let message = match response.text().await {
|
||||
Ok(text) => text,
|
||||
Err(err) => return SoraError::ReqwestError(err),
|
||||
};
|
||||
|
||||
let cookie_expired =
|
||||
message.contains("Your authentication token has expired. Please try signing in again.")
|
||||
|| message.contains("token_expired");
|
||||
|
||||
if cookie_expired {
|
||||
SoraError::UnauthorizedCookieOrBearerExpired
|
||||
} else {
|
||||
SoraError::OtherBadStatus(anyhow::anyhow!("Upload failed with status {}: {}", status, message))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
/// NB: `sqlx::query` is spammy and dumps all queries as "info"-level log lines.
|
||||
/// NB: `hyper::proto::h1::io` is incredibly spammy and logs every chunk of bytes in very large files being downloaded.
|
||||
pub const DEFAULT_RUST_LOG: &str = concat!(
|
||||
"debug,",
|
||||
"actix_web=info,",
|
||||
"sqlx::query=warn,",
|
||||
"hyper::proto::h1::io=warn,",
|
||||
|
||||
@@ -68,7 +68,7 @@ reusable_types = { path = "../../../_types/reusable_types" }
|
||||
server_environment = { path = "../../../lib/server_environment" }
|
||||
shared_service_components = { workspace = true }
|
||||
thumbnail_generator = { path = "../../../lib/api_clients/thumbnail_generator" }
|
||||
tokens = { workspace = true }
|
||||
tokens = { path = "../../../_types/tokens" }
|
||||
tts_common = { path = "../../../lib/tts_common" }
|
||||
url_config = { path = "../../../lib/url_config" }
|
||||
user_input_common = { path = "../../../lib/user_input_common" }
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
This is an `nx` monorepo that can contain multiple apps and shared libraries.
|
||||
|
||||
All commands to run these projects are performed from _this_ directory.
|
||||
All commands to run these projects are performed from _this_ directory.
|
||||
|
||||
|
||||
## Install dependencies
|
||||
|
||||
@@ -26,33 +27,4 @@ Run dev server:
|
||||
nx dev editor3d
|
||||
```
|
||||
|
||||
```
|
||||
call when starting on main or a new branch:
|
||||
./clean_modules
|
||||
|
||||
The names for @frontend and @storyteller come from the package.json file in the libs folder.
|
||||
import { Login } from "@frontend/login";
|
||||
import { api } from "@storyteller/api";
|
||||
|
||||
For shared UI components, import from @storyteller/ui-[componentname].
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
|
||||
building:
|
||||
npx nx build editor3d
|
||||
npx nx build editor2d
|
||||
|
||||
dev:
|
||||
npx nx dev editor3d
|
||||
npx nx dev editor2d
|
||||
|
||||
|
||||
|
||||
# component build
|
||||
1.
|
||||
npx nx g @nx/react:library libs/components/toaster --import-path=@storyteller/ui-toaster --bundler=vite
|
||||
npm install
|
||||
2.import it first in code
|
||||
3.nx sync
|
||||
4.nx build
|
||||
```
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"nx": {
|
||||
"name": "editor2d",
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
}
|
||||
"name": "editor2d"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "echo 'editor2d' && vite",
|
||||
@@ -22,12 +15,14 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storyteller/shared": "file:../../shared/*",
|
||||
"@devnomic/marquee": "^1.0.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@headlessui/react": "^2.1.3",
|
||||
"@preact/signals-react": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||
|
||||
@@ -62,7 +62,6 @@ export class EditEngine {
|
||||
this.editMode = EditModes.SELECT;
|
||||
|
||||
this.boardCanvasRef = boardCanvasRef;
|
||||
|
||||
this.stage = new Konva.Stage({
|
||||
container: this.boardCanvasRef,
|
||||
width: window.innerWidth,
|
||||
@@ -239,19 +238,13 @@ export class EditEngine {
|
||||
}
|
||||
|
||||
private onBoardCanvasResize() {
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(0, 0);
|
||||
console.log("onBoardCanvasResize Event called");
|
||||
console.log("Board Canvas values below");
|
||||
console.log(this.boardCanvasRef.offsetWidth);
|
||||
console.log(this.boardCanvasRef.offsetHeight);
|
||||
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(undefined, undefined);
|
||||
this.matteBox.updateSize({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.offsetWidth,
|
||||
height: this.boardCanvasRef.offsetHeight,
|
||||
},
|
||||
});
|
||||
|
||||
this.uiLayer.draw();
|
||||
this.stage.width(this.boardCanvasRef.offsetWidth);
|
||||
this.stage.height(this.boardCanvasRef.offsetHeight);
|
||||
|
||||
@@ -510,20 +510,7 @@ export class Engine {
|
||||
}
|
||||
|
||||
private onBoardCanvasResize() {
|
||||
console.log("onBoardCanvasResize Event called");
|
||||
// Print window dimensions for debugging
|
||||
console.log("Window dimensions:", {
|
||||
innerWidth: window.innerWidth,
|
||||
innerHeight: window.innerHeight,
|
||||
outerWidth: window.outerWidth,
|
||||
outerHeight: window.outerHeight,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(
|
||||
window.innerWidth,
|
||||
window.innerHeight,
|
||||
);
|
||||
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(undefined, undefined);
|
||||
this.matteBox.updateSize({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.offsetWidth,
|
||||
|
||||
@@ -516,74 +516,36 @@ export class RealTimeDrawEngine {
|
||||
width: number | undefined,
|
||||
height: number | undefined,
|
||||
) {
|
||||
console.log("updateCaptureCanvas");
|
||||
if (!this.captureCanvas) {
|
||||
return;
|
||||
}
|
||||
const scaleFactor = 0.9;
|
||||
// Store old values for calculating delta
|
||||
if (width) {
|
||||
this.width = width;
|
||||
}
|
||||
if (height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
// Ensures that all the nodes stag in the same place should
|
||||
// there be a window resize.
|
||||
// recompute the position
|
||||
// to ensure that the position of this stays
|
||||
|
||||
const oldPositionX = this.positionX;
|
||||
const oldPositionY = this.positionY;
|
||||
|
||||
const oldWidth = this.width;
|
||||
const oldHeight = this.height;
|
||||
// recompute the position
|
||||
|
||||
// Original aspect ratio
|
||||
const originalAspectRatio = oldWidth / oldHeight;
|
||||
|
||||
// Calculate new dimensions based on window size
|
||||
let newWidth: number;
|
||||
let newHeight: number;
|
||||
|
||||
if (width === undefined) {
|
||||
newWidth = window.innerWidth * scaleFactor;
|
||||
} else {
|
||||
newWidth = width * scaleFactor;
|
||||
}
|
||||
|
||||
if (height === undefined) {
|
||||
newHeight = window.innerHeight * scaleFactor;
|
||||
} else {
|
||||
newHeight = height * scaleFactor;
|
||||
}
|
||||
|
||||
// Preserve aspect ratio
|
||||
const newAspectRatio = newWidth / newHeight;
|
||||
|
||||
if (newAspectRatio > originalAspectRatio) {
|
||||
// Width is constraining factor
|
||||
newWidth = newHeight * originalAspectRatio;
|
||||
} else {
|
||||
// Height is constraining factor
|
||||
newHeight = newWidth / originalAspectRatio;
|
||||
}
|
||||
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
|
||||
console.log("New dimensions:", this.width, this.height);
|
||||
|
||||
// Calculate new position to center the canvas in the window
|
||||
this.positionX = (window.innerWidth - this.width) / 2;
|
||||
this.positionY = (window.innerHeight - this.height) / 2;
|
||||
|
||||
console.log("New centered position:", this.positionX, this.positionY);
|
||||
|
||||
// Update the canvas position and size
|
||||
this.captureCanvas.setPosition({
|
||||
x: this.positionX,
|
||||
y: this.positionY,
|
||||
x: this.positionX / 2,
|
||||
y: this.positionY / 2,
|
||||
});
|
||||
this.captureCanvas.size({ width: this.width, height: this.height });
|
||||
|
||||
// Calculate scale factors to maintain proportions
|
||||
const scaleX = this.width / oldWidth;
|
||||
const scaleY = this.height / oldHeight;
|
||||
// this is the change in positions
|
||||
const deltaX = this.positionX - oldPositionX;
|
||||
const deltaY = this.positionY - oldPositionY;
|
||||
|
||||
// Use a uniform scale factor to preserve aspect ratio
|
||||
const uniformScale = Math.min(scaleX, scaleY);
|
||||
|
||||
// Update all children except the capture canvas
|
||||
var children = this.mediaLayerRef.getChildren();
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let node = children[i];
|
||||
@@ -592,27 +554,11 @@ export class RealTimeDrawEngine {
|
||||
if (node.name() === "CaptureCanvas") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pos = node.getPosition();
|
||||
const oldScale = node.scale();
|
||||
|
||||
// Apply both position shift and scaling to maintain relative positioning
|
||||
node.setPosition({
|
||||
x: (pos.x - oldPositionX) * uniformScale + this.positionX,
|
||||
y: (pos.y - oldPositionY) * uniformScale + this.positionY,
|
||||
x: pos.x + deltaX,
|
||||
y: pos.y + deltaY,
|
||||
});
|
||||
|
||||
// Update scale proportionally if the node has a scale property
|
||||
if (
|
||||
oldScale &&
|
||||
typeof oldScale.x === "number" &&
|
||||
typeof oldScale.y === "number"
|
||||
) {
|
||||
node.scale({
|
||||
x: oldScale.x * uniformScale,
|
||||
y: oldScale.y * uniformScale,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.mediaLayerRef.batchDraw();
|
||||
|
||||
@@ -2,38 +2,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #363636 #171717;
|
||||
}
|
||||
|
||||
/* Works on Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #171717;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #363636;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #171717;
|
||||
}
|
||||
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@apply select-none bg-ui-background font-sans text-white;
|
||||
@@ -59,7 +27,28 @@ input[type="number"] {
|
||||
background-size: 36px 36px;
|
||||
@apply bg-center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-3xl font-medium md:text-5xl;
|
||||
}
|
||||
h2 {
|
||||
@apply text-2xl font-medium md:text-4xl;
|
||||
}
|
||||
h3 {
|
||||
@apply text-xl font-medium md:text-3xl;
|
||||
}
|
||||
h4 {
|
||||
@apply text-lg font-medium;
|
||||
}
|
||||
h5,
|
||||
label {
|
||||
@apply text-base;
|
||||
}
|
||||
h6 {
|
||||
@apply text-sm;
|
||||
}
|
||||
p {
|
||||
@apply text-sm font-normal;
|
||||
}
|
||||
a {
|
||||
@apply font-medium text-primary-400 hover:text-primary-300;
|
||||
}
|
||||
@@ -136,10 +125,6 @@ input[type="number"] {
|
||||
@apply relative border border-ui-border shadow-lg backdrop-blur-xl transition-colors duration-300;
|
||||
@apply bg-ui-controls/50 hover:bg-ui-controls/90 !important;
|
||||
}
|
||||
|
||||
.glass-no-hover {
|
||||
@apply hover:bg-ui-controls/50 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RouterProvider } from "react-router-dom";
|
||||
import { router } from "./router";
|
||||
import { posthog } from "posthog-js";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import { Toaster } from "~/components/ui/Toast";
|
||||
import { Toaster } from "@storyteller/shared/components";
|
||||
import { JobProvider } from "~/components/JobContext";
|
||||
import "./global.css";
|
||||
import { useRenderCounter } from "~/hooks/useRenderCounter";
|
||||
@@ -33,7 +33,7 @@ const App = () => {
|
||||
<JobProvider>
|
||||
<PostHogProvider client={posthog}>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
<Toaster topOffset={70} rightOffset={12} />
|
||||
</PostHogProvider>
|
||||
</JobProvider>
|
||||
</StrictMode>
|
||||
@@ -43,7 +43,7 @@ const App = () => {
|
||||
<>
|
||||
<JobProvider>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
<Toaster topOffset={70} rightOffset={12} />
|
||||
</JobProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useRef } from "react";
|
||||
import { useNavigate, useSearchParams, Link } from "react-router-dom";
|
||||
import { useSignalEffect } from "@preact/signals-react/runtime";
|
||||
|
||||
import { faKey, faUser } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, Input, LoadingSpinner } from "~/components/ui";
|
||||
import { authentication } from "~/signals";
|
||||
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const Login = () => {
|
||||
@@ -59,21 +62,24 @@ export const Login = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pegboard fixed flex h-full w-full flex-col items-center justify-center bg-center bg-repeat">
|
||||
<div className="mb-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
|
||||
<div className="fixed flex h-full w-full flex-col items-center justify-center bg-[url('/svg/bg-dots.svg')] bg-[length:200px_200px] bg-center bg-repeat">
|
||||
<div className="my-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
|
||||
<img
|
||||
src="/brand/artcraft-logo.png"
|
||||
alt="ArtCraft Logo"
|
||||
className="h-9 select-none"
|
||||
src="/brand/Storyteller-Logo-Black.png"
|
||||
alt="Storyteller Logo"
|
||||
className="h-11 select-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full max-w-xl rounded-xl shadow-lg">
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass glass-no-hover relative mx-auto w-full rounded-none rounded-t-xl p-6 shadow-none",
|
||||
paperWrapperStyles,
|
||||
"relative mx-auto w-full rounded-none rounded-t-xl p-8 shadow-none",
|
||||
)}
|
||||
>
|
||||
<h1 className="mb-6 text-center text-2xl font-bold">Log in</h1>
|
||||
<h1 className="mb-9 text-center text-2xl font-bold">
|
||||
Log in to Board
|
||||
</h1>
|
||||
<form
|
||||
ref={formRef}
|
||||
onSubmit={handleOnSumbit}
|
||||
@@ -100,7 +106,7 @@ export const Login = () => {
|
||||
required
|
||||
/>
|
||||
|
||||
<Button className="mt-4 py-3">Continue</Button>
|
||||
<Button className="mt-6 py-3">Continue</Button>
|
||||
</form>
|
||||
{shouldShowLoader && (
|
||||
<div className="absolute left-0 top-0 flex h-full w-full items-center justify-center">
|
||||
@@ -108,10 +114,10 @@ export const Login = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="align-items flex w-full max-w-xl rounded-b-xl bg-ui-controls px-6 py-4">
|
||||
<div className="align-items flex w-full max-w-xl rounded-b-xl border border-t-0 bg-gray-100 px-8 py-4">
|
||||
<a
|
||||
href="https://storyteller.ai/password-reset"
|
||||
className="grow text-sm !text-primary-400 transition-all duration-150 hover:!text-primary-300"
|
||||
className="text-brand-primary hover:text-brand-primary-400 grow text-sm transition-all duration-150"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { withProtectionRoute } from "~/components/hoc";
|
||||
import { useRenderCounter } from "~/hooks/useRenderCounter";
|
||||
import { Resource, invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// Components of the page
|
||||
import { ToolbarUserProfile } from "~/components/features";
|
||||
@@ -15,22 +14,10 @@ import { appMode } from "~/signals";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { GenerationRootComponent } from "~/GenerationRootComponent/GenerationRootComponent";
|
||||
import { EditModeRootComponent } from "~/EditModeRootComponent/EditModeRootComponent";
|
||||
import { useRef, useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { EngineType } from "~/KonvaApp";
|
||||
import { GenerationEngine } from "~/KonvaApp/GenerationEngine";
|
||||
import { GalleryRootComponent } from "~/GalleryRootComponent";
|
||||
import { BaseDialog } from "~/components/ui/BaseDialog";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import {
|
||||
faArrowRight,
|
||||
faMagicWandSparkles,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export const isTauri = (): boolean => {
|
||||
console.log(window.__TAURI_INTERNALS__);
|
||||
return window.__TAURI_INTERNALS__;
|
||||
};
|
||||
|
||||
export const Main = withProtectionRoute(() => {
|
||||
// This is a hook that will log the number of times the component has rerendered
|
||||
@@ -45,8 +32,7 @@ export const Main = withProtectionRoute(() => {
|
||||
|
||||
const realtimeEngineRef = useRef<EngineType | null>(null);
|
||||
const generationEngineRef = useRef<GenerationEngine | null>(null);
|
||||
const [firstTimeDialogOpen, setFirstTimeDialogOpen] = useState(true);
|
||||
const [signinDialogOpen, setSigninDialogOpen] = useState(false);
|
||||
|
||||
const appModeValue = appMode.value;
|
||||
let childView;
|
||||
|
||||
@@ -74,87 +60,26 @@ export const Main = withProtectionRoute(() => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed h-full w-full">
|
||||
<div className="fixed grid h-full w-full grid-cols-12 grid-rows-12">
|
||||
{childView}
|
||||
</div>
|
||||
<div className="absolute top-0 w-full p-3.5">
|
||||
<div className="relative flex w-full">
|
||||
<div className="absolute left-0">
|
||||
<ToolbarTopLeft />
|
||||
</div>
|
||||
<div className="flex w-full justify-center">
|
||||
<ToolbarTopCenter />
|
||||
</div>
|
||||
<div className="absolute right-0">
|
||||
<ToolbarTopRight />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{appModeValue === "gallery" && (
|
||||
<GalleryRootComponent className="z-1 fixed h-full w-full bg-black" />
|
||||
)}
|
||||
<div className="fixed h-full w-full">
|
||||
<div className="fixed grid h-full w-full grid-cols-12 grid-rows-12">
|
||||
{childView}
|
||||
</div>
|
||||
<BaseDialog
|
||||
isOpen={firstTimeDialogOpen}
|
||||
onClose={() => {
|
||||
setFirstTimeDialogOpen(false);
|
||||
}}
|
||||
className="max-w-5xl"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<h1 className="text-3xl font-bold">
|
||||
<FontAwesomeIcon
|
||||
icon={faMagicWandSparkles}
|
||||
className="mr-3 text-[24px]"
|
||||
/>
|
||||
Welcome to ArtCraft
|
||||
</h1>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-medium text-white/80">
|
||||
Your creative canvas for digital art and design
|
||||
</p>
|
||||
<p className="text-white/60">
|
||||
Draw freely, create collages, and design with our intuitive
|
||||
tool. Bring your ideas to life in seconds!
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute top-0 w-full p-3.5">
|
||||
<div className="relative flex w-full">
|
||||
<div className="absolute left-0">
|
||||
<ToolbarTopLeft />
|
||||
</div>
|
||||
|
||||
<div className="aspect-video w-full overflow-hidden rounded-md">
|
||||
<video autoPlay muted loop controls={false}>
|
||||
<source src="/videos/demo_video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<div className="flex w-full justify-center">
|
||||
<ToolbarTopCenter />
|
||||
</div>
|
||||
<div className="absolute right-0">
|
||||
<ToolbarTopRight />
|
||||
</div>
|
||||
<Button
|
||||
className="font-semibold"
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
onClick={async () => {
|
||||
if (isTauri()) {
|
||||
await invoke('open_sora_login_command');
|
||||
setFirstTimeDialogOpen(false);
|
||||
} else {
|
||||
console.error('Tauri is not available in this environment');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Signin to OpenAI to get Started
|
||||
</Button>
|
||||
{/* <Button
|
||||
className="font-semibold"
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
onClick={() => {
|
||||
setFirstTimeDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
Start creating now
|
||||
</Button> */}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</>
|
||||
</div>
|
||||
{appModeValue === "gallery" && (
|
||||
<GalleryRootComponent className="z-1 fixed h-full w-full bg-black" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useRef } from "react";
|
||||
import { useNavigate, useSearchParams, Link } from "react-router-dom";
|
||||
import { useSignalEffect } from "@preact/signals-react/runtime";
|
||||
|
||||
import { faKey, faUser } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button, Input, LoadingSpinner } from "~/components/ui";
|
||||
import { authentication } from "~/signals";
|
||||
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const Signup = () => {
|
||||
@@ -74,21 +77,22 @@ export const Signup = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pegboard fixed flex h-full w-full flex-col items-center justify-center bg-center bg-repeat">
|
||||
<div className="mb-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
|
||||
<div className="fixed flex h-full w-full flex-col items-center justify-center bg-[url('/svg/bg-dots.svg')] bg-[length:200px_200px] bg-center bg-repeat">
|
||||
<div className="my-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
|
||||
<img
|
||||
src="/brand/artcraft-logo.png"
|
||||
alt="ArtCraft Logo"
|
||||
className="h-9 select-none"
|
||||
src="/brand/Storyteller-Logo-Black.png"
|
||||
alt="Storyteller Logo"
|
||||
className="h-11 select-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full max-w-xl rounded-xl shadow-lg">
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass glass-no-hover relative mx-auto w-full rounded-none rounded-t-xl p-6 shadow-none",
|
||||
paperWrapperStyles,
|
||||
"relative mx-auto w-full rounded-none rounded-t-xl p-8 shadow-none",
|
||||
)}
|
||||
>
|
||||
<h1 className="mb-6 text-center text-2xl font-bold">
|
||||
<h1 className="mb-9 text-center text-2xl font-bold">
|
||||
Create your Account
|
||||
</h1>
|
||||
<form
|
||||
@@ -135,7 +139,7 @@ export const Signup = () => {
|
||||
required
|
||||
/>
|
||||
|
||||
<Button className="mt-4 py-3">Create Account</Button>
|
||||
<Button className="mt-6 py-3">Create Account</Button>
|
||||
</form>
|
||||
{shouldShowLoader && (
|
||||
<div className="absolute left-0 top-0 flex h-full w-full items-center justify-center">
|
||||
@@ -143,7 +147,7 @@ export const Signup = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="align-items flex w-full max-w-xl rounded-b-xl bg-ui-controls px-6 py-4">
|
||||
<div className="align-items flex w-full max-w-xl rounded-b-xl border border-t-0 bg-gray-100 px-8 py-4">
|
||||
<div className="flex w-full justify-end gap-1 text-sm">
|
||||
<p className="opacity-75">Already have an account?</p>
|
||||
<Link to="/login">Log In</Link>
|
||||
|
||||
@@ -177,100 +177,7 @@ export const PromptBox = () => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setPrompt(e.target.value);
|
||||
};
|
||||
const handleTauriEnqueue = async () => {
|
||||
const api = new Api();
|
||||
let image = getCanvasRenderBitmap();
|
||||
if (image === undefined) {
|
||||
return;
|
||||
}
|
||||
const base64Bitmap = await EncodeImageBitmapToBase64(image);
|
||||
|
||||
const byteString = atob(base64Bitmap);
|
||||
const mimeString = "image/png";
|
||||
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const uuid = crypto.randomUUID(); // Generate a new UUID
|
||||
const file = new File([ab], `${uuid}.png`, { type: mimeString });
|
||||
|
||||
const snapshotMediaToken = await api.uploadSceneSnapshot({
|
||||
screenshot: file,
|
||||
});
|
||||
|
||||
if (snapshotMediaToken.data === undefined) {
|
||||
toast.error("Error: Unable to upload scene snapshot Please try again.");
|
||||
return;
|
||||
}
|
||||
console.log("useSystemPrompt", useSystemPrompt);
|
||||
console.log("Snapshot media token:", snapshotMediaToken.data);
|
||||
|
||||
const generateResponse = await invoke("sora_image_generation_command", {
|
||||
snapshot_media_token: snapshotMediaToken.data,
|
||||
disable_system_prompt: !useSystemPrompt,
|
||||
prompt: prompt,
|
||||
maybe_additional_images: referenceImages.map((image) => image.mediaToken),
|
||||
maybe_number_of_samples: 1,
|
||||
});
|
||||
toast.success("Please wait while we process your image.");
|
||||
};
|
||||
|
||||
const handleWebEnqueue = async () => {
|
||||
const api = new Api();
|
||||
let image = getCanvasRenderBitmap();
|
||||
if (image === undefined) {
|
||||
toast.error(
|
||||
"Error: Unable to generate image. Please check the input and try again.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const base64Bitmap = await EncodeImageBitmapToBase64(image);
|
||||
|
||||
const byteString = atob(base64Bitmap);
|
||||
const mimeString = "image/png";
|
||||
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const uuid = crypto.randomUUID(); // Generate a new UUID
|
||||
const file = new File([ab], `${uuid}.png`, { type: mimeString });
|
||||
|
||||
const snapshotMediaToken = await api.uploadSceneSnapshot({
|
||||
screenshot: file,
|
||||
});
|
||||
|
||||
if (snapshotMediaToken.data === undefined) {
|
||||
toast.error("Error: Unable to upload scene snapshot Please try again.");
|
||||
return;
|
||||
}
|
||||
console.log("useSystemPrompt", useSystemPrompt);
|
||||
console.log("Snapshot media token:", snapshotMediaToken.data);
|
||||
|
||||
const response = await api.enqueueImageGeneration({
|
||||
disableSystemPrompt: !useSystemPrompt,
|
||||
prompt: prompt,
|
||||
snapshotMediaToken: snapshotMediaToken.data,
|
||||
additionalImages: referenceImages.map((image) => image.mediaToken),
|
||||
});
|
||||
|
||||
if (response.success === true) {
|
||||
toast.success("Please wait while we process your image.");
|
||||
if (response.data) {
|
||||
addJobToken(response.data);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
toast.error("Failed to enqueue image generation. Please try again.");
|
||||
}
|
||||
};
|
||||
const handleEnqueue = async () => {
|
||||
if (!prompt.trim()) return;
|
||||
|
||||
@@ -284,10 +191,69 @@ export const PromptBox = () => {
|
||||
referenceImages,
|
||||
);
|
||||
|
||||
if (isDesktopApp == true) {
|
||||
handleTauriEnqueue();
|
||||
} else {
|
||||
handleWebEnqueue();
|
||||
if (isDesktopApp) {
|
||||
let image = getCanvasRenderBitmap();
|
||||
if (image === undefined) {
|
||||
return;
|
||||
}
|
||||
const base64Bitmap = await EncodeImageBitmapToBase64(image);
|
||||
const generateResponse = await invoke("image_generation_command", {
|
||||
image: base64Bitmap,
|
||||
prompt: prompt,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
if (!isDesktopApp) {
|
||||
const api = new Api();
|
||||
let image = getCanvasRenderBitmap();
|
||||
if (image === undefined) {
|
||||
toast.error(
|
||||
"Error: Unable to generate image. Please check the input and try again.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const base64Bitmap = await EncodeImageBitmapToBase64(image);
|
||||
const byteString = atob(base64Bitmap);
|
||||
const mimeString = "image/png";
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const uuid = crypto.randomUUID(); // Generate a new UUID
|
||||
const file = new File([ab], `${uuid}.png`, { type: mimeString });
|
||||
|
||||
const snapshotMediaToken = await api.uploadSceneSnapshot({
|
||||
screenshot: file,
|
||||
});
|
||||
|
||||
if (snapshotMediaToken.data === undefined) {
|
||||
toast.error(
|
||||
"Error: Unable to upload scene snapshot Please try again.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log("useSystemPrompt", useSystemPrompt);
|
||||
console.log("Snapshot media token:", snapshotMediaToken.data);
|
||||
const response = await api.enqueueImageGeneration({
|
||||
disableSystemPrompt: !useSystemPrompt,
|
||||
prompt: prompt,
|
||||
snapshotMediaToken: snapshotMediaToken.data,
|
||||
additionalImages: referenceImages.map((image) => image.mediaToken),
|
||||
});
|
||||
|
||||
if (response.success === true) {
|
||||
toast.success("Please wait while we process your image.");
|
||||
if (response.data) {
|
||||
addJobToken(response.data);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
toast.error("Failed to enqueue image generation. Please try again.");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during image generation:", error);
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
GenerationLoadingState,
|
||||
generationSignal,
|
||||
} from "~/signals";
|
||||
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
|
||||
// import { paperWrapperStyles } from "~/components/styles";
|
||||
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
|
||||
@@ -34,10 +32,6 @@ export const ToolbarTopLeft = () => {
|
||||
|
||||
const generationState = generationSignal.value;
|
||||
|
||||
const handleClick = () => {
|
||||
window.location.href = "https://storyteller-3d.netlify.app/";
|
||||
};
|
||||
|
||||
console.log(appMode.value);
|
||||
|
||||
return (
|
||||
@@ -52,25 +46,14 @@ export const ToolbarTopLeft = () => {
|
||||
className="h-[28px] select-none"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={faChevronRight}
|
||||
iconClassName="text-xs"
|
||||
iconFlip={true}
|
||||
onClick={handleClick}
|
||||
className="bg-transparent p-0 text-sm text-white/80 hover:bg-transparent hover:text-white hover:underline hover:underline-offset-2"
|
||||
>
|
||||
Go to 3D Stage Editor
|
||||
</Button>
|
||||
|
||||
{/* <TabSelector
|
||||
<TabSelector
|
||||
tabs={tabs}
|
||||
activeTab={activeTab.value}
|
||||
onTabChange={handleTabChange}
|
||||
disabled={
|
||||
generationState.loadingState === GenerationLoadingState.GENERATING
|
||||
}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
{/* <ToolbarButton icon={faPlus}>
|
||||
<span className="font-semibold">New Board</span>
|
||||
|
||||
@@ -1,32 +1,16 @@
|
||||
import { faGear } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Login } from "~/app/pages/login";
|
||||
import { Activity } from "~/components/ui/Activity/Activity";
|
||||
// import { paperWrapperStyles } from "~/components/styles";
|
||||
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
|
||||
// import { ToolbarButton } from "~/components/features/ToolbarButton";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { SettingsModal } from "@storyteller/ui-settings-modal";
|
||||
import { useState } from "react";
|
||||
|
||||
export const ToolbarTopRight = () => {
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={twMerge("relative z-50 flex h-fit w-fit items-center gap-2")}
|
||||
>
|
||||
<Button
|
||||
icon={faGear}
|
||||
className="h-[42px] w-[42px] bg-[#5F5F68]/60 backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
onClick={() => setIsSettingsModalOpen(true)}
|
||||
/>
|
||||
<Activity />
|
||||
</div>
|
||||
|
||||
<SettingsModal
|
||||
isOpen={isSettingsModalOpen}
|
||||
onClose={() => setIsSettingsModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
<div
|
||||
className={twMerge("relative z-50 flex h-fit w-fit items-center gap-2")}
|
||||
>
|
||||
<Activity />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CompletedCard } from "./CompletedCard";
|
||||
import { InProgressCard } from "./InProgressCard";
|
||||
import { useJobContext } from "~/components/JobContext";
|
||||
import { Api } from "~/KonvaApp/Api";
|
||||
import { toast } from "sonner";
|
||||
import { toast } from "@storyteller/shared/components";
|
||||
|
||||
// Dummy data types
|
||||
interface CompletedItem {
|
||||
@@ -199,7 +199,7 @@ export function Activity() {
|
||||
</div>
|
||||
) : completedItems.length === 0 && jobs.length === 0 ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
<h3 className="text-gray-300">No activities yet</h3>
|
||||
<h3 className="text-lg text-gray-300">No activities yet</h3>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
|
||||
@@ -4,11 +4,7 @@ import defaultTheme from "tailwindcss/defaultTheme";
|
||||
import { storytellerColors } from "./tailwind.stcolors";
|
||||
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"../../libs/components/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
@@ -48,42 +44,14 @@ export default {
|
||||
info: colors.sky[500],
|
||||
|
||||
// brand colors
|
||||
primary: {
|
||||
DEFAULT: "#2d81ff",
|
||||
50: "#eef6ff",
|
||||
100: "#d9ebff",
|
||||
200: "#bcdcff",
|
||||
300: "#8ec7ff",
|
||||
400: "#59a7ff",
|
||||
500: "#2d81ff",
|
||||
600: "#1b63f5",
|
||||
700: "#144ee1",
|
||||
800: "#173fb6",
|
||||
900: "#19398f",
|
||||
950: "#142457",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "#3E3E41",
|
||||
50: "#f6f6f6",
|
||||
100: "#e7e7e7",
|
||||
200: "#d1d1d1",
|
||||
300: "#b0b0b0",
|
||||
400: "#888888",
|
||||
500: "#6d6d6d",
|
||||
600: "#5d5d5d",
|
||||
700: "#4f4f4f",
|
||||
800: "#454545",
|
||||
900: "#3d3d3d",
|
||||
950: "#3E3E41",
|
||||
},
|
||||
|
||||
primary: storytellerColors.azureRadiance,
|
||||
secondary: storytellerColors.gunpowder,
|
||||
tertiary: storytellerColors.aquamarineBlue,
|
||||
|
||||
ui: {
|
||||
background: "#101014",
|
||||
panel: "#3E3E41",
|
||||
border: "#FFFFFF0D",
|
||||
"panel-border": "#3F3F3F",
|
||||
controls: "#3E3E41",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -21,18 +21,9 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
"@ApiManager/*": ["./src/Classes/ApiManager/*"]
|
||||
"@ApiManager/*": ["./src/Classes/ApiManager/*"],
|
||||
"@storyteller/shared/*": ["../../shared/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../libs/components/settings-modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/button/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../libs/components/settings-modal"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/button"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
_
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../**/*.mdx", "../**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-interactions",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: "sb-vite.config.ts",
|
||||
},
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,34 @@
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
module.exports = function expressMiddleware(router) {
|
||||
router.use(
|
||||
"/v1",
|
||||
createProxyMiddleware({
|
||||
target: "https://api.storyteller.ai/v1",
|
||||
changeOrigin: true,
|
||||
on: {
|
||||
proxyReq: async (req, res) => {
|
||||
req.setHeader("Access-Control-Allow-Origin", "*");
|
||||
req.setHeader("Origin", "http://localhost:5173");
|
||||
req.setHeader("Referer", "http://localhost:5173");
|
||||
},
|
||||
},
|
||||
logger: console,
|
||||
}),
|
||||
);
|
||||
router.use(
|
||||
"/cdn-cgi",
|
||||
createProxyMiddleware({
|
||||
target: "https://cdn.storyteller.ai/cdn-cgi",
|
||||
changeOrigin: true,
|
||||
on: {
|
||||
proxyReq: async (req, res) => {
|
||||
req.setHeader("Access-Control-Allow-Origin", "*");
|
||||
req.setHeader("Origin", "http://localhost:5173");
|
||||
req.setHeader("Referer", "http://localhost:5173");
|
||||
},
|
||||
},
|
||||
logger: console,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
|
||||
Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "tailwindcss/tailwind.css";
|
||||
import "../app/styles/tailwind.css";
|
||||
import "../app/styles/normalize.css?url";
|
||||
import "../app//styles/tailwind.css?url";
|
||||
import "../app//styles/base.css?url";
|
||||
import { MemoryRouter } from "react-router";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>,
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||