1/* TODO: 2- check that cleared args fallback to default values, particulary for colors ... i.e. TextColor$="" -> fallback to default (white) 3- default values should be defined either here or in the CSS file but be more consistant 4*/ 5 6// FVL: 12/03/24 7// AOZ_NS.ui_chat is an IIFE. It returns a class-like object. 8// This is on purpose so it has its own scope and its child-classes (Participant and Chatbox) don't pollute the global scope. 9// It also uses WeakMap to "hide" the list of participants and chatboxes it manages 10// Checks were made to ensure that methods of classes are re-used and not copied 11// Thus manager1.summonChatbox === manager2.summonChatbox, participant1.setupCSS === participant2.setupCSS and chatbox1.setupCSS === chatbox2.setupCSS (etc.) 12// Participant and Chatbox are real "JS classes" 13// https://dev.to/allenwoods/defining-private-members-in-javascript-classes-how-and-why-244m 14// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap 15 16// Add "ui_chat" to AOZ NameSpace 17AOZ_NS = (typeof AOZ_NS === 'undefined' ? {} : AOZ_NS); 18 19AOZ_NS.ui_chat = (function() { 20 let isValidCSSColor = Utilities.isValidCSSColor; 21 let isValidCSSBackground = Utilities.isValidCSSBackground; 22 let getColorMix = Utilities.getColorMix; 23 let asHTMLImgSrc = Utilities.asHTMLImgSrc; 24 let formatId = Utilities.formatId; 25 let formatDOMId = Utilities.formatDOMId; 26 let ensureString = Utilities.ensureString; 27 let createAArray = AArray.fromJavascriptArray; 28 let formatUIBackground = Utilities.formatUIBackground; 29 let prepStyleSheet = Utilities.prepStyleSheet 30 31 // private properties 32 let _chatboxes = {}; 33 let _participants = {}; 34 35 36 let ChatManager = {}; 37 let UI2Manager = AOZ_NS.ui2; 38 let UI2 = UI2Manager._classes.UI2; 39 let UI2Feature = UI2Manager._classes.UI2Feature; 40 41 42 43 // setting up mardkown-it and highlightjs 44 ChatManager.md = window.markdownit({ 45 html: true, 46 breaks: true, 47 linkify: true, 48 typographer: true, 49 highlight: function (str, lang) { 50 let hl = ''; 51 52 if (lang && hljs.getLanguage(lang)) { 53 try { 54 hl = hljs.highlight(str, { language: lang }); 55 } catch (__) {} 56 } else if (!lang ) { 57 try { 58 hl = hljs.highlightAuto(str); 59 } catch (__) {} 60 } 61 if (!hl) hl = {value: ChatManager.md.utils.escapeHtml(str)}; 62 63 hl.language = hljs.getLanguage(hl.language)?.name || ""; 64 hl.value = hljs.lineNumbersValue(hl.value, { 65 language: hl.language, 66 tooltip: { 67 copy: "Copy to clipboard", 68 insert: "Insert into code", 69 replace: "Replace current code and run" 70 }, 71 singleLine: true 72 }); 73 74 return '<pre class="hljs"><code class="hljs' + (hl.language ? ' language-' + hl.language : '') + '">' + hl.value + '</code></pre>'; 75 } 76 }).disable(["code","smartquotes"]).use(markdownitEmoji); 77 78 ChatManager.md.copy_code = function(evt) { 79 //let el = this.querySelector("i"); 80 let el = this; 81 let cn = el.className; 82 83 el.className = "bi-check"; //"fas fa-check"; 84 85 let copy = ''; 86 // div .hljs-lang tr tbody 87 this.parentElement.parentElement.parentElement.parentElement.querySelectorAll("td.hljs-ln-code").forEach((e)=> { 88 let text = e.innerText; 89 let tabs = 0; 90 while (text.startsWith(" ")) { 91 text = text.replace(" ", ""); 92 tabs++; 93 } 94 text = "\t".repeat(tabs) + text; 95 96 copy += (copy ? '\n' : '') + text; 97 }); 98 if (el.dataset.type == 'copy') navigator.clipboard.writeText(copy); 99 100 if ( el.chatbox.onCopy ) { 101 application.aoz.runProcedure( el.chatbox.onCopy, 102 {INDEX$ : el.chatbox.id , TEXT$ : copy, TYPE$: el.dataset.type }, 103 ); 104 105 } 106 107 setTimeout(function() { 108 el.className = cn; //"bi-copy"; //"far fa-copy"; 109 }, 2000); 110 } 111 112 // add some tlds 113 // hostname with any 2-letter root zones are ok. 114 // biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|ÑÑ are ok. 115 // could add more by loading lib tlds 116 // some popular tlds from https://w3techs.com/technologies/overview/top_level_domain 117 ChatManager.md.linkify.tlds(['studio','news','link','club','mobi','cat','wiki','xyz','jobs','online','tips','apps','store','live','tech','dev','blog','media','cloud'],true); 118 119 // open links in new tabs 120 // https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer 121 ChatManager.md.renderer.rules.link_open_original = 122 ChatManager.md.renderer.rules.link_open || 123 function (tokens, idx, options, env, self) { 124 return self.renderToken(tokens, idx, options); 125 }; 126 127 ChatManager.md.renderer.rules.link_open = function (tokens, idx, options, env, self) { 128 // Add a new `target` attribute, or replace the value of the existing one. 129 tokens[idx].attrSet('target', '_blank'); 130 131 // Pass the token to the default renderer. 132 return this.link_open_original(tokens, idx, options, env, self); 133 }; 134 135 136 137 //} 138 139 140 141 142/////////////////////////////////////////////////////////////////////////// PARTICIPANT CLASS 143 144 145 class Participant { 146 constructor(id) { 147 this.id = id; // id is supposed valid 148 this.domId = "participant_" + formatDOMId( id )[1]; 149 this.name = ""; 150 this.position = "left"; 151 this.avatar = { 152 src: "run/ui_chat/avatar_wizard_hat.png", 153 background: "moccasin", 154 flip: false, 155 width: 30, 156 border_radius: 30, 157 border: "2px solid transparent" 158 }; 159 this.background = "linear-gradient(0deg, rgb(61 17 222), rgb(140 123 225))"; 160 this.background_color_tip = "rgb(61 17 222)"; 161 this.balloon_border_color = ""; 162 this.color = "gold"; 163 this.border_radius = 10; 164 this.fontSize = null; 165 this.fontName = null; 166 this.steroid = false; // fourre-tout pour Steroid 167 this.onIconClick = ""; 168 169 // TODO: options for md ? 170 this.md = true; 171 172 } 173 174 // class methods/functions 175 updateParticipant ( args ) { 176 let participant = this; 177 178 if ( typeof args.position !== 'undefined' ) participant.position = args.position; 179 if ( typeof args.avatar.src !== 'undefined' ) { 180 // TODO: update messages 181 // TODO: decide what happens if no image 182 participant.avatar.src = args.avatar.src; 183 let n = document.querySelectorAll(".messages .message." + participant.domId + " .avatar img"); 184 185 for (let i=0; i<n.length; i++) n[i].src = asHTMLImgSrc ( participant.avatar.src ); 186 } 187 188 if ( typeof args.avatar.background !== 'undefined' ) {
189 participant.avatar.background = formatUIBackground( args.avatar.background ); 190 } 191 192 if ( typeof args.avatar.flip !== 'undefined' ) { 193 participant.avatar.flip = args.avatar.flip; 194 } 195 196 if ( typeof args.avatar.width !== 'undefined' ) { 197 participant.avatar.width = args.avatar.width; 198 } 199 200 if ( typeof args.avatar.border_radius !== 'undefined' ) { 201 participant.avatar.border_radius = args.avatar.border_radius; 202 } 203 204 if ( typeof args.avatar.border !== 'undefined' ) { 205 args.avatar.border = args.avatar.border.trim().toLowerCase(); 206 // either empty or unset, or a color 207 if (args.avatar.border == "") args.avatar.border = "none"; 208 209 // either a single color value or something more complex ("none" is "complex" because it's not a CSS color) 210 if ( isValidCSSColor( args.avatar.border ) ) { 211 args.avatar.border = "2px solid " + args.avatar.border; 212 } else { 213 // nothing, we will use this value as CSS border 214 } 215 participant.avatar.border = args.avatar.border; 216 } 217 218 219 if ( typeof args.background !== 'undefined' ) participant.background = formatUIBackground ( args.background ); 220 if ( typeof args.background_color_tip !== 'undefined' ) participant.background_color_tip = getColorMix ( args.background_color_tip ); 221 222 if ( typeof args.balloon_border_color !== 'undefined' ) { 223 participant.balloon_border_color = getColorMix( args.balloon_border_color.trim().toLowerCase() ); 224 225 if ( participant.balloon_border_color == "transparent" || participant.balloon_border_color == "none" ) participant.balloon_border_color = ""; 226 } 227 if ( typeof args.color !== 'undefined' ) participant.color = getColorMix ( args.color ); 228 229 if ( typeof args.fontName !== 'undefined' ) participant.fontName = args.fontName; 230 if ( typeof args.fontSize !== 'undefined' ) participant.fontSize = args.fontSize; 231 232 if ( typeof args.steroid !== 'undefined' ) participant.steroid = args.steroid; 233 if ( typeof args.onIconClick !== 'undefined' ) participant.onIconClick = args.onIconClick; 234 235 participant.setupCSS(); 236 } 237 238 setupCSS () { 239 let participant = this; 240 let id = participant.domId; 241 242 let s = prepStyleSheet( "style_" + id ); 243 244 let avatarMargin = participant.avatar.width * 7/6; 245 246 s.sheet.insertRule( 247 ".messages .message." + id + " {\n" + 248 "float: " + participant.position + ";\n" + 249 "color: " + participant.color + ";\n" + 250 //"text-align: " + participant.position + ";\n" + 251 "background: " + participant.background + ";\n" + 252 "border-radius: " + participant.border_radius + "px " + participant.border_radius + "px " + 253 (participant.position == "right" ? "0 " + participant.border_radius + "px" : participant.border_radius + "px 0 ") + 254 ";\n" + 255 //"margin-" + participant.position + ": " + (participant.avatar.src ? "35px" : "0") + ";\n" + 256 ( participant.fontName ? "font-family: " + participant.fontName + ";\n" : "" ) + 257 //( participant.fontSize ? "font-size: " + ( participant.fontSize <= 1 ? participant.fontSize*100 + "%" : participant.fontSize + "px" ) + ";\n" : "" ) + 258 ( participant.fontSize ? "font-size: " + participant.fontSize + "px;\n" : "" ) + 259 //"margin-" + participant.position + ": " + avatarMargin + "px;\n" + 260 "margin-left" + ": " + avatarMargin + "px;\n" + // TODO: this should take into account the various participants widths 261 "margin-right" + ": " + avatarMargin + "px;\n" + 262 "margin-bottom: " + ( participant.avatar.width * 3/4 ) + "px;\n" + 263 ( participant.balloon_border_color ? "border: 2px solid " + participant.balloon_border_color + ";\n" : "" ) + 264 "}" 265 ); 266 267 s.sheet.insertRule( 268 ".messages .message." + id + " .avatar{\n" + 269 participant.position + ": -" + avatarMargin + "px;\n" + 270 "width: " + participant.avatar.width + "px;\n" + 271 "height: " + participant.avatar.width + "px;\n" + 272 "background: " + participant.avatar.background + ";\n" + 273 //"bottom: -" + ( participant.avatar.width * 0.375 ) + "px;\n" + 274 "bottom: -" + ( participant.avatar.width / (4/3) ) + "px;\n" + 275 "border-radius: " + participant.avatar.border_radius + "px;\n" + 276 "border: " + participant.avatar.border + "\n" + 277 "}" 278 ); 279 280 if ( participant.balloon_border_color ) { 281 s.sheet.insertRule( 282 ".messages .message." + id + "::before {\n" + 283 //"border-top-color: " + participant.background_color_tip + ";\n" + 284 ( participant.balloon_border_color ? "border-top-color: " + participant.balloon_border_color + ";\n" : "" ) + 285 //participant.position + ":0; \n" + 286 participant.position + ":-2px; \n" + 287 //"border-" + participant.position + ": none;\n" + 288 //"border-" + participant.position + ": 0px solid " + participant.balloon_border_color + ";\n" + 289 ( participant.position == "right" ? "left:" : "right:" ) + "auto;\n" + 290 //( participant.position == "right" ? "border-left:" : "border-right:" ) + "7px solid transparent; \n" + 291 ( participant.position == "right" ? "border-left:" : "border-right:" ) + "8px solid transparent; \n" + 292 "}" 293 ); 294 } 295 296 s.sheet.insertRule( 297 ".messages .message." + id + "::after {\n" + 298 "border-top: 10px solid " + participant.background_color_tip + ";\n" + 299 participant.position + ":0; \n" + 300 "border-" + participant.position + ": none;\n" + 301 ( participant.position == "right" ? "left:" : "right:" ) + "auto;\n" + 302 ( participant.position == "right" ? "border-left:" : "border-right:" ) + "10px solid transparent; \n" + 303 "}" 304 ); 305 306 s.sheet.insertRule( 307 ".messages .message.loading." + id + "::after {\n" + 308 "display: none\n" + // no bubble arrow 309 "}" 310 ); 311 312 s.sheet.insertRule( 313 ".messages .message.loading." + id + " {\n" + 314 "border-radius: " + participant.border_radius + "px;
314\n" + // no bubble arrow, thus all round borders 315 "}" 316 ); 317 318 s.sheet.insertRule( 319 ".messages .message.loading." + id + "::before, " + 320 ".messages .message.loading." + id + " span::before, " + 321 ".messages .message.loading." + id + " span::after " + 322 "{\n" + 323 "background-color :" + participant.color + ";\n" + 324 "left: 50%;\n" + 325 "border-right: unset;\n" + 326 "border-left: unset;\n" + 327 "}" 328 ); 329 330 if ( participant.avatar.flip ) { 331 s.sheet.insertRule( 332 ".messages .message." + id + " figure.avatar img" + 333 "{\n" + 334 "-webkit-transform: scaleX(-1);\n" + 335 "transform: scaleX(-1);\n" + 336 "}" 337 ); 338 } 339 } 340 341 } 342 343 ChatManager.summonParticipant = function(id) { 344 let err; 345 [err, id] = formatId( id ); 346 if (err) throw "UI_" + err; 347 let n = _participants[id]; 348 349 return _participants[id] || ( _participants[id] = new Participant(id, this) ); 350 }; 351 352 ChatManager.getParticipant = function ( id ) { 353 let err; 354 [err, id] = formatId( id ); 355 if ( err ) throw "UI_" + err; 356 return _participants[id]; 357 }; 358 ChatManager.cmdParticipant = function ( args ) { 359 if( typeof args !== "object" || Array.isArray(args) ) throw 'UI_participant_arguments_missing'; 360 361 let participant = this.summonParticipant( args.id ); 362 363 participant.updateParticipant( args ); 364 365 return participant; 366 }; 367 368 // setting up default participants 369 ChatManager.cmdParticipant ({ 370 id: "host", 371 name: "", 372 position: "right", 373 avatar: { 374 src: "run/ui_chat/avatar_lucie.png", 375 background: "seagreen", 376 flip: false, 377 width: 30, 378 border_radius: 30, 379 border: "2px solid transparent" 380 }, 381 background: "linear-gradient(120deg, #248A52, #257287)", 382 background_color_tip : "#257287", 383 balloon_border_color: "", 384 color: "white", 385 border_radius: 10, 386 fontSize: null, 387 fontName: null 388 }); 389 ChatManager.cmdParticipant ({ 390 id: "guest", 391 name: "", 392 position: "left", 393 avatar: { 394 src: "run/ui_chat/avatar_ai.png", 395 background: "goldenrod", 396 flip: false, 397 width: 30, 398 border_radius: 30, 399 border: "2px solid transparent" 400 }, 401 background: "linear-gradient(60deg, gold, #da9520)", //goldenrod)", //"rgba(0, 0, 0, 0.3)", 402 background_color_tip : "gold", 403 balloon_border_color: "", 404 color: "maroon", //"rgba(255, 255, 255, 0.5)", 405 border_radius: 10, 406 fontSize: null, 407 fontName: null 408 }); 409 410/////////////////////////////////////////////////////////////////// CLASS CHATBOX 411 412 class Chatbox extends UI2 { 413 // constructor 414 constructor(id, args) { 415 super (id, args, "Chatbox", "div", {baseClass: "aoz-ui-chatbox"} ); 416 417 //let domId = "chatbox_" + formatDOMId( id )[1]; 418 let domId = this.domId; 419 420 // TODO: this is probably already taken care of by UI2 421 if ( document.querySelector(".aoz-ui-chatbox#" + domId) !== null ) throw "UI_chatbox_id_already_exists"; 422 if ( document.getElementById(domId) ) throw "UI_id_already_attributed"; 423 424 let chatbox = this; 425 426 //this.id = id; // id is supposed valid 427 //this.domId = domId; 428 429 this.onInput = ""; 430 this.onCopy = ""; 431 this.onTitleButton = ""; 432 this.onCitationClick = ""; 433 this.autoScroll = true; 434 this.msg_obj = {}; 435 436 // all the params like title, subTitle, avatar.src/ref, hint, submit text etc will be set by chatCommand/update 437 // default values ... 438 this.x = 0; 439 this.y = 0; 440 this.width = application.aoz.currentScreen.dimension.width; 441 this.height = application.aoz.currentScreen.dimension.height; 442 this.insertIcon = false; 443 this.replaceIcon = false; 444 445 this.reception_step0_4 = 0; 446 447 this.title = { text: "", background: "rgba(0, 0, 0, 0.2)" }; 448 this.subTitle = {text: ""}; 449 this.avatar = {border: "unset", border_radius:0, src: "run/ui_chat/icon_uichat.png" }; 450 this.input = {background: "rgba(0, 0, 0, 0.3)"} //, color: "rgba(255, 255, 255, 0.7)"}; 451 this.submit = {background: "#248A52", color: "white", text:"Send", cancel:false, cancelText : "Cancel", icon: "fa-solid fa-circle-chevron-right", cancelIcon : "fa-solid fa-ban"}; 452 this.attach = {background: "#248A52", color: "white", visible: true}; 453 this.background = "rgba(0, 0, 0, 0.5)"; 454 this.color = "white"; 455 this.border_radius = "10px"; // border_radius is a string here ... 456 this.timeTextColor = "rgba(255,255,255,0.4)"; 457 this.scrollbarColor = "black"; 458 459 this.currentHost = "host"; 460 this.currentGuest = "guest"; 461 462 463 // create html 464 //let parent = document.querySelector("body"); 465 466 467 /*let html = '<div class="chat" id="' + domId + '">\n' + 468 '\t<div class="chat-title">\n' +*/ 469 let html_title = 470 '<h1></h1>' + 471 '<h2></h2>' + 472 '<figure class="avatar">' + 473 '<img src="' + this.avatar.src + '" />' + // default avatar 474 '</figure>' + 475 '<button class="title-button" type="button"><i class="bi-arrow-clockwise"></i></button>'; /* + 476 '\t</div>\n' +*/ 477 478 /*'\t<div class="messages">\n' +*/ 479 let html_msg = 480 '<div class="messages-content">' + 481 '<div class="messages-scroller">' + 482 '<div class="messages-scroller-container"></div>' + 483 '</div>' + 484 '</div>';/* + 485 '\t</div>\n' +*/ 486 /*'\t<div class="message-box">\n' +*/ 487 let html_msgbox = 488 '<div class="files-box" style="display:none">
488' + 489 490 (application.aoz.is_touch ? '' : '<div class="files-left-arrow"><span><i class="bi-arrow-left-circle"></i></<span></div>') + 491 '<div class="files"></div>' + 492 493 (application.aoz.is_touch ? '' : '<div class="files-right-arrow-wrap"><div class="files-right-arrow"><span><i class="bi-arrow-right-circle"></i></<span></div></div>') + 494 495 '</div>' + // files-box 496 497 '<label for="kb_filepicker_default" class="message-attach"><span></span><i class="bi-paperclip"></i></label>' + 498 499 '<textarea cols="1" keys-binding="yes" rows="1" type="text" class="message-input" placeholder="Message ..."></textarea>' + 500 '<button type="submit" class="message-submit"><i class="' + this.submit.icon + '"></i><span data-submit-text>' + this.submit.text + '</span></button>'; /* + 501 '\t</div>\n' + 502 '</div>`\n';*/ 503 504 //parent.insertAdjacentHTML("beforeend", html); 505 506 let div_title = document.createElement("div"); 507 div_title.className = "chat-title"; 508 div_title.innerHTML = html_title; 509 this.satellites.push([div_title]); 510 let div_msg = document.createElement("div"); 511 div_msg.className = "messages"; 512 div_msg.innerHTML = html_msg; 513 this.satellites.push([div_msg]); 514 let div_msgbox = document.createElement("div"); 515 div_msgbox.className = "message-box"; 516 div_msgbox.innerHTML = html_msgbox; 517 this.satellites.push([div_msgbox]); 518 519 this.update({}); // forces satellites 520 521 /* 522 // widget default dimension/position 523 this.el = document.getElementById(domId);; 524 this.el.setAttribute('data-x', this.x); 525 this.el.setAttribute('data-y', this.y); 526 this.el.setAttribute('data-width', this.width); 527 this.el.setAttribute('data-height', this.height); 528 */ 529 // add simplebar scroller 530 let sbar = new SimpleBar(document.querySelector(chatbox.chatID_and('.messages-scroller'))); 531 532 sbar.getScrollElement().addEventListener('scroll', function(e) { 533//console.log(this.scrollTop, this.scrollHeight); 534 if (chatbox.scrollByCode) { 535 if (this.scrollTop + this.offsetHeight == this.scrollHeight) chatbox.scrollByCode = false; 536 return; 537 } 538 if (this.scrollTop + this.offsetHeight != this.scrollHeight) chatbox.autoScroll = false; 539 }); 540 541 /* 542 application.aoz.ui.elements.push(this.el); 543 544 // Rendering context 545 application.aoz.ui.setRenderingContext( {}, this.el ); 546 application.aoz.ui.resize() 547 */ 548 this.btnSubmit = document.querySelector(chatbox.chatID_and('.message-submit')); 549 this.btnSubmit.addEventListener("click", (e) => { 550 let t = document.querySelector (chatbox.chatID_and ('textarea') ); 551 if ( t.getAttribute ('disabled') === '' ) { 552 e.preventDefault(); 553 return; 554 } 555 556 chatbox.sendHostMessage(); 557 }); 558 559 // TOFIX 560 this.titleButton = document.querySelector(chatbox.chatID_and('.title-button')); 561 this.titleButton.addEventListener("click", (e) => { 562 if ( chatbox.onTitleButton ) { 563 application.aoz.runProcedure( chatbox.onTitleButton, 564 {INDEX$ : chatbox.id , BUTTON$ : "refresh" }, 565 ); 566 } 567 }); 568 569 570 571 this.btnAttach = document.querySelector(chatbox.chatID_and('.message-attach')); 572 573 this.divFilesBox = document.querySelector(chatbox.chatID_and('.files-box')); 574 575 this.divFiles = document.querySelector(chatbox.chatID_and('.files')); 576 this.divFiles.addEventListener("wheel", function (e) { 577 chatbox.divFiles.scrollLeft += e.deltaY; 578 }); 579 580 this.startFilesArrowClick = function (step) { 581 let scrollIt = function(step) { 582 chatbox.divFiles.scrollLeft += step; 583 } 584 let scrollItFirst = function(step) { 585 clearInterval( chatbox.clickInterval ); 586 chatbox.clickInterval = setInterval(function() { scrollIt (step); }, 5); 587 } 588 589 scrollIt(step); 590 chatbox.clickInterval = setTimeout(function() { scrollItFirst (step) }, 250); 591 } 592 593 this.btnFilesLeftArrow = document.querySelector(chatbox.chatID_and('.files-left-arrow')); 594 if (this.btnFilesLeftArrow) 595 this.btnFilesLeftArrow.addEventListener("mousedown", function(e) { 596 chatbox.startFilesArrowClick(-3); 597 }); 598 599 this.btnFilesRightArrow = document.querySelector(chatbox.chatID_and('.files-right-arrow')); 600 if (this.btnFilesRightArrow) 601 this.btnFilesRightArrow.addEventListener("mousedown", function(e) { 602 chatbox.startFilesArrowClick(3); 603 }); 604 605 ["mouseup", "touchend"].forEach(function(e) { 606 document.addEventListener(e, function(e) { 607 if ( chatbox.clickInterval ) { 608 clearInterval( chatbox.clickInterval ); 609 delete chatbox.clickInterval; 610 chatbox.focus(); 611 } 612 }); 613 }); 614 615 if (this.btnFilesRightArrow) 616 this.divFiles.addEventListener("scrollend", function(e) { 617 chatbox.updateFilesArrows(); 618 }); 619 620 621 622 this.updateFilesArrows = function() { 623 if (chatbox.divFiles.scrollLeft == 0 || chatbox.divFiles.offsetWidth == chatbox.divFiles.scrollWidth) { 624 chatbox.btnFilesLeftArrow.style.cursor = "auto"; 625 chatbox.btnFilesLeftArrow.style.opacity = 0.25; 626 } else { 627 chatbox.btnFilesLeftArrow.style.cursor = "pointer"; 628 chatbox.btnFilesLeftArrow.style.opacity = 1; 629 } 630 631 if (chatbox.divFiles.scrollLeft + chatbox.divFiles.clientWidth == chatbox.divFiles.scrollWidth || chatbox.divFiles.offsetWidth == chatbox.divFiles.scrollWidth) { 632 chatbox.btnFilesRightArrow.style.cursor = "auto"; 633 chatbox.btnFilesRightArrow.style.opacity = 0.25; 634 } else { 635 chatbox.btnFilesRightArrow.style.cursor = "pointer"; 636 chatbox.btnFilesRightArrow.style.opacity = 1; 637 } 638 } 639 640 if (this.btnFilesRightArrow) this.updateFilesArrows(); 641 642 // create default KBFilePicker 643 // TODO: support KBFilePicker$ 644 // TODO: make sure label is made "for" this filepicker 645 646 this.KBFilePicker = KBFilePicker.command({id: "default"}); 647 document.querySelector(chatbox.chatID_and('label.message-attach')).setAttribute ("for", this.KBFilePicker.el.id); 648 649 this.lastSelectedFiles = []; 650 this.selectedFiles = []; 651 this.selectedFileNames = []; 652 653 // TODO: support KBFilePicker$ -> inputFile must point to that filepicker... 654 // TODO ?: if multiple selection is disabled, the attach button must disappear ... and reappear only when the selected file has been removed from the filesbox 655 this.inputFile = document.querySelector("#" + this.KBFilePicker.el.id); 656 657 this.setupFilePickerChangeEvent(); 658 659 this.removeFile = function() { 660 let id = this.previousElementSibling.dataset.kbf; 661 let p = chatbox.selectedFiles.indexOf(id); 662 if (p>=0) { 663 chatbox.selectedFiles.splice(p,1); 664 chatbox.selectedFileNames.splice(p,1); 665 } 666 667 let fileElem = this.parentElement; 668 //fileElem.style.overflow = 'hidden'; 669 let prevOffsetWidth = 0 670 let interval = setInterval (function() { 671 if (fileElem.offsetWidth != prevOffsetWidth) { 672 prevOffsetWidth = fileElem.offsetWidth; 673 fileElem.style.width = (fileElem.offsetWidth - 4) + "px"; 674 } else { 675 clearInterval(interval); 676 fileElem.remove(); 677 chatbox.updateFilesBox(); 678 } 679 }, 10); 680 KBFile.delete(id); 681 } 682 683 this.updateFilesBox = function() { 684 if (chatbox.selectedFiles.length==0) { 685 chatbox.divFilesBox.style.display = 'none'; 686 return; 687 } 688 689 chatbox.divFilesBox.style.display = ''; 690 if (chatbox.btnFilesRightArrow) chatbox.updateFilesArrows(); 691 } 692 693 694 695 this.txtInput = document.querySelector(chatbox.chatID_and('.message-input')); 696 this.txtInput.addEventListener("keydown", (e) => { 697 if (e.which == 13 && !e.shiftKey) { 698 e.preventDefault(); 699 chatbox.sendHostMessage(); 700 } 701 }); 702 // https://stackoverflow.com/a/25621277/991782 703 this.txtInput.addEventListener("input", function() {chatbox.inputResize(c
703hatbox)} ); 704 this.txtInput.addEventListener("focus", function() {chatbox.inputResize(chatbox)} ); 705 706 //this.txtInput.style.width = "calc(100% - " + (this.btnSubmit.offsetWidth + this.btnAttach.offsetWidth + 12) + "px)" 707 this.inputResize(this, true); 708 709 this.setupCSS(); 710 711 } 712 713 setupFilePickerChangeEvent () { 714 let chatbox = this; 715 716 this.filePickerChangeEvent = function () { 717 chatbox.selectedFiles = chatbox.selectedFiles.concat(Object.keys(chatbox.KBFilePicker.selection)); 718 chatbox.selectedFileNames = chatbox.selectedFileNames.concat(chatbox.KBFilePicker.filenames); 719 720 Object.values(chatbox.KBFilePicker.selection).forEach((kbf) => { 721 let divFile = document.createElement('div'); 722 divFile.className = "file"; 723 let iFileIcon = document.createElement('i'); 724 iFileIcon.className = "bi-file-earmark"; 725 divFile.appendChild(iFileIcon); 726 let spanFileName = document.createElement('span'); 727 spanFileName.className="filename"; 728 //let kbf = KBFile.getItem(e); 729 spanFileName.title = kbf.filename; 730 spanFileName.innerHTML = kbf.filename; 731 spanFileName.dataset.kbf = kbf.id; 732 divFile.appendChild(spanFileName); 733 let iCloseIcon = document.createElement('i'); 734 iCloseIcon.className = "bi-x-circle"; 735 736 iCloseIcon.addEventListener("click", chatbox.removeFile); 737 738 divFile.appendChild(iCloseIcon); 739 chatbox.divFiles.appendChild(divFile); 740 741 chatbox.updateFilesBox(); 742 743 //<div class="file"><i class="bi-file-earmark"></i><span class="filename" data-kbf="filename.ext+">filename.ext</span><i class="bi-x-circle"></i></div> 744 745 }); 746 } 747 748 this.inputFile.addEventListener("change", this.filePickerChangeEvent); 749 } 750 751 // class methods/functions 752 updateChatbox ( args ) { 753 let chatbox = this; 754 let el = chatbox.el; 755 756 let needResize = false; 757 let needSetupCSS = false; 758 759 // ================== size and position 760 761 // TODO: data-screenIndex and data-renderingContext are not properly handled by ui instructions ... fix that ... one day ... 762 //if ( typeof args.screenIndex != 'undefined' ) el.setAttribute( 'data-screenIndex', args.screenIndex ); 763 764 if ( typeof chatbox.x === 'undefined' && typeof args.x === 'undefined' ) args.x = 0; 765 766 if (typeof args.x !== 'undefined' && chatbox.x != args.x) { 767 chatbox.x = args.x; 768 el.setAttribute('data-x', args.x); 769 needResize = true; 770 } 771 772 if ( typeof chatbox.y === 'undefined' && typeof args.y === 'undefined' ) args.y = 0; 773 774 if (typeof args.y !== 'undefined' && chatbox.y != args.y) { 775 chatbox.y = args.y; 776 el.setAttribute('data-y', args.y); 777 needResize = true; 778 } 779 780 if ( typeof chatbox.width === 'undefined' && typeof args.width === 'undefined' ) args.width = application.aoz.currentScreen.dimension.width; 781 782 if (typeof args.width !== 'undefined' && chatbox.width != args.width) { 783 chatbox.width = args.width; 784 el.setAttribute('data-width', args.width); 785 needResize = true; 786 } 787 788 if ( typeof chatbox.height === 'undefined' && typeof args.height === 'undefined' ) args.height = application.aoz.currentScreen.dimension.height; 789 790 if (typeof args.height !== 'undefined' && chatbox.height != args.height) { 791 chatbox.height = args.height; 792 el.setAttribute('data-height', args.height); 793 el.style.overflowY = 'auto'; // TODO: ????? what ???? 794 needResize = true; 795 } 796 797 // ======================= FONT 798 // defaults are in CSS 799 if ( typeof args.fontSize !== 'undefined' ) { 800 el.style.fontSize = args.fontSize + 'px'; 801 } 802 803 if ( typeof args.fontName !== 'undefined' ) { 804 el.style.fontFamily = args.fontName 805 } 806 807 // ======================= GROUP .... useless for now ... 808 if (typeof args.group !== 'undefined') { 809 chatbox.group = args.group; 810 el.setAttribute('data-group', args.group); 811 } 812 chatbox.group = chatbox.group || ""; // default is "" 813 814 // ======================= TITLE BANNER 815 args.title = args.title || {}; 816 args.subTitle = args.subTitle || {}; 817 args.avatar = args.avatar || {}; 818 if ( typeof args.title.text !== 'undefined') { 819 chatbox.setTitle ( args.title.text ); 820 } 821 822 if ( typeof args.title.background !== 'undefined') {
823 chatbox.title.background = formatUIBackground( args.title.background ); 824 needSetupCSS = true; 825 } 826 827 if ( typeof args.subTitle.text !== 'undefined' ) { 828 chatbox.setSubTitle ( args.subTitle.text ); 829 } 830 831 if ( typeof args.avatar.src !== 'undefined' ) { 832 // default is "run/ui_chat/icon_uichat.png" and was set in initial html 833 chatbox.setAvatar ( args.avatar.src ); 834 } 835 836 if ( typeof args.avatar.border_radius !== 'undefined' ) { 837 // default is zero (class constructor) 838 chatbox.avatar.border_radius = args.avatar.border_radius + "px"; 839 needSetupCSS = true; 840 } 841 842 if ( typeof args.avatar.border !== 'undefined' ) { 843 // default is "unset" (class constructor) 844 args.avatar.border = getColorMix( args.avatar.border.trim().toLowerCase() ); 845 // either empty or unset, or a color 846 if ( args.avatar.border == "" || args.avatar.border == "none" ) args.avatar.border = "unset"; 847 else if (args.avatar.border != "unset") { 848 // a color is expected. 849 if ( isValidCSSColor( args.avatar.border ) ) { 850 args.avatar.border = "2px solid " + args.avatar.border; 851 } else { 852 // nothing, we will use this value as CSS border 853 } 854 } 855 856 chatbox.avatar.border = args.avatar.border; 857 needSetupCSS = true; 858 } 859 860 // ====================== SUBMIT BUTTON 861 args.submit = args.submit || {}; 862 if ( typeof args.submit.text !== 'undefined' ) { 863 // default is SEND (html) 864 this.submit.text = args.submit.text; 865 chatbox.setSubmitButtonText ( this.submit.text ); 866 } 867 868 // TODO : fix submit and cancel icons 869 if ( typeof args.submit.icon !== 'undefined' ) { 870 // default is fa-chevron-right (html) 871 this.submit.icon = args.submit.icon; 872 chatbox.setSubmitButtonText ( this.submit.text ); 873 } 874 875 if ( typeof args.submit.cancelText !== 'undefined' ) { 876 // default is CANCEL (html) 877 chatbox.submit.cancelText = args.submit.cancelText; 878 } 879 880 if ( typeof args.submit.cancelIcon !== 'undefined' ) { 881 // default is fa-ban 882 chatbox.submit.cancelIcon = args.submit.cancelIcon; 883 } 884 885 886 887 if ( typeof args.submit.background !== 'undefined' ) { 888 // default is #248A52 (class) 889 chatbox.submit.background = formatUIBackground ( args.submit.background ); 890 needSetupCSS = true; 891 } 892 893 if ( typeof args.submit.color !== 'undefined' ) { 894 // default is white (class) 895 chatbox.submit.color = args.submit.color; 896 needSetupCSS = true; 897 } 898 899 // ====================== ATTACH BUTTON 900 args.attach = args.attach || {}; 901 if ( typeof args.attach.text !== 'undefined' ) { 902 // default is empty (html) 903 chatbox.setAttachButtonText ( args.attach.text ); 904 } 905 906 if ( typeof args.attach.background !== 'undefined' ) { 907 // default is #248A52 (class) 908 chatbox.attach.background = formatUIBackground ( args.attach.background ); 909 needSetupCSS = true; 910 } 911 912 if ( typeof args.attach.color !== 'undefined' ) { 913 // default is white (class) 914 chatbox.attach.color = args.attach.color; 915 needSetupCSS = true; 916 } 917 918 if ( typeof args.attach.visible !== 'undefined' ) { 919 // default is true (html) 920 chatbox.attach.visible = !!args.attach.visible; 921 needSetupCSS = true; 922 } 923 924 // ====================== INPUT BANNER 925 args.input = args.input || {}; 926 if ( typeof args.input.hint !== 'undefined' ) { 927 // default is "Message ..." (html) 928 chatbox.setInputHint ( args.input.hint ); 929 } 930 931 if ( typeof args.input.background !== 'undefined' ) { 932 // default is rgba(0, 0, 0, 0.3) (class) 933 chatbox.input.background = formatUIBackground( args.input.background ); 934 needSetupCSS = true; 935 } 936 937 if ( typeof args.input.color !== 'undefined' ) { 938 // default is TextColor with 30% transparency (setupCSS) 939 chatbox.input.color = getColorMix( args.input.color ); 940 needSetupCSS = true; 941 } 942 943 if ( typeof args.input.hint_color !== 'undefined' ) { 944 // default is not defined (browser default) 945 chatbox.input.hint_color = getColorMix( args.input.hint_color ); 946 needSetupCSS = true; 947 } 948 949 // ==================== MAIN APPEARANCE 950 if ( typeof args.background !== 'undefined' ) { 951 // default is rgba(0, 0, 0, 0.5) (class)
952 chatbox.background = formatUIBackground ( args.background ); 953 needSetupCSS = true; 954 } 955 956 if ( typeof args.border_radius !== 'undefined' ) { 957 // default is 20 (class) 958 // either a number or a number+px, OR A STRING ! (coming from Arc$) 959 960 if ( !isNaN(args.border_radius) ) args.border_radius = args.border_radius + "px"; 961 chatbox.border_radius = args.border_radius; 962 needSetupCSS = true; 963 } 964 965 if ( typeof args.color !== 'undefined' ) { 966 // text color. Default is white (class) 967 chatbox.color = getColorMix( args.color ); 968 needSetupCSS = true; 969 } 970 971 if ( typeof args.timeTextColor !== 'undefined' ) { 972 // default is rgba(255,255,255,0.4) (class) 973 chatbox.timeTextColor = args.timeTextColor; 974 needSetupCSS = true; 975 } 976 977 if ( typeof args.scrollbarColor !== 'undefined' ) { 978 // default is black (class) 979 chatbox.scrollbarColor = args.scrollbarColor; 980 needSetupCSS = true; 981 } 982 983 if ( typeof args.insertIcon !== 'undefined') { 984 chatbox.insertIcon = !!args.insertIcon; 985 needSetupCSS = true; 986 } 987 if ( typeof args.replaceIcon !== 'undefined') { 988 chatbox.replaceIcon = !!args.replaceIcon; 989 needSetupCSS = true; 990 } 991 992 993 994 //if ( needResize ) application.aoz.ui.resize() 995 if ( needSetupCSS ) chatbox.setupCSS(); 996 997 // ================================== CHATBOX FEATURES/COMMANDS 998 //args.host = formatId( args.host )[1] || "host"; 999 //args.guest = formatId( args.guest )[1] || "guest"; 1000 1001 chatbox.currentHost = formatId ( args.host )[1] || chatbox.currentHost || "host"; 1002 chatbox.currentGuest = formatId ( args.guest )[1] || chatbox.currentGuest || "guest"; 1003 1004 if ( typeof args.reset !== 'undefined' && args.reset === true ) { 1005 chatbox.clearMessages(); 1006 } 1007 1008 if ( typeof args.message !== 'undefined' ) { 1009 chatbox.addMessage( args.message, {role: args.role || chatbox.currentHost, replace: args.replace, more: args.more} ); 1010 } 1011 1012 1013 1014 if ( typeof args.answer !== 'undefined' ) { 1015 let answerTo=""; 1016 1017 if ( typeof args.answerTo !== 'undefined' ) { 1018 answerTo = args.answerTo; // valid only with Answer$ and only once. 1019 } 1020 1021 chatbox.addMessage( args.answer, {role: args.role || chatbox.currentGuest, replace: args.replace, more: args.more, answerTo: answerTo} ); 1022 } 1023 1024 if ( typeof args.inputEnable !== 'undefined' ) { 1025 let t = document.querySelector (chatbox.chatID_and ('textarea') ); 1026 let bs = document.querySelector (chatbox.chatID_and(".message-box button.message-submit")); 1027 let ba = this.KBFilePicker.el; 1028 let br = document.querySelector (chatbox.chatID_and(".title-button")); 1029 1030 if ( args.inputEnable ) { 1031 t.removeAttribute('disabled'); 1032 bs.removeAttribute('disabled'); 1033 ba.removeAttribute('disabled'); 1034 br.removeAttribute('disabled'); 1035 1036 } 1037 else { 1038 t.setAttribute('disabled',''); 1039 bs.setAttribute('disabled',''); 1040 ba.setAttribute('disabled',''); 1041 br.setAttribute('disabled','') 1042 } 1043 } 1044 1045 if ( typeof args.focus !== 'undefined' && args.focus == true ) chatbox.focus(); 1046 1047 if ( typeof args.typing !== 'undefined' ) { 1048 chatbox.addMessage ( "", {role: args.role || chatbox.currentGuest, typing: args.typing} ); 1049 } 1050 1051 1052 if ( typeof args.onInput !== 'undefined' ) { 1053 chatbox.onInput = args.onInput; 1054 } 1055 1056 if ( typeof args.onCopy !== 'undefined' ) { 1057 chatbox.onCopy = args.onCopy; 1058 } 1059 1060 if ( typeof args.onTitleButton !== 'undefined' ) { 1061 chatbox.onTitleButton = args.onTitleButton; 1062 } 1063 1064 if ( typeof args.onCitationClick !== 'undefined' ) { 1065 chatbox.onCitationClick = args.onCitationClick; 1066 } 1067 1068 if (typeof args.kbfp !== 'undefined' ) { 1069 chatbox.inputFile.removeEventListener("change", chatbox.filePickerChangeEvent); 1070 1071 chatbox.KBFilePicker = KBFilePicker.command({id: args.kbfp}); 1072 document.querySelector(chatbox.chatID_and('label.message-attach')).setAttribute ("for", chatbox.KBFilePicker.el.id); 1073 1074 chatbox.inputFile = document.querySelector("#" + chatbox.KBFilePicker.el.id); 1075 1076 chatbox.setupFilePickerChangeEvent(); 1077 } 1078 1079 if (typeof args.scrollTo !== 'undefined') { 1080 if (typeof args.scrollTo == 'string') { 1081 args.scrollTo = args.scrollTo.toLowerCase(); 1082 if (args.scrollTo == "top" || args.scrollTo == "min" || args.scrollTo == "start") args.scrollTo = 0; 1083 else args.scrollTo = undefined; 1084 } else if (args.scrollTo === true) args.scrollTo = undefined; 1085 1086 chatbox.updateScrollbar(args.scrollTo, "smooth", true); 1087 } 1088 1089 } 1090 1091 chatID_and (s) { 1092 return '.aoz-ui-chatbox#' + this.domId + ' ' + (s || ""); 1093 } 1094 1095 setupCSS () { 1096 let chatbox = this; 1097 1098 let s = prepStyleSheet( "style_" + chatbox.domId ); 1099 1100 s.sheet.insertRule( 1101 chatbox.chatID_and() +" {\n" + 1102 "background: " + chatbox.background + ";\n" + 1103 "border-radius: " + chatbox.border_radius + ";\n" + 1104 "}" 1105 ); 1106 1107 s.sheet.insertRule( 1108 chatbox.chatID_and(".message-box .message-input") +" {\n" + 1109 "color: " + (chatbox.input.color ? chatbox.input.color : " color-mix(in srgb, " + chatbox.color + ", transparent 30%)" ) + ";\n" + 1110 "}" 1111 ); 1112 1113 s.sheet.insertRule( 1114 chatbox.chatID_and(".message-box") +" {\n" + 1115 "background: " + chatbox.input.background + ";\n" + 1116 "}" 1117 ); 1118 1119 if ( chatbox.input.hint_color ) { 1120 s.sheet.insertRule( 1121 chatbox.chatID_and(".message-box .message-input::placeholder") +" {\n" + 1122 "color: " + chatbox.input.hint_color + ";\n" + 1123 "}" 1124 ); 1125 } 1126 s.sheet.insertRule( 1127 chatbox.chatID_and(".message-box button.message-submit") + " {\n" + 1128 "background: " + chatbox.submit.background + ";\n" + 1129 "color: " + chatbox.submit.color + ";\n" + 1130 "}" 1131 ); 1132 1133 s.sheet.insertRule( 1134 chatbox.chatID_and(".message-box label.message-attach") + " {\n" + 1135 "background: " + chatbox.attach.background + ";\n" + 1136 "color: " + chatbox.attach.color + ";\n" + 1137 (!chatbox.attach.visible ? "display:none" : '') + 1138 "}" 1139 ); 1140 1141 s.sheet.insertRule( 1142 chatbox.chatID_and(".message-box .files .file") +" {\n" + 1143 //"color: " + (chatbox.input.color ? chatbox.input.color : " color-mix(in srgb, " + chatbox.color + ", transparent 10%)" ) + ";\n" + 1144 "border: 1px solid " + (chatbox.input.color ? chatbox.input.color : " color-mix(in srgb, " + chatbox.color + ", transparent 10%)" ) + ";\n" + 1145 "}" 1146 ); 1147 1148 s.sheet.insertRule( 1149 chatbox.chatID_and(".message-box .files-box") +" {\n" + 1150 "color: " + (chatbox.input.color ? chatbox.input.color : " color-mix(in srgb, " + chatbox.color + ", transparent 10%)" ) + ";\n" + 1151 "}" 1152 ); 1153 1154 s.sheet.insertRule( 1155 chatbox.chatID_and(".chat-title") + " {\n" + 1156 "color: " + chatbox.color + ";\n" + 1157 "background: " + chatbox.title.background + ";\n" + 1158 (chatbox.avatar.src ? "" : "padding: 10px;\n" ) + 1159 (chatbox.title.text == '' && chatbox.subTitle.text == '' && chatbox.avatar.src == '' ? "display: none;\n" : "" ) + 1160 "}" 1161 ); 1162 1163 s.sheet.insertRule( 1164 chatbox.chatID_and(".chat-title h2") +" {\n" + 1165 "color: color-mix(in srgb, " + chatbox.color + ", transparent 50%);\n" + // widget text color 50% 1166 "}" 1167 ); 1168 1169 s.sheet.insertRule( 1170 chatbox.chatID_and(".messages .message .under") +" {\n" + 1171 "color: " + chatbox.timeTextColor + ";\n" + 1172 "}" 1173 ); 1174 1175 s.sheet.insertRule( 1176 chatbox.chatID_and(".simplebar-scrollbar:before") +" {\n" + 1177 "background: " + chatbox.scrollbarColor + ";\n" + 1178 "}" 1179 ); 1180 1181 s.sheet.insertRule( 1182 chatbox.chatID_and(".chat-title .avatar") + " {\n" + 1183 "border-radius: " + chatbox.avatar.border_radius + ";\n" + 1184 "border: " + chatbox.avatar.border + ";\n" + 1185 "}" 1186 ); 1187 1188 1189 if (!chatbox.insertIcon && !chatbox.replaceIcon) { 1190 s.sheet.insertRule( 1191 chatbox.chatID_and('.msg td.hljs-lang div i[data-type="copy"]') + " {\n" + 1192 "position: static;\n" + 1193 "}" 1194 ); 1195 } 1196 if (!chatbox.insertIcon) { 1197 s.sheet.insertRule( 1198 chatbox.chatID_and('.msg td.hljs-lang div i[data-type="insert"]') + " {\n" + 1199 "display: none;\n" + 1200 "}" 1201 ); 1202 } 1203 if (!chatbox.replaceIcon) { 1204 s.sheet.insertRule( 1205 chatbox.chatID_and('.msg td.hljs-lang div i[data-type="replace"]') + " {\n" + 1206 "display: none;\n" + 1207 "}" 1208 ); 1209 } 1210 1211 } 1212 1213 setTitle ( txt ) { 1214 let chatbox = this; 1215 chatbox.title.text = txt; 1216 let n = document.querySelector (chatbox.chatID_and(".chat-title h1")); 1217 n.innerHTML = txt; 1218 } 1219 1220 setSubTitle ( txt ) { 1221 let chatbox = this; 1222 chatbox.subTitle.text = txt; 1223 let n = document.querySelector (chatbox.chatID_and(".chat-title h2")); 1224 n.innerHTML = txt; 1225 } 1226 1227 setAvatar ( src ) { 1228 let chatbox = this; 1229 chatbox.avatar.src = src; 1230 chatbox.avatar.ref = asHTMLImgSrc ( src ); 1231 let img = document.querySelector (chatbox.chatID_and(".chat-title img")); 1232 img.src = chatbox.avatar.ref; 1233 chatbox.setupCSS(); 1234 } 1235 1236 setInputHint ( txt ) { 1237 let chatbox = this; 1238 chatbox.input.hint = txt; 1239 let n = document.querySelector (chatbox.chatID_and(".message-box textarea")); 1240 n.setAttribute('placeholder', txt); 1241 } 1242 1243 setSubmitButtonText ( txt, cancel ) { 1244 let chatbox = this; 1245 //chatbox.submit.text = txt; 1246 chatbox.submit.cancel = !!cancel; 1247 let n = document.querySelector (chatbox.chatID_and(".message-box button.message-submit span")); 1248 n.innerHTML = txt; 1249 n = document.querySelector (chatbox.chatID_and(".message-box button.message-submit i")); 1250 n.className = !cancel ? chatbox.submit.icon : chatbox.submit.cancelIcon; 1251 } 1252 1253 setAttachButtonText ( txt ) { 1254 let chatbox = this; 1255 chatbox.attach.text = txt; 1256 let n = document.querySelector (chatbox.chatID_and(".message-box label.message-attach span")); 1257 n.innerHTML = txt; 1258 } 1259
1260 inputResize(chatbox, firstCall) { 1261 let txtInput = chatbox.txtInput; 1262 1263 txtInput.style.height = "auto"; 1264 txtInput.style.height = txtInput.scrollHeight + "px"; 1265 if (!firstCall) txtInput.style.maxHeight = "calc(100% - " + chatbox.divFiles.offsetHeight + "px)"; 1266 } 1267 1268 sendHostMessage() { 1269 let chatbox = this; 1270 chatbox.autoScroll = true; 1271 1272 let msg = chatbox.txtInput.value.trim(); //.replace("\n","<br>"); 1273 if ( msg || chatbox.lastSelectedFiles.length ) chatbox.addMessage( msg, {role: chatbox.currentHost, fromLocalUser: true } ); 1274 chatbox.lastSelectedFiles = [...chatbox.selectedFiles]; // copy 1275 chatbox.selectedFiles = []; 1276 chatbox.selectedFileNames = []; 1277 1278 document.querySelectorAll(chatbox.chatID_and( 'div.file' )).forEach((e) => e.remove()); 1279 chatbox.updateFilesBox(); 1280 1281 chatbox.txtInput.value = ""; 1282 chatbox.txtInput.focus(); 1283 chatbox.txtInput.style.height = "auto"; 1284 chatbox.txtInput.style.height = chatbox.txtInput.scrollHeight + "px"; 1285 1286 if ( msg || chatbox.lastSelectedFiles.length || chatbox.submit.cancel /*.text == 'CANCEL'*/ ) { 1287 let wasCancelling = chatbox.submit.cancel; 1288 chatbox.setSubmitButtonText( chatbox.submit.text ); 1289 if ( chatbox.onInput ) { 1290 application.aoz.runProcedure( chatbox.onInput, 1291 {INDEX$ : chatbox.id , TEXT$ : msg , CANCEL: wasCancelling }, 1292 ); 1293 1294 } 1295 if ( chatbox.waitInput ) { 1296 chatbox.waitInput = false; 1297 chatbox.inputResult = msg; 1298 chatbox.cmd_done = true; 1299 1300 if ( chatbox.thenDisableInput ) { 1301 let t = document.querySelector (chatbox.chatID_and('textarea')); 1302 t.setAttribute('disabled',''); 1303 } 1304 1305 if ( chatbox.thenGuestTyping ) { 1306 let delay = chatbox.thenGuestTyping; 1307 if ( typeof delay !== 'number' ) delay = Math.random (1000); 1308 setTimeout( function() { 1309 chatbox.addMessage ( '', {role: chatbox.currentGuest, typing: true} ); 1310 }, delay); 1311 1312 1313 } 1314 1315 } 1316 } 1317 } 1318 1319 lastMessage (role, msg_span) { 1320 let chatbox = this; 1321 1322 let z = document.querySelectorAll( chatbox.chatID_and( '.message' + (role ? '.' + role.domId : '') + (msg_span ? ' span.msg' : '') ) ); 1323 if (z && z.length) return z[z.length-1]; 1324 return undefined; 1325 } 1326 1327 addMessage (msg, opts) { 1328 let role = opts.role; 1329 let typing = opts.typing; 1330 let replace = opts.replace; 1331 let more = opts.more; 1332 let streamed = opts.streamed; 1333 let fromLocalUser = opts.fromLocalUser; 1334 let answerTo = opts.answerTo; 1335 1336 if (msg.streamer instanceof ReadableStream) { 1337 this.msg_obj = msg; 1338 1339console.log(msg.msg_id); 1340 //if (msg.subscriber) throw "UI_chatbox_stream_locked"; 1341 //readable stream is now reserved for this chatbox ... 1342 // TODO: determine if this is needed. What happens if a second subscriber comes ? 1343 msg.subscriber = this; 1344 let streamReader = msg.reader; //msg.getReader(); 1345 1346 let msg_id_step = 0; 1347 1348 this.setSubmitButtonText ( this.submit.cancelText, true ); 1349 1350 function readStream(chatbox) { 1351 let charsReceived = 0; 1352 let msgSoFar = ""; 1353 let techwork_width = 50; 1354 let msg_id; 1355 1356 chatbox.rndColor1 = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, "0"); 1357 chatbox.rndColor2 = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, "0"); 1358 1359 // read() returns a promise that resolves 1360 // when a value has been received 1361 streamReader.read().then(function processText({ done, value }) { 1362 // Result objects contain two properties: 1363 // done - true if the stream has already given you all its data. 1364 // value - some data. Always undefined when done is true. 1365 let tq = document.querySelector("#thrdqueue"); 1366 if (tq) { 1367 // the thread has started, we don't need the icon anymore but removing it from the DOM is slow 1368 // and we'll receive new streaming events before the element is actually removed from the DOM 1369 // therefore we hide the icon in the meantime 1370 tq.classList.add("aoz-ui-hidden"); 1371 tq.remove(); 1372 } 1373 1374 if (done) { 1375console.log(">>> Stream complete - msg id = [" + msg_id + "]"); 1376//console.log(msg); 1377//console.log(msgSoFar); 1378 streamReader.releaseLock();
1379//console.log(">>> Stream Lock released !"); 1380 let el = document.querySelectorAll("i.loading"); 1381//console.log(">>> Found : " + el.length + " loading items"); 1382 for (let i =0; i<el.length; i++) el[i].remove(); 1383 let end_icon = ' <i data-feather class="bi bi-feather"></i>'; 1384//console.log(">>> Selecting bubble [" + msg_id + "]"); 1385 el = document.querySelector("#" + msg_id); 1386 if (el) { 1387console.log("Found bubble",el); 1388 let s = el.lastElementChild; 1389//console.log("lastElementChild",s); 1390 if (s) { 1391 s.insertAdjacentHTML('beforeend', end_icon); 1392 } else { 1393 el.insertAdjacentHTML('beforeend', end_icon); 1394 } 1395 } else { 1396console.log("msg id " + msg_id + " not found !!???"); 1397 } 1398 1399 chatbox.setSubmitButtonText ( chatbox.submit.text ); 1400 1401 return; 1402 } 1403 1404 charsReceived += value.length; 1405 let chunk = value; 1406//console.log( '>>> Read/received ' + charsReceived + ' characters so far. Current chunk = ' + chunk); 1407 1408 let tw = document.querySelector("#techwork"); 1409 let wasFirst = tw?.getAttribute("data-pos"); 1410 let prevMsgSoFar = msgSoFar; 1411 1412 msgSoFar += chunk; 1413 1414 let delimiter = "¬AI_TECHWORK¬"; 1415 1416 if (msgSoFar.indexOf(delimiter)>=0) { 1417 techwork_width++; 1418 if (tw) { 1419 if (techwork_width>100) techwork_width = 0; 1420 tw.style.width = techwork_width + "%"; 1421 if (techwork_width>10) tw.style.transition = ""; 1422 msgSoFar = msgSoFar.replaceAll(delimiter, ""); 1423 } else { 1424 // addMessage will create tw 1425 } 1426 } else { 1427 if (tw) tw.remove(); 1428 techwork_width = 50; 1429 tw = undefined; 1430 } 1431 1432 let replace = !!prevMsgSoFar || tw || wasFirst || tq; // replace if msgSoFar was != "" or if tw exists or if tq just now 1433 //if ((msgSoFar && msgSoFar != delimiter) || techwork_width>2) { 1434 msg_id = chatbox.addMessage(msgSoFar, {role: role, 1435 replace: replace, 1436 more: false, 1437 streamed: true, 1438 answerTo: answerTo } ); 1439 if (!msg_id_step) { 1440 console.log("GOT MSG ID: " + msg_id); 1441 if (msg_id) msg_id_step = 1; 1442 } else if (msg_id_step==1 && msg.msg_id) { 1443 // AI sent a msg_id 1444 let el = document.querySelector("span#" + msg_id); 1445 el.id = msg.msg_id; // set the proper ID 1446 msg_id = el.id; 1447 msg_id_step = 2; // done ! 1448 } 1449 //} 1450 1451 // Read some more, and call this function again 1452 return streamReader.read().then(processText).catch((err)=> { 1453 // we don't really care ... 1454 //console.log(err); 1455 }); 1456 }).catch((err)=>{ 1457 // we don't really care ... 1458 //console.log(err); 1459 }); 1460 } 1461 1462 readStream(this); 1463 1464 return; 1465 } else if (msg instanceof AArray) { 1466 this.msg_obj = msg.toJSObject(); // convert AArray to regular JS Object 1467 msg = this.msg_obj.msg; 1468 } 1469 1470 1471 let chatbox = this; 1472 1473 1474 let container = document.querySelector(chatbox.chatID_and('.messages-scroller-container')); 1475 let scrollArea = document.querySelector(chatbox.chatID_and('.simplebar-content-wrapper')); 1476 chatbox.updateScrollbar = (pos, instant, force) => { 1477 if (chatbox.autoScroll || force) { 1478 chatbox.autoScroll = true; 1479 chatbox.scrollByCode = true; 1480 scrollArea.scrollTo({top: (typeof pos !== 'undefined' ? pos : scrollArea.scrollHeight), behavior: (instant ? instant: "smooth") }); 1481 } 1482 } 1483 1484 1485 1486 1487 let setDate = function(always = false, role) { 1488 let d = new Date(); 1489 1490 let div = document.createElement('div'); 1491 div.className = "under"; 1492 1493 if ( chatbox.prevMinutes != d.getMinutes() || always ) { 1494 chatbox.prevMinutes = d.getMinutes(); 1495 1496 let divH = document.createElement('div'); 1497 divH.className = "timestamp"; 1498 divH.innerHTML = d.getHours() + ':' + ( chatbox.prevMinutes < 10 ? '0' : '' ) + chatbox.prevMinutes; 1499 div.appendChild(divH) 1500 } 1501 1502 let divCC = document.createElement('div'); 1503 divCC.className = "cc"; 1504 divCC.style.width="1em"; 1505 let i = document.createElement('i'); 1506 i.className = "bi-copy"; 1507 i.title = "Copy to clipboard"; 1508 divCC.appendChild(i); 1509 1510 divCC.addEventListener("click", copy_bubble); 1511 1512 div.appendChild(divCC); 1513
1514 if (role.steroid) { 1515 let icon_click; 1516 if (role.onIconClick) { 1517 icon_click = function() { 1518 let msg = this.parentElement.parentElement.querySelectorAll(".msg"); 1519 if (msg) msg = msg[0]; 1520 else msg = {innerHTML: "", innerText: ""}; 1521 1522 let obj = {INDEX$ : chatbox.id , PARTICIPANT$: role.id, TEXT$ : msg.innerText, HTML$: msg.innerHTML, Type$: this.dataset.type } 1523 let proxy = Utilities.getProxy(obj); 1524 1525 application.aoz.runProcedure( role.onIconClick, proxy ); 1526 } 1527 } 1528 1529 let divCC = document.createElement('div'); 1530 divCC.className = "cc"; 1531 divCC.setAttribute("data-type", "msword"); 1532 let i = document.createElement('i'); 1533 i.className = "bi-file-earmark-richtext"; 1534 i.title = "Create MS Word file"; 1535 divCC.appendChild(i); 1536 if (icon_click) divCC.addEventListener("click", icon_click); 1537 div.appendChild(divCC); 1538 1539 divCC = document.createElement('div'); 1540 divCC.className = "cc"; 1541 divCC.setAttribute("data-type", "sendmail"); 1542 i = document.createElement('i'); 1543 i.className = "bi-envelope-arrow-up"; 1544 i.title = "Send by mail"; 1545 divCC.appendChild(i); 1546 if (icon_click) divCC.addEventListener("click", icon_click); 1547 div.appendChild(divCC); 1548 1549 divCC = document.createElement('div'); 1550 divCC.className = "cc"; 1551 divCC.setAttribute("data-type", "copynotes"); 1552 i = document.createElement('i'); 1553 i.className = "bi-list-task"; 1554 i.title = "Copy to notes"; 1555 divCC.appendChild(i); 1556 if (icon_click) divCC.addEventListener("click", icon_click); 1557 div.appendChild(divCC); 1558 } 1559 1560 1561 chatbox.lastMessage(role).appendChild(div); 1562 1563 } 1564 1565 1566 let copy_bubble = function() { 1567 let el = this.querySelector("i"); 1568 1569 el.className = "bi-check"; 1570 1571 let copy = ''; 1572 let msg = this.parentElement.parentElement.querySelectorAll(".msg"); 1573 if (msg) { 1574 msg = msg[0];
1575 pasteRich(msg.innerHTML, msg.innerText) //.forEach((e)=> copy += (copy ? '\n' : '') + e.innerText); 1576 //navigator.clipboard.writeText(copy); 1577 } 1578 1579 setTimeout(function() { 1580 el.className = "bi-copy"; 1581 }, 2000); 1582 } 1583 1584 //https://stackoverflow.com/a/77305170/991782 1585 let pasteRich = function(rich, plain) { 1586 if (typeof ClipboardItem !== "undefined") { 1587 // Shiny new Clipboard API, not fully supported in Firefox. 1588 // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#browser_compatibility 1589 const html = new Blob([rich], { type: "text/html" }); 1590 const text = new Blob([plain], { type: "text/plain" }); 1591 const data = new ClipboardItem({ "text/html": html, "text/plain": text }); 1592 navigator.clipboard.write([data]); 1593 } else { 1594 // Fallback using the deprecated `document.execCommand`. 1595 // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#browser_compatibility 1596 const cb = e => { 1597 e.clipboardData.setData("text/html", rich); 1598 e.clipboardData.setData("text/plain", plain); 1599 e.preventDefault(); 1600 }; 1601 document.addEventListener("copy", cb); 1602 document.execCommand("copy"); 1603 document.removeEventListener("copy", cb); 1604 } 1605 } 1606 1607 1608 role = ChatManager.getParticipant(role); 1609 if (!role) throw "UI_role_not_found"; 1610 1611 document.querySelector(chatbox.chatID_and('.message.' + role.domId + '.loading'))?.remove(); 1612 1613 // TODO: test typing = false to remove msg even when msg is empty 1614 // TODO: allow to send empty msg if files are attached (not allowed for now because AI needs a msg) 1615 1616 let tw = document.querySelector("#techwork"); 1617 let tw_first_time = false; 1618 if (tw && msg.trim() == '') { 1619 let el = tw.previousElementSibling; 1620 let c = document.querySelector(chatbox.chatID_and()); 1621 if (el && el.tagName == 'P') el.style.minWidth = c.clientWidth / 4 + 'px'; 1622 } 1623 1624//console.log("TRQ: " + chatbox.msg_obj?.threadRunQueued + " - " + typing); 1625 if ( msg.trim() == '' && !typing && !chatbox.msg_obj?.threadRunQueued && chatbox.selectedFiles.length == 0 && chatbox.submit.text != "CANCEL" ) { 1626 console.log ("Empty message not displayed/sent ..."); 1627 return false; 1628 } 1629 1630 let citations = []; 1631 if ( !typing ) { 1632 /* 1633 const regex =///\b(?<!@)(?:(?:https?|ftp):\/\/)?(?:[\w\-]+(?:\.[\w\-]+)+(?:[a-zA-Z_\-]+)|localhost(?::\d{1,5}|\/)|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d{1,5})?(?:\/[\w\-\~@]*)*\??[\w\-\&\=\%\+]*(?:#[\w\-]*)?/gmi; 1634 /\b(?<!@)(?:(?:https?|ftp):\/\/)?(?:[\w\-]+(?:\.[\w\-]+)+(?:[a-zA-Z_\-]+)|localhost(?::\d{1,5}|\/)|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d{1,5})?(?:\/[\w\-\~@]*)*\??[\w\-\&\=\%\+]*(?:#[\w\-]*)?/gmi; 1635 1636 1637 // more complete would enable to test if domain has a valid tld, otherwise it's not a domain. \b(?<!@)(?<protocol>(?:https?|ftp):\/\/)?(?<domain>[\w\-]+(?:\.[\w\-]+)+|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::(?<port>\d{1,5}))?((?<path>(?:[\/\w\-\~@\u00a1-\uffff]*\/)*)(?<file>[\.\w\-\~\u00a1-\uffff]*\.[\w\-\~\u00a1-\uffff]*)|(?<path_alone>(?:[\/\w\-\~@\u00a1-\uffff]*)*))(?<param>\?[\w\-\&\=\%\+\@\u00a1-\uffff]*)?(?<anchor>(?:#[\w\-\u00a1-\uffff]*)?)? 1638 1639 1640 let m; 1641 let _msg = msg; 1642 let matches = []; 1643 while ((m = regex.exec(_msg)) !== null) { 1644 // This is necessary to avoid infinite loops with zero-width matches 1645 if (m.index === regex.lastIndex) { 1646 regex.lastIndex++; 1647 } 1648 1649 m.forEach ((match) => { 1650 matches.push(match); 1651 let subst = "²²" + (matches.length-1) + "²²"; 1652 1653 msg = msg.replace (match, '<a href="' + 1654 (match.toLowerCase().startsWith("http") || match.toLowerCase().startsWith("ftp") ? "" : "http://") + subst + 1655 '" target="_blank" style="color:inherit; font-weight:bold; overflow-wrap:anywhere;">' + subst + '</a>'); 1656 }); 1657 } 1658 1659 for (let i=0; i<matches.length; i++) { 1660 msg = msg.replaceAll ("²²" + i + "²²", matches[i]); 1661 } 1662 1663 1664 msg = msg.replaceAll("\n","<br/>"); 1665 */ 1666 1667 //msg = marked.parse(msg); 1668/*if (!streamed) { 1669 console.log("Adding msg for [" + role.id + "] :"); 1670 console.log(msg); 1671} else { 1672 console.log("Streaming msg from [" + role.id + "] :"); 1673}*/ 1674 msg = ChatManager.md.render(msg); 1675 chatbox.reception_step0_4 = chatbox.reception_step0_4==4 ? 0 : chatbox.reception_step0_4 + 1; 1676 1677 // ANNOTATIONS 1678 let delimiter = "¬AI_SOURCE"; 1679 let new_msg, i; 1680 let p = msg.indexOf(delimiter); 1681 1682 while (p>=0) { 1683 let title = ""; 1684//console.log("Found source in: " + msg); 1685 new_msg = msg.substring(0,p) + ' <i class="bi bi-chat-square-quote" title="'; 1686 i = p + delimiter.length ; 1687 while (i<msg.length && msg.charAt(i)!='¬') { 1688 title += msg.charAt(i); 1689 i++; 1690 } 1691//console.log(title); 1692 1693 let pb0 = title.indexOf("[");
1694 let citation_num = -1; 1695 if (pb0==0) { 1696 let pb1 = title.indexOf("]"); 1697 citation_num = title.substring(pb0+1, pb1); 1698 } 1699 1700 if (citation_num>=0 && msg.charAt(i)=='¬') { 1701 // a citation_num and citation is complete 1702 let c = chatbox.msg_obj?.citations?.[citation_num]; 1703 1704 if (typeof c !== 'undefined') { 1705 citations.push(c); 1706 if (c.quote) new_msg += '«' + c.quote + '»\n\n'; 1707 new_msg += '[ ' + c.file.file_name + ' ]"'; 1708 1709 new_msg += ' data-citation=' + citation_num; 1710 } else { 1711 new_msg += title + '"'; 1712 } 1713 } else { 1714 // either no citation_num or citation not complete yet 1715 new_msg += title + '"'; 1716 } 1717 1718 new_msg += '></i>' + msg.substring(i+1); 1719 msg = new_msg; 1720 p = msg.indexOf(delimiter, p); 1721 } 1722 1723 // IMAGE GENERATED BY CODE_INTERPRETER 1724 delimiter = "¬AI_IMAGE"; 1725 p = msg.indexOf(delimiter); 1726 1727 let fid = '', sndbox = ''; 1728 while (p>=0) { 1729 new_msg = msg.substring(0,p) + '</p><p><i class="bi bi-reception-' + chatbox.reception_step0_4 + ' loading" style="font-size: xx-large;"></i><a id="link_fid"><img id="img_fid"'; 1730 i = p + delimiter.length; 1731 fid = '' 1732 while (i<msg.length && msg.charAt(i)!='¬') { 1733 let c = msg.charAt(i); 1734 fid += c; 1735 i++; 1736 } 1737 1738 new_msg = new_msg.replaceAll("link_fid", fid).replaceAll("img_fid", "img_" + fid); 1739 1740 new_msg += ' style="clear:both; max-height:40vh; max-width:100%; margin:10px 0 10px 0;"></a></p><p>' + msg.substring(i+1); 1741 msg = new_msg; 1742 p = msg.indexOf(delimiter); 1743 } 1744 1745 // DOWNLOADABLE FILE 1746 delimiter = "¬AI_LINK"; 1747 p = msg.indexOf(delimiter); 1748 1749 while (p>=0) { 1750 new_msg = msg.substring(0,p) + ' <a id="link_fid">'; 1751 i = p + delimiter.length; 1752 fid = ''; 1753 sndbox = null; 1754 while (i<msg.length && msg.charAt(i)!='¬') { 1755 let c = msg.charAt(i); 1756 if (c == '*') sndbox = ''; 1757 if (sndbox == null) fid += c; 1758 else if (c != '*') sndbox += c; 1759 i++; 1760 } 1761 1762 new_msg = new_msg.replaceAll("link_fid", fid).replaceAll("img_fid", "img_" + fid).replaceAll(sndbox, fid + '" id="md_link_' + fid); 1763 1764 new_msg += '</a> ' + msg.substring(i+1); 1765 msg = new_msg; 1766 p = msg.indexOf(delimiter); 1767 } 1768 1769 delimiter = "¬AI_TECHWORK¬"; 1770 if (msg.indexOf(delimiter)>=0) { 1771 msg = msg.replaceAll(delimiter, ""); 1772 let datapos = ""; 1773 if (msg.startsWith('<p></p>') && msg.length<10) { 1774 msg = '<p style="width: 200px"></p>'; 1775 datapos = "first"; 1776 } 1777 msg += '<p id="techwork" data-pos="' + datapos + '" style="width:20px; transition:width 5s; background: linear-gradient(90deg,' + chatbox.rndColor1 + ',' + chatbox.rndColor2 + ');"> </p>'; 1778 tw_first_time = true; 1779 } else { 1780 if (tw) msg += tw.outerHTML; 1781 } 1782 1783 if (chatbox.msg_obj?.threadRunQueued) { 1784 msg += '<p id="thrdqueue"><i class="bi bi-arrow-repeat bi-aoz-spin"></i></p>'; 1785 } 1786 1787 } 1788 else msg = '<span></span>'; 1789 1790 1791 1792 let m, msg_id; 1793 1794 if ( replace || more ) m = chatbox.lastMessage ( role , true ); 1795 1796 if ( !m ) { 1797 // TODO: present attached files in a better way 1798 if (chatbox.selectedFileNames.length /*&& !msg*/ && fromLocalUser) { 1799 msg = msg + '<br/><span class="attached_files">' + chatbox.selectedFileNames.join(", ") + "</span>"; 1800 } 1801 1802 msg_id = chatbox.msg_obj?.msg_id || Utilities.formatDOMId(undefined, 10)[1]; // random id that respects dom id starting character 1803 1804 let html = '<div class="message ' + role.domId + ( typing ? ' loading' : '') + ' new">'; 1805 if ( role.avatar.src ) { 1806 html += '<figure class="avatar">'; 1807 html += '<img src="' + asHTMLImgSrc( role.avatar.src ) + '" /></figure>' 1808 } 1809 html += '<span id="' + msg_id + '" class="msg">' + msg + '</span></div>'; 1810 1811 container.insertAdjacentHTML('beforeend', html); 1812 1813 // TODO: add an option for "always" 1814 if (!typing) setDate(true, role, role.steroid, role.onIconClick); 1815 if (tw_first_time) { 1816 let tw = document.querySelector("#techwork"); 1817 setTimeout(function() { tw.style.width = '100px'; }, 200); // this starts a 5s anim from 10% to 50% (100px) 1818 } 1819 1820 } else { 1821 // more or replace ... more has priority 1822 if ( more ) m.innerHTML += msg; 1823 else m.innerHTML = msg; 1824 msg_id = m.id 1825 } 1826 1827 chatbox.updateScrollbar(); 1828 1829 if (citations.length && chatbox.onCitationClick) { 1830 document.querySelectorAll("span#" + msg_id + " i[data-citation]").forEach((item) => {
1831 let citation_num = item.getAttribute("data-citation"); 1832 let c = citations[citation_num]; 1833 item.addEventListener("click", function(el) { 1834 let obj = { 1835 INDEX$ : chatbox.id , MSG_ID: msg_id, CITATION_NUM: citation_num, 1836 FILENAME$: c.file.file_name, 1837 FILE_ID$: c.file.file_id, 1838 PARAPHRASE$: c.paraphrase.text, 1839 CITATION$_array: AArray.fromJavascriptObject(c), 1840 AnswerTo$: answerTo 1841 } 1842 let proxy = Utilities.getProxy(obj); 1843 1844 application.aoz.runProcedure( chatbox.onCitationClick, proxy ); 1845 }); 1846 }); 1847 } 1848 1849 document.querySelectorAll(chatbox.chatID_and( '.message .hljs-lang div')).forEach((item) => { 1850 for (const child of item.children) { 1851 if (child.dataset.type != 'lang') child.addEventListener('click', ChatManager.md.copy_code ); 1852 child.chatbox = chatbox; 1853 //else if (child.dataset.type == 'insert') child.addEventListener('click', ChatManager.md.insert_code ); 1854 //else if (child.dataset.type == 'replace') child.addEventListener('click', ChatManager.md.replace_code ); 1855 }; 1856 }); 1857 1858 return msg_id; 1859 } 1860 1861 focus () { 1862 let chatbox = this; 1863 chatbox.txtInput.focus(); 1864 } 1865 1866 1867 clearMessages () { 1868 let chatbox = this; 1869 let m = document.querySelectorAll(chatbox.chatID_and ('.messages .message')); 1870 1871 for ( let i=0 ; i< m.length; i++ ) m[i].remove(); 1872 } 1873 } 1874 1875 UI2Manager._classes.Chatbox = Chatbox; 1876 1877 // static functions that should be called instead of the underlying functions 1878 ChatManager.summonChatbox = function(id) { 1879 let err; 1880 [err, id] = formatId( id ); 1881 if (err) throw "UI_" + err; 1882 1883 return _chatboxes[id] || (_chatboxes[id] = new Chatbox( id, this )); 1884 }; 1885 1886 ChatManager.getChatbox = function ( id, returnErr = false ) { 1887 let err; 1888 [err, id] = formatId( id ); 1889 if ( returnErr ) { 1890 if ( !err ) return [ undefined, _chatboxes[id] ]; 1891 return [ err, undefined ] 1892 } else { 1893 if ( !err ) return _chatboxes[id]; 1894 return undefined; 1895 } 1896 }; 1897 1898 ChatManager.getChatboxAuto = function ( id ) { 1899 let [err, chatbox] = this.getChatbox( id, true ) 1900 if ( err ) throw "UI_" + err; 1901 1902 if ( !chatbox ) throw "UI_chatbox_unknown_id"; 1903 1904 return chatbox; 1905 } 1906 1907 1908 ChatManager.cmdChatbox = function ( args ) { 1909 if( typeof args !== "object" || Array.isArray(args) ) throw 'UI_chatbox_arguments_missing'; 1910 1911 let chatbox = this.summonChatbox( args.id ); 1912 1913 chatbox.updateChatbox( args ); 1914 }; 1915 1916 1917 1918 ChatManager.chatInput = function ( args ) { 1919 let chatbox = this.getChatboxAuto( args.id ); 1920 1921 let t = document.querySelector (chatbox.chatID_and ('textarea')); 1922 if ( !t ) throw "UI_chatbox_unknown_id"; 1923 1924 chatbox.cmd_done = false; 1925 chatbox.cmd_error = false; 1926 1927 t.removeAttribute('disabled'); 1928 1929 chatbox.inputResult = undefined; 1930 chatbox.thenDisableInput = args.thenDisableInput; 1931 chatbox.thenGuestTyping = args.thenGuestTyping; 1932 chatbox.focus(); 1933 chatbox.waitInput = true; 1934 } 1935 1936 ChatManager.chatFiles = function ( args ) { 1937 let chatbox = this.getChatboxAuto( args.id ); 1938 1939 return createAArray ( chatbox.lastSelectedFiles, "string", KBFile.family, true ) ; 1940 } 1941 1942 1943 ChatManager.cmd_wait = function(section) { 1944 // wait fct ... only for UI Chat 1945 let chatbox = this.getChatbox( section.waitArgs.id ); 1946 let cmd_done = chatbox.cmd_done; 1947 let cmd_error = chatbox.cmd_error 1948 1949 if ( cmd_error ) { 1950 cmd_done = true; 1951 throw cmd_error; 1952 } 1953 1954 return cmd_done; 1955 } 1956 1957 return ChatManager; 1958})() 1959 1960
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.