PageSourceSearch

https://mysteroid.ai/run/ui_chat/ui_chat.js

js mysteroid.ai collected 2026-10-05 13:35:41 UTC 68,779 bytes, 1,960 lines download raw bytes

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 + ');">&nbsp;</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.