Changes for page Home

Last modified by Iris on 2026/04/14 17:40

From version 15.1
edited by Jiahao Lai
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
Change comment: There is no comment for this version

Summary

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> - &Delta;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">(&#128202;)</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>&Delta;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> - &Delta;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> - &Delta;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}}