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