Wiki source code of Home
Version 15.1 by Jiahao Lai on 2026/03/26 16:08
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 1 | {{include reference="Help.Code.VelocityMacros"/}} | ||
| 2 | |||
| 3 | {{velocity output="false"}} | ||
| 4 | #macro (display4Cards $cards) | ||
| 5 | <div class="row"> | ||
| 6 | #foreach ($card in $cards) | ||
| 7 | ## See http://getbootstrap.com/css/#grid-responsive-resets . | ||
| 8 | #if ($foreach.index > 0 && $foreach.index % 2 == 0) | ||
| 9 | <div class="clearfix visible-sm-block "></div> | ||
| 10 | #end | ||
| 11 | #if ($foreach.index > 0 && $foreach.index % 3 == 0) | ||
| 12 | <div class="clearfix visible-md-block"></div> | ||
| 13 | #end | ||
| 14 | #if ($foreach.index > 0 && $foreach.index % 4 == 0) | ||
| 15 | <div class="clearfix visible-lg-block"></div> | ||
| 16 | #end | ||
| 17 | <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3"> | ||
| 18 | #helpExampleCard($card) | ||
| 19 | </div> | ||
| 20 | #end | ||
| 21 | </div> | ||
| 22 | #end | ||
| 23 | |||
| 24 | |||
| 25 | #set ($PI = [{ | ||
| 26 | 'icon': 'fa fa-file-text-o', | ||
| 27 | 'title': "Manual", | ||
| 28 | 'description':'Mainly software configuration manuals', | ||
| 29 | 'documentation': "https://docs.we-con.com.cn/bin/view/PIStudio/2.Installation%20Software/" | ||
| 30 | }, { | ||
| 31 | 'icon': 'fa fa-support', | ||
| 32 | 'title': "Demo", | ||
| 33 | 'description': 'Abundant functions are demonstrated through different demo', | ||
| 34 | 'documentation': "https://docs.we-con.com.cn/bin/view/PIStudio/2%20Demo/" | ||
| 35 | }, { | ||
| 36 | 'icon': 'fa fa-play-circle', | ||
| 37 | 'title': "Videos", | ||
| 38 | 'description': 'Video shows function more clearly', | ||
| 39 | 'documentation': "https://www.youtube.com/playlist?list=PL_Bpnb2RgaktphrxRaCpFA809H_0xs-cU" | ||
| 40 | },{ | ||
| 41 | 'icon': 'fa fa-download', | ||
| 42 | 'title': "Download", | ||
| 43 | 'description': 'Software Download', | ||
| 44 | 'documentation': "https://docs.we-con.com.cn/bin/view/PIStudio/Download/" | ||
| 45 | }]) | ||
| 46 | |||
| 47 | |||
| 48 | #set ($PLC = [{ | ||
| 49 | 'icon': 'fa fa-file-text-o', | ||
| 50 | 'title': "Manual", | ||
| 51 | 'description': 'Mainly software configuration manuals', | ||
| 52 | 'documentation': "https://docs.we-con.com.cn/bin/view/PLC%20Editor2/01%20Program%20execution/" | ||
| 53 | }, { | ||
| 54 | 'icon': 'fa fa-support', | ||
| 55 | 'title': "Demo", | ||
| 56 | 'description': 'Abundant functions are demonstrated through different demo', | ||
| 57 | 'documentation': "https://docs.we-con.com.cn/bin/view/PLC%20Editor2/2%20Demos/" | ||
| 58 | }, { | ||
| 59 | 'icon': 'fa fa-play-circle', | ||
| 60 | 'title': "Videos", | ||
| 61 | 'description': 'Video shows function more clearly', | ||
| 62 | 'documentation': "https://www.youtube.com/playlist?list=PL_Bpnb2RgaktxcT6G9n1meunomIw3T81_" | ||
| 63 | },{ | ||
| 64 | 'icon': 'fa fa-download', | ||
| 65 | 'title': "Download", | ||
| 66 | 'description': 'Software Download', | ||
| 67 | 'documentation': "https://docs.we-con.com.cn/bin/view/PLC%20Editor2/Download/" | ||
| 68 | }]) | ||
| 69 | |||
| 70 | #set ($V-BOX = [{ | ||
| 71 | 'icon': 'fa fa-file-text-o', | ||
| 72 | 'title': "Manual", | ||
| 73 | 'description': 'Mainly software configuration manuals', | ||
| 74 | 'documentation': "https://docs.we-con.com.cn/bin/view/V-BOX/V-Net/Manual/" | ||
| 75 | }, { | ||
| 76 | 'icon': 'fa fa-support', | ||
| 77 | 'title': "Demo", | ||
| 78 | 'description': 'Abundant functions are demonstrated through different demo', | ||
| 79 | 'documentation': "https://docs.we-con.com.cn/bin/view/V-BOX/V-Net/Training/" | ||
| 80 | }, { | ||
| 81 | 'icon': 'fa fa-play-circle', | ||
| 82 | 'title': "Videos", | ||
| 83 | 'description': 'Video shows function more clearly', | ||
| 84 | 'documentation': "https://www.youtube.com/playlist?list=PL_Bpnb2RgakvYq_Ypk9bydIP7lUfkTDBN" | ||
| 85 | },{ | ||
| 86 | 'icon': 'fa fa-download', | ||
| 87 | 'title': "Download", | ||
| 88 | 'description': 'Software Download', | ||
| 89 | 'documentation': "https://docs.we-con.com.cn/bin/view/V-BOX/V-Net/Download/" | ||
| 90 | }]) | ||
| 91 | |||
| 92 | #set ($Servo = [{ | ||
| 93 | 'icon': 'fa fa-file-text-o', | ||
| 94 | 'title': "Manual", | ||
| 95 | 'description': 'Mainly software configuration manuals', | ||
| 96 | 'documentation': "https://docs.we-con.com.cn/bin/view/Servo/Manual/" | ||
| 97 | }, { | ||
| 98 | 'icon': 'fa fa-support', | ||
| 99 | 'title': "Demo", | ||
| 100 | 'description': 'Abundant functions are demonstrated through different demo', | ||
| 101 | 'documentation': "https://docs.we-con.com.cn/bin/view/Servo/Demo/" | ||
| 102 | }, { | ||
| 103 | 'icon': 'fa fa-play-circle', | ||
| 104 | 'title': "Videos", | ||
| 105 | 'description': 'Video shows function more clearly', | ||
| 106 | 'documentation': "https://www.youtube.com/playlist?list=PL_Bpnb2Rgakum51_QOIFgMjqsod2i1UcB" | ||
| 107 | },{ | ||
| 108 | 'icon': 'fa fa-download', | ||
| 109 | 'title': "Download", | ||
| 110 | 'description': 'Software Download', | ||
| 111 | 'documentation': "https://docs.we-con.com.cn/bin/view/Servo/Download/" | ||
| 112 | }]) | ||
| 113 | |||
| 114 | #set ($Inverter = [{ | ||
| 115 | 'icon': 'fa fa-file-text-o', | ||
| 116 | 'title': "Manual", | ||
| 117 | 'description': 'Mainly software configuration manuals', | ||
| 118 | 'documentation': "https://docs.we-con.com.cn/bin/view/VFD/VM%20AC%20Drive%20User%20Manual/" | ||
| 119 | }, { | ||
| 120 | 'icon': 'fa fa-support', | ||
| 121 | 'title': "Demo", | ||
| 122 | 'description': 'Abundant functions are demonstrated through different demo', | ||
| 123 | 'documentation': "https://docs.we-con.com.cn/bin/view/VFD/3.%20Demo/" | ||
| 124 | }, { | ||
| 125 | 'icon': 'fa fa-play-circle', | ||
| 126 | 'title': "Videos", | ||
| 127 | 'description': 'Video shows function more clearly', | ||
| 128 | 'documentation': "https://www.youtube.com/playlist?list=PL_Bpnb2RgaktOITeNo-g3fq0jBFoubYGz" | ||
| 129 | },{ | ||
| 130 | 'icon': 'fa fa-download', | ||
| 131 | 'title': "Download", | ||
| 132 | 'description': 'Software Download', | ||
| 133 | 'documentation': "https://docs.we-con.com.cn/bin/view/VFD/Download/" | ||
| 134 | }]) | ||
| 135 | |||
| 136 | {{/velocity}} | ||
| 137 | |||
| 138 | {{velocity}} | ||
| 139 | = $services.localization.render("PI HMI") = | ||
| 140 | |||
| 141 | $services.localization.render("") | ||
| 142 | |||
| 143 | {{html clean="false"}} | ||
| 144 | #display4Cards($PI) | ||
| 145 | {{/html}} | ||
| 146 | {{/velocity}} | ||
| 147 | |||
| 148 | |||
| 149 | {{velocity}} | ||
| 150 | = $services.localization.render("PLC") = | ||
| 151 | |||
| 152 | $services.localization.render("") | ||
| 153 | |||
| 154 | {{html clean="false"}} | ||
| 155 | #display4Cards($PLC) | ||
| 156 | {{/html}} | ||
| 157 | {{/velocity}} | ||
| 158 | |||
| 159 | {{velocity}} | ||
| 160 | = $services.localization.render("V-BOX") = | ||
| 161 | |||
| 162 | $services.localization.render("") | ||
| 163 | |||
| 164 | {{html clean="false"}} | ||
| 165 | #display4Cards($V-BOX) | ||
| 166 | {{/html}} | ||
| 167 | {{/velocity}} | ||
| 168 | |||
| 169 | {{velocity}} | ||
| 170 | = $services.localization.render("Servo") = | ||
| 171 | |||
| 172 | $services.localization.render("") | ||
| 173 | |||
| 174 | {{html clean="false"}} | ||
| 175 | #display4Cards($Servo) | ||
| 176 | {{/html}} | ||
| 177 | {{/velocity}} | ||
| 178 | |||
| 179 | {{velocity}} | ||
| 180 | = $services.localization.render("Inverter") = | ||
| 181 | |||
| 182 | $services.localization.render("") | ||
| 183 | |||
| 184 | {{html clean="false"}} | ||
| 185 | #display4Cards($Inverter) | ||
| 186 | {{/html}} | ||
| 187 | {{/velocity}} | ||
| 188 | |||
| 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}} |