mirror of
https://github.com/ggerganov/llama.cpp.git
synced 2024-12-27 20:04:35 +00:00
3791ad2193
* SimpleChat: Allow for chat req bool options to be user controlled * SimpleChat: Allow user to control cache_prompt flag in request * SimpleChat: Add sample GUI images to readme file Show the chat screen and the settings screen * SimpleChat:Readme: Add quickstart block, title to image, cleanup * SimpleChat: RePosition contents of the Info and Settings UI Make it more logically structured and flow through. * SimpleChat: Rename to apiRequestOptions from chatRequestOptions So that it is not wrongly assumed that these request options are used only for chat/completions endpoint. Rather these are used for both the end points, so rename to match semantic better. * SimpleChat: Update image included with readme wrt settings ui * SimpleChat:ReadMe: Switch to webp screen image to reduce size
927 lines
30 KiB
JavaScript
927 lines
30 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);
|
|
}
|
|
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);
|