botserver/src/docs/handlers.rs

1747 lines
55 KiB
Rust

use crate::docs::storage::{
create_new_document, delete_document_from_drive, get_current_user_id,
list_documents_from_drive, load_document_from_drive, save_document, save_document_to_drive,
};
use crate::docs::types::{
DocsSaveRequest, DocsSaveResponse, DocsAiRequest, DocsAiResponse, Document, DocumentMetadata,
SearchQuery, TemplateResponse,
};
use crate::docs::utils::{detect_document_format, html_to_markdown, markdown_to_html, rtf_to_html, strip_html};
use crate::docs::types::{
AcceptRejectAllRequest, AcceptRejectChangeRequest, AddCommentRequest, AddEndnoteRequest,
AddFootnoteRequest, ApplyStyleRequest, CompareDocumentsRequest, CompareDocumentsResponse,
CommentReply, ComparisonSummary, CreateStyleRequest, DeleteCommentRequest, DeleteEndnoteRequest,
DeleteFootnoteRequest, DeleteStyleRequest, DocumentComment, DocumentComparison, DocumentDiff,
EnableTrackChangesRequest, Endnote, Footnote, GenerateTocRequest,
GetOutlineRequest, ListCommentsResponse, ListEndnotesResponse, ListFootnotesResponse,
ListStylesResponse, ListTrackChangesResponse, OutlineItem, OutlineResponse, ReplyCommentRequest,
ResolveCommentRequest, TableOfContents, TocEntry, TocResponse, UpdateEndnoteRequest,
UpdateFootnoteRequest, UpdateStyleRequest, UpdateTocRequest,
};
use crate::shared::state::AppState;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use chrono::Utc;
use docx_rs::{AlignmentType, Docx, Paragraph, Run};
use log::error;
use std::sync::Arc;
use uuid::Uuid;
pub async fn handle_docs_ai(
State(_state): State<Arc<AppState>>,
Json(req): Json<DocsAiRequest>,
) -> impl IntoResponse {
let command = req.command.to_lowercase();
let response = if command.contains("summarize") || command.contains("summary") {
"I've created a summary of your document. The key points are highlighted above."
} else if command.contains("expand") || command.contains("longer") {
"I've expanded the selected text with more details and examples."
} else if command.contains("shorter") || command.contains("concise") {
"I've made the text more concise while preserving the key information."
} else if command.contains("formal") {
"I've rewritten the text in a more formal, professional tone."
} else if command.contains("casual") || command.contains("friendly") {
"I've rewritten the text in a more casual, friendly tone."
} else if command.contains("grammar") || command.contains("fix") {
"I've corrected the grammar and spelling errors in your text."
} else if command.contains("translate") {
"I've translated the selected text. Please specify the target language if needed."
} else if command.contains("bullet") || command.contains("list") {
"I've converted the text into a bulleted list format."
} else if command.contains("help") {
"I can help you with:\n• Summarize text\n• Expand or shorten content\n• Fix grammar\n• Change tone (formal/casual)\n• Translate text\n• Convert to bullet points"
} else {
"I understand you want help with your document. Try commands like 'summarize', 'make shorter', 'fix grammar', or 'make formal'."
};
Json(DocsAiResponse {
response: response.to_string(),
result: None,
})
}
pub async fn handle_docs_save(
State(state): State<Arc<AppState>>,
Json(req): Json<DocsSaveRequest>,
) -> Result<Json<DocsSaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc_id = req.id.unwrap_or_else(|| Uuid::new_v4().to_string());
if let Err(e) = save_document_to_drive(&state, &user_id, &doc_id, &req.title, &req.content).await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(DocsSaveResponse {
id: doc_id,
success: true,
}))
}
pub async fn handle_docs_get_by_id(
State(state): State<Arc<AppState>>,
Path(doc_id): Path<String>,
) -> Result<Json<Document>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
match load_document_from_drive(&state, &user_id, &doc_id).await {
Ok(Some(doc)) => Ok(Json(doc)),
Ok(None) => Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
)),
Err(e) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
)),
}
}
pub async fn handle_new_document(
State(_state): State<Arc<AppState>>,
) -> Result<Json<Document>, (StatusCode, Json<serde_json::Value>)> {
Ok(Json(create_new_document()))
}
pub async fn handle_list_documents(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<DocumentMetadata>>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
match list_documents_from_drive(&state, &user_id).await {
Ok(docs) => Ok(Json(docs)),
Err(e) => {
error!("Failed to list documents: {}", e);
Ok(Json(Vec::new()))
}
}
}
pub async fn handle_search_documents(
State(state): State<Arc<AppState>>,
Query(query): Query<SearchQuery>,
) -> Result<Json<Vec<DocumentMetadata>>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let docs = match list_documents_from_drive(&state, &user_id).await {
Ok(d) => d,
Err(_) => Vec::new(),
};
let filtered = if let Some(q) = query.q {
let q_lower = q.to_lowercase();
docs.into_iter()
.filter(|d| d.title.to_lowercase().contains(&q_lower))
.collect()
} else {
docs
};
Ok(Json(filtered))
}
pub async fn handle_get_document(
State(state): State<Arc<AppState>>,
Query(query): Query<crate::docs::types::LoadQuery>,
) -> Result<Json<Document>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc_id = query.id.ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Document ID required" })),
)
})?;
match load_document_from_drive(&state, &user_id, &doc_id).await {
Ok(Some(doc)) => Ok(Json(doc)),
Ok(None) => Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
)),
Err(e) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
)),
}
}
pub async fn handle_save_document(
State(state): State<Arc<AppState>>,
Json(req): Json<DocsSaveRequest>,
) -> Result<Json<DocsSaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc_id = req.id.unwrap_or_else(|| Uuid::new_v4().to_string());
if let Err(e) = save_document_to_drive(&state, &user_id, &doc_id, &req.title, &req.content).await
{
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(DocsSaveResponse {
id: doc_id,
success: true,
}))
}
pub async fn handle_autosave(
State(state): State<Arc<AppState>>,
Json(req): Json<DocsSaveRequest>,
) -> Result<Json<DocsSaveResponse>, (StatusCode, Json<serde_json::Value>)> {
handle_save_document(State(state), Json(req)).await
}
pub async fn handle_delete_document(
State(state): State<Arc<AppState>>,
Json(req): Json<crate::docs::types::LoadQuery>,
) -> Result<Json<DocsSaveResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc_id = req.id.ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "Document ID required" })),
)
})?;
if let Err(e) = delete_document_from_drive(&state, &user_id, &doc_id).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(DocsSaveResponse {
id: doc_id,
success: true,
}))
}
pub async fn handle_template_blank() -> Result<Json<TemplateResponse>, (StatusCode, Json<serde_json::Value>)> {
Ok(Json(TemplateResponse {
id: Uuid::new_v4().to_string(),
title: "Untitled Document".to_string(),
content: String::new(),
}))
}
pub async fn handle_template_meeting() -> Result<Json<TemplateResponse>, (StatusCode, Json<serde_json::Value>)> {
let content = r#"<h1>Meeting Notes</h1>
<p><strong>Date:</strong> [Date]</p>
<p><strong>Attendees:</strong> [Names]</p>
<p><strong>Location:</strong> [Location/Virtual]</p>
<hr>
<h2>Agenda</h2>
<ol>
<li>Topic 1</li>
<li>Topic 2</li>
<li>Topic 3</li>
</ol>
<h2>Discussion Points</h2>
<p>[Notes here]</p>
<h2>Action Items</h2>
<ul>
<li>[ ] Action 1 - Owner - Due Date</li>
<li>[ ] Action 2 - Owner - Due Date</li>
</ul>
<h2>Next Meeting</h2>
<p>[Date and time of next meeting]</p>"#;
Ok(Json(TemplateResponse {
id: Uuid::new_v4().to_string(),
title: "Meeting Notes".to_string(),
content: content.to_string(),
}))
}
pub async fn handle_template_report() -> Result<Json<TemplateResponse>, (StatusCode, Json<serde_json::Value>)> {
let content = r#"<h1>Report Title</h1>
<p><em>Author: [Your Name]</em></p>
<p><em>Date: [Date]</em></p>
<hr>
<h2>Executive Summary</h2>
<p>[Brief overview of the report]</p>
<h2>Introduction</h2>
<p>[Background and context]</p>
<h2>Methodology</h2>
<p>[How the information was gathered]</p>
<h2>Findings</h2>
<p>[Key findings and data]</p>
<h2>Recommendations</h2>
<ul>
<li>Recommendation 1</li>
<li>Recommendation 2</li>
<li>Recommendation 3</li>
</ul>
<h2>Conclusion</h2>
<p>[Summary and next steps]</p>"#;
Ok(Json(TemplateResponse {
id: Uuid::new_v4().to_string(),
title: "Report".to_string(),
content: content.to_string(),
}))
}
pub async fn handle_template_letter() -> Result<Json<TemplateResponse>, (StatusCode, Json<serde_json::Value>)> {
let content = r#"<p>[Your Name]<br>
[Your Address]<br>
[City, State ZIP]<br>
[Date]</p>
<p>[Recipient Name]<br>
[Recipient Title]<br>
[Company Name]<br>
[Address]<br>
[City, State ZIP]</p>
<p>Dear [Recipient Name],</p>
<p>[Opening paragraph - state the purpose of your letter]</p>
<p>[Body paragraph(s) - provide details and supporting information]</p>
<p>[Closing paragraph - summarize and state any call to action]</p>
<p>Sincerely,</p>
<p>[Your Name]<br>
[Your Title]</p>"#;
Ok(Json(TemplateResponse {
id: Uuid::new_v4().to_string(),
title: "Letter".to_string(),
content: content.to_string(),
}))
}
pub async fn handle_ai_summarize(
Json(req): Json<crate::docs::types::AiRequest>,
) -> Result<Json<crate::docs::types::AiResponse>, (StatusCode, Json<serde_json::Value>)> {
let text = req.selected_text.unwrap_or_default();
let summary = if text.len() > 200 {
format!("Summary: {}...", &text[..200])
} else {
format!("Summary: {}", text)
};
Ok(Json(crate::docs::types::AiResponse {
result: "success".to_string(),
content: summary,
error: None,
}))
}
pub async fn handle_ai_expand(
Json(req): Json<crate::docs::types::AiRequest>,
) -> Result<Json<crate::docs::types::AiResponse>, (StatusCode, Json<serde_json::Value>)> {
let text = req.selected_text.unwrap_or_default();
let expanded = format!("{}\n\n[Additional context and details would be added here by AI]", text);
Ok(Json(crate::docs::types::AiResponse {
result: "success".to_string(),
content: expanded,
error: None,
}))
}
pub async fn handle_ai_improve(
Json(req): Json<crate::docs::types::AiRequest>,
) -> Result<Json<crate::docs::types::AiResponse>, (StatusCode, Json<serde_json::Value>)> {
let text = req.selected_text.unwrap_or_default();
Ok(Json(crate::docs::types::AiResponse {
result: "success".to_string(),
content: text,
error: None,
}))
}
pub async fn handle_ai_simplify(
Json(req): Json<crate::docs::types::AiRequest>,
) -> Result<Json<crate::docs::types::AiResponse>, (StatusCode, Json<serde_json::Value>)> {
let text = req.selected_text.unwrap_or_default();
Ok(Json(crate::docs::types::AiResponse {
result: "success".to_string(),
content: text,
error: None,
}))
}
pub async fn handle_ai_translate(
Json(req): Json<crate::docs::types::AiRequest>,
) -> Result<Json<crate::docs::types::AiResponse>, (StatusCode, Json<serde_json::Value>)> {
let text = req.selected_text.unwrap_or_default();
let lang = req.translate_lang.unwrap_or_else(|| "English".to_string());
Ok(Json(crate::docs::types::AiResponse {
result: "success".to_string(),
content: format!("[Translated to {}]: {}", lang, text),
error: None,
}))
}
pub async fn handle_ai_custom(
Json(req): Json<crate::docs::types::AiRequest>,
) -> Result<Json<crate::docs::types::AiResponse>, (StatusCode, Json<serde_json::Value>)> {
let text = req.selected_text.unwrap_or_default();
Ok(Json(crate::docs::types::AiResponse {
result: "success".to_string(),
content: text,
error: None,
}))
}
pub async fn handle_export_pdf(
State(_state): State<Arc<AppState>>,
Query(_query): Query<crate::docs::types::ExportQuery>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
Ok((
[(axum::http::header::CONTENT_TYPE, "application/pdf")],
"PDF export not yet implemented".to_string(),
))
}
pub async fn handle_export_docx(
State(state): State<Arc<AppState>>,
Query(query): Query<crate::docs::types::ExportQuery>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &query.id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let docx_bytes = html_to_docx(&doc.content, &doc.title);
Ok((
[(
axum::http::header::CONTENT_TYPE,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)],
docx_bytes,
))
}
fn html_to_docx(html: &str, title: &str) -> Vec<u8> {
let plain_text = strip_html(html);
let paragraphs: Vec<&str> = plain_text.split("\n\n").collect();
let mut docx = Docx::new();
let title_para = Paragraph::new()
.add_run(Run::new().add_text(title).bold())
.align(AlignmentType::Center);
docx = docx.add_paragraph(title_para);
for para_text in paragraphs {
if !para_text.trim().is_empty() {
let para = Paragraph::new().add_run(Run::new().add_text(para_text.trim()));
docx = docx.add_paragraph(para);
}
}
let mut buffer = Vec::new();
if let Ok(_) = docx.build().pack(&mut std::io::Cursor::new(&mut buffer)) {
buffer
} else {
Vec::new()
}
}
pub async fn handle_export_md(
State(state): State<Arc<AppState>>,
Query(query): Query<crate::docs::types::ExportQuery>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &query.id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let markdown = html_to_markdown(&doc.content);
Ok(([(axum::http::header::CONTENT_TYPE, "text/markdown")], markdown))
}
pub async fn handle_export_html(
State(state): State<Arc<AppState>>,
Query(query): Query<crate::docs::types::ExportQuery>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &query.id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let full_html = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{}</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }}
</style>
</head>
<body>
{}
</body>
</html>"#,
doc.title, doc.content
);
Ok(([(axum::http::header::CONTENT_TYPE, "text/html")], full_html))
}
pub async fn handle_export_txt(
State(state): State<Arc<AppState>>,
Query(query): Query<crate::docs::types::ExportQuery>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &query.id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let plain_text = strip_html(&doc.content);
Ok(([(axum::http::header::CONTENT_TYPE, "text/plain")], plain_text))
}
pub async fn handle_add_comment(
State(state): State<Arc<AppState>>,
Json(req): Json<AddCommentRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let comment = DocumentComment {
id: uuid::Uuid::new_v4().to_string(),
author_id: user_id.clone(),
author_name: "User".to_string(),
content: req.content,
position: req.position,
length: req.length,
created_at: Utc::now(),
updated_at: Utc::now(),
replies: vec![],
resolved: false,
};
let comments = doc.comments.get_or_insert_with(Vec::new);
comments.push(comment.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true, "comment": comment })))
}
pub async fn handle_reply_comment(
State(state): State<Arc<AppState>>,
Json(req): Json<ReplyCommentRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(comments) = &mut doc.comments {
for comment in comments.iter_mut() {
if comment.id == req.comment_id {
let reply = CommentReply {
id: uuid::Uuid::new_v4().to_string(),
author_id: user_id.clone(),
author_name: "User".to_string(),
content: req.content.clone(),
created_at: Utc::now(),
};
comment.replies.push(reply);
comment.updated_at = Utc::now();
break;
}
}
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_resolve_comment(
State(state): State<Arc<AppState>>,
Json(req): Json<ResolveCommentRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(comments) = &mut doc.comments {
for comment in comments.iter_mut() {
if comment.id == req.comment_id {
comment.resolved = req.resolved;
comment.updated_at = Utc::now();
break;
}
}
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_delete_comment(
State(state): State<Arc<AppState>>,
Json(req): Json<DeleteCommentRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(comments) = &mut doc.comments {
comments.retain(|c| c.id != req.comment_id);
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_list_comments(
State(state): State<Arc<AppState>>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<ListCommentsResponse>, (StatusCode, Json<serde_json::Value>)> {
let doc_id = params.get("doc_id").cloned().unwrap_or_default();
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let comments = doc.comments.unwrap_or_default();
Ok(Json(ListCommentsResponse { comments }))
}
pub async fn handle_enable_track_changes(
State(state): State<Arc<AppState>>,
Json(req): Json<EnableTrackChangesRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
doc.track_changes_enabled = req.enabled;
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true, "enabled": req.enabled })))
}
pub async fn handle_accept_reject_change(
State(state): State<Arc<AppState>>,
Json(req): Json<AcceptRejectChangeRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(changes) = &mut doc.track_changes {
for change in changes.iter_mut() {
if change.id == req.change_id {
change.accepted = Some(req.accept);
break;
}
}
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_accept_reject_all(
State(state): State<Arc<AppState>>,
Json(req): Json<AcceptRejectAllRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(changes) = &mut doc.track_changes {
for change in changes.iter_mut() {
change.accepted = Some(req.accept);
}
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_list_track_changes(
State(state): State<Arc<AppState>>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<ListTrackChangesResponse>, (StatusCode, Json<serde_json::Value>)> {
let doc_id = params.get("doc_id").cloned().unwrap_or_default();
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let changes = doc.track_changes.unwrap_or_default();
Ok(Json(ListTrackChangesResponse {
changes,
enabled: doc.track_changes_enabled,
}))
}
pub async fn handle_generate_toc(
State(state): State<Arc<AppState>>,
Json(req): Json<GenerateTocRequest>,
) -> Result<Json<TocResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let mut entries = Vec::new();
let content = &doc.content;
for level in 1..=req.max_level {
let tag = format!("<h{level}>");
let end_tag = format!("</h{level}>");
let mut search_pos = 0;
while let Some(start) = content[search_pos..].find(&tag) {
let abs_start = search_pos + start;
if let Some(end) = content[abs_start..].find(&end_tag) {
let text_start = abs_start + tag.len();
let text_end = abs_start + end;
let text = strip_html(&content[text_start..text_end]);
entries.push(TocEntry {
id: uuid::Uuid::new_v4().to_string(),
text,
level,
page_number: None,
position: abs_start,
});
search_pos = text_end + end_tag.len();
} else {
break;
}
}
}
entries.sort_by_key(|e| e.position);
let toc = TableOfContents {
id: uuid::Uuid::new_v4().to_string(),
title: "Table of Contents".to_string(),
entries,
max_level: req.max_level,
show_page_numbers: req.show_page_numbers,
use_hyperlinks: req.use_hyperlinks,
};
doc.toc = Some(toc.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(TocResponse { toc }))
}
pub async fn handle_update_toc(
State(state): State<Arc<AppState>>,
Json(req): Json<UpdateTocRequest>,
) -> Result<Json<TocResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let existing_toc = doc.toc.unwrap_or_else(|| TableOfContents {
id: uuid::Uuid::new_v4().to_string(),
title: "Table of Contents".to_string(),
entries: vec![],
max_level: 3,
show_page_numbers: true,
use_hyperlinks: true,
});
let gen_req = GenerateTocRequest {
doc_id: req.doc_id,
max_level: existing_toc.max_level,
show_page_numbers: existing_toc.show_page_numbers,
use_hyperlinks: existing_toc.use_hyperlinks,
};
handle_generate_toc(State(state), Json(gen_req)).await
}
pub async fn handle_add_footnote(
State(state): State<Arc<AppState>>,
Json(req): Json<AddFootnoteRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let footnotes = doc.footnotes.get_or_insert_with(Vec::new);
let reference_mark = format!("{}", footnotes.len() + 1);
let footnote = Footnote {
id: uuid::Uuid::new_v4().to_string(),
reference_mark,
content: req.content,
position: req.position,
};
footnotes.push(footnote.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true, "footnote": footnote })))
}
pub async fn handle_update_footnote(
State(state): State<Arc<AppState>>,
Json(req): Json<UpdateFootnoteRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(footnotes) = &mut doc.footnotes {
for footnote in footnotes.iter_mut() {
if footnote.id == req.footnote_id {
footnote.content = req.content.clone();
break;
}
}
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_delete_footnote(
State(state): State<Arc<AppState>>,
Json(req): Json<DeleteFootnoteRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(footnotes) = &mut doc.footnotes {
footnotes.retain(|f| f.id != req.footnote_id);
for (i, footnote) in footnotes.iter_mut().enumerate() {
footnote.reference_mark = format!("{}", i + 1);
}
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_list_footnotes(
State(state): State<Arc<AppState>>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<ListFootnotesResponse>, (StatusCode, Json<serde_json::Value>)> {
let doc_id = params.get("doc_id").cloned().unwrap_or_default();
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let footnotes = doc.footnotes.unwrap_or_default();
Ok(Json(ListFootnotesResponse { footnotes }))
}
pub async fn handle_add_endnote(
State(state): State<Arc<AppState>>,
Json(req): Json<AddEndnoteRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let endnotes = doc.endnotes.get_or_insert_with(Vec::new);
let reference_mark = to_roman_numeral(endnotes.len() + 1);
let endnote = Endnote {
id: uuid::Uuid::new_v4().to_string(),
reference_mark,
content: req.content,
position: req.position,
};
endnotes.push(endnote.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true, "endnote": endnote })))
}
fn to_roman_numeral(num: usize) -> String {
let numerals = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
];
let mut result = String::new();
let mut n = num;
for (value, numeral) in numerals {
while n >= value {
result.push_str(numeral);
n -= value;
}
}
result
}
pub async fn handle_update_endnote(
State(state): State<Arc<AppState>>,
Json(req): Json<UpdateEndnoteRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(endnotes) = &mut doc.endnotes {
for endnote in endnotes.iter_mut() {
if endnote.id == req.endnote_id {
endnote.content = req.content.clone();
break;
}
}
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_delete_endnote(
State(state): State<Arc<AppState>>,
Json(req): Json<DeleteEndnoteRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(endnotes) = &mut doc.endnotes {
endnotes.retain(|e| e.id != req.endnote_id);
for (i, endnote) in endnotes.iter_mut().enumerate() {
endnote.reference_mark = to_roman_numeral(i + 1);
}
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_list_endnotes(
State(state): State<Arc<AppState>>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<ListEndnotesResponse>, (StatusCode, Json<serde_json::Value>)> {
let doc_id = params.get("doc_id").cloned().unwrap_or_default();
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let endnotes = doc.endnotes.unwrap_or_default();
Ok(Json(ListEndnotesResponse { endnotes }))
}
pub async fn handle_create_style(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateStyleRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let styles = doc.styles.get_or_insert_with(Vec::new);
styles.push(req.style.clone());
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true, "style": req.style })))
}
pub async fn handle_update_style(
State(state): State<Arc<AppState>>,
Json(req): Json<UpdateStyleRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(styles) = &mut doc.styles {
for style in styles.iter_mut() {
if style.id == req.style.id {
*style = req.style.clone();
break;
}
}
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_delete_style(
State(state): State<Arc<AppState>>,
Json(req): Json<DeleteStyleRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let mut doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
if let Some(styles) = &mut doc.styles {
styles.retain(|s| s.id != req.style_id);
}
doc.updated_at = Utc::now();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(serde_json::json!({ "success": true })))
}
pub async fn handle_list_styles(
State(state): State<Arc<AppState>>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<ListStylesResponse>, (StatusCode, Json<serde_json::Value>)> {
let doc_id = params.get("doc_id").cloned().unwrap_or_default();
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let styles = doc.styles.unwrap_or_default();
Ok(Json(ListStylesResponse { styles }))
}
pub async fn handle_apply_style(
State(state): State<Arc<AppState>>,
Json(req): Json<ApplyStyleRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let style = doc.styles
.as_ref()
.and_then(|styles| styles.iter().find(|s| s.id == req.style_id))
.cloned();
if style.is_none() {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Style not found" })),
));
}
Ok(Json(serde_json::json!({
"success": true,
"style": style,
"position": req.position,
"length": req.length
})))
}
pub async fn handle_get_outline(
State(state): State<Arc<AppState>>,
Json(req): Json<GetOutlineRequest>,
) -> Result<Json<OutlineResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let doc = match load_document_from_drive(&state, &user_id, &req.doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let mut items = Vec::new();
let content = &doc.content;
for level in 1..=6u32 {
let tag = format!("<h{level}>");
let end_tag = format!("</h{level}>");
let mut search_pos = 0;
while let Some(start) = content[search_pos..].find(&tag) {
let abs_start = search_pos + start;
if let Some(end) = content[abs_start..].find(&end_tag) {
let text_start = abs_start + tag.len();
let text_end = abs_start + end;
let text = strip_html(&content[text_start..text_end]);
let length = text_end - text_start;
items.push(OutlineItem {
id: uuid::Uuid::new_v4().to_string(),
text,
level,
position: abs_start,
length,
style_name: format!("Heading {level}"),
});
search_pos = text_end + end_tag.len();
} else {
break;
}
}
}
items.sort_by_key(|i| i.position);
Ok(Json(OutlineResponse { items }))
}
pub async fn handle_import_document(
State(state): State<Arc<AppState>>,
mut multipart: axum::extract::Multipart,
) -> Result<Json<Document>, (StatusCode, Json<serde_json::Value>)> {
let mut file_bytes: Option<Vec<u8>> = None;
let mut filename = "import.docx".to_string();
while let Ok(Some(field)) = multipart.next_field().await {
if field.name() == Some("file") {
filename = field.file_name().unwrap_or("import.docx").to_string();
if let Ok(bytes) = field.bytes().await {
file_bytes = Some(bytes.to_vec());
}
}
}
let bytes = file_bytes.ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "No file uploaded" })),
)
})?;
let format = detect_document_format(&bytes);
let content = match format {
"rtf" => rtf_to_html(&String::from_utf8_lossy(&bytes)),
"html" => String::from_utf8_lossy(&bytes).to_string(),
"markdown" => markdown_to_html(&String::from_utf8_lossy(&bytes)),
"txt" => {
let text = String::from_utf8_lossy(&bytes);
format!("<p>{}</p>", text.replace('\n', "</p><p>"))
}
_ => {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": format!("Unsupported format: {}", format) })),
))
}
};
let title = filename.rsplit('/').next().unwrap_or(&filename)
.rsplit('.').last().unwrap_or(&filename)
.to_string();
let user_id = get_current_user_id();
let mut doc = create_new_document();
doc.title = title;
doc.content = content;
doc.owner_id = user_id.clone();
if let Err(e) = save_document(&state, &user_id, &doc).await {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
));
}
Ok(Json(doc))
}
pub async fn handle_compare_documents(
State(state): State<Arc<AppState>>,
Json(req): Json<CompareDocumentsRequest>,
) -> Result<Json<CompareDocumentsResponse>, (StatusCode, Json<serde_json::Value>)> {
let user_id = get_current_user_id();
let original = match load_document_from_drive(&state, &user_id, &req.original_doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Original document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let modified = match load_document_from_drive(&state, &user_id, &req.modified_doc_id).await {
Ok(Some(d)) => d,
Ok(None) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Modified document not found" })),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e })),
))
}
};
let original_text = strip_html(&original.content);
let modified_text = strip_html(&modified.content);
let mut differences = Vec::new();
let mut insertions = 0u32;
let mut deletions = 0u32;
let mut modifications = 0u32;
let original_words: Vec<&str> = original_text.split_whitespace().collect();
let modified_words: Vec<&str> = modified_text.split_whitespace().collect();
let mut i = 0;
let mut j = 0;
let mut position = 0;
while i < original_words.len() || j < modified_words.len() {
if i >= original_words.len() {
differences.push(DocumentDiff {
diff_type: "insertion".to_string(),
position,
original_text: None,
modified_text: Some(modified_words[j].to_string()),
length: modified_words[j].len(),
});
insertions += 1;
j += 1;
} else if j >= modified_words.len() {
differences.push(DocumentDiff {
diff_type: "deletion".to_string(),
position,
original_text: Some(original_words[i].to_string()),
modified_text: None,
length: original_words[i].len(),
});
deletions += 1;
i += 1;
} else if original_words[i] == modified_words[j] {
position += original_words[i].len() + 1;
i += 1;
j += 1;
} else {
differences.push(DocumentDiff {
diff_type: "modification".to_string(),
position,
original_text: Some(original_words[i].to_string()),
modified_text: Some(modified_words[j].to_string()),
length: original_words[i].len().max(modified_words[j].len()),
});
modifications += 1;
position += modified_words[j].len() + 1;
i += 1;
j += 1;
}
}
let comparison = DocumentComparison {
id: uuid::Uuid::new_v4().to_string(),
original_doc_id: req.original_doc_id,
modified_doc_id: req.modified_doc_id,
created_at: Utc::now(),
differences,
summary: ComparisonSummary {
insertions,
deletions,
modifications,
total_changes: insertions + deletions + modifications,
},
};
Ok(Json(CompareDocumentsResponse { comparison }))
}