xref: /plugin/dokullm/script.js (revision f46311824306f4d3ac07e21907e5cfa50d3ab4bc)
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&copyfrom=' + 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