2026-01-06 22:57:00 -03:00
( 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 : [ ] ,
2026-01-11 09:56:44 -03:00
chatPanelOpen : true ,
2026-01-10 20:12:48 -03:00
driveSource : null ,
2026-01-11 09:56:44 -03:00
zoom : 100 ,
2026-01-06 22:57:00 -03:00
} ;
2026-01-11 09:56:44 -03:00
const elements = { } ;
2026-01-06 22:57:00 -03:00
function init ( ) {
cacheElements ( ) ;
bindEvents ( ) ;
loadFromUrlParams ( ) ;
setupToolbar ( ) ;
setupKeyboardShortcuts ( ) ;
updateWordCount ( ) ;
2026-01-11 09:56:44 -03:00
connectWebSocket ( ) ;
2026-01-06 22:57:00 -03:00
}
function cacheElements ( ) {
2026-01-11 09:56:44 -03:00
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" ) ;
2026-01-06 22:57:00 -03:00
}
function bindEvents ( ) {
if ( elements . editorContent ) {
elements . editorContent . addEventListener ( "input" , handleEditorInput ) ;
elements . editorContent . addEventListener ( "keydown" , handleEditorKeydown ) ;
elements . editorContent . addEventListener ( "paste" , handlePaste ) ;
}
2026-01-11 09:56:44 -03:00
if ( elements . docName ) {
elements . docName . addEventListener ( "change" , handleDocNameChange ) ;
elements . docName . addEventListener ( "keydown" , ( e ) => {
if ( e . key === "Enter" ) {
e . preventDefault ( ) ;
elements . editorContent ? . focus ( ) ;
}
} ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
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 ) ) ;
} ) ;
2026-01-06 22:57:00 -03:00
window . addEventListener ( "beforeunload" , handleBeforeUnload ) ;
}
2026-01-11 09:56:44 -03:00
function handleEditorInput ( ) {
saveToHistory ( ) ;
2026-01-06 22:57:00 -03:00
state . isDirty = true ;
updateWordCount ( ) ;
scheduleAutoSave ( ) ;
2026-01-11 09:56:44 -03:00
broadcastChange ( ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function handleDocNameChange ( ) {
state . docTitle = elements . docName . value || "Untitled Document" ;
2026-01-06 22:57:00 -03:00
state . isDirty = true ;
scheduleAutoSave ( ) ;
}
function handleEditorKeydown ( e ) {
2026-01-11 09:56:44 -03:00
if ( e . ctrlKey || e . metaKey ) {
switch ( e . key . toLowerCase ( ) ) {
case "b" :
2026-01-06 22:57:00 -03:00
e . preventDefault ( ) ;
2026-01-11 09:56:44 -03:00
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 ;
2026-01-06 22:57:00 -03:00
}
}
}
function handlePaste ( e ) {
e . preventDefault ( ) ;
2026-01-11 09:56:44 -03:00
const text = e . clipboardData . getData ( "text/plain" ) ;
2026-01-06 22:57:00 -03:00
document . execCommand ( "insertText" , false , text ) ;
}
function handleBeforeUnload ( e ) {
if ( state . isDirty ) {
e . preventDefault ( ) ;
e . returnValue = "" ;
}
}
2026-01-11 09:56:44 -03:00
function setupToolbar ( ) {
updateToolbarState ( ) ;
if ( elements . editorContent ) {
elements . editorContent . addEventListener ( "mouseup" , updateToolbarState ) ;
elements . editorContent . addEventListener ( "keyup" , updateToolbarState ) ;
2026-01-06 22:57:00 -03:00
}
}
2026-01-11 09:56:44 -03:00
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" ) ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function setupKeyboardShortcuts ( ) {
document . addEventListener ( "keydown" , ( e ) => {
if ( e . target . closest ( ".chat-input, .modal input" ) ) return ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
if ( e . key === "Escape" ) {
closeModals ( ) ;
}
2026-01-06 22:57:00 -03:00
} ) ;
}
2026-01-11 09:56:44 -03:00
function execCommand ( command , value = null ) {
elements . editorContent ? . focus ( ) ;
document . execCommand ( command , false , value ) ;
saveToHistory ( ) ;
2026-01-06 22:57:00 -03:00
state . isDirty = true ;
scheduleAutoSave ( ) ;
2026-01-11 09:56:44 -03:00
updateToolbarState ( ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function handleHeadingChange ( e ) {
const value = e . target . value ;
execCommand ( "formatBlock" , value ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function handleFontFamilyChange ( e ) {
execCommand ( "fontName" , e . target . value ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function handleFontSizeChange ( e ) {
execCommand ( "fontSize" , e . target . value ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function handleTextColorChange ( e ) {
execCommand ( "foreColor" , e . target . value ) ;
const indicator = document . querySelector ( "#textColorBtn .color-indicator" ) ;
if ( indicator ) indicator . style . background = e . target . value ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function handleHighlightChange ( e ) {
execCommand ( "hiliteColor" , e . target . value ) ;
const indicator = document . querySelector ( "#highlightBtn .color-indicator" ) ;
if ( indicator ) indicator . style . background = e . target . value ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
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 ++ ;
2026-01-06 22:57:00 -03:00
}
}
2026-01-11 09:56:44 -03:00
function undo ( ) {
if ( state . historyIndex > 0 ) {
state . historyIndex -- ;
if ( elements . editorContent ) {
elements . editorContent . innerHTML = state . history [ state . historyIndex ] ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
state . isDirty = true ;
updateWordCount ( ) ;
2026-01-06 22:57:00 -03:00
}
}
2026-01-11 09:56:44 -03:00
function redo ( ) {
if ( state . historyIndex < state . history . length - 1 ) {
state . historyIndex ++ ;
if ( elements . editorContent ) {
elements . editorContent . innerHTML = state . history [ state . historyIndex ] ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
state . isDirty = true ;
updateWordCount ( ) ;
2026-01-06 22:57:00 -03:00
}
}
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 ) {
2026-01-11 09:56:44 -03:00
elements . wordCount . textContent = ` ${ words . length } word ${ words . length !== 1 ? "s" : "" } ` ;
2026-01-06 22:57:00 -03:00
}
if ( elements . charCount ) {
2026-01-11 09:56:44 -03:00
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 ( ) ;
2026-01-06 22:57:00 -03:00
}
}
2026-01-11 09:56:44 -03:00
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 } % ` ;
}
}
2026-01-06 22:57:00 -03:00
function scheduleAutoSave ( ) {
if ( state . autoSaveTimer ) {
clearTimeout ( state . autoSaveTimer ) ;
}
2026-01-11 09:56:44 -03:00
state . autoSaveTimer = setTimeout ( saveDocument , CONFIG . AUTOSAVE _DELAY ) ;
if ( elements . saveStatus ) {
elements . saveStatus . textContent = "Saving..." ;
}
2026-01-06 22:57:00 -03:00
}
async function saveDocument ( ) {
2026-01-11 09:56:44 -03:00
if ( ! state . isDirty ) return ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
const content = elements . editorContent ? . innerHTML || "" ;
const title = state . docTitle ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
try {
const response = await fetch ( "/api/docs/save" , {
2026-01-06 22:57:00 -03:00
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( {
id : state . docId ,
title ,
content ,
2026-01-11 09:56:44 -03:00
driveSource : state . driveSource ,
2026-01-06 22:57:00 -03:00
} ) ,
} ) ;
if ( response . ok ) {
2026-01-11 09:56:44 -03:00
const result = await response . json ( ) ;
if ( result . id ) {
state . docId = result . id ;
2026-01-06 22:57:00 -03:00
window . history . replaceState ( { } , "" , ` #id= ${ state . docId } ` ) ;
}
state . isDirty = false ;
2026-01-11 09:56:44 -03:00
if ( elements . saveStatus ) {
elements . saveStatus . textContent = "Saved" ;
}
2026-01-06 22:57:00 -03:00
} else {
if ( elements . saveStatus ) {
2026-01-11 09:56:44 -03:00
elements . saveStatus . textContent = "Save failed" ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
}
} catch ( e ) {
console . error ( "Save error:" , e ) ;
if ( elements . saveStatus ) {
elements . saveStatus . textContent = "Save failed" ;
}
2026-01-06 22:57:00 -03:00
}
}
async function loadFromUrlParams ( ) {
const urlParams = new URLSearchParams ( window . location . search ) ;
const hash = window . location . hash ;
let docId = urlParams . get ( "id" ) ;
2026-01-10 20:12:48 -03:00
let bucket = urlParams . get ( "bucket" ) ;
let path = urlParams . get ( "path" ) ;
if ( hash ) {
const hashQueryIndex = hash . indexOf ( "?" ) ;
2026-01-11 09:56:44 -03:00
if ( hashQueryIndex > - 1 ) {
const hashParams = new URLSearchParams ( hash . slice ( hashQueryIndex + 1 ) ) ;
2026-01-10 20:12:48 -03:00
docId = docId || hashParams . get ( "id" ) ;
bucket = bucket || hashParams . get ( "bucket" ) ;
path = path || hashParams . get ( "path" ) ;
2026-01-11 09:56:44 -03:00
} else if ( hash . startsWith ( "#id=" ) ) {
docId = hash . slice ( 4 ) ;
2026-01-10 20:12:48 -03:00
}
2026-01-06 22:57:00 -03:00
}
2026-01-10 20:12:48 -03:00
if ( bucket && path ) {
2026-01-11 09:56:44 -03:00
state . driveSource = { bucket , path } ;
2026-01-10 20:12:48 -03:00
await loadFromDrive ( bucket , path ) ;
} else if ( docId ) {
2026-01-06 22:57:00 -03:00
try {
2026-01-11 09:56:44 -03:00
const response = await fetch ( ` /api/docs/ ${ docId } ` ) ;
2026-01-06 22:57:00 -03:00
if ( response . ok ) {
const data = await response . json ( ) ;
state . docId = docId ;
state . docTitle = data . title || "Untitled Document" ;
2026-01-11 09:56:44 -03:00
if ( elements . docName ) elements . docName . value = state . docTitle ;
if ( elements . editorContent )
2026-01-06 22:57:00 -03:00
elements . editorContent . innerHTML = data . content || "" ;
2026-01-11 09:56:44 -03:00
saveToHistory ( ) ;
2026-01-06 22:57:00 -03:00
updateWordCount ( ) ;
}
} catch ( e ) {
console . error ( "Load failed:" , e ) ;
}
2026-01-11 09:56:44 -03:00
} else {
saveToHistory ( ) ;
2026-01-06 22:57:00 -03:00
}
}
2026-01-10 20:12:48 -03:00
async function loadFromDrive ( bucket , path ) {
2026-01-11 09:56:44 -03:00
const fileName = path . split ( "/" ) . pop ( ) || "Document" ;
const ext = fileName . split ( "." ) . pop ( ) ? . toLowerCase ( ) ;
2026-01-10 20:12:48 -03:00
try {
2026-01-11 09:56:44 -03:00
const response = await fetch ( "/api/drive/content" , {
2026-01-10 20:12:48 -03:00
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { bucket , path } ) ,
} ) ;
2026-01-11 09:56:44 -03:00
if ( response . ok ) {
const data = await response . json ( ) ;
const content = data . content || "" ;
2026-01-10 20:12:48 -03:00
2026-01-11 09:56:44 -03:00
state . docTitle = fileName . replace ( /\.[^.]+$/ , "" ) ;
if ( elements . docName ) elements . docName . value = state . docTitle ;
2026-01-10 20:12:48 -03:00
2026-01-11 09:56:44 -03:00
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 ;
}
2026-01-10 20:12:48 -03:00
}
2026-01-11 09:56:44 -03:00
saveToHistory ( ) ;
updateWordCount ( ) ;
}
} catch ( e ) {
console . error ( "Drive load failed:" , e ) ;
2026-01-10 20:12:48 -03:00
}
}
function markdownToHtml ( md ) {
return md
2026-01-11 09:56:44 -03:00
. replace ( /^### (.+)$/gm , "<h3>$1</h3>" )
. replace ( /^## (.+)$/gm , "<h2>$1</h2>" )
. replace ( /^# (.+)$/gm , "<h1>$1</h1>" )
2026-01-10 20:12:48 -03:00
. replace ( /\*\*(.+?)\*\*/g , "<strong>$1</strong>" )
. replace ( /\*(.+?)\*/g , "<em>$1</em>" )
. replace ( /`(.+?)`/g , "<code>$1</code>" )
2026-01-11 09:56:44 -03:00
. replace ( /\n/g , "<br>" ) ;
2026-01-10 20:12:48 -03:00
}
2026-01-11 09:56:44 -03:00
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 ;
}
2026-01-10 20:12:48 -03:00
}
2026-01-11 09:56:44 -03:00
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 ) ;
}
}
2026-01-06 22:57:00 -03:00
function exportDocument ( format ) {
2026-01-11 09:56:44 -03:00
const title = state . docTitle || "document" ;
2026-01-06 22:57:00 -03:00
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 ;
}
2026-01-11 09:56:44 -03:00
hideModal ( "exportModal" ) ;
2026-01-06 22:57:00 -03:00
}
function exportAsPDF ( title , content ) {
const printWindow = window . open ( "" , "_blank" ) ;
if ( printWindow ) {
printWindow . document . write ( `
< ! DOCTYPE html >
< html >
< head >
< title > $ { escapeHtml ( title ) } < / t i t l e >
< style >
2026-01-11 09:56:44 -03:00
body { font - family : Arial , sans - serif ; padding : 40 px ; max - width : 800 px ; margin : 0 auto ; }
h1 , h2 , h3 { margin - top : 1 em ; }
p { line - height : 1.6 ; }
table { border - collapse : collapse ; width : 100 % ; }
th , td { border : 1 px solid # ccc ; padding : 8 px ; }
2026-01-06 22:57:00 -03:00
< / s t y l e >
< / h e a d >
2026-01-11 09:56:44 -03:00
< body > $ { content } < / b o d y >
2026-01-06 22:57:00 -03:00
< / h t m l >
` );
printWindow . document . close ( ) ;
printWindow . print ( ) ;
}
}
function exportAsDocx ( title , content ) {
2026-01-11 09:56:44 -03:00
addChatMessage (
"assistant" ,
"DOCX export requires server-side processing. Feature coming soon!" ,
) ;
2026-01-06 22:57:00 -03:00
}
function exportAsHTML ( title , content ) {
const html = ` <!DOCTYPE html>
2026-01-11 09:56:44 -03:00
< html lang = "en" >
2026-01-06 22:57:00 -03:00
< head >
< meta charset = "UTF-8" >
< title > $ { escapeHtml ( title ) } < / t i t l e >
< style >
2026-01-11 09:56:44 -03:00
body { font - family : Arial , sans - serif ; padding : 40 px ; max - width : 800 px ; margin : 0 auto ; }
h1 , h2 , h3 { margin - top : 1 em ; }
p { line - height : 1.6 ; }
table { border - collapse : collapse ; width : 100 % ; }
th , td { border : 1 px solid # ccc ; padding : 8 px ; }
2026-01-06 22:57:00 -03:00
< / s t y l e >
< / h e a d >
< body >
2026-01-11 09:56:44 -03:00
$ { content }
2026-01-06 22:57:00 -03:00
< / b o d y >
< / h t m l > ` ;
2026-01-11 09:56:44 -03:00
downloadFile ( html , ` ${ title } .html ` , "text/html" ) ;
2026-01-06 22:57:00 -03:00
}
function exportAsTxt ( title ) {
const text = elements . editorContent ? . innerText || "" ;
2026-01-11 09:56:44 -03:00
downloadFile ( text , ` ${ title } .txt ` , "text/plain" ) ;
2026-01-06 22:57:00 -03:00
}
function exportAsMarkdown ( title ) {
const text = elements . editorContent ? . innerText || "" ;
2026-01-11 09:56:44 -03:00
const md = ` # ${ title } \n \n ${ text } ` ;
downloadFile ( md , ` ${ title } .md ` , "text/markdown" ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function downloadFile ( content , filename , mimeType ) {
const blob = new Blob ( [ content ] , { type : mimeType } ) ;
2026-01-06 22:57:00 -03:00
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( "a" ) ;
a . href = url ;
a . download = filename ;
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
}
2026-01-11 09:56:44 -03:00
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 ) ;
}
} ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
state . ws . onclose = ( ) => {
setTimeout ( connectWebSocket , CONFIG . WS _RECONNECT _DELAY ) ;
} ;
} catch ( e ) {
console . error ( "WebSocket failed:" , e ) ;
2026-01-06 22:57:00 -03:00
}
}
2026-01-11 09:56:44 -03:00
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 ;
}
}
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
function broadcastChange ( ) {
if ( state . ws && state . ws . readyState === WebSocket . OPEN ) {
state . ws . send (
JSON . stringify ( {
type : "content_update" ,
userId : getUserId ( ) ,
content : elements . editorContent ? . innerHTML || "" ,
} ) ,
) ;
}
}
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
function addCollaborator ( user ) {
if ( ! state . collaborators . find ( ( u ) => u . id === user . id ) ) {
state . collaborators . push ( user ) ;
renderCollaborators ( ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
}
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 || " # 4285 f4 "}" title = "${escapeHtml(u.name)}" >
$ { u . name . charAt ( 0 ) . toUpperCase ( ) }
< / d i v >
` ,
)
. 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 ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
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." ;
}
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
addChatMessage ( "assistant" , response ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
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." ;
}
}
2026-01-06 22:57:00 -03:00
function escapeHtml ( str ) {
if ( ! str ) return "" ;
const div = document . createElement ( "div" ) ;
div . textContent = str ;
return div . innerHTML ;
}
2026-01-11 09:56:44 -03:00
function createNewDocument ( ) {
state . docId = null ;
state . docTitle = "Untitled Document" ;
state . isDirty = false ;
state . history = [ ] ;
state . historyIndex = - 1 ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
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 ( ) ;
}
2026-01-06 22:57:00 -03:00
window . gbDocs = {
init ,
createNewDocument ,
saveDocument ,
exportDocument ,
showModal ,
hideModal ,
closeModals ,
2026-01-11 09:56:44 -03:00
toggleChatPanel ,
execCommand ,
2026-01-06 22:57:00 -03:00
} ;
if ( document . readyState === "loading" ) {
document . addEventListener ( "DOMContentLoaded" , init ) ;
} else {
init ( ) ;
}
} ) ( ) ;