1/** 2 * JavaScript for LLM Integration Plugin 3 * 4 * This script adds LLM processing capabilities to DokuWiki's edit interface. 5 * It creates a toolbar with buttons for various text processing operations 6 * and handles the communication with the backend plugin. 7 * 8 * Features: 9 * - Context-aware text processing with metadata support 10 * - Template content insertion 11 * - Custom prompt input 12 * - Selected text processing 13 * - Full page content processing 14 * - Text analysis in modal dialog 15 */ 16 17(function() { 18 'use strict'; 19 20 /** 21 * Initialize the plugin when the DOM is ready 22 * 23 * This is the main initialization function that runs when the DOM is fully loaded. 24 * It checks if we're on an edit page and adds the LLM tools if so. 25 * Only runs on pages with the wiki text editor. 26 * Also sets up the copy page button event listener. 27 * 28 * Complex logic includes: 29 * 1. Checking for the presence of the wiki text editor element 30 * 2. Conditionally adding LLM tools based on page context 31 * 3. Setting up event listeners for the copy page functionality 32 */ 33 document.addEventListener('DOMContentLoaded', function() { 34 console.log('DokuLLM: DOM loaded, initializing plugin'); 35 // Only run on edit pages 36 if (document.getElementById('wiki__text')) { 37 // Add LLM tools to the editor 38 console.log('DokuLLM: Adding LLM tools to editor'); 39 addLLMTools(); 40 } 41 42 // Add event listener for copy button 43 const copyButton = document.querySelector('.dokullmplugin__copy'); 44 if (copyButton) { 45 copyButton.addEventListener('click', function(event) { 46 event.preventDefault(); 47 copyPage(); 48 }); 49 } 50 51 // Dirty hack to handle selection of mobile menu 52 // See: https://github.com/splitbrain/dokuwiki/blob/release_stable_2018-04-22/lib/scripts/behaviour.js#L102-L115 53 const quickSelect = jQuery('select.quickselect'); 54 if (quickSelect.length > 0) { 55 quickSelect 56 .unbind('change') // Remove dokuwiki's default handler to override its behavior 57 .change(function(e) { 58 if (e.target.value != 'dokullmplugin__copy') { 59 // do the default action 60 e.target.form.submit(); 61 return; 62 } 63 64 e.target.value = ''; // Reset selection to enable re-select when a prompt is canceled 65 copyPage(); 66 }); 67 } 68 }); 69 70 /** 71 * Add the LLM toolbar to the editor interface 72 * 73 * Creates a toolbar with buttons for each LLM operation and inserts 74 * it before the wiki text editor. Also adds a custom prompt input 75 * below the editor. 76 * 77 * Dynamically adds a template button when template metadata is present. 78 * 79 * Complex logic includes: 80 * 1. Creating and positioning the main toolbar container 81 * 2. Dynamically adding a template button based on metadata presence 82 * 3. Creating standard LLM operation buttons with event handlers 83 * 4. Adding a custom prompt input field with Enter key handling 84 * 5. Inserting all UI elements at appropriate positions in the DOM 85 * 6. Applying CSS styles for consistent appearance 86 */ 87 function addLLMTools() { 88 const editor = document.getElementById('wiki__text'); 89 if (!editor) { 90 console.log('DokuLLM: Editor div not found'); 91 return; 92 } 93 94 console.log('DokuLLM: Creating LLM toolbar'); 95 // Create toolbar container 96 const toolbar = document.createElement('div'); 97 toolbar.id = 'llm-toolbar'; 98 toolbar.className = 'toolbar'; 99 100 // Get metadata to check if template exists 101 const metadata = getMetadata(); 102 console.log('DokuLLM: Page metadata retrieved', metadata); 103 104 // Add "Insert template" button if template is defined 105 if (metadata.template) { 106 console.log('DokuLLM: Adding insert template button for', metadata.template); 107 const templateBtn = document.createElement('button'); 108 templateBtn.type = 'button'; 109 templateBtn.className = 'toolbutton'; 110 templateBtn.textContent = 'Insert Template'; 111 templateBtn.addEventListener('click', () => insertTemplateContent(metadata.template)); 112 toolbar.appendChild(templateBtn); 113 } else { 114 // Add "Find Template" button if no template is defined and ChromaDB is enabled 115 // Check if ChromaDB is enabled through JSINFO 116console.log(JSINFO); 117 const chromaDBEnabled = typeof JSINFO !== 'undefined' && JSINFO.dokullm && JSINFO.dokullm.enable_chromadb; 118 if (chromaDBEnabled) { 119 console.log('DokuLLM: Adding find template button'); 120 const findTemplateBtn = document.createElement('button'); 121 findTemplateBtn.type = 'button'; 122 findTemplateBtn.className = 'toolbutton'; 123 findTemplateBtn.textContent = 'Find Template'; 124 findTemplateBtn.addEventListener('click', findTemplate); 125 toolbar.appendChild(findTemplateBtn); 126 } 127 } 128 129 // Add loading indicator while fetching actions 130 const loadingIndicator = document.createElement('span'); 131 loadingIndicator.textContent = 'Loading LLM actions...'; 132 loadingIndicator.id = 'llm-loading'; 133 toolbar.appendChild(loadingIndicator); 134 135 // Insert toolbar before the editor 136 editor.parentNode.insertBefore(toolbar, editor); 137 138 // Add custom prompt input below the editor 139 console.log('DokuLLM: Adding custom prompt input below editor'); 140 const customPromptContainer = document.createElement('div'); 141 customPromptContainer.className = 'llm-custom-prompt'; 142 customPromptContainer.id = 'llm-custom-prompt'; 143 144 const promptInput = document.createElement('input'); 145 promptInput.type = 'text'; 146 promptInput.placeholder = 'Enter custom prompt...'; 147 promptInput.className = 'llm-prompt-input'; 148 149 // Add event listener for Enter key 150 promptInput.addEventListener('keypress', function(e) { 151 if (e.key === 'Enter') { 152 processCustomPrompt(promptInput.value); 153 } 154 }); 155 156 const sendButton = document.createElement('button'); 157 sendButton.type = 'button'; 158 sendButton.className = 'toolbutton'; 159 sendButton.textContent = 'Send'; 160 sendButton.addEventListener('click', () => processCustomPrompt(promptInput.value)); 161 162 customPromptContainer.appendChild(promptInput); 163 customPromptContainer.appendChild(sendButton); 164 165 // Insert custom prompt container after the editor 166 editor.parentNode.insertBefore(customPromptContainer, editor.nextSibling); 167 168 // Fetch action definitions from the API 169 getActions() 170 .then(actions => { 171 // Remove loading indicator 172 const loadingElement = document.getElementById('llm-loading'); 173 if (loadingElement) { 174 loadingElement.remove(); 175 } 176 177 // Add buttons based on fetched actions 178 actions.forEach(action => { 179 const btn = document.createElement('button'); 180 btn.type = 'button'; 181 btn.className = 'toolbutton'; 182 btn.textContent = action.label; 183 btn.title = action.description || ''; 184 btn.dataset.action = action.id; 185 btn.dataset.result = action.result; 186 btn.addEventListener('click', function(event) { 187 processLLMAction(action.id, event); 188 }); 189 toolbar.appendChild(btn); 190 }); 191 192 console.log('DokuLLM: LLM toolbars added successfully'); 193 }) 194 .catch(error => { 195 console.error('DokuLLM: Error fetching action definitions:', error); 196 // Remove loading indicator and show error 197 const loadingElement = document.getElementById('llm-loading'); 198 if (loadingElement) { 199 loadingElement.textContent = 'Failed to load LLM actions'; 200 } 201 }); 202 } 203 204 /** 205 * Copy the current page to a new page ID 206 * 207 * Prompts the user for a new page ID and redirects to the edit page 208 * with the current page content pre-filled. 209 * 210 * Validates that the new ID is different from the current page ID. 211 * Handles user cancellation of the prompt dialog. 212 * 213 * Complex logic includes: 214 * 1. Prompting user for new page ID with current ID as default 215 * 2. Validating that new ID is different from current ID 216 * 3. Handling browser differences in prompt cancellation (null vs empty string) 217 * 4. Constructing the redirect URL with proper encoding 218 * 5. Redirecting to the new page edit view with copyfrom parameter 219 * 220 * Based on the DokuWiki CopyPage plugin 221 * @see https://www.dokuwiki.org/plugin:copypage 222 */ 223 function copyPage() { 224 // Original code: https://www.dokuwiki.org/plugin:copypage 225 var oldId = JSINFO.id; 226 while (true) { 227 var newId = prompt('Enter the new page ID:', oldId); 228 // Note: When a user canceled, most browsers return the null, but Safari returns the empty string 229 if (newId) { 230 if (newId === oldId) { 231 alert('The new page ID must be different from the current page ID.'); 232 continue; 233 } 234 var url = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(newId) + 235 '&do=edit©from=' + encodeURIComponent(oldId); 236 location.href = url; 237 } 238 break; 239 } 240 } 241 242 243 /** 244 * Process text using the specified LLM action 245 * 246 * Gets the selected text (or full editor content), sends it to the 247 * backend for processing, and handles the result based on the button's 248 * dataset.result property ('replace', 'append', 'insert', 'show'). 249 * 250 * Preserves page metadata when doing full page updates. 251 * Shows loading indicators during processing. 252 * 253 * Complex logic includes: 254 * 1. Determining text to process (selected vs full content) 255 * 2. Managing UI state during processing (loading indicators, readonly) 256 * 3. Constructing and sending AJAX requests with proper metadata 257 * 4. Handling response processing and error conditions 258 * 5. Updating editor content while preserving metadata based on result handling mode 259 * 6. Restoring UI state after processing 260 * 261 * @param {string} action - The action to perform (create, rewrite, etc.) 262 */ 263 // Store selection range for processing 264 let currentSelectionRange = null; 265 266 function processLLMAction(action, event) { 267 console.log('DokuLLM: Processing text with action:', action); 268 const editor = document.getElementById('wiki__text'); 269 if (!editor) { 270 console.log('DokuLLM: Editor not found'); 271 return; 272 } 273 274 // Store the current selection range 275 currentSelectionRange = { 276 start: editor.selectionStart, 277 end: editor.selectionEnd 278 }; 279 280 // Get metadata from the page 281 const metadata = getMetadata(); 282 console.log('DokuLLM: Retrieved metadata:', metadata); 283 284 const selectedText = getSelectedText(editor); 285 const fullText = editor.value; 286 const textToProcess = selectedText || fullText; 287 console.log('DokuLLM: Text to process length:', textToProcess.length); 288 289 if (!textToProcess.trim()) { 290 console.log('DokuLLM: No text to process'); 291 alert('Please select text or enter content to process'); 292 return; 293 } 294 295 // Disable the entire toolbar and prompt input 296 const toolbar = document.getElementById('llm-toolbar'); 297 const promptContainer = document.getElementById('llm-custom-prompt'); 298 const promptInput = promptContainer ? promptContainer.querySelector('.llm-prompt-input') : null; 299 const buttons = toolbar.querySelectorAll('button:not(.llm-modal-close)'); 300 301 // Store original states for restoration 302 const originalStates = { 303 promptInput: promptInput ? promptInput.disabled : false, 304 buttons: [] 305 }; 306 307 // Disable prompt input if it exists 308 if (promptInput) { 309 originalStates.promptInput = promptInput.disabled; 310 promptInput.disabled = true; 311 } 312 313 // Disable all buttons and store their original states 314 buttons.forEach(button => { 315 originalStates.buttons.push({ 316 element: button, 317 text: button.textContent, 318 disabled: button.disabled 319 }); 320 // Only change text of the button that triggered the action 321 if (event && event.target === button) { 322 button.textContent = 'Processing...'; 323 } 324 button.disabled = true; 325 }); 326 console.log('DokuLLM: Toolbar disabled, showing processing state'); 327 328 // Make textarea readonly during processing 329 editor.readOnly = true; 330 331 // Send AJAX request 332 console.log('DokuLLM: Sending AJAX request to backend'); 333 const formData = new FormData(); 334 formData.append('call', 'plugin_dokullm'); 335 formData.append('action', action); 336 formData.append('text', textToProcess); 337 formData.append('prompt', ''); 338 // Append metadata fields generically 339 for (const [key, value] of Object.entries(metadata)) { 340 if (Array.isArray(value)) { 341 formData.append(key, value.join('\n')); 342 } else if (value) { 343 formData.append(key, value); 344 } 345 } 346 347 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 348 method: 'POST', 349 body: formData 350 }) 351 .then(response => { 352 if (!response.ok) { 353 return response.text().then(text => { 354 throw new Error(`Network response was not ok: ${response.status} ${response.statusText} - ${text}`); 355 }); 356 } 357 console.log('DokuLLM: Received response from backend'); 358 return response.json(); 359 }) 360 .then(data => { 361 if (data.error) { 362 console.log('DokuLLM: Error from backend:', data.error); 363 throw new Error(data.error); 364 } 365 366 console.log('DokuLLM: Processing successful, result length:', data.result.length); 367 368 // Remove some part 369 const [cleanedResult, thinkingContent] = cleanThinking(data.result); 370 371 // Determine how to handle the result based on button's dataset.result property 372 const resultHandling = event.target.dataset.result || 'replace'; 373 374 // Replace selected text or handle result based on resultHandling 375 if (resultHandling === 'show') { 376 console.log('DokuLLM: Showing result in modal'); 377 const buttonTitle = event.target.title || action; 378 showModal(cleanedResult, action, buttonTitle); 379 } else if (resultHandling === 'append') { 380 console.log('DokuLLM: Appending result to existing text'); 381 // Append to the end of existing content (preserving metadata) 382 const metadata = extractMetadata(editor.value); 383 const contentWithoutMetadata = editor.value.substring(metadata.length); 384 editor.value = metadata + contentWithoutMetadata + '\n\n' + cleanedResult; 385 // Show thinking content in modal if it exists and thinking is enabled 386 if (thinkingContent) { 387 showModal(thinkingContent, 'thinking', 'AI Thinking Process'); 388 } 389 } else if (resultHandling === 'insert') { 390 console.log('DokuLLM: Inserting result before existing text'); 391 // Insert before existing content (preserving metadata) 392 const metadata = extractMetadata(editor.value); 393 const contentWithoutMetadata = editor.value.substring(metadata.length); 394 editor.value = metadata + cleanedResult + '\n\n' + contentWithoutMetadata; 395 // Show thinking content in modal if it exists and thinking is enabled 396 if (thinkingContent) { 397 showModal(thinkingContent, 'thinking', 'AI Thinking Process'); 398 } 399 } else if (selectedText) { 400 console.log('DokuLLM: Replacing selected text'); 401 replaceSelectedText(editor, cleanedResult); 402 // Show thinking content in modal if it exists and thinking is enabled 403 if (thinkingContent) { 404 showModal(thinkingContent, 'thinking', 'AI Thinking Process'); 405 } 406 } else { 407 console.log('DokuLLM: Replacing full text content'); 408 // Preserve metadata when doing full page update 409 const metadata = extractMetadata(editor.value); 410 editor.value = metadata + cleanedResult; 411 // Show thinking content in modal if it exists and thinking is enabled 412 if (thinkingContent) { 413 showModal(thinkingContent, 'thinking', 'AI Thinking Process'); 414 } 415 } 416 }) 417 .catch(error => { 418 console.log('DokuLLM: Error during processing:', error.message); 419 alert('Error: ' + error.message); 420 }) 421 .finally(() => { 422 console.log('DokuLLM: Resetting toolbar and enabling editor'); 423 // Re-enable the toolbar and prompt input 424 if (promptInput) { 425 promptInput.disabled = originalStates.promptInput; 426 } 427 originalStates.buttons.forEach(buttonState => { 428 buttonState.element.textContent = buttonState.text; 429 buttonState.element.disabled = buttonState.disabled; 430 }); 431 editor.readOnly = false; 432 }); 433 } 434 435 /** 436 * Convert markdown/DokuWiki text to HTML 437 * 438 * Performs basic conversion of markdown/DokuWiki syntax to HTML. 439 * Supports headings, lists, inline formatting, and code blocks. 440 * 441 * @param {string} text - The markdown/DokuWiki text to convert 442 * @returns {string} The converted HTML 443 */ 444 function convertToHtml(text) { 445 // Process code blocks first (```code```) 446 let html = text.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>'); 447 448 // Process DokuWiki file blocks ({{file>page#section}}) 449 html = html.replace(/\{\{file>([^}]+)\}\}/g, '<div class="include">$1</div>'); 450 451 // Process DokuWiki includes ({{page}}) 452 html = html.replace(/\{\{([^}]+)\}\}/g, '<div class="include">$1</div>'); 453 454 // Process inline code (`code`) 455 html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); 456 457 // Process DokuWiki headings (====== Heading ======) 458 html = html.replace(/^====== (.*?) ======$/gm, '<h1>$1</h1>'); 459 html = html.replace(/^===== (.*?) =====$/gm, '<h2>$1</h2>'); 460 html = html.replace(/^==== (.*?) ====$/gm, '<h3>$1</h3>'); 461 html = html.replace(/^=== (.*?) ===$/gm, '<h4>$1</h4>'); 462 html = html.replace(/^== (.*?) ==$/gm, '<h5>$1</h5>'); 463 html = html.replace(/^= (.*?) =$/gm, '<h6>$1</h6>'); 464 465 // Process markdown headings (# Heading, ## Heading, etc.) 466 html = html.replace(/^###### (.*$)/gm, '<h6>$1</h6>'); 467 html = html.replace(/^##### (.*$)/gm, '<h5>$1</h5>'); 468 html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>'); 469 html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>'); 470 html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>'); 471 html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>'); 472 473 // Process DokuWiki bold (**text**) 474 html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); 475 476 // Process DokuWiki italic (//text//) 477 html = html.replace(/\/\/(.*?)\/\//g, '<em>$1</em>'); 478 479 // Process markdown bold (__text__) 480 html = html.replace(/__(.*?)__/g, '<strong>$1</strong>'); 481 482 // Process markdown italic (*text* or _text_) 483 html = html.replace(/\*(.*?)\*/g, '<em>$1</em>'); 484 html = html.replace(/_(.*?)_/g, '<em>$1</em>'); 485 486 // Process DokuWiki external links ([[http://example.com|text]]) 487 html = html.replace(/\[\[(https?:\/\/[^\]|]+)\|([^\]]+)\]\]/g, '<a href="$1" target="_blank">$2</a>'); 488 489 // Process DokuWiki external links ([[http://example.com]]) 490 html = html.replace(/\[\[(https?:\/\/[^\]]+)\]\]/g, '<a href="$1" target="_blank">$1</a>'); 491 492 // Process DokuWiki internal links ([[page|text]]) 493 html = html.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '<a href="?id=$1">$2</a>'); 494 495 // Process DokuWiki internal links ([[page]]) 496 html = html.replace(/\[\[([^\]]+)\]\]/g, '<a href="?id=$1">$1</a>'); 497 498 // Process unordered lists (* item or - item) with role attribute 499 html = html.replace(/^\* (.*$)/gm, '<li role="ul">$1</li>'); 500 html = html.replace(/^- (.*$)/gm, '<li role="ul">$1</li>'); 501 502 // Process ordered lists (1. item) with role attribute 503 html = html.replace(/^\d+\. (.*$)/gm, '<li role="ol">$1</li>'); 504 505 // Wrap consecutive <li role="ul"> elements in <ul> 506 html = html.replace(/(<li role="ul">.*<\/li>(\s*<li role="ul">.*<\/li>)*)/g, '<ul>$1</ul>'); 507 508 // Wrap consecutive <li role="ol"> elements in <ol> 509 html = html.replace(/(<li role="ol">.*<\/li>(\s*<li role="ol">.*<\/li>)*)/g, '<ol>$1</ol>'); 510 511 // Remove role attributes from li elements (they were only used for identification) 512 html = html.replace(/<li role="(ul|ol)">/g, '<li>'); 513 514 return html; 515 } 516 517 /** 518 * Show analysis or summarize results in a modal dialog 519 * 520 * Creates and displays a modal dialog with the analysis or summarize results. 521 * Includes a close button and proper styling. 522 * 523 * @param {string} contentText - The content text to display 524 * @param {string} action - The action type ('analyze' or 'summarize') 525 * @param {string} titleText - The title to display in the modal 526 */ 527 function showModal(contentText, action = 'analyze', titleText = '') { 528 // Create modal container 529 const modal = document.createElement('div'); 530 modal.id = 'llm-' + action + '-modal'; 531 modal.className = 'llm-modal'; 532 533 // Create modal content 534 const modalContent = document.createElement('div'); 535 modalContent.className = 'llm-modal-content'; 536 537 // Create close button 538 const closeButton = document.createElement('button'); 539 closeButton.textContent = 'Close'; 540 closeButton.className = 'llm-modal-close'; 541 closeButton.addEventListener('click', () => { 542 document.body.removeChild(modal); 543 }); 544 545 // Create append button 546 const appendButton = document.createElement('button'); 547 appendButton.textContent = 'Append'; 548 appendButton.title = 'Append to report'; 549 appendButton.className = 'llm-modal-append'; 550 appendButton.addEventListener('click', () => { 551 appendToReport(contentText); 552 document.body.removeChild(modal); 553 }); 554 555 // Create title based on action or use provided title 556 const title = document.createElement('h3'); 557 if (titleText) { 558 title.textContent = titleText; 559 } else { 560 title.textContent = action.charAt(0).toUpperCase() + action.slice(1); 561 } 562 title.style.marginTop = '0'; 563 564 // Create content area 565 const content = document.createElement('div'); 566 content.innerHTML = convertToHtml(contentText); 567 content.style.cssText = ` 568 margin-top: 20px; 569 white-space: pre-wrap; 570 `; 571 572 // Assemble modal 573 modalContent.appendChild(closeButton); 574 modalContent.appendChild(appendButton); 575 modalContent.appendChild(title); 576 modalContent.appendChild(content); 577 modal.appendChild(modalContent); 578 579 // Add to document and set up close event 580 document.body.appendChild(modal); 581 modal.addEventListener('click', (e) => { 582 if (e.target === modal) { 583 document.body.removeChild(modal); 584 } 585 }); 586 } 587 588 /** 589 * Append content to the end of the report 590 * 591 * Adds the provided content to the end of the editor content, 592 * preserving metadata at the beginning. 593 * 594 * @param {string} content - The content to append 595 */ 596 function appendToReport(content) { 597 const editor = document.getElementById('wiki__text'); 598 if (!editor) { 599 console.log('DokuLLM: Editor not found for appending content'); 600 return; 601 } 602 603 // Preserve metadata when appending content 604 const metadata = extractMetadata(editor.value); 605 const contentWithoutMetadata = editor.value.substring(metadata.length); 606 607 // Append new content with proper spacing 608 editor.value = metadata + contentWithoutMetadata + '\n\n' + content; 609 610 // Focus the editor at the end 611 editor.focus(); 612 editor.setSelectionRange(editor.value.length, editor.value.length); 613 } 614 615 /** 616 * Process text with a custom user prompt 617 * 618 * Sends selected or full text content to the backend with a user-provided 619 * custom prompt for processing. 620 * 621 * Clears the prompt input after successful processing. 622 * Shows loading indicators during processing. 623 * 624 * Complex logic includes: 625 * 1. Validating custom prompt input 626 * 2. Determining text to process (selected vs full content) 627 * 3. Managing UI state for the custom prompt interface 628 * 4. Constructing and sending AJAX requests with custom prompts 629 * 5. Handling response processing and error conditions 630 * 6. Updating editor content and clearing input fields 631 * 7. Restoring UI state after processing 632 * 633 * @param {string} customPrompt - The user's custom prompt 634 */ 635 function processCustomPrompt(customPrompt) { 636 console.log('DokuLLM: Processing custom prompt:', customPrompt); 637 if (!customPrompt.trim()) { 638 console.log('DokuLLM: No custom prompt provided'); 639 alert('Please enter a prompt'); 640 return; 641 } 642 643 const editor = document.getElementById('wiki__text'); 644 if (!editor) { 645 console.log('DokuLLM: Editor not found for custom prompt'); 646 return; 647 } 648 649 // Store the current selection range 650 currentSelectionRange = { 651 start: editor.selectionStart, 652 end: editor.selectionEnd 653 }; 654 655 const selectedText = getSelectedText(editor); 656 const fullText = editor.value; 657 const textToProcess = selectedText || fullText; 658 console.log('DokuLLM: Text to process length:', textToProcess.length); 659 660 if (!textToProcess.trim()) { 661 console.log('DokuLLM: No text to process for custom prompt'); 662 alert('Please select text or enter content to process'); 663 return; 664 } 665 666 // Get metadata from the page 667 const metadata = getMetadata(); 668 console.log('DokuLLM: Retrieved metadata for custom prompt:', metadata); 669 670 // Find the Send button and show loading state 671 const toolbar = document.getElementById('llm-custom-prompt'); 672 const sendButton = toolbar.querySelector('.toolbutton'); 673 const originalText = sendButton.textContent; 674 sendButton.textContent = 'Processing...'; 675 sendButton.disabled = true; 676 console.log('DokuLLM: Send button disabled, showing processing state'); 677 678 // Make textarea readonly during processing 679 editor.readOnly = true; 680 681 // Send AJAX request 682 console.log('DokuLLM: Sending custom prompt AJAX request to backend'); 683 const formData = new FormData(); 684 formData.append('call', 'plugin_dokullm'); 685 formData.append('action', 'custom'); 686 formData.append('text', textToProcess); 687 formData.append('prompt', customPrompt); 688 // Append metadata fields generically 689 for (const [key, value] of Object.entries(metadata)) { 690 if (Array.isArray(value)) { 691 formData.append(key, value.join('\n')); 692 } else if (value) { 693 formData.append(key, value); 694 } 695 } 696 697 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 698 method: 'POST', 699 body: formData 700 }) 701 .then(response => { 702 if (!response.ok) { 703 return response.text().then(text => { 704 throw new Error(`Network response was not ok: ${response.status} ${response.statusText} - ${text}`); 705 }); 706 } 707 console.log('DokuLLM: Received response for custom prompt'); 708 return response.json(); 709 }) 710 .then(data => { 711 if (data.error) { 712 console.log('DokuLLM: Error from backend for custom prompt:', data.error); 713 throw new Error(data.error); 714 } 715 716 console.log('DokuLLM: Custom prompt processing successful, result length:', data.result.length); 717 // Extract AI thinking parts (between <think> tags) from the result 718 let thinkingContent = ''; 719 const thinkingMatch = data.result.match(/<think>([\s\S]*?)<\/think>/); 720 if (thinkingMatch && thinkingMatch[1]) { 721 thinkingContent = thinkingMatch[1].trim(); 722 } 723 724 // Remove AI thinking parts (between <think> tags) from the result 725 const cleanedResult = data.result.replace(/<think>[\s\S]*?<\/think>/g, '').trim(); 726 727 // Replace selected text or append to editor 728 if (selectedText) { 729 console.log('DokuLLM: Replacing selected text for custom prompt'); 730 replaceSelectedText(editor, cleanedResult); 731 } else { 732 console.log('DokuLLM: Replacing full text content for custom prompt'); 733 // Preserve metadata when doing full page update 734 const metadata = extractMetadata(editor.value); 735 editor.value = metadata + cleanedResult; 736 } 737 738 // Clear the input field 739 const promptInput = toolbar.querySelector('.llm-prompt-input'); 740 if (promptInput) { 741 promptInput.value = ''; 742 } 743 // Show thinking content in modal if it exists and thinking is enabled 744 if (thinkingContent) { 745 showModal(thinkingContent, 'thinking', 'AI Thinking Process'); 746 } 747 }) 748 .catch(error => { 749 console.log('DokuLLM: Error during custom prompt processing:', error.message); 750 alert('Error: ' + error.message); 751 }) 752 .finally(() => { 753 console.log('DokuLLM: Resetting send button and enabling editor'); 754 if (sendButton) { 755 resetButton(sendButton, originalText); 756 } 757 editor.readOnly = false; 758 }); 759 } 760 761 /** 762 * Get the currently selected text in the textarea 763 * 764 * Uses the textarea's selectionStart and selectionEnd properties 765 * to extract the selected portion of text. 766 * 767 * @param {HTMLTextAreaElement} textarea - The textarea element 768 * @returns {string} The selected text 769 */ 770 function getSelectedText(textarea) { 771 const start = textarea.selectionStart; 772 const end = textarea.selectionEnd; 773 return textarea.value.substring(start, end); 774 } 775 776 /** 777 * Replace the selected text in the textarea with new text 778 * 779 * When replacing the entire content, preserves metadata directives 780 * at the beginning of the page. 781 * 782 * Complex logic includes: 783 * 1. Determining if text is selected or if it's a full content replacement 784 * 2. Preserving metadata directives when doing full content replacement 785 * 3. Properly replacing only selected text when applicable 786 * 4. Managing cursor position after text replacement 787 * 5. Maintaining focus on the textarea 788 * 789 * @param {HTMLTextAreaElement} textarea - The textarea element 790 * @param {string} newText - The new text to insert 791 */ 792 function replaceSelectedText(textarea, newText) { 793 // Use stored selection range if available, otherwise use current selection 794 const start = currentSelectionRange ? currentSelectionRange.start : textarea.selectionStart; 795 const end = currentSelectionRange ? currentSelectionRange.end : textarea.selectionEnd; 796 const text = textarea.value; 797 798 // Reset the stored selection range 799 currentSelectionRange = null; 800 801 // If there's no selection (start === end), it's not a replacement of selected text 802 if (start === end) { 803 // No selection, so we're processing the full text 804 const metadata = extractMetadata(text); 805 textarea.value = metadata + newText; 806 } else { 807 // There is a selection, replace only the selected text 808 textarea.value = text.substring(0, start) + newText + text.substring(end); 809 810 // Set cursor position after inserted text 811 const newCursorPos = start + newText.length; 812 textarea.setSelectionRange(newCursorPos, newCursorPos); 813 } 814 815 textarea.focus(); 816 } 817 818 /** 819 * Extract metadata directives from the beginning of the text 820 * 821 * Finds and returns LLM metadata directives (~~LLM_*~~) that appear 822 * at the beginning of the page content. 823 * 824 * @param {string} text - The full text content 825 * @returns {string} The metadata directives 826 */ 827 function extractMetadata(text) { 828 const metadataRegex = /^(~~LLM_[A-Z]+:[^~]+~~\s*)*/; 829 const match = text.match(metadataRegex); 830 return match ? match[0] : ''; 831 } 832 833 /** 834 * Reset a button to its original state 835 * 836 * Restores the button's text content and enables it. 837 * 838 * @param {HTMLButtonElement} button - The button to reset 839 * @param {string} originalText - The original button text 840 */ 841 function resetButton(button, originalText) { 842 button.textContent = originalText; 843 button.disabled = false; 844 } 845 846 /** 847 * Get page metadata for LLM context 848 * 849 * Extracts template and example page information from page metadata 850 * directives in the page content. 851 * 852 * Looks for: 853 * - ~~LLM_TEMPLATE:page_id~~ for template page reference 854 * - ~~LLM_EXAMPLES:page1,page2~~ for example page references 855 * 856 * Complex logic includes: 857 * 1. Initializing metadata structure with default values 858 * 2. Safely accessing page content from the editor 859 * 3. Using regular expressions to extract metadata directives 860 * 4. Parsing comma-separated example page lists 861 * 5. Trimming whitespace from extracted values 862 * 863 * @returns {Object} Metadata object with template and examples 864 */ 865 function getMetadata() { 866 const metadata = { 867 template: '', 868 examples: [], 869 previous: '' 870 }; 871 872 // Look for metadata in the page content 873 const pageContent = document.getElementById('wiki__text')?.value || ''; 874 875 // Extract template page from metadata 876 const templateMatch = pageContent.match(/~~LLM_TEMPLATE:([^~]+)~~/); 877 if (templateMatch) { 878 metadata.template = templateMatch[1].trim(); 879 } 880 881 // Extract example pages from metadata 882 const exampleMatches = pageContent.match(/~~LLM_EXAMPLES:([^~]+)~~/); 883 if (exampleMatches) { 884 metadata.examples = exampleMatches[1].split(',').map(example => example.trim()); 885 } 886 887 // Extract previous report page from metadata 888 const previousReportMatch = pageContent.match(/~~LLM_PREVIOUS:([^~]+)~~/); 889 if (previousReportMatch) { 890 metadata.previous = previousReportMatch[1].trim(); 891 } 892 893 return metadata; 894 } 895 896 /** 897 * Find and insert template metadata 898 * 899 * Searches for an appropriate template based on the current content 900 * and inserts the LLM_TEMPLATE metadata at the top of the text. 901 * 902 * Shows loading indicators during the search operation. 903 * 904 * @param {Event} event - The click event 905 */ 906 function findTemplate(event) { 907 console.log('DokuLLM: Finding and inserting template'); 908 const editor = document.getElementById('wiki__text'); 909 if (!editor) { 910 console.log('DokuLLM: Editor not found for template search'); 911 return; 912 } 913 914 // Disable the entire toolbar and prompt input 915 const toolbar = document.getElementById('llm-toolbar'); 916 const promptContainer = document.getElementById('llm-custom-prompt'); 917 const promptInput = promptContainer ? promptContainer.querySelector('.llm-prompt-input') : null; 918 const buttons = toolbar.querySelectorAll('button:not(.llm-modal-close)'); 919 920 // Store original states for restoration 921 const originalStates = { 922 promptInput: promptInput ? promptInput.disabled : false, 923 buttons: [] 924 }; 925 926 // Disable prompt input if it exists 927 if (promptInput) { 928 originalStates.promptInput = promptInput.disabled; 929 promptInput.disabled = true; 930 } 931 932 // Disable all buttons and store their original states 933 buttons.forEach(button => { 934 originalStates.buttons.push({ 935 element: button, 936 text: button.textContent, 937 disabled: button.disabled 938 }); 939 button.textContent = 'Searching...'; 940 button.disabled = true; 941 }); 942 editor.readOnly = true; 943 console.log('DokuLLM: Showing loading indicator for template search'); 944 945 // Get the current text to use for template search 946 const currentText = editor.value; 947 948 // Send AJAX request to find template 949 console.log('DokuLLM: Sending AJAX request to find template'); 950 const formData = new FormData(); 951 formData.append('call', 'plugin_dokullm'); 952 formData.append('action', 'find_template'); 953 formData.append('text', currentText); 954 955 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 956 method: 'POST', 957 body: formData 958 }) 959 .then(response => { 960 console.log('DokuLLM: Received template search response'); 961 return response.json(); 962 }) 963 .then(data => { 964 if (data.error) { 965 console.log('DokuLLM: Error finding template:', data.error); 966 throw new Error(data.error); 967 } 968 969 if (data.result && data.result.template) { 970 console.log('DokuLLM: Template found:', data.result.template); 971 // Insert template metadata at the top of the text 972 const metadataLine = `~~LLM_TEMPLATE:${data.result.template}~~\n`; 973 const existingText = editor.value; 974 editor.value = metadataLine + existingText; 975 976 // Show success message 977 alert(`Template found and inserted: ${data.result.template}`); 978 } else { 979 console.log('DokuLLM: No template found'); 980 alert('No suitable template found for this content.'); 981 } 982 }) 983 .catch(error => { 984 console.log('DokuLLM: Error during template search:', error.message); 985 alert('Error: ' + error.message); 986 }) 987 .finally(() => { 988 console.log('DokuLLM: Restoring toolbar and enabling editor'); 989 // Re-enable the toolbar and prompt input 990 if (promptInput) { 991 promptInput.disabled = originalStates.promptInput; 992 } 993 originalStates.buttons.forEach(buttonState => { 994 buttonState.element.textContent = buttonState.text; 995 buttonState.element.disabled = buttonState.disabled; 996 }); 997 editor.readOnly = false; 998 }); 999 } 1000 1001 /** 1002 * Insert template content into the editor 1003 * 1004 * Fetches template content from the backend and inserts it into the editor 1005 * at the current cursor position. 1006 * 1007 * Shows loading indicators during the fetch operation. 1008 * 1009 * Complex logic includes: 1010 * 1. Managing UI state during template loading (loading indicator, readonly) 1011 * 2. Constructing and sending AJAX requests for template content 1012 * 3. Handling response processing and error conditions 1013 * 4. Inserting template content at the correct cursor position 1014 * 5. Managing cursor position after content insertion 1015 * 6. Restoring UI state after template insertion 1016 * 1017 * @param {string} templateId - The template page ID 1018 */ 1019 function insertTemplateContent(templateId) { 1020 console.log('DokuLLM: Inserting template content for:', templateId); 1021 const editor = document.getElementById('wiki__text'); 1022 if (!editor) { 1023 console.log('DokuLLM: Editor not found for template insertion'); 1024 return; 1025 } 1026 1027 // Show loading indicator 1028 const toolbar = document.getElementById('llm-toolbar'); 1029 const originalContent = toolbar.innerHTML; 1030 toolbar.innerHTML = '<span>Loading template...</span>'; 1031 editor.readOnly = true; 1032 console.log('DokuLLM: Showing loading indicator for template'); 1033 1034 // Send AJAX request to get template content 1035 console.log('DokuLLM: Sending AJAX request to get template content'); 1036 const formData = new FormData(); 1037 formData.append('call', 'plugin_dokullm'); 1038 formData.append('action', 'get_template'); 1039 formData.append('template', templateId); 1040 1041 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 1042 method: 'POST', 1043 body: formData 1044 }) 1045 .then(response => { 1046 console.log('DokuLLM: Received template response'); 1047 return response.json(); 1048 }) 1049 .then(data => { 1050 if (data.error) { 1051 console.log('DokuLLM: Error retrieving template:', data.error); 1052 throw new Error(data.error); 1053 } 1054 1055 console.log('DokuLLM: Template retrieved successfully, content length:', data.result.content.length); 1056 // Insert template content at cursor position or at the beginning 1057 const cursorPos = editor.selectionStart; 1058 const text = editor.value; 1059 editor.value = text.substring(0, cursorPos) + data.result.content + text.substring(cursorPos); 1060 1061 // Set cursor position after inserted content 1062 const newCursorPos = cursorPos + data.result.content.length; 1063 editor.setSelectionRange(newCursorPos, newCursorPos); 1064 editor.focus(); 1065 }) 1066 .catch(error => { 1067 console.log('DokuLLM: Error during template insertion:', error.message); 1068 alert('Error: ' + error.message); 1069 }) 1070 .finally(() => { 1071 console.log('DokuLLM: Restoring toolbar and enabling editor'); 1072 toolbar.innerHTML = originalContent; 1073 editor.readOnly = false; 1074 }); 1075 } 1076 1077 1078 /** 1079 * Fetch action definitions from the API endpoint 1080 * 1081 * Makes an AJAX request to get the LLM action definitions from the backend 1082 * 1083 * @returns {Promise<Array>} Promise that resolves to an array of action definitions 1084 */ 1085 function getActions() { 1086 return new Promise((resolve, reject) => { 1087 const formData = new FormData(); 1088 formData.append('call', 'plugin_dokullm'); 1089 formData.append('action', 'get_actions'); 1090 1091 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 1092 method: 'POST', 1093 body: formData 1094 }) 1095 .then(response => { 1096 if (!response.ok) { 1097 return response.text().then(text => { 1098 throw new Error(`Network response was not ok: ${response.status} ${response.statusText} - ${text}`); 1099 }); 1100 } 1101 return response.json(); 1102 }) 1103 .then(data => { 1104 if (data.error) { 1105 throw new Error(data.error); 1106 } 1107 resolve(data.result); 1108 }) 1109 .catch(error => { 1110 reject(error); 1111 }); 1112 }); 1113 } 1114 1115 function cleanThinking(text) { 1116 // Extract AI thinking parts (between 'think' tags) from the result 1117 let thinkingContent = ''; 1118 const thinkingMatch = text.match(/<think>([\s\S]*?)<\/think>/); 1119 if (thinkingMatch && thinkingMatch[1]) { 1120 thinkingContent = thinkingMatch[1].trim(); 1121 } 1122 // Remove AI thinking parts (between <think> tags) from the result 1123 const cleanedResult = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim(); 1124 return [cleanedResult, thinkingContent]; 1125 } 1126 1127})(); 1128