botui/ui/suite/docs/docs.js
Rodrigo Rodriguez (Pragmatismo) 10299814b2 M365-like UI for Sheet, Docs, and Slides editors
- Clean editor experience without document list sidebars
- Theme-aware using --sentient-* CSS variables
- Modern toolbar design like Microsoft 365
- Sheet: Formula bar, spreadsheet grid, sheet tabs, zoom controls
- Docs: A4 page centered with shadow, formatting toolbar
- Slides: Thumbnails on left, canvas in center, properties panel
- AI chat panel (collapsible) for all editors
- Responsive design with mobile support
- Print styles for all editors
- Dark/light mode support via theme system
2026-01-11 09:56:44 -03:00

1043 lines
31 KiB
JavaScript

(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 = `<p>${escapeHtml(content).replace(/\n/g, "</p><p>")}</p>`;
}
} 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, "<h3>$1</h3>")
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
.replace(/^# (.+)$/gm, "<h1>$1</h1>")
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.+?)\*/g, "<em>$1</em>")
.replace(/`(.+?)`/g, "<code>$1</code>")
.replace(/\n/g, "<br>");
}
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,
`<a href="${escapeHtml(url)}" target="_blank">${escapeHtml(text)}</a>`,
);
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,
`<img src="${escapeHtml(url)}" alt="${escapeHtml(alt)}" style="max-width:100%">`,
);
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 = '<table style="border-collapse:collapse;width:100%">';
for (let r = 0; r < rows; r++) {
html += "<tr>";
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 += "</tr>";
}
html += "</table><p></p>";
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(`
<!DOCTYPE html>
<html>
<head>
<title>${escapeHtml(title)}</title>
<style>
body { font-family: Arial, sans-serif; padding: 40px; max-width: 800px; margin: 0 auto; }
h1, h2, h3 { margin-top: 1em; }
p { line-height: 1.6; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccc; padding: 8px; }
</style>
</head>
<body>${content}</body>
</html>
`);
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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${escapeHtml(title)}</title>
<style>
body { font-family: Arial, sans-serif; padding: 40px; max-width: 800px; margin: 0 auto; }
h1, h2, h3 { margin-top: 1em; }
p { line-height: 1.6; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccc; padding: 8px; }
</style>
</head>
<body>
${content}
</body>
</html>`;
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) => `
<div class="collaborator-avatar" style="background:${u.color || "#4285f4"}" title="${escapeHtml(u.name)}">
${u.name.charAt(0).toUpperCase()}
</div>
`,
)
.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 = `<div class="message-bubble">${escapeHtml(content)}</div>`;
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();
}
})();