use axum::{ extract::{Path, Query, State}, response::Html, routing::get, Router, }; use diesel::prelude::*; use serde::Deserialize; use std::sync::Arc; use uuid::Uuid; use crate::core::bot::get_default_bot; use crate::core::shared::schema::{canvas_elements, canvases}; use crate::core::shared::state::AppState; use super::{DbCanvas, DbCanvasElement}; #[derive(Debug, Deserialize)] pub struct ListQuery { pub search: Option, pub is_template: Option, } fn get_bot_context(state: &AppState) -> (Uuid, Uuid) { let Ok(mut conn) = state.conn.get() else { return (Uuid::nil(), Uuid::nil()); }; let (bot_id, _bot_name) = get_default_bot(&mut conn); let org_id = Uuid::nil(); (org_id, bot_id) } fn html_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } fn render_empty_state(icon: &str, title: &str, description: &str) -> String { format!( r##"
{icon}

{title}

{description}

"## ) } fn render_canvas_card(canvas: &DbCanvas, element_count: i64) -> String { let name = html_escape(&canvas.name); let description = canvas .description .as_deref() .map(html_escape) .unwrap_or_default(); let bg_color = canvas .background_color .as_deref() .unwrap_or("#ffffff"); let updated = canvas.updated_at.format("%Y-%m-%d %H:%M").to_string(); let id = canvas.id; let template_badge = if canvas.is_template { r##"Template"## } else { "" }; let public_badge = if canvas.is_public { r##"Public"## } else { "" }; format!( r##"
{element_count} elements

{name}

{description}

{updated} {template_badge} {public_badge}
"## ) } fn render_canvas_row(canvas: &DbCanvas, element_count: i64) -> String { let name = html_escape(&canvas.name); let description = canvas .description .as_deref() .map(html_escape) .unwrap_or_else(|| "-".to_string()); let updated = canvas.updated_at.format("%Y-%m-%d %H:%M").to_string(); let id = canvas.id; let status = if canvas.is_public { "Public" } else { "Private" }; format!( r##" {name} {description} {element_count} {status} {updated} "## ) } pub async fn canvas_list( State(state): State>, Query(query): Query, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let (org_id, bot_id) = get_bot_context(&state); let mut q = canvases::table .filter(canvases::org_id.eq(org_id)) .filter(canvases::bot_id.eq(bot_id)) .into_boxed(); if let Some(is_template) = query.is_template { q = q.filter(canvases::is_template.eq(is_template)); } if let Some(search) = &query.search { let pattern = format!("%{search}%"); q = q.filter( canvases::name .ilike(pattern.clone()) .or(canvases::description.ilike(pattern)), ); } let db_canvases: Vec = match q .order(canvases::updated_at.desc()) .limit(50) .load(&mut conn) { Ok(c) => c, Err(_) => { return Html(render_empty_state("⚠️", "Error", "Failed to load canvases")); } }; if db_canvases.is_empty() { return Html(render_empty_state( "🎨", "No Canvases", "Create your first canvas to get started", )); } let mut rows = String::new(); for canvas in &db_canvases { let element_count: i64 = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas.id)) .count() .get_result(&mut conn) .unwrap_or(0); rows.push_str(&render_canvas_row(canvas, element_count)); } Html(format!( r##"{rows}
Name Description Elements Status Updated Actions
"## )) } pub async fn canvas_cards( State(state): State>, Query(query): Query, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let (org_id, bot_id) = get_bot_context(&state); let mut q = canvases::table .filter(canvases::org_id.eq(org_id)) .filter(canvases::bot_id.eq(bot_id)) .into_boxed(); if let Some(is_template) = query.is_template { q = q.filter(canvases::is_template.eq(is_template)); } if let Some(search) = &query.search { let pattern = format!("%{search}%"); q = q.filter( canvases::name .ilike(pattern.clone()) .or(canvases::description.ilike(pattern)), ); } let db_canvases: Vec = match q .order(canvases::updated_at.desc()) .limit(50) .load(&mut conn) { Ok(c) => c, Err(_) => { return Html(render_empty_state("⚠️", "Error", "Failed to load canvases")); } }; if db_canvases.is_empty() { return Html(render_empty_state( "🎨", "No Canvases", "Create your first canvas to get started", )); } let mut cards = String::new(); for canvas in &db_canvases { let element_count: i64 = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas.id)) .count() .get_result(&mut conn) .unwrap_or(0); cards.push_str(&render_canvas_card(canvas, element_count)); } Html(format!(r##"
{cards}
"##)) } pub async fn canvas_count(State(state): State>) -> Html { let Ok(mut conn) = state.conn.get() else { return Html("0".to_string()); }; let (org_id, bot_id) = get_bot_context(&state); let count: i64 = canvases::table .filter(canvases::org_id.eq(org_id)) .filter(canvases::bot_id.eq(bot_id)) .count() .get_result(&mut conn) .unwrap_or(0); Html(count.to_string()) } pub async fn canvas_templates_count(State(state): State>) -> Html { let Ok(mut conn) = state.conn.get() else { return Html("0".to_string()); }; let (org_id, bot_id) = get_bot_context(&state); let count: i64 = canvases::table .filter(canvases::org_id.eq(org_id)) .filter(canvases::bot_id.eq(bot_id)) .filter(canvases::is_template.eq(true)) .count() .get_result(&mut conn) .unwrap_or(0); Html(count.to_string()) } pub async fn canvas_detail( State(state): State>, Path(canvas_id): Path, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let canvas: DbCanvas = match canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) { Ok(c) => c, Err(_) => { return Html(render_empty_state("❌", "Not Found", "Canvas not found")); } }; let elements: Vec = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas_id)) .order(canvas_elements::z_index.asc()) .load(&mut conn) .unwrap_or_default(); let name = html_escape(&canvas.name); let description = canvas .description .as_deref() .map(html_escape) .unwrap_or_else(|| "No description".to_string()); let bg_color = canvas.background_color.as_deref().unwrap_or("#ffffff"); let created = canvas.created_at.format("%Y-%m-%d %H:%M").to_string(); let updated = canvas.updated_at.format("%Y-%m-%d %H:%M").to_string(); let element_count = elements.len(); let status = if canvas.is_public { "Public" } else { "Private" }; let template_status = if canvas.is_template { "Yes" } else { "No" }; Html(format!( r##"

{name}

{description}

Elements {element_count}
Size {width}x{height}
Background {bg_color}
Status {status}
Template {template_status}
Created: {created} Updated: {updated}
"##, width = canvas.width, height = canvas.height )) } pub async fn canvas_editor( State(state): State>, Path(canvas_id): Path, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let canvas: DbCanvas = match canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) { Ok(c) => c, Err(_) => { return Html(render_empty_state("❌", "Not Found", "Canvas not found")); } }; let name = html_escape(&canvas.name); let bg_color = canvas.background_color.as_deref().unwrap_or("#ffffff"); Html(format!( r##"
{name}

Properties

Select an element to edit its properties

"##, width = canvas.width, height = canvas.height )) } pub async fn canvas_elements_svg( State(state): State>, Path(canvas_id): Path, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(String::new()); }; let elements: Vec = canvas_elements::table .filter(canvas_elements::canvas_id.eq(canvas_id)) .order(canvas_elements::z_index.asc()) .load(&mut conn) .unwrap_or_default(); let mut svg_elements = String::new(); for el in &elements { let svg = render_element_svg(el); svg_elements.push_str(&svg); } Html(svg_elements) } fn render_element_svg(element: &DbCanvasElement) -> String { let id = element.id; let x = element.x; let y = element.y; let width = element.width; let height = element.height; let rotation = element.rotation; let transform = if rotation != 0.0 { format!( r##" transform="rotate({rotation} {} {})""##, x + width / 2.0, y + height / 2.0 ) } else { String::new() }; let fill = element .properties .get("fill_color") .and_then(|v| v.as_str()) .unwrap_or("transparent"); let stroke = element .properties .get("stroke_color") .and_then(|v| v.as_str()) .unwrap_or("#000000"); let stroke_width = element .properties .get("stroke_width") .and_then(|v| v.as_f64()) .unwrap_or(2.0); let opacity = element .properties .get("opacity") .and_then(|v| v.as_f64()) .unwrap_or(1.0); match element.element_type.as_str() { "rectangle" => { let corner_radius = element .properties .get("corner_radius") .and_then(|v| v.as_f64()) .unwrap_or(0.0); format!( r##""## ) } "ellipse" => { let cx = x + width / 2.0; let cy = y + height / 2.0; let rx = width / 2.0; let ry = height / 2.0; format!( r##""## ) } "line" | "arrow" => { let x2 = x + width; let y2 = y + height; let marker = if element.element_type == "arrow" { r##" marker-end="url(#arrowhead)""## } else { "" }; format!( r##""## ) } "text" => { let text = element .properties .get("text") .and_then(|v| v.as_str()) .unwrap_or(""); let font_size = element .properties .get("font_size") .and_then(|v| v.as_f64()) .unwrap_or(16.0); let font_family = element .properties .get("font_family") .and_then(|v| v.as_str()) .unwrap_or("sans-serif"); let text_y = y + font_size; format!( r##"{text}"##, text = html_escape(text) ) } "freehand_path" => { let path_data = element .properties .get("path_data") .and_then(|v| v.as_str()) .unwrap_or(""); format!( r##""## ) } "sticky" => { let text = element .properties .get("text") .and_then(|v| v.as_str()) .unwrap_or(""); let bg = element .properties .get("fill_color") .and_then(|v| v.as_str()) .unwrap_or("#ffeb3b"); format!( r##" {text} "##, text_x = x + 8.0, text_y = y + 24.0, text = html_escape(text) ) } _ => format!( r##""## ), } } pub async fn canvas_settings( State(state): State>, Path(canvas_id): Path, ) -> Html { let Ok(mut conn) = state.conn.get() else { return Html(render_empty_state("⚠️", "Database Error", "Could not connect to database")); }; let canvas: DbCanvas = match canvases::table .filter(canvases::id.eq(canvas_id)) .first(&mut conn) { Ok(c) => c, Err(_) => { return Html(render_empty_state("❌", "Not Found", "Canvas not found")); } }; let name = html_escape(&canvas.name); let description = canvas.description.as_deref().map(html_escape).unwrap_or_default(); let bg_color = canvas.background_color.as_deref().unwrap_or("#ffffff"); let is_public_checked = if canvas.is_public { "checked" } else { "" }; let is_template_checked = if canvas.is_template { "checked" } else { "" }; Html(format!( r##"
"##, width = canvas.width, height = canvas.height )) } pub async fn new_canvas_form(State(_state): State>) -> Html { Html( r##"
"## .to_string(), ) } pub fn configure_canvas_ui_routes() -> Router> { Router::new() .route("/api/ui/canvas", get(canvas_list)) .route("/api/ui/canvas/cards", get(canvas_cards)) .route("/api/ui/canvas/count", get(canvas_count)) .route("/api/ui/canvas/templates/count", get(canvas_templates_count)) .route("/api/ui/canvas/new", get(new_canvas_form)) .route("/api/ui/canvas/{canvas_id}", get(canvas_detail)) .route("/api/ui/canvas/{canvas_id}/editor", get(canvas_editor)) .route("/api/ui/canvas/{canvas_id}/elements", get(canvas_elements_svg)) .route("/api/ui/canvas/{canvas_id}/settings", get(canvas_settings)) }