use crate::shared::state::AppState; use crate::slides::collaboration::broadcast_slide_change; use crate::slides::storage::{ create_new_presentation, create_slide_with_layout, delete_presentation_from_drive, get_current_user_id, list_presentations_from_drive, load_presentation_by_id, load_presentation_from_drive, save_presentation_to_drive, }; use crate::slides::types::{ AddElementRequest, AddSlideRequest, ApplyThemeRequest, DeleteElementRequest, DeleteSlideRequest, DuplicateSlideRequest, ExportRequest, LoadQuery, Presentation, PresentationMetadata, ReorderSlidesRequest, SavePresentationRequest, SaveResponse, SearchQuery, SlidesAiRequest, SlidesAiResponse, UpdateElementRequest, UpdateSlideNotesRequest, }; use crate::slides::utils::export_to_html; use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, Json, }; use chrono::Utc; use log::error; use std::sync::Arc; use uuid::Uuid; pub async fn handle_slides_ai( State(_state): State>, Json(req): Json, ) -> impl IntoResponse { let command = req.command.to_lowercase(); let response = if command.contains("add") && command.contains("slide") { "I've added a new slide to your presentation." } else if command.contains("duplicate") { "I've duplicated the current slide." } else if command.contains("delete") || command.contains("remove") { "I've removed the slide from your presentation." } else if command.contains("text") || command.contains("title") { "I've added a text box to your slide. Click to edit." } else if command.contains("image") || command.contains("picture") { "I've added an image placeholder. Click to upload an image." } else if command.contains("shape") { "I've added a shape to your slide. You can resize and move it." } else if command.contains("chart") { "I've added a chart. Click to edit the data." } else if command.contains("table") { "I've added a table. Click cells to edit." } else if command.contains("theme") || command.contains("design") { "I can help you change the theme. Choose from the Design menu." } else if command.contains("animate") || command.contains("animation") { "I've added an animation to the selected element." } else if command.contains("transition") { "I've applied a transition effect to this slide." } else if command.contains("help") { "I can help you with:\n• Add/duplicate/delete slides\n• Insert text, images, shapes\n• Add charts and tables\n• Apply themes and animations\n• Set slide transitions" } else { "I understand you want help with your presentation. Try commands like 'add slide', 'insert image', 'add chart', or 'apply animation'." }; Json(SlidesAiResponse { response: response.to_string(), action: None, data: None, }) } pub async fn handle_new_presentation( State(_state): State>, ) -> Result, (StatusCode, Json)> { Ok(Json(create_new_presentation())) } pub async fn handle_list_presentations( State(state): State>, ) -> Result>, (StatusCode, Json)> { let user_id = get_current_user_id(); match list_presentations_from_drive(&state, &user_id).await { Ok(presentations) => Ok(Json(presentations)), Err(e) => { error!("Failed to list presentations: {}", e); Ok(Json(Vec::new())) } } } pub async fn handle_search_presentations( State(state): State>, Query(query): Query, ) -> Result>, (StatusCode, Json)> { let user_id = get_current_user_id(); let presentations = match list_presentations_from_drive(&state, &user_id).await { Ok(p) => p, Err(_) => Vec::new(), }; let filtered = if let Some(q) = query.q { let q_lower = q.to_lowercase(); presentations .into_iter() .filter(|p| p.name.to_lowercase().contains(&q_lower)) .collect() } else { presentations }; Ok(Json(filtered)) } pub async fn handle_load_presentation( State(state): State>, Query(query): Query, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); match load_presentation_from_drive(&state, &user_id, &query.id).await { Ok(presentation) => Ok(Json(presentation)), Err(e) => Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )), } } pub async fn handle_save_presentation( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let presentation_id = req.id.unwrap_or_else(|| Uuid::new_v4().to_string()); let presentation = Presentation { id: presentation_id.clone(), name: req.name, owner_id: user_id.clone(), slides: req.slides, theme: req.theme, created_at: Utc::now(), updated_at: Utc::now(), }; if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } Ok(Json(SaveResponse { id: presentation_id, success: true, message: Some("Presentation saved successfully".to_string()), })) } pub async fn handle_delete_presentation( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); if let Err(e) = delete_presentation_from_drive(&state, &user_id, &req.id).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } Ok(Json(SaveResponse { id: req.id.unwrap_or_default(), success: true, message: Some("Presentation deleted".to_string()), })) } pub async fn handle_get_presentation_by_id( State(state): State>, Path(presentation_id): Path, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); match load_presentation_by_id(&state, &user_id, &presentation_id).await { Ok(presentation) => Ok(Json(presentation)), Err(e) => Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )), } } pub async fn handle_add_slide( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; let new_slide = create_slide_with_layout(&req.layout, &presentation.theme); if let Some(position) = req.position { if position <= presentation.slides.len() { presentation.slides.insert(position, new_slide); } else { presentation.slides.push(new_slide); } } else { presentation.slides.push(new_slide); } presentation.updated_at = Utc::now(); if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } Ok(Json(SaveResponse { id: req.presentation_id, success: true, message: Some("Slide added".to_string()), })) } pub async fn handle_delete_slide( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; if req.slide_index >= presentation.slides.len() { return Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Invalid slide index" })), )); } presentation.slides.remove(req.slide_index); presentation.updated_at = Utc::now(); if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } Ok(Json(SaveResponse { id: req.presentation_id, success: true, message: Some("Slide deleted".to_string()), })) } pub async fn handle_duplicate_slide( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; if req.slide_index >= presentation.slides.len() { return Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Invalid slide index" })), )); } let mut duplicated = presentation.slides[req.slide_index].clone(); duplicated.id = Uuid::new_v4().to_string(); presentation.slides.insert(req.slide_index + 1, duplicated); presentation.updated_at = Utc::now(); if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } Ok(Json(SaveResponse { id: req.presentation_id, success: true, message: Some("Slide duplicated".to_string()), })) } pub async fn handle_reorder_slides( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; if req.slide_order.len() != presentation.slides.len() { return Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Invalid slide order" })), )); } let old_slides = presentation.slides.clone(); presentation.slides = req .slide_order .iter() .filter_map(|&idx| old_slides.get(idx).cloned()) .collect(); presentation.updated_at = Utc::now(); if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } Ok(Json(SaveResponse { id: req.presentation_id, success: true, message: Some("Slides reordered".to_string()), })) } pub async fn handle_update_slide_notes( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; if req.slide_index >= presentation.slides.len() { return Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Invalid slide index" })), )); } presentation.slides[req.slide_index].notes = Some(req.notes); presentation.updated_at = Utc::now(); if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } Ok(Json(SaveResponse { id: req.presentation_id, success: true, message: Some("Slide notes updated".to_string()), })) } pub async fn handle_add_element( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; if req.slide_index >= presentation.slides.len() { return Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Invalid slide index" })), )); } presentation.slides[req.slide_index].elements.push(req.element); presentation.updated_at = Utc::now(); if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } broadcast_slide_change( &req.presentation_id, &user_id, "User", "element_added", Some(req.slide_index), None, None, ) .await; Ok(Json(SaveResponse { id: req.presentation_id, success: true, message: Some("Element added".to_string()), })) } pub async fn handle_update_element( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; if req.slide_index >= presentation.slides.len() { return Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Invalid slide index" })), )); } let slide = &mut presentation.slides[req.slide_index]; if let Some(pos) = slide.elements.iter().position(|e| e.id == req.element.id) { slide.elements[pos] = req.element.clone(); } else { slide.elements.push(req.element.clone()); } presentation.updated_at = Utc::now(); if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } broadcast_slide_change( &req.presentation_id, &user_id, "User", "element_updated", Some(req.slide_index), Some(&req.element.id), None, ) .await; Ok(Json(SaveResponse { id: req.presentation_id, success: true, message: Some("Element updated".to_string()), })) } pub async fn handle_delete_element( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; if req.slide_index >= presentation.slides.len() { return Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Invalid slide index" })), )); } presentation.slides[req.slide_index] .elements .retain(|e| e.id != req.element_id); presentation.updated_at = Utc::now(); if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } Ok(Json(SaveResponse { id: req.presentation_id, success: true, message: Some("Element deleted".to_string()), })) } pub async fn handle_apply_theme( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let user_id = get_current_user_id(); let mut presentation = match load_presentation_by_id(&state, &user_id, &req.presentation_id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; presentation.theme = req.theme; presentation.updated_at = Utc::now(); if let Err(e) = save_presentation_to_drive(&state, &user_id, &presentation).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), )); } Ok(Json(SaveResponse { id: req.presentation_id, success: true, message: Some("Theme applied".to_string()), })) } pub async fn handle_export_presentation( State(state): State>, Json(req): Json, ) -> Result)> { let user_id = get_current_user_id(); let presentation = match load_presentation_by_id(&state, &user_id, &req.id).await { Ok(p) => p, Err(e) => { return Err(( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": e })), )) } }; match req.format.as_str() { "html" => { let html = export_to_html(&presentation); Ok(([(axum::http::header::CONTENT_TYPE, "text/html")], html)) } "json" => { let json = serde_json::to_string_pretty(&presentation).unwrap_or_default(); Ok(([(axum::http::header::CONTENT_TYPE, "application/json")], json)) } "pptx" => { Ok(( [( axum::http::header::CONTENT_TYPE, "application/vnd.openxmlformats-officedocument.presentationml.presentation", )], "PPTX export not yet implemented".to_string(), )) } _ => Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Unsupported format" })), )), } }