artcraft task queue 2 (#869)

This commit is contained in:
Brandon Thomas
2025-10-08 21:14:16 -04:00
committed by GitHub
parent 0fec772d9a
commit 3838cc3f21
52 changed files with 1269 additions and 211 deletions
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET is_dismissed_by_user = 1\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "13d435d8f681b73e4080748dcef0579e246e41049af67e1bd79ffc06aa6ba974"
}
@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "task_status",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "task_type",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "model_type",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "provider",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "provider_job_id",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "frontend_caller",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "frontend_subscriber_id",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "frontend_subscriber_payload",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 9,
"type_info": "Int64"
},
{
"name": "updated_at: DateTime<Utc>",
"ordinal": 10,
"type_info": "Int64"
},
{
"name": "completed_at: DateTime<Utc>",
"ordinal": 11,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false,
true
]
},
"hash": "a142dc3fdf5b6e741c268d8f2e9996a28c214f5835681297d58a9431da8de939"
}
@@ -0,0 +1,20 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
select
u.display_name,
count(*) as sessions,
a.session_duration_seconds / 60 / 60 as hours,
a.measurement_count,
u.email_address,
a.os_platform,
a.os_version,
u.created_at,
u.maybe_source,
a.last_active_at
from analytics_app_sessions as a
left outer join users as u
on a.user_token = u.token
group by a.user_token
order by hours desc;
@@ -9,6 +9,7 @@
-- tasks_v1.sqlite - initial version
-- tasks_v2.sqlite - added model_type (nullable)
-- tasks_v3.sqlite - added frontend_caller, comments
-- tasks_v4.sqlite - added is_dismissed_by_user
CREATE TABLE tasks (
-- Task auto-incrementing primary key.
@@ -50,6 +51,9 @@ CREATE TABLE tasks (
-- will be re-emitted back to the frontend.
frontend_subscriber_payload TEXT,
-- Whether the user has dismissed the task from view.
is_dismissed_by_user INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
completed_at INTEGER DEFAULT NULL
@@ -7,4 +7,5 @@ pub mod load_without_cors_command;
pub mod media_files;
pub mod platform_info_command;
pub mod providers;
pub mod response;
pub mod response;
pub mod task_queue;
@@ -0,0 +1,92 @@
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use chrono::{DateTime, Utc};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_model_type::TaskModelType;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use sqlite_tasks::queries::list_tasks_for_frontend::list_tasks_for_frontend;
use storyteller_client::endpoints::media_files::delete_media_file::delete_media_file;
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::sqlite::tasks::TaskId;
#[derive(Serialize)]
pub struct GetTaskQueueCommandResponse {
tasks: Vec<TaskQueueItem>,
}
#[derive(Serialize)]
pub struct TaskQueueItem {
pub id: TaskId,
pub task_status: TaskStatus,
pub task_type: TaskType,
pub model_type: Option<TaskModelType>,
pub provider: Option<GenerationProvider>,
pub provider_job_id: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
impl SerializeMarker for GetTaskQueueCommandResponse {}
#[tauri::command]
pub async fn get_task_queue_command(
app: AppHandle,
app_env_configs: State<'_, AppEnvConfigs>,
task_database: State<'_, TaskDatabase>,
) -> ResponseOrErrorMessage<GetTaskQueueCommandResponse> {
info!("get_task_queue_command called");
let result = handle_request(
&task_database,
).await;
let tasks = match result {
Ok(items) => items,
Err(err) => {
error!("get_task_queue_command failed: {:?}", err);
return Err("get_task_queue_command failed".into())
}
};
Ok(GetTaskQueueCommandResponse{
tasks,
}.into())
}
pub async fn handle_request(
task_database: &TaskDatabase,
) -> AnyhowResult<Vec<TaskQueueItem>> {
let tasks = list_tasks_for_frontend(task_database.get_connection())
.await?;
Ok(tasks.tasks.into_iter()
.map(|task| TaskQueueItem {
id: task.id,
task_status: task.status,
task_type: task.task_type,
model_type: task.model_type,
provider: task.provider,
provider_job_id: task.provider_job_id,
created_at: task.created_at,
updated_at: task.updated_at,
completed_at: task.completed_at,
})
.collect())
}
@@ -0,0 +1,70 @@
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
use crate::services::fal::state::fal_credential_manager::FalCredentialManager;
use crate::services::fal::state::fal_task_queue::FalTaskQueue;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use chrono::{DateTime, Utc};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_model_type::TaskModelType;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use sqlite_tasks::queries::list_tasks_for_frontend::list_tasks_for_frontend;
use sqlite_tasks::queries::mark_task_as_dismissed::mark_task_as_dismissed;
use storyteller_client::endpoints::media_files::delete_media_file::delete_media_file;
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::sqlite::tasks::TaskId;
#[derive(Deserialize)]
pub struct MarkTaskAsDismissedRequest {
task: TaskId,
}
#[derive(Serialize)]
pub struct MarkTaskAsDismissedResponse {
success: bool,
}
impl SerializeMarker for MarkTaskAsDismissedResponse {}
#[tauri::command]
pub async fn mark_task_as_dismissed_command(
request: MarkTaskAsDismissedRequest,
app: AppHandle,
app_env_configs: State<'_, AppEnvConfigs>,
task_database: State<'_, TaskDatabase>,
) -> ResponseOrErrorMessage<MarkTaskAsDismissedResponse> {
info!("mark_task_as_dismissed_command called");
let result = handle_request(
&request.task,
&task_database,
).await;
if let Err(err) = &result {
error!("mark_task_as_dismissed_command failed: {:?}", err);
return Err("mark_task_as_dismissed_command failed".into())
}
Ok(MarkTaskAsDismissedResponse{
success: true,
}.into())
}
pub async fn handle_request(
task_id: &TaskId,
task_database: &TaskDatabase,
) -> AnyhowResult<()> {
let _result = mark_task_as_dismissed(task_database.get_connection(), task_id).await?;
Ok(())
}
@@ -0,0 +1,2 @@
pub mod get_task_queue_command;
pub mod mark_task_as_dismissed_command;
@@ -1,4 +1,5 @@
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::task_database_version::TASK_DATABASE_VERSION;
use std::path::{Path, PathBuf};
#[derive(Clone)]
@@ -24,7 +25,7 @@ impl AppStateDir {
pub fn get_tasks_sqlite_database_path(&self) -> PathBuf {
// NB: This must be incremented on any change to the database schema (including comments).
// The database houses ephemeral content and will be migrated automatically.
self.path.join("tasks_v3.sqlite")
self.path.join(format!("tasks_v{TASK_DATABASE_VERSION}.sqlite"))
}
pub fn get_window_size_config_file(&self) -> PathBuf {
@@ -7,4 +7,5 @@ pub mod expanduser;
pub mod os_platform;
pub mod provider_priority;
pub mod task_database;
pub mod task_database_version;
pub mod window;
@@ -0,0 +1,10 @@
/// Used to bump the task database schema version.
/// Migrations on customers' machines tend to fail on existing databases,
/// even if we instruct that the database file be deleted first.
/// It's easiest to just write to a new file and migrate from scratch.
/// We increment this whenever we make a change to the database schema.
/// This prevents deadlock on startup if the schema has changed from the
/// previous version.
pub const TASK_DATABASE_VERSION: u32 = 4;
+5 -1
View File
@@ -18,6 +18,8 @@ use crate::core::commands::media_files::media_file_delete_command::media_file_de
use crate::core::commands::platform_info_command::platform_info_command;
use crate::core::commands::providers::get_provider_order_command::get_provider_order_command;
use crate::core::commands::providers::set_provider_order_command::set_provider_order_command;
use crate::core::commands::task_queue::get_task_queue_command::get_task_queue_command;
use crate::core::commands::task_queue::mark_task_as_dismissed_command::mark_task_as_dismissed_command;
use crate::core::lifecycle::startup::handle_tauri_startup::handle_tauri_startup;
use crate::core::lifecycle::startup::setup_main_window::setup_main_window;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -190,8 +192,10 @@ pub fn run() {
get_app_preferences_command,
get_fal_api_key_command,
get_provider_order_command,
media_file_delete_command,
get_task_queue_command,
load_without_cors_command,
mark_task_as_dismissed_command,
media_file_delete_command,
midjourney_clear_credentials_command,
midjourney_get_credential_info_command,
midjourney_open_login_command,
@@ -1,10 +1,11 @@
use enums::error::enum_error::EnumError;
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum SqliteTasksError {
SqlxError(sqlx::Error),
TaskParseError(String),
EnumError(EnumError),
//TaskNotFound,
//TaskAlreadyExists,
//InvalidTaskStatus,
@@ -17,8 +18,8 @@ impl Error for SqliteTasksError {}
impl Display for SqliteTasksError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SqliteTasksError::SqlxError(err) => write!(f, "SQLx error: {}", err),
SqliteTasksError::TaskParseError(err) => write!(f, "Task parse error: {}", err),
SqliteTasksError::SqlxError(err) => write!(f, "SQLx error: {:?}", err),
SqliteTasksError::EnumError(err) => write!(f, "Error parsing enum: {:?}", err),
}
}
}
@@ -28,3 +29,9 @@ impl From<sqlx::Error> for SqliteTasksError {
SqliteTasksError::SqlxError(err)
}
}
impl From<EnumError> for SqliteTasksError {
fn from(err: EnumError) -> Self {
SqliteTasksError::EnumError(err)
}
}
@@ -92,20 +92,15 @@ pub async fn list_tasks_by_provider_and_status(
for task in results {
tasks.push(Task {
id: TaskId::new_from_str(&task.id),
status: TaskStatus::from_str(&task.task_status)
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
task_type: TaskType::from_str(&task.task_type)
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
status: TaskStatus::from_str(&task.task_status)?,
task_type: TaskType::from_str(&task.task_type)?,
model_type: task.model_type
.map(|model| TaskModelType::from_str(&model)
.map_err(|err| SqliteTasksError::TaskParseError(err)))
.map(|model| TaskModelType::from_str(&model))
.transpose()?,
provider: GenerationProvider::from_str(&task.provider)
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
provider: GenerationProvider::from_str(&task.provider)?,
provider_job_id: task.provider_job_id,
frontend_caller: task.frontend_caller
.map(|caller| TauriCommandCaller::from_str(&caller)
.map_err(|err| SqliteTasksError::TaskParseError(err)))
.map(|caller| TauriCommandCaller::from_str(&caller))
.transpose()?,
frontend_subscriber_id: task.frontend_subscriber_id,
frontend_subscriber_payload: task.frontend_subscriber_payload,
@@ -91,20 +91,15 @@ pub async fn list_tasks_by_provider_and_tokens(
for task in results {
tasks.push(Task {
id: TaskId::new_from_str(&task.id),
status: TaskStatus::from_str(&task.task_status)
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
task_type: TaskType::from_str(&task.task_type)
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
status: TaskStatus::from_str(&task.task_status)?,
task_type: TaskType::from_str(&task.task_type)?,
model_type: task.model_type
.map(|model| TaskModelType::from_str(&model)
.map_err(|err| SqliteTasksError::TaskParseError(err)))
.map(|model| TaskModelType::from_str(&model))
.transpose()?,
provider: GenerationProvider::from_str(&task.provider)
.map_err(|err| SqliteTasksError::TaskParseError(err))?,
provider: GenerationProvider::from_str(&task.provider)?,
provider_job_id: task.provider_job_id,
frontend_caller: task.frontend_caller
.map(|caller| TauriCommandCaller::from_str(&caller)
.map_err(|err| SqliteTasksError::TaskParseError(err)))
.map(|caller| TauriCommandCaller::from_str(&caller))
.transpose()?,
frontend_subscriber_id: task.frontend_subscriber_id,
frontend_subscriber_payload: task.frontend_subscriber_payload,
@@ -0,0 +1,99 @@
use crate::connection::TaskDbConnection;
use crate::error::SqliteTasksError;
use chrono::{DateTime, Utc};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_model_type::TaskModelType;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
use tokens::tokens::sqlite::tasks::TaskId;
pub struct TaskList {
pub tasks: Vec<TaskItem>,
}
pub struct TaskItem {
pub id: TaskId,
pub status: TaskStatus,
pub task_type: TaskType,
pub model_type: Option<TaskModelType>,
pub provider: Option<GenerationProvider>,
pub provider_job_id: Option<String>,
pub frontend_caller: Option<TauriCommandCaller>,
pub frontend_subscriber_id: Option<String>,
pub frontend_subscriber_payload: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
pub async fn list_tasks_for_frontend(
db: & TaskDbConnection
) -> Result<TaskList, SqliteTasksError> {
let query = sqlx::query_as!(
TaskItemRaw,
r#"
SELECT
id,
task_status,
task_type,
model_type,
provider,
provider_job_id,
frontend_caller,
frontend_subscriber_id,
frontend_subscriber_payload,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
completed_at as "completed_at: DateTime<Utc>"
FROM tasks
WHERE is_dismissed_by_user == 0
"#);
let result = query.fetch_all(db.get_pool())
.await?;
let mut tasks = Vec::with_capacity(result.len());
for raw in result.into_iter() {
tasks.push(TaskItem {
id: TaskId::new(raw.id),
status: TaskStatus::from_str(&raw.task_status)?,
task_type: TaskType::from_str(&raw.task_type)?,
model_type: raw.model_type
.map(|model| TaskModelType::from_str(&model))
.transpose()?,
provider: raw.provider
.map(|provider| GenerationProvider::from_str(&provider))
.transpose()?,
provider_job_id: raw.provider_job_id,
frontend_caller: raw.frontend_caller
.map(|caller| TauriCommandCaller::from_str(&caller))
.transpose()?,
frontend_subscriber_id: raw.frontend_subscriber_id,
frontend_subscriber_payload: raw.frontend_subscriber_payload,
created_at: raw.created_at,
updated_at: raw.updated_at,
completed_at: raw.completed_at,
})
}
Ok(TaskList {
tasks,
})
}
struct TaskItemRaw {
id: String,
task_status: String,
task_type: String,
model_type: Option<String>,
provider: Option<String>,
provider_job_id: Option<String>,
frontend_caller: Option<String>,
frontend_subscriber_id: Option<String>,
frontend_subscriber_payload: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
completed_at: Option<DateTime<Utc>>,
}
@@ -0,0 +1,27 @@
use crate::connection::TaskDbConnection;
use crate::error::SqliteTasksError;
use tokens::tokens::sqlite::tasks::TaskId;
/// Returns true if rows were updated.
pub async fn mark_task_as_dismissed(
db: &TaskDbConnection,
task_id: &TaskId,
) -> Result<bool, SqliteTasksError> {
// TODO(bt,2025-07-12): Fix this. The sqlx mysql queries never required temporaries
let task_id_temp = task_id.as_str();
// TODO(bt,2025-07-15): We can't set a LIMIT without a certain compiler flag for SQLite ?
let query = sqlx::query!(r#"
UPDATE tasks
SET is_dismissed_by_user = 1
WHERE id = ?
"#,
task_id_temp,
);
let res = query.execute(db.get_pool()).await?;
let rows_updated = res.rows_affected() > 0;
Ok(rows_updated)
}
@@ -1,4 +1,6 @@
pub mod create_task;
pub mod list_tasks_by_provider_and_status;
pub mod list_tasks_by_provider_and_tokens;
pub mod list_tasks_for_frontend;
pub mod mark_task_as_dismissed;
pub mod update_task_status;
@@ -1,5 +1,6 @@
use std::collections::BTreeSet;
use crate::error::enum_error::EnumError;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
@@ -22,6 +23,9 @@ impl_enum_display_and_debug_using_to_str!(GenerationProvider);
impl_mysql_enum_coders!(GenerationProvider);
impl_mysql_from_row!(GenerationProvider);
// For Tauri
impl_sqlite_enum_coders!(GenerationProvider);
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
impl GenerationProvider {
@@ -34,13 +38,13 @@ impl GenerationProvider {
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
pub fn from_str(value: &str) -> Result<Self, EnumError> {
match value {
"artcraft" => Ok(Self::Artcraft),
"fal" => Ok(Self::Fal),
"midjourney" => Ok(Self::Midjourney),
"sora" => Ok(Self::Sora),
_ => Err(format!("invalid generation_provider: {:?}", job_status)),
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
}
}
@@ -58,8 +62,9 @@ impl GenerationProvider {
#[cfg(test)]
mod tests {
use crate::test_helpers::assert_serialization;
use crate::common::generation_provider::GenerationProvider;
use crate::error::enum_error::EnumError;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
@@ -88,6 +93,17 @@ mod tests {
assert_eq!(GenerationProvider::from_str("sora").unwrap(), GenerationProvider::Sora);
}
#[test]
fn from_str_err() {
let result = GenerationProvider::from_str("asdf");
assert!(result.is_err());
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
assert_eq!(value, "asdf");
} else {
panic!("Expected EnumError::CouldNotConvertFromString");
}
}
#[test]
fn all_variants() {
let mut variants = GenerationProvider::all_variants();
@@ -0,0 +1,18 @@
use std::error::Error;
#[derive(Debug)]
pub enum EnumError {
CouldNotConvertFromString(String),
}
impl Error for EnumError {}
impl std::fmt::Display for EnumError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EnumError::CouldNotConvertFromString(value) => {
write!(f, "Could not convert from string: {}", value)
}
}
}
}
@@ -0,0 +1 @@
pub mod enum_error;
+1
View File
@@ -33,6 +33,7 @@ mod macros;
pub mod by_table;
pub mod common;
pub mod error;
pub mod no_table;
pub mod tauri;
pub mod traits;
@@ -1,5 +1,6 @@
use std::collections::BTreeSet;
use crate::error::enum_error::EnumError;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
@@ -98,8 +99,8 @@ impl TaskModelType {
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
pub fn from_str(value: &str) -> Result<Self, EnumError> {
match value {
// Image models
"flux_1_dev" => Ok(Self::Flux1Dev),
"flux_1_schnell" => Ok(Self::Flux1Schnell),
@@ -124,7 +125,7 @@ impl TaskModelType {
// 3D Object generation models
"hunyuan_3d_2.0" => Ok(Self::Hunyuan3d2_0),
"hunyuan_3d_2.1" => Ok(Self::Hunyuan3d2_1),
_ => Err(format!("invalid task_model_type: {:?}", job_status)),
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
}
}
@@ -164,6 +165,7 @@ impl TaskModelType {
mod tests {
use crate::tauri::tasks::task_model_type::TaskModelType;
use crate::test_helpers::assert_serialization;
use crate::error::enum_error::EnumError;
mod explicit_checks {
use super::*;
@@ -252,6 +254,17 @@ mod tests {
assert_eq!(TaskModelType::from_str("hunyuan_3d_2.1").unwrap(), TaskModelType::Hunyuan3d2_1);
}
#[test]
fn from_str_err() {
let result = TaskModelType::from_str("asdf");
assert!(result.is_err());
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
assert_eq!(value, "asdf");
} else {
panic!("Expected EnumError::CouldNotConvertFromString");
}
}
#[test]
fn all_variants() {
let mut variants = TaskModelType::all_variants();
@@ -1,5 +1,6 @@
use std::collections::BTreeSet;
use crate::error::enum_error::EnumError;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
@@ -48,8 +49,8 @@ impl TaskStatus {
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
pub fn from_str(value: &str) -> Result<Self, EnumError> {
match value {
"pending" => Ok(Self::Pending),
"started" => Ok(Self::Started),
"complete_success" => Ok(Self::CompleteSuccess),
@@ -59,7 +60,7 @@ impl TaskStatus {
"cancelled_by_user" => Ok(Self::CancelledByUser),
"cancelled_by_provider" => Ok(Self::CancelledByProvider),
"cancelled_by_us" => Ok(Self::CancelledByUs),
_ => Err(format!("invalid task_status: {:?}", job_status)),
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
}
}
@@ -82,11 +83,12 @@ impl TaskStatus {
#[cfg(test)]
mod tests {
use crate::test_helpers::assert_serialization;
use crate::tauri::tasks::task_status::TaskStatus;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
use crate::error::enum_error::EnumError;
#[test]
fn test_default() {
@@ -132,6 +134,17 @@ mod tests {
assert_eq!(TaskStatus::from_str("cancelled_by_us").unwrap(), TaskStatus::CancelledByUs);
}
#[test]
fn from_str_err() {
let result = TaskStatus::from_str("asdf");
assert!(result.is_err());
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
assert_eq!(value, "asdf");
} else {
panic!("Expected EnumError::CouldNotConvertFromString");
}
}
#[test]
fn all_variants() {
let mut variants = TaskStatus::all_variants();
@@ -1,5 +1,6 @@
use std::collections::BTreeSet;
use crate::error::enum_error::EnumError;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
@@ -34,14 +35,14 @@ impl TaskType {
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
pub fn from_str(value: &str) -> Result<Self, EnumError> {
match value {
"image_generation" => Ok(Self::ImageGeneration),
"image_inpaint_edit" => Ok(Self::ImageInpaintEdit),
"video_generation" => Ok(Self::VideoGeneration),
"object_generation" => Ok(Self::ObjectGeneration),
"background_removal" => Ok(Self::BackgroundRemoval),
_ => Err(format!("invalid task_type: {:?}", job_status)),
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
}
}
@@ -60,11 +61,12 @@ impl TaskType {
#[cfg(test)]
mod tests {
use crate::test_helpers::assert_serialization;
use crate::tauri::tasks::task_type::TaskType;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
use crate::error::enum_error::EnumError;
#[test]
fn test_serialization() {
@@ -92,6 +94,17 @@ mod tests {
assert_eq!(TaskType::from_str("object_generation").unwrap(), TaskType::ObjectGeneration);
assert_eq!(TaskType::from_str("background_removal").unwrap(), TaskType::BackgroundRemoval);
}
#[test]
fn from_str_err() {
let result = TaskType::from_str("asdf");
assert!(result.is_err());
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
assert_eq!(value, "asdf");
} else {
panic!("Expected EnumError::CouldNotConvertFromString");
}
}
#[test]
fn all_variants() {
@@ -1,5 +1,6 @@
use std::collections::BTreeSet;
use crate::error::enum_error::EnumError;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
@@ -38,13 +39,13 @@ impl TauriCommandCaller {
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
pub fn from_str(value: &str) -> Result<Self, EnumError> {
match value {
"canvas" => Ok(Self::Canvas),
"image_editor" => Ok(Self::ImageEditor),
"text_to_image" => Ok(Self::TextToImage),
"image_to_video" => Ok(Self::ImageToVideo),
_ => Err(format!("invalid tauri_command_caller: {:?}", job_status)),
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
}
}
@@ -62,8 +63,9 @@ impl TauriCommandCaller {
#[cfg(test)]
mod tests {
use crate::test_helpers::assert_serialization;
use crate::error::enum_error::EnumError;
use crate::tauri::ux::tauri_command_caller::TauriCommandCaller;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
@@ -92,6 +94,17 @@ mod tests {
assert_eq!(TauriCommandCaller::from_str("image_to_video").unwrap(), TauriCommandCaller::ImageToVideo);
}
#[test]
fn from_str_err() {
let result = TauriCommandCaller::from_str("asdf");
assert!(result.is_err());
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
assert_eq!(value, "asdf");
} else {
panic!("Expected EnumError::CouldNotConvertFromString");
}
}
#[test]
fn all_variants() {
let mut variants = TauriCommandCaller::all_variants();
+3
View File
@@ -156,6 +156,9 @@
},
{
"path": "../../libs/api"
},
{
"path": "../../libs/api-enums"
}
]
}
+50
View File
@@ -0,0 +1,50 @@
{
"extends": [
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.json"
],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}",
"{projectRoot}/vite.config.{js,ts,mjs,mts}"
]
}
]
}
}
]
}
+11
View File
@@ -0,0 +1,11 @@
# api-enums
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build api-enums` to build the library.
## Running unit tests
Run `nx test api-enums` to execute the unit tests via [Vitest](https://vitest.dev/).
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@storyteller/api-enums",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"dependencies": {}
}
+1
View File
@@ -0,0 +1 @@
export * from './lib/index.js';
@@ -0,0 +1,10 @@
// NOTE: These are defined in Rust (as the source of truth) and duplicated in the frontend.
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
export enum GenerationProvider {
Artcraft = "artcraft",
Fal = "fal",
Midjourney = "midjourney",
Sora = "sora",
}
@@ -0,0 +1 @@
export * from "./generation_provider.js";
+3
View File
@@ -0,0 +1,3 @@
export * from "./common/index.js";
export * from "./tauri/index.js";
@@ -0,0 +1 @@
export * from "./tasks/index.js";
@@ -0,0 +1,3 @@
export * from "./task_model_type.js";
export * from "./task_status.js";
export * from "./task_type.js";
@@ -0,0 +1,33 @@
// NOTE: These are defined in Rust (as the source of truth) and duplicated in the frontend.
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
export enum TaskModelType {
// Image models
Flux1Dev = "flux_1_dev",
Flux1Schnell = "flux_1_schnell",
FluxDevJuggernaut = "flux_dev_juggernaut",
FluxPro1 = "flux_pro_1",
FluxPro11 = "flux_pro_1.1",
FluxPro11Ultra = "flux_pro_1.1_ultra",
FluxProKontextMax = "flux_pro_kontext_max",
Gemini25Flash = "gemini_25_flash",
GptImage1 = "gpt_image_1",
Recraft3 = "recraft_3",
// Generic Midjourney model, version unknown.
Midjourney = "midjourney",
// Video models
Kling16Pro = "kling_1.6_pro",
Kling21Pro = "kling_2.1_pro",
Kling21Master = "kling_2.1_master",
Seedance10Lite = "seedance_1.0_lite",
Sora2 = "sora_2",
Veo2 = "veo_2",
Veo3 = "veo_3",
Veo3Fast = "veo_3_fast",
// 3D Object generation models
Hunyuan3d2_0 = "hunyuan_3d_2.0",
Hunyuan3d2_1 = "hunyuan_3d_2.1",
}
@@ -0,0 +1,14 @@
// NOTE: These are defined in Rust (as the source of truth) and duplicated in the frontend.
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
export enum TaskStatus {
Pending = "pending",
Started = "started",
CompleteSuccess = "complete_success",
CompleteFailure = "complete_failure",
AttemptFailed = "attempt_failed",
Dead = "dead",
CancelledByUser = "cancelled_by_user",
CancelledByProvider = "cancelled_by_provider",
CancelledByUs = "cancelled_by_us",
}
@@ -0,0 +1,10 @@
// NOTE: These are defined in Rust (as the source of truth) and duplicated in the frontend.
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
export enum TaskType {
ImageGeneration = "image_generation",
ImageInpaintEdit = "image_inpaint_edit",
VideoGeneration = "video_generation",
ObjectGeneration = "object_generation",
BackgroundRemoval = "background_removal",
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+35
View File
@@ -0,0 +1,35 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"emitDeclarationOnly": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"forceConsistentCasingInFileNames": true,
"types": [
"node",
"vite/client"
]
},
"include": [
"src/**/*.ts"
],
"references": [],
"exclude": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
]
}
@@ -0,0 +1,36 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"module": "nodenext",
"moduleResolution": "nodenext",
"forceConsistentCasingInFileNames": true
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+48
View File
@@ -0,0 +1,48 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/api-enums',
plugins: [dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/api-enums',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: []
},
},
test: {
watch: false,
globals: true,
environment: 'node',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+3
View File
@@ -6,6 +6,9 @@
{
"path": "../tauri-utils"
},
{
"path": "../api-enums"
},
{
"path": "./tsconfig.lib.json"
},
+3
View File
@@ -18,6 +18,9 @@
"references": [
{
"path": "../tauri-utils/tsconfig.lib.json"
},
{
"path": "../api-enums/tsconfig.lib.json"
}
],
"exclude": [
+1
View File
@@ -20,6 +20,7 @@ export * from "./lib/openai/GetOpenAIApiKey.js";
export * from "./lib/openai/SetOpenAIApiKey.js";
export * from "./lib/providers/GetProviderOrder.js";
export * from "./lib/providers/Provider.js";
export * from "./lib/artcraft/task_queue/GetTaskQueue.js";
export * from "./lib/providers/SetProviderOrder.js";
export * from "./lib/settings/GetAppPreferences.js";
export * from "./lib/settings/UpdateAppPreference.js";
@@ -0,0 +1,34 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../../common/CommandStatus";
import { TaskStatus, TaskType, TaskModelType, GenerationProvider } from "@storyteller/api-enums";
interface GetTaskQueueResponse {
tasks: TaskQueueItem[],
}
export interface TaskQueueItem {
id: string,
task_status: TaskStatus,
task_type: TaskType,
model_type?: TaskModelType,
provider?: GenerationProvider,
provider_job_id?: string,
created_at: Date,
updated_at: Date,
completed_at?: Date,
}
interface GetTaskQueueSuccess extends CommandResult {
payload: GetTaskQueueResponse;
}
export const GetTaskQueue = async () : Promise<GetTaskQueueResponse> => {
try {
let result = await invoke("get_task_queue_command") as GetTaskQueueSuccess;
return result.payload;
} catch (error) {
throw error;
}
}
(window as any).test_get_task_queue = GetTaskQueue;
@@ -0,0 +1,22 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../../common/CommandStatus";
interface MarkTaskAsDismissedSuccess extends CommandResult {
payload: void;
}
export const MarkTaskAsDismissed = async (task_id: string) : Promise<MarkTaskAsDismissedSuccess> => {
try {
let result = await invoke("mark_task_as_dismissed_command", {
request: {
task: task_id
}
}) as MarkTaskAsDismissedSuccess;
return result;
} catch (error) {
throw error;
}
}
// Temporary just for testing -
(window as any).test_mark_task_as_dismissed = MarkTaskAsDismissed;
+3
View File
@@ -10,6 +10,9 @@
},
{
"path": "../model-list"
},
{
"path": "../api-enums"
}
],
"extends": "../../tsconfig.base.json"
@@ -48,6 +48,9 @@
"references": [
{
"path": "../model-list/tsconfig.lib.json"
},
{
"path": "../api-enums/tsconfig.lib.json"
}
]
}
+345 -166
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -170,6 +170,9 @@
},
{
"path": "./libs/components/tutorial-modal"
},
{
"path": "./libs/api-enums"
}
]
}