1// @ts-check
2// A simple completions and chat/completions test related web front end logic
3// by Humans for All
4
5import * as du from "./datautils.mjs";
6import * as ui from "./ui.mjs"
7
8class Roles {
9 static System = "system";
10 static User = "user";
11 static Assistant = "assistant";
12}
13
14class ApiEP {
15 static Type = {
16 Chat: "chat",
17 Completion: "completion",
18 }
19 static UrlSuffix = {
20 'chat': `/chat/completions`,
21 'completion': `/completions`,
22 }
23
24 /**
25 * Build the url from given baseUrl and apiEp id.
26 * @param {string} baseUrl
27 * @param {string} apiEP
28 */
29 static Url(baseUrl, apiEP) {
30 if (baseUrl.endsWith("/")) {
31 baseUrl = baseUrl.substring(0, baseUrl.length-1);
32 }
33 return `${baseUrl}${this.UrlSuffix[apiEP]}`;
34 }
35
36}
37
38
39let gUsageMsg = `
40 <p class="role-system">Usage</p>
41 <ul class="ul1">
42 <li> System prompt above, to try control ai response characteristics.</li>
43 <ul class="ul2">
44 <li> Completion mode - no system prompt normally.</li>
45 </ul>
46 <li> Use shift+enter for inserting enter/newline.</li>
47 <li> Enter your query to ai assistant below.</li>
48 <li> Default ContextWindow = [System, Last Query+Resp, Cur Query].</li>
49 <ul class="ul2">
50 <li> ChatHistInCtxt, MaxTokens, ModelCtxt window to expand</li>
51 </ul>
52 </ul>
53`;
54
55
56/** @typedef {{role: string, content: string}[]} ChatMessages */
57
58/** @typedef {{iLastSys: number, xchat: ChatMessages}} SimpleChatODS */
59
60class SimpleChat {
61
62 /**
63 * @param {string} chatId
64 */
65 constructor(chatId) {
66 this.chatId = chatId;
67 /**
68 * Maintain in a form suitable for common LLM web service chat/completions' messages entry
69 * @type {ChatMessages}
70 */
71 this.xchat = [];
72 this.iLastSys = -1;
73 this.latestResponse = "";
74 }
75
76 clear() {
77 this.xchat = [];
78 this.iLastSys = -1;
79 }
80
81 ods_key() {
82 return `SimpleChat-${this.chatId}`
83 }
84
85 save() {
86 /** @type {SimpleChatODS} */
87 let ods = {iLastSys: this.iLastSys, xchat: this.xchat};
88 localStorage.setItem(this.ods_key(), JSON.stringify(ods));
89 }
90
91 load() {
92 let sods = localStorage.getItem(this.ods_key());
93 if (sods == null) {
94 return;
95 }
96 /** @type {SimpleChatODS} */
97 let ods = JSON.parse(sods);
98 this.iLastSys = ods.iLastSys;
99 this.xchat = ods.xchat;
100 }
101
102 /**
103 * Recent chat messages.
104 * If iRecentUserMsgCnt < 0
105 * Then return the full chat history
106 * Else
107 * Return chat messages from latest going back till the last/latest system prompt.
108 * While keeping track that the number of user queries/messages doesnt exceed iRecentUserMsgCnt.
109 * @param {number} iRecentUserMsgCnt
110 */
111 recent_chat(iRecentUserMsgCnt) {
112 if (iRecentUserMsgCnt < 0) {
113 return this.xchat;
114 }
115 if (iRecentUserMsgCnt == 0) {
116 console.warn("WARN:SimpleChat:SC:RecentChat:iRecentUsermsgCnt of 0 means no user message/query sent");
117 }
118 /** @type{ChatMessages} */
119 let rchat = [];
120 let sysMsg = this.get_system_latest();
121 if (sysMsg.length != 0) {
122 rchat.push({role: Roles.System, content: sysMsg});
123 }
124 let iUserCnt = 0;
125 let iStart = this.xchat.length;
126 for(let i=this.xchat.length-1; i > this.iLastSys; i--) {
127 if (iUserCnt >= iRecentUserMsgCnt) {
128 break;
129 }
130 let msg = this.xchat[i];
131 if (msg.role == Roles.User) {
132 iStart = i;
133 iUserCnt += 1;
134 }
135 }
136 for(let i = iStart; i < this.xchat.length; i++) {
137 let msg = this.xchat[i];
138 if (msg.role == Roles.System) {
139 continue;
140 }
141 rchat.push({role: msg.role, content: msg.content});
142 }
143 return rchat;
144 }
145
146 /**
147 * Collate the latest response from the server/ai-model, as it is becoming available.
148 * This is mainly useful for the stream mode.
149 * @param {string} content
150 */
151 append_response(content) {
152 this.latestResponse += content;
153 }
154
155 /**
156 * Add an entry into xchat
157 * @param {string} role
158 * @param {string|undefined|null} content
159 */
160 add(role, content) {
161 if ((content == undefined) || (content == null) || (content == "")) {
162 return false;
163 }
164 this.xchat.push( {role: role, content: content} );
165 if (role == Roles.System) {
166 this.iLastSys = this.xchat.length - 1;
167 }
168 this.save();
169 return true;
170 }
171
172 /**
173 * Show the contents in the specified div
174 * @param {HTMLDivElement} div
175 * @param {boolean} bClear
176 */
177 show(div, bClear=true) {
178 if (bClear) {
179 div.replaceChildren();
180 }
181 let last = undefined;
182 for(const x of this.recent_chat(gMe.iRecentUserMsgCnt)) {
183 let entry = ui.el_create_append_p(`${x.role}: ${x.content}`, div);
184 entry.className = `role-${x.role}`;
185 last = entry;
186 }
187 if (last !== undefined) {
188 last.scrollIntoView(false);
189 } else {
190 if (bClear) {
191 div.innerHTML = gUsageMsg;
192 gMe.setup_load(div, this);
193 gMe.show_info(div);
194 }
195 }
196 return last;
197 }
198
199 /**
200 * Setup the fetch headers.
201 * It picks the headers from gMe.headers.
202 * It inserts Authorization only if its non-empty.
203 * @param {string} apiEP
204 */
205 fetch_headers(apiEP) {
206 let headers = new Headers();
207 for(let k in gMe.headers) {
208 let v = gMe.headers[k];
209 if ((k == "Authorization") && (v.trim() == "")) {
210 continue;
211 }
212 headers.append(k, v);
213 }
214 return headers;
215 }
216
217 /**
218 * Add needed fields wrt json object to be sent wrt LLM web services completions endpoint.
219 * The needed fields/options are picked from a global object.
220 * Add optional stream flag, if required.
221 * Convert the json into string.
222 * @param {Object} obj
223 */
224 request_jsonstr_extend(obj) {
225 for(let k in gMe.apiRequestOptions) {
226 obj[k] = gMe.apiRequestOptions[k];
227 }
228 if (gMe.bStream) {
229 obj["stream"] = true;
230 }
231 return JSON.stringify(obj);
232 }
233
234 /**
235 * Return a string form of json object suitable for chat/completions
236 */
237 request_messages_jsonstr() {
238 let req = {
239 messages: this.recent_chat(gMe.iRecentUserMsgCnt),
240 }
241 return this.request_jsonstr_extend(req);
242 }
243
244 /**
245 * Return a string form of json object suitable for /completions
246 * @param {boolean} bInsertStandardRolePrefix Insert "<THE_ROLE>: " as prefix wrt each role's message
247 */
248 request_prompt_jsonstr(bInsertStandardRolePrefix) {
249 let prompt = "";
250 let iCnt = 0;
251 for(const chat of this.recent_chat(gMe.iRecentUserMsgCnt)) {
252 iCnt += 1;
253 if (iCnt > 1) {
254 prompt += "\n";
255 }
256 if (bInsertStandardRolePrefix) {
257 prompt += `${chat.role}: `;
258 }
259 prompt += `${chat.content}`;
260 }
261 let req = {
262 prompt: prompt,
263 }
264 return this.request_jsonstr_extend(req);
265 }
266
267 /**
268 * Return a string form of json object suitable for specified api endpoint.
269 * @param {string} apiEP
270 */
271 request_jsonstr(apiEP) {
272 if (apiEP == ApiEP.Type.Chat) {
273 return this.request_messages_jsonstr();
274 } else {
275 return this.request_prompt_jsonstr(gMe.bCompletionInsertStandardRolePrefix);
276 }
277 }
278
279 /**
280 * Extract the ai-model/assistant's response from the http response got.
281 * Optionally trim the message wrt any garbage at the end.
282 * @param {any} respBody
283 * @param {string} apiEP
284 */
285 response_extract(respBody, apiEP) {
286 let assistant = "";
287 if (apiEP == ApiEP.Type.Chat) {
288 assistant = respBody["choices"][0]["message"]["content"];
289 } else {
290 try {
291 assistant = respBody["choices"][0]["text"];
292 } catch {
293 assistant = respBody["content"];
294 }
295 }
296 return assistant;
297 }
298
299 /**
300 * Extract the ai-model/assistant's response from the http response got in streaming mode.
301 * @param {any} respBody
302 * @param {string} apiEP
303 */
304 response_extract_stream(respBody, apiEP) {
305 let assistant = "";
306 if (apiEP == ApiEP.Type.Chat) {
307 if (respBody["choices"][0]["finish_reason"] !== "stop") {
308 assistant = respBody["choices"][0]["delta"]["content"];
309 }
310 } else {
311 try {
312 assistant = respBody["choices"][0]["text"];
313 } catch {
314 assistant = respBody["content"];
315 }
316 }
317 return assistant;
318 }
319
320 /**
321 * Allow setting of system prompt, but only at begining.
322 * @param {string} sysPrompt
323 * @param {string} msgTag
324 */
325 add_system_begin(sysPrompt, msgTag) {
326 if (this.xchat.length == 0) {
327 if (sysPrompt.length > 0) {
328 return this.add(Roles.System, sysPrompt);
329 }
330 } else {
331 if (sysPrompt.length > 0) {
332 if (this.xchat[0].role !== Roles.System) {
333 console.error(`ERRR:SimpleChat:SC:${msgTag}:You need to specify system prompt before any user query, ignoring...`);
334 } else {
335 if (this.xchat[0].content !== sysPrompt) {
336 console.error(`ERRR:SimpleChat:SC:${msgTag}:You cant change system prompt, mid way through, ignoring...`);
337 }
338 }
339 }
340 }
341 return false;
342 }
343
344 /**
345 * Allow setting of system prompt, at any time.
346 * @param {string} sysPrompt
347 * @param {string} msgTag
348 */
349 add_system_anytime(sysPrompt, msgTag) {
350 if (sysPrompt.length <= 0) {
351 return false;
352 }
353
354 if (this.iLastSys < 0) {
355 return this.add(Roles.System, sysPrompt);
356 }
357
358 let lastSys = this.xchat[this.iLastSys].content;
359 if (lastSys !== sysPrompt) {
360 return this.add(Roles.System, sysPrompt);
361 }
362 return false;
363 }
364
365 /**
366 * Retrieve the latest system prompt.
367 */
368 get_system_latest() {
369 if (this.iLastSys == -1) {
370 return "";
371 }
372 let sysPrompt = this.xchat[this.iLastSys].content;
373 return sysPrompt;
374 }
375
376
377 /**
378 * Handle the multipart response from server/ai-model
379 * @param {Response} resp
380 * @param {string} apiEP
381 * @param {HTMLDivElement} elDiv
382 */
383 async handle_response_multipart(resp, apiEP, elDiv) {
384 let elP = ui.el_create_append_p("", elDiv);
385 if (!resp.body) {
386 throw Error("ERRR:SimpleChat:SC:HandleResponseMultiPart:No body...");
387 }
388 let tdUtf8 = new TextDecoder("utf-8");
389 let rr = resp.body.getReader();
390 this.latestResponse = "";
391 let xLines = new du.NewLines();
392 while(true) {
393 let { value: cur, done: done } = await rr.read();
394 if (cur) {
395 let curBody = tdUtf8.decode(cur, {stream: true});
396 console.debug("DBUG:SC:PART:Str:", curBody);
397 xLines.add_append(curBody);
398 }
399 while(true) {
400 let curLine = xLines.shift(!done);
401 if (curLine == undefined) {
402 break;
403 }
404 if (curLine.trim() == "") {
405 continue;
406 }
407 if (curLine.startsWith("data:")) {
408 curLine = curLine.substring(5);
409 }
410 if (curLine.trim() === "[DONE]") {
411 break;
412 }
413 let curJson = JSON.parse(curLine);
414 console.debug("DBUG:SC:PART:Json:", curJson);
415 this.append_response(this.response_extract_stream(curJson, apiEP));
416 }
417 elP.innerText = this.latestResponse;
418 elP.scrollIntoView(false);
419 if (done) {
420 break;
421 }
422 }
423 console.debug("DBUG:SC:PART:Full:", this.latestResponse);
424 return this.latestResponse;
425 }
426
427 /**
428 * Handle the oneshot response from server/ai-model
429 * @param {Response} resp
430 * @param {string} apiEP
431 */
432 async handle_response_oneshot(resp, apiEP) {
433 let respBody = await resp.json();
434 console.debug(`DBUG:SimpleChat:SC:${this.chatId}:HandleUserSubmit:RespBody:${JSON.stringify(respBody)}`);
435 return this.response_extract(respBody, apiEP);
436 }
437
438 /**
439 * Handle the response from the server be it in oneshot or multipart/stream mode.
440 * Also take care of the optional garbage trimming.
441 * @param {Response} resp
442 * @param {string} apiEP
443 * @param {HTMLDivElement} elDiv
444 */
445 async handle_response(resp, apiEP, elDiv) {
446 let theResp = {
447 assistant: "",
448 trimmed: "",
449 }
450 if (gMe.bStream) {
451 try {
452 theResp.assistant = await this.handle_response_multipart(resp, apiEP, elDiv);
453 this.latestResponse = "";
454 } catch (error) {
455 theResp.assistant = this.latestResponse;
456 this.add(Roles.Assistant, theResp.assistant);
457 this.latestResponse = "";
458 throw error;
459 }
460 } else {
461 theResp.assistant = await this.handle_response_oneshot(resp, apiEP);
462 }
463 if (gMe.bTrimGarbage) {
464 let origMsg = theResp.assistant;
465 theResp.assistant = du.trim_garbage_at_end(origMsg);
466 theResp.trimmed = origMsg.substring(theResp.assistant.length);
467 }
468 this.add(Roles.Assistant, theResp.assistant);
469 return theResp;
470 }
471
472}
473
474
475class MultiChatUI {
476
477 constructor() {
478 /** @type {Object<string, SimpleChat>} */
479 this.simpleChats = {};
480 /** @type {string} */
481 this.curChatId = "";
482
483 // the ui elements
484 this.elInSystem = /** @type{HTMLInputElement} */(document.getElementById("system-in"));
485 this.elDivChat = /** @type{HTMLDivElement} */(document.getElementById("chat-div"));
486 this.elBtnUser = /** @type{HTMLButtonElement} */(document.getElementById("user-btn"));
487 this.elInUser = /** @type{HTMLInputElement} */(document.getElementById("user-in"));
488 this.elDivHeading = /** @type{HTMLSelectElement} */(document.getElementById("heading"));
489 this.elDivSessions = /** @type{HTMLDivElement} */(document.getElementById("sessions-div"));
490 this.elBtnSettings = /** @type{HTMLButtonElement} */(document.getElementById("settings"));
491
492 this.validate_element(this.elInSystem, "system-in");
493 this.validate_element(this.elDivChat, "chat-div");
494 this.validate_element(this.elInUser, "user-in");
495 this.validate_element(this.elDivHeading, "heading");
496 this.validate_element(this.elDivChat, "sessions-div");
497 this.validate_element(this.elBtnSettings, "settings");
498 }
499
500 /**
501 * Check if the element got
502 * @param {HTMLElement | null} el
503 * @param {string} msgTag
504 */
505 validate_element(el, msgTag) {
506 if (el == null) {
507 throw Error(`ERRR:SimpleChat:MCUI:${msgTag} element missing in html...`);
508 } else {
509 console.debug(`INFO:SimpleChat:MCUI:${msgTag} Id[${el.id}] Name[${el["name"]}]`);
510 }
511 }
512
513 /**
514 * Reset user input ui.
515 * * clear user input
516 * * enable user input
517 * * set focus to user input
518 */
519 ui_reset_userinput() {
520 this.elInUser.value = "";
521 this.elInUser.disabled = false;
522 this.elInUser.focus();
523 }
524
525 /**
526 * Setup the needed callbacks wrt UI, curChatId to defaultChatId and
527 * optionally switch to specified defaultChatId.
528 * @param {string} defaultChatId
529 * @param {boolean} bSwitchSession
530 */
531 setup_ui(defaultChatId, bSwitchSession=false) {
532
533 this.curChatId = defaultChatId;
534 if (bSwitchSession) {
535 this.handle_session_switch(this.curChatId);
536 }
537
538 this.elBtnSettings.addEventListener("click", (ev)=>{
539 this.elDivChat.replaceChildren();
540 gMe.show_settings(this.elDivChat);
541 });
542
543 this.elBtnUser.addEventListener("click", (ev)=>{
544 if (this.elInUser.disabled) {
545 return;
546 }
547 this.handle_user_submit(this.curChatId, gMe.apiEP).catch((/** @type{Error} */reason)=>{
548 let msg = `ERRR:SimpleChat\nMCUI:HandleUserSubmit:${this.curChatId}\n${reason.name}:${reason.message}`;
549 console.error(msg.replace("\n", ":"));
550 alert(msg);
551 this.ui_reset_userinput();
552 });
553 });
554
555 this.elInUser.addEventListener("keyup", (ev)=> {
556 // allow user to insert enter into their message using shift+enter.
557 // while just pressing enter key will lead to submitting.
558 if ((ev.key === "Enter") && (!ev.shiftKey)) {
559 let value = this.elInUser.value;
560 this.elInUser.value = value.substring(0,value.length-1);
561 this.elBtnUser.click();
562 ev.preventDefault();
563 }
564 });
565
566 this.elInSystem.addEventListener("keyup", (ev)=> {
567 // allow user to insert enter into the system prompt using shift+enter.
568 // while just pressing enter key will lead to setting the system prompt.
569 if ((ev.key === "Enter") && (!ev.shiftKey)) {
570 let value = this.elInSystem.value;
571 this.elInSystem.value = value.substring(0,value.length-1);
572 let chat = this.simpleChats[this.curChatId];
573 chat.add_system_anytime(this.elInSystem.value, this.curChatId);
574 chat.show(this.elDivChat);
575 ev.preventDefault();
576 }
577 });
578
579 }
580
581 /**
582 * Setup a new chat session and optionally switch to it.
583 * @param {string} chatId
584 * @param {boolean} bSwitchSession
585 */
586 new_chat_session(chatId, bSwitchSession=false) {
587 this.simpleChats[chatId] = new SimpleChat(chatId);
588 if (bSwitchSession) {
589 this.handle_session_switch(chatId);
590 }
591 }
592
593
594 /**
595 * Handle user query submit request, wrt specified chat session.
596 * @param {string} chatId
597 * @param {string} apiEP
598 */
599 async handle_user_submit(chatId, apiEP) {
600
601 let chat = this.simpleChats[chatId];
602
603 // In completion mode, if configured, clear any previous chat history.
604 // So if user wants to simulate a multi-chat based completion query,
605 // they will have to enter the full thing, as a suitable multiline
606 // user input/query.
607 if ((apiEP == ApiEP.Type.Completion) && (gMe.bCompletionFreshChatAlways)) {
608 chat.clear();
609 }
610
611 chat.add_system_anytime(this.elInSystem.value, chatId);
612
613 let content = this.elInUser.value;
614 if (!chat.add(Roles.User, content)) {
615 console.debug(`WARN:SimpleChat:MCUI:${chatId}:HandleUserSubmit:Ignoring empty user input...`);
616 return;
617 }
618 chat.show(this.elDivChat);
619
620 let theUrl = ApiEP.Url(gMe.baseURL, apiEP);
621 let theBody = chat.request_jsonstr(apiEP);
622
623 this.elInUser.value = "working...";
624 this.elInUser.disabled = true;
625 console.debug(`DBUG:SimpleChat:MCUI:${chatId}:HandleUserSubmit:${theUrl}:ReqBody:${theBody}`);
626 let theHeaders = chat.fetch_headers(apiEP);
627 let resp = await fetch(theUrl, {
628 method: "POST",
629 headers: theHeaders,
630 body: theBody,
631 });
632
633 let theResp = await chat.handle_response(resp, apiEP, this.elDivChat);
634 if (chatId == this.curChatId) {
635 chat.show(this.elDivChat);
636 if (theResp.trimmed.length > 0) {
637 let p = ui.el_create_append_p(`TRIMMED:${theResp.trimmed}`, this.elDivChat);
638 p.className="role-trim";
639 }
640 } else {
641 console.debug(`DBUG:SimpleChat:MCUI:HandleUserSubmit:ChatId has changed:[${chatId}] [${this.curChatId}]`);
642 }
643 this.ui_reset_userinput();
644 }
645
646 /**
647 * Show buttons for NewChat and available chat sessions, in the passed elDiv.
648 * If elDiv is undefined/null, then use this.elDivSessions.
649 * Take care of highlighting the selected chat-session's btn.
650 * @param {HTMLDivElement | undefined} elDiv
651 */
652 show_sessions(elDiv=undefined) {
653 if (!elDiv) {
654 elDiv = this.elDivSessions;
655 }
656 elDiv.replaceChildren();
657 // Btn for creating new chat session
658 let btnNew = ui.el_create_button("New CHAT", (ev)=> {
659 if (this.elInUser.disabled) {
660 console.error(`ERRR:SimpleChat:MCUI:NewChat:Current session [${this.curChatId}] awaiting response, ignoring request...`);
661 alert("ERRR:SimpleChat\nMCUI:NewChat\nWait for response to pending query, before starting new chat session");
662 return;
663 }
664 let chatId = `Chat${Object.keys(this.simpleChats).length}`;
665 let chatIdGot = prompt("INFO:SimpleChat\nMCUI:NewChat\nEnter id for new chat session", chatId);
666 if (!chatIdGot) {
667 console.error("ERRR:SimpleChat:MCUI:NewChat:Skipping based on user request...");
668 return;
669 }
670 this.new_chat_session(chatIdGot, true);
671 this.create_session_btn(elDiv, chatIdGot);
672 ui.el_children_config_class(elDiv, chatIdGot, "session-selected", "");
673 });
674 elDiv.appendChild(btnNew);
675 // Btns for existing chat sessions
676 let chatIds = Object.keys(this.simpleChats);
677 for(let cid of chatIds) {
678 let btn = this.create_session_btn(elDiv, cid);
679 if (cid == this.curChatId) {
680 btn.className = "session-selected";
681 }
682 }
683 }
684
685 create_session_btn(elDiv, cid) {
686 let btn = ui.el_create_button(cid, (ev)=>{
687 let target = /** @type{HTMLButtonElement} */(ev.target);
688 console.debug(`DBUG:SimpleChat:MCUI:SessionClick:${target.id}`);
689 if (this.elInUser.disabled) {
690 console.error(`ERRR:SimpleChat:MCUI:SessionClick:${target.id}:Current session [${this.curChatId}] awaiting response, ignoring switch...`);
691 alert("ERRR:SimpleChat\nMCUI:SessionClick\nWait for response to pending query, before switching");
692 return;
693 }
694 this.handle_session_switch(target.id);
695 ui.el_children_config_class(elDiv, target.id, "session-selected", "");
696 });
697 elDiv.appendChild(btn);
698 return btn;
699 }
700
701 /**
702 * Switch ui to the specified chatId and set curChatId to same.
703 * @param {string} chatId
704 */
705 async handle_session_switch(chatId) {
706 let chat = this.simpleChats[chatId];
707 if (chat == undefined) {
708 console.error(`ERRR:SimpleChat:MCUI:HandleSessionSwitch:${chatId} missing...`);
709 return;
710 }
711 this.elInSystem.value = chat.get_system_latest();
712 this.elInUser.value = "";
713 chat.show(this.elDivChat);
714 this.elInUser.focus();
715 this.curChatId = chatId;
716 console.log(`INFO:SimpleChat:MCUI:HandleSessionSwitch:${chatId} entered...`);
717 }
718
719}
720
721
722class Me {
723
724 constructor() {
725 this.baseURL = "http://127.0.0.1:8080";
726 this.defaultChatIds = [ "Default", "Other" ];
727 this.multiChat = new MultiChatUI();
728 this.bStream = true;
729 this.bCompletionFreshChatAlways = true;
730 this.bCompletionInsertStandardRolePrefix = false;
731 this.bTrimGarbage = true;
732 this.iRecentUserMsgCnt = 2;
733 this.sRecentUserMsgCnt = {
734 "Full": -1,
735 "Last0": 1,
736 "Last1": 2,
737 "Last2": 3,
738 "Last4": 5,
739 };
740 this.apiEP = ApiEP.Type.Chat;
741 this.headers = {
742 "Content-Type": "application/json",
743 "Authorization": "", // Authorization: Bearer OPENAI_API_KEY
744 }
745 // Add needed fields wrt json object to be sent wrt LLM web services completions endpoint.
746 this.apiRequestOptions = {
747 "model": "gpt-3.5-turbo",
748 "temperature": 0.7,
749 "max_tokens": 1024,
750 "n_predict": 1024,
751 "cache_prompt": false,
752 //"frequency_penalty": 1.2,
753 //"presence_penalty": 1.2,
754 };
755 }
756
757 /**
758 * Disable console.debug by mapping it to a empty function.
759 */
760 debug_disable() {
761 this.console_debug = console.debug;
762 console.debug = () => {
763
764 };
765 }
766
767 /**
768 * Setup the load saved chat ui.
769 * @param {HTMLDivElement} div
770 * @param {SimpleChat} chat
771 */
772 setup_load(div, chat) {
773 if (!(chat.ods_key() in localStorage)) {
774 return;
775 }
776 div.innerHTML += `<p class="role-system">Restore</p>
777 <p>Load previously saved chat session, if available</p>`;
778 let btn = ui.el_create_button(chat.ods_key(), (ev)=>{
779 console.log("DBUG:SimpleChat:SC:Load", chat);
780 chat.load();
781 queueMicrotask(()=>{
782 chat.show(div);
783 this.multiChat.elInSystem.value = chat.get_system_latest();
784 });
785 });
786 div.appendChild(btn);
787 }
788
789 /**
790 * Show the configurable parameters info in the passed Div element.
791 * @param {HTMLDivElement} elDiv
792 * @param {boolean} bAll
793 */
794 show_info(elDiv, bAll=false) {
795
796 let p = ui.el_create_append_p("Settings (devel-tools-console document[gMe])", elDiv);
797 p.className = "role-system";
798
799 if (bAll) {
800
801 ui.el_create_append_p(`baseURL:${this.baseURL}`, elDiv);
802
803 ui.el_create_append_p(`Authorization:${this.headers["Authorization"]}`, elDiv);
804
805 ui.el_create_append_p(`bStream:${this.bStream}`, elDiv);
806
807 ui.el_create_append_p(`bTrimGarbage:${this.bTrimGarbage}`, elDiv);
808
809 ui.el_create_append_p(`ApiEndPoint:${this.apiEP}`, elDiv);
810
811 ui.el_create_append_p(`iRecentUserMsgCnt:${this.iRecentUserMsgCnt}`, elDiv);
812
813 ui.el_create_append_p(`bCompletionFreshChatAlways:${this.bCompletionFreshChatAlways}`, elDiv);
814
815 ui.el_create_append_p(`bCompletionInsertStandardRolePrefix:${this.bCompletionInsertStandardRolePrefix}`, elDiv);
816
817 }
818
819 ui.el_create_append_p(`apiRequestOptions:${JSON.stringify(this.apiRequestOptions, null, " - ")}`, elDiv);
820 ui.el_create_append_p(`headers:${JSON.stringify(this.headers, null, " - ")}`, elDiv);
821
822 }
823
824 /**
825 * Auto create ui input elements for fields in apiRequestOptions
826 * Currently supports text and number field types.
827 * @param {HTMLDivElement} elDiv
828 */
829 show_settings_apirequestoptions(elDiv) {
830 let typeDict = {
831 "string": "text",
832 "number": "number",
833 };
834 let fs = document.createElement("fieldset");
835 let legend = document.createElement("legend");
836 legend.innerText = "ApiRequestOptions";
837 fs.appendChild(legend);
838 elDiv.appendChild(fs);
839 for(const k in this.apiRequestOptions) {
840 let val = this.apiRequestOptions[k];
841 let type = typeof(val);
842 if (((type == "string") || (type == "number"))) {
843 let inp = ui.el_creatediv_input(`Set${k}`, k, typeDict[type], this.apiRequestOptions[k], (val)=>{
844 if (type == "number") {
845 val = Number(val);
846 }
847 this.apiRequestOptions[k] = val;
848 });
849 fs.appendChild(inp.div);
850 } else if (type == "boolean") {
851 let bbtn = ui.el_creatediv_boolbutton(`Set{k}`, k, {true: "true", false: "false"}, val, (userVal)=>{
852 this.apiRequestOptions[k] = userVal;
853 });
854 fs.appendChild(bbtn.div);
855 }
856 }
857 }
858
859 /**
860 * Show settings ui for configurable parameters, in the passed Div element.
861 * @param {HTMLDivElement} elDiv
862 */
863 show_settings(elDiv) {
864
865 let inp = ui.el_creatediv_input("SetBaseURL", "BaseURL", "text", this.baseURL, (val)=>{
866 this.baseURL = val;
867 });
868 elDiv.appendChild(inp.div);
869
870 inp = ui.el_creatediv_input("SetAuthorization", "Authorization", "text", this.headers["Authorization"], (val)=>{
871 this.headers["Authorization"] = val;
872 });
873 inp.el.placeholder = "Bearer OPENAI_API_KEY";
874 elDiv.appendChild(inp.div);
875
876 let bb = ui.el_creatediv_boolbutton("SetStream", "Stream", {true: "[+] yes stream", false: "[-] do oneshot"}, this.bStream, (val)=>{
877 this.bStream = val;
878 });
879 elDiv.appendChild(bb.div);
880
881 bb = ui.el_creatediv_boolbutton("SetTrimGarbage", "TrimGarbage", {true: "[+] yes trim", false: "[-] dont trim"}, this.bTrimGarbage, (val)=>{
882 this.bTrimGarbage = val;
883 });
884 elDiv.appendChild(bb.div);
885
886 this.show_settings_apirequestoptions(elDiv);
887
888 let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{
889 this.apiEP = ApiEP.Type[val];
890 });
891 elDiv.appendChild(sel.div);
892
893 sel = ui.el_creatediv_select("SetChatHistoryInCtxt", "ChatHistoryInCtxt", this.sRecentUserMsgCnt, this.iRecentUserMsgCnt, (val)=>{
894 this.iRecentUserMsgCnt = this.sRecentUserMsgCnt[val];
895 });
896 elDiv.appendChild(sel.div);
897
898 bb = ui.el_creatediv_boolbutton("SetCompletionFreshChatAlways", "CompletionFreshChatAlways", {true: "[+] yes fresh", false: "[-] no, with history"}, this.bCompletionFreshChatAlways, (val)=>{
899 this.bCompletionFreshChatAlways = val;
900 });
901 elDiv.appendChild(bb.div);
902
903 bb = ui.el_creatediv_boolbutton("SetCompletionInsertStandardRolePrefix", "CompletionInsertStandardRolePrefix", {true: "[+] yes insert", false: "[-] dont insert"}, this.bCompletionInsertStandardRolePrefix, (val)=>{
904 this.bCompletionInsertStandardRolePrefix = val;
905 });
906 elDiv.appendChild(bb.div);
907
908 }
909
910}
911
912
913/** @type {Me} */
914let gMe;
915
916function startme() {
917 console.log("INFO:SimpleChat:StartMe:Starting...");
918 gMe = new Me();
919 gMe.debug_disable();
920 document["gMe"] = gMe;
921 document["du"] = du;
922 for (let cid of gMe.defaultChatIds) {
923 gMe.multiChat.new_chat_session(cid);
924 }
925 gMe.multiChat.setup_ui(gMe.defaultChatIds[0], true);
926 gMe.multiChat.show_sessions();
927}
928
929document.addEventListener("DOMContentLoaded", startme);