Compare commits
5 Commits
d1ade2c3c3
...
18917b6de1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18917b6de1 | ||
|
|
aab9f0dbf1 | ||
|
|
4571d4d042 | ||
|
|
c6bb72682b | ||
|
|
13523762aa |
@ -23,20 +23,28 @@ pub struct BillingRecord {
|
||||
pub output_tokens: i64,
|
||||
}
|
||||
|
||||
/// Record AI usage for a project.
|
||||
/// Extended result that includes insufficient balance flag for system message creation.
|
||||
#[derive(Debug)]
|
||||
pub enum BillingResult {
|
||||
Success(BillingRecord),
|
||||
InsufficientBalance { message: String },
|
||||
}
|
||||
|
||||
/// Record AI usage for a project with cascading billing.
|
||||
///
|
||||
/// If the project belongs to a workspace, the cost is deducted from the
|
||||
/// workspace's shared quota. Otherwise it is deducted from the project's own
|
||||
/// billing balance.
|
||||
/// Billing strategy:
|
||||
/// 1. Try to deduct from project balance first
|
||||
/// 2. If insufficient, fallback to workspace balance (if project belongs to workspace)
|
||||
/// 3. If both insufficient or no workspace, return InsufficientBalance error with room_id
|
||||
///
|
||||
/// Returns an error if there is insufficient balance.
|
||||
/// Returns BillingError::InsufficientBalance with room_id for system message creation.
|
||||
pub async fn record_ai_usage(
|
||||
db: &AppDatabase,
|
||||
project_uid: Uuid,
|
||||
model_id: Uuid,
|
||||
input_tokens: i64,
|
||||
output_tokens: i64,
|
||||
) -> Result<BillingRecord, AgentError> {
|
||||
) -> Result<BillingResult, AgentError> {
|
||||
// 1. Look up the active price for this model.
|
||||
let pricing = model_pricing::Entity::find()
|
||||
.filter(model_pricing::Column::ModelVersionId.eq(model_id))
|
||||
@ -68,106 +76,27 @@ pub async fn record_ai_usage(
|
||||
|
||||
let currency = pricing.currency.clone();
|
||||
|
||||
// 3. Determine whether to bill the project or its workspace.
|
||||
// 3. Cascading billing: project balance first, then workspace if insufficient.
|
||||
let proj = project::Entity::find_by_id(project_uid)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| AgentError::Internal("Project not found".into()))?;
|
||||
|
||||
if let Some(workspace_id) = proj.workspace_id {
|
||||
// ── Workspace-shared quota ──────────────────────────────────
|
||||
let txn = db.begin().await?;
|
||||
let txn = db.begin().await?;
|
||||
|
||||
// SELECT FOR UPDATE to prevent race conditions
|
||||
let current = workspace_billing::Entity::find_by_id(workspace_id)
|
||||
.lock_exclusive()
|
||||
.one(&txn)
|
||||
.await?
|
||||
.ok_or_else(|| AgentError::Internal("Workspace billing account not found".into()))?;
|
||||
// Always check project balance first
|
||||
let project_billing = project_billing::Entity::find_by_id(project_uid)
|
||||
.lock_exclusive()
|
||||
.one(&txn)
|
||||
.await?
|
||||
.ok_or_else(|| AgentError::Internal("Project billing account not found".into()))?;
|
||||
|
||||
// Validate balance before any modifications
|
||||
if current.balance < total_cost {
|
||||
txn.rollback().await?;
|
||||
return Err(AgentError::Internal(format!(
|
||||
"Insufficient workspace billing balance. Required: {:.4} {}, Available: {:.4} {}",
|
||||
total_cost, currency, current.balance, currency
|
||||
)));
|
||||
}
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
if project_billing.balance >= total_cost {
|
||||
// ── Project has sufficient balance ──────────────────────────
|
||||
let amount_dec = -total_cost;
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// Insert workspace billing history AFTER validation
|
||||
workspace_billing_history::ActiveModel {
|
||||
uid: Set(Uuid::new_v4()),
|
||||
workspace_id: Set(workspace_id),
|
||||
user_id: Set(Some(proj.created_by)),
|
||||
amount: Set(amount_dec),
|
||||
currency: Set(currency.clone()),
|
||||
reason: Set(format!("ai_usage:{}", project_uid)),
|
||||
extra: Set(Some(serde_json::json!({
|
||||
"project_id": project_uid.to_string(),
|
||||
"model_id": model_id.to_string(),
|
||||
"input_tokens": input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
}))),
|
||||
created_at: Set(now),
|
||||
}
|
||||
.insert(&txn)
|
||||
.await?;
|
||||
|
||||
// Deduct from workspace balance
|
||||
let new_balance = current.balance - total_cost;
|
||||
let mut updated: workspace_billing::ActiveModel = current.into();
|
||||
updated.balance = Set(new_balance);
|
||||
updated.updated_at = Set(now);
|
||||
updated.update(&txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
let cost_f64 = total_cost.to_string().parse().unwrap_or(0.0);
|
||||
|
||||
tracing::info!(
|
||||
project_id = %project_uid,
|
||||
model_id = %model_id,
|
||||
input_tokens = input_tokens,
|
||||
output_tokens = output_tokens,
|
||||
cost = %cost_f64,
|
||||
currency = %currency,
|
||||
workspace_id = %workspace_id.to_string(),
|
||||
"ai_usage_recorded"
|
||||
);
|
||||
|
||||
Ok(BillingRecord {
|
||||
cost: cost_f64,
|
||||
currency,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
})
|
||||
} else {
|
||||
// ── Project-owned quota ─────────────────────────────────────
|
||||
let txn = db.begin().await?;
|
||||
|
||||
// SELECT FOR UPDATE to prevent race conditions
|
||||
let current = project_billing::Entity::find_by_id(project_uid)
|
||||
.lock_exclusive()
|
||||
.one(&txn)
|
||||
.await?
|
||||
.ok_or_else(|| AgentError::Internal("Project billing account not found".into()))?;
|
||||
|
||||
// Validate balance before any modifications
|
||||
if current.balance < total_cost {
|
||||
txn.rollback().await?;
|
||||
return Err(AgentError::Internal(format!(
|
||||
"Insufficient billing balance. Required: {:.4} {}, Available: {:.4} {}",
|
||||
total_cost, currency, current.balance, currency
|
||||
)));
|
||||
}
|
||||
|
||||
let amount_dec = -total_cost;
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// Insert project billing history AFTER validation
|
||||
project_billing_history::ActiveModel {
|
||||
uid: Set(Uuid::new_v4()),
|
||||
project: Set(project_uid),
|
||||
@ -186,9 +115,8 @@ pub async fn record_ai_usage(
|
||||
.insert(&txn)
|
||||
.await?;
|
||||
|
||||
// Deduct from project balance
|
||||
let new_balance = current.balance - total_cost;
|
||||
let mut updated: project_billing::ActiveModel = current.into();
|
||||
let new_balance = project_billing.balance - total_cost;
|
||||
let mut updated: project_billing::ActiveModel = project_billing.into();
|
||||
updated.balance = Set(new_balance);
|
||||
updated.update(&txn).await?;
|
||||
|
||||
@ -203,14 +131,93 @@ pub async fn record_ai_usage(
|
||||
output_tokens = output_tokens,
|
||||
cost = %cost_f64,
|
||||
currency = %currency,
|
||||
source = "project",
|
||||
"ai_usage_recorded"
|
||||
);
|
||||
|
||||
Ok(BillingRecord {
|
||||
Ok(BillingResult::Success(BillingRecord {
|
||||
cost: cost_f64,
|
||||
currency,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
}))
|
||||
} else if let Some(workspace_id) = proj.workspace_id {
|
||||
// ── Project insufficient, fallback to workspace ─────────────
|
||||
let workspace_billing = workspace_billing::Entity::find_by_id(workspace_id)
|
||||
.lock_exclusive()
|
||||
.one(&txn)
|
||||
.await?
|
||||
.ok_or_else(|| AgentError::Internal("Workspace billing account not found".into()))?;
|
||||
|
||||
if workspace_billing.balance < total_cost {
|
||||
txn.rollback().await?;
|
||||
return Ok(BillingResult::InsufficientBalance {
|
||||
message: format!(
|
||||
"Insufficient balance. Project: {:.4} {}, Workspace: {:.4} {}, Required: {:.4} {}",
|
||||
project_billing.balance, currency,
|
||||
workspace_billing.balance, currency,
|
||||
total_cost, currency
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let amount_dec = -total_cost;
|
||||
|
||||
workspace_billing_history::ActiveModel {
|
||||
uid: Set(Uuid::new_v4()),
|
||||
workspace_id: Set(workspace_id),
|
||||
user_id: Set(Some(proj.created_by)),
|
||||
amount: Set(amount_dec),
|
||||
currency: Set(currency.clone()),
|
||||
reason: Set(format!("ai_usage:{}", project_uid)),
|
||||
extra: Set(Some(serde_json::json!({
|
||||
"project_id": project_uid.to_string(),
|
||||
"model_id": model_id.to_string(),
|
||||
"input_tokens": input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
"fallback_reason": "project_balance_insufficient"
|
||||
}))),
|
||||
created_at: Set(now),
|
||||
}
|
||||
.insert(&txn)
|
||||
.await?;
|
||||
|
||||
let new_balance = workspace_billing.balance - total_cost;
|
||||
let mut updated: workspace_billing::ActiveModel = workspace_billing.into();
|
||||
updated.balance = Set(new_balance);
|
||||
updated.updated_at = Set(now);
|
||||
updated.update(&txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
let cost_f64 = total_cost.to_string().parse().unwrap_or(0.0);
|
||||
|
||||
tracing::info!(
|
||||
project_id = %project_uid,
|
||||
model_id = %model_id,
|
||||
input_tokens = input_tokens,
|
||||
output_tokens = output_tokens,
|
||||
cost = %cost_f64,
|
||||
currency = %currency,
|
||||
workspace_id = %workspace_id.to_string(),
|
||||
source = "workspace_fallback",
|
||||
"ai_usage_recorded"
|
||||
);
|
||||
|
||||
Ok(BillingResult::Success(BillingRecord {
|
||||
cost: cost_f64,
|
||||
currency,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
}))
|
||||
} else {
|
||||
// ── Project insufficient and no workspace ───────────────────
|
||||
txn.rollback().await?;
|
||||
Ok(BillingResult::InsufficientBalance {
|
||||
message: format!(
|
||||
"Insufficient balance. Required: {:.4} {}, Available: {:.4} {}",
|
||||
total_cost, currency, project_billing.balance, currency
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,17 +58,24 @@ async fn record_ai_session(
|
||||
output_tokens: i64,
|
||||
latency_ms: i64,
|
||||
) {
|
||||
let (cost, currency) = match billing::record_ai_usage(
|
||||
let (cost, currency, error_msg) = match billing::record_ai_usage(
|
||||
db,
|
||||
project_id,
|
||||
model_id,
|
||||
version_id,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(record) => (Some(record.cost), Some(record.currency)),
|
||||
Err(_) => (None, None),
|
||||
Ok(billing::BillingResult::Success(record)) => {
|
||||
(Some(record.cost), Some(record.currency), None)
|
||||
}
|
||||
Ok(billing::BillingResult::InsufficientBalance { message }) => {
|
||||
// Create system message for insufficient balance
|
||||
create_system_message(db, room_id, &message).await;
|
||||
(None, None, Some(message))
|
||||
}
|
||||
Err(_) => (None, None, None),
|
||||
};
|
||||
|
||||
let _ = models::ai::ai_session::ActiveModel {
|
||||
@ -81,7 +88,7 @@ async fn record_ai_session(
|
||||
latency_ms: Set(Some(latency_ms)),
|
||||
cost: Set(cost),
|
||||
currency: Set(currency),
|
||||
error_message: Set(None),
|
||||
error_message: Set(error_msg),
|
||||
error_code: Set(None),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
}
|
||||
@ -89,6 +96,71 @@ async fn record_ai_session(
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Create a system message in the room for billing errors.
|
||||
async fn create_system_message(
|
||||
db: &db::database::AppDatabase,
|
||||
room_id: Uuid,
|
||||
message: &str,
|
||||
) {
|
||||
use models::rooms::{room_message, MessageSenderType, MessageContentType};
|
||||
use sea_orm::Set;
|
||||
|
||||
// Get next sequence number - we don't have cache here, so we query directly
|
||||
let last_seq = match room_message::Entity::find()
|
||||
.filter(room_message::Column::Room.eq(room_id))
|
||||
.order_by_desc(room_message::Column::Seq)
|
||||
.one(db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(m)) => m.seq,
|
||||
Ok(None) => 0,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Failed to get last seq for system message");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let seq = last_seq + 1;
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let result = room_message::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
seq: Set(seq),
|
||||
room: Set(room_id),
|
||||
sender_type: Set(MessageSenderType::System),
|
||||
sender_id: Set(None),
|
||||
model_id: Set(None),
|
||||
thread: Set(None),
|
||||
in_reply_to: Set(None),
|
||||
content: Set(message.to_string()),
|
||||
content_type: Set(MessageContentType::Text),
|
||||
thinking_content: Set(None),
|
||||
edited_at: Set(None),
|
||||
send_at: Set(now),
|
||||
revoked: Set(None),
|
||||
revoked_by: Set(None),
|
||||
}
|
||||
.insert(db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
tracing::info!(
|
||||
room_id = %room_id,
|
||||
message = %message,
|
||||
"system_message_created_for_billing_error"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
room_id = %room_id,
|
||||
"Failed to create system message for billing error"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Service for handling AI chat requests in rooms.
|
||||
pub struct ChatService {
|
||||
ai_base_url: Option<String>,
|
||||
@ -614,7 +686,26 @@ impl ChatService {
|
||||
for call in &calls {
|
||||
let start = std::time::Instant::now();
|
||||
let executor = crate::tool::ToolExecutor::new();
|
||||
let results = match executor.execute_batch(vec![call.clone()], &mut ctx).await {
|
||||
|
||||
// Use select! loop to send heartbeat chunks at 30s intervals
|
||||
// during long tool execution, resetting the frontend streaming timer.
|
||||
let fut = executor.execute_batch(vec![call.clone()], &mut ctx);
|
||||
tokio::pin!(fut);
|
||||
|
||||
let results = loop {
|
||||
tokio::select! {
|
||||
result = fut.as_mut() => break result,
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(30)) => {
|
||||
on_chunk(AiStreamChunk {
|
||||
content: String::new(),
|
||||
done: false,
|
||||
chunk_type: AiChunkType::ToolCall,
|
||||
}).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let results = match results {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
let elapsed = start.elapsed().as_millis() as i64;
|
||||
|
||||
@ -760,9 +760,9 @@ async fn call_stream_once(
|
||||
})
|
||||
};
|
||||
|
||||
// 60s timeout for the entire stream
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(60), stream_fut).await {
|
||||
// 120s timeout for the entire stream
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(120), stream_fut).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(AgentError::Timeout { task_id: 0, seconds: 60 }),
|
||||
Err(_) => Err(AgentError::Timeout { task_id: 0, seconds: 120 }),
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ pub mod sync;
|
||||
pub mod task;
|
||||
pub mod tokent;
|
||||
pub mod tool;
|
||||
pub use billing::{BillingRecord, record_ai_usage};
|
||||
pub use billing::{BillingRecord, BillingResult, record_ai_usage};
|
||||
pub use sync::list_accessible_models;
|
||||
pub use task::TaskService;
|
||||
pub use tokent::{TokenUsage, resolve_usage};
|
||||
|
||||
@ -16,8 +16,8 @@ async fn git_blob_info_exec(
|
||||
let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let commit_oid = resolve_oid(&domain, oid)?;
|
||||
let info = domain.blob_get(&commit_oid).map_err(|e| e.to_string())?;
|
||||
let blob_oid = git::commit::types::CommitOid::new(oid);
|
||||
let info = domain.blob_get(&blob_oid).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"oid": info.oid.to_string(),
|
||||
@ -37,10 +37,10 @@ async fn git_blob_exists_exec(
|
||||
let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let commit_oid = resolve_oid(&domain, oid)?;
|
||||
let exists = domain.blob_exists(&commit_oid);
|
||||
let blob_oid = git::commit::types::CommitOid::new(oid);
|
||||
let exists = domain.blob_exists(&blob_oid);
|
||||
|
||||
Ok(serde_json::json!({ "oid": commit_oid.to_string(), "exists": exists }))
|
||||
Ok(serde_json::json!({ "oid": blob_oid.to_string(), "exists": exists }))
|
||||
}
|
||||
|
||||
async fn git_blob_content_exec(
|
||||
@ -55,8 +55,8 @@ async fn git_blob_content_exec(
|
||||
let max_size = p.get("max_size").and_then(|v| v.as_u64()).unwrap_or(1_048_576) as usize; // 1MB default
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let commit_oid = resolve_oid(&domain, oid)?;
|
||||
let blob = domain.blob_content(&commit_oid).map_err(|e| e.to_string())?;
|
||||
let blob_oid = git::commit::types::CommitOid::new(oid);
|
||||
let blob = domain.blob_content(&blob_oid).map_err(|e| e.to_string())?;
|
||||
|
||||
if blob.size > max_size {
|
||||
return Err(format!(
|
||||
@ -109,18 +109,6 @@ async fn git_blob_create_exec(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_oid(
|
||||
domain: &git::GitDomain,
|
||||
rev: &str,
|
||||
) -> Result<git::commit::types::CommitOid, String> {
|
||||
if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
Ok(git::commit::types::CommitOid::new(rev))
|
||||
} else if let Ok(Some(oid)) = domain.ref_target(rev) {
|
||||
Ok(oid)
|
||||
} else {
|
||||
domain.commit_get_prefix(rev).map_err(|e| e.to_string()).map(|m| m.oid)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
// git_blob_info
|
||||
|
||||
@ -47,16 +47,11 @@ async fn git_log_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_
|
||||
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
/// Resolve a rev string to commit metadata. Tries full OID first (exactly 40 hex chars),
|
||||
/// then reference name resolution (branch, tag, HEAD), then hex prefix lookup.
|
||||
/// Resolve a rev string to commit metadata using the full rev-parse machinery
|
||||
/// (branch names, tags, HEAD, hex prefixes, etc.).
|
||||
fn resolve_commit(domain: &git::GitDomain, rev: &str) -> Result<git::commit::types::CommitMeta, String> {
|
||||
if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
domain.commit_get(&git::commit::types::CommitOid::new(rev)).map_err(|e| e.to_string())
|
||||
} else if let Ok(Some(oid)) = domain.ref_target(rev) {
|
||||
domain.commit_get(&oid).map_err(|e| e.to_string())
|
||||
} else {
|
||||
domain.commit_get_prefix(rev).map_err(|e| e.to_string())
|
||||
}
|
||||
let oid = domain.resolve_rev(rev).map_err(|e| e.to_string())?;
|
||||
domain.commit_get(&oid).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn git_show_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
|
||||
@ -5,16 +5,10 @@ use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
||||
use base64::Engine;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Resolve a rev string to a commit OID. Tries full OID first (exactly 40 hex chars),
|
||||
/// then reference name resolution (branch, tag, HEAD), then hex prefix lookup.
|
||||
/// Resolve a rev string to a commit OID using the full rev-parse machinery
|
||||
/// (branch names, tags, HEAD, hex prefixes, etc.).
|
||||
fn resolve_commit_oid(domain: &git::GitDomain, rev: &str) -> Result<git::commit::types::CommitOid, String> {
|
||||
if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
Ok(git::commit::types::CommitOid::new(rev))
|
||||
} else if let Ok(Some(oid)) = domain.ref_target(rev) {
|
||||
Ok(oid)
|
||||
} else {
|
||||
domain.commit_get_prefix(rev).map_err(|e| e.to_string()).map(|m| m.oid)
|
||||
}
|
||||
domain.resolve_rev(rev).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn git_file_content_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
@ -54,14 +48,18 @@ async fn git_tree_ls_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<se
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_else(|| "HEAD".to_string());
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let commit_oid = resolve_commit_oid(&domain, &rev).map_err(|e| e.to_string())?;
|
||||
let commit_oid = resolve_commit_oid(&domain, &rev)?;
|
||||
|
||||
// Get tree OID from commit
|
||||
let commit_meta = domain.commit_get(&commit_oid).map_err(|e| e.to_string())?;
|
||||
let tree_oid = &commit_meta.tree_id;
|
||||
|
||||
let entries = match dir_path {
|
||||
Some(ref dp) => {
|
||||
let entry = domain.tree_entry_by_path(&commit_oid, dp).map_err(|e| e.to_string())?;
|
||||
let entry = domain.tree_entry_by_path(tree_oid, dp).map_err(|e| e.to_string())?;
|
||||
domain.tree_list(&entry.oid).map_err(|e| e.to_string())?
|
||||
}
|
||||
None => domain.tree_list(&commit_oid).map_err(|e| e.to_string())?,
|
||||
None => domain.tree_list(tree_oid).map_err(|e| e.to_string())?,
|
||||
};
|
||||
|
||||
let result: Vec<_> = entries.iter().map(|e| {
|
||||
|
||||
@ -4,6 +4,7 @@ mod m20260420_000003_add_model_id_to_room_message;
|
||||
pub mod m20260421_000001_add_agent_type_to_room_ai;
|
||||
pub mod m20260426_000001_add_thinking_content_to_room_message;
|
||||
pub mod m20260428_000001_backfill_content_tsv;
|
||||
pub mod m20260428_000002_default_use_exact_true;
|
||||
|
||||
pub async fn execute_sql(manager: &SchemaManager<'_>, sql: &str) -> Result<(), DbErr> {
|
||||
for stmt in split_sql_statements(sql) {
|
||||
@ -93,6 +94,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260421_000001_add_agent_type_to_room_ai::Migration),
|
||||
Box::new(m20260426_000001_add_thinking_content_to_room_message::Migration),
|
||||
Box::new(m20260428_000001_backfill_content_tsv::Migration),
|
||||
Box::new(m20260428_000002_default_use_exact_true::Migration),
|
||||
// Repo tables
|
||||
Box::new(m20250628_000028_create_repo::Migration),
|
||||
Box::new(m20250628_000029_create_repo_branch::Migration),
|
||||
|
||||
17
libs/migrate/m20260428_000002_default_use_exact_true.rs
Normal file
17
libs/migrate/m20260428_000002_default_use_exact_true.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let sql = include_str!("sql/m20260428_000002_default_use_exact_true.sql");
|
||||
super::execute_sql(manager, sql).await
|
||||
}
|
||||
|
||||
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// No-op: data migration is non-reversible
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
-- Fix: set use_exact = true for all existing room_ai records
|
||||
-- This changes the default behavior so AI only responds when explicitly @mentioned.
|
||||
-- Previously use_exact defaulted to false, causing AI to reply to every message.
|
||||
UPDATE room_ai SET use_exact = true WHERE use_exact = false;
|
||||
@ -70,7 +70,7 @@ impl RoomService {
|
||||
active.max_tokens = Set(request.max_tokens);
|
||||
}
|
||||
if request.use_exact.is_some() {
|
||||
active.use_exact = Set(request.use_exact.unwrap_or(false));
|
||||
active.use_exact = Set(request.use_exact.unwrap_or(true));
|
||||
}
|
||||
if request.think.is_some() {
|
||||
active.think = Set(request.think.unwrap_or(false));
|
||||
@ -97,7 +97,7 @@ impl RoomService {
|
||||
system_prompt: Set(request.system_prompt),
|
||||
temperature: Set(request.temperature),
|
||||
max_tokens: Set(request.max_tokens),
|
||||
use_exact: Set(request.use_exact.unwrap_or(false)),
|
||||
use_exact: Set(request.use_exact.unwrap_or(true)),
|
||||
think: Set(request.think.unwrap_or(false)),
|
||||
stream: Set(request.stream.unwrap_or(false)),
|
||||
min_score: Set(request.min_score),
|
||||
|
||||
@ -12,13 +12,21 @@ impl AppService {
|
||||
input_tokens: i64,
|
||||
output_tokens: i64,
|
||||
) -> Result<agent::billing::BillingRecord, AppError> {
|
||||
Ok(agent::billing::record_ai_usage(
|
||||
use agent::billing::BillingResult;
|
||||
|
||||
match agent::billing::record_ai_usage(
|
||||
&self.db,
|
||||
project_uid,
|
||||
model_id,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
)
|
||||
.await?)
|
||||
.await?
|
||||
{
|
||||
BillingResult::Success(record) => Ok(record),
|
||||
BillingResult::InsufficientBalance { message } => {
|
||||
Err(AppError::BadRequest(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
204
public/load.html
Normal file
204
public/load.html
Normal file
@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Topo Draw Animation</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
path {
|
||||
fill-opacity: 0;
|
||||
stroke: rgb(0, 0, 0);
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 0;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<svg id="logo" viewBox="0 0 400 400" width="100" height="100" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
desc="Created with imagetracer.js version 1.2.6">
|
||||
<path fill="rgb(249,248,247)" stroke="rgb(249,248,247)" stroke-width="1" opacity="1"
|
||||
d="M 335.5 20 Q 341.3 19.3 342 23.5 L 347 41.5 L 347 63.5 L 344 79.5 L 336 101 Q 333.3 102.1 334 99.5 Q 320.8 70.7 297.5 52 L 287 43.5 Q 285.9 40.2 288.5 41 L 313.5 27 L 335.5 20 Z " />
|
||||
<path fill="rgb(249,248,247)" stroke="rgb(249,248,247)" stroke-width="1" opacity="1"
|
||||
d="M 95.5 21 L 105.5 22 L 115.5 25 L 147 41.5 L 145.5 45 L 130 57 L 110 83.5 L 100.5 102 L 94 88.5 L 90 70.5 Q 91.3 65.8 89 64.5 L 89 38.5 L 92 25.5 L 95.5 21 Z " />
|
||||
<path fill="rgb(249,248,247)" stroke="rgb(249,248,247)" stroke-width="1" opacity="1"
|
||||
d="M 154.5 112 Q 164 110.5 169.5 113 L 183 123.5 L 189 136.5 L 189 147.5 Q 186.4 158.4 178.5 164 L 167.5 169 L 156.5 169 L 150.5 167 L 140.5 159 Q 124.4 174.2 120 200 Q 114.6 199.4 112 194.5 L 100 179.5 L 96 171.5 Q 107.1 144.1 127.5 126 Q 138.8 116.8 154.5 112 Z M 158 123 L 152 126 Q 145 130 143 140 L 143 148 L 147 157 Q 152 162 161 164 Q 168 164 173 161 Q 178 157 181 149 Q 183 135 176 130 Q 171 122 158 123 Z " />
|
||||
<path fill="rgb(249,248,247)" stroke="rgb(249,248,247)" stroke-width="1" opacity="1"
|
||||
d="M 269.5 112 Q 281.6 110.4 288.5 114 L 302.5 121 L 319 136.5 L 332 154.5 L 338 168.5 L 338 172.5 Q 330.1 188.6 317.5 200 L 315.5 201 Q 312.3 185.2 306 172.5 L 294.5 160 L 292.5 159 Q 286.5 170 268.5 169 Q 257.4 166.6 252 158.5 L 247 147.5 L 247 136.5 Q 249.6 125.6 256.5 119 L 269.5 112 Z M 272 123 L 264 126 Q 258 130 255 139 Q 253 151 259 157 Q 263 162 271 164 L 278 164 L 288 158 L 293 147 L 293 140 L 288 130 Q 283 123 272 123 Z " />
|
||||
<path fill="rgb(249,248,247)" stroke="rgb(249,248,247)" stroke-width="1" opacity="1"
|
||||
d="M 154.5 130 Q 160.8 128.8 162 132.5 Q 162.6 137.6 159.5 139 Q 154.3 140.3 153 137.5 L 152 133.5 L 154.5 130 Z " />
|
||||
<path fill="rgb(249,248,247)" stroke="rgb(249,248,247)" stroke-width="1" opacity="1"
|
||||
d="M 266.5 130 Q 272 129 273 132.5 L 272 139 L 268.5 140 L 264 136.5 Q 263 131 266.5 130 Z " />
|
||||
<path fill="rgb(249,248,247)" stroke="rgb(249,248,247)" stroke-width="1" opacity="1"
|
||||
d="M 206.5 154 L 226.5 154 L 235.5 156 L 249.5 162 L 266 175.5 Q 274.8 185.2 279 199.5 L 279 216.5 L 273.5 227 L 254.5 233 L 237.5 235 L 236.5 236 L 197.5 236 L 167.5 230 L 159 225.5 L 156 214.5 L 156 204.5 Q 159.9 184.9 171.5 173 Q 173.8 173.8 173 171.5 Q 185 158 206.5 154 Z M 213 162 L 206 165 L 202 170 Q 201 176 204 179 L 214 184 L 219 184 L 227 181 L 233 174 L 232 168 Q 226 161 213 162 Z M 196 189 L 194 191 L 196 198 Q 203 206 216 208 L 225 207 L 235 202 L 242 194 L 242 190 L 237 191 L 234 197 L 222 203 L 214 203 L 204 199 Q 199 196 198 191 L 196 189 Z " />
|
||||
<path fill="rgb(6,6,6)" stroke="rgb(6,6,6)" stroke-width="1" opacity="0.9372549019607843"
|
||||
d="M 87.5 4 L 97.5 4 L 115.5 8 L 142.5 20 L 161.5 34 L 164.5 34 L 176.5 28 L 190.5 24 L 200.5 22 L 221.5 21 L 222.5 22 L 233.5 22 L 247.5 25 L 272.5 35 Q 288.3 20.8 309.5 12 L 330.5 5 L 346.5 4 L 354 9.5 L 359 19.5 L 362 35.5 L 362 64.5 L 356 91.5 L 342 121.5 L 350 144.5 L 350 159.5 Q 340.5 189 319.5 207 L 303.5 220 L 297 223 L 296 225.5 Q 302.8 234.2 306 246.5 L 310 263.5 L 312 282.5 Q 322.5 292 328 306.5 L 330 312.5 L 331 325.5 L 327 341.5 L 336 349.5 L 342 361.5 L 342 368.5 Q 340 378.5 332.5 383 L 322.5 386 L 297.5 386 L 296.5 385 L 284.5 385 L 273.5 393 Q 266.3 396.3 254.5 395 Q 241.6 392.4 235 383.5 L 233 379 L 204.5 379 L 203.5 378 L 201 379 Q 199.9 385.6 194.5 389 L 180.5 395 L 168.5 395 L 148.5 385 L 147.5 386 L 112.5 386 L 100.5 382 L 96 377.5 Q 90.8 371.7 93 358.5 Q 94.4 351.9 99 348.5 L 99 346 Q 73.7 335 57 314.5 L 43 294.5 L 37 280.5 L 32 261.5 L 32 245.5 Q 33.9 236.4 40.5 232 Q 44.9 227.9 54.5 229 L 66.5 234 L 77.5 242 L 99.5 254 L 121.5 264 L 125 264 L 130 244.5 L 140 226.5 L 140 224 L 134.5 222 L 123.5 214 L 108.5 199 L 107 199 L 95 183.5 L 88 169.5 L 86 162.5 L 86 143.5 L 93 118.5 L 80 92.5 L 75 74.5 L 73 63.5 L 73 36.5 L 78 16.5 Q 80.2 9.2 85.5 5 L 87.5 4 Z M 91 10 L 89 11 L 84 18 L 79 39 L 79 64 L 81 75 L 87 94 L 94 110 L 97 112 L 97 108 L 87 85 L 83 65 L 83 40 Q 86 38 84 33 Q 85 23 90 18 L 94 15 L 106 16 L 129 24 Q 143 29 154 39 Q 157 40 156 38 L 138 24 L 118 15 L 107 13 L 103 11 L 91 10 Z M 338 10 L 329 13 L 321 14 L 311 18 L 281 36 L 279 39 L 284 38 L 311 22 L 327 16 L 338 14 L 344 16 L 349 25 L 353 42 L 353 65 L 351 71 L 351 76 L 346 93 L 338 110 Q 337 114 341 112 L 353 82 L 357 62 L 357 38 L 353 20 L 351 16 Q 349 8 338 10 Z M 337 19 L 317 25 L 288 41 Q 285 40 286 44 L 305 59 Q 323 76 334 101 Q 333 103 336 102 L 342 89 L 346 74 L 347 60 L 348 59 L 347 39 L 342 23 L 337 19 Z M 95 21 L 90 29 L 89 40 L 88 41 L 88 63 L 89 64 L 90 75 L 94 91 L 101 103 Q 111 79 128 61 L 147 44 Q 150 45 148 41 L 118 25 L 104 21 L 95 21 Z M 213 27 L 212 28 L 201 28 L 188 31 L 167 39 L 151 48 L 132 65 Q 133 67 131 66 Q 102 99 91 150 Q 91 163 95 173 L 113 197 Q 131 214 156 226 L 159 226 L 164 229 L 177 233 L 193 236 L 201 236 L 202 237 L 232 237 L 233 236 L 242 236 L 269 230 L 295 218 L 320 199 L 333 183 Q 340 172 344 158 L 344 146 L 333 111 L 326 96 L 306 67 L 286 50 L 268 39 L 243 30 L 233 28 L 213 27 Z M 289 227 L 279 232 L 255 239 L 238 241 L 237 242 L 224 242 L 223 243 L 198 242 L 197 241 L 185 240 L 168 236 L 145 228 L 133 254 L 129 272 L 129 299 Q 132 300 130 306 L 142 348 L 152 350 L 169 349 L 192 341 L 189 320 L 177 311 L 166 298 L 167 296 Q 175 307 188 314 L 208 321 L 226 321 Q 227 319 232 320 L 251 312 L 259 306 L 264 300 L 269 296 L 265 305 L 250 317 L 247 320 L 243 340 L 250 344 L 267 349 L 288 349 L 292 348 L 294 345 L 298 333 L 306 301 L 306 275 L 305 274 L 304 262 Q 300 244 292 230 L 289 227 Z M 47 236 L 45 237 Q 39 241 38 249 L 38 258 L 41 275 L 47 288 Q 46 290 49 289 Q 55 273 66 262 L 78 252 Q 81 253 79 249 L 57 236 L 47 236 Z M 107 265 L 102 269 L 93 279 L 90 280 L 77 302 L 74 311 L 74 324 L 99 340 L 106 342 L 104 334 Q 102 321 105 313 Q 111 296 123 286 Q 122 276 124 271 L 111 265 L 107 265 Z M 313 293 L 311 295 Q 310 317 303 334 L 321 338 L 324 331 Q 326 319 323 312 Q 320 300 313 293 Z M 123 294 Q 114 303 110 318 Q 109 331 111 339 L 124 336 L 131 336 L 132 335 L 126 314 L 124 296 L 123 294 Z M 194 321 L 195 332 L 202 362 L 202 371 L 207 373 L 232 373 L 234 353 L 242 326 L 242 321 L 228 325 L 208 325 L 194 321 Z " />
|
||||
<path fill="rgb(6,6,6)" stroke="rgb(6,6,6)" stroke-width="1" opacity="0.9372549019607843"
|
||||
d="M 157.5 124 Q 171.5 122.5 176 130.5 Q 181 135.5 181 145.5 Q 179.3 156.3 171.5 161 L 165.5 163 L 159.5 163 Q 151 161.5 147 155.5 Q 142.5 150 144 138.5 Q 145.3 131.8 149.5 128 L 157.5 124 Z M 156 129 L 153 130 L 152 133 L 153 139 L 156 140 L 161 139 L 163 135 L 162 132 Q 161 128 156 129 Z " />
|
||||
<path fill="rgb(6,6,6)" stroke="rgb(6,6,6)" stroke-width="1" opacity="0.9372549019607843"
|
||||
d="M 270.5 124 Q 283.2 123.3 288 130.5 Q 293.7 135.8 292 148.5 L 285.5 159 L 278.5 163 L 269.5 163 Q 262.3 161.2 259 155.5 L 256 150.5 L 255 142.5 Q 256.7 131.2 264.5 126 L 270.5 124 Z M 267 129 Q 262 130 263 137 Q 264 141 271 140 Q 275 139 274 133 Q 273 128 267 129 Z " />
|
||||
<path fill="rgb(6,6,6)" stroke="rgb(6,6,6)" stroke-width="1" opacity="0.9372549019607843"
|
||||
d="M 211.5 163 Q 226.5 161 232 168.5 L 233 172.5 L 225.5 181 Q 221.1 184.1 212.5 183 Q 206.8 181.8 204 177.5 L 202 172.5 L 206.5 165 L 211.5 163 Z " />
|
||||
<path fill="rgb(6,6,6)" stroke="rgb(6,6,6)" stroke-width="1" opacity="0.9372549019607843"
|
||||
d="M 195 190 L 198 191.5 L 202.5 199 Q 208.4 204.1 219.5 204 Q 231.5 202 237 193.5 L 238.5 191 L 240.5 190 L 241 193.5 L 233.5 202 L 222.5 207 L 212.5 207 Q 202.4 204.6 197 197.5 L 195 190 Z " />
|
||||
<path fill="rgb(0,0,0)" stroke="rgb(0,0,0)" stroke-width="1" opacity="0"
|
||||
d="M 0 0 L 400 0 L 400 400 L 0 400 L 0 0 Z M 88 4 L 86 5 Q 80 9 78 17 L 73 37 L 73 64 L 75 75 L 80 93 L 93 119 L 86 144 L 86 163 L 88 170 L 95 184 L 107 199 L 109 199 L 124 214 L 135 222 L 140 224 L 140 227 L 130 245 L 125 264 L 122 264 L 100 254 L 78 242 L 67 234 L 55 229 Q 45 228 41 232 Q 34 236 32 246 L 32 262 L 37 281 L 43 295 L 57 315 Q 74 335 99 346 L 99 349 Q 94 352 93 359 Q 91 372 96 378 L 101 382 L 113 386 L 148 386 L 149 385 L 169 395 L 181 395 L 195 389 Q 200 386 201 379 L 204 378 L 205 379 L 233 379 L 235 384 Q 242 392 255 395 Q 266 396 274 393 L 285 385 L 297 385 L 298 386 L 323 386 L 333 383 Q 340 378 342 369 L 342 362 L 336 350 L 327 342 L 331 326 L 330 313 L 328 307 Q 322 292 312 283 L 310 264 L 306 247 Q 303 234 296 226 L 297 223 L 304 220 L 320 207 Q 341 189 350 160 L 350 145 L 342 122 L 356 92 L 362 65 L 362 36 L 359 20 L 354 10 L 347 4 L 331 5 L 310 12 Q 288 21 273 35 L 248 25 L 234 22 L 223 22 L 222 21 L 201 22 L 191 24 L 177 28 L 165 34 L 162 34 L 143 20 L 116 8 L 98 4 L 88 4 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 90.5 10 L 102.5 11 L 106.5 13 L 117.5 15 L 137.5 24 L 156 37.5 Q 156.8 40.1 153.5 39 Q 143.1 29.4 128.5 24 L 105.5 16 L 93.5 15 L 90 17.5 Q 85.5 23.5 84 32.5 Q 85.5 38 83 39.5 L 83 64.5 L 87 84.5 L 97 107.5 L 97 111.5 L 94 109.5 L 87 93.5 L 81 74.5 L 79 63.5 L 79 38.5 L 84 17.5 L 88.5 11 L 90.5 10 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 337.5 10 Q 348.6 8.4 351 15.5 L 353 19.5 L 357 37.5 L 357 61.5 L 353 81.5 L 341 112 Q 336.9 113.7 338 109.5 L 346 92.5 L 351 75.5 L 351 70.5 L 353 64.5 L 353 41.5 L 349 24.5 L 343.5 16 L 337.5 14 L 326.5 16 L 310.5 22 L 283.5 38 L 279 39 L 280.5 36 L 310.5 18 L 320.5 14 L 328.5 13 L 337.5 10 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 212.5 27 L 232.5 28 L 242.5 30 L 267.5 39 L 285.5 50 L 306 67 L 326 95.5 L 333 110.5 L 344 145.5 L 344 157.5 L 338.5 172 Q 335.5 159 329 149.5 L 309.5 127 L 302.5 121 L 288.5 114 Q 281.6 110.4 269.5 112 Q 259.9 114.9 254 121.5 L 247 136.5 L 247 147.5 Q 249.9 159.1 258.5 165 L 268.5 169 L 278.5 169 Q 287.5 166 292.5 159 L 304 169.5 L 310 181.5 L 315.5 201 L 317 200.5 Q 298.4 219.4 270.5 229 L 270.5 228 L 275 225.5 L 279 216.5 L 279 199.5 Q 274.5 184 264.5 174 L 249.5 162 L 235.5 156 L 226.5 154 L 206.5 154 Q 185 158 173 171.5 Q 173.8 173.8 171.5 173 Q 163.3 181.3 159 193.5 L 156 204.5 L 156 214.5 L 160 226.5 L 158.5 226 L 155.5 226 L 140.5 218 L 117 199.5 L 118.5 200 Q 121.9 200.9 120 195.5 L 127 177.5 Q 132.3 166.8 140.5 159 L 150.5 167 Q 156.6 170.4 167.5 169 Q 178.3 166.3 184 158.5 Q 190.7 151.7 189 136.5 Q 186 124.5 177.5 118 Q 171.1 109.9 154.5 112 Q 132.3 118.8 119 134.5 Q 104.1 150.1 96 172.5 L 96.5 174 L 93 166.5 L 91 149.5 Q 101.6 98.6 130.5 66 Q 132.8 66.8 132 64.5 L 150.5 48 L 166.5 39 L 187.5 31 L 200.5 28 L 211.5 28 L 212.5 27 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 297.5 35 L 296.5 37 L 292.5 39 L 293.5 37 L 297.5 35 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 88.5 40 L 89 62.5 L 88 62.5 L 88.5 40 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 286.5 41 L 290 46.5 L 286 43.5 L 286.5 41 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 347.5 46 L 348 58.5 L 347 58.5 L 347.5 46 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 140.5 48 L 136.5 53 L 140.5 48 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 297.5 52 L 303.5 59 L 297.5 52 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 127.5 61 L 121.5 68 L 127.5 61 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 306.5 61 L 314.5 70 L 306.5 61 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 157.5 123 L 166 123.5 L 155.5 125 L 155.5 124 L 157.5 123 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 263.5 126 L 259.5 131 L 263.5 126 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 266.5 129 Q 273 128 274 132.5 Q 275 137 272.5 138 L 273 136.5 Q 274 131 270.5 130 Q 265.5 129 264.5 132 L 266.5 129 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 176.5 131 L 180 136.5 L 179 136.5 L 176.5 131 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 263.5 133 Q 263.3 137.7 266.5 139 L 268 139.5 L 266.5 140 Q 261.6 138.6 263.5 133 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 143.5 139 L 144 147.5 L 143 147.5 L 143.5 139 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 176.5 156 L 172.5 161 L 176.5 156 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 259.5 156 L 263.5 161 L 259.5 156 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 212.5 162 L 222 162.5 L 212.5 163 L 212.5 162 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 231.5 175 L 226.5 181 L 231.5 175 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 106.5 188 L 111.5 194 L 106.5 188 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 326.5 190 L 321.5 196 L 326.5 190 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 195.5 194 L 201 201.5 L 196 197.5 L 195.5 194 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 239.5 196 L 234.5 202 L 239.5 196 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 227.5 200 L 229 200.5 L 222.5 203 L 222.5 202 L 227.5 200 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 288.5 227 L 292 229.5 Q 299.7 243.8 304 261.5 L 305 273.5 L 306 274.5 L 306 300.5 L 298 332.5 L 294 344.5 L 291.5 348 L 287.5 349 L 266.5 349 L 249.5 344 L 243 339.5 L 247 319.5 L 249.5 317 L 265 304.5 L 269 295.5 L 264 299.5 L 258.5 306 L 250.5 312 L 231.5 320 Q 226.7 318.8 225.5 321 L 207.5 321 L 187.5 314 Q 175.3 306.7 166.5 296 L 166 297.5 L 176.5 311 L 189 319.5 L 192 341 L 168.5 349 L 151.5 350 L 142 348 L 130 305.5 Q 131.5 300 129 298.5 L 129 271.5 L 133 253.5 L 145 228 L 167.5 236 L 184.5 240 L 196.5 241 L 197.5 242 L 222.5 243 L 223.5 242 L 236.5 242 L 237.5 241 L 254.5 239 L 278.5 232 L 288.5 227 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 46.5 236 L 56.5 236 L 79 249 Q 80.5 252.7 77.5 252 L 66 261.5 Q 54.7 272.7 48.5 289 Q 46.3 289.8 47 287.5 L 41 274.5 L 38 257.5 L 38 248.5 Q 39.2 240.7 44.5 237 L 46.5 236 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 201.5 236 L 232 236.5 L 201.5 237 L 201.5 236 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 106.5 265 L 110.5 265 L 124 271 Q 122.2 276.3 123 285.5 Q 110.8 295.8 105 312.5 Q 102.3 320.8 104 333.5 L 106 342 L 98.5 340 L 74 324 L 74 310.5 L 77 301.5 L 90 280 L 93 278.5 L 101.5 269 L 106.5 265 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 312.5 293 Q 319.7 300.3 323 311.5 Q 325.5 319 324 330.5 L 321 338 L 303 333.5 Q 309.5 316.5 311 294.5 L 312.5 293 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 122.5 294 L 124 295.5 L 126 313.5 L 132 334.5 L 130.5 336 L 123.5 336 L 111 339 Q 108.6 331.1 110 317.5 Q 113.6 303.1 122.5 294 Z " />
|
||||
<path fill="rgb(220,67,4)" stroke="rgb(220,67,4)" stroke-width="1" opacity="0.996078431372549"
|
||||
d="M 194 321 L 207.5 325 L 227.5 325 L 242 321 L 242 325.5 L 234 352.5 L 232 373 L 206.5 373 L 202 370.5 L 202 361.5 L 195 331.5 L 194 321 Z " />
|
||||
</svg>
|
||||
|
||||
<script>
|
||||
const paths = Array.from(document.querySelectorAll('#logo path'));
|
||||
|
||||
function getStartPoint(path) {
|
||||
const p = path.getPointAtLength(0);
|
||||
return { x: p.x, y: p.y };
|
||||
}
|
||||
|
||||
function dist(a, b) {
|
||||
return Math.hypot(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
|
||||
function sortPaths(paths) {
|
||||
const remaining = [...paths];
|
||||
const result = [];
|
||||
|
||||
let current = remaining.shift();
|
||||
result.push(current);
|
||||
|
||||
while (remaining.length) {
|
||||
const currPt = getStartPoint(current);
|
||||
|
||||
let minIdx = 0;
|
||||
let minDist = Infinity;
|
||||
|
||||
for (let i = 0; i < remaining.length; i++) {
|
||||
const pt = getStartPoint(remaining[i]);
|
||||
const d = dist(currPt, pt);
|
||||
if (d < minDist) {
|
||||
minDist = d;
|
||||
minIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
current = remaining.splice(minIdx, 1)[0];
|
||||
result.push(current);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
paths.forEach(p => {
|
||||
const len = p.getTotalLength();
|
||||
p.style.strokeDasharray = len;
|
||||
p.style.strokeDashoffset = len;
|
||||
});
|
||||
|
||||
const ordered = sortPaths(paths);
|
||||
|
||||
let delay = 0;
|
||||
|
||||
ordered.forEach((p, i) => {
|
||||
const len = p.getTotalLength();
|
||||
|
||||
p.style.transition = `stroke-dashoffset 0.4s ease ${delay}s`;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
p.style.strokeDashoffset = 0;
|
||||
});
|
||||
|
||||
delay += 0.01;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
paths.forEach(p => {
|
||||
p.style.transition = "fill-opacity 0.8s ease";
|
||||
p.style.fillOpacity = 1;
|
||||
p.style.stroke = "none";
|
||||
});
|
||||
}, delay * 1000 + 300);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -3,7 +3,7 @@ import {useQuery} from '@tanstack/react-query';
|
||||
import {workspaceList, workspaceInfo} from '@/client';
|
||||
import type {WorkspaceInfoResponse} from '@/client';
|
||||
import {WorkspaceSidebar} from '@/components/layout/workspace-sidebar';
|
||||
import {Spinner} from '@/components/ui/spinner';
|
||||
import LoadingAnimation from '@/components/ui/loading-animation';
|
||||
|
||||
export default function HomePageLayout() {
|
||||
const {data, isLoading} = useQuery({
|
||||
@ -28,11 +28,7 @@ export default function HomePageLayout() {
|
||||
});
|
||||
|
||||
if (isLoading || infoLoading) {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center bg-background">
|
||||
<Spinner/>
|
||||
</div>
|
||||
);
|
||||
return <LoadingAnimation/>;
|
||||
}
|
||||
|
||||
if (!workspaceInfoData) {
|
||||
|
||||
@ -2,7 +2,7 @@ import {useEffect} from 'react';
|
||||
import {useNavigate} from 'react-router-dom';
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
import {workspaceList} from '@/client';
|
||||
import {Spinner} from '@/components/ui/spinner';
|
||||
import LoadingAnimation from '@/components/ui/loading-animation';
|
||||
|
||||
export function WorkspaceRedirect() {
|
||||
const navigate = useNavigate();
|
||||
@ -25,9 +25,9 @@ export function WorkspaceRedirect() {
|
||||
}
|
||||
}, [isLoading, data, navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Spinner/>
|
||||
</div>
|
||||
);
|
||||
if (isLoading) {
|
||||
return <LoadingAnimation />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import LoadingAnimation from '@/components/ui/loading-animation';
|
||||
import { useUser } from '@/contexts/user-context';
|
||||
|
||||
export function ProtectedRoute() {
|
||||
@ -7,11 +7,7 @@ export function ProtectedRoute() {
|
||||
const location = useLocation();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-svh items-center justify-center">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
);
|
||||
return <LoadingAnimation />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
|
||||
@ -23,7 +23,7 @@ export function SidebarUser({collapsed}: { collapsed: boolean }) {
|
||||
<div className="w-full mb-2">
|
||||
<button type="button" className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')}
|
||||
onClick={() => navigate('/invitations')}>
|
||||
<span className="flex h-6 items-center shrink-0 w-6">
|
||||
<span className={cn('flex h-6 items-center shrink-0 w-6', collapsed && 'justify-center')}>
|
||||
<UserPlus className="h-4 w-4"/>
|
||||
</span>
|
||||
{!collapsed && <span className="text-sm leading-none">Invitations</span>}
|
||||
|
||||
161
src/components/ui/loading-animation.tsx
Normal file
161
src/components/ui/loading-animation.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
const pathsData = [
|
||||
'M 335.5 20 Q 341.3 19.3 342 23.5 L 347 41.5 L 347 63.5 L 344 79.5 L 336 101 Q 333.3 102.1 334 99.5 Q 320.8 70.7 297.5 52 L 287 43.5 Q 285.9 40.2 288.5 41 L 313.5 27 L 335.5 20 Z',
|
||||
'M 95.5 21 L 105.5 22 L 115.5 25 L 147 41.5 L 145.5 45 L 130 57 L 110 83.5 L 100.5 102 L 94 88.5 L 90 70.5 Q 91.3 65.8 89 64.5 L 89 38.5 L 92 25.5 L 95.5 21 Z',
|
||||
'M 154.5 112 Q 164 110.5 169.5 113 L 183 123.5 L 189 136.5 L 189 147.5 Q 186.4 158.4 178.5 164 L 167.5 169 L 156.5 169 L 150.5 167 L 140.5 159 Q 124.4 174.2 120 200 Q 114.6 199.4 112 194.5 L 100 179.5 L 96 171.5 Q 107.1 144.1 127.5 126 Q 138.8 116.8 154.5 112 Z M 158 123 L 152 126 Q 145 130 143 140 L 143 148 L 147 157 Q 152 162 161 164 Q 168 164 173 161 Q 178 157 181 149 Q 183 135 176 130 Q 171 122 158 123 Z',
|
||||
'M 269.5 112 Q 281.6 110.4 288.5 114 L 302.5 121 L 319 136.5 L 332 154.5 L 338 168.5 L 338 172.5 Q 330.1 188.6 317.5 200 L 315.5 201 Q 312.3 185.2 306 172.5 L 294.5 160 L 292.5 159 Q 286.5 170 268.5 169 Q 257.4 166.6 252 158.5 L 247 147.5 L 247 136.5 Q 249.6 125.6 256.5 119 L 269.5 112 Z M 272 123 L 264 126 Q 258 130 255 139 Q 253 151 259 157 Q 263 162 271 164 L 278 164 L 288 158 L 293 147 L 293 140 L 288 130 Q 283 123 272 123 Z',
|
||||
'M 154.5 130 Q 160.8 128.8 162 132.5 Q 162.6 137.6 159.5 139 Q 154.3 140.3 153 137.5 L 152 133.5 L 154.5 130 Z',
|
||||
'M 266.5 130 Q 272 129 273 132.5 L 272 139 L 268.5 140 L 264 136.5 Q 263 131 266.5 130 Z',
|
||||
'M 206.5 154 L 226.5 154 L 235.5 156 L 249.5 162 L 266 175.5 Q 274.8 185.2 279 199.5 L 279 216.5 L 273.5 227 L 254.5 233 L 237.5 235 L 236.5 236 L 197.5 236 L 167.5 230 L 159 225.5 L 156 214.5 L 156 204.5 Q 159.9 184.9 171.5 173 Q 173.8 173.8 173 171.5 Q 185 158 206.5 154 Z M 213 162 L 206 165 L 202 170 Q 201 176 204 179 L 214 184 L 219 184 L 227 181 L 233 174 L 232 168 Q 226 161 213 162 Z M 196 189 L 194 191 L 196 198 Q 203 206 216 208 L 225 207 L 235 202 L 242 194 L 242 190 L 237 191 L 234 197 L 222 203 L 214 203 L 204 199 Q 199 196 198 191 L 196 189 Z',
|
||||
'M 87.5 4 L 97.5 4 L 115.5 8 L 142.5 20 L 161.5 34 L 164.5 34 L 176.5 28 L 190.5 24 L 200.5 22 L 221.5 21 L 222.5 22 L 233.5 22 L 247.5 25 L 272.5 35 Q 288.3 20.8 309.5 12 L 330.5 5 L 346.5 4 L 354 9.5 L 359 19.5 L 362 35.5 L 362 64.5 L 356 91.5 L 342 121.5 L 350 144.5 L 350 159.5 Q 340.5 189 319.5 207 L 303.5 220 L 297 223 L 296 225.5 Q 302.8 234.2 306 246.5 L 310 263.5 L 312 282.5 Q 322.5 292 328 306.5 L 330 312.5 L 331 325.5 L 327 341.5 L 336 349.5 L 342 361.5 L 342 368.5 Q 340 378.5 332.5 383 L 322.5 386 L 297.5 386 L 296.5 385 L 284.5 385 L 273.5 393 Q 266.3 396.3 254.5 395 Q 241.6 392.4 235 383.5 L 233 379 L 204.5 379 L 203.5 378 L 201 379 Q 199.9 385.6 194.5 389 L 180.5 395 L 168.5 395 L 148.5 385 L 147.5 386 L 112.5 386 L 100.5 382 L 96 377.5 Q 90.8 371.7 93 358.5 Q 94.4 351.9 99 348.5 L 99 346 Q 73.7 335 57 314.5 L 43 294.5 L 37 280.5 L 32 261.5 L 32 245.5 Q 33.9 236.4 40.5 232 Q 44.9 227.9 54.5 229 L 66.5 234 L 77.5 242 L 99.5 254 L 121.5 264 L 125 264 L 130 244.5 L 140 226.5 L 140 224 L 134.5 222 L 123.5 214 L 108.5 199 L 107 199 L 95 183.5 L 88 169.5 L 86 162.5 L 86 143.5 L 93 118.5 L 80 92.5 L 75 74.5 L 73 63.5 L 73 36.5 L 78 16.5 Q 80.2 9.2 85.5 5 L 87.5 4 Z M 91 10 L 89 11 L 84 18 L 79 39 L 79 64 L 81 75 L 87 94 L 94 110 L 97 112 L 97 108 L 87 85 L 83 65 L 83 40 Q 86 38 84 33 Q 85 23 90 18 L 94 15 L 106 16 L 129 24 Q 143 29 154 39 Q 157 40 156 38 L 138 24 L 118 15 L 107 13 L 103 11 L 91 10 Z M 338 10 L 329 13 L 321 14 L 311 18 L 281 36 L 279 39 L 284 38 L 311 22 L 327 16 L 338 14 L 344 16 L 349 25 L 353 42 L 353 65 L 351 71 L 351 76 L 346 93 L 338 110 Q 337 114 341 112 L 353 82 L 357 62 L 357 38 L 353 20 L 351 16 Q 349 8 338 10 Z M 337 19 L 317 25 L 288 41 Q 285 40 286 44 L 305 59 Q 323 76 334 101 Q 333 103 336 102 L 342 89 L 346 74 L 347 60 L 348 59 L 347 39 L 342 23 L 337 19 Z M 95 21 L 90 29 L 89 40 L 88 41 L 88 63 L 89 64 L 90 75 L 94 91 L 101 103 Q 111 79 128 61 L 147 44 Q 150 45 148 41 L 118 25 L 104 21 L 95 21 Z M 213 27 L 212 28 L 201 28 L 188 31 L 167 39 L 151 48 L 132 65 Q 133 67 131 66 Q 102 99 91 150 Q 91 163 95 173 L 113 197 Q 131 214 156 226 L 159 226 L 164 229 L 177 233 L 193 236 L 201 236 L 202 237 L 232 237 L 233 236 L 242 236 L 269 230 L 295 218 L 320 199 L 333 183 Q 340 172 344 158 L 344 146 L 333 111 L 326 96 L 306 67 L 286 50 L 268 39 L 243 30 L 233 28 L 213 27 Z M 289 227 L 279 232 L 255 239 L 238 241 L 237 242 L 224 242 L 223 243 L 198 242 L 197 241 L 185 240 L 168 236 L 145 228 L 133 254 L 129 272 L 129 299 Q 132 300 130 306 L 142 348 L 152 350 L 169 349 L 192 341 L 189 320 L 177 311 L 166 298 L 167 296 Q 175 307 188 314 L 208 321 L 226 321 Q 227 319 232 320 L 251 312 L 259 306 L 264 300 L 269 296 L 265 305 L 250 317 L 247 320 L 243 340 L 250 344 L 267 349 L 288 349 L 292 348 L 294 345 L 298 333 L 306 301 L 306 275 L 305 274 L 304 262 Q 300 244 292 230 L 289 227 Z M 47 236 L 45 237 Q 39 241 38 249 L 38 258 L 41 275 L 47 288 Q 46 290 49 289 Q 55 273 66 262 L 78 252 Q 81 253 79 249 L 57 236 L 47 236 Z M 107 265 L 102 269 L 93 279 L 90 280 L 77 302 L 74 311 L 74 324 L 99 340 L 106 342 L 104 334 Q 102 321 105 313 Q 111 296 123 286 Q 122 276 124 271 L 111 265 L 107 265 Z M 313 293 L 311 295 Q 310 317 303 334 L 321 338 L 324 331 Q 326 319 323 312 Q 320 300 313 293 Z M 123 294 Q 114 303 110 318 Q 109 331 111 339 L 124 336 L 131 336 L 132 335 L 126 314 L 124 296 L 123 294 Z M 194 321 L 195 332 L 202 362 L 202 371 L 207 373 L 232 373 L 234 353 L 242 326 L 242 321 L 228 325 L 208 325 L 194 321 Z',
|
||||
'M 157.5 124 Q 171.5 122.5 176 130.5 Q 181 135.5 181 145.5 Q 179.3 156.3 171.5 161 L 165.5 163 L 159.5 163 Q 151 161.5 147 155.5 Q 142.5 150 144 138.5 Q 145.3 131.8 149.5 128 L 157.5 124 Z M 156 129 L 153 130 L 152 133 L 153 139 L 156 140 L 161 139 L 163 135 L 162 132 Q 161 128 156 129 Z',
|
||||
'M 270.5 124 Q 283.2 123.3 288 130.5 Q 293.7 135.8 292 148.5 L 285.5 159 L 278.5 163 L 269.5 163 Q 262.3 161.2 259 155.5 L 256 150.5 L 255 142.5 Q 256.7 131.2 264.5 126 L 270.5 124 Z M 267 129 Q 262 130 263 137 Q 264 141 271 140 Q 275 139 274 133 Q 273 128 267 129 Z',
|
||||
'M 211.5 163 Q 226.5 161 232 168.5 L 233 172.5 L 225.5 181 Q 221.1 184.1 212.5 183 Q 206.8 181.8 204 177.5 L 202 172.5 L 206.5 165 L 211.5 163 Z',
|
||||
'M 195 190 L 198 191.5 L 202.5 199 Q 208.4 204.1 219.5 204 Q 231.5 202 237 193.5 L 238.5 191 L 240.5 190 L 241 193.5 L 233.5 202 L 222.5 207 L 212.5 207 Q 202.4 204.6 197 197.5 L 195 190 Z',
|
||||
'M 0 0 L 400 0 L 400 400 L 0 400 L 0 0 Z M 88 4 L 86 5 Q 80 9 78 17 L 73 37 L 73 64 L 75 75 L 80 93 L 93 119 L 86 144 L 86 163 L 88 170 L 95 184 L 107 199 L 109 199 L 124 214 L 135 222 L 140 224 L 140 227 L 130 245 L 125 264 L 122 264 L 100 254 L 78 242 L 67 234 L 55 229 Q 45 228 41 232 Q 34 236 32 246 L 32 262 L 37 281 L 43 295 L 57 315 Q 74 335 99 346 L 99 349 Q 94 352 93 359 Q 91 372 96 378 L 101 382 L 113 386 L 148 386 L 149 385 L 169 395 L 181 395 L 195 389 Q 200 386 201 379 L 204 378 L 205 379 L 233 379 L 235 384 Q 242 392 255 395 Q 266 396 274 393 L 285 385 L 297 385 L 298 386 L 323 386 L 333 383 Q 340 378 342 369 L 342 362 L 336 350 L 327 342 L 331 326 L 330 313 L 328 307 Q 322 292 312 283 L 310 264 L 306 247 Q 303 234 296 226 L 297 223 L 304 220 L 320 207 Q 341 189 350 160 L 350 145 L 342 122 L 356 92 L 362 65 L 362 36 L 359 20 L 354 10 L 347 4 L 331 5 L 310 12 Q 288 21 273 35 L 248 25 L 234 22 L 223 22 L 222 21 L 201 22 L 191 24 L 177 28 L 165 34 L 162 34 L 143 20 L 116 8 L 98 4 L 88 4 Z',
|
||||
'M 90.5 10 L 102.5 11 L 106.5 13 L 117.5 15 L 137.5 24 L 156 37.5 Q 156.8 40.1 153.5 39 Q 143.1 29.4 128.5 24 L 105.5 16 L 93.5 15 L 90 17.5 Q 85.5 23.5 84 32.5 Q 85.5 38 83 39.5 L 83 64.5 L 87 84.5 L 97 107.5 L 97 111.5 L 94 109.5 L 87 93.5 L 81 74.5 L 79 63.5 L 79 38.5 L 84 17.5 L 88.5 11 L 90.5 10 Z',
|
||||
'M 337.5 10 Q 348.6 8.4 351 15.5 L 353 19.5 L 357 37.5 L 357 61.5 L 353 81.5 L 341 112 Q 336.9 113.7 338 109.5 L 346 92.5 L 351 75.5 L 351 70.5 L 353 64.5 L 353 41.5 L 349 24.5 L 343.5 16 L 337.5 14 L 326.5 16 L 310.5 22 L 283.5 38 L 279 39 L 280.5 36 L 310.5 18 L 320.5 14 L 328.5 13 L 337.5 10 Z',
|
||||
'M 212.5 27 L 232.5 28 L 242.5 30 L 267.5 39 L 285.5 50 L 306 67 L 326 95.5 L 333 110.5 L 344 145.5 L 344 157.5 L 338.5 172 Q 335.5 159 329 149.5 L 309.5 127 L 302.5 121 L 288.5 114 Q 281.6 110.4 269.5 112 Q 259.9 114.9 254 121.5 L 247 136.5 L 247 147.5 Q 249.9 159.1 258.5 165 L 268.5 169 L 278.5 169 Q 287.5 166 292.5 159 L 304 169.5 L 310 181.5 L 315.5 201 L 317 200.5 Q 298.4 219.4 270.5 229 L 270.5 228 L 275 225.5 L 279 216.5 L 279 199.5 Q 274.5 184 264.5 174 L 249.5 162 L 235.5 156 L 226.5 154 L 206.5 154 Q 185 158 173 171.5 Q 173.8 173.8 171.5 173 Q 163.3 181.3 159 193.5 L 156 204.5 L 156 214.5 L 160 226.5 L 158.5 226 L 155.5 226 L 140.5 218 L 117 199.5 L 118.5 200 Q 121.9 200.9 120 195.5 L 127 177.5 Q 132.3 166.8 140.5 159 L 150.5 167 Q 156.6 170.4 167.5 169 Q 178.3 166.3 184 158.5 Q 190.7 151.7 189 136.5 Q 186 124.5 177.5 118 Q 171.1 109.9 154.5 112 Q 132.3 118.8 119 134.5 Q 104.1 150.1 96 172.5 L 96.5 174 L 93 166.5 L 91 149.5 Q 101.6 98.6 130.5 66 Q 132.8 66.8 132 64.5 L 150.5 48 L 166.5 39 L 187.5 31 L 200.5 28 L 211.5 28 L 212.5 27 Z',
|
||||
'M 297.5 35 L 296.5 37 L 292.5 39 L 293.5 37 L 297.5 35 Z',
|
||||
'M 88.5 40 L 89 62.5 L 88 62.5 L 88.5 40 Z',
|
||||
'M 286.5 41 L 290 46.5 L 286 43.5 L 286.5 41 Z',
|
||||
'M 347.5 46 L 348 58.5 L 347 58.5 L 347.5 46 Z',
|
||||
'M 140.5 48 L 136.5 53 L 140.5 48 Z',
|
||||
'M 297.5 52 L 303.5 59 L 297.5 52 Z',
|
||||
'M 127.5 61 L 121.5 68 L 127.5 61 Z',
|
||||
'M 306.5 61 L 314.5 70 L 306.5 61 Z',
|
||||
'M 157.5 123 L 166 123.5 L 155.5 125 L 155.5 124 L 157.5 123 Z',
|
||||
'M 263.5 126 L 259.5 131 L 263.5 126 Z',
|
||||
'M 266.5 129 Q 273 128 274 132.5 Q 275 137 272.5 138 L 273 136.5 Q 274 131 270.5 130 Q 265.5 129 264.5 132 L 266.5 129 Z',
|
||||
'M 176.5 131 L 180 136.5 L 179 136.5 L 176.5 131 Z',
|
||||
'M 263.5 133 Q 263.3 137.7 266.5 139 L 268 139.5 L 266.5 140 Q 261.6 138.6 263.5 133 Z',
|
||||
'M 143.5 139 L 144 147.5 L 143 147.5 L 143.5 139 Z',
|
||||
'M 176.5 156 L 172.5 161 L 176.5 156 Z',
|
||||
'M 259.5 156 L 263.5 161 L 259.5 156 Z',
|
||||
'M 212.5 162 L 222 162.5 L 212.5 163 L 212.5 162 Z',
|
||||
'M 231.5 175 L 226.5 181 L 231.5 175 Z',
|
||||
'M 106.5 188 L 111.5 194 L 106.5 188 Z',
|
||||
'M 326.5 190 L 321.5 196 L 326.5 190 Z',
|
||||
'M 195.5 194 L 201 201.5 L 196 197.5 L 195.5 194 Z',
|
||||
'M 239.5 196 L 234.5 202 L 239.5 196 Z',
|
||||
'M 227.5 200 L 229 200.5 L 222.5 203 L 222.5 202 L 227.5 200 Z',
|
||||
'M 288.5 227 L 292 229.5 Q 299.7 243.8 304 261.5 L 305 273.5 L 306 274.5 L 306 300.5 L 298 332.5 L 294 344.5 L 291.5 348 L 287.5 349 L 266.5 349 L 249.5 344 L 243 339.5 L 247 319.5 L 249.5 317 L 265 304.5 L 269 295.5 L 264 299.5 L 258.5 306 L 250.5 312 L 231.5 320 Q 226.7 318.8 225.5 321 L 207.5 321 L 187.5 314 Q 175.3 306.7 166.5 296 L 166 297.5 L 176.5 311 L 189 319.5 L 192 341 L 168.5 349 L 151.5 350 L 142 348 L 130 305.5 Q 131.5 300 129 298.5 L 129 271.5 L 133 253.5 L 145 228 L 167.5 236 L 184.5 240 L 196.5 241 L 197.5 242 L 222.5 243 L 223.5 242 L 236.5 242 L 237.5 241 L 254.5 239 L 278.5 232 L 288.5 227 Z',
|
||||
'M 46.5 236 L 56.5 236 L 79 249 Q 80.5 252.7 77.5 252 L 66 261.5 Q 54.7 272.7 48.5 289 Q 46.3 289.8 47 287.5 L 41 274.5 L 38 257.5 L 38 248.5 Q 39.2 240.7 44.5 237 L 46.5 236 Z',
|
||||
'M 201.5 236 L 232 236.5 L 201.5 237 L 201.5 236 Z',
|
||||
'M 106.5 265 L 110.5 265 L 124 271 Q 122.2 276.3 123 285.5 Q 110.8 295.8 105 312.5 Q 102.3 320.8 104 333.5 L 106 342 L 98.5 340 L 74 324 L 74 310.5 L 77 301.5 L 90 280 L 93 278.5 L 101.5 269 L 106.5 265 Z',
|
||||
'M 312.5 293 Q 319.7 300.3 323 311.5 Q 325.5 319 324 330.5 L 321 338 L 303 333.5 Q 309.5 316.5 311 294.5 L 312.5 293 Z',
|
||||
'M 122.5 294 L 124 295.5 L 126 313.5 L 132 334.5 L 130.5 336 L 123.5 336 L 111 339 Q 108.6 331.1 110 317.5 Q 113.6 303.1 122.5 294 Z',
|
||||
'M 194 321 L 207.5 325 L 227.5 325 L 242 321 L 242 325.5 L 234 352.5 L 232 373 L 206.5 373 L 202 370.5 L 202 361.5 L 195 331.5 L 194 321 Z',
|
||||
];
|
||||
|
||||
/** Paths with orange fill (rgb(220,67,4)) */
|
||||
const orangeIndices = new Set([14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]);
|
||||
|
||||
interface LoadingAnimationProps {
|
||||
onFinish?: () => void;
|
||||
}
|
||||
|
||||
export default function LoadingAnimation({ onFinish }: LoadingAnimationProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const svg = container.querySelector('svg');
|
||||
if (!svg) return;
|
||||
|
||||
/* All paths except the background rect (index 13) */
|
||||
const pathElements = Array.from(svg.querySelectorAll('path')).filter(
|
||||
(_, i) => i !== 13,
|
||||
);
|
||||
|
||||
const getStartPoint = (path: SVGPathElement) => {
|
||||
const p = path.getPointAtLength(0);
|
||||
return { x: p.x, y: p.y };
|
||||
};
|
||||
|
||||
const dist = (a: { x: number; y: number }, b: { x: number; y: number }) =>
|
||||
Math.hypot(a.x - b.x, a.y - b.y);
|
||||
|
||||
// Sort paths by proximity
|
||||
const remaining = [...pathElements];
|
||||
const ordered: SVGPathElement[] = [];
|
||||
let current = remaining.shift()!;
|
||||
ordered.push(current);
|
||||
while (remaining.length) {
|
||||
const currPt = getStartPoint(current);
|
||||
let minIdx = 0;
|
||||
let minDist = Infinity;
|
||||
for (let i = 0; i < remaining.length; i++) {
|
||||
const pt = getStartPoint(remaining[i]);
|
||||
const d = dist(currPt, pt);
|
||||
if (d < minDist) {
|
||||
minDist = d;
|
||||
minIdx = i;
|
||||
}
|
||||
}
|
||||
current = remaining.splice(minIdx, 1)[0];
|
||||
ordered.push(current);
|
||||
}
|
||||
|
||||
// Set dash lengths
|
||||
ordered.forEach((p) => {
|
||||
const len = p.getTotalLength();
|
||||
p.style.strokeDasharray = String(len);
|
||||
p.style.strokeDashoffset = String(len);
|
||||
});
|
||||
|
||||
// Animate
|
||||
let delay = 0;
|
||||
ordered.forEach((p) => {
|
||||
p.style.transition = `stroke-dashoffset 0.4s ease ${delay}s`;
|
||||
requestAnimationFrame(() => {
|
||||
p.style.strokeDashoffset = '0';
|
||||
});
|
||||
delay += 0.01;
|
||||
});
|
||||
|
||||
// Fill + done
|
||||
const fillDelay = 1500; // minimum 1.5s total
|
||||
const animEnd = delay * 1000 + 300;
|
||||
|
||||
setTimeout(() => {
|
||||
pathElements.forEach((p) => {
|
||||
p.style.transition = 'fill-opacity 0.8s ease';
|
||||
p.style.fillOpacity = '1';
|
||||
p.style.stroke = 'none';
|
||||
});
|
||||
onFinish?.();
|
||||
}, Math.max(animEnd, fillDelay));
|
||||
}, [onFinish]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex h-screen w-full items-center justify-center bg-white"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 400 400"
|
||||
width="200"
|
||||
height="200"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{pathsData.map((d, i) => {
|
||||
const isOrange = orangeIndices.has(i);
|
||||
return (
|
||||
<path
|
||||
key={i}
|
||||
fill={isOrange ? 'rgb(220,67,4)' : 'rgb(6,6,6)'}
|
||||
stroke={isOrange ? 'rgb(220,67,4)' : 'rgb(6,6,6)'}
|
||||
strokeWidth="1"
|
||||
fillOpacity="0"
|
||||
d={d}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user