Spaces:
Runtime error
Runtime error
// @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); | |