/** * JavaScript for LLM Integration Plugin * * This script adds LLM processing capabilities to DokuWiki's edit interface. * It creates a toolbar with buttons for various text processing operations * and handles the communication with the backend plugin. * * Features: * - Context-aware text processing with metadata support * - Template content insertion * - Custom prompt input * - Selected text processing * - Full page content processing * - Text analysis in modal dialog */ (function() { 'use strict'; /** * Initialize the plugin when the DOM is ready * * This is the main initialization function that runs when the DOM is fully loaded. * It checks if we're on an edit page and adds the LLM tools if so. * Only runs on pages with the wiki text editor. * Also sets up the copy page button event listener. * * Complex logic includes: * 1. Checking for the presence of the wiki text editor element * 2. Conditionally adding LLM tools based on page context * 3. Setting up event listeners for the copy page functionality */ document.addEventListener('DOMContentLoaded', function() { console.log('DokuLLM: DOM loaded, initializing plugin'); // Only run on edit pages if (document.getElementById('wiki__text')) { // Add LLM tools to the editor console.log('DokuLLM: Adding LLM tools to editor'); addLLMTools(); } // Add event listener for copy button const copyButton = document.querySelector('.dokullmplugin__copy'); if (copyButton) { copyButton.addEventListener('click', function(event) { event.preventDefault(); copyPage(); }); } // Dirty hack to handle selection of mobile menu // See: https://github.com/splitbrain/dokuwiki/blob/release_stable_2018-04-22/lib/scripts/behaviour.js#L102-L115 const quickSelect = jQuery('select.quickselect'); if (quickSelect.length > 0) { quickSelect .unbind('change') // Remove dokuwiki's default handler to override its behavior .change(function(e) { if (e.target.value != 'dokullmplugin__copy') { // do the default action e.target.form.submit(); return; } e.target.value = ''; // Reset selection to enable re-select when a prompt is canceled copyPage(); }); } }); /** * Add the LLM toolbar to the editor interface * * Creates a toolbar with buttons for each LLM operation and inserts * it before the wiki text editor. Also adds a custom prompt input * below the editor. * * Dynamically adds a template button when template metadata is present. * * Complex logic includes: * 1. Creating and positioning the main toolbar container * 2. Dynamically adding a template button based on metadata presence * 3. Creating standard LLM operation buttons with event handlers * 4. Adding a custom prompt input field with Enter key handling * 5. Inserting all UI elements at appropriate positions in the DOM * 6. Applying CSS styles for consistent appearance */ function addLLMTools() { const editor = document.getElementById('wiki__text'); if (!editor) { console.log('DokuLLM: Editor div not found'); return; } console.log('DokuLLM: Creating LLM toolbar'); // Create toolbar container const toolbar = document.createElement('div'); toolbar.id = 'llm-toolbar'; toolbar.className = 'toolbar'; // Get metadata to check if template exists const metadata = getMetadata(); console.log('DokuLLM: Page metadata retrieved', metadata); // Add "Insert template" button if template is defined if (metadata.template) { console.log('DokuLLM: Adding insert template button for', metadata.template); const templateBtn = document.createElement('button'); templateBtn.type = 'button'; templateBtn.className = 'toolbutton'; templateBtn.textContent = 'Insert Template'; templateBtn.addEventListener('click', () => insertTemplateContent(metadata.template)); toolbar.appendChild(templateBtn); } else { // Add "Find Template" button if no template is defined and ChromaDB is enabled // Check if ChromaDB is enabled through JSINFO const chromaDBEnabled = typeof JSINFO !== 'undefined' && JSINFO.plugins && JSINFO.plugins.dokullm && JSINFO.plugins.dokullm.enable_chromadb; if (chromaDBEnabled) { console.log('DokuLLM: Adding find template button'); const findTemplateBtn = document.createElement('button'); findTemplateBtn.type = 'button'; findTemplateBtn.className = 'toolbutton'; findTemplateBtn.textContent = 'Find Template'; findTemplateBtn.addEventListener('click', findTemplate); toolbar.appendChild(findTemplateBtn); } } // Add loading indicator while fetching actions const loadingIndicator = document.createElement('span'); loadingIndicator.textContent = 'Loading LLM actions...'; loadingIndicator.id = 'llm-loading'; toolbar.appendChild(loadingIndicator); // Insert toolbar before the editor editor.parentNode.insertBefore(toolbar, editor); // Add custom prompt input below the editor console.log('DokuLLM: Adding custom prompt input below editor'); const customPromptContainer = document.createElement('div'); customPromptContainer.className = 'llm-custom-prompt'; customPromptContainer.id = 'llm-custom-prompt'; const promptInput = document.createElement('input'); promptInput.type = 'text'; promptInput.placeholder = 'Enter custom prompt...'; promptInput.className = 'llm-prompt-input'; // Add event listener for Enter key promptInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { processCustomPrompt(promptInput.value); } }); const sendButton = document.createElement('button'); sendButton.type = 'button'; sendButton.className = 'toolbutton'; sendButton.textContent = 'Send'; sendButton.addEventListener('click', () => processCustomPrompt(promptInput.value)); customPromptContainer.appendChild(promptInput); customPromptContainer.appendChild(sendButton); // Insert custom prompt container after the editor editor.parentNode.insertBefore(customPromptContainer, editor.nextSibling); // Fetch action definitions from the API getActions() .then(actions => { // Remove loading indicator const loadingElement = document.getElementById('llm-loading'); if (loadingElement) { loadingElement.remove(); } // Add buttons based on fetched actions actions.forEach(action => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'toolbutton'; btn.textContent = action.label; btn.title = action.description || ''; btn.dataset.action = action.id; btn.dataset.result = action.result; btn.addEventListener('click', function(event) { processLLMAction(action.id, event); }); toolbar.appendChild(btn); }); console.log('DokuLLM: LLM toolbars added successfully'); }) .catch(error => { console.error('DokuLLM: Error fetching action definitions:', error); // Remove loading indicator and show error const loadingElement = document.getElementById('llm-loading'); if (loadingElement) { loadingElement.textContent = 'Failed to load LLM actions'; } }); } /** * Copy the current page to a new page ID * * Prompts the user for a new page ID and redirects to the edit page * with the current page content pre-filled. * * Validates that the new ID is different from the current page ID. * Handles user cancellation of the prompt dialog. * * Complex logic includes: * 1. Prompting user for new page ID with current ID as default * 2. Validating that new ID is different from current ID * 3. Handling browser differences in prompt cancellation (null vs empty string) * 4. Constructing the redirect URL with proper encoding * 5. Redirecting to the new page edit view with copyfrom parameter * * Based on the DokuWiki CopyPage plugin * @see https://www.dokuwiki.org/plugin:copypage */ function copyPage() { // Original code: https://www.dokuwiki.org/plugin:copypage var oldId = JSINFO.id; while (true) { var newId = prompt('Enter the new page ID:', oldId); // Note: When a user canceled, most browsers return the null, but Safari returns the empty string if (newId) { if (newId === oldId) { alert('The new page ID must be different from the current page ID.'); continue; } var url = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(newId) + '&do=edit©from=' + encodeURIComponent(oldId); location.href = url; } break; } } /** * Process text using the specified LLM action * * Gets the selected text (or full editor content), sends it to the * backend for processing, and handles the result based on the button's * dataset.result property ('replace', 'append', 'insert', 'show'). * * Preserves page metadata when doing full page updates. * Shows loading indicators during processing. * * Complex logic includes: * 1. Determining text to process (selected vs full content) * 2. Managing UI state during processing (loading indicators, readonly) * 3. Constructing and sending AJAX requests with proper metadata * 4. Handling response processing and error conditions * 5. Updating editor content while preserving metadata based on result handling mode * 6. Restoring UI state after processing * * @param {string} action - The action to perform (create, rewrite, etc.) */ // Store selection range for processing let currentSelectionRange = null; function processLLMAction(action, event) { console.log('DokuLLM: Processing text with action:', action); const editor = document.getElementById('wiki__text'); if (!editor) { console.log('DokuLLM: Editor not found'); return; } // Store the current selection range currentSelectionRange = { start: editor.selectionStart, end: editor.selectionEnd }; // Get metadata from the page const metadata = getMetadata(); console.log('DokuLLM: Retrieved metadata:', metadata); const selectedText = getSelectedText(editor); const fullText = editor.value; const textToProcess = selectedText || fullText; console.log('DokuLLM: Text to process length:', textToProcess.length); if (!textToProcess.trim()) { console.log('DokuLLM: No text to process'); alert('Please select text or enter content to process'); return; } // Disable the entire toolbar and prompt input const toolbar = document.getElementById('llm-toolbar'); const promptContainer = document.getElementById('llm-custom-prompt'); const promptInput = promptContainer ? promptContainer.querySelector('.llm-prompt-input') : null; const buttons = toolbar.querySelectorAll('button:not(.llm-modal-close)'); // Store original states for restoration const originalStates = { promptInput: promptInput ? promptInput.disabled : false, buttons: [] }; // Disable prompt input if it exists if (promptInput) { originalStates.promptInput = promptInput.disabled; promptInput.disabled = true; } // Disable all buttons and store their original states buttons.forEach(button => { originalStates.buttons.push({ element: button, text: button.textContent, disabled: button.disabled }); // Only change text of the button that triggered the action if (event && event.target === button) { button.textContent = 'Processing...'; } button.disabled = true; }); console.log('DokuLLM: Toolbar disabled, showing processing state'); // Make textarea readonly during processing editor.readOnly = true; // Send AJAX request console.log('DokuLLM: Sending AJAX request to backend'); const formData = new FormData(); formData.append('call', 'plugin_dokullm'); formData.append('action', action); formData.append('text', textToProcess); formData.append('prompt', ''); // Append metadata fields generically for (const [key, value] of Object.entries(metadata)) { if (Array.isArray(value)) { formData.append(key, value.join('\n')); } else if (value) { formData.append(key, value); } } fetch(DOKU_BASE + 'lib/exe/ajax.php', { method: 'POST', body: formData }) .then(response => { if (!response.ok) { return response.text().then(text => { throw new Error(`Network response was not ok: ${response.status} ${response.statusText} - ${text}`); }); } console.log('DokuLLM: Received response from backend'); return response.json(); }) .then(data => { if (data.error) { console.log('DokuLLM: Error from backend:', data.error); throw new Error(data.error); } console.log('DokuLLM: Processing successful, result length:', data.result.length); // Remove some part const [thinkingContent, cleanedResult] = removeBetweenXmlTags(data.result, 'think'); // Determine how to handle the result based on button's dataset.result property const resultHandling = event.target.dataset.result || 'replace'; // Replace selected text or handle result based on resultHandling if (resultHandling === 'show') { console.log('DokuLLM: Showing result in modal'); const buttonTitle = event.target.title || action; showModal(cleanedResult, action, buttonTitle); } else if (resultHandling === 'append') { console.log('DokuLLM: Appending result to existing text'); // Append to the end of existing content (preserving metadata) const metadata = extractMetadata(editor.value); const contentWithoutMetadata = editor.value.substring(metadata.length); editor.value = metadata + contentWithoutMetadata + '\n\n' + cleanedResult; // Show thinking content in modal if it exists and thinking is enabled if (thinkingContent) { showModal(thinkingContent, 'thinking', 'AI Thinking Process'); } } else if (resultHandling === 'insert') { console.log('DokuLLM: Inserting result before existing text'); // Insert before existing content (preserving metadata) const metadata = extractMetadata(editor.value); const contentWithoutMetadata = editor.value.substring(metadata.length); editor.value = metadata + cleanedResult + '\n\n' + contentWithoutMetadata; // Show thinking content in modal if it exists and thinking is enabled if (thinkingContent) { showModal(thinkingContent, 'thinking', 'AI Thinking Process'); } } else if (selectedText) { console.log('DokuLLM: Replacing selected text'); replaceSelectedText(editor, cleanedResult); // Show thinking content in modal if it exists and thinking is enabled if (thinkingContent) { showModal(thinkingContent, 'thinking', 'AI Thinking Process'); } } else { console.log('DokuLLM: Replacing full text content'); // Preserve metadata when doing full page update const metadata = extractMetadata(editor.value); editor.value = metadata + cleanedResult; // Show thinking content in modal if it exists and thinking is enabled if (thinkingContent) { showModal(thinkingContent, 'thinking', 'AI Thinking Process'); } } }) .catch(error => { console.log('DokuLLM: Error during processing:', error.message); alert('Error: ' + error.message); }) .finally(() => { console.log('DokuLLM: Resetting toolbar and enabling editor'); // Re-enable the toolbar and prompt input if (promptInput) { promptInput.disabled = originalStates.promptInput; } originalStates.buttons.forEach(buttonState => { buttonState.element.textContent = buttonState.text; buttonState.element.disabled = buttonState.disabled; }); editor.readOnly = false; }); } /** * Convert markdown/DokuWiki text to HTML * * Performs basic conversion of markdown/DokuWiki syntax to HTML. * Supports headings, lists, inline formatting, and code blocks. * * @param {string} text - The markdown/DokuWiki text to convert * @returns {string} The converted HTML */ function convertToHtml(text) { // Process code blocks first (```code```) let html = text.replace(/```([\s\S]*?)```/g, '
$1
'); // Process DokuWiki file blocks ({{file>page#section}}) html = html.replace(/\{\{file>([^}]+)\}\}/g, '
$1
'); // Process DokuWiki includes ({{page}}) html = html.replace(/\{\{([^}]+)\}\}/g, '
$1
'); // Process inline code (`code`) html = html.replace(/`([^`]+)`/g, '$1'); // Process DokuWiki headings (====== Heading ======) html = html.replace(/^====== (.*?) ======$/gm, '

$1

'); html = html.replace(/^===== (.*?) =====$/gm, '

$1

'); html = html.replace(/^==== (.*?) ====$/gm, '

$1

'); html = html.replace(/^=== (.*?) ===$/gm, '

$1

'); html = html.replace(/^== (.*?) ==$/gm, '
$1
'); html = html.replace(/^= (.*?) =$/gm, '
$1
'); // Process markdown headings (# Heading, ## Heading, etc.) html = html.replace(/^###### (.*$)/gm, '
$1
'); html = html.replace(/^##### (.*$)/gm, '
$1
'); html = html.replace(/^#### (.*$)/gm, '

$1

'); html = html.replace(/^### (.*$)/gm, '

$1

'); html = html.replace(/^## (.*$)/gm, '

$1

'); html = html.replace(/^# (.*$)/gm, '

$1

'); // Process DokuWiki bold (**text**) html = html.replace(/\*\*(.*?)\*\*/g, '$1'); // Process DokuWiki italic (//text//) html = html.replace(/\/\/(.*?)\/\//g, '$1'); // Process markdown bold (__text__) html = html.replace(/__(.*?)__/g, '$1'); // Process markdown italic (*text* or _text_) html = html.replace(/\*(.*?)\*/g, '$1'); html = html.replace(/_(.*?)_/g, '$1'); // Process DokuWiki external links ([[http://example.com|text]]) html = html.replace(/\[\[(https?:\/\/[^\]|]+)\|([^\]]+)\]\]/g, '$2'); // Process DokuWiki external links ([[http://example.com]]) html = html.replace(/\[\[(https?:\/\/[^\]]+)\]\]/g, '$1'); // Process DokuWiki internal links ([[page|text]]) html = html.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '$2'); // Process DokuWiki internal links ([[page]]) html = html.replace(/\[\[([^\]]+)\]\]/g, '$1'); // Process unordered lists (* item or - item) with role attribute html = html.replace(/^\* (.*$)/gm, '
  • $1
  • '); html = html.replace(/^- (.*$)/gm, '
  • $1
  • '); // Process ordered lists (1. item) with role attribute html = html.replace(/^\d+\. (.*$)/gm, '
  • $1
  • '); // Wrap consecutive
  • elements in