(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,
findMatches: [],
findMatchIndex: -1,
};
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");
elements.findReplaceModal = document.getElementById("findReplaceModal");
elements.printPreviewModal = document.getElementById("printPreviewModal");
elements.headerFooterModal = document.getElementById("headerFooterModal");
elements.editorHeader = document.getElementById("editorHeader");
elements.editorFooter = document.getElementById("editorFooter");
}
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));
});
document
.getElementById("findReplaceBtn")
?.addEventListener("click", showFindReplaceModal);
document
.getElementById("closeFindReplaceModal")
?.addEventListener("click", () => hideModal("findReplaceModal"));
document.getElementById("findNextBtn")?.addEventListener("click", findNext);
document.getElementById("findPrevBtn")?.addEventListener("click", findPrev);
document
.getElementById("replaceBtn")
?.addEventListener("click", replaceOne);
document
.getElementById("replaceAllBtn")
?.addEventListener("click", replaceAll);
document
.getElementById("findInput")
?.addEventListener("input", performFind);
document
.getElementById("printPreviewBtn")
?.addEventListener("click", showPrintPreview);
document
.getElementById("closePrintPreviewModal")
?.addEventListener("click", () => hideModal("printPreviewModal"));
document
.getElementById("printBtn")
?.addEventListener("click", printDocument);
document
.getElementById("cancelPrintBtn")
?.addEventListener("click", () => hideModal("printPreviewModal"));
document
.getElementById("printOrientation")
?.addEventListener("change", updatePrintPreview);
document
.getElementById("printPaperSize")
?.addEventListener("change", updatePrintPreview);
document
.getElementById("printHeaders")
?.addEventListener("change", updatePrintPreview);
document
.getElementById("pageBreakBtn")
?.addEventListener("click", insertPageBreak);
document
.getElementById("headerFooterBtn")
?.addEventListener("click", showHeaderFooterModal);
document
.getElementById("closeHeaderFooterModal")
?.addEventListener("click", () => hideModal("headerFooterModal"));
document
.getElementById("applyHeaderFooterBtn")
?.addEventListener("click", applyHeaderFooter);
document
.getElementById("cancelHeaderFooterBtn")
?.addEventListener("click", () => hideModal("headerFooterModal"));
document
.getElementById("removeHeaderFooterBtn")
?.addEventListener("click", removeHeaderFooter);
document.querySelectorAll(".hf-tab").forEach((tab) => {
tab.addEventListener("click", () => switchHfTab(tab.dataset.tab));
});
document
.getElementById("insertPageNum")
?.addEventListener("click", () => insertHfField("header", "pageNum"));
document
.getElementById("insertDate")
?.addEventListener("click", () => insertHfField("header", "date"));
document
.getElementById("insertDocTitle")
?.addEventListener("click", () => insertHfField("header", "title"));
document
.getElementById("insertFooterPageNum")
?.addEventListener("click", () => insertHfField("footer", "pageNum"));
document
.getElementById("insertFooterDate")
?.addEventListener("click", () => insertHfField("footer", "date"));
document
.getElementById("insertFooterDocTitle")
?.addEventListener("click", () => insertHfField("footer", "title"));
if (elements.editorHeader) {
elements.editorHeader.addEventListener("input", handleHeaderFooterInput);
}
if (elements.editorFooter) {
elements.editorFooter.addEventListener("input", handleHeaderFooterInput);
}
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,
`
`,
);
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" : ""}${cell}>`;
}
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 showFindReplaceModal() {
showModal("findReplaceModal");
document.getElementById("findInput")?.focus();
state.findMatches = [];
state.findMatchIndex = -1;
clearFindHighlights();
}
function performFind() {
const searchText = document.getElementById("findInput")?.value || "";
const matchCase = document.getElementById("findMatchCase")?.checked;
const wholeWord = document.getElementById("findWholeWord")?.checked;
clearFindHighlights();
state.findMatches = [];
state.findMatchIndex = -1;
if (!searchText || !elements.editorContent) {
updateFindResults();
return;
}
const content = elements.editorContent.innerHTML;
let flags = "g";
if (!matchCase) flags += "i";
let searchPattern = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
if (wholeWord) {
searchPattern = `\\b${searchPattern}\\b`;
}
const regex = new RegExp(searchPattern, flags);
const textContent = elements.editorContent.textContent;
let match;
while ((match = regex.exec(textContent)) !== null) {
state.findMatches.push({
index: match.index,
length: match[0].length,
text: match[0],
});
}
if (state.findMatches.length > 0) {
state.findMatchIndex = 0;
highlightAllMatches(searchText, matchCase, wholeWord);
scrollToMatch();
}
updateFindResults();
}
function highlightAllMatches(searchText, matchCase, wholeWord) {
if (!elements.editorContent) return;
const walker = document.createTreeWalker(
elements.editorContent,
NodeFilter.SHOW_TEXT,
null,
false,
);
const textNodes = [];
let node;
while ((node = walker.nextNode())) {
textNodes.push(node);
}
let flags = "g";
if (!matchCase) flags += "i";
let searchPattern = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
if (wholeWord) {
searchPattern = `\\b${searchPattern}\\b`;
}
const regex = new RegExp(`(${searchPattern})`, flags);
textNodes.forEach((textNode) => {
const text = textNode.textContent;
if (regex.test(text)) {
const span = document.createElement("span");
span.innerHTML = text.replace(
regex,
'$1',
);
textNode.parentNode.replaceChild(span, textNode);
}
});
updateCurrentHighlight();
}
function updateCurrentHighlight() {
const highlights =
elements.editorContent?.querySelectorAll(".find-highlight");
if (!highlights) return;
highlights.forEach((el, index) => {
el.classList.toggle("current", index === state.findMatchIndex);
});
}
function clearFindHighlights() {
if (!elements.editorContent) return;
const highlights =
elements.editorContent.querySelectorAll(".find-highlight");
highlights.forEach((el) => {
const parent = el.parentNode;
parent.replaceChild(document.createTextNode(el.textContent), el);
parent.normalize();
});
const wrapperSpans = elements.editorContent.querySelectorAll("span:empty");
wrapperSpans.forEach((span) => {
if (span.childNodes.length === 0) {
span.remove();
}
});
}
function updateFindResults() {
const resultsEl = document.getElementById("findResults");
if (resultsEl) {
const count = state.findMatches.length;
const span = resultsEl.querySelector("span");
if (span) {
span.textContent =
count === 0
? "0 matches found"
: `${state.findMatchIndex + 1} of ${count} matches`;
}
}
}
function scrollToMatch() {
const highlights =
elements.editorContent?.querySelectorAll(".find-highlight");
if (highlights && highlights[state.findMatchIndex]) {
highlights[state.findMatchIndex].scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}
function findNext() {
if (state.findMatches.length === 0) return;
state.findMatchIndex =
(state.findMatchIndex + 1) % state.findMatches.length;
updateCurrentHighlight();
scrollToMatch();
updateFindResults();
}
function findPrev() {
if (state.findMatches.length === 0) return;
state.findMatchIndex =
(state.findMatchIndex - 1 + state.findMatches.length) %
state.findMatches.length;
updateCurrentHighlight();
scrollToMatch();
updateFindResults();
}
function replaceOne() {
if (state.findMatches.length === 0 || state.findMatchIndex < 0) return;
const replaceText = document.getElementById("replaceInput")?.value || "";
const highlights =
elements.editorContent?.querySelectorAll(".find-highlight");
if (highlights && highlights[state.findMatchIndex]) {
const highlight = highlights[state.findMatchIndex];
highlight.replaceWith(document.createTextNode(replaceText));
elements.editorContent.normalize();
state.findMatches.splice(state.findMatchIndex, 1);
if (state.findMatches.length > 0) {
state.findMatchIndex = state.findMatchIndex % state.findMatches.length;
updateCurrentHighlight();
scrollToMatch();
} else {
state.findMatchIndex = -1;
}
updateFindResults();
state.isDirty = true;
scheduleAutoSave();
}
}
function replaceAll() {
if (state.findMatches.length === 0) return;
const replaceText = document.getElementById("replaceInput")?.value || "";
const highlights =
elements.editorContent?.querySelectorAll(".find-highlight");
if (highlights) {
const count = highlights.length;
highlights.forEach((highlight) => {
highlight.replaceWith(document.createTextNode(replaceText));
});
elements.editorContent.normalize();
state.findMatches = [];
state.findMatchIndex = -1;
updateFindResults();
state.isDirty = true;
scheduleAutoSave();
addChatMessage("assistant", `Replaced ${count} occurrences.`);
}
}
function showPrintPreview() {
showModal("printPreviewModal");
updatePrintPreview();
}
function updatePrintPreview() {
const orientation =
document.getElementById("printOrientation")?.value || "portrait";
const showHeaders = document.getElementById("printHeaders")?.checked;
const printPage = document.getElementById("printPage");
const printContent = document.getElementById("printContent");
const printHeader = document.getElementById("printHeader");
const printFooter = document.getElementById("printFooter");
if (printPage) {
printPage.className = `print-page ${orientation}`;
}
if (printHeader) {
printHeader.innerHTML = showHeaders ? state.docTitle : "";
printHeader.style.display = showHeaders ? "block" : "none";
}
if (printFooter) {
printFooter.innerHTML = showHeaders ? "Page 1" : "";
printFooter.style.display = showHeaders ? "block" : "none";
}
if (printContent && elements.editorContent) {
printContent.innerHTML = elements.editorContent.innerHTML;
}
}
function printDocument() {
const orientation =
document.getElementById("printOrientation")?.value || "portrait";
const showHeaders = document.getElementById("printHeaders")?.checked;
const content = elements.editorContent?.innerHTML || "";
const printWindow = window.open("", "_blank");
printWindow.document.write(`
${state.docTitle}
${showHeaders ? `` : ""}
${content}
`);
printWindow.document.close();
printWindow.focus();
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 250);
hideModal("printPreviewModal");
}
function insertPageBreak() {
if (!elements.editorContent) return;
const pageBreak = document.createElement("div");
pageBreak.className = "page-break";
pageBreak.contentEditable = "false";
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(pageBreak);
const newParagraph = document.createElement("p");
newParagraph.innerHTML = "
";
pageBreak.after(newParagraph);
range.setStartAfter(newParagraph);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
} else {
elements.editorContent.appendChild(pageBreak);
}
state.isDirty = true;
scheduleAutoSave();
}
function showHeaderFooterModal() {
showModal("headerFooterModal");
const headerEditor = document.getElementById("headerEditor");
const footerEditor = document.getElementById("footerEditor");
if (headerEditor && elements.editorHeader) {
headerEditor.innerHTML = elements.editorHeader.innerHTML;
}
if (footerEditor && elements.editorFooter) {
footerEditor.innerHTML = elements.editorFooter.innerHTML;
}
}
function switchHfTab(tabName) {
document.querySelectorAll(".hf-tab").forEach((tab) => {
tab.classList.toggle("active", tab.dataset.tab === tabName);
});
document
.getElementById("hfHeaderTab")
?.classList.toggle("active", tabName === "header");
document
.getElementById("hfFooterTab")
?.classList.toggle("active", tabName === "footer");
}
function insertHfField(type, field) {
const editorId = type === "header" ? "headerEditor" : "footerEditor";
const editor = document.getElementById(editorId);
if (!editor) return;
let fieldContent = "";
switch (field) {
case "pageNum":
fieldContent =
'[Page #]';
break;
case "date":
fieldContent = `${new Date().toLocaleDateString()}`;
break;
case "title":
fieldContent = `${state.docTitle}`;
break;
}
editor.focus();
document.execCommand("insertHTML", false, fieldContent);
}
function applyHeaderFooter() {
const headerEditor = document.getElementById("headerEditor");
const footerEditor = document.getElementById("footerEditor");
if (elements.editorHeader && headerEditor) {
elements.editorHeader.innerHTML = headerEditor.innerHTML;
}
if (elements.editorFooter && footerEditor) {
elements.editorFooter.innerHTML = footerEditor.innerHTML;
}
hideModal("headerFooterModal");
state.isDirty = true;
scheduleAutoSave();
addChatMessage("assistant", "Header and footer updated!");
}
function removeHeaderFooter() {
if (elements.editorHeader) {
elements.editorHeader.innerHTML = "";
}
if (elements.editorFooter) {
elements.editorFooter.innerHTML = "";
}
const headerEditor = document.getElementById("headerEditor");
const footerEditor = document.getElementById("footerEditor");
if (headerEditor) headerEditor.innerHTML = "";
if (footerEditor) footerEditor.innerHTML = "";
hideModal("headerFooterModal");
state.isDirty = true;
scheduleAutoSave();
addChatMessage("assistant", "Header and footer removed.");
}
function handleHeaderFooterInput() {
state.isDirty = true;
scheduleAutoSave();
}
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();
}
})();