(function () { "use strict"; const CONFIG = { AUTOSAVE_DELAY: 3000, MAX_HISTORY: 50, WS_RECONNECT_DELAY: 5000, }; const state = { docId: null, docTitle: "Untitled Document", content: "", history: [], historyIndex: -1, isDirty: false, autoSaveTimer: null, ws: null, collaborators: [], chatPanelOpen: true, driveSource: null, zoom: 100, }; const elements = {}; function init() { cacheElements(); bindEvents(); loadFromUrlParams(); setupToolbar(); setupKeyboardShortcuts(); updateWordCount(); connectWebSocket(); } function cacheElements() { elements.app = document.getElementById("docs-app"); elements.docName = document.getElementById("docName"); elements.editorContent = document.getElementById("editorContent"); elements.editorPage = document.getElementById("editorPage"); elements.collaborators = document.getElementById("collaborators"); elements.pageInfo = document.getElementById("pageInfo"); elements.wordCount = document.getElementById("wordCount"); elements.charCount = document.getElementById("charCount"); elements.saveStatus = document.getElementById("saveStatus"); elements.zoomLevel = document.getElementById("zoomLevel"); elements.chatPanel = document.getElementById("chatPanel"); elements.chatMessages = document.getElementById("chatMessages"); elements.chatInput = document.getElementById("chatInput"); elements.chatForm = document.getElementById("chatForm"); elements.shareModal = document.getElementById("shareModal"); elements.linkModal = document.getElementById("linkModal"); elements.imageModal = document.getElementById("imageModal"); elements.tableModal = document.getElementById("tableModal"); elements.exportModal = document.getElementById("exportModal"); } function bindEvents() { if (elements.editorContent) { elements.editorContent.addEventListener("input", handleEditorInput); elements.editorContent.addEventListener("keydown", handleEditorKeydown); elements.editorContent.addEventListener("paste", handlePaste); } if (elements.docName) { elements.docName.addEventListener("change", handleDocNameChange); elements.docName.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); elements.editorContent?.focus(); } }); } document.getElementById("undoBtn")?.addEventListener("click", undo); document.getElementById("redoBtn")?.addEventListener("click", redo); document .getElementById("boldBtn") ?.addEventListener("click", () => execCommand("bold")); document .getElementById("italicBtn") ?.addEventListener("click", () => execCommand("italic")); document .getElementById("underlineBtn") ?.addEventListener("click", () => execCommand("underline")); document .getElementById("strikeBtn") ?.addEventListener("click", () => execCommand("strikeThrough")); document .getElementById("alignLeftBtn") ?.addEventListener("click", () => execCommand("justifyLeft")); document .getElementById("alignCenterBtn") ?.addEventListener("click", () => execCommand("justifyCenter")); document .getElementById("alignRightBtn") ?.addEventListener("click", () => execCommand("justifyRight")); document .getElementById("alignJustifyBtn") ?.addEventListener("click", () => execCommand("justifyFull")); document .getElementById("bulletListBtn") ?.addEventListener("click", () => execCommand("insertUnorderedList")); document .getElementById("numberListBtn") ?.addEventListener("click", () => execCommand("insertOrderedList")); document .getElementById("indentBtn") ?.addEventListener("click", () => execCommand("indent")); document .getElementById("outdentBtn") ?.addEventListener("click", () => execCommand("outdent")); document .getElementById("linkBtn") ?.addEventListener("click", () => showModal("linkModal")); document .getElementById("imageBtn") ?.addEventListener("click", () => showModal("imageModal")); document .getElementById("tableBtn") ?.addEventListener("click", () => showModal("tableModal")); document .getElementById("shareBtn") ?.addEventListener("click", () => showModal("shareModal")); document .getElementById("headingSelect") ?.addEventListener("change", handleHeadingChange); document .getElementById("fontFamily") ?.addEventListener("change", handleFontFamilyChange); document .getElementById("fontSize") ?.addEventListener("change", handleFontSizeChange); document.getElementById("textColorBtn")?.addEventListener("click", () => { document.getElementById("textColorPicker")?.click(); }); document .getElementById("textColorPicker") ?.addEventListener("input", handleTextColorChange); document.getElementById("highlightBtn")?.addEventListener("click", () => { document.getElementById("highlightPicker")?.click(); }); document .getElementById("highlightPicker") ?.addEventListener("input", handleHighlightChange); document.getElementById("zoomInBtn")?.addEventListener("click", zoomIn); document.getElementById("zoomOutBtn")?.addEventListener("click", zoomOut); document .getElementById("chatToggle") ?.addEventListener("click", toggleChatPanel); document .getElementById("chatClose") ?.addEventListener("click", toggleChatPanel); elements.chatForm?.addEventListener("submit", handleChatSubmit); document.querySelectorAll(".suggestion-btn").forEach((btn) => { btn.addEventListener("click", () => handleSuggestionClick(btn.dataset.action), ); }); document.querySelectorAll(".btn-close, .modal").forEach((el) => { el.addEventListener("click", (e) => { if (e.target === el) closeModals(); }); }); document .getElementById("closeShareModal") ?.addEventListener("click", () => hideModal("shareModal")); document .getElementById("closeLinkModal") ?.addEventListener("click", () => hideModal("linkModal")); document .getElementById("closeImageModal") ?.addEventListener("click", () => hideModal("imageModal")); document .getElementById("closeTableModal") ?.addEventListener("click", () => hideModal("tableModal")); document .getElementById("closeExportModal") ?.addEventListener("click", () => hideModal("exportModal")); document .getElementById("insertLinkBtn") ?.addEventListener("click", insertLink); document .getElementById("insertImageBtn") ?.addEventListener("click", insertImage); document .getElementById("insertTableBtn") ?.addEventListener("click", insertTable); document .getElementById("copyLinkBtn") ?.addEventListener("click", copyShareLink); document.querySelectorAll(".export-option").forEach((btn) => { btn.addEventListener("click", () => exportDocument(btn.dataset.format)); }); window.addEventListener("beforeunload", handleBeforeUnload); } function handleEditorInput() { saveToHistory(); state.isDirty = true; updateWordCount(); scheduleAutoSave(); broadcastChange(); } function handleDocNameChange() { state.docTitle = elements.docName.value || "Untitled Document"; state.isDirty = true; scheduleAutoSave(); } function handleEditorKeydown(e) { if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case "b": e.preventDefault(); execCommand("bold"); break; case "i": e.preventDefault(); execCommand("italic"); break; case "u": e.preventDefault(); execCommand("underline"); break; case "z": e.preventDefault(); if (e.shiftKey) { redo(); } else { undo(); } break; case "y": e.preventDefault(); redo(); break; case "s": e.preventDefault(); saveDocument(); break; } } } function handlePaste(e) { e.preventDefault(); const text = e.clipboardData.getData("text/plain"); document.execCommand("insertText", false, text); } function handleBeforeUnload(e) { if (state.isDirty) { e.preventDefault(); e.returnValue = ""; } } function setupToolbar() { updateToolbarState(); if (elements.editorContent) { elements.editorContent.addEventListener("mouseup", updateToolbarState); elements.editorContent.addEventListener("keyup", updateToolbarState); } } function updateToolbarState() { document .getElementById("boldBtn") ?.classList.toggle("active", document.queryCommandState("bold")); document .getElementById("italicBtn") ?.classList.toggle("active", document.queryCommandState("italic")); document .getElementById("underlineBtn") ?.classList.toggle("active", document.queryCommandState("underline")); document .getElementById("strikeBtn") ?.classList.toggle("active", document.queryCommandState("strikeThrough")); } function setupKeyboardShortcuts() { document.addEventListener("keydown", (e) => { if (e.target.closest(".chat-input, .modal input")) return; if (e.key === "Escape") { closeModals(); } }); } function execCommand(command, value = null) { elements.editorContent?.focus(); document.execCommand(command, false, value); saveToHistory(); state.isDirty = true; scheduleAutoSave(); updateToolbarState(); } function handleHeadingChange(e) { const value = e.target.value; execCommand("formatBlock", value); } function handleFontFamilyChange(e) { execCommand("fontName", e.target.value); } function handleFontSizeChange(e) { execCommand("fontSize", e.target.value); } function handleTextColorChange(e) { execCommand("foreColor", e.target.value); const indicator = document.querySelector("#textColorBtn .color-indicator"); if (indicator) indicator.style.background = e.target.value; } function handleHighlightChange(e) { execCommand("hiliteColor", e.target.value); const indicator = document.querySelector("#highlightBtn .color-indicator"); if (indicator) indicator.style.background = e.target.value; } function saveToHistory() { if (!elements.editorContent) return; const content = elements.editorContent.innerHTML; if (state.history[state.historyIndex] === content) return; state.history = state.history.slice(0, state.historyIndex + 1); state.history.push(content); if (state.history.length > CONFIG.MAX_HISTORY) { state.history.shift(); } else { state.historyIndex++; } } function undo() { if (state.historyIndex > 0) { state.historyIndex--; if (elements.editorContent) { elements.editorContent.innerHTML = state.history[state.historyIndex]; } state.isDirty = true; updateWordCount(); } } function redo() { if (state.historyIndex < state.history.length - 1) { state.historyIndex++; if (elements.editorContent) { elements.editorContent.innerHTML = state.history[state.historyIndex]; } state.isDirty = true; updateWordCount(); } } function updateWordCount() { if (!elements.editorContent) return; const text = elements.editorContent.innerText || ""; const words = text .trim() .split(/\s+/) .filter((w) => w.length > 0); const chars = text.length; if (elements.wordCount) { elements.wordCount.textContent = `${words.length} word${words.length !== 1 ? "s" : ""}`; } if (elements.charCount) { elements.charCount.textContent = `${chars} character${chars !== 1 ? "s" : ""}`; } const pageHeight = 1056; const contentHeight = elements.editorContent.scrollHeight || pageHeight; const pages = Math.max(1, Math.ceil(contentHeight / pageHeight)); if (elements.pageInfo) { elements.pageInfo.textContent = `Page 1 of ${pages}`; } } function zoomIn() { if (state.zoom < 200) { state.zoom += 10; applyZoom(); } } function zoomOut() { if (state.zoom > 50) { state.zoom -= 10; applyZoom(); } } function applyZoom() { if (elements.editorPage) { elements.editorPage.style.transform = `scale(${state.zoom / 100})`; elements.editorPage.style.transformOrigin = "top center"; } if (elements.zoomLevel) { elements.zoomLevel.textContent = `${state.zoom}%`; } } function scheduleAutoSave() { if (state.autoSaveTimer) { clearTimeout(state.autoSaveTimer); } state.autoSaveTimer = setTimeout(saveDocument, CONFIG.AUTOSAVE_DELAY); if (elements.saveStatus) { elements.saveStatus.textContent = "Saving..."; } } async function saveDocument() { if (!state.isDirty) return; const content = elements.editorContent?.innerHTML || ""; const title = state.docTitle; try { const response = await fetch("/api/docs/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: state.docId, title, content, driveSource: state.driveSource, }), }); if (response.ok) { const result = await response.json(); if (result.id) { state.docId = result.id; window.history.replaceState({}, "", `#id=${state.docId}`); } state.isDirty = false; if (elements.saveStatus) { elements.saveStatus.textContent = "Saved"; } } else { if (elements.saveStatus) { elements.saveStatus.textContent = "Save failed"; } } } catch (e) { console.error("Save error:", e); if (elements.saveStatus) { elements.saveStatus.textContent = "Save failed"; } } } async function loadFromUrlParams() { const urlParams = new URLSearchParams(window.location.search); const hash = window.location.hash; let docId = urlParams.get("id"); let bucket = urlParams.get("bucket"); let path = urlParams.get("path"); if (hash) { const hashQueryIndex = hash.indexOf("?"); if (hashQueryIndex > -1) { const hashParams = new URLSearchParams(hash.slice(hashQueryIndex + 1)); docId = docId || hashParams.get("id"); bucket = bucket || hashParams.get("bucket"); path = path || hashParams.get("path"); } else if (hash.startsWith("#id=")) { docId = hash.slice(4); } } if (bucket && path) { state.driveSource = { bucket, path }; await loadFromDrive(bucket, path); } else if (docId) { try { const response = await fetch(`/api/docs/${docId}`); if (response.ok) { const data = await response.json(); state.docId = docId; state.docTitle = data.title || "Untitled Document"; if (elements.docName) elements.docName.value = state.docTitle; if (elements.editorContent) elements.editorContent.innerHTML = data.content || ""; saveToHistory(); updateWordCount(); } } catch (e) { console.error("Load failed:", e); } } else { saveToHistory(); } } async function loadFromDrive(bucket, path) { const fileName = path.split("/").pop() || "Document"; const ext = fileName.split(".").pop()?.toLowerCase(); try { const response = await fetch("/api/drive/content", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ bucket, path }), }); if (response.ok) { const data = await response.json(); const content = data.content || ""; state.docTitle = fileName.replace(/\.[^.]+$/, ""); if (elements.docName) elements.docName.value = state.docTitle; if (ext === "md") { if (elements.editorContent) { elements.editorContent.innerHTML = markdownToHtml(content); } } else if (ext === "txt") { if (elements.editorContent) { elements.editorContent.innerHTML = `

${escapeHtml(content).replace(/\n/g, "

")}

`; } } else { if (elements.editorContent) { elements.editorContent.innerHTML = content; } } saveToHistory(); updateWordCount(); } } catch (e) { console.error("Drive load failed:", e); } } function markdownToHtml(md) { return md .replace(/^### (.+)$/gm, "

$1

") .replace(/^## (.+)$/gm, "

$1

") .replace(/^# (.+)$/gm, "

$1

") .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/\*(.+?)\*/g, "$1") .replace(/`(.+?)`/g, "$1") .replace(/\n/g, "
"); } function showModal(modalId) { const modal = document.getElementById(modalId); if (modal) modal.classList.remove("hidden"); } function hideModal(modalId) { const modal = document.getElementById(modalId); if (modal) modal.classList.add("hidden"); } function closeModals() { document .querySelectorAll(".modal") .forEach((m) => m.classList.add("hidden")); } function insertLink() { const url = document.getElementById("linkUrl")?.value; const text = document.getElementById("linkText")?.value || url; if (url) { elements.editorContent?.focus(); document.execCommand( "insertHTML", false, `${escapeHtml(text)}`, ); hideModal("linkModal"); saveToHistory(); state.isDirty = true; } } function insertImage() { const url = document.getElementById("imageUrl")?.value; const alt = document.getElementById("imageAlt")?.value || "Image"; if (url) { elements.editorContent?.focus(); document.execCommand( "insertHTML", false, `${escapeHtml(alt)}`, ); hideModal("imageModal"); saveToHistory(); state.isDirty = true; } } function insertTable() { const rows = parseInt(document.getElementById("tableRows")?.value, 10) || 3; const cols = parseInt(document.getElementById("tableCols")?.value, 10) || 3; let html = ''; for (let r = 0; r < rows; r++) { html += ""; for (let c = 0; c < cols; c++) { const cell = r === 0 ? "th" : "td"; html += `<${cell} style="border:1px solid var(--sentient-border,#e0e0e0);padding:8px">${r === 0 ? "Header" : ""}`; } html += ""; } html += "

"; elements.editorContent?.focus(); document.execCommand("insertHTML", false, html); hideModal("tableModal"); saveToHistory(); state.isDirty = true; } function copyShareLink() { const linkInput = document.getElementById("shareLink"); if (linkInput) { const shareUrl = `${window.location.origin}${window.location.pathname}#id=${state.docId || "new"}`; linkInput.value = shareUrl; linkInput.select(); navigator.clipboard.writeText(shareUrl); } } function exportDocument(format) { const title = state.docTitle || "document"; const content = elements.editorContent?.innerHTML || ""; switch (format) { case "pdf": exportAsPDF(title, content); break; case "docx": exportAsDocx(title, content); break; case "html": exportAsHTML(title, content); break; case "txt": exportAsTxt(title); break; case "md": exportAsMarkdown(title); break; } hideModal("exportModal"); } function exportAsPDF(title, content) { const printWindow = window.open("", "_blank"); if (printWindow) { printWindow.document.write(` ${escapeHtml(title)} ${content} `); printWindow.document.close(); printWindow.print(); } } function exportAsDocx(title, content) { addChatMessage( "assistant", "DOCX export requires server-side processing. Feature coming soon!", ); } function exportAsHTML(title, content) { const html = ` ${escapeHtml(title)} ${content} `; downloadFile(html, `${title}.html`, "text/html"); } function exportAsTxt(title) { const text = elements.editorContent?.innerText || ""; downloadFile(text, `${title}.txt`, "text/plain"); } function exportAsMarkdown(title) { const text = elements.editorContent?.innerText || ""; const md = `# ${title}\n\n${text}`; downloadFile(md, `${title}.md`, "text/markdown"); } function downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } function connectWebSocket() { if (!state.docId) return; try { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${protocol}//${window.location.host}/api/docs/ws/${state.docId}`; state.ws = new WebSocket(wsUrl); state.ws.onopen = () => { state.ws.send( JSON.stringify({ type: "join", userId: getUserId(), userName: getUserName(), }), ); }; state.ws.onmessage = (e) => { try { const msg = JSON.parse(e.data); handleWebSocketMessage(msg); } catch (err) { console.error("WS message error:", err); } }; state.ws.onclose = () => { setTimeout(connectWebSocket, CONFIG.WS_RECONNECT_DELAY); }; } catch (e) { console.error("WebSocket failed:", e); } } function handleWebSocketMessage(msg) { switch (msg.type) { case "user_joined": addCollaborator(msg.user); break; case "user_left": removeCollaborator(msg.userId); break; case "content_update": if (msg.userId !== getUserId() && elements.editorContent) { const selection = window.getSelection(); const range = selection?.rangeCount > 0 ? selection.getRangeAt(0) : null; elements.editorContent.innerHTML = msg.content; if (range) { try { selection?.removeAllRanges(); selection?.addRange(range); } catch (e) { // Ignore selection restoration errors } } } break; } } function broadcastChange() { if (state.ws && state.ws.readyState === WebSocket.OPEN) { state.ws.send( JSON.stringify({ type: "content_update", userId: getUserId(), content: elements.editorContent?.innerHTML || "", }), ); } } function addCollaborator(user) { if (!state.collaborators.find((u) => u.id === user.id)) { state.collaborators.push(user); renderCollaborators(); } } function removeCollaborator(userId) { state.collaborators = state.collaborators.filter((u) => u.id !== userId); renderCollaborators(); } function renderCollaborators() { if (!elements.collaborators) return; elements.collaborators.innerHTML = state.collaborators .slice(0, 4) .map( (u) => `
${u.name.charAt(0).toUpperCase()}
`, ) .join(""); } function getUserId() { let id = localStorage.getItem("gb-user-id"); if (!id) { id = "user-" + Math.random().toString(36).substr(2, 9); localStorage.setItem("gb-user-id", id); } return id; } function getUserName() { return localStorage.getItem("gb-user-name") || "Anonymous"; } function toggleChatPanel() { state.chatPanelOpen = !state.chatPanelOpen; elements.chatPanel?.classList.toggle("collapsed", !state.chatPanelOpen); } function handleChatSubmit(e) { e.preventDefault(); const message = elements.chatInput?.value.trim(); if (!message) return; addChatMessage("user", message); if (elements.chatInput) elements.chatInput.value = ""; processAICommand(message); } function handleSuggestionClick(action) { const commands = { shorter: "Make the selected text shorter", grammar: "Fix grammar and spelling in the document", formal: "Make the text more formal", summarize: "Summarize this document", }; const message = commands[action] || action; addChatMessage("user", message); processAICommand(message); } function addChatMessage(role, content) { if (!elements.chatMessages) return; const div = document.createElement("div"); div.className = `chat-message ${role}`; div.innerHTML = `
${escapeHtml(content)}
`; elements.chatMessages.appendChild(div); elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; } async function processAICommand(command) { const lower = command.toLowerCase(); const selectedText = window.getSelection()?.toString() || ""; let response = ""; if (lower.includes("shorter") || lower.includes("concise")) { if (selectedText) { response = await callAI("shorten", selectedText); } else { response = "Please select some text first, then ask me to make it shorter."; } } else if ( lower.includes("grammar") || lower.includes("spelling") || lower.includes("fix") ) { const text = selectedText || elements.editorContent?.innerText || ""; response = await callAI("grammar", text); } else if (lower.includes("formal")) { if (selectedText) { response = await callAI("formal", selectedText); } else { response = "Please select some text first, then ask me to make it formal."; } } else if (lower.includes("casual") || lower.includes("informal")) { if (selectedText) { response = await callAI("casual", selectedText); } else { response = "Please select some text first, then ask me to make it casual."; } } else if (lower.includes("summarize") || lower.includes("summary")) { const text = selectedText || elements.editorContent?.innerText || ""; response = await callAI("summarize", text); } else if (lower.includes("translate")) { const langMatch = lower.match(/to (\w+)/); const lang = langMatch ? langMatch[1] : "Spanish"; const text = selectedText || elements.editorContent?.innerText || ""; response = await callAI("translate", text, lang); } else if (lower.includes("expand") || lower.includes("longer")) { if (selectedText) { response = await callAI("expand", selectedText); } else { response = "Please select some text first, then ask me to expand it."; } } else if (lower.includes("heading") || lower.includes("title")) { execCommand("formatBlock", "h1"); response = "Applied heading format to selected text."; } else if (lower.includes("bullet") || lower.includes("list")) { execCommand("insertUnorderedList"); response = "Created a bullet list."; } else if (lower.includes("number") && lower.includes("list")) { execCommand("insertOrderedList"); response = "Created a numbered list."; } else if (lower.includes("bold")) { execCommand("bold"); response = "Applied bold formatting."; } else if (lower.includes("italic")) { execCommand("italic"); response = "Applied italic formatting."; } else if (lower.includes("underline")) { execCommand("underline"); response = "Applied underline formatting."; } else { try { const res = await fetch("/api/docs/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command, selectedText, docId: state.docId, }), }); const data = await res.json(); response = data.response || "I processed your request."; } catch { response = "I can help you with:\n• Make text shorter or longer\n• Fix grammar and spelling\n• Translate to another language\n• Change tone (formal/casual)\n• Summarize the document\n• Format as heading, list, etc."; } } addChatMessage("assistant", response); } async function callAI(action, text, extra = "") { try { const res = await fetch("/api/docs/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, text, extra, docId: state.docId }), }); if (res.ok) { const data = await res.json(); return data.result || data.response || "Done!"; } return "AI processing failed. Please try again."; } catch { return "Unable to connect to AI service. Please try again later."; } } function escapeHtml(str) { if (!str) return ""; const div = document.createElement("div"); div.textContent = str; return div.innerHTML; } function createNewDocument() { state.docId = null; state.docTitle = "Untitled Document"; state.isDirty = false; state.history = []; state.historyIndex = -1; if (elements.docName) elements.docName.value = state.docTitle; if (elements.editorContent) elements.editorContent.innerHTML = ""; window.history.replaceState({}, "", window.location.pathname); saveToHistory(); updateWordCount(); elements.editorContent?.focus(); } window.gbDocs = { init, createNewDocument, saveDocument, exportDocument, showModal, hideModal, closeModals, toggleChatPanel, execCommand, }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();