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);