Spaces:
Running
Running
| /* | |
| ELYSIA MARKDOWN STUDIO v1.0 - Main Application | |
| Orchestrates all modules | |
| */ | |
| import Utils from "./utils.js"; | |
| import DB from "./db.js"; | |
| import Editor from "./editor.js"; | |
| import Preview from "./preview.js"; | |
| import Documents from "./documents.js"; | |
| import AITools from "./ai-tools.js"; | |
| import Export from "./export.js"; | |
| import Templates from "./templates.js"; | |
| class App { | |
| constructor() { | |
| this.currentDoc = null; | |
| this.currentDocId = null; | |
| this.unsavedChanges = false; | |
| // Module references | |
| this.editor = Editor; | |
| this.preview = Preview; | |
| this.documents = Documents; | |
| this.aiTools = AITools; | |
| this.export = Export; | |
| this.templates = Templates; | |
| } | |
| async init() { | |
| console.log("π Elysia Markdown Studio v1.3.0 initializing..."); | |
| // Initialize modules FIRST (before loadSettings!) | |
| this.editor.init(); | |
| this.preview.init(); | |
| this.documents.init(); | |
| this.aiTools.init(); | |
| this.export.init(); | |
| this.templates.init(); | |
| // Load settings (now that modules are ready) | |
| this.loadSettings(); | |
| // Setup event listeners | |
| this.setupEventListeners(); | |
| // Welcome message | |
| this.showWelcome(); | |
| // Start auto-save AFTER everything is initialized | |
| const autoSave = Utils.storage.get("autoSave", true); | |
| if (autoSave) { | |
| this.editor.startAutoSave(); | |
| } | |
| console.log("β¨ Elysia Markdown Studio ready!"); | |
| } | |
| setupEventListeners() { | |
| // New document - show template selection | |
| document.getElementById("btn-new-doc").addEventListener("click", () => { | |
| this.templates.showTemplatesModal(); | |
| }); | |
| // Save document | |
| document.getElementById("btn-save").addEventListener("click", () => { | |
| this.saveDocument(); | |
| }); | |
| // Settings - OPEN modal | |
| document.getElementById("btn-settings").addEventListener("click", () => { | |
| Utils.modal.open("modal-settings"); | |
| }); | |
| // Settings - SAVE settings | |
| document.getElementById("btn-save-settings").addEventListener("click", () => { | |
| this.saveSettings(); | |
| }); | |
| // Document title | |
| document.getElementById("doc-title").addEventListener("input", e => { | |
| if (this.currentDoc) { | |
| this.currentDoc.title = e.target.value || "Untitled Document"; | |
| this.unsavedChanges = true; | |
| } | |
| }); | |
| // Mobile view toggle (Editor <-> Preview) | |
| const mobileToggle = document.getElementById("mobile-view-toggle"); | |
| if (mobileToggle) { | |
| mobileToggle.addEventListener("click", () => { | |
| this.toggleMobileView(); | |
| }); | |
| } | |
| // Keyboard shortcuts | |
| document.addEventListener("keydown", e => { | |
| if (e.ctrlKey || e.metaKey) { | |
| if (e.key === "s") { | |
| e.preventDefault(); | |
| this.saveDocument(); | |
| } else if (e.key === "n") { | |
| e.preventDefault(); | |
| this.templates.showTemplatesModal(); | |
| } else if (e.key === "/") { | |
| e.preventDefault(); | |
| this.showKeyboardShortcuts(); | |
| } | |
| } | |
| }); | |
| // Before unload warning | |
| window.addEventListener("beforeunload", e => { | |
| if (this.unsavedChanges) { | |
| e.preventDefault(); | |
| e.returnValue = ""; | |
| } | |
| }); | |
| } | |
| async newDocument(templateName = null) { | |
| if (this.unsavedChanges) { | |
| const save = confirm("Save current document before creating new one?"); | |
| if (save) { | |
| await this.saveDocument(); | |
| } | |
| } | |
| // Get template content | |
| let content = ""; | |
| if (templateName) { | |
| const template = await this.templates.getTemplate(templateName); | |
| content = template?.content || ""; | |
| } | |
| // Create new document | |
| this.currentDoc = { | |
| id: Utils.uuid(), | |
| title: "Untitled Document", | |
| content, | |
| tags: [], | |
| favorite: false, | |
| createdAt: Date.now(), | |
| updatedAt: Date.now(), | |
| wordCount: 0, | |
| charCount: 0 | |
| }; | |
| this.currentDocId = null; // Not saved yet | |
| this.unsavedChanges = false; | |
| // Update UI | |
| document.getElementById("doc-title").value = this.currentDoc.title; | |
| this.editor.setContent(content); | |
| this.editor.currentDoc = this.currentDoc; | |
| Utils.toast.info("New document created"); | |
| } | |
| async loadDocument(id) { | |
| if (this.unsavedChanges) { | |
| const save = confirm("Save current document before loading another?"); | |
| if (save) { | |
| await this.saveDocument(); | |
| } | |
| } | |
| const doc = await DB.getDocument(id); | |
| if (!doc) { | |
| Utils.toast.error("Document not found"); | |
| return; | |
| } | |
| this.currentDoc = doc; | |
| this.currentDocId = id; | |
| this.unsavedChanges = false; | |
| // Update UI | |
| document.getElementById("doc-title").value = doc.title; | |
| this.editor.setContent(doc.content); | |
| this.editor.currentDoc = doc; | |
| // Update sidebar highlight | |
| this.documents.updateActiveDoc(id); | |
| Utils.toast.success(`Loaded: ${doc.title}`); | |
| } | |
| async saveDocument(silent = false) { | |
| const content = this.editor.getContent(); | |
| const title = document.getElementById("doc-title").value || "Untitled Document"; | |
| if (!content && !silent) { | |
| Utils.toast.warning("Document is empty. Add some content before saving!"); | |
| return; | |
| } | |
| try { | |
| if (this.currentDocId) { | |
| // Update existing | |
| await DB.updateDocument(this.currentDocId, { | |
| title, | |
| content, | |
| updatedAt: Date.now() | |
| }); | |
| if (!silent) Utils.toast.success("Document saved!"); | |
| } else { | |
| // Check for potential duplicates before creating | |
| const existingDocs = await DB.getAllDocuments(); | |
| const duplicate = existingDocs.find( | |
| doc => doc.title.toLowerCase() === title.toLowerCase() && doc.content === content | |
| ); | |
| if (duplicate && !silent) { | |
| const shouldCreate = confirm( | |
| `A document with the title "${title}" and identical content already exists.\n\n` + | |
| "Do you want to create it anyway?" | |
| ); | |
| if (!shouldCreate) return; | |
| } | |
| // Create new | |
| const doc = await DB.createDocument({ | |
| title, | |
| content | |
| }); | |
| this.currentDocId = doc.id; | |
| this.currentDoc = doc; | |
| if (!silent) Utils.toast.success("Document created and saved!"); | |
| } | |
| this.unsavedChanges = false; | |
| // Refresh documents list | |
| this.documents.loadDocuments(); | |
| } catch (err) { | |
| console.error("Save failed:", err); | |
| Utils.toast.error("Failed to save document"); | |
| } | |
| } | |
| loadSettings() { | |
| const apiKey = Utils.storage.get("apiKey"); | |
| const model = Utils.storage.get("model", "anthropic/claude-sonnet-4.5"); | |
| const previewTheme = Utils.storage.get("previewTheme", "elysia"); | |
| const autoSave = Utils.storage.get("autoSave", true); | |
| const livePreview = Utils.storage.get("livePreview", true); | |
| // Populate settings form | |
| if (document.getElementById("api-key")) { | |
| document.getElementById("api-key").value = apiKey || ""; | |
| } | |
| if (document.getElementById("model-select")) { | |
| document.getElementById("model-select").value = model; | |
| } | |
| if (document.getElementById("preview-theme")) { | |
| document.getElementById("preview-theme").value = previewTheme; | |
| } | |
| if (document.getElementById("auto-save")) { | |
| document.getElementById("auto-save").checked = autoSave; | |
| } | |
| if (document.getElementById("live-preview")) { | |
| document.getElementById("live-preview").checked = livePreview; | |
| } | |
| // Apply theme | |
| this.preview.setTheme(previewTheme); | |
| } | |
| saveSettings() { | |
| const apiKey = document.getElementById("api-key").value.trim(); | |
| const model = document.getElementById("model-select").value; | |
| const previewTheme = document.getElementById("preview-theme").value; | |
| const autoSave = document.getElementById("auto-save").checked; | |
| const livePreview = document.getElementById("live-preview").checked; | |
| // Validate API key format (OpenRouter keys start with "sk-or-") | |
| if (apiKey && !apiKey.startsWith("sk-or-")) { | |
| Utils.toast.warning("API key should start with 'sk-or-'. Please check your key."); | |
| return; | |
| } | |
| Utils.storage.set("apiKey", apiKey); | |
| Utils.storage.set("model", model); | |
| Utils.storage.set("previewTheme", previewTheme); | |
| Utils.storage.set("autoSave", autoSave); | |
| Utils.storage.set("livePreview", livePreview); | |
| // Apply theme | |
| this.preview.setTheme(previewTheme); | |
| // Restart auto-save if needed | |
| if (autoSave) { | |
| this.editor.startAutoSave(); | |
| } else { | |
| this.editor.stopAutoSave(); | |
| } | |
| // Update preview immediately if toggling live preview | |
| if (livePreview) { | |
| this.preview.update(); | |
| } | |
| Utils.toast.success("Settings saved!"); | |
| Utils.modal.close("modal-settings"); | |
| } | |
| // Mobile view toggle (Editor <-> Preview) | |
| toggleMobileView() { | |
| const previewPane = document.querySelector(".preview-pane"); | |
| const editorPane = document.querySelector(".editor-pane"); | |
| const toggleBtn = document.getElementById("mobile-view-toggle"); | |
| const toggleIcon = toggleBtn?.querySelector(".toggle-icon"); | |
| const toggleText = toggleBtn?.querySelector(".toggle-text"); | |
| if (!previewPane || !editorPane) return; | |
| const isPreviewActive = previewPane.classList.contains("active"); | |
| if (isPreviewActive) { | |
| // Switch to Editor | |
| previewPane.classList.remove("active"); | |
| editorPane.style.display = "block"; | |
| if (toggleIcon) toggleIcon.textContent = "ποΈ"; | |
| if (toggleText) toggleText.textContent = "Preview"; | |
| } else { | |
| // Switch to Preview | |
| this.preview.update(); // Ensure preview is current | |
| previewPane.classList.add("active"); | |
| editorPane.style.display = "none"; | |
| if (toggleIcon) toggleIcon.textContent = "βοΈ"; | |
| if (toggleText) toggleText.textContent = "Editor"; | |
| } | |
| } | |
| showWelcome() { | |
| const welcomeMessage = `# Welcome to Elysia Markdown Studio π | |
| Your AI-powered writing companion is ready! | |
| ## β¨ Features | |
| - **Live Preview** - See your markdown rendered in real-time | |
| - **AI Tools** - Summarize, improve, merge documents with Elysia's intelligence | |
| - **Rich Markdown** - Support for tables, math (KaTeX), diagrams (Mermaid), code highlighting | |
| - **Smart Export** - Export to Markdown, HTML, Artifact, JSON, Plain Text | |
| - **Document Management** - Organize with tags, favorites, collections | |
| - **Templates** - Quick start with README, Blog, Meeting Notes, and more! | |
| ## π Quick Start | |
| 1. Click **βοΈ Settings** to add your OpenRouter API key (for AI features) | |
| 2. Start writing in the left pane | |
| 3. See live preview on the right | |
| 4. Use toolbar for quick formatting | |
| 5. Save with **πΎ** or Ctrl+S | |
| 6. Access AI tools with **π§ ** | |
| ## β¨οΈ Keyboard Shortcuts | |
| | Shortcut | Action | | |
| |----------|--------| | |
| | **Ctrl+S** | Save document | | |
| | **Ctrl+N** | New document | | |
| | **Ctrl+B** | Bold text | | |
| | **Ctrl+I** | Italic text | | |
| | **Ctrl+/** | Show all shortcuts | | |
| Press **Ctrl+/** anytime to see the full shortcuts guide! | |
| ## π‘ Tips | |
| - Right-click documents in the sidebar for quick actions (rename, delete, favorite) | |
| - AI tools work best with content > 100 words | |
| - Auto-save runs every 30 seconds (configurable in Settings) | |
| - Export to "Artifact" format for beautiful standalone HTML pages | |
| Start writing your masterpiece! Delete this text and create something amazing! π | |
| --- | |
| *Built with love by Jean & Elysia* ππ`; | |
| this.editor.setContent(welcomeMessage); | |
| this.currentDoc = { | |
| title: "Welcome to Elysia Markdown Studio", | |
| content: welcomeMessage | |
| }; | |
| document.getElementById("doc-title").value = this.currentDoc.title; | |
| } | |
| showKeyboardShortcuts() { | |
| const shortcuts = `# β¨οΈ Keyboard Shortcuts - Elysia Markdown Studio | |
| ## Document Management | |
| - **Ctrl+S** - Save current document | |
| - **Ctrl+N** - Create new document | |
| - **Ctrl+/** - Show this shortcuts guide | |
| ## Text Formatting | |
| - **Ctrl+B** - **Bold** text | |
| - **Ctrl+I** - *Italic* text | |
| ## Navigation | |
| - **Tab** - Indent / Next field | |
| - **Shift+Tab** - Outdent / Previous field | |
| - **Ctrl+F** - Search in document (browser default) | |
| ## Pro Tips π‘ | |
| - Right-click documents for context menu | |
| - Use toolbar buttons for advanced formatting (tables, links, images) | |
| - Drag & drop images into editor (coming soon!) | |
| --- | |
| Press **Esc** to close this guide and continue writing!`; | |
| // Create temporary modal | |
| const modal = document.createElement("div"); | |
| modal.className = "modal active"; | |
| modal.id = "modal-shortcuts"; | |
| modal.innerHTML = ` | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h2>β¨οΈ Keyboard Shortcuts</h2> | |
| <button class="modal-close" onclick="this.closest('.modal').remove()">Γ</button> | |
| </div> | |
| <div class="modal-body"> | |
| <div class="markdown-preview">${marked.parse(shortcuts)}</div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button class="btn-primary" onclick="this.closest('.modal').remove()">Got it!</button> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(modal); | |
| // Close on Esc | |
| const handleEsc = e => { | |
| if (e.key === "Escape") { | |
| modal.remove(); | |
| document.removeEventListener("keydown", handleEsc); | |
| } | |
| }; | |
| document.addEventListener("keydown", handleEsc); | |
| // Close on backdrop click | |
| modal.addEventListener("click", e => { | |
| if (e.target === modal) { | |
| modal.remove(); | |
| } | |
| }); | |
| } | |
| } | |
| // Create global app instance | |
| const app = new App(); | |
| window.app = app; // Make accessible to other modules | |
| // Initialize on DOM ready | |
| document.addEventListener("DOMContentLoaded", () => { | |
| app.init(); | |
| }); | |
| export default app; | |