use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; pub mod blocks; pub mod pages; pub mod collaboration; pub mod templates; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Workspace { pub id: Uuid, pub organization_id: Uuid, pub name: String, pub description: Option, pub icon: Option, pub cover_image: Option, pub members: Vec, pub settings: WorkspaceSettings, pub root_pages: Vec, pub created_at: DateTime, pub updated_at: DateTime, pub created_by: Uuid, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkspaceIcon { pub icon_type: IconType, pub value: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum IconType { Emoji, Image, Lucide, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkspaceMember { pub user_id: Uuid, pub role: WorkspaceRole, pub joined_at: DateTime, pub invited_by: Option, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum WorkspaceRole { Owner, Admin, Editor, Commenter, Viewer, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkspaceSettings { pub default_page_width: PageWidth, pub allow_public_pages: bool, pub enable_comments: bool, pub enable_reactions: bool, pub enable_gb_assist: bool, pub gb_bot_id: Option, } impl Default for WorkspaceSettings { fn default() -> Self { Self { default_page_width: PageWidth::Normal, allow_public_pages: false, enable_comments: true, enable_reactions: true, enable_gb_assist: true, gb_bot_id: None, } } } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum PageWidth { Small, Normal, Wide, Full, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Page { pub id: Uuid, pub workspace_id: Uuid, pub parent_id: Option, pub title: String, pub icon: Option, pub cover_image: Option, pub blocks: Vec, pub children: Vec, pub properties: HashMap, pub permissions: PagePermissions, pub is_template: bool, pub template_id: Option, pub created_at: DateTime, pub updated_at: DateTime, pub created_by: Uuid, pub last_edited_by: Uuid, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PagePermissions { pub inherit_from_parent: bool, pub public: bool, pub public_edit: bool, pub allowed_users: Vec, pub allowed_roles: Vec, } impl Default for PagePermissions { fn default() -> Self { Self { inherit_from_parent: true, public: false, public_edit: false, allowed_users: Vec::new(), allowed_roles: Vec::new(), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Block { pub id: Uuid, pub block_type: BlockType, pub content: BlockContent, pub properties: BlockProperties, pub children: Vec, pub created_at: DateTime, pub updated_at: DateTime, pub created_by: Uuid, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum BlockType { Paragraph, Heading1, Heading2, Heading3, BulletedList, NumberedList, Checklist, Toggle, Quote, Callout, Divider, Table, Code, Image, Video, File, Embed, Bookmark, LinkToPage, SyncedBlock, TableOfContents, Breadcrumb, Equation, ColumnList, Column, GbComponent, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum BlockContent { Text(RichText), Media(MediaContent), Table(TableContent), Code(CodeContent), Embed(EmbedContent), Callout(CalloutContent), Toggle(ToggleContent), Checklist(ChecklistContent), GbComponent(GbComponentContent), Empty, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RichText { pub segments: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TextSegment { pub text: String, pub annotations: TextAnnotations, pub link: Option, pub mention: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct TextAnnotations { pub bold: bool, pub italic: bool, pub underline: bool, pub strikethrough: bool, pub code: bool, pub color: Option, pub background_color: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Mention { pub mention_type: MentionType, pub target_id: Uuid, pub display_text: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum MentionType { User, Page, Date, Database, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MediaContent { pub url: String, pub caption: Option, pub alt_text: Option, pub width: Option, pub height: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TableContent { pub rows: Vec, pub has_header_row: bool, pub has_header_column: bool, pub column_widths: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TableRow { pub id: Uuid, pub cells: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TableCell { pub content: RichText, pub background_color: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CodeContent { pub code: String, pub language: String, pub caption: Option, pub wrap: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmbedContent { pub url: String, pub embed_type: EmbedType, pub caption: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum EmbedType { Youtube, Vimeo, Figma, GoogleDrive, GoogleMaps, Twitter, Github, Codepen, Generic, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CalloutContent { pub icon: WorkspaceIcon, pub text: RichText, pub background_color: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToggleContent { pub title: RichText, pub expanded: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChecklistContent { pub items: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChecklistItem { pub id: Uuid, pub text: RichText, pub checked: bool, pub assignee: Option, pub due_date: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GbComponentContent { pub component_type: GbComponentType, pub bot_id: Option, pub config: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum GbComponentType { AskGb, SummarizePage, CreateContent, TranslateBlock, FormEmbed, DataTable, Chart, KbSearch, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct BlockProperties { pub color: Option, pub background_color: Option, pub indent_level: u8, pub collapsed: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum PropertyValue { Text(String), Number(f64), Boolean(bool), Date(DateTime), Select(String), MultiSelect(Vec), User(Uuid), Users(Vec), Url(String), Email(String), Phone(String), Relation(Vec), } pub struct WorkspacesService { workspaces: Arc>>, pages: Arc>>, page_versions: Arc>>>, comments: Arc>>>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageVersion { pub id: Uuid, pub page_id: Uuid, pub version_number: u32, pub title: String, pub blocks: Vec, pub created_at: DateTime, pub created_by: Uuid, pub change_summary: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Comment { pub id: Uuid, pub page_id: Uuid, pub block_id: Option, pub parent_comment_id: Option, pub author_id: Uuid, pub content: RichText, pub resolved: bool, pub resolved_by: Option, pub resolved_at: Option>, pub reactions: Vec, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Reaction { pub emoji: String, pub user_id: Uuid, pub created_at: DateTime, } impl WorkspacesService { pub fn new() -> Self { Self { workspaces: Arc::new(RwLock::new(HashMap::new())), pages: Arc::new(RwLock::new(HashMap::new())), page_versions: Arc::new(RwLock::new(HashMap::new())), comments: Arc::new(RwLock::new(HashMap::new())), } } pub async fn create_workspace( &self, organization_id: Uuid, name: &str, created_by: Uuid, ) -> Result { let now = Utc::now(); let workspace = Workspace { id: Uuid::new_v4(), organization_id, name: name.to_string(), description: None, icon: None, cover_image: None, members: vec![WorkspaceMember { user_id: created_by, role: WorkspaceRole::Owner, joined_at: now, invited_by: None, }], settings: WorkspaceSettings::default(), root_pages: Vec::new(), created_at: now, updated_at: now, created_by, }; let mut workspaces = self.workspaces.write().await; workspaces.insert(workspace.id, workspace.clone()); Ok(workspace) } pub async fn get_workspace(&self, workspace_id: Uuid) -> Option { let workspaces = self.workspaces.read().await; workspaces.get(&workspace_id).cloned() } pub async fn list_workspaces(&self, organization_id: Uuid) -> Vec { let workspaces = self.workspaces.read().await; workspaces .values() .filter(|w| w.organization_id == organization_id) .cloned() .collect() } pub async fn list_user_workspaces(&self, user_id: Uuid) -> Vec { let workspaces = self.workspaces.read().await; workspaces .values() .filter(|w| w.members.iter().any(|m| m.user_id == user_id)) .cloned() .collect() } pub async fn update_workspace( &self, workspace_id: Uuid, name: Option, description: Option, icon: Option, ) -> Result { let mut workspaces = self.workspaces.write().await; let workspace = workspaces .get_mut(&workspace_id) .ok_or(WorkspacesError::WorkspaceNotFound)?; if let Some(n) = name { workspace.name = n; } if description.is_some() { workspace.description = description; } if icon.is_some() { workspace.icon = icon; } workspace.updated_at = Utc::now(); Ok(workspace.clone()) } pub async fn delete_workspace(&self, workspace_id: Uuid) -> Result<(), WorkspacesError> { let mut workspaces = self.workspaces.write().await; workspaces .remove(&workspace_id) .ok_or(WorkspacesError::WorkspaceNotFound)?; let mut pages = self.pages.write().await; pages.retain(|_, p| p.workspace_id != workspace_id); Ok(()) } pub async fn add_member( &self, workspace_id: Uuid, user_id: Uuid, role: WorkspaceRole, invited_by: Uuid, ) -> Result<(), WorkspacesError> { let mut workspaces = self.workspaces.write().await; let workspace = workspaces .get_mut(&workspace_id) .ok_or(WorkspacesError::WorkspaceNotFound)?; if workspace.members.iter().any(|m| m.user_id == user_id) { return Err(WorkspacesError::MemberAlreadyExists); } workspace.members.push(WorkspaceMember { user_id, role, joined_at: Utc::now(), invited_by: Some(invited_by), }); workspace.updated_at = Utc::now(); Ok(()) } pub async fn remove_member( &self, workspace_id: Uuid, user_id: Uuid, ) -> Result<(), WorkspacesError> { let mut workspaces = self.workspaces.write().await; let workspace = workspaces .get_mut(&workspace_id) .ok_or(WorkspacesError::WorkspaceNotFound)?; let owner_count = workspace .members .iter() .filter(|m| m.role == WorkspaceRole::Owner) .count(); if let Some(member) = workspace.members.iter().find(|m| m.user_id == user_id) { if member.role == WorkspaceRole::Owner && owner_count <= 1 { return Err(WorkspacesError::CannotRemoveLastOwner); } } workspace.members.retain(|m| m.user_id != user_id); workspace.updated_at = Utc::now(); Ok(()) } pub async fn update_member_role( &self, workspace_id: Uuid, user_id: Uuid, new_role: WorkspaceRole, ) -> Result<(), WorkspacesError> { let mut workspaces = self.workspaces.write().await; let workspace = workspaces .get_mut(&workspace_id) .ok_or(WorkspacesError::WorkspaceNotFound)?; let member = workspace .members .iter_mut() .find(|m| m.user_id == user_id) .ok_or(WorkspacesError::MemberNotFound)?; member.role = new_role; workspace.updated_at = Utc::now(); Ok(()) } pub async fn create_page( &self, workspace_id: Uuid, parent_id: Option, title: &str, created_by: Uuid, ) -> Result { let workspaces = self.workspaces.read().await; if !workspaces.contains_key(&workspace_id) { return Err(WorkspacesError::WorkspaceNotFound); } drop(workspaces); let now = Utc::now(); let page = Page { id: Uuid::new_v4(), workspace_id, parent_id, title: title.to_string(), icon: None, cover_image: None, blocks: Vec::new(), children: Vec::new(), properties: HashMap::new(), permissions: PagePermissions::default(), is_template: false, template_id: None, created_at: now, updated_at: now, created_by, last_edited_by: created_by, }; let mut pages = self.pages.write().await; pages.insert(page.id, page.clone()); drop(pages); if let Some(pid) = parent_id { let mut pages = self.pages.write().await; if let Some(parent) = pages.get_mut(&pid) { parent.children.push(page.id); parent.updated_at = Utc::now(); } } else { let mut workspaces = self.workspaces.write().await; if let Some(workspace) = workspaces.get_mut(&workspace_id) { workspace.root_pages.push(page.id); workspace.updated_at = Utc::now(); } } Ok(page) } pub async fn get_page(&self, page_id: Uuid) -> Option { let pages = self.pages.read().await; pages.get(&page_id).cloned() } pub async fn get_page_tree(&self, workspace_id: Uuid) -> Vec { let workspaces = self.workspaces.read().await; let workspace = match workspaces.get(&workspace_id) { Some(w) => w, None => return Vec::new(), }; let pages = self.pages.read().await; let mut tree = Vec::new(); for page_id in &workspace.root_pages { if let Some(node) = self.build_page_tree_node(*page_id, &pages) { tree.push(node); } } tree } fn build_page_tree_node( &self, page_id: Uuid, pages: &HashMap, ) -> Option { let page = pages.get(&page_id)?; let children: Vec = page .children .iter() .filter_map(|child_id| self.build_page_tree_node(*child_id, pages)) .collect(); Some(PageTreeNode { id: page.id, title: page.title.clone(), icon: page.icon.clone(), children, has_children: !page.children.is_empty(), }) } pub async fn update_page( &self, page_id: Uuid, title: Option, icon: Option, cover_image: Option, edited_by: Uuid, ) -> Result { let mut pages = self.pages.write().await; let page = pages .get_mut(&page_id) .ok_or(WorkspacesError::PageNotFound)?; if let Some(t) = title { page.title = t; } if icon.is_some() { page.icon = icon; } if cover_image.is_some() { page.cover_image = cover_image; } page.updated_at = Utc::now(); page.last_edited_by = edited_by; Ok(page.clone()) } pub async fn update_page_blocks( &self, page_id: Uuid, blocks: Vec, edited_by: Uuid, ) -> Result { let old_page = { let pages = self.pages.read().await; pages.get(&page_id).cloned() }; if let Some(old) = old_page { self.save_page_version(&old).await; } let mut pages = self.pages.write().await; let page = pages .get_mut(&page_id) .ok_or(WorkspacesError::PageNotFound)?; page.blocks = blocks; page.updated_at = Utc::now(); page.last_edited_by = edited_by; Ok(page.clone()) } async fn save_page_version(&self, page: &Page) { let mut versions = self.page_versions.write().await; let page_versions = versions.entry(page.id).or_default(); let version_number = page_versions.len() as u32 + 1; let version = PageVersion { id: Uuid::new_v4(), page_id: page.id, version_number, title: page.title.clone(), blocks: page.blocks.clone(), created_at: Utc::now(), created_by: page.last_edited_by, change_summary: None, }; page_versions.push(version); if page_versions.len() > 100 { page_versions.remove(0); } } pub async fn get_page_versions(&self, page_id: Uuid) -> Vec { let versions = self.page_versions.read().await; versions.get(&page_id).cloned().unwrap_or_default() } pub async fn restore_page_version( &self, page_id: Uuid, version_id: Uuid, restored_by: Uuid, ) -> Result { let version = { let versions = self.page_versions.read().await; versions .get(&page_id) .and_then(|v| v.iter().find(|pv| pv.id == version_id).cloned()) }; let version = version.ok_or(WorkspacesError::VersionNotFound)?; self.update_page_blocks(page_id, version.blocks, restored_by) .await } pub async fn delete_page(&self, page_id: Uuid) -> Result<(), WorkspacesError> { let page = { let pages = self.pages.read().await; pages.get(&page_id).cloned() }; let page = page.ok_or(WorkspacesError::PageNotFound)?; for child_id in &page.children { let _ = Box::pin(self.delete_page(*child_id)).await; } let mut pages = self.pages.write().await; pages.remove(&page_id); drop(pages); if let Some(parent_id) = page.parent_id { let mut pages = self.pages.write().await; if let Some(parent) = pages.get_mut(&parent_id) { parent.children.retain(|id| *id != page_id); } } else { let mut workspaces = self.workspaces.write().await; if let Some(workspace) = workspaces.get_mut(&page.workspace_id) { workspace.root_pages.retain(|id| *id != page_id); } } let mut versions = self.page_versions.write().await; versions.remove(&page_id); let mut comments = self.comments.write().await; comments.remove(&page_id); Ok(()) } pub async fn move_page( &self, page_id: Uuid, new_parent_id: Option, new_workspace_id: Option, ) -> Result { let mut pages = self.pages.write().await; let page = pages .get_mut(&page_id) .ok_or(WorkspacesError::PageNotFound)?; let old_parent_id = page.parent_id; let old_workspace_id = page.workspace_id; page.parent_id = new_parent_id; if let Some(ws_id) = new_workspace_id { page.workspace_id = ws_id; } page.updated_at = Utc::now(); let page_clone = page.clone(); drop(pages); if let Some(old_pid) = old_parent_id { let mut pages = self.pages.write().await; if let Some(old_parent) = pages.get_mut(&old_pid) { old_parent.children.retain(|id| *id != page_id); } } else { let mut workspaces = self.workspaces.write().await; if let Some(workspace) = workspaces.get_mut(&old_workspace_id) { workspace.root_pages.retain(|id| *id != page_id); } } if let Some(new_pid) = new_parent_id { let mut pages = self.pages.write().await; if let Some(new_parent) = pages.get_mut(&new_pid) { if !new_parent.children.contains(&page_id) { new_parent.children.push(page_id); } } } else { let ws_id = new_workspace_id.unwrap_or(old_workspace_id); let mut workspaces = self.workspaces.write().await; if let Some(workspace) = workspaces.get_mut(&ws_id) { if !workspace.root_pages.contains(&page_id) { workspace.root_pages.push(page_id); } } } Ok(page_clone) } pub async fn add_comment( &self, page_id: Uuid, block_id: Option, author_id: Uuid, content: RichText, parent_comment_id: Option, ) -> Result { let pages = self.pages.read().await; if !pages.contains_key(&page_id) { return Err(WorkspacesError::PageNotFound); } drop(pages); let now = Utc::now(); let comment = Comment { id: Uuid::new_v4(), page_id, block_id, parent_comment_id, author_id, content, resolved: false, resolved_by: None, resolved_at: None, reactions: Vec::new(), created_at: now, updated_at: now, }; let mut comments = self.comments.write().await; comments.entry(page_id).or_default().push(comment.clone()); Ok(comment) } pub async fn get_page_comments(&self, page_id: Uuid) -> Vec { let comments = self.comments.read().await; comments.get(&page_id).cloned().unwrap_or_default() } pub async fn resolve_comment( &self, page_id: Uuid, comment_id: Uuid, resolved_by: Uuid, ) -> Result { let mut comments = self.comments.write().await; let page_comments = comments .get_mut(&page_id) .ok_or(WorkspacesError::CommentNotFound)?; let comment = page_comments .iter_mut() .find(|c| c.id == comment_id) .ok_or(WorkspacesError::CommentNotFound)?; comment.resolved = true; comment.resolved_by = Some(resolved_by); comment.resolved_at = Some(Utc::now()); comment.updated_at = Utc::now(); Ok(comment.clone()) } pub async fn add_reaction( &self, page_id: Uuid, comment_id: Uuid, user_id: Uuid, emoji: &str, ) -> Result<(), WorkspacesError> { let mut comments = self.comments.write().await; let page_comments = comments .get_mut(&page_id) .ok_or(WorkspacesError::CommentNotFound)?; let comment = page_comments .iter_mut() .find(|c| c.id == comment_id) .ok_or(WorkspacesError::CommentNotFound)?; if comment.reactions.iter().any(|r| r.user_id == user_id && r.emoji == emoji) { return Ok(()); } comment.reactions.push(Reaction { emoji: emoji.to_string(), user_id, created_at: Utc::now(), }); Ok(()) } pub async fn search_pages(&self, workspace_id: Uuid, query: &str) -> Vec { let pages = self.pages.read().await; let query_lower = query.to_lowercase(); pages .values() .filter(|p| p.workspace_id == workspace_id) .filter(|p| { p.title.to_lowercase().contains(&query_lower) || self.blocks_contain_text(&p.blocks, &query_lower) }) .map(|p| PageSearchResult { page_id: p.id, title: p.title.clone(), icon: p.icon.clone(), snippet: self.extract_snippet(&p.blocks, &query_lower), updated_at: p.updated_at, }) .collect() } fn blocks_contain_text(&self, blocks: &[Block], query: &str) -> bool { for block in blocks { if let BlockContent::Text(rich_text) = &block.content { for segment in &rich_text.segments { if segment.text.to_lowercase().contains(query) { return true; } } } if self.blocks_contain_text(&block.children, query) { return true; } } false } fn extract_snippet(&self, blocks: &[Block], query: &str) -> Option { for block in blocks { if let BlockContent::Text(rich_text) = &block.content { let full_text: String = rich_text.segments.iter().map(|s| s.text.as_str()).collect(); if full_text.to_lowercase().contains(query) { let max_len = 150; if full_text.len() <= max_len { return Some(full_text); } return Some(format!("{}...", &full_text[..max_len])); } } } None } } impl Default for WorkspacesService { fn default() -> Self { Self::new() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageTreeNode { pub id: Uuid, pub title: String, pub icon: Option, pub children: Vec, pub has_children: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageSearchResult { pub page_id: Uuid, pub title: String, pub icon: Option, pub snippet: Option, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SlashCommand { pub id: String, pub name: String, pub description: String, pub icon: String, pub category: SlashCommandCategory, pub keywords: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum SlashCommandCategory { GbAssist, General, Media, Embed, Advanced, } pub fn get_slash_commands() -> Vec { vec![ SlashCommand { id: "ask_gb".to_string(), name: "Ask General Bots".to_string(), description: "Ask GB to answer a question using your knowledge base".to_string(), icon: "bot".to_string(), category: SlashCommandCategory::GbAssist, keywords: vec!["ai".to_string(), "ask".to_string(), "question".to_string()], }, SlashCommand { id: "create_content".to_string(), name: "Create page content".to_string(), description: "Use GB to generate content for this page".to_string(), icon: "sparkles".to_string(), category: SlashCommandCategory::GbAssist, keywords: vec!["generate".to_string(), "write".to_string(), "create".to_string()], }, SlashCommand { id: "summarize".to_string(), name: "Summarize page".to_string(), description: "Generate a summary of this page using GB".to_string(), icon: "file-text".to_string(), category: SlashCommandCategory::GbAssist, keywords: vec!["summary".to_string(), "tldr".to_string()], }, SlashCommand { id: "translate".to_string(), name: "Translate block".to_string(), description: "Translate selected content to another language".to_string(), icon: "languages".to_string(), category: SlashCommandCategory::GbAssist, keywords: vec!["language".to_string(), "translate".to_string()], }, SlashCommand { id: "paragraph".to_string(), name: "Text".to_string(), description: "Plain text paragraph".to_string(), icon: "type".to_string(), category: SlashCommandCategory::General, keywords: vec!["text".to_string(), "paragraph".to_string()], }, SlashCommand { id: "heading1".to_string(), name: "Heading 1".to_string(), description: "Large section heading".to_string(), icon: "heading-1".to_string(), category: SlashCommandCategory::General, keywords: vec!["h1".to_string(), "heading".to_string(), "title".to_string()], }, SlashCommand { id: "heading2".to_string(), name: "Heading 2".to_string(), description: "Medium section heading".to_string(), icon: "heading-2".to_string(), category: SlashCommandCategory::General, keywords: vec!["h2".to_string(), "heading".to_string()], }, SlashCommand { id: "heading3".to_string(), name: "Heading 3".to_string(), description: "Small section heading".to_string(), icon: "heading-3".to_string(), category: SlashCommandCategory::General, keywords: vec!["h3".to_string(), "heading".to_string()], }, SlashCommand { id: "bulleted_list".to_string(), name: "Bulleted list".to_string(), description: "Create a bulleted list".to_string(), icon: "list".to_string(), category: SlashCommandCategory::General, keywords: vec!["bullet".to_string(), "list".to_string(), "ul".to_string()], }, SlashCommand { id: "numbered_list".to_string(), name: "Numbered list".to_string(), description: "Create a numbered list".to_string(), icon: "list-ordered".to_string(), category: SlashCommandCategory::General, keywords: vec!["number".to_string(), "list".to_string(), "ol".to_string()], }, SlashCommand { id: "checklist".to_string(), name: "Checklist".to_string(), description: "Create a checklist with checkboxes".to_string(), icon: "check-square".to_string(), category: SlashCommandCategory::General, keywords: vec!["todo".to_string(), "checkbox".to_string(), "task".to_string()], }, SlashCommand { id: "toggle".to_string(), name: "Toggle".to_string(), description: "Create a collapsible toggle block".to_string(), icon: "chevron-right".to_string(), category: SlashCommandCategory::General, keywords: vec!["collapse".to_string(), "expand".to_string(), "toggle".to_string()], }, SlashCommand { id: "table".to_string(), name: "Table".to_string(), description: "Create a table".to_string(), icon: "table".to_string(), category: SlashCommandCategory::General, keywords: vec!["table".to_string(), "grid".to_string()], }, SlashCommand { id: "divider".to_string(), name: "Divider".to_string(), description: "Create a horizontal divider".to_string(), icon: "minus".to_string(), category: SlashCommandCategory::General, keywords: vec!["hr".to_string(), "line".to_string(), "separator".to_string()], }, SlashCommand { id: "quote".to_string(), name: "Quote".to_string(), description: "Create a quote block".to_string(), icon: "quote".to_string(), category: SlashCommandCategory::General, keywords: vec!["blockquote".to_string(), "quote".to_string()], }, SlashCommand { id: "callout".to_string(), name: "Callout".to_string(), description: "Create a callout block with icon".to_string(), icon: "alert-circle".to_string(), category: SlashCommandCategory::General, keywords: vec!["callout".to_string(), "note".to_string(), "tip".to_string()], }, SlashCommand { id: "code".to_string(), name: "Code".to_string(), description: "Create a code block".to_string(), icon: "code".to_string(), category: SlashCommandCategory::General, keywords: vec!["code".to_string(), "snippet".to_string()], }, SlashCommand { id: "image".to_string(), name: "Image".to_string(), description: "Upload or embed an image".to_string(), icon: "image".to_string(), category: SlashCommandCategory::Media, keywords: vec!["image".to_string(), "picture".to_string(), "photo".to_string()], }, SlashCommand { id: "video".to_string(), name: "Video".to_string(), description: "Embed a video".to_string(), icon: "video".to_string(), category: SlashCommandCategory::Media, keywords: vec!["video".to_string(), "youtube".to_string()], }, SlashCommand { id: "file".to_string(), name: "File".to_string(), description: "Upload a file".to_string(), icon: "file".to_string(), category: SlashCommandCategory::Media, keywords: vec!["file".to_string(), "upload".to_string(), "attachment".to_string()], }, SlashCommand { id: "embed".to_string(), name: "Embed".to_string(), description: "Embed external content".to_string(), icon: "globe".to_string(), category: SlashCommandCategory::Embed, keywords: vec!["embed".to_string(), "iframe".to_string()], }, SlashCommand { id: "link_to_page".to_string(), name: "Link to page".to_string(), description: "Create a link to another page".to_string(), icon: "link".to_string(), category: SlashCommandCategory::Advanced, keywords: vec!["link".to_string(), "page".to_string()], }, SlashCommand { id: "toc".to_string(), name: "Table of contents".to_string(), description: "Generate a table of contents".to_string(), icon: "list-tree".to_string(), category: SlashCommandCategory::Advanced, keywords: vec!["toc".to_string(), "contents".to_string()], }, ] } #[derive(Debug, Clone)] pub enum WorkspacesError { WorkspaceNotFound, PageNotFound, BlockNotFound, CommentNotFound, VersionNotFound, MemberNotFound, MemberAlreadyExists, CannotRemoveLastOwner, PermissionDenied, InvalidOperation(String), } impl std::fmt::Display for WorkspacesError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::WorkspaceNotFound => write!(f, "Workspace not found"), Self::PageNotFound => write!(f, "Page not found"), Self::BlockNotFound => write!(f, "Block not found"), Self::CommentNotFound => write!(f, "Comment not found"), Self::VersionNotFound => write!(f, "Version not found"), Self::MemberNotFound => write!(f, "Member not found"), Self::MemberAlreadyExists => write!(f, "Member already exists in workspace"), Self::CannotRemoveLastOwner => write!(f, "Cannot remove the last owner"), Self::PermissionDenied => write!(f, "Permission denied"), Self::InvalidOperation(e) => write!(f, "Invalid operation: {e}"), } } } impl std::error::Error for WorkspacesError {}