From version 15.1
edited by Jiahao Lai
on 2026/03/26 16:08
on 2026/03/26 16:08
Change comment:
There is no comment for this version
To version 16.1
edited by Jiahao Lai
on 2026/03/26 16:09
on 2026/03/26 16:09
Change comment:
There is no comment for this version
Summary
-
Page properties (1 modified, 0 added, 0 removed)
Details
- Page properties
-
- Content
-
... ... @@ -187,630 +187,3 @@ 187 187 {{/velocity}} 188 188 189 189 190 - 191 -{{html clean="false"}} 192 - 193 -// Initialize marked library 194 -marked.use({ breaks: true }); 195 - 196 -// Initialize variables 197 -let currentRequest = null; 198 -let abortController = null; 199 -let conversationHistory = []; 200 -let userSettings = { 201 - model: '', 202 - temperature: 0, 203 - stream: true, 204 - settingsCollapsed: false 205 -}; 206 -let chatHistory; 207 -let chatInput; 208 -let sendButton; 209 -let stopButton; 210 -let modelSelect; 211 -let temperatureInput; 212 -let streamCheckbox; 213 -let chatWidget; 214 -let toggleChatButton; 215 -let settingsContainer; 216 -let settingsToggle; 217 -let newConvButton; 218 -let isResizing = false; 219 -let startX, startY, startWidth, startHeight; 220 - 221 -// Get the script tag 222 -const scriptTag = document.getElementById('chat-widget'); 223 - 224 -XWikiAiAPI.setBaseURL(scriptTag.dataset.baseUrl || ''); 225 - 226 -// Set the wiki name 227 -if (scriptTag && scriptTag.dataset.wikiName) { 228 - XWikiAiAPI.setWikiName(scriptTag.dataset.wikiName); 229 -} else { 230 - // Set default wiki name to 'xwiki' if not provided 231 - XWikiAiAPI.setWikiName('xwiki'); 232 -} 233 - 234 - 235 -// Create the chat widget HTML dynamically 236 -function createChatWidget() { 237 - const chatWidgetElement = document.createElement('div'); 238 - chatWidgetElement.id = 'chat-widget'; 239 - chatWidgetElement.innerHTML = ` 240 - <div id="resize-handle"></div> 241 - <div id="chat-widget-divider"></div> 242 - <div id="chat-container"> 243 - <h2>XWiki AI Chat</h2> 244 - <div class="settings-container"> 245 - <button id="new-conv">New</button> 246 - <button id="settings-toggle">Settings</button> 247 - <div class="settings-wrapper"> 248 - <div class="settings-top-line"></div> 249 - <div class="settings"> 250 - <div> 251 - <label for="model-select">Model:</label> 252 - <select id="model-select"></select> 253 - </div> 254 - <div> 255 - <label for="temperature-input">Temp:</label> 256 - <input type="number" id="temperature-input" min="0" max="2" step="0.1" value="0"> 257 - </div> 258 - <div> 259 - <label for="stream-checkbox">Stream:</label> 260 - <input type="checkbox" id="stream-checkbox" checked> 261 - </div> 262 - </div> 263 - <div class="settings-bottom-line"></div> 264 - </div> 265 - </div> 266 - <div id="chat-history"></div> 267 - <textarea id="chat-input" placeholder="Type your message..."></textarea> 268 - <div id="button-container"> 269 - <button id="send-button">Send</button> 270 - <button id="stop-button" style="display: none;">Stop</button> 271 - </div> 272 - </div> 273 - `; 274 - document.body.appendChild(chatWidgetElement); 275 - return chatWidgetElement; 276 -} 277 - 278 -// Create the toggle chat button HTML dynamically 279 -function createToggleChatButton() { 280 - const toggleChatButtonElement = document.createElement('button'); 281 - toggleChatButtonElement.id = 'toggle-chat-button'; 282 - toggleChatButtonElement.textContent = '✨ Chat'; 283 - document.body.appendChild(toggleChatButtonElement); 284 - return toggleChatButtonElement; 285 -} 286 - 287 -// Create an expandable bubble with sources 288 -function createSourcesBubble(sources) { 289 - const sourcesBubble = document.createElement('div'); 290 - sourcesBubble.classList.add('sources-bubble'); 291 - const sourceLinks = sources.split('\n').map(source => { 292 - const trimmedSource = source.trim(); 293 - return `<li><a href="${trimmedSource}" target="_blank">${trimmedSource}</a></li>`; 294 - }).join(''); 295 - sourcesBubble.innerHTML = ` 296 - <div class="sources-header">Sources <span class="expand-icon">+</span></div> 297 - <div class="sources-content hidden"><ul>${sourceLinks}</ul></div> 298 - `; 299 - sourcesBubble.querySelector('.sources-header').addEventListener('click', () => { 300 - sourcesBubble.querySelector('.sources-content').classList.toggle('hidden'); 301 - sourcesBubble.querySelector('.expand-icon').textContent = sourcesBubble.querySelector('.sources-content').classList.contains('hidden') ? '+' : '-'; 302 - }); 303 - return sourcesBubble; 304 -} 305 - 306 -// Fetch available models and populate the select dropdown 307 -async function populateModelSelect() { 308 - try { 309 - const response = await XWikiAiAPI.getModels(); 310 - const models = response.data; 311 - modelSelect.innerHTML = ''; 312 - models.forEach(model => { 313 - const option = document.createElement('option'); 314 - option.value = model.id; 315 - option.text = model.name; 316 - modelSelect.appendChild(option); 317 - }); 318 - // Set the selected model to the user's stored setting or the first option 319 - const storedModel = userSettings.model; 320 - if (storedModel && models.some(model => model.id === storedModel)) { 321 - modelSelect.value = storedModel; 322 - } else { 323 - modelSelect.selectedIndex = 0; 324 - userSettings.model = modelSelect.value; 325 - } 326 - } catch (error) { 327 - console.error('Failed to fetch models:', error); 328 - } 329 -} 330 - 331 -// Initialize the chat widget and toggle button 332 -async function initializeChatWidget() { 333 - chatWidget = createChatWidget(); 334 - toggleChatButton = createToggleChatButton(); 335 - 336 - // Get references to the dynamically created elements 337 - chatHistory = document.getElementById('chat-history'); 338 - chatInput = document.getElementById('chat-input'); 339 - sendButton = document.getElementById('send-button'); 340 - stopButton = document.getElementById('stop-button'); 341 - modelSelect = document.getElementById('model-select'); 342 - temperatureInput = document.getElementById('temperature-input'); 343 - streamCheckbox = document.getElementById('stream-checkbox'); 344 - settingsContainer = document.querySelector('.settings-container'); 345 - settingsToggle = document.getElementById('settings-toggle'); 346 - newConvButton = document.getElementById('new-conv'); 347 - const resizeHandle = document.getElementById('resize-handle'); 348 - resizeHandle.addEventListener('mousedown', initResize); 349 - document.addEventListener('mousemove', resize); 350 - document.addEventListener('mouseup', stopResize); 351 - 352 - // Populate the model select dropdown 353 - await populateModelSelect(); 354 - 355 - // Load user settings from local storage 356 - loadUserSettings(); 357 - 358 - // Update user settings when changed 359 - modelSelect.addEventListener('change', updateUserSettings); 360 - temperatureInput.addEventListener('input', updateUserSettings); 361 - streamCheckbox.addEventListener('change', updateUserSettings); 362 - 363 - // Add event listeners 364 - newConvButton.addEventListener('click', startNewConversation); 365 - sendButton.addEventListener('click', sendMessage); 366 - chatInput.addEventListener('keydown', handleChatInputKeydown); 367 - toggleChatButton.addEventListener('click', toggleChatWidget); 368 - chatInput.addEventListener('focus', handleChatInputFocus); 369 - chatInput.addEventListener('blur', handleChatInputBlur); 370 - settingsToggle.addEventListener('click', toggleSettings); 371 - 372 - // Load last conversation from local storage 373 - loadLastConversation(); 374 -} 375 - 376 - 377 -function initResize(e) { 378 - isResizing = true; 379 - startX = e.clientX; 380 - startY = e.clientY; 381 - startWidth = parseInt(document.defaultView.getComputedStyle(chatWidget).width, 10); 382 - startHeight = parseInt(document.defaultView.getComputedStyle(chatWidget).height, 10); 383 - e.preventDefault(); 384 -} 385 - 386 -function startResize(e) { 387 - isResizing = true; 388 - startX = e.clientX; 389 - startY = e.clientY; 390 - startWidth = parseInt(document.defaultView.getComputedStyle(chatWidget).width, 10); 391 - startHeight = parseInt(document.defaultView.getComputedStyle(chatWidget).height, 10); 392 - document.addEventListener('mousemove', resize); 393 - document.addEventListener('mouseup', stopResize); 394 - e.preventDefault(); 395 -} 396 - 397 -function resize(e) { 398 - if (!isResizing) return; 399 - const width = startWidth - (e.clientX - startX); 400 - const height = startHeight - (e.clientY - startY); 401 - if (width > 300 && height > 400) { 402 - chatWidget.style.width = width + 'px'; 403 - chatWidget.style.height = height + 'px'; 404 - } 405 -} 406 - 407 -function stopResize() { 408 - isResizing = false; 409 -} 410 - 411 -// Modify the toggleChatWidget function 412 -function toggleChatWidget() { 413 - if (isPanelMode) { 414 - chatWidget.style.display = chatWidget.style.display === 'none' ? 'flex' : 'none'; 415 - } else { 416 - chatWidget.style.display = chatWidget.style.display === 'none' ? 'block' : 'none'; 417 - } 418 -} 419 - 420 -// Handle new conversation 421 -function startNewConversation() { 422 - // Clear the conversation history 423 - conversationHistory = []; 424 - saveConversationHistory(); 425 - 426 - // Clear the chat history 427 - chatHistory.innerHTML = ''; 428 -} 429 - 430 -// Handle chat input keydown event 431 -function handleChatInputKeydown(event) { 432 - if (event.key === 'Enter' && !event.shiftKey) { 433 - event.preventDefault(); 434 - sendMessage(); 435 - } 436 -} 437 - 438 -// Toggle the visibility of the chat widget 439 -function toggleChatWidget() { 440 - chatWidget.style.display = chatWidget.style.display === 'none' ? 'block' : 'none'; 441 -} 442 - 443 -// Handle chat input focus event 444 -function handleChatInputFocus() { 445 - chatWidget.classList.add('keyboard-open'); 446 -} 447 - 448 -// Handle chat input blur event 449 -function handleChatInputBlur() { 450 - chatWidget.classList.remove('keyboard-open'); 451 -} 452 - 453 -// Change the state of the send and stop buttons 454 -function changeBtnState(enabled) { 455 - sendButton.disabled = !enabled; 456 - stopButton.style.display = enabled ? 'none' : 'inline-block'; 457 -} 458 - 459 -// Show the waiting animation 460 -function showWaitingAnimation() { 461 - const waitingLine = document.createElement('div'); 462 - waitingLine.classList.add('waiting-line'); 463 - waitingLine.innerHTML = ` 464 - <div class="dot dot1"></div> 465 - <div class="dot dot2"></div> 466 - <div class="dot dot3"></div> 467 - `; 468 - chatHistory.appendChild(waitingLine); 469 - chatHistory.scrollTop = chatHistory.scrollHeight; 470 -} 471 - 472 -// Remove the waiting animation 473 -function removeWaitingAnimation() { 474 - const waitingLine = chatHistory.querySelector('.waiting-line'); 475 - if (waitingLine) { 476 - waitingLine.remove(); 477 - } 478 -} 479 - 480 -// Send a message to the assistant 481 -function sendMessage() { 482 - const userMessage = chatInput.value.trim(); 483 - if (userMessage === '') return; 484 - 485 - // Add user message to conversation history 486 - conversationHistory.push({ role: 'user', content: userMessage }); 487 - 488 - // Display user message in the chat history 489 - displayUserMessage(userMessage); 490 - 491 - // Clear the chat input 492 - chatInput.value = ''; 493 - 494 - // Create a new abort controller for the request 495 - abortController = new AbortController(); 496 - const signal = abortController.signal; 497 - 498 - // Create a new request with the full conversation history 499 - const request = new ChatCompletionRequest( 500 - modelSelect.value, 501 - parseFloat(temperatureInput.value), 502 - conversationHistory, 503 - streamCheckbox.checked 504 - ); 505 - 506 - // Display the assistant message container in the chat history 507 - const assistantMessageElement = displayAssistantMessage('', modelSelect.options[modelSelect.selectedIndex].text, 0); 508 - 509 - // Show the waiting animation 510 - showWaitingAnimation(); 511 - 512 - // Send the request to the API 513 - if (request.stream) { 514 - handleStreamingRequest(request, signal, assistantMessageElement); 515 - } else { 516 - handleNonStreamingRequest(request, signal, assistantMessageElement); 517 - } 518 - 519 - // Disable the send button and show the stop button 520 - changeBtnState(false); 521 - 522 - // Add event listener to the stop button 523 - stopButton.addEventListener('click', stopRequest); 524 -} 525 - 526 -// Handle streaming request 527 -function handleStreamingRequest(request, signal, assistantMessageElement) { 528 - let messageText = ''; 529 - let sourcesText = ''; 530 - const startTime = new Date().getTime(); 531 - let updateTimer; 532 - let sourcesBubble = null; 533 - 534 - const assistantInfoElement = assistantMessageElement.previousElementSibling; 535 - updateTimer = setInterval(() => { 536 - updateResponseTime(startTime, assistantInfoElement); 537 - }, 100); 538 - 539 - XWikiAiAPI.getCompletions(request, messageChunk => { 540 - if (messageChunk.choices.length > 0 && messageChunk.choices[0].delta.content !== null) { 541 - const content = messageChunk.choices[0].delta.content; 542 - 543 - if (content.startsWith("Sources:")) { 544 - sourcesText += content.replace("Sources:", "").trim(); 545 - if (!sourcesBubble) { 546 - sourcesBubble = createSourcesBubble(sourcesText); 547 - assistantMessageElement.parentElement.insertBefore(sourcesBubble, assistantMessageElement); 548 - } else { 549 - sourcesBubble.querySelector('.sources-content').innerHTML = sourcesText; 550 - } 551 - } else { 552 - messageText += content; 553 - assistantMessageElement.innerHTML = DOMPurify.sanitize(marked.parse(messageText), { FORBID_TAGS: ['style'], FORBID_ATTR: ['src'] }); 554 - } 555 - 556 - removeWaitingAnimation(); 557 - } 558 - }, signal) 559 - .then((usageData) => { 560 - if (messageText !== '') { 561 - conversationHistory.push({ role: 'assistant', content: assistantMessageElement.textContent }); 562 - saveConversationHistory(); 563 - } 564 - clearInterval(updateTimer); 565 - 566 - if (usageData) { 567 - displayUsageInfo(assistantInfoElement, usageData, startTime); 568 - } 569 - }) 570 - .catch(error => { 571 - handleRequestError(error, updateTimer); 572 - }) 573 - .finally(() => { 574 - changeBtnState(true); 575 - }); 576 -} 577 - 578 -// Display usage information 579 -function displayUsageInfo(assistantLabel, usageData, startTime) { 580 - const responseTime = endTimer(startTime); 581 - assistantLabel.innerHTML = `<strong>Assistant (${modelSelect.options[modelSelect.selectedIndex].text})</strong><em> - ΔT ${responseTime.toFixed(1)}s </em>`; 582 - 583 - const usageSpan = document.createElement('span'); 584 - usageSpan.classList.add('usage'); 585 - usageSpan.innerHTML = '<em> - tokens<span class="usage-info">(📊)</span></em>'; 586 - assistantLabel.appendChild(usageSpan); 587 - 588 - const usageInfo = document.createElement('div'); 589 - usageInfo.classList.add('usage-info-box'); 590 - usageInfo.innerHTML = ` 591 - <p>Prompt tokens: ${usageData.prompt_tokens}</p> 592 - <p>Completion tokens: ${usageData.completion_tokens}</p> 593 - <p>Total tokens: ${usageData.total_tokens}</p> 594 - `; 595 - usageSpan.appendChild(usageInfo); 596 -} 597 - 598 - 599 -// Handle non-streaming request 600 -function handleNonStreamingRequest(request, signal, assistantMessageElement) { 601 - const startTime = new Date().getTime(); 602 - 603 - XWikiAiAPI.getCompletions(request, null, signal) 604 - .then(response => { 605 - handleNonStreamingResponse(response, startTime, assistantMessageElement); 606 - }) 607 - .catch(error => { 608 - handleRequestError(error); 609 - }) 610 - .finally(() => { 611 - changeBtnState(true); 612 - }); 613 -} 614 - 615 -// Handle non-streaming response 616 -function handleNonStreamingResponse(response, startTime, assistantMessageElement) { 617 - const endTime = new Date().getTime(); 618 - const responseTime = (endTime - startTime) / 1000; 619 - 620 - const assistantMessage = response.choices[0].message.content; 621 - const llmMemory = response.choices[0].message.memory; 622 - console.debug('LLM memory before response:', llmMemory); 623 - 624 - const sourcesMatch = assistantMessage.match(/Sources:([\s\S]*?)(?=\n\n|$)/); 625 - if (sourcesMatch) { 626 - const sources = sourcesMatch[1].trim(); 627 - const content = assistantMessage.replace(sourcesMatch[0], '').trim(); 628 - const sourcesBubble = createSourcesBubble(sources); 629 - assistantMessageElement.parentElement.insertBefore(sourcesBubble, assistantMessageElement); 630 - assistantMessageElement.innerHTML = DOMPurify.sanitize(marked.parse(content), { FORBID_TAGS: ['style'], FORBID_ATTR: ['src'] }); 631 - } else { 632 - assistantMessageElement.innerHTML = DOMPurify.sanitize(marked.parse(assistantMessage), { FORBID_TAGS: ['style'], FORBID_ATTR: ['src'] }); 633 - } 634 - 635 - conversationHistory.push({ role: 'assistant', content: assistantMessage }); 636 - chatHistory.scrollTop = chatHistory.scrollHeight; 637 - removeWaitingAnimation(); 638 - saveConversationHistory(); 639 - 640 - const assistantInfoElement = assistantMessageElement.previousElementSibling.querySelector('.msg-info'); 641 - if (assistantInfoElement) { 642 - assistantInfoElement.innerHTML = `<strong>Assistant (${modelSelect.options[modelSelect.selectedIndex].text})</strong> - <em>ΔT ${responseTime.toFixed(1)}s </em>`; 643 - 644 - if (response.usage) { 645 - displayUsageInfo(assistantInfoElement, response.usage, startTime); 646 - } 647 - } else { 648 - console.warn('Assistant info element not found'); 649 - } 650 -} 651 - 652 -function endTimer(startTime) { 653 - const endTime = new Date().getTime(); 654 - const responseTime = (endTime - startTime) / 1000; 655 - return responseTime; 656 -} 657 - 658 -// Handle request error 659 -function handleRequestError(error, updateTimer) { 660 - if (error.name === 'AbortError') { 661 - console.log('Request aborted'); 662 - } else { 663 - console.error('Failed to get chat completions:', error); 664 - displayErrorMessage('An error occurred: ' + error.message); 665 - } 666 - removeWaitingAnimation(); 667 - clearInterval(updateTimer); 668 -} 669 - 670 -// Stop the current request 671 -function stopRequest() { 672 - if (abortController) { 673 - console.log('Aborting request...'); 674 - abortController.abort(); 675 - abortController = null; 676 - removeWaitingAnimation(); 677 - changeBtnState(true); 678 - } 679 -} 680 - 681 -// Display user message in the chat history 682 -function displayUserMessage(message) { 683 - const messageElement = document.createElement('div'); 684 - messageElement.classList.add('user-message'); 685 - 686 - const userLabel = document.createElement('div'); 687 - userLabel.classList.add('msg-info'); 688 - userLabel.textContent = 'User:'; 689 - 690 - const userMessageContent = document.createElement('div'); 691 - userMessageContent.classList.add('message-content'); 692 - userMessageContent.textContent = message; 693 - 694 - messageElement.appendChild(userLabel); 695 - messageElement.appendChild(userMessageContent); 696 - 697 - chatHistory.appendChild(messageElement); 698 - chatHistory.scrollTop = chatHistory.scrollHeight; 699 -} 700 - 701 -// Display assistant message in the chat history 702 -function displayAssistantMessage(message, modelName = '', responseTime = null) { 703 - const messageElement = document.createElement('div'); 704 - messageElement.classList.add('assistant'); 705 - 706 - const assistantLabel = document.createElement('div'); 707 - assistantLabel.classList.add('msg-info'); 708 - 709 - if (modelName && responseTime !== null) { 710 - assistantLabel.innerHTML = `<strong>Assistant (${modelName})</strong><em> - ΔT ${responseTime.toFixed(1)}s:</em>`; 711 - } else { 712 - assistantLabel.innerHTML = '<strong>Assistant:</strong>'; 713 - } 714 - 715 - const assistantMessageContent = document.createElement('div'); 716 - assistantMessageContent.classList.add('message-text'); 717 - assistantMessageContent.innerHTML = DOMPurify.sanitize(marked.parse(message), { FORBID_TAGS: ['style'], FORBID_ATTR: ['src'] }); 718 - 719 - messageElement.appendChild(assistantLabel); 720 - messageElement.appendChild(assistantMessageContent); 721 - 722 - chatHistory.appendChild(messageElement); 723 - chatHistory.scrollTop = chatHistory.scrollHeight; 724 - 725 - return assistantMessageContent; 726 -} 727 - 728 - 729 - 730 -// Update the response time in the assistant label 731 -function updateResponseTime(startTime, assistantInfoElement) { 732 - const currentTime = new Date().getTime(); 733 - const elapsedTime = (currentTime - startTime) / 1000; 734 - assistantInfoElement.innerHTML = `<strong>Assistant (${modelSelect.options[modelSelect.selectedIndex].text})</strong> - ΔT ${elapsedTime.toFixed(1)}s:`; 735 -} 736 - 737 -// Display error message in the chat history 738 -function displayErrorMessage(message) { 739 - const messageElement = document.createElement('div'); 740 - messageElement.classList.add('error-message'); 741 - messageElement.textContent = message; 742 - 743 - chatHistory.appendChild(messageElement); 744 - chatHistory.scrollTop = chatHistory.scrollHeight; 745 -} 746 - 747 -// Update user settings 748 -function updateUserSettings() { 749 - userSettings.model = modelSelect.value; 750 - userSettings.temperature = parseFloat(temperatureInput.value); 751 - userSettings.stream = streamCheckbox.checked; 752 - saveUserSettings(); 753 -} 754 - 755 -// Save user settings to local storage 756 -function saveUserSettings() { 757 - localStorage.setItem('userSettings', JSON.stringify(userSettings)); 758 -} 759 - 760 -// Load user settings from local storage 761 -function loadUserSettings() { 762 - const storedSettings = localStorage.getItem('userSettings'); 763 - if (storedSettings) { 764 - userSettings = JSON.parse(storedSettings); 765 - modelSelect.value = userSettings.model; 766 - temperatureInput.value = userSettings.temperature; 767 - streamCheckbox.checked = userSettings.stream; 768 - 769 - // Apply the collapsed state to the settings section 770 - const settingsWrapper = document.querySelector('.settings-wrapper'); 771 - if (userSettings.settingsCollapsed) { 772 - settingsWrapper.classList.add('collapsed'); 773 - } else { 774 - settingsWrapper.classList.remove('collapsed'); 775 - } 776 - } 777 -} 778 - 779 - 780 -// Save conversation history to local storage 781 -function saveConversationHistory() { 782 - if (conversationHistory.length === 0) { 783 - localStorage.removeItem('conversationHistory'); 784 - } else { 785 - localStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); 786 - } 787 -} 788 - 789 -// Load last conversation from local storage 790 -function loadLastConversation() { 791 - const storedConversation = localStorage.getItem('conversationHistory'); 792 - if (storedConversation) { 793 - conversationHistory = JSON.parse(storedConversation); 794 - conversationHistory.forEach(message => { 795 - if (message.role === 'user') { 796 - displayUserMessage(message.content); 797 - } else if (message.role === 'assistant') { 798 - displayAssistantMessage(message.content); 799 - } 800 - }); 801 -} 802 -} 803 - 804 -// Toggle settings visibility 805 -function toggleSettings() { 806 - const settingsWrapper = document.querySelector('.settings-wrapper'); 807 - settingsWrapper.classList.toggle('collapsed'); 808 - userSettings.settingsCollapsed = settingsWrapper.classList.contains('collapsed'); 809 - saveUserSettings(); 810 -} 811 - 812 - 813 -// Call the initialization function when the DOM content is loaded 814 -document.addEventListener('DOMContentLoaded', initializeChatWidget); 815 - 816 -{{/html}}