Compare commits

..
Author SHA1 Message Date
Brandon Thomas 585320c499 cleanup routes 2026-03-11 00:12:51 -04:00
Brandon Thomas 649c71d62e codegen db 2026-03-11 00:08:40 -04:00
Brandon Thomas 4348d1da42 cleanup 2026-03-11 00:08:20 -04:00
Brandon Thomas 13ac5eb12b handler to create wallet for user 2026-03-11 00:05:43 -04:00
179 changed files with 729 additions and 12460 deletions
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n \n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 14
},
"nullable": []
},
"hash": "002cdb3d086f3b4181e2997befa54bd8516cb1d3d3bb6e389862695318d5c299"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 16
},
"nullable": []
},
"hash": "29916e73a1784ea0b70ef4f637520b05efccc31e01840f681e9b042f82eb8f7e"
}
@@ -9,8 +9,6 @@ use serde::Deserialize;
use serde::Serialize;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use utoipa::{IntoParams, ToSchema};
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_api_clients::FrontendFailureCategoryForApiClients;
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_old_clients::FrontendFailureCategoryForOldClients;
use tokens::tokens::batch_generations::BatchGenerationToken;
pub const LIST_SESSION_JOBS_URL_PATH: &str = "/v1/jobs/session";
@@ -100,16 +98,9 @@ pub struct ListSessionStatusDetailsResponse {
/// This is typically only for non-premium users.
pub requires_keepalive: bool,
/// NB: DO NOT CHANGE: Old clients can't read the new enum payload values as they are introduced.
///
/// An enum the frontend can use to display localized/I18N error
/// messages. These pertain to both transient and permanent failures.
#[deprecated(note="use maybe_failure_category_updated")]
pub maybe_failure_category: Option<FrontendFailureCategoryForOldClients>,
/// An enum the frontend can use to display localized/I18N error
/// messages. These pertain to both transient and permanent failures.
pub maybe_failure_category_updated: Option<FrontendFailureCategoryForApiClients>,
pub maybe_failure_category: Option<FrontendFailureCategory>,
/// This is an integer number between 0 and 100 (both inclusive) that
/// reports the completeness.
@@ -134,3 +125,4 @@ pub struct ListSessionResultDetailsResponse {
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
}
@@ -63,8 +63,6 @@ pub async fn insert_generic_inference_job_for_fal_queue<'e, 'c : 'e, E>(args: In
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
mysql_executor: args.mysql_executor,
phantom: args.phantom,
};
@@ -2,7 +2,6 @@ use idempotency::uuid::generate_random_uuid;
use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -38,9 +37,6 @@ pub struct InsertGenericInferenceForFalMockFailureArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
pub maybe_failure_reason: Option<&'e str>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -66,8 +62,6 @@ pub async fn insert_generic_inference_job_for_fal_queue_mock_failure<'e, 'c: 'e,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
starting_job_status_override: Some(JobStatusPlus::CompleteFailure),
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
mysql_executor: args.mysql_executor,
phantom: args.phantom,
};
@@ -4,38 +4,41 @@ use sqlx::MySqlPool;
use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_input_source_token_type::InferenceInputSourceTokenType;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
use crate::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use crate::queries::tts::tts_inference_jobs::kill_tts_inference_jobs::JobStatus;
pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
where E: 'e + Executor<'c, Database = MySql>
{
pub uuid_idempotency_token: &'e str,
// NOTE: We'll generate this ahead of time so we can save it with billing info!
pub apriori_job_token: &'e InferenceJobToken,
/// The external primary key identifier for the job.
pub maybe_external_third_party_id: &'e str,
pub fal_category: FalCategory,
pub maybe_inference_args: Option<GenericInferenceArgs>,
pub maybe_prompt_token: Option<&'e PromptToken>,
pub maybe_creator_user_token: Option<&'e UserToken>,
@@ -47,11 +50,8 @@ pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
/// Set to `Some(JobStatusPlus::CompleteFailure)` for mock/test failure jobs.
pub starting_job_status_override: Option<JobStatusPlus>,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
pub maybe_failure_reason: Option<&'e str>,
pub mysql_executor: E,
// TODO: Not sure if this works to tell the compiler we need the lifetime annotation.
// See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters
pub phantom: PhantomData<&'c E>,
@@ -87,16 +87,11 @@ pub async fn insert_generic_inference_job_for_fal_queue_with_apriori_job_token<'
),
};
let maybe_external_third_party = InferenceJobExternalThirdParty::Fal;
let status = args.starting_job_status_override.unwrap_or(JobStatusPlus::Pending);
let maybe_frontend_failure_category_str = args.maybe_frontend_failure_category
.map(|c| c.to_str());
let maybe_truncated_failure_reason = args.maybe_failure_reason
.map(|s| if s.len() > 255 { &s[..255] } else { s });
let query = sqlx::query!(
r#"
INSERT INTO generic_inference_jobs
@@ -120,7 +115,7 @@ SET
maybe_download_url = NULL,
maybe_cover_image_media_file_token = NULL,
maybe_prompt_token = ?,
maybe_raw_inference_text = NULL,
@@ -139,22 +134,19 @@ SET
is_debug_request = FALSE,
maybe_routing_tag = NULL,
frontend_failure_category = ?,
failure_reason = ?,
status = ?
"#,
args.apriori_job_token.as_str(),
args.uuid_idempotency_token,
JOB_TYPE.to_str(),
maybe_external_third_party.to_str(),
args.maybe_external_third_party_id,
product_category.to_str(),
inference_category.to_str(),
args.maybe_prompt_token.map(|t| t.to_string()),
serialized_args_payload,
@@ -164,9 +156,6 @@ SET
args.creator_ip_address,
args.creator_set_visibility.to_str(),
maybe_frontend_failure_category_str,
maybe_truncated_failure_reason,
status.to_str(),
);
@@ -1,122 +0,0 @@
use crate::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use utoipa::ToSchema;
/// A forward-compatible version of `FrontendFailureCategory` for API clients.
///
/// Contains all known variants plus an `Unknown(String)` catch-all so that
/// newer server-side values never cause deserialization failures on the client.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum FrontendFailureCategoryForApiClients {
FaceNotDetected,
KeepAliveElapsed,
NotYetImplemented,
RetryableWorkerError,
ModelRulesViolation,
RuleBansUserImage,
RuleBansUserImageWithFaces,
RuleBansUserTextPrompt,
RuleBansUserContent,
RuleBansGeneratedVideo,
RuleBansGeneratedAudio,
RuleBansGeneratedContent,
GenerationFailed,
/// Catch-all for values the client doesn't yet know about.
/// The contained string is the raw serialized value from the server.
#[serde(untagged)]
Unknown(String),
}
impl FrontendFailureCategoryForApiClients {
pub fn from_db_enum(value: FrontendFailureCategory) -> Self {
match value {
FrontendFailureCategory::FaceNotDetected => Self::FaceNotDetected,
FrontendFailureCategory::KeepAliveElapsed => Self::KeepAliveElapsed,
FrontendFailureCategory::NotYetImplemented => Self::NotYetImplemented,
FrontendFailureCategory::RetryableWorkerError => Self::RetryableWorkerError,
FrontendFailureCategory::ModelRulesViolation => Self::ModelRulesViolation,
FrontendFailureCategory::RuleBansUserImage => Self::RuleBansUserImage,
FrontendFailureCategory::RuleBansUserImageWithFaces => Self::RuleBansUserImageWithFaces,
FrontendFailureCategory::RuleBansUserTextPrompt => Self::RuleBansUserTextPrompt,
FrontendFailureCategory::RuleBansUserContent => Self::RuleBansUserContent,
FrontendFailureCategory::RuleBansGeneratedVideo => Self::RuleBansGeneratedVideo,
FrontendFailureCategory::RuleBansGeneratedAudio => Self::RuleBansGeneratedAudio,
FrontendFailureCategory::RuleBansGeneratedContent => Self::RuleBansGeneratedContent,
FrontendFailureCategory::GenerationFailed => Self::GenerationFailed,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_known_variants() {
let cases = vec![
("\"face_not_detected\"", FrontendFailureCategoryForApiClients::FaceNotDetected),
("\"keep_alive_elapsed\"", FrontendFailureCategoryForApiClients::KeepAliveElapsed),
("\"not_yet_implemented\"", FrontendFailureCategoryForApiClients::NotYetImplemented),
("\"retryable_worker_error\"", FrontendFailureCategoryForApiClients::RetryableWorkerError),
("\"model_rules_violation\"", FrontendFailureCategoryForApiClients::ModelRulesViolation),
("\"rule_bans_user_image\"", FrontendFailureCategoryForApiClients::RuleBansUserImage),
("\"rule_bans_user_image_with_faces\"", FrontendFailureCategoryForApiClients::RuleBansUserImageWithFaces),
("\"rule_bans_user_text_prompt\"", FrontendFailureCategoryForApiClients::RuleBansUserTextPrompt),
("\"rule_bans_user_content\"", FrontendFailureCategoryForApiClients::RuleBansUserContent),
("\"rule_bans_generated_video\"", FrontendFailureCategoryForApiClients::RuleBansGeneratedVideo),
("\"rule_bans_generated_audio\"", FrontendFailureCategoryForApiClients::RuleBansGeneratedAudio),
("\"rule_bans_generated_content\"", FrontendFailureCategoryForApiClients::RuleBansGeneratedContent),
("\"generation_failed\"", FrontendFailureCategoryForApiClients::GenerationFailed),
];
for (json, expected) in cases {
let parsed: FrontendFailureCategoryForApiClients = serde_json::from_str(json)
.unwrap_or_else(|e| panic!("failed to parse {}: {}", json, e));
assert_eq!(parsed, expected, "mismatch for {}", json);
}
}
#[test]
fn test_deserialize_unknown_variant() {
let json = "\"some_future_category\"";
let parsed: FrontendFailureCategoryForApiClients = serde_json::from_str(json).unwrap();
assert_eq!(parsed, FrontendFailureCategoryForApiClients::Unknown("some_future_category".to_string()));
}
#[test]
fn test_deserialize_another_unknown_variant() {
let json = "\"totally_new_thing\"";
let parsed: FrontendFailureCategoryForApiClients = serde_json::from_str(json).unwrap();
assert_eq!(parsed, FrontendFailureCategoryForApiClients::Unknown("totally_new_thing".to_string()));
}
#[test]
fn test_serialize_known_variants() {
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForApiClients::FaceNotDetected).unwrap(), "\"face_not_detected\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForApiClients::ModelRulesViolation).unwrap(), "\"model_rules_violation\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForApiClients::GenerationFailed).unwrap(), "\"generation_failed\"");
}
#[test]
fn test_serialize_unknown_variant() {
let unknown = FrontendFailureCategoryForApiClients::Unknown("some_future_category".to_string());
assert_eq!(serde_json::to_string(&unknown).unwrap(), "\"some_future_category\"");
}
#[test]
fn test_from_db_enum() {
assert_eq!(
FrontendFailureCategoryForApiClients::from_db_enum(FrontendFailureCategory::FaceNotDetected),
FrontendFailureCategoryForApiClients::FaceNotDetected
);
assert_eq!(
FrontendFailureCategoryForApiClients::from_db_enum(FrontendFailureCategory::GenerationFailed),
FrontendFailureCategoryForApiClients::GenerationFailed
);
assert_eq!(
FrontendFailureCategoryForApiClients::from_db_enum(FrontendFailureCategory::RuleBansGeneratedContent),
FrontendFailureCategoryForApiClients::RuleBansGeneratedContent
);
}
}
@@ -1,81 +0,0 @@
use crate::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use utoipa::ToSchema;
/// A restricted subset of `FrontendFailureCategory` that only includes values
/// known to old deployed clients. New failure categories are omitted so that
/// old clients never receive an enum value they cannot deserialize.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Deserialize, Serialize, ToSchema)]
pub enum FrontendFailureCategoryForOldClients {
#[serde(rename = "face_not_detected")]
FaceNotDetected,
#[serde(rename = "keep_alive_elapsed")]
KeepAliveElapsed,
#[serde(rename = "not_yet_implemented")]
NotYetImplemented,
#[serde(rename = "retryable_worker_error")]
RetryableWorkerError,
}
impl FrontendFailureCategoryForOldClients {
/// Attempts to convert a database-level `FrontendFailureCategory` into the
/// restricted old-client enum. Returns `None` for categories that old
/// clients do not understand.
pub fn try_from_db_enum(value: FrontendFailureCategory) -> Option<Self> {
match value {
FrontendFailureCategory::FaceNotDetected => Some(Self::FaceNotDetected),
FrontendFailureCategory::KeepAliveElapsed => Some(Self::KeepAliveElapsed),
FrontendFailureCategory::NotYetImplemented => Some(Self::NotYetImplemented),
FrontendFailureCategory::RetryableWorkerError => Some(Self::RetryableWorkerError),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_known_variants_convert() {
assert_eq!(
FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::FaceNotDetected),
Some(FrontendFailureCategoryForOldClients::FaceNotDetected)
);
assert_eq!(
FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::KeepAliveElapsed),
Some(FrontendFailureCategoryForOldClients::KeepAliveElapsed)
);
assert_eq!(
FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::NotYetImplemented),
Some(FrontendFailureCategoryForOldClients::NotYetImplemented)
);
assert_eq!(
FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RetryableWorkerError),
Some(FrontendFailureCategoryForOldClients::RetryableWorkerError)
);
}
#[test]
fn test_new_variants_return_none() {
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::ModelRulesViolation), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansUserImage), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansUserImageWithFaces), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansUserTextPrompt), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansUserContent), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansGeneratedVideo), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansGeneratedAudio), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansGeneratedContent), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::GenerationFailed), None);
}
#[test]
fn test_serialization() {
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForOldClients::FaceNotDetected).unwrap(), "\"face_not_detected\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForOldClients::KeepAliveElapsed).unwrap(), "\"keep_alive_elapsed\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForOldClients::NotYetImplemented).unwrap(), "\"not_yet_implemented\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForOldClients::RetryableWorkerError).unwrap(), "\"retryable_worker_error\"");
}
}
@@ -1,2 +0,0 @@
pub mod frontend_failure_category_for_old_clients;
pub mod frontend_failure_category_for_api_clients;
@@ -1 +0,0 @@
pub mod generic_inference_jobs;
@@ -1 +0,0 @@
pub mod by_table;
-1
View File
@@ -37,4 +37,3 @@ pub mod error;
pub mod no_table;
pub mod tauri;
pub mod traits;
pub mod api_safe;
@@ -281,7 +281,6 @@ use artcraft_api_defs::moderation::wallet_ledger_entries::list_wallet_ledger_ent
use artcraft_api_defs::moderation::wallet_ledger_entries::moderator_get_wallet_ledger_entry::*;
use artcraft_api_defs::moderation::wallets::list_user_wallets::*;
use artcraft_api_defs::moderation::wallets::moderator_add_banked_balance_to_wallet::*;
use artcraft_api_defs::moderation::wallets::moderator_create_wallet_for_user::*;
use artcraft_api_defs::moderation::wallets::moderator_get_wallet::*;
use artcraft_api_defs::web_referrals::log_web_referral::*;
use crate::http_server::endpoints::web_referrals::log_web_referral_handler::*;
@@ -469,7 +468,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::moderation::wallet_ledger_entries::moderator_get_wallet_ledger_entry_handler::moderator_get_wallet_ledger_entry_handler,
crate::http_server::endpoints::moderation::wallets::list_user_wallets_handler::list_user_wallets_handler,
crate::http_server::endpoints::moderation::wallets::moderator_add_banked_balance_to_wallet_handler::moderator_add_banked_balance_to_wallet_handler,
crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler,
crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler,
// Credits
crate::http_server::endpoints::credits::get_session_credits_handler::get_session_credits_handler,
@@ -668,8 +666,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ModeratorAddBankedBalanceToWalletPathInfo,
ModeratorAddBankedBalanceToWalletRequest,
ModeratorAddBankedBalanceToWalletResponse,
ModeratorCreateWalletForUserRequest,
ModeratorCreateWalletForUserResponse,
ModeratorGetWalletPathInfo,
ModeratorGetWalletResponse,
ModeratorGetWalletDetails,
@@ -1,7 +1,5 @@
use crate::http_server::common_responses::media::cdn_link;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use server_environment::ServerEnvironment;
use url::Url;
use utoipa::ToSchema;
@@ -26,27 +24,24 @@ impl CoverImageLinks {
pub fn from_media_path(
domain: MediaDomain,
server_environment: ServerEnvironment,
bucket_path: &MediaFileBucketPath,
) -> Option<Self> {
let rooted_path = bucket_path.get_full_object_path_str();
Self::from_rooted_path(domain, server_environment, rooted_path)
Self::from_rooted_path(domain, rooted_path)
}
pub fn from_maybe_media_path(
domain: MediaDomain,
server_environment: ServerEnvironment,
bucket_path: Option<&MediaFileBucketPath>,
) -> Option<Self> {
match bucket_path {
None => None,
Some(bucket_path) => Self::from_media_path(domain, server_environment, bucket_path),
Some(bucket_path) => Self::from_media_path(domain, bucket_path),
}
}
pub fn from_parts(
domain: MediaDomain,
server_environment: ServerEnvironment,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
maybe_cover_image_public_bucket_extension: Option<&str>,
@@ -60,12 +55,11 @@ impl CoverImageLinks {
maybe_cover_image_public_bucket_prefix,
maybe_cover_image_public_bucket_extension
);
Self::from_media_path(domain, server_environment, &bucket_path)
Self::from_media_path(domain, &bucket_path)
}
pub fn from_rooted_path(
domain: MediaDomain,
server_environment: ServerEnvironment,
rooted_path: &str,
) -> Option<Self> {
if !rooted_path.ends_with(".jpg")
@@ -74,24 +68,17 @@ impl CoverImageLinks {
return None;
}
let mut cdn_url = cdn_link::new_cdn_url(domain, server_environment);
let mut cdn_url = domain.new_cdn_url();
cdn_url.set_path(rooted_path);
Some(Self {
cdn_url,
thumbnail_template: thumbnail_template(domain, server_environment, rooted_path),
thumbnail_template: thumbnail_template(domain, rooted_path),
})
}
}
fn thumbnail_template(media_domain: MediaDomain, server_environment: ServerEnvironment, rooted_path: &str) -> String {
let host = cdn_link::get_cdn_host(media_domain, server_environment);
match server_environment {
ServerEnvironment::Production => {
format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}")
}
ServerEnvironment::Development => {
format!("{host}{rooted_path}")
}
}
fn thumbnail_template(media_domain: MediaDomain, rooted_path: &str) -> String {
let host = media_domain.cdn_url_str();
format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}")
}
@@ -1,8 +1,6 @@
use crate::http_server::common_responses::media::cdn_link;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use artcraft_api_defs::common::responses::cover_image_links::CoverImageLinks;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use server_environment::ServerEnvironment;
// TODO(bt,2024-09-18): Consolidate thumbnail builder code and vars
// TODO(bt,2024-09-05): Worth reducing the quality at all?
@@ -14,27 +12,24 @@ impl CoverImageLinksBuilder {
pub fn from_media_path(
domain: MediaDomain,
server_environment: ServerEnvironment,
bucket_path: &MediaFileBucketPath,
) -> Option<CoverImageLinks> {
let rooted_path = bucket_path.get_full_object_path_str();
Self::from_rooted_path(domain, server_environment, rooted_path)
Self::from_rooted_path(domain, rooted_path)
}
pub fn from_maybe_media_path(
domain: MediaDomain,
server_environment: ServerEnvironment,
bucket_path: Option<&MediaFileBucketPath>,
) -> Option<CoverImageLinks> {
match bucket_path {
None => None,
Some(bucket_path) => Self::from_media_path(domain, server_environment, bucket_path),
Some(bucket_path) => Self::from_media_path(domain, bucket_path),
}
}
pub fn from_parts(
domain: MediaDomain,
server_environment: ServerEnvironment,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
maybe_cover_image_public_bucket_extension: Option<&str>,
@@ -48,12 +43,11 @@ impl CoverImageLinksBuilder {
maybe_cover_image_public_bucket_prefix,
maybe_cover_image_public_bucket_extension
);
Self::from_media_path(domain, server_environment, &bucket_path)
Self::from_media_path(domain, &bucket_path)
}
pub fn from_rooted_path(
domain: MediaDomain,
server_environment: ServerEnvironment,
rooted_path: &str,
) -> Option<CoverImageLinks> {
if !rooted_path.ends_with(".jpg")
@@ -62,24 +56,17 @@ impl CoverImageLinksBuilder {
return None;
}
let mut cdn_url = cdn_link::new_cdn_url(domain, server_environment);
let mut cdn_url = domain.new_cdn_url();
cdn_url.set_path(rooted_path);
Some(CoverImageLinks {
cdn_url,
thumbnail_template: thumbnail_template(domain, server_environment, rooted_path),
thumbnail_template: thumbnail_template(domain, rooted_path),
})
}
}
fn thumbnail_template(media_domain: MediaDomain, server_environment: ServerEnvironment, rooted_path: &str) -> String {
let host = cdn_link::get_cdn_host(media_domain, server_environment);
match server_environment {
ServerEnvironment::Production => {
format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}")
}
ServerEnvironment::Development => {
format!("{host}{rooted_path}")
}
}
fn thumbnail_template(media_domain: MediaDomain, rooted_path: &str) -> String {
let host = media_domain.cdn_url_str();
format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}")
}
@@ -3,11 +3,10 @@ use std::hash::{Hash, Hasher};
use url::Url;
use utoipa::ToSchema;
use crate::http_server::common_responses::media::cdn_link;
use crate::http_server::common_responses::media::cover_image_links::CoverImageLinks;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::web_utils::bucket_urls::bucket_url_from_media_path::bucket_url_from_media_path;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use server_environment::ServerEnvironment;
use tokens::tokens::media_files::MediaFileToken;
/// There are currently 25 cover images numbered 0 to 24 (0-indexed).
@@ -63,11 +62,7 @@ impl MediaFileCoverImageDetails {
}
/// For non-media file tokens (eg. emulated TTS results)
pub fn from_legacy_token_str(token: &str) -> Self {
Self::from_token_str(token)
}
fn from_token_str(token: &str) -> Self {
pub fn from_token_str(token: &str) -> Self {
Self {
// TODO(bt,2024-04-07): Add column to schema to support + CRUD to add.
maybe_cover_image_public_bucket_path: None,
@@ -81,7 +76,6 @@ impl MediaFileCoverImageDetails {
pub fn from_optional_db_fields(
token: &MediaFileToken,
domain: MediaDomain,
server_environment: ServerEnvironment,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
maybe_cover_image_public_bucket_extension: Option<&str>,
@@ -89,17 +83,15 @@ impl MediaFileCoverImageDetails {
Self::from_optional_db_str_fields(
token.as_str(),
domain,
server_environment,
maybe_cover_image_public_bucket_path,
maybe_cover_image_public_bucket_prefix,
maybe_cover_image_public_bucket_extension
)
}
fn from_optional_db_str_fields(
pub fn from_optional_db_str_fields(
token: &str,
domain: MediaDomain,
server_environment: ServerEnvironment,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
maybe_cover_image_public_bucket_extension: Option<&str>,
@@ -120,19 +112,19 @@ impl MediaFileCoverImageDetails {
// NB: Fail construction open.
let maybe_cover_image_public_bucket_url = maybe_bucket_path
.as_ref()
.and_then(|bucket_path| {
let rooted_path = bucket_path.get_full_object_path_str();
let mut url = cdn_link::new_cdn_url(domain, server_environment);
url.set_path(rooted_path);
Some(url)
});
.map(|bucket_path| bucket_url_from_media_path(bucket_path).ok())
.flatten();
let maybe_links = CoverImageLinks::from_maybe_media_path(
domain, server_environment, maybe_bucket_path.as_ref());
domain, maybe_bucket_path.as_ref());
// let maybe_media_links = maybe_bucket_path
// .map(|path| MediaLinks::from_media_path(domain, &path));
Self {
maybe_cover_image_public_bucket_path,
maybe_cover_image_public_bucket_url,
//maybe_media_links,
maybe_links,
default_cover: MediaFileDefaultCover::from_token_str(token),
}
@@ -189,142 +181,4 @@ mod tests {
assert_eq!(cover.color_index, 0);
assert_eq!(cover.image_index, 23);
}
#[test]
fn test_cover_image_url_production() {
let token = MediaFileToken::new_from_str("test_token");
let domain = super::MediaDomain::FakeYou;
let env = super::ServerEnvironment::Production;
let details = super::MediaFileCoverImageDetails::from_optional_db_fields(
&token,
domain,
env,
Some("bucket_hash"),
Some("image_"),
Some(".png"),
);
let url = details.maybe_cover_image_public_bucket_url.unwrap();
assert!(
url.as_str().starts_with("https://cdn-2.fakeyou.com"),
"Production URL should start with https://cdn-2.fakeyou.com, got: {}",
url
);
}
#[test]
fn test_cover_image_url_development() {
let token = MediaFileToken::new_from_str("test_token");
let domain = super::MediaDomain::FakeYou;
let env = super::ServerEnvironment::Development;
let details = super::MediaFileCoverImageDetails::from_optional_db_fields(
&token,
domain,
env,
Some("bucket_hash"),
Some("image_"),
Some(".png"),
);
let url = details.maybe_cover_image_public_bucket_url.unwrap();
assert!(
url.as_str().starts_with("https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/"),
"Development URL should start with https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/, got: {}",
url
);
}
#[test]
fn test_cover_image_links_cdn_url_production() {
let token = MediaFileToken::new_from_str("test_token");
let domain = super::MediaDomain::FakeYou;
let env = super::ServerEnvironment::Production;
let details = super::MediaFileCoverImageDetails::from_optional_db_fields(
&token,
domain,
env,
Some("bucket_hash"),
Some("image_"),
Some(".png"),
);
let links = details.maybe_links.unwrap();
assert!(
links.cdn_url.as_str().starts_with("https://cdn-2.fakeyou.com"),
"Production cdn_url should start with https://cdn-2.fakeyou.com, got: {}",
links.cdn_url
);
}
#[test]
fn test_cover_image_links_cdn_url_development() {
let token = MediaFileToken::new_from_str("test_token");
let domain = super::MediaDomain::FakeYou;
let env = super::ServerEnvironment::Development;
let details = super::MediaFileCoverImageDetails::from_optional_db_fields(
&token,
domain,
env,
Some("bucket_hash"),
Some("image_"),
Some(".png"),
);
let links = details.maybe_links.unwrap();
assert!(
links.cdn_url.as_str().starts_with("https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/"),
"Development cdn_url should start with https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/, got: {}",
links.cdn_url
);
}
#[test]
fn test_cover_image_links_thumbnail_template_production() {
let token = MediaFileToken::new_from_str("test_token");
let domain = super::MediaDomain::FakeYou;
let env = super::ServerEnvironment::Production;
let details = super::MediaFileCoverImageDetails::from_optional_db_fields(
&token,
domain,
env,
Some("bucket_hash"),
Some("image_"),
Some(".png"),
);
let links = details.maybe_links.unwrap();
assert!(
links.thumbnail_template.starts_with("https://cdn-2.fakeyou.com"),
"Production thumbnail_template should start with https://cdn-2.fakeyou.com, got: {}",
links.thumbnail_template
);
}
#[test]
fn test_cover_image_links_thumbnail_template_development() {
let token = MediaFileToken::new_from_str("test_token");
let domain = super::MediaDomain::FakeYou;
let env = super::ServerEnvironment::Development;
let details = super::MediaFileCoverImageDetails::from_optional_db_fields(
&token,
domain,
env,
Some("bucket_hash"),
Some("image_"),
Some(".png"),
);
let links = details.maybe_links.unwrap();
assert!(
links.thumbnail_template.starts_with("https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/"),
"Development thumbnail_template should start with https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/, got: {}",
links.thumbnail_template
);
}
}
@@ -1,8 +1,8 @@
use crate::http_server::common_responses::media::cover_image_links_builder::CoverImageLinksBuilder;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::web_utils::bucket_urls::bucket_url_from_media_path::bucket_url_from_media_path;
use artcraft_api_defs::common::responses::media_file_cover_image_details::{MediaFileCoverImageDetails, MediaFileDefaultCover};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use server_environment::ServerEnvironment;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use tokens::tokens::media_files::MediaFileToken;
@@ -42,7 +42,6 @@ impl MediaFileCoverImageDetailsBuilder {
pub fn from_optional_db_fields(
token: &MediaFileToken,
domain: MediaDomain,
server_environment: ServerEnvironment,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
maybe_cover_image_public_bucket_extension: Option<&str>,
@@ -50,7 +49,6 @@ impl MediaFileCoverImageDetailsBuilder {
Self::from_optional_db_str_fields(
token.as_str(),
domain,
server_environment,
maybe_cover_image_public_bucket_path,
maybe_cover_image_public_bucket_prefix,
maybe_cover_image_public_bucket_extension
@@ -60,7 +58,6 @@ impl MediaFileCoverImageDetailsBuilder {
pub fn from_optional_db_str_fields(
token: &str,
domain: MediaDomain,
server_environment: ServerEnvironment,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
maybe_cover_image_public_bucket_extension: Option<&str>,
@@ -72,10 +69,29 @@ impl MediaFileCoverImageDetailsBuilder {
maybe_cover_image_public_bucket_extension
));
let maybe_cover_image_public_bucket_path = maybe_bucket_path
.as_ref()
.map(|bucket_path| bucket_path
.get_full_object_path_str()
.to_string());
// NB: Fail construction open.
let maybe_cover_image_public_bucket_url = maybe_bucket_path
.as_ref()
.map(|bucket_path| bucket_url_from_media_path(bucket_path).ok())
.flatten();
let maybe_links = CoverImageLinksBuilder::from_maybe_media_path(
domain, server_environment, maybe_bucket_path.as_ref());
domain, maybe_bucket_path.as_ref());
// let maybe_media_links = maybe_bucket_path
// .map(|path| MediaLinks::from_media_path(domain, &path));
MediaFileCoverImageDetails {
// NB: The code using this builder does not populate or read these legacy fields:
// maybe_cover_image_public_bucket_path,
// maybe_cover_image_public_bucket_url,
// maybe_media_links,
maybe_links,
default_cover: MediaFileDefaultCoverBuilder::from_token_str(token),
}
@@ -1,13 +1,12 @@
use url::Url;
use utoipa::ToSchema;
use crate::http_server::common_responses::media::cdn_link;
use crate::http_server::common_responses::media::cover_image_links::CoverImageLinks;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::web_utils::bucket_urls::bucket_url_from_media_path::bucket_url_from_media_path;
use crate::util::placeholder_images::cover_images::default_cover_image_color_from_token::default_cover_image_color_from_token;
use crate::util::placeholder_images::cover_images::default_cover_image_from_token::default_cover_image_from_token;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use server_environment::ServerEnvironment;
use tokens::tokens::model_weights::ModelWeightToken;
/// Everything we need to create a cover image.
@@ -46,7 +45,6 @@ impl WeightsCoverImageDetails {
pub fn from_optional_db_fields(
domain: MediaDomain,
server_environment: ServerEnvironment,
model_weight_token: &ModelWeightToken,
maybe_cover_image_public_bucket_path: Option<&str>,
maybe_cover_image_public_bucket_prefix: Option<&str>,
@@ -68,15 +66,13 @@ impl WeightsCoverImageDetails {
// NB: Fail construction open.
let maybe_cover_image_public_bucket_url = maybe_bucket_path
.as_ref()
.and_then(|bucket_path| {
let rooted_path = bucket_path.get_full_object_path_str();
let mut url = cdn_link::new_cdn_url(domain, server_environment);
url.set_path(rooted_path);
Some(url)
});
.map(|bucket_path| bucket_url_from_media_path(bucket_path).ok())
.flatten();
let image_index = default_cover_image_from_token(model_weight_token);
let maybe_links = CoverImageLinks::from_maybe_media_path(
domain, server_environment, maybe_bucket_path.as_ref());
domain, maybe_bucket_path.as_ref());
Self {
maybe_cover_image_public_bucket_path,
@@ -100,7 +96,6 @@ impl WeightsDefaultCoverInfo {
mod tests {
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::common_responses::media::weights_cover_image_details::WeightsCoverImageDetails;
use server_environment::ServerEnvironment;
use tokens::tokens::model_weights::ModelWeightToken;
use url::Url;
@@ -114,7 +109,6 @@ mod tests {
let cover_image = WeightsCoverImageDetails::from_optional_db_fields(
domain,
ServerEnvironment::Production,
&token,
maybe_public_bucket_hash,
maybe_prefix,
@@ -1,31 +1,3 @@
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
pub struct SyntheticFailure {
pub frontend_failure_category: FrontendFailureCategory,
pub frontend_failure_message: Option<String>,
}
/// If the prompt contains a trigger phrase, parse a `FrontendFailureCategory`
/// from the remaining words (falling back to `GenerationFailed`) and return a
/// `SyntheticFailure`. Returns `None` when the trigger phrase is absent.
pub fn test_synthetic_failure_reason(prompt: &str) -> Option<SyntheticFailure> {
let lower = prompt.to_ascii_lowercase();
if !lower.contains("simulate_artcraft_failure") && !lower.contains("test_artcraft_failure") {
return None;
}
let maybe_category = prompt
.split_whitespace()
.find_map(|word| FrontendFailureCategory::from_str(word).ok());
let category = maybe_category.unwrap_or(FrontendFailureCategory::GenerationFailed);
Some(SyntheticFailure {
frontend_failure_category: category,
frontend_failure_message: Some("This was a simulated failure".to_string()),
})
}
/// Returns `true` if the prompt contains the secret test phrase that triggers
/// a synthetic job failure. The job record is inserted with
/// `JobStatusPlus::CompleteFailure` immediately, without calling any external
@@ -33,57 +5,3 @@ pub fn test_synthetic_failure_reason(prompt: &str) -> Option<SyntheticFailure> {
pub fn is_job_failure_test(prompt: &str) -> bool {
prompt.to_ascii_lowercase().contains("artcraft_test_job_failure")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_trigger_returns_none() {
assert!(test_synthetic_failure_reason("a normal prompt").is_none());
}
#[test]
fn trigger_simulate_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("simulate_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
}
#[test]
fn trigger_test_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("test_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
}
#[test]
fn trigger_with_known_category() {
let result = test_synthetic_failure_reason("simulate_artcraft_failure face_not_detected").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::FaceNotDetected);
}
#[test]
fn trigger_with_category_before_keyword() {
let result = test_synthetic_failure_reason("rule_bans_user_image simulate_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::RuleBansUserImage);
}
#[test]
fn trigger_with_model_rules_violation() {
let result = test_synthetic_failure_reason("test_artcraft_failure model_rules_violation please").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::ModelRulesViolation);
}
#[test]
fn trigger_is_case_insensitive() {
let result = test_synthetic_failure_reason("SIMULATE_ARTCRAFT_FAILURE").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
}
#[test]
fn trigger_embedded_in_text() {
let result = test_synthetic_failure_reason("hello world simulate_artcraft_failure keep_alive_elapsed goodbye").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::KeepAliveElapsed);
}
}
@@ -1,3 +1,2 @@
pub mod job_failure_test;
pub mod payments_error_test;
mod test_synthetic_failure_reason;
pub mod payments_error_test;
@@ -1,83 +0,0 @@
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
pub struct SyntheticFailure {
pub frontend_failure_category: FrontendFailureCategory,
pub frontend_failure_message: Option<String>,
}
/// If the prompt contains the secret trigger phrase, parse a
/// `FrontendFailureCategory` from the remaining words (falling back to
/// `GenerationFailed`) and return a `SyntheticFailure`.
///
/// Returns `None` when the trigger phrase is absent.
pub fn test_synthetic_failure_reason(prompt: &str) -> Option<SyntheticFailure> {
let lower = prompt.to_ascii_lowercase();
if !lower.contains("simulate_artcraft_failure") && !lower.contains("test_artcraft_failure") {
return None;
}
let maybe_category = prompt
.split_whitespace()
.find_map(|word| FrontendFailureCategory::from_str(word).ok());
let category = maybe_category.unwrap_or(FrontendFailureCategory::GenerationFailed);
Some(SyntheticFailure {
frontend_failure_category: category,
frontend_failure_message: Some("This was a simulated failure".to_string()),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_trigger_returns_none() {
assert!(test_synthetic_failure_reason("a normal prompt").is_none());
}
#[test]
fn trigger_simulate_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("simulate_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
}
#[test]
fn trigger_test_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("test_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
}
#[test]
fn trigger_with_known_category() {
let result = test_synthetic_failure_reason("simulate_artcraft_failure face_not_detected").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::FaceNotDetected);
}
#[test]
fn trigger_with_category_before_keyword() {
let result = test_synthetic_failure_reason("rule_bans_user_image simulate_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::RuleBansUserImage);
}
#[test]
fn trigger_with_model_rules_violation() {
let result = test_synthetic_failure_reason("test_artcraft_failure model_rules_violation please").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::ModelRulesViolation);
}
#[test]
fn trigger_is_case_insensitive() {
let result = test_synthetic_failure_reason("SIMULATE_ARTCRAFT_FAILURE").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
}
#[test]
fn trigger_embedded_in_text() {
let result = test_synthetic_failure_reason("hello world simulate_artcraft_failure keep_alive_elapsed goodbye").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::KeepAliveElapsed);
}
}
@@ -216,8 +216,6 @@ pub async fn flux_2_lora_edit_image_angle_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -217,8 +217,6 @@ pub async fn qwen_edit_2511_edit_image_angle_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -279,8 +279,6 @@ pub async fn gemini_25_flash_edit_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -301,8 +301,6 @@ pub async fn gpt_image_1_edit_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -253,8 +253,6 @@ pub async fn qwen_edit_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -2,7 +2,7 @@ use std::sync::Arc;
use crate::billing::wallets::temporary_test_wallet_deduction::temporary_test_wallet_deduction;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::job_failure_test::test_synthetic_failure_reason;
use crate::http_server::endpoints::generate::common::job_failure_test::is_job_failure_test;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
@@ -10,7 +10,6 @@ use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::FluxProKontextMaxEditImageNumImages;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages, GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse};
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
@@ -96,15 +95,13 @@ pub async fn generate_flux_1_dev_text_to_image_handler(
})?;
// Secret test hook: insert a synthetic "complete_failure" job without calling Fal.
if let Some(synthetic_failure) = test_synthetic_failure_reason(request.prompt.as_deref().unwrap_or("")) {
if is_job_failure_test(request.prompt.as_deref().unwrap_or("")) {
return insert_mock_failure_job(
&http_request,
request.prompt.as_deref(),
&request.uuid_idempotency_token,
maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token.as_ref(),
Some(synthetic_failure.frontend_failure_category),
synthetic_failure.frontend_failure_message.as_deref(),
&mut mysql_connection,
).await;
}
@@ -255,8 +252,6 @@ async fn insert_mock_failure_job(
uuid_idempotency_token: &str,
maybe_creator_user_token: Option<&UserToken>,
maybe_avt_token: Option<&AnonymousVisitorTrackingToken>,
maybe_frontend_failure_category: Option<FrontendFailureCategory>,
maybe_failure_reason: Option<&str>,
mysql_connection: &mut sqlx::pool::PoolConnection<sqlx::MySql>,
) -> Result<Json<GenerateFlux1DevTextToImageResponse>, CommonWebError> {
let ip_address = get_request_ip(http_request);
@@ -300,8 +295,6 @@ async fn insert_mock_failure_job(
maybe_avt_token,
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_frontend_failure_category,
maybe_failure_reason,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
@@ -188,8 +188,6 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -192,8 +192,6 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -235,8 +235,6 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -247,7 +245,7 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
@@ -275,8 +275,6 @@ pub async fn bytedance_seedream_5_lite_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -300,8 +300,6 @@ pub async fn bytedance_seedream_v4_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -298,8 +298,6 @@ pub async fn bytedance_seedream_v4p5_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -388,8 +388,6 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -298,8 +298,6 @@ pub async fn nano_banana_2_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -331,8 +331,6 @@ pub async fn nano_banana_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -346,8 +346,6 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -249,8 +249,6 @@ pub async fn generate_kling_1_6_pro_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -234,8 +234,6 @@ pub async fn generate_kling_2_1_master_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -249,8 +249,6 @@ pub async fn generate_kling_2_1_pro_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -264,8 +264,6 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -241,8 +241,6 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -253,7 +251,7 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
@@ -247,8 +247,6 @@ pub async fn generate_veo_2_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -240,8 +240,6 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -248,8 +248,6 @@ pub async fn generate_veo_3_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -318,8 +318,6 @@ pub async fn kling_2p5_turbo_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -317,8 +317,6 @@ pub async fn kling_2p6_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -366,8 +366,6 @@ pub async fn kling_3p0_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -366,8 +366,6 @@ pub async fn kling_3p0_standard_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -371,8 +371,6 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -337,8 +337,6 @@ pub async fn sora_2_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -339,8 +339,6 @@ pub async fn sora_2_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -402,8 +402,6 @@ pub async fn veo_3p1_fast_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -414,7 +412,7 @@ pub async fn veo_3p1_fast_multi_function_video_gen_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
@@ -402,8 +402,6 @@ pub async fn veo_3p1_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -414,7 +412,7 @@ pub async fn veo_3p1_multi_function_video_gen_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
@@ -21,8 +21,6 @@ use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsItem, ListSessio
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use bucket_paths::legacy::typified_paths::public::voice_conversion_results::bucket_file_path::VoiceConversionResultOriginalFilePath;
use chrono::{DateTime, Utc};
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_api_clients::FrontendFailureCategoryForApiClients;
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_old_clients::FrontendFailureCategoryForOldClients;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::common::job_status_plus::JobStatusPlus;
@@ -292,12 +290,7 @@ fn db_record_to_response_payload(
maybe_first_started_at: record.maybe_first_started_at,
attempt_count: record.attempt_count as u8,
requires_keepalive: record.is_keepalive_required,
maybe_failure_category: record
.maybe_frontend_failure_category
.and_then(|val| FrontendFailureCategoryForOldClients::try_from_db_enum(val)),
maybe_failure_category_updated: record
.maybe_frontend_failure_category
.map(|val| FrontendFailureCategoryForApiClients::from_db_enum(val)),
maybe_failure_category: record.maybe_frontend_failure_category,
progress_percentage,
maybe_current_execution_duration_seconds,
},
@@ -341,11 +341,10 @@ pub async fn batch_get_media_files_handler(
public_bucket_path: public_bucket_path
.get_full_object_path_str()
.to_string(),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path, media_domain, server_state.server_environment),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path),
cover_image: MediaFileCoverImageDetails::from_optional_db_fields(
&result.token,
media_domain,
server_state.server_environment,
result.maybe_file_cover_image_public_bucket_hash.as_deref(),
result.maybe_file_cover_image_public_bucket_prefix.as_deref(),
result.maybe_file_cover_image_public_bucket_extension.as_deref(),
@@ -366,7 +366,7 @@ async fn modern_media_file_lookup(
maybe_batch_token: result.maybe_batch_token,
maybe_scene_source_media_file_token: result.maybe_scene_source_media_file_token,
media_links: MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &public_bucket_path),
public_bucket_url: bucket_url_from_media_path(&public_bucket_path, media_domain, server_state.server_environment)
public_bucket_url: bucket_url_from_media_path(&public_bucket_path)
.map_err(|err| {
warn!("error creating URL: {:?}", err);
GetMediaFileError::ServerError
@@ -377,7 +377,6 @@ async fn modern_media_file_lookup(
cover_image: MediaFileCoverImageDetails::from_optional_db_fields(
&result.token,
media_domain,
server_state.server_environment,
result.maybe_file_cover_image_public_bucket_hash.as_deref(),
result.maybe_file_cover_image_public_bucket_prefix.as_deref(),
result.maybe_file_cover_image_public_bucket_extension.as_deref(),
@@ -504,13 +503,13 @@ async fn emulate_media_file_with_legacy_tts_result_lookup(
maybe_batch_token: None,
maybe_scene_source_media_file_token: None,
media_links: MediaLinksBuilder::from_rooted_path_and_env(media_domain, server_state.server_environment, &public_bucket_path),
public_bucket_url: bucket_url_from_str_path(&public_bucket_path, media_domain, server_state.server_environment)
public_bucket_url: bucket_url_from_str_path(&public_bucket_path)
.map_err(|err| {
warn!("error creating URL: {:?}", err);
GetMediaFileError::ServerError
})?,
public_bucket_path,
cover_image: MediaFileCoverImageDetails::from_legacy_token_str(&result.tts_result_token),
cover_image: MediaFileCoverImageDetails::from_token_str(&result.tts_result_token),
maybe_model_weight_info: Some(GetMediaFileModelInfo {
// NB: These should be reasonable synthetic defaults for emulated TT2 results, even the "ModelWeightToken".
weight_token: ModelWeightToken::new_from_str(&result.tts_model_token),
@@ -8,6 +8,7 @@ use crate::http_server::common_responses::media::media_links_builder::MediaLinks
use crate::http_server::common_responses::user_details_lite_builder::UserDetailsLightBuilder;
use crate::http_server::endpoints::media_files::get::get_media_file_handler::GetMediaFilePathInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
@@ -119,7 +120,6 @@ pub async fn list_batch_generated_redux_media_files_handler(
cover_image: MediaFileCoverImageDetailsBuilder::from_optional_db_fields(
&result.token,
media_domain,
server_state.server_environment,
result.maybe_file_cover_image_public_bucket_hash.as_deref(),
result.maybe_file_cover_image_public_bucket_prefix.as_deref(),
result.maybe_file_cover_image_public_bucket_extension.as_deref(),
@@ -296,11 +296,10 @@ pub async fn list_featured_media_files_handler(
public_bucket_path: public_bucket_path
.get_full_object_path_str()
.to_string(),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path, media_domain, server_state.server_environment),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path),
cover_image: MediaFileCoverImageDetails::from_optional_db_fields(
&m.token,
media_domain,
server_state.server_environment,
m.maybe_file_cover_image_public_bucket_hash.as_deref(),
m.maybe_file_cover_image_public_bucket_prefix.as_deref(),
m.maybe_file_cover_image_public_bucket_extension.as_deref(),
@@ -285,7 +285,7 @@ pub async fn list_media_files_by_batch_token_handler(
public_bucket_path: public_bucket_path
.get_full_object_path_str()
.to_string(),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path, media_domain, server_state.server_environment),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path),
cover_image: MediaFileCoverImageDetails::from_token(&record.token),
creator_set_visibility: record.creator_set_visibility,
is_user_upload: record.is_user_upload,
@@ -344,11 +344,10 @@ pub async fn list_media_files_for_user_handler(
public_bucket_path: public_bucket_path
.get_full_object_path_str()
.to_string(),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path, media_domain, server_state.server_environment),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path),
cover_image: MediaFileCoverImageDetails::from_optional_db_fields(
&record.token,
media_domain,
server_state.server_environment,
record.maybe_file_cover_image_public_bucket_hash.as_deref(),
record.maybe_file_cover_image_public_bucket_prefix.as_deref(),
record.maybe_file_cover_image_public_bucket_extension.as_deref(),
@@ -353,11 +353,10 @@ pub async fn list_media_files_handler(
public_bucket_path: public_bucket_path
.get_full_object_path_str()
.to_string(),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path, media_domain, server_state.server_environment),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path),
cover_image: MediaFileCoverImageDetails::from_optional_db_fields(
&record.token,
media_domain,
server_state.server_environment,
record.maybe_file_cover_image_public_bucket_hash.as_deref(),
record.maybe_file_cover_image_public_bucket_prefix.as_deref(),
record.maybe_file_cover_image_public_bucket_extension.as_deref(),
@@ -223,11 +223,10 @@ pub async fn list_pinned_media_files_handler(
public_bucket_path: public_bucket_path
.get_full_object_path_str()
.to_string(),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path, media_domain, server_state.server_environment),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path),
cover_image: MediaFileCoverImageDetails::from_optional_db_fields(
&m.token,
media_domain,
server_state.server_environment,
m.maybe_file_cover_image_public_bucket_hash.as_deref(),
m.maybe_file_cover_image_public_bucket_prefix.as_deref(),
m.maybe_file_cover_image_public_bucket_extension.as_deref(),
@@ -235,11 +235,10 @@ pub async fn search_featured_media_files_handler(
public_bucket_path: public_bucket_path
.get_full_object_path_str()
.to_string(),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path, media_domain, server_state.server_environment),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path),
cover_image: MediaFileCoverImageDetails::from_optional_db_fields(
&result.token,
media_domain,
server_state.server_environment,
result.maybe_cover_image_public_bucket_hash.as_deref(),
result.maybe_cover_image_public_bucket_prefix.as_deref(),
result.maybe_cover_image_public_bucket_extension.as_deref(),
@@ -262,11 +262,10 @@ pub async fn search_session_media_files_handler(
public_bucket_path: public_bucket_path
.get_full_object_path_str()
.to_string(),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path, media_domain, server_state.server_environment),
public_bucket_url: bucket_url_string_from_media_path(&public_bucket_path),
cover_image: MediaFileCoverImageDetails::from_optional_db_fields(
&result.token,
media_domain,
server_state.server_environment,
result.maybe_cover_image_public_bucket_hash.as_deref(),
result.maybe_cover_image_public_bucket_prefix.as_deref(),
result.maybe_cover_image_public_bucket_extension.as_deref(),
@@ -243,7 +243,6 @@ pub async fn list_user_bookmarks_for_user_handler(
maybe_media_file_cover = Some(MediaFileCoverImageDetails::from_optional_db_fields(
&MediaFileToken::new_from_str(&user_bookmark.entity_token),
media_domain,
server_state.server_environment,
user_bookmark.maybe_media_file_cover_image_public_bucket_hash.as_deref(),
user_bookmark.maybe_media_file_cover_image_public_bucket_prefix.as_deref(),
user_bookmark.maybe_media_file_cover_image_public_bucket_extension.as_deref(),
@@ -252,7 +251,6 @@ pub async fn list_user_bookmarks_for_user_handler(
UserBookmarkEntityType::ModelWeight => {
maybe_model_weight_cover = Some(WeightsCoverImageDetails::from_optional_db_fields(
media_domain,
server_state.server_environment,
&ModelWeightToken::new_from_str(&user_bookmark.entity_token),
user_bookmark.maybe_model_weight_cover_image_public_bucket_hash.as_deref(),
user_bookmark.maybe_model_weight_cover_image_public_bucket_prefix.as_deref(),
@@ -184,7 +184,6 @@ pub async fn get_weight_handler(
let cover_image_details = WeightsCoverImageDetails::from_optional_db_fields(
media_domain,
server_state.server_environment,
&weight.token,
weight.maybe_cover_image_public_bucket_hash.as_deref(),
weight.maybe_cover_image_public_bucket_prefix.as_deref(),
@@ -234,7 +234,6 @@ pub async fn list_available_weights_handler(
.map(|weight| {
let cover_image_details = WeightsCoverImageDetails::from_optional_db_fields(
media_domain,
server_state.server_environment,
&weight.token,
weight.maybe_cover_image_public_bucket_hash.as_deref(),
weight.maybe_cover_image_public_bucket_prefix.as_deref(),
@@ -235,7 +235,6 @@ pub async fn list_featured_weights_handler(
.map(|w| {
let cover_image_details = WeightsCoverImageDetails::from_optional_db_fields(
media_domain,
server_state.server_environment,
&w.token,
w.maybe_cover_image_public_bucket_hash.as_deref(),
w.maybe_cover_image_public_bucket_prefix.as_deref(),
@@ -156,7 +156,6 @@ pub async fn list_pinned_weights_handler(
.map(|w| {
let cover_image_details = WeightsCoverImageDetails::from_optional_db_fields(
media_domain,
server_state.server_environment,
&w.token,
w.maybe_cover_image_public_bucket_hash.as_deref(),
w.maybe_cover_image_public_bucket_prefix.as_deref(),
@@ -212,7 +212,6 @@ pub async fn list_weights_by_user_handler(
let cover_image_details = WeightsCoverImageDetails::from_optional_db_fields(
media_domain,
server_state.server_environment,
&weight.token,
weight.maybe_cover_image_public_bucket_hash.as_deref(),
weight.maybe_cover_image_public_bucket_prefix.as_deref(),
@@ -201,7 +201,6 @@ pub async fn search_model_weights_impl(
.map(|result| {
let cover_image_details = WeightsCoverImageDetails::from_optional_db_fields(
media_domain,
server_state.server_environment,
&result.token,
result.maybe_cover_image_public_bucket_hash.as_deref(),
result.maybe_cover_image_public_bucket_prefix.as_deref(),
@@ -2,20 +2,13 @@ use url::Url;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use errors::AnyhowResult;
use server_environment::ServerEnvironment;
use crate::http_server::common_responses::media::cdn_link;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
#[deprecated(note = "This is tied to GCP and is legacy.")]
pub fn bucket_url_from_media_path(
bucket_path: &MediaFileBucketPath,
domain: MediaDomain,
server_environment: ServerEnvironment,
) -> AnyhowResult<Url> {
let path = bucket_path.get_full_object_path_str();
let host = cdn_link::get_cdn_host(domain, server_environment);
let url = format!("{host}{path}");
let url = format!("https://cdn-2.fakeyou.com{}", path);
let url = Url::parse(&url)?;
Ok(url)
}
@@ -25,24 +18,14 @@ mod tests {
use url::Url;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use server_environment::ServerEnvironment;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::web_utils::bucket_urls::bucket_url_from_media_path::bucket_url_from_media_path;
#[test]
fn test_production() {
fn test() {
let bucket_path = MediaFileBucketPath::from_object_hash("test", Some("pre_"), Some(".ext"));
assert_eq!(bucket_url_from_media_path(&bucket_path, MediaDomain::FakeYou, ServerEnvironment::Production).unwrap(),
Url::parse("https://cdn-2.fakeyou.com/media/t/e/s/test/pre_test.ext").unwrap());
}
#[test]
fn test_development() {
let bucket_path = MediaFileBucketPath::from_object_hash("test", Some("pre_"), Some(".ext"));
assert_eq!(bucket_url_from_media_path(&bucket_path, MediaDomain::FakeYou, ServerEnvironment::Development).unwrap(),
Url::parse("https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/t/e/s/test/pre_test.ext").unwrap());
assert_eq!(bucket_url_from_media_path(&bucket_path).unwrap(),
Url::parse("https://storage.googleapis.com/vocodes-public/media/t/e/s/test/pre_test.ext").unwrap());
}
}
@@ -1,18 +1,11 @@
use url::Url;
use errors::AnyhowResult;
use server_environment::ServerEnvironment;
use crate::http_server::common_responses::media::cdn_link;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
pub fn bucket_url_from_str_path(
bucket_path: &str,
domain: MediaDomain,
server_environment: ServerEnvironment,
) -> AnyhowResult<Url> {
let host = cdn_link::get_cdn_host(domain, server_environment);
let url = format!("{host}{bucket_path}");
let url = format!("https://cdn-2.fakeyou.com{}", bucket_path);
let url = Url::parse(&url)?;
Ok(url)
}
@@ -20,20 +13,12 @@ pub fn bucket_url_from_str_path(
#[cfg(test)]
mod tests {
use url::Url;
use server_environment::ServerEnvironment;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::web_utils::bucket_urls::bucket_url_from_str_path::bucket_url_from_str_path;
#[test]
fn test_production() {
assert_eq!(bucket_url_from_str_path("/foo/bar", MediaDomain::FakeYou, ServerEnvironment::Production).unwrap(),
Url::parse("https://cdn-2.fakeyou.com/foo/bar").unwrap());
}
#[test]
fn test_development() {
assert_eq!(bucket_url_from_str_path("/foo/bar", MediaDomain::FakeYou, ServerEnvironment::Development).unwrap(),
Url::parse("https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/foo/bar").unwrap());
fn test() {
assert_eq!(bucket_url_from_str_path("/foo/bar").unwrap(),
Url::parse("https://storage.googleapis.com/vocodes-public/foo/bar").unwrap());
}
}
@@ -1,41 +1,24 @@
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use server_environment::ServerEnvironment;
use crate::http_server::common_responses::media::cdn_link;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
/// An infallible version of `bucket_url_from_media_path` that returns a string.
pub fn bucket_url_string_from_media_path(
bucket_path: &MediaFileBucketPath,
domain: MediaDomain,
server_environment: ServerEnvironment,
) -> String {
let path = bucket_path.get_full_object_path_str();
let host = cdn_link::get_cdn_host(domain, server_environment);
format!("{host}{path}")
format!("https://cdn-2.fakeyou.com{}", path)
}
#[cfg(test)]
mod tests {
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use server_environment::ServerEnvironment;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
#[test]
fn test_production() {
fn test() {
let bucket_path = MediaFileBucketPath::from_object_hash("test", Some("pre_"), Some(".ext"));
assert_eq!(&bucket_url_string_from_media_path(&bucket_path, MediaDomain::FakeYou, ServerEnvironment::Production),
"https://cdn-2.fakeyou.com/media/t/e/s/test/pre_test.ext");
}
#[test]
fn test_development() {
let bucket_path = MediaFileBucketPath::from_object_hash("test", Some("pre_"), Some(".ext"));
assert_eq!(&bucket_url_string_from_media_path(&bucket_path, MediaDomain::FakeYou, ServerEnvironment::Development),
"https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/t/e/s/test/pre_test.ext");
assert_eq!(&bucket_url_string_from_media_path(&bucket_path),
"https://storage.googleapis.com/vocodes-public/media/t/e/s/test/pre_test.ext");
}
}
@@ -1,7 +1,7 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import tsconfigPaths from 'vite-tsconfig-paths';
import { execSync } from 'child_process';
import path from 'path';
@@ -42,7 +42,7 @@ export default defineConfig(() => ({
port: 4300,
host: 'localhost',
},
plugins: [generateNewsPlugin(), nxViteTsPaths(), react()],
plugins: [generateNewsPlugin(), tsconfigPaths(), react()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
@@ -140,12 +140,6 @@ export const ImageTo3DExperience = ({
const clearPendingExternalImage = useImageTo3DWorldStore(
(s) => s.clearPendingExternalImage,
);
const pendingObjectImage = useImageTo3DStore(
(s) => s.pendingExternalImage,
);
const clearPendingObjectImage = useImageTo3DStore(
(s) => s.clearPendingExternalImage,
);
const results = variant === "object" ? objectResults : worldResults;
const resetResults = variant === "object" ? objectReset : worldReset;
@@ -159,34 +153,24 @@ export const ImageTo3DExperience = ({
useEffect(() => {
// Read from store directly to avoid Strict Mode double-execution adding the image twice
if (variant === "world") {
const pending = useImageTo3DWorldStore.getState().pendingExternalImage;
if (pending) {
setWorldImages((prev) => {
if (prev.length >= MAX_WORLD_IMAGES) return prev;
return [
...prev,
{
id: generateId(),
preview: pending.url,
mediaToken: pending.mediaToken,
name: "Library Image",
isUploading: false,
},
];
});
clearPendingExternalImage();
}
} else if (variant === "object") {
const pending = useImageTo3DStore.getState().pendingExternalImage;
if (pending) {
setUploadedPreview(pending.url);
setUploadedMediaToken(pending.mediaToken);
setUploadedName("Library Image");
clearPendingObjectImage();
}
const pending = useImageTo3DWorldStore.getState().pendingExternalImage;
if (variant === "world" && pending) {
setWorldImages((prev) => {
if (prev.length >= MAX_WORLD_IMAGES) return prev;
return [
...prev,
{
id: generateId(),
preview: pending.url,
mediaToken: pending.mediaToken,
name: "Library Image",
isUploading: false,
},
];
});
clearPendingExternalImage();
}
}, [variant, pendingExternalImage, clearPendingExternalImage, pendingObjectImage, clearPendingObjectImage]);
}, [variant, pendingExternalImage, clearPendingExternalImage]);
useEffect(() => {
const onResize = () => setVh(window.innerHeight);
@@ -10,7 +10,6 @@ import {
faBroom,
faBomb,
faCircleExclamation,
faTriangleExclamation,
} from "@fortawesome/pro-solid-svg-icons";
import { Modal } from "@storyteller/ui-modal";
import {
@@ -56,7 +55,6 @@ type InProgressTask = {
updatedAt?: Date;
canDismiss?: boolean;
estimatedTimeLeftMs?: number;
modelType?: string;
};
type CompletedTask = {
@@ -78,7 +76,6 @@ type FailedTask = {
subtitle?: string;
failedAt?: Date;
status: string;
failureReason?: string;
};
const formatTimeLeft = (ms: number): string => {
@@ -107,7 +104,6 @@ const InProgressCard = ({
: task.estimatedTimeLeftMs != null && task.estimatedTimeLeftMs > 0
? formatTimeLeft(task.estimatedTimeLeftMs)
: null;
const isSeedance2 = task.modelType === "seedance_2p0";
return (
<div className="rounded-md p-2 transition-colors hover:bg-ui-controls/40">
<div className="flex items-center gap-2.5">
@@ -120,23 +116,8 @@ const InProgressCard = ({
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1.5 truncate font-medium text-base-fg/90">
<div className="truncate font-medium text-base-fg/90">
{task.title}
{isSeedance2 && (
<Tooltip
content="Seedance 2.0 is in Early Alpha. Generations may be slow and may experience outages."
position="top"
strategy="fixed"
className="w-[200px] text-wrap bg-yellow-400/60 backdrop-blur-3xl"
zIndex={50}
delay={100}
>
<FontAwesomeIcon
icon={faTriangleExclamation}
className="h-3 w-3 shrink-0 text-yellow-400/60 transition-all hover:text-yellow-400"
/>
</Tooltip>
)}
</div>
<div className="ml-2 shrink-0 text-xs tabular-nums text-base-fg/60">
{progressPercent}%
@@ -256,18 +237,6 @@ const FAILED_STATUS_LABEL: Record<string, string> = {
cancelled_by_us: "Cancelled",
};
const FAILURE_REASON_LABEL: Record<string, string> = {
rule_bans_user_image: "Image violates content policy",
rule_bans_user_image_with_faces: "Images with faces are not allowed",
rule_bans_user_text_prompt: "Text prompt violates content policy",
rule_bans_user_content: "Content violates content policy",
rule_bans_generated_video: "Generated video flagged by content policy",
rule_bans_generated_audio: "Generated audio flagged by content policy",
rule_bans_generated_content: "Generated content flagged by content policy",
generation_failed: "Generation failed",
unknown: "An unknown error occurred",
};
const FailedCard = ({
task,
onDismiss,
@@ -300,11 +269,6 @@ const FailedCard = ({
<div className="mt-1 text-xs font-medium text-red-400">
{statusLabel}
</div>
{task.failureReason && (
<div className="mt-0.5 text-xs text-red-400/60">
{task.failureReason}
</div>
)}
</div>
{onDismiss && (
<button
@@ -448,9 +412,7 @@ export const TaskQueue = () => {
? getProviderDisplayName(String(t.provider).toLowerCase())
: undefined;
const taskTypeStr = t.task_type ? String(t.task_type).toLowerCase() : "";
const modelTypeStr = t.model_type
? String(t.model_type).toLowerCase()
: "";
const modelTypeStr = t.model_type ? String(t.model_type).toLowerCase() : "";
const isSplatModel =
taskTypeStr.includes("gaussian") ||
modelTypeStr.includes("marble") ||
@@ -536,7 +498,6 @@ export const TaskQueue = () => {
updatedAt: t.updated_at,
canDismiss,
estimatedTimeLeftMs,
modelType: t.model_type ? String(t.model_type) : undefined,
};
});
@@ -601,19 +562,12 @@ export const TaskQueue = () => {
.sort((a, b) => b.updated_at.getTime() - a.updated_at.getTime())
.map((t: TaskQueueItem) => {
const parts = formatTitleParts(t);
const fr = t.failure_reason;
const failureReason = fr
? fr.failure_message ||
FAILURE_REASON_LABEL[fr.failure_type] ||
undefined
: undefined;
return {
id: t.id,
title: parts.title || "Task",
subtitle: parts.subtitle,
failedAt: t.completed_at || t.updated_at,
status: t.task_status,
failureReason,
};
});
@@ -63,7 +63,6 @@ import {
is3DSceneLoaded,
set3DPageMounted,
} from "~/pages/PageEnigma/Editor/editor";
import { useImageTo3DStore } from "~/pages/PageImageTo3DObject/ImageTo3DStore";
import { useImageTo3DWorldStore } from "~/pages/PageImageTo3DWorld/ImageTo3DWorldStore";
import { useRemoveBackgroundStore } from "~/pages/PageRemoveBackground/RemoveBackgroundStore";
import { TabId, useTabStore } from "~/pages/Stores/TabState";
@@ -275,23 +274,6 @@ export const TopBar = ({ pageName }: Props) => {
}
};
const handleMake3DObjectFromGallery = async (
url: string,
mediaId?: string,
) => {
try {
if (mediaId) {
useImageTo3DStore.getState().setPendingExternalImage(url, mediaId);
}
useTabStore.getState().setActiveTab("IMAGE_TO_3D_OBJECT");
galleryModalVisibleViewMode.value = false;
galleryModalVisibleDuringDrag.value = false;
galleryModalLightboxVisible.value = false;
} catch (e) {
// no-op
}
};
const handleMake3DWorldFromGallery = async (
url: string,
mediaId?: string,
@@ -638,7 +620,6 @@ export const TopBar = ({ pageName }: Props) => {
onEditClicked={handleEditFromGallery}
onTurnIntoVideoClicked={handleTurnIntoVideoFromGallery}
onRemoveBackgroundClicked={handleRemoveBackgroundFromGallery}
onMake3DObjectClicked={handleMake3DObjectFromGallery}
onMake3DWorldClicked={handleMake3DWorldFromGallery}
/>
@@ -1,9 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react";
import {
JobContextType,
getThumbnailUrl,
THUMBNAIL_SIZES,
} from "@storyteller/common";
import { JobContextType } from "@storyteller/common";
import { PromptBoxImage } from "@storyteller/ui-promptbox";
import { uploadImage } from "../../components/reusable/UploadModalMedia/uploadImage";
import BackgroundGallery from "./BackgroundGallery";
@@ -198,7 +194,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
key={`sk-${batch.id}-${i}`}
className="aspect-square w-full overflow-hidden rounded-lg bg-white/[0.03]"
>
<div className="h-full w-full animate-shimmer bg-gradient-to-r from-transparent via-white/10 to-transparent bg-[length:200%_100%]" />
<div className="animate-shimmer h-full w-full bg-gradient-to-r from-transparent via-white/10 to-transparent bg-[length:200%_100%]" />
</div>
))}
{Array.from({
@@ -226,16 +222,8 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
const lightboxItem = {
id: img.media_token,
label: batch.prompt || "Generated Image",
thumbnail:
getThumbnailUrl(
img.maybe_thumbnail_template,
{ width: THUMBNAIL_SIZES.MEDIUM },
) ?? img.cdn_url,
fullImage:
getThumbnailUrl(
img.maybe_thumbnail_template,
{ width: 2048 },
) ?? img.cdn_url,
thumbnail: img.cdn_url,
fullImage: img.cdn_url,
createdAt: new Date(
batch.createdAt,
).toISOString(),
@@ -244,13 +232,6 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
(image) => image.media_token,
),
imageUrls: batch.images.map(
(image) =>
getThumbnailUrl(
image.maybe_thumbnail_template,
{ width: 3200 },
) ?? image.cdn_url,
),
actionUrls: batch.images.map(
(image) => image.cdn_url,
),
thumbnailUrlTemplate:
@@ -264,14 +245,8 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
className="aspect-square w-full overflow-hidden rounded-lg transition-opacity duration-200 hover:cursor-pointer hover:opacity-75"
>
<img
src={
getThumbnailUrl(
img.maybe_thumbnail_template,
{ width: THUMBNAIL_SIZES.LARGE },
) ?? img.cdn_url
}
src={img.cdn_url}
alt="Generated"
loading="lazy"
className="h-full w-full bg-black/10 object-cover"
/>
</button>
@@ -129,11 +129,6 @@ async function captureModelThumbnail(modelUrl: string): Promise<string | null> {
});
}
export interface PendingExternalImage {
url: string;
mediaToken: string;
}
export type ImageTo3DResult = {
id: string;
mode: "image" | "text";
@@ -154,9 +149,6 @@ export const coverImageCache = new Map<string, string>();
type ImageTo3DState = {
results: ImageTo3DResult[];
pendingExternalImage: PendingExternalImage | null;
setPendingExternalImage: (url: string, mediaToken: string) => void;
clearPendingExternalImage: () => void;
startGeneration: (
mode: "image" | "text",
note: string,
@@ -183,13 +175,6 @@ type ImageTo3DState = {
export const useImageTo3DStore = create<ImageTo3DState>((set, get) => ({
results: [],
pendingExternalImage: null,
setPendingExternalImage: (url: string, mediaToken: string) => {
set({ pendingExternalImage: { url, mediaToken } });
},
clearPendingExternalImage: () => {
set({ pendingExternalImage: null });
},
startGeneration: (
mode: "image" | "text",
note: string,
-1
View File
@@ -1 +0,0 @@
/* /index.html 200
-25
View File
@@ -1,25 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "~/components",
"utils": "~/lib/utils",
"ui": "~/components/ui",
"lib": "~/lib",
"hooks": "~/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
-44
View File
@@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Primary Meta Tags -->
<title>GenHub - Discover & Share AI-Generated Art and Prompts</title>
<meta name="description" content="Discover, share, and remix AI-generated art and prompts. Browse a community-driven gallery of images created with Stable Diffusion, Midjourney, DALL-E, and more." />
<meta name="keywords" content="AI art, AI prompts, stable diffusion, midjourney, DALL-E, prompt gallery, AI generated images, text to image, prompt engineering, generative AI" />
<meta name="theme-color" content="#000000" />
<meta name="author" content="GenHub" />
<!-- Canonical -->
<link rel="canonical" href="https://genhub.app/" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://genhub.app/" />
<meta property="og:title" content="GenHub - Discover & Share AI-Generated Art and Prompts" />
<meta property="og:description" content="Discover, share, and remix AI-generated art and prompts. Browse a community-driven gallery of images created with Stable Diffusion, Midjourney, DALL-E, and more." />
<meta property="og:image" content="https://genhub.app/og-image.png" />
<meta property="og:site_name" content="GenHub" />
<meta property="og:locale" content="en_US" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://genhub.app/" />
<meta name="twitter:title" content="GenHub - Discover & Share AI-Generated Art and Prompts" />
<meta name="twitter:description" content="Discover, share, and remix AI-generated art and prompts. Browse a community-driven gallery of images created with Stable Diffusion, Midjourney, DALL-E, and more." />
<meta name="twitter:image" content="https://genhub.app/og-image.png" />
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/og-image.png" />
<!-- Styles -->
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-41
View File
@@ -1,41 +0,0 @@
[build]
command = "./apps/genhub/script/netlify_build.sh"
publish = "apps/genhub/dist/"
[[headers]]
for = "/build/*"
[headers.values]
"Cache-Control" = "public, max-age=31560000, immutable"
[[headers]]
for = "/og-image.png"
[headers.values]
"Cache-Control" = "public, max-age=86400"
[[headers]]
for = "/robots.txt"
[headers.values]
"Cache-Control" = "public, max-age=3600"
[[headers]]
for = "/sitemap.xml"
[headers.values]
"Cache-Control" = "public, max-age=3600"
# [[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
-26
View File
@@ -1,26 +0,0 @@
{
"name": "@frontend/genhub",
"version": "0.0.1",
"private": true,
"dependencies": {
"@fontsource-variable/figtree": "^5.2.10",
"@fontsource-variable/geist": "^5.2.8",
"@tailwindcss/vite": "^4.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.512.0",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react-helmet-async": "^3.0.0",
"react-masonry-css": "^1.0.16",
"react-resizable-panels": "^4.7.2",
"shadcn": "^4.0.5",
"sonner": "^2.0.7",
"tailwind-merge": "^2.2.2",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0",
"vaul": "^1.1.2"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

-4
View File
@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://genhub.app/sitemap.xml
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://genhub.app/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72.22 65.68">
<g id="Layer_1-2" data-name="Layer 1">
<path d="M69.38,45.46c-3.18-6.24-6.58-12.49-9.86-18.53-3.31-6.08-6.72-12.37-9.91-18.63l-.05-.09C46.44,3.07,41.24,0,35.66,0s-10.34,3-13.13,8.23L.97,48.96l-.03.07c-1.51,3.8-1.2,8.03.82,11.31,1.9,3.08,5.01,4.96,8.76,5.28.38.03.75.05,1.1.05,2.94,0,4.98-1.12,7.33-2.41,7.13-3.91,19.45-11.21,27.61-16.04,2.6-1.54,4.95-2.93,6.31-3.73l10.71,20.43c.2.39.54.7.95.86.46.19.98.2,1.43,0,1.39-.66,3.05-2.47,3.07-2.49,5.22-5.9,3.03-11.55.34-16.83ZM34.53,14.8c.31-.46.77-.99,1.32-.99.58,0,1.29.57,1.85,1.49,1.57,2.56,2.99,5.44,4.35,8.22,1.34,2.73,2.72,5.55,4.29,8.15l.02.05-29.6,16.99L34.53,14.8Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 781 B

@@ -1,20 +0,0 @@
#!/bin/bash
set -euxo pipefail
echo "Run build script (TODO: Make strict)"
nx build genhub
echo "Change to project dir"
pushd apps/genhub/
echo "Copy netlify configs into dist"
#cp _headers dist/
cp _redirects dist/
#echo "Copy netlify 404.html page into dist"
#cp "404.html" dist/
echo "List final files in build"
find dist/
@@ -1 +0,0 @@
/* Your styles goes here. */
-16
View File
@@ -1,16 +0,0 @@
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import App from './app';
describe('App', () => {
it('should render successfully', () => {
const { baseElement } = render(<BrowserRouter><App /></BrowserRouter>);
expect(baseElement).toBeTruthy();
});
it('should have a greeting as the title', () => {
const { getAllByText } = render(<BrowserRouter><App /></BrowserRouter>);
expect(getAllByText(new RegExp('Welcome @frontend/genhub', 'gi')).length > 0).toBeTruthy();
});
});
-60
View File
@@ -1,60 +0,0 @@
import { Routes, Route, useLocation, useNavigate, useParams, Navigate } from 'react-router-dom';
import type { Location } from 'react-router-dom';
import { ThemeProvider } from 'next-themes';
import { SEO } from '~/components/seo';
import { Navbar } from '~/components/layout/navbar';
import { AuthProvider } from '~/hooks/use-auth';
import { FeedPage } from '~/pages/feed';
import { LoginPage } from '~/pages/login';
import { SignupPage } from '~/pages/signup';
import { PromptPage } from '~/pages/prompt';
import { MediaDialog } from '~/components/feed/media-dialog';
import { getItemById } from '~/data/mock-gallery';
/** Modal overlay rendered when navigating to /prompt/:id from the feed. */
function PromptModal() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const item = id ? getItemById(id) : null;
if (!item) return <Navigate to="/" replace />;
return (
<MediaDialog
item={item}
open
onOpenChange={(open) => {
if (!open) navigate(-1);
}}
/>
);
}
export function App() {
const location = useLocation();
const state = location.state as { backgroundLocation?: Location } | null;
return (
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
<AuthProvider>
<SEO />
<Navbar />
<Routes location={state?.backgroundLocation || location}>
<Route path="/" element={<FeedPage />} />
<Route path="/prompt/:id" element={<PromptPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
</Routes>
{/* When coming from feed, show the prompt as a modal overlay */}
{state?.backgroundLocation && (
<Routes>
<Route path="/prompt/:id" element={<PromptModal />} />
</Routes>
)}
</AuthProvider>
</ThemeProvider>
);
}
export default App;
-857
View File
@@ -1,857 +0,0 @@
/*
* * * * * * * * * * * * * * * * * * * * * * * * * * * *
This is a starter component and can be deleted.
* * * * * * * * * * * * * * * * * * * * * * * * * * * *
Delete this file and get started with your project!
* * * * * * * * * * * * * * * * * * * * * * * * * * * *
*/
export function NxWelcome({ title }: { title: string }) {
return (
<>
<style
dangerouslySetInnerHTML={{
__html: `
html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
}
body {
font-family: inherit;
line-height: inherit;
margin: 0;
}
h1,
h2,
p,
pre {
margin: 0;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
h1,
h2 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
}
svg {
display: block;
vertical-align: middle;
shape-rendering: auto;
text-rendering: optimizeLegibility;
}
pre {
background-color: rgba(55, 65, 81, 1);
border-radius: 0.25rem;
color: rgba(229, 231, 235, 1);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
overflow: auto;
padding: 0.5rem 0.75rem;
}
.shadow {
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded {
border-radius: 1.5rem;
}
.wrapper {
width: 100%;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 768px;
padding-bottom: 3rem;
padding-left: 1rem;
padding-right: 1rem;
color: rgba(55, 65, 81, 1);
width: 100%;
}
#welcome {
margin-top: 2.5rem;
}
#welcome h1 {
font-size: 3rem;
font-weight: 500;
letter-spacing: -0.025em;
line-height: 1;
}
#welcome span {
display: block;
font-size: 1.875rem;
font-weight: 300;
line-height: 2.25rem;
margin-bottom: 0.5rem;
}
#hero {
align-items: center;
background-color: hsla(214, 62%, 21%, 1);
border: none;
box-sizing: border-box;
color: rgba(55, 65, 81, 1);
display: grid;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#hero .text-container {
color: rgba(255, 255, 255, 1);
padding: 3rem 2rem;
}
#hero .text-container h2 {
font-size: 1.5rem;
line-height: 2rem;
position: relative;
}
#hero .text-container h2 svg {
color: hsla(162, 47%, 50%, 1);
height: 2rem;
left: -0.25rem;
position: absolute;
top: 0;
width: 2rem;
}
#hero .text-container h2 span {
margin-left: 2.5rem;
}
#hero .text-container a {
background-color: rgba(255, 255, 255, 1);
border-radius: 0.75rem;
color: rgba(55, 65, 81, 1);
display: inline-block;
margin-top: 1.5rem;
padding: 1rem 2rem;
text-decoration: inherit;
}
#hero .logo-container {
display: none;
justify-content: center;
padding-left: 2rem;
padding-right: 2rem;
}
#hero .logo-container svg {
color: rgba(255, 255, 255, 1);
width: 66.666667%;
}
#middle-content {
align-items: flex-start;
display: grid;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#middle-content #middle-content-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
#learning-materials {
padding: 2.5rem 2rem;
}
#learning-materials h2 {
font-weight: 500;
font-size: 1.25rem;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.list-item-link {
align-items: center;
border-radius: 0.75rem;
display: flex;
margin-top: 1rem;
padding: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 100%;
}
.list-item-link svg:first-child {
margin-right: 1rem;
height: 1.5rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1.5rem;
}
.list-item-link > span {
flex-grow: 1;
font-weight: 400;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.list-item-link > span > span {
color: rgba(107, 114, 128, 1);
display: block;
flex-grow: 1;
font-size: 0.75rem;
font-weight: 300;
line-height: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.list-item-link svg:last-child {
height: 1rem;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1rem;
}
.list-item-link:hover {
color: rgba(255, 255, 255, 1);
background-color: hsla(162, 55%, 33%, 1);
}
.list-item-link:hover > span {}
.list-item-link:hover > span > span {
color: rgba(243, 244, 246, 1);
}
.list-item-link:hover svg:last-child {
transform: translateX(0.25rem);
}
#other-links {}
.button-pill {
padding: 1.5rem 2rem;
margin-bottom: 2rem;
transition-duration: 300ms;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
align-items: center;
display: flex;
}
.button-pill svg {
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
flex-shrink: 0;
width: 3rem;
}
.button-pill > span {
letter-spacing: -0.025em;
font-weight: 400;
font-size: 1.125rem;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.button-pill span span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
.button-pill:hover svg,
.button-pill:hover {
color: rgba(255, 255, 255, 1) !important;
}
#nx-console:hover {
background-color: rgba(0, 122, 204, 1);
}
#nx-console svg {
color: rgba(0, 122, 204, 1);
}
#nx-console-jetbrains {
margin-top: 2rem;
}
#nx-console-jetbrains:hover {
background-color: rgba(255, 49, 140, 1);
}
#nx-console-jetbrains svg {
color: rgba(255, 49, 140, 1);
}
#nx-repo:hover {
background-color: rgba(24, 23, 23, 1);
}
#nx-repo svg {
color: rgba(24, 23, 23, 1);
}
#nx-cloud {
margin-bottom: 2rem;
margin-top: 2rem;
padding: 2.5rem 2rem;
}
#nx-cloud > div {
align-items: center;
display: flex;
}
#nx-cloud > div svg {
border-radius: 0.375rem;
flex-shrink: 0;
width: 3rem;
}
#nx-cloud > div h2 {
font-size: 1.125rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#nx-cloud > div h2 span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
#nx-cloud p {
font-size: 1rem;
line-height: 1.5rem;
margin-top: 1rem;
}
#nx-cloud pre {
margin-top: 1rem;
}
#nx-cloud a {
color: rgba(107, 114, 128, 1);
display: block;
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 1.5rem;
text-align: right;
}
#nx-cloud a:hover {
text-decoration: underline;
}
#commands {
padding: 2.5rem 2rem;
margin-top: 3.5rem;
}
#commands h2 {
font-size: 1.25rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#commands p {
font-size: 1rem;
font-weight: 300;
line-height: 1.5rem;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
details {
align-items: center;
display: flex;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
width: 100%;
}
details pre > span {
color: rgba(181, 181, 181, 1);
display: block;
}
summary {
border-radius: 0.5rem;
display: flex;
font-weight: 400;
padding: 0.5rem;
cursor: pointer;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
summary:hover {
background-color: rgba(243, 244, 246, 1);
}
summary svg {
height: 1.5rem;
margin-right: 1rem;
width: 1.5rem;
}
#love {
color: rgba(107, 114, 128, 1);
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 3.5rem;
opacity: 0.6;
text-align: center;
}
#love svg {
color: rgba(252, 165, 165, 1);
width: 1.25rem;
height: 1.25rem;
display: inline;
margin-top: -0.25rem;
}
@media screen and (min-width: 768px) {
#hero {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#hero .logo-container {
display: flex;
}
#middle-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4rem;
}
}
`,
}}
/>
<div className="wrapper">
<div className="container">
<div id="welcome">
<h1>
<span> Hello there, </span>
Welcome {title} 👋
</h1>
</div>
<div id="hero" className="rounded">
<div className="text-container">
<h2>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
/>
</svg>
<span>You&apos;re up and running</span>
</h2>
<a href="#commands"> What&apos;s next? </a>
</div>
<div className="logo-container">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z" />
</svg>
</div>
</div>
<div id="middle-content">
<div id="middle-content-container">
<div id="learning-materials" className="rounded shadow">
<h2>Learning materials</h2>
<a
href="https://nx.dev/getting-started/intro?utm_source=nx-project"
target="_blank"
rel="noreferrer"
className="list-item-link"
>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
<span>
Documentation
<span> Everything is in there </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a
href="https://nx.dev/blog/?utm_source=nx-project"
target="_blank"
rel="noreferrer"
className="list-item-link"
>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
/>
</svg>
<span>
Blog
<span> Changelog, features & events </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a
href="https://www.youtube.com/@NxDevtools/videos?utm_source=nx-project&sub_confirmation=1"
target="_blank"
rel="noreferrer"
className="list-item-link"
>
<svg
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>YouTube</title>
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
<span>
YouTube channel
<span> Nx Show, talks & tutorials </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a
href="https://nx.dev/react-tutorial/1-code-generation?utm_source=nx-project"
target="_blank"
rel="noreferrer"
className="list-item-link"
>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
/>
</svg>
<span>
Interactive tutorials
<span> Create an app, step-by-step </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
</div>
<a
id="nx-repo"
className="button-pill rounded shadow"
href="https://github.com/nrwl/nx?utm_source=nx-project"
target="_blank"
rel="noreferrer"
>
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
<span>
Nx is open source
<span> Love Nx? Give us a star! </span>
</span>
</a>
</div>
<div id="other-links">
<a
id="nx-console"
className="button-pill rounded shadow"
href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project"
target="_blank"
rel="noreferrer"
>
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Visual Studio Code</title>
<path d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z" />
</svg>
<span>
Install Nx Console for VSCode
<span>The official VSCode extension for Nx.</span>
</span>
</a>
<a
id="nx-console-jetbrains"
className="button-pill rounded shadow"
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
target="_blank"
rel="noreferrer"
>
<svg
height="48"
width="48"
viewBox="20 20 60 60"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m22.5 22.5h60v60h-60z" />
<g fill="#fff">
<path d="m29.03 71.25h22.5v3.75h-22.5z" />
<path d="m28.09 38 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.44 1.06-1.31v-5.98h2.58v6a3.48 3.48 0 0 1 -.87 2.6 3.56 3.56 0 0 1 -2.57.95 3.84 3.84 0 0 1 -3.34-1.55z" />
<path d="m36 30h7.53v2.19h-5v1.44h4.49v2h-4.42v1.49h5v2.21h-7.6z" />
<path d="m47.23 32.29h-2.8v-2.29h8.21v2.27h-2.81v7.1h-2.6z" />
<path d="m29.13 43.08h4.42a3.53 3.53 0 0 1 2.55.83 2.09 2.09 0 0 1 .6 1.53 2.16 2.16 0 0 1 -1.44 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.61-1.31 2.59-3.55 2.59h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.24c.79 0 1.25-.26 1.25-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86 0-.5-.4-.87-1.27-.87z" />
<path d="m38 43.08h4.1a4.19 4.19 0 0 1 3 1 2.93 2.93 0 0 1 .9 2.19 3 3 0 0 1 -1.93 2.89l2.24 3.27h-3l-1.88-2.84h-.87v2.84h-2.56zm4 4.5c.87 0 1.39-.43 1.39-1.11 0-.75-.54-1.12-1.4-1.12h-1.44v2.26z" />
<path d="m49.59 43h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.69h-2.71zm2.27 5.73-1-2.65-1.06 2.65z" />
<path d="m56.46 43.05h2.6v9.37h-2.6z" />
<path d="m60.06 43.05h2.42l3.37 5v-5h2.57v9.37h-2.26l-3.53-5.14v5.14h-2.57z" />
<path d="m68.86 51 1.45-1.73a4.84 4.84 0 0 0 3 1.13c.71 0 1.08-.24 1.08-.65 0-.4-.31-.6-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.37-3 3.62-3a5.89 5.89 0 0 1 3.86 1.25l-1.26 1.84a4.63 4.63 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .42.32.61 1.63.91 2.14.46 3.44 1.16 3.44 2.91 0 1.91-1.51 3-3.79 3a6.58 6.58 0 0 1 -4.35-1.5z" />
</g>
</svg>
<span>
Install Nx Console for JetBrains
<span>
Available for WebStorm, Intellij IDEA Ultimate and more!
</span>
</span>
</a>
<div id="nx-cloud" className="rounded shadow">
<div>
<svg
id="nx-cloud-logo"
role="img"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
fill="transparent"
viewBox="0 0 24 24"
>
<path
strokeWidth="2"
d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z"
/>
<path
strokeWidth="2"
d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z"
/>
</svg>
<h2>
Nx Cloud
<span>Enable faster CI & better DX</span>
</h2>
</div>
<p>
You can activate distributed tasks executions and caching by
running:
</p>
<pre>nx connect</pre>
<a
href="https://nx.dev/nx-cloud?utm_source=nx-project"
target="_blank"
rel="noreferrer"
>
{' '}
What is Nx Cloud?{' '}
</a>
</div>
</div>
</div>
<div id="commands" className="rounded shadow">
<h2>Next steps</h2>
<p>Here are some things you can do with Nx:</p>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Build, test and lint your app
</summary>
<pre>
<span># Build</span>
nx build {title}
<span># Test</span>
nx test {title}
<span># Lint</span>
nx lint {title}
<span># Run them together!</span>
nx run-many -p {title} -t build test lint
</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
View project details
</summary>
<pre>nx show project {title}</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
View interactive project graph
</summary>
<pre>nx graph</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Add UI library
</summary>
<pre>
<span># Generate UI lib</span>
nx g @nx/react:lib ui
<span># Add a component</span>
nx g @nx/react:component ui/src/lib/button
</pre>
</details>
</div>
<p id="love">
Carefully crafted with
<svg
fill="currentColor"
stroke="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
</p>
</div>
</div>
</>
);
}
export default NxWelcome;
@@ -1,31 +0,0 @@
import { cn } from "~/lib/utils";
import { CATEGORIES } from "~/data/mock-gallery";
interface CategoryTagsProps {
activeTag: string | null;
onTagChange: (tag: string | null) => void;
}
export function CategoryTags({ activeTag, onTagChange }: CategoryTagsProps) {
return (
<div className="flex gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{CATEGORIES.map((tag) => {
const isActive = activeTag === tag;
return (
<button
key={tag}
onClick={() => onTagChange(isActive ? null : tag)}
className={cn(
"shrink-0 rounded-full border px-3 py-1 text-sm font-medium transition-colors cursor-pointer",
isActive
? "border-transparent bg-foreground text-background"
: "border-border bg-foreground/4 text-foreground hover:bg-foreground/8 dark:bg-foreground/6 dark:hover:bg-foreground/12",
)}
>
{tag}
</button>
);
})}
</div>
);
}
@@ -1,68 +0,0 @@
import { Image, Play, Video } from "lucide-react";
import { memo, useState } from "react";
import { Skeleton } from "~/components/ui/skeleton";
import type { GalleryItem } from "~/data/mock-gallery";
interface GalleryCardProps {
item: GalleryItem;
onClick?: () => void;
}
export const GalleryCard = memo(function GalleryCard({
item,
onClick,
}: GalleryCardProps) {
const [loaded, setLoaded] = useState(false);
const isVideo = item.mediaType === "video";
return (
<article
onClick={onClick}
className="group relative mb-4 cursor-pointer overflow-hidden rounded-xl bg-muted"
>
{/* Image always in flow — width/height reserves space, opacity handles fade */}
<img
src={item.imageUrl}
alt={item.title}
width={item.imageWidth}
height={item.imageHeight}
loading="lazy"
decoding="async"
onLoad={() => setLoaded(true)}
draggable={false}
className={`w-full object-cover transition-all select-none duration-700 ease-out group-hover:scale-110 ${loaded ? "opacity-100" : "opacity-0"}`}
/>
{/* Skeleton overlay — sits on top until image fades in, then unmounts */}
{!loaded && <Skeleton className="absolute inset-0 rounded-none" />}
{/* Hover dark overlay */}
<div className="pointer-events-none absolute inset-0 bg-black/0 transition-colors duration-500 group-hover:bg-black/20" />
{/* Center play button for videos */}
{isVideo && loaded && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="flex size-12 items-center justify-center rounded-full bg-black/50 text-white backdrop-blur-sm transition-transform duration-300 group-hover:scale-110">
<Play className="size-5 fill-white" />
</div>
</div>
)}
{/* Icon badge — video or image */}
<div className="absolute right-2.5 top-2.5 rounded-lg bg-black/40 p-1.5 text-white/80 backdrop-blur-sm transition-opacity duration-500 group-hover:opacity-100 sm:opacity-0">
{isVideo ? <Video className="size-3.5" /> : <Image className="size-3.5" />}
</div>
{/* Bottom overlay — fades in with image */}
<div
className="absolute inset-x-0 bottom-0 bg-linear-to-t from-black/70 via-black/30 to-transparent px-3 pb-3 pt-10 transition-opacity duration-500"
style={{ opacity: loaded ? 1 : 0 }}
>
<h3 className="text-sm font-semibold leading-tight text-white">
{item.title}
</h3>
<p className="mt-0.5 text-xs text-white/70">{item.creator}</p>
</div>
</article>
);
});
@@ -1,53 +0,0 @@
import Masonry from "react-masonry-css";
import { useEffect, useRef, type ReactNode } from "react";
import { Loader2 } from "lucide-react";
const BREAKPOINT_COLS = {
default: 5,
1536: 4,
1024: 3,
640: 2,
};
interface MasonryGridProps {
children: ReactNode;
hasMore: boolean;
onLoadMore: () => void;
}
export function MasonryGrid({ children, hasMore, onLoadMore }: MasonryGridProps) {
const sentinelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const sentinel = sentinelRef.current;
if (!sentinel || !hasMore) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) onLoadMore();
},
{ rootMargin: "400px" },
);
observer.observe(sentinel);
return () => observer.disconnect();
}, [hasMore, onLoadMore]);
return (
<>
<Masonry
breakpointCols={BREAKPOINT_COLS}
className="flex w-auto -ml-4"
columnClassName="pl-4 bg-clip-padding"
>
{children}
</Masonry>
{hasMore && (
<div ref={sentinelRef} className="flex justify-center py-8">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
)}
</>
);
}

Some files were not shown because too many files have changed in this diff Show More