mirror of
				https://github.com/ggml-org/llama.cpp.git
				synced 2025-10-29 08:41:22 +00:00 
			
		
		
		
	 1d36b3670b
			
		
	
	1d36b3670b
	
	
	
		
			
			* llama : move end-user examples to tools directory --------- Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
		
			
				
	
	
		
			930 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			930 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // @ts-check
 | |
| // A simple completions and chat/completions test related web front end logic
 | |
| // by Humans for All
 | |
| 
 | |
| import * as du from "./datautils.mjs";
 | |
| import * as ui from "./ui.mjs"
 | |
| 
 | |
| class Roles {
 | |
|     static System = "system";
 | |
|     static User = "user";
 | |
|     static Assistant = "assistant";
 | |
| }
 | |
| 
 | |
| class ApiEP {
 | |
|     static Type = {
 | |
|         Chat: "chat",
 | |
|         Completion: "completion",
 | |
|     }
 | |
|     static UrlSuffix = {
 | |
|         'chat': `/chat/completions`,
 | |
|         'completion': `/completions`,
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Build the url from given baseUrl and apiEp id.
 | |
|      * @param {string} baseUrl
 | |
|      * @param {string} apiEP
 | |
|      */
 | |
|     static Url(baseUrl, apiEP) {
 | |
|         if (baseUrl.endsWith("/")) {
 | |
|             baseUrl = baseUrl.substring(0, baseUrl.length-1);
 | |
|         }
 | |
|         return `${baseUrl}${this.UrlSuffix[apiEP]}`;
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| 
 | |
| let gUsageMsg = `
 | |
|     <p class="role-system">Usage</p>
 | |
|     <ul class="ul1">
 | |
|     <li> System prompt above, to try control ai response characteristics.</li>
 | |
|         <ul class="ul2">
 | |
|         <li> Completion mode - no system prompt normally.</li>
 | |
|         </ul>
 | |
|     <li> Use shift+enter for inserting enter/newline.</li>
 | |
|     <li> Enter your query to ai assistant below.</li>
 | |
|     <li> Default ContextWindow = [System, Last Query+Resp, Cur Query].</li>
 | |
|         <ul class="ul2">
 | |
|         <li> ChatHistInCtxt, MaxTokens, ModelCtxt window to expand</li>
 | |
|         </ul>
 | |
|     </ul>
 | |
| `;
 | |
| 
 | |
| 
 | |
| /** @typedef {{role: string, content: string}[]} ChatMessages */
 | |
| 
 | |
| /** @typedef {{iLastSys: number, xchat: ChatMessages}} SimpleChatODS */
 | |
| 
 | |
| class SimpleChat {
 | |
| 
 | |
|     /**
 | |
|      * @param {string} chatId
 | |
|      */
 | |
|     constructor(chatId) {
 | |
|         this.chatId = chatId;
 | |
|         /**
 | |
|          * Maintain in a form suitable for common LLM web service chat/completions' messages entry
 | |
|          * @type {ChatMessages}
 | |
|          */
 | |
|         this.xchat = [];
 | |
|         this.iLastSys = -1;
 | |
|         this.latestResponse = "";
 | |
|     }
 | |
| 
 | |
|     clear() {
 | |
|         this.xchat = [];
 | |
|         this.iLastSys = -1;
 | |
|     }
 | |
| 
 | |
|     ods_key() {
 | |
|         return `SimpleChat-${this.chatId}`
 | |
|     }
 | |
| 
 | |
|     save() {
 | |
|         /** @type {SimpleChatODS} */
 | |
|         let ods = {iLastSys: this.iLastSys, xchat: this.xchat};
 | |
|         localStorage.setItem(this.ods_key(), JSON.stringify(ods));
 | |
|     }
 | |
| 
 | |
|     load() {
 | |
|         let sods = localStorage.getItem(this.ods_key());
 | |
|         if (sods == null) {
 | |
|             return;
 | |
|         }
 | |
|         /** @type {SimpleChatODS} */
 | |
|         let ods = JSON.parse(sods);
 | |
|         this.iLastSys = ods.iLastSys;
 | |
|         this.xchat = ods.xchat;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Recent chat messages.
 | |
|      * If iRecentUserMsgCnt < 0
 | |
|      *   Then return the full chat history
 | |
|      * Else
 | |
|      *   Return chat messages from latest going back till the last/latest system prompt.
 | |
|      *   While keeping track that the number of user queries/messages doesnt exceed iRecentUserMsgCnt.
 | |
|      * @param {number} iRecentUserMsgCnt
 | |
|      */
 | |
|     recent_chat(iRecentUserMsgCnt) {
 | |
|         if (iRecentUserMsgCnt < 0) {
 | |
|             return this.xchat;
 | |
|         }
 | |
|         if (iRecentUserMsgCnt == 0) {
 | |
|             console.warn("WARN:SimpleChat:SC:RecentChat:iRecentUsermsgCnt of 0 means no user message/query sent");
 | |
|         }
 | |
|         /** @type{ChatMessages} */
 | |
|         let rchat = [];
 | |
|         let sysMsg = this.get_system_latest();
 | |
|         if (sysMsg.length != 0) {
 | |
|             rchat.push({role: Roles.System, content: sysMsg});
 | |
|         }
 | |
|         let iUserCnt = 0;
 | |
|         let iStart = this.xchat.length;
 | |
|         for(let i=this.xchat.length-1; i > this.iLastSys; i--) {
 | |
|             if (iUserCnt >= iRecentUserMsgCnt) {
 | |
|                 break;
 | |
|             }
 | |
|             let msg = this.xchat[i];
 | |
|             if (msg.role == Roles.User) {
 | |
|                 iStart = i;
 | |
|                 iUserCnt += 1;
 | |
|             }
 | |
|         }
 | |
|         for(let i = iStart; i < this.xchat.length; i++) {
 | |
|             let msg = this.xchat[i];
 | |
|             if (msg.role == Roles.System) {
 | |
|                 continue;
 | |
|             }
 | |
|             rchat.push({role: msg.role, content: msg.content});
 | |
|         }
 | |
|         return rchat;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Collate the latest response from the server/ai-model, as it is becoming available.
 | |
|      * This is mainly useful for the stream mode.
 | |
|      * @param {string} content
 | |
|      */
 | |
|     append_response(content) {
 | |
|         this.latestResponse += content;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Add an entry into xchat
 | |
|      * @param {string} role
 | |
|      * @param {string|undefined|null} content
 | |
|      */
 | |
|     add(role, content) {
 | |
|         if ((content == undefined) || (content == null) || (content == "")) {
 | |
|             return false;
 | |
|         }
 | |
|         this.xchat.push( {role: role, content: content} );
 | |
|         if (role == Roles.System) {
 | |
|             this.iLastSys = this.xchat.length - 1;
 | |
|         }
 | |
|         this.save();
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Show the contents in the specified div
 | |
|      * @param {HTMLDivElement} div
 | |
|      * @param {boolean} bClear
 | |
|      */
 | |
|     show(div, bClear=true) {
 | |
|         if (bClear) {
 | |
|             div.replaceChildren();
 | |
|         }
 | |
|         let last = undefined;
 | |
|         for(const x of this.recent_chat(gMe.iRecentUserMsgCnt)) {
 | |
|             let entry = ui.el_create_append_p(`${x.role}: ${x.content}`, div);
 | |
|             entry.className = `role-${x.role}`;
 | |
|             last = entry;
 | |
|         }
 | |
|         if (last !== undefined) {
 | |
|             last.scrollIntoView(false);
 | |
|         } else {
 | |
|             if (bClear) {
 | |
|                 div.innerHTML = gUsageMsg;
 | |
|                 gMe.setup_load(div, this);
 | |
|                 gMe.show_info(div);
 | |
|             }
 | |
|         }
 | |
|         return last;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Setup the fetch headers.
 | |
|      * It picks the headers from gMe.headers.
 | |
|      * It inserts Authorization only if its non-empty.
 | |
|      * @param {string} apiEP
 | |
|      */
 | |
|     fetch_headers(apiEP) {
 | |
|         let headers = new Headers();
 | |
|         for(let k in gMe.headers) {
 | |
|             let v = gMe.headers[k];
 | |
|             if ((k == "Authorization") && (v.trim() == "")) {
 | |
|                 continue;
 | |
|             }
 | |
|             headers.append(k, v);
 | |
|         }
 | |
|         return headers;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Add needed fields wrt json object to be sent wrt LLM web services completions endpoint.
 | |
|      * The needed fields/options are picked from a global object.
 | |
|      * Add optional stream flag, if required.
 | |
|      * Convert the json into string.
 | |
|      * @param {Object} obj
 | |
|      */
 | |
|     request_jsonstr_extend(obj) {
 | |
|         for(let k in gMe.apiRequestOptions) {
 | |
|             obj[k] = gMe.apiRequestOptions[k];
 | |
|         }
 | |
|         if (gMe.bStream) {
 | |
|             obj["stream"] = true;
 | |
|         }
 | |
|         return JSON.stringify(obj);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return a string form of json object suitable for chat/completions
 | |
|      */
 | |
|     request_messages_jsonstr() {
 | |
|         let req = {
 | |
|             messages: this.recent_chat(gMe.iRecentUserMsgCnt),
 | |
|         }
 | |
|         return this.request_jsonstr_extend(req);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return a string form of json object suitable for /completions
 | |
|      * @param {boolean} bInsertStandardRolePrefix Insert "<THE_ROLE>: " as prefix wrt each role's message
 | |
|      */
 | |
|     request_prompt_jsonstr(bInsertStandardRolePrefix) {
 | |
|         let prompt = "";
 | |
|         let iCnt = 0;
 | |
|         for(const chat of this.recent_chat(gMe.iRecentUserMsgCnt)) {
 | |
|             iCnt += 1;
 | |
|             if (iCnt > 1) {
 | |
|                 prompt += "\n";
 | |
|             }
 | |
|             if (bInsertStandardRolePrefix) {
 | |
|                 prompt += `${chat.role}: `;
 | |
|             }
 | |
|             prompt += `${chat.content}`;
 | |
|         }
 | |
|         let req = {
 | |
|             prompt: prompt,
 | |
|         }
 | |
|         return this.request_jsonstr_extend(req);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Return a string form of json object suitable for specified api endpoint.
 | |
|      * @param {string} apiEP
 | |
|      */
 | |
|     request_jsonstr(apiEP) {
 | |
|         if (apiEP == ApiEP.Type.Chat) {
 | |
|             return this.request_messages_jsonstr();
 | |
|         } else {
 | |
|             return this.request_prompt_jsonstr(gMe.bCompletionInsertStandardRolePrefix);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Extract the ai-model/assistant's response from the http response got.
 | |
|      * Optionally trim the message wrt any garbage at the end.
 | |
|      * @param {any} respBody
 | |
|      * @param {string} apiEP
 | |
|      */
 | |
|     response_extract(respBody, apiEP) {
 | |
|         let assistant = "";
 | |
|         if (apiEP == ApiEP.Type.Chat) {
 | |
|             assistant = respBody["choices"][0]["message"]["content"];
 | |
|         } else {
 | |
|             try {
 | |
|                 assistant = respBody["choices"][0]["text"];
 | |
|             } catch {
 | |
|                 assistant = respBody["content"];
 | |
|             }
 | |
|         }
 | |
|         return assistant;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Extract the ai-model/assistant's response from the http response got in streaming mode.
 | |
|      * @param {any} respBody
 | |
|      * @param {string} apiEP
 | |
|      */
 | |
|     response_extract_stream(respBody, apiEP) {
 | |
|         let assistant = "";
 | |
|         if (apiEP == ApiEP.Type.Chat) {
 | |
|             if (respBody["choices"][0]["finish_reason"] !== "stop") {
 | |
|                 assistant = respBody["choices"][0]["delta"]["content"];
 | |
|             }
 | |
|         } else {
 | |
|             try {
 | |
|                 assistant = respBody["choices"][0]["text"];
 | |
|             } catch {
 | |
|                 assistant = respBody["content"];
 | |
|             }
 | |
|         }
 | |
|         return assistant;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Allow setting of system prompt, but only at begining.
 | |
|      * @param {string} sysPrompt
 | |
|      * @param {string} msgTag
 | |
|      */
 | |
|     add_system_begin(sysPrompt, msgTag) {
 | |
|         if (this.xchat.length == 0) {
 | |
|             if (sysPrompt.length > 0) {
 | |
|                 return this.add(Roles.System, sysPrompt);
 | |
|             }
 | |
|         } else {
 | |
|             if (sysPrompt.length > 0) {
 | |
|                 if (this.xchat[0].role !== Roles.System) {
 | |
|                     console.error(`ERRR:SimpleChat:SC:${msgTag}:You need to specify system prompt before any user query, ignoring...`);
 | |
|                 } else {
 | |
|                     if (this.xchat[0].content !== sysPrompt) {
 | |
|                         console.error(`ERRR:SimpleChat:SC:${msgTag}:You cant change system prompt, mid way through, ignoring...`);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Allow setting of system prompt, at any time.
 | |
|      * @param {string} sysPrompt
 | |
|      * @param {string} msgTag
 | |
|      */
 | |
|     add_system_anytime(sysPrompt, msgTag) {
 | |
|         if (sysPrompt.length <= 0) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         if (this.iLastSys < 0) {
 | |
|             return this.add(Roles.System, sysPrompt);
 | |
|         }
 | |
| 
 | |
|         let lastSys = this.xchat[this.iLastSys].content;
 | |
|         if (lastSys !== sysPrompt) {
 | |
|             return this.add(Roles.System, sysPrompt);
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Retrieve the latest system prompt.
 | |
|      */
 | |
|     get_system_latest() {
 | |
|         if (this.iLastSys == -1) {
 | |
|             return "";
 | |
|         }
 | |
|         let sysPrompt = this.xchat[this.iLastSys].content;
 | |
|         return sysPrompt;
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Handle the multipart response from server/ai-model
 | |
|      * @param {Response} resp
 | |
|      * @param {string} apiEP
 | |
|      * @param {HTMLDivElement} elDiv
 | |
|      */
 | |
|     async handle_response_multipart(resp, apiEP, elDiv) {
 | |
|         let elP = ui.el_create_append_p("", elDiv);
 | |
|         if (!resp.body) {
 | |
|             throw Error("ERRR:SimpleChat:SC:HandleResponseMultiPart:No body...");
 | |
|         }
 | |
|         let tdUtf8 = new TextDecoder("utf-8");
 | |
|         let rr = resp.body.getReader();
 | |
|         this.latestResponse = "";
 | |
|         let xLines = new du.NewLines();
 | |
|         while(true) {
 | |
|             let { value: cur,  done: done } = await rr.read();
 | |
|             if (cur) {
 | |
|                 let curBody = tdUtf8.decode(cur, {stream: true});
 | |
|                 console.debug("DBUG:SC:PART:Str:", curBody);
 | |
|                 xLines.add_append(curBody);
 | |
|             }
 | |
|             while(true) {
 | |
|                 let curLine = xLines.shift(!done);
 | |
|                 if (curLine == undefined) {
 | |
|                     break;
 | |
|                 }
 | |
|                 if (curLine.trim() == "") {
 | |
|                     continue;
 | |
|                 }
 | |
|                 if (curLine.startsWith("data:")) {
 | |
|                     curLine = curLine.substring(5);
 | |
|                 }
 | |
|                 if (curLine.trim() === "[DONE]") {
 | |
|                     break;
 | |
|                 }
 | |
|                 let curJson = JSON.parse(curLine);
 | |
|                 console.debug("DBUG:SC:PART:Json:", curJson);
 | |
|                 this.append_response(this.response_extract_stream(curJson, apiEP));
 | |
|             }
 | |
|             elP.innerText = this.latestResponse;
 | |
|             elP.scrollIntoView(false);
 | |
|             if (done) {
 | |
|                 break;
 | |
|             }
 | |
|         }
 | |
|         console.debug("DBUG:SC:PART:Full:", this.latestResponse);
 | |
|         return this.latestResponse;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Handle the oneshot response from server/ai-model
 | |
|      * @param {Response} resp
 | |
|      * @param {string} apiEP
 | |
|      */
 | |
|     async handle_response_oneshot(resp, apiEP) {
 | |
|         let respBody = await resp.json();
 | |
|         console.debug(`DBUG:SimpleChat:SC:${this.chatId}:HandleUserSubmit:RespBody:${JSON.stringify(respBody)}`);
 | |
|         return this.response_extract(respBody, apiEP);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Handle the response from the server be it in oneshot or multipart/stream mode.
 | |
|      * Also take care of the optional garbage trimming.
 | |
|      * @param {Response} resp
 | |
|      * @param {string} apiEP
 | |
|      * @param {HTMLDivElement} elDiv
 | |
|      */
 | |
|     async handle_response(resp, apiEP, elDiv) {
 | |
|         let theResp = {
 | |
|             assistant: "",
 | |
|             trimmed: "",
 | |
|         }
 | |
|         if (gMe.bStream) {
 | |
|             try {
 | |
|                 theResp.assistant = await this.handle_response_multipart(resp, apiEP, elDiv);
 | |
|                 this.latestResponse = "";
 | |
|             } catch (error) {
 | |
|                 theResp.assistant = this.latestResponse;
 | |
|                 this.add(Roles.Assistant, theResp.assistant);
 | |
|                 this.latestResponse = "";
 | |
|                 throw error;
 | |
|             }
 | |
|         } else {
 | |
|             theResp.assistant = await this.handle_response_oneshot(resp, apiEP);
 | |
|         }
 | |
|         if (gMe.bTrimGarbage) {
 | |
|             let origMsg = theResp.assistant;
 | |
|             theResp.assistant = du.trim_garbage_at_end(origMsg);
 | |
|             theResp.trimmed = origMsg.substring(theResp.assistant.length);
 | |
|         }
 | |
|         this.add(Roles.Assistant, theResp.assistant);
 | |
|         return theResp;
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| 
 | |
| class MultiChatUI {
 | |
| 
 | |
|     constructor() {
 | |
|         /** @type {Object<string, SimpleChat>} */
 | |
|         this.simpleChats = {};
 | |
|         /** @type {string} */
 | |
|         this.curChatId = "";
 | |
| 
 | |
|         // the ui elements
 | |
|         this.elInSystem = /** @type{HTMLInputElement} */(document.getElementById("system-in"));
 | |
|         this.elDivChat = /** @type{HTMLDivElement} */(document.getElementById("chat-div"));
 | |
|         this.elBtnUser = /** @type{HTMLButtonElement} */(document.getElementById("user-btn"));
 | |
|         this.elInUser = /** @type{HTMLInputElement} */(document.getElementById("user-in"));
 | |
|         this.elDivHeading = /** @type{HTMLSelectElement} */(document.getElementById("heading"));
 | |
|         this.elDivSessions = /** @type{HTMLDivElement} */(document.getElementById("sessions-div"));
 | |
|         this.elBtnSettings = /** @type{HTMLButtonElement} */(document.getElementById("settings"));
 | |
| 
 | |
|         this.validate_element(this.elInSystem, "system-in");
 | |
|         this.validate_element(this.elDivChat, "chat-div");
 | |
|         this.validate_element(this.elInUser, "user-in");
 | |
|         this.validate_element(this.elDivHeading, "heading");
 | |
|         this.validate_element(this.elDivChat, "sessions-div");
 | |
|         this.validate_element(this.elBtnSettings, "settings");
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check if the element got
 | |
|      * @param {HTMLElement | null} el
 | |
|      * @param {string} msgTag
 | |
|      */
 | |
|     validate_element(el, msgTag) {
 | |
|         if (el == null) {
 | |
|             throw Error(`ERRR:SimpleChat:MCUI:${msgTag} element missing in html...`);
 | |
|         } else {
 | |
|             console.debug(`INFO:SimpleChat:MCUI:${msgTag} Id[${el.id}] Name[${el["name"]}]`);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Reset user input ui.
 | |
|      * * clear user input
 | |
|      * * enable user input
 | |
|      * * set focus to user input
 | |
|      */
 | |
|     ui_reset_userinput() {
 | |
|         this.elInUser.value = "";
 | |
|         this.elInUser.disabled = false;
 | |
|         this.elInUser.focus();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Setup the needed callbacks wrt UI, curChatId to defaultChatId and
 | |
|      * optionally switch to specified defaultChatId.
 | |
|      * @param {string} defaultChatId
 | |
|      * @param {boolean} bSwitchSession
 | |
|      */
 | |
|     setup_ui(defaultChatId, bSwitchSession=false) {
 | |
| 
 | |
|         this.curChatId = defaultChatId;
 | |
|         if (bSwitchSession) {
 | |
|             this.handle_session_switch(this.curChatId);
 | |
|         }
 | |
| 
 | |
|         this.elBtnSettings.addEventListener("click", (ev)=>{
 | |
|             this.elDivChat.replaceChildren();
 | |
|             gMe.show_settings(this.elDivChat);
 | |
|         });
 | |
| 
 | |
|         this.elBtnUser.addEventListener("click", (ev)=>{
 | |
|             if (this.elInUser.disabled) {
 | |
|                 return;
 | |
|             }
 | |
|             this.handle_user_submit(this.curChatId, gMe.apiEP).catch((/** @type{Error} */reason)=>{
 | |
|                 let msg = `ERRR:SimpleChat\nMCUI:HandleUserSubmit:${this.curChatId}\n${reason.name}:${reason.message}`;
 | |
|                 console.error(msg.replace("\n", ":"));
 | |
|                 alert(msg);
 | |
|                 this.ui_reset_userinput();
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         this.elInUser.addEventListener("keyup", (ev)=> {
 | |
|             // allow user to insert enter into their message using shift+enter.
 | |
|             // while just pressing enter key will lead to submitting.
 | |
|             if ((ev.key === "Enter") && (!ev.shiftKey)) {
 | |
|                 let value = this.elInUser.value;
 | |
|                 this.elInUser.value = value.substring(0,value.length-1);
 | |
|                 this.elBtnUser.click();
 | |
|                 ev.preventDefault();
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         this.elInSystem.addEventListener("keyup", (ev)=> {
 | |
|             // allow user to insert enter into the system prompt using shift+enter.
 | |
|             // while just pressing enter key will lead to setting the system prompt.
 | |
|             if ((ev.key === "Enter") && (!ev.shiftKey)) {
 | |
|                 let value = this.elInSystem.value;
 | |
|                 this.elInSystem.value = value.substring(0,value.length-1);
 | |
|                 let chat = this.simpleChats[this.curChatId];
 | |
|                 chat.add_system_anytime(this.elInSystem.value, this.curChatId);
 | |
|                 chat.show(this.elDivChat);
 | |
|                 ev.preventDefault();
 | |
|             }
 | |
|         });
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Setup a new chat session and optionally switch to it.
 | |
|      * @param {string} chatId
 | |
|      * @param {boolean} bSwitchSession
 | |
|      */
 | |
|     new_chat_session(chatId, bSwitchSession=false) {
 | |
|         this.simpleChats[chatId] = new SimpleChat(chatId);
 | |
|         if (bSwitchSession) {
 | |
|             this.handle_session_switch(chatId);
 | |
|         }
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Handle user query submit request, wrt specified chat session.
 | |
|      * @param {string} chatId
 | |
|      * @param {string} apiEP
 | |
|      */
 | |
|     async handle_user_submit(chatId, apiEP) {
 | |
| 
 | |
|         let chat = this.simpleChats[chatId];
 | |
| 
 | |
|         // In completion mode, if configured, clear any previous chat history.
 | |
|         // So if user wants to simulate a multi-chat based completion query,
 | |
|         // they will have to enter the full thing, as a suitable multiline
 | |
|         // user input/query.
 | |
|         if ((apiEP == ApiEP.Type.Completion) && (gMe.bCompletionFreshChatAlways)) {
 | |
|             chat.clear();
 | |
|         }
 | |
| 
 | |
|         chat.add_system_anytime(this.elInSystem.value, chatId);
 | |
| 
 | |
|         let content = this.elInUser.value;
 | |
|         if (!chat.add(Roles.User, content)) {
 | |
|             console.debug(`WARN:SimpleChat:MCUI:${chatId}:HandleUserSubmit:Ignoring empty user input...`);
 | |
|             return;
 | |
|         }
 | |
|         chat.show(this.elDivChat);
 | |
| 
 | |
|         let theUrl = ApiEP.Url(gMe.baseURL, apiEP);
 | |
|         let theBody = chat.request_jsonstr(apiEP);
 | |
| 
 | |
|         this.elInUser.value = "working...";
 | |
|         this.elInUser.disabled = true;
 | |
|         console.debug(`DBUG:SimpleChat:MCUI:${chatId}:HandleUserSubmit:${theUrl}:ReqBody:${theBody}`);
 | |
|         let theHeaders = chat.fetch_headers(apiEP);
 | |
|         let resp = await fetch(theUrl, {
 | |
|             method: "POST",
 | |
|             headers: theHeaders,
 | |
|             body: theBody,
 | |
|         });
 | |
| 
 | |
|         let theResp = await chat.handle_response(resp, apiEP, this.elDivChat);
 | |
|         if (chatId == this.curChatId) {
 | |
|             chat.show(this.elDivChat);
 | |
|             if (theResp.trimmed.length > 0) {
 | |
|                 let p = ui.el_create_append_p(`TRIMMED:${theResp.trimmed}`, this.elDivChat);
 | |
|                 p.className="role-trim";
 | |
|             }
 | |
|         } else {
 | |
|             console.debug(`DBUG:SimpleChat:MCUI:HandleUserSubmit:ChatId has changed:[${chatId}] [${this.curChatId}]`);
 | |
|         }
 | |
|         this.ui_reset_userinput();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Show buttons for NewChat and available chat sessions, in the passed elDiv.
 | |
|      * If elDiv is undefined/null, then use this.elDivSessions.
 | |
|      * Take care of highlighting the selected chat-session's btn.
 | |
|      * @param {HTMLDivElement | undefined} elDiv
 | |
|      */
 | |
|     show_sessions(elDiv=undefined) {
 | |
|         if (!elDiv) {
 | |
|             elDiv = this.elDivSessions;
 | |
|         }
 | |
|         elDiv.replaceChildren();
 | |
|         // Btn for creating new chat session
 | |
|         let btnNew = ui.el_create_button("New CHAT", (ev)=> {
 | |
|             if (this.elInUser.disabled) {
 | |
|                 console.error(`ERRR:SimpleChat:MCUI:NewChat:Current session [${this.curChatId}] awaiting response, ignoring request...`);
 | |
|                 alert("ERRR:SimpleChat\nMCUI:NewChat\nWait for response to pending query, before starting new chat session");
 | |
|                 return;
 | |
|             }
 | |
|             let chatId = `Chat${Object.keys(this.simpleChats).length}`;
 | |
|             let chatIdGot = prompt("INFO:SimpleChat\nMCUI:NewChat\nEnter id for new chat session", chatId);
 | |
|             if (!chatIdGot) {
 | |
|                 console.error("ERRR:SimpleChat:MCUI:NewChat:Skipping based on user request...");
 | |
|                 return;
 | |
|             }
 | |
|             this.new_chat_session(chatIdGot, true);
 | |
|             this.create_session_btn(elDiv, chatIdGot);
 | |
|             ui.el_children_config_class(elDiv, chatIdGot, "session-selected", "");
 | |
|         });
 | |
|         elDiv.appendChild(btnNew);
 | |
|         // Btns for existing chat sessions
 | |
|         let chatIds = Object.keys(this.simpleChats);
 | |
|         for(let cid of chatIds) {
 | |
|             let btn = this.create_session_btn(elDiv, cid);
 | |
|             if (cid == this.curChatId) {
 | |
|                 btn.className = "session-selected";
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     create_session_btn(elDiv, cid) {
 | |
|         let btn = ui.el_create_button(cid, (ev)=>{
 | |
|             let target = /** @type{HTMLButtonElement} */(ev.target);
 | |
|             console.debug(`DBUG:SimpleChat:MCUI:SessionClick:${target.id}`);
 | |
|             if (this.elInUser.disabled) {
 | |
|                 console.error(`ERRR:SimpleChat:MCUI:SessionClick:${target.id}:Current session [${this.curChatId}] awaiting response, ignoring switch...`);
 | |
|                 alert("ERRR:SimpleChat\nMCUI:SessionClick\nWait for response to pending query, before switching");
 | |
|                 return;
 | |
|             }
 | |
|             this.handle_session_switch(target.id);
 | |
|             ui.el_children_config_class(elDiv, target.id, "session-selected", "");
 | |
|         });
 | |
|         elDiv.appendChild(btn);
 | |
|         return btn;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Switch ui to the specified chatId and set curChatId to same.
 | |
|      * @param {string} chatId
 | |
|      */
 | |
|     async handle_session_switch(chatId) {
 | |
|         let chat = this.simpleChats[chatId];
 | |
|         if (chat == undefined) {
 | |
|             console.error(`ERRR:SimpleChat:MCUI:HandleSessionSwitch:${chatId} missing...`);
 | |
|             return;
 | |
|         }
 | |
|         this.elInSystem.value = chat.get_system_latest();
 | |
|         this.elInUser.value = "";
 | |
|         chat.show(this.elDivChat);
 | |
|         this.elInUser.focus();
 | |
|         this.curChatId = chatId;
 | |
|         console.log(`INFO:SimpleChat:MCUI:HandleSessionSwitch:${chatId} entered...`);
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| 
 | |
| class Me {
 | |
| 
 | |
|     constructor() {
 | |
|         this.baseURL = "http://127.0.0.1:8080";
 | |
|         this.defaultChatIds = [ "Default", "Other" ];
 | |
|         this.multiChat = new MultiChatUI();
 | |
|         this.bStream = true;
 | |
|         this.bCompletionFreshChatAlways = true;
 | |
|         this.bCompletionInsertStandardRolePrefix = false;
 | |
|         this.bTrimGarbage = true;
 | |
|         this.iRecentUserMsgCnt = 2;
 | |
|         this.sRecentUserMsgCnt = {
 | |
|             "Full": -1,
 | |
|             "Last0": 1,
 | |
|             "Last1": 2,
 | |
|             "Last2": 3,
 | |
|             "Last4": 5,
 | |
|         };
 | |
|         this.apiEP = ApiEP.Type.Chat;
 | |
|         this.headers = {
 | |
|             "Content-Type": "application/json",
 | |
|             "Authorization": "", // Authorization: Bearer OPENAI_API_KEY
 | |
|         }
 | |
|         // Add needed fields wrt json object to be sent wrt LLM web services completions endpoint.
 | |
|         this.apiRequestOptions = {
 | |
|             "model": "gpt-3.5-turbo",
 | |
|             "temperature": 0.7,
 | |
|             "max_tokens": 1024,
 | |
|             "n_predict": 1024,
 | |
|             "cache_prompt": false,
 | |
|             //"frequency_penalty": 1.2,
 | |
|             //"presence_penalty": 1.2,
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Disable console.debug by mapping it to a empty function.
 | |
|      */
 | |
|     debug_disable() {
 | |
|         this.console_debug = console.debug;
 | |
|         console.debug = () => {
 | |
| 
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Setup the load saved chat ui.
 | |
|      * @param {HTMLDivElement} div
 | |
|      * @param {SimpleChat} chat
 | |
|      */
 | |
|     setup_load(div, chat) {
 | |
|         if (!(chat.ods_key() in localStorage)) {
 | |
|             return;
 | |
|         }
 | |
|         div.innerHTML += `<p class="role-system">Restore</p>
 | |
|         <p>Load previously saved chat session, if available</p>`;
 | |
|         let btn = ui.el_create_button(chat.ods_key(), (ev)=>{
 | |
|             console.log("DBUG:SimpleChat:SC:Load", chat);
 | |
|             chat.load();
 | |
|             queueMicrotask(()=>{
 | |
|                 chat.show(div);
 | |
|                 this.multiChat.elInSystem.value = chat.get_system_latest();
 | |
|             });
 | |
|         });
 | |
|         div.appendChild(btn);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Show the configurable parameters info in the passed Div element.
 | |
|      * @param {HTMLDivElement} elDiv
 | |
|      * @param {boolean} bAll
 | |
|      */
 | |
|     show_info(elDiv, bAll=false) {
 | |
| 
 | |
|         let p = ui.el_create_append_p("Settings (devel-tools-console document[gMe])", elDiv);
 | |
|         p.className = "role-system";
 | |
| 
 | |
|         if (bAll) {
 | |
| 
 | |
|             ui.el_create_append_p(`baseURL:${this.baseURL}`, elDiv);
 | |
| 
 | |
|             ui.el_create_append_p(`Authorization:${this.headers["Authorization"]}`, elDiv);
 | |
| 
 | |
|             ui.el_create_append_p(`bStream:${this.bStream}`, elDiv);
 | |
| 
 | |
|             ui.el_create_append_p(`bTrimGarbage:${this.bTrimGarbage}`, elDiv);
 | |
| 
 | |
|             ui.el_create_append_p(`ApiEndPoint:${this.apiEP}`, elDiv);
 | |
| 
 | |
|             ui.el_create_append_p(`iRecentUserMsgCnt:${this.iRecentUserMsgCnt}`, elDiv);
 | |
| 
 | |
|             ui.el_create_append_p(`bCompletionFreshChatAlways:${this.bCompletionFreshChatAlways}`, elDiv);
 | |
| 
 | |
|             ui.el_create_append_p(`bCompletionInsertStandardRolePrefix:${this.bCompletionInsertStandardRolePrefix}`, elDiv);
 | |
| 
 | |
|         }
 | |
| 
 | |
|         ui.el_create_append_p(`apiRequestOptions:${JSON.stringify(this.apiRequestOptions, null, " - ")}`, elDiv);
 | |
|         ui.el_create_append_p(`headers:${JSON.stringify(this.headers, null, " - ")}`, elDiv);
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Auto create ui input elements for fields in apiRequestOptions
 | |
|      * Currently supports text and number field types.
 | |
|      * @param {HTMLDivElement} elDiv
 | |
|      */
 | |
|     show_settings_apirequestoptions(elDiv) {
 | |
|         let typeDict = {
 | |
|             "string": "text",
 | |
|             "number": "number",
 | |
|         };
 | |
|         let fs = document.createElement("fieldset");
 | |
|         let legend = document.createElement("legend");
 | |
|         legend.innerText = "ApiRequestOptions";
 | |
|         fs.appendChild(legend);
 | |
|         elDiv.appendChild(fs);
 | |
|         for(const k in this.apiRequestOptions) {
 | |
|             let val = this.apiRequestOptions[k];
 | |
|             let type = typeof(val);
 | |
|             if (((type == "string") || (type == "number"))) {
 | |
|                 let inp = ui.el_creatediv_input(`Set${k}`, k, typeDict[type], this.apiRequestOptions[k], (val)=>{
 | |
|                     if (type == "number") {
 | |
|                         val = Number(val);
 | |
|                     }
 | |
|                     this.apiRequestOptions[k] = val;
 | |
|                 });
 | |
|                 fs.appendChild(inp.div);
 | |
|             } else if (type == "boolean") {
 | |
|                 let bbtn = ui.el_creatediv_boolbutton(`Set{k}`, k, {true: "true", false: "false"}, val, (userVal)=>{
 | |
|                     this.apiRequestOptions[k] = userVal;
 | |
|                 });
 | |
|                 fs.appendChild(bbtn.div);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Show settings ui for configurable parameters, in the passed Div element.
 | |
|      * @param {HTMLDivElement} elDiv
 | |
|      */
 | |
|     show_settings(elDiv) {
 | |
| 
 | |
|         let inp = ui.el_creatediv_input("SetBaseURL", "BaseURL", "text", this.baseURL, (val)=>{
 | |
|             this.baseURL = val;
 | |
|         });
 | |
|         elDiv.appendChild(inp.div);
 | |
| 
 | |
|         inp = ui.el_creatediv_input("SetAuthorization", "Authorization", "text", this.headers["Authorization"], (val)=>{
 | |
|             this.headers["Authorization"] = val;
 | |
|         });
 | |
|         inp.el.placeholder = "Bearer OPENAI_API_KEY";
 | |
|         elDiv.appendChild(inp.div);
 | |
| 
 | |
|         let bb = ui.el_creatediv_boolbutton("SetStream", "Stream", {true: "[+] yes stream", false: "[-] do oneshot"}, this.bStream, (val)=>{
 | |
|             this.bStream = val;
 | |
|         });
 | |
|         elDiv.appendChild(bb.div);
 | |
| 
 | |
|         bb = ui.el_creatediv_boolbutton("SetTrimGarbage", "TrimGarbage", {true: "[+] yes trim", false: "[-] dont trim"}, this.bTrimGarbage, (val)=>{
 | |
|             this.bTrimGarbage = val;
 | |
|         });
 | |
|         elDiv.appendChild(bb.div);
 | |
| 
 | |
|         this.show_settings_apirequestoptions(elDiv);
 | |
| 
 | |
|         let sel = ui.el_creatediv_select("SetApiEP", "ApiEndPoint", ApiEP.Type, this.apiEP, (val)=>{
 | |
|             this.apiEP = ApiEP.Type[val];
 | |
|         });
 | |
|         elDiv.appendChild(sel.div);
 | |
| 
 | |
|         sel = ui.el_creatediv_select("SetChatHistoryInCtxt", "ChatHistoryInCtxt", this.sRecentUserMsgCnt, this.iRecentUserMsgCnt, (val)=>{
 | |
|             this.iRecentUserMsgCnt = this.sRecentUserMsgCnt[val];
 | |
|         });
 | |
|         elDiv.appendChild(sel.div);
 | |
| 
 | |
|         bb = ui.el_creatediv_boolbutton("SetCompletionFreshChatAlways", "CompletionFreshChatAlways", {true: "[+] yes fresh", false: "[-] no, with history"}, this.bCompletionFreshChatAlways, (val)=>{
 | |
|             this.bCompletionFreshChatAlways = val;
 | |
|         });
 | |
|         elDiv.appendChild(bb.div);
 | |
| 
 | |
|         bb = ui.el_creatediv_boolbutton("SetCompletionInsertStandardRolePrefix", "CompletionInsertStandardRolePrefix", {true: "[+] yes insert", false: "[-] dont insert"}, this.bCompletionInsertStandardRolePrefix, (val)=>{
 | |
|             this.bCompletionInsertStandardRolePrefix = val;
 | |
|         });
 | |
|         elDiv.appendChild(bb.div);
 | |
| 
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| 
 | |
| /** @type {Me} */
 | |
| let gMe;
 | |
| 
 | |
| function startme() {
 | |
|     console.log("INFO:SimpleChat:StartMe:Starting...");
 | |
|     gMe = new Me();
 | |
|     gMe.debug_disable();
 | |
|     document["gMe"] = gMe;
 | |
|     document["du"] = du;
 | |
|     for (let cid of gMe.defaultChatIds) {
 | |
|         gMe.multiChat.new_chat_session(cid);
 | |
|     }
 | |
|     gMe.multiChat.setup_ui(gMe.defaultChatIds[0], true);
 | |
|     gMe.multiChat.show_sessions();
 | |
| }
 | |
| 
 | |
| document.addEventListener("DOMContentLoaded", startme);
 |