llama.cpp/examples/server/public_simplechat/simplechat.js

254 lines
6.9 KiB
JavaScript

// @ts-check
// A simple completions and chat/completions test related web front end logic
// by Humans for All
class Roles {
static System = "system";
static User = "user";
static Assistant = "assistant";
}
class ApiEP {
static Chat = "chat";
static Completion = "completion";
}
class SimpleChat {
constructor() {
/**
* Maintain in a form suitable for common LLM web service chat/completions' messages entry
* @type {{role: string, content: string}[]}
*/
this.xchat = [];
this.iLastSys = -1;
}
/**
* 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;
}
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.xchat) {
let entry = document.createElement("p");
entry.className = `role-${x.role}`;
entry.innerText = `${x.role}: ${x.content}`;
div.appendChild(entry);
last = entry;
}
if (last !== undefined) {
last.scrollIntoView(false);
}
}
/**
* Add needed fields wrt json object to be sent wrt LLM web services completions endpoint
* Convert the json into string.
* @param {Object} obj
*/
request_jsonstr(obj) {
obj["temperature"] = 0.7;
return JSON.stringify(obj);
}
/**
* Return a string form of json object suitable for chat/completions
*/
request_messages_jsonstr() {
let req = {
messages: this.xchat,
}
return this.request_jsonstr(req);
}
/**
* Return a string form of json object suitable for /completions
*/
request_prompt_jsonstr() {
let prompt = "";
for(const chat of this.xchat) {
prompt += `${chat.role}: ${chat.content}\n`;
}
let req = {
prompt: prompt,
}
return this.request_jsonstr(req);
}
}
/**
* Handle setting of system prompt, but only at begining.
* @param {HTMLInputElement} inputSystem
*/
function handle_systemprompt_begin(inputSystem) {
let sysPrompt = inputSystem.value;
if (gChat.xchat.length == 0) {
if (sysPrompt.length > 0) {
gChat.add(Roles.System, sysPrompt);
}
} else {
if (sysPrompt.length > 0) {
if (gChat.xchat[0].role !== Roles.System) {
console.error("ERRR:HandleSubmit:You need to specify system prompt before any user query, ignoring...");
} else {
if (gChat.xchat[0].content !== sysPrompt) {
console.error("ERRR:HandleSubmit:You cant change system prompt, mid way through, ignoring...");
}
}
}
}
}
/**
* Handle setting of system prompt, at any time.
* @param {HTMLInputElement} inputSystem
*/
function handle_systemprompt_anytime(inputSystem) {
let sysPrompt = inputSystem.value;
if (sysPrompt.length <= 0) {
return;
}
if (gChat.iLastSys < 0) {
gChat.add(Roles.System, sysPrompt);
return;
}
let lastSys = gChat.xchat[gChat.iLastSys].content;
if (lastSys !== sysPrompt) {
gChat.add(Roles.System, sysPrompt);
return;
}
}
/**
* Handle submit request by user
* @param {HTMLInputElement} inputSystem
* @param {HTMLInputElement} inputUser
* @param {HTMLDivElement} divChat
* @param {string} apiEP
*/
async function handle_submit(inputSystem, inputUser, divChat, apiEP) {
handle_systemprompt_anytime(inputSystem);
let content = inputUser?.value;
if (!gChat.add(Roles.User, content)) {
console.debug("WARN:HandleSubmit:Ignoring empty user input...");
return;
}
gChat.show(divChat);
let theBody;
let theUrl = gChatURL[apiEP]
if (apiEP == ApiEP.Chat) {
theBody = gChat.request_messages_jsonstr();
} else {
theBody = gChat.request_prompt_jsonstr();
}
inputUser.value = "working...";
inputUser.disabled = true;
console.debug(`DBUG:HandleSubmit:${theUrl}:ReqBody:${theBody}`);
let resp = await fetch(theUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: theBody,
});
inputUser.value = "";
inputUser.disabled = false;
let respBody = await resp.json();
console.debug("DBUG:HandleSubmit:RespBody:", respBody);
let assistantMsg;
if (apiEP == ApiEP.Chat) {
assistantMsg = respBody["choices"][0]["message"]["content"];
} else {
try {
assistantMsg = respBody["choices"][0]["text"];
} catch {
assistantMsg = respBody["content"];
}
}
gChat.add(Roles.Assistant, assistantMsg);
gChat.show(divChat);
// Purposefully clear at end rather than begin of this function
// so that one can switch from chat to completion mode and sequece
// in a completion mode with multiple user-assistant chat data
// from before to be sent/occur once.
if ((apiEP == ApiEP.Completion) && (gbCompletionFreshChatAlways)) {
gChat.xchat.length = 0;
}
inputUser.focus();
}
let gChat = new SimpleChat();
let gBaseURL = "http://127.0.0.1:8080";
let gChatURL = {
'chat': `${gBaseURL}/chat/completions`,
'completion': `${gBaseURL}/completions`,
}
const gbCompletionFreshChatAlways = true;
function startme() {
let inputSystem = /** @type{HTMLInputElement} */(document.getElementById("system"));
let divChat = /** @type{HTMLDivElement} */(document.getElementById("chat"));
let btnSubmit = document.getElementById("submit");
let inputUser = /** @type{HTMLInputElement} */(document.getElementById("user"));
let selectApiEP = /** @type{HTMLInputElement} */(document.getElementById("api-ep"));
if (divChat == null) {
throw Error("ERRR:StartMe:Chat element missing");
}
btnSubmit?.addEventListener("click", (ev)=>{
if (inputUser.disabled) {
return;
}
handle_submit(inputSystem, inputUser, divChat, selectApiEP.value);
});
inputUser?.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)) {
btnSubmit?.click();
ev.preventDefault();
}
});
}
document.addEventListener("DOMContentLoaded", startme);