simplechat.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. // @ts-check
  2. // A simple completions and chat/completions test related web front end logic
  3. // by Humans for All
  4. class Roles {
  5. static System = "system";
  6. static User = "user";
  7. static Assistant = "assistant";
  8. }
  9. class ApiEP {
  10. static Chat = "chat";
  11. static Completion = "completion";
  12. }
  13. let gUsageMsg = `
  14. <p> Enter the system prompt above, before entering/submitting any user query.</p>
  15. <p> Enter your text to the ai assistant below.</p>
  16. <p> Use shift+enter for inserting enter.</p>
  17. <p> Refresh the page to start over fresh.</p>
  18. `;
  19. class SimpleChat {
  20. constructor() {
  21. /**
  22. * Maintain in a form suitable for common LLM web service chat/completions' messages entry
  23. * @type {{role: string, content: string}[]}
  24. */
  25. this.xchat = [];
  26. this.iLastSys = -1;
  27. }
  28. /**
  29. * Add an entry into xchat
  30. * @param {string} role
  31. * @param {string|undefined|null} content
  32. */
  33. add(role, content) {
  34. if ((content == undefined) || (content == null) || (content == "")) {
  35. return false;
  36. }
  37. this.xchat.push( {role: role, content: content} );
  38. if (role == Roles.System) {
  39. this.iLastSys = this.xchat.length - 1;
  40. }
  41. return true;
  42. }
  43. /**
  44. * Show the contents in the specified div
  45. * @param {HTMLDivElement} div
  46. * @param {boolean} bClear
  47. */
  48. show(div, bClear=true) {
  49. if (bClear) {
  50. div.replaceChildren();
  51. }
  52. let last = undefined;
  53. for(const x of this.xchat) {
  54. let entry = document.createElement("p");
  55. entry.className = `role-${x.role}`;
  56. entry.innerText = `${x.role}: ${x.content}`;
  57. div.appendChild(entry);
  58. last = entry;
  59. }
  60. if (last !== undefined) {
  61. last.scrollIntoView(false);
  62. } else {
  63. if (bClear) {
  64. div.innerHTML = gUsageMsg;
  65. }
  66. }
  67. }
  68. /**
  69. * Add needed fields wrt json object to be sent wrt LLM web services completions endpoint
  70. * Convert the json into string.
  71. * @param {Object} obj
  72. */
  73. request_jsonstr(obj) {
  74. obj["temperature"] = 0.7;
  75. return JSON.stringify(obj);
  76. }
  77. /**
  78. * Return a string form of json object suitable for chat/completions
  79. */
  80. request_messages_jsonstr() {
  81. let req = {
  82. messages: this.xchat,
  83. }
  84. return this.request_jsonstr(req);
  85. }
  86. /**
  87. * Return a string form of json object suitable for /completions
  88. */
  89. request_prompt_jsonstr() {
  90. let prompt = "";
  91. for(const chat of this.xchat) {
  92. prompt += `${chat.role}: ${chat.content}\n`;
  93. }
  94. let req = {
  95. prompt: prompt,
  96. }
  97. return this.request_jsonstr(req);
  98. }
  99. /**
  100. * Allow setting of system prompt, but only at begining.
  101. * @param {string} sysPrompt
  102. * @param {string} msgTag
  103. */
  104. add_system_begin(sysPrompt, msgTag) {
  105. if (this.xchat.length == 0) {
  106. if (sysPrompt.length > 0) {
  107. return this.add(Roles.System, sysPrompt);
  108. }
  109. } else {
  110. if (sysPrompt.length > 0) {
  111. if (this.xchat[0].role !== Roles.System) {
  112. console.error(`ERRR:SimpleChat:SC:${msgTag}:You need to specify system prompt before any user query, ignoring...`);
  113. } else {
  114. if (this.xchat[0].content !== sysPrompt) {
  115. console.error(`ERRR:SimpleChat:SC:${msgTag}:You cant change system prompt, mid way through, ignoring...`);
  116. }
  117. }
  118. }
  119. }
  120. return false;
  121. }
  122. /**
  123. * Allow setting of system prompt, at any time.
  124. * @param {string} sysPrompt
  125. * @param {string} msgTag
  126. */
  127. add_system_anytime(sysPrompt, msgTag) {
  128. if (sysPrompt.length <= 0) {
  129. return false;
  130. }
  131. if (this.iLastSys < 0) {
  132. return this.add(Roles.System, sysPrompt);
  133. }
  134. let lastSys = this.xchat[this.iLastSys].content;
  135. if (lastSys !== sysPrompt) {
  136. return this.add(Roles.System, sysPrompt);
  137. }
  138. return false;
  139. }
  140. /**
  141. * Retrieve the latest system prompt.
  142. */
  143. get_system_latest() {
  144. if (this.iLastSys == -1) {
  145. return "";
  146. }
  147. let sysPrompt = this.xchat[this.iLastSys].content;
  148. return sysPrompt;
  149. }
  150. }
  151. let gBaseURL = "http://127.0.0.1:8080";
  152. let gChatURL = {
  153. 'chat': `${gBaseURL}/chat/completions`,
  154. 'completion': `${gBaseURL}/completions`,
  155. }
  156. const gbCompletionFreshChatAlways = true;
  157. /**
  158. * Set the class of the children, based on whether it is the idSelected or not.
  159. * @param {HTMLDivElement} elBase
  160. * @param {string} idSelected
  161. * @param {string} classSelected
  162. * @param {string} classUnSelected
  163. */
  164. function el_children_config_class(elBase, idSelected, classSelected, classUnSelected="") {
  165. for(let child of elBase.children) {
  166. if (child.id == idSelected) {
  167. child.className = classSelected;
  168. } else {
  169. child.className = classUnSelected;
  170. }
  171. }
  172. }
  173. /**
  174. * Create button and set it up.
  175. * @param {string} id
  176. * @param {(this: HTMLButtonElement, ev: MouseEvent) => any} callback
  177. * @param {string | undefined} name
  178. * @param {string | undefined} innerText
  179. */
  180. function el_create_button(id, callback, name=undefined, innerText=undefined) {
  181. if (!name) {
  182. name = id;
  183. }
  184. if (!innerText) {
  185. innerText = id;
  186. }
  187. let btn = document.createElement("button");
  188. btn.id = id;
  189. btn.name = name;
  190. btn.innerText = innerText;
  191. btn.addEventListener("click", callback);
  192. return btn;
  193. }
  194. class MultiChatUI {
  195. constructor() {
  196. /** @type {Object<string, SimpleChat>} */
  197. this.simpleChats = {};
  198. /** @type {string} */
  199. this.curChatId = "";
  200. // the ui elements
  201. this.elInSystem = /** @type{HTMLInputElement} */(document.getElementById("system-in"));
  202. this.elDivChat = /** @type{HTMLDivElement} */(document.getElementById("chat-div"));
  203. this.elBtnUser = /** @type{HTMLButtonElement} */(document.getElementById("user-btn"));
  204. this.elInUser = /** @type{HTMLInputElement} */(document.getElementById("user-in"));
  205. this.elSelectApiEP = /** @type{HTMLSelectElement} */(document.getElementById("api-ep"));
  206. this.elDivSessions = /** @type{HTMLDivElement} */(document.getElementById("sessions-div"));
  207. this.validate_element(this.elInSystem, "system-in");
  208. this.validate_element(this.elDivChat, "chat-div");
  209. this.validate_element(this.elInUser, "user-in");
  210. this.validate_element(this.elSelectApiEP, "api-ep");
  211. this.validate_element(this.elDivChat, "sessions-div");
  212. }
  213. /**
  214. * Check if the element got
  215. * @param {HTMLElement | null} el
  216. * @param {string} msgTag
  217. */
  218. validate_element(el, msgTag) {
  219. if (el == null) {
  220. throw Error(`ERRR:SimpleChat:MCUI:${msgTag} element missing in html...`);
  221. } else {
  222. console.debug(`INFO:SimpleChat:MCUI:${msgTag} Id[${el.id}] Name[${el["name"]}]`);
  223. }
  224. }
  225. /**
  226. * Reset user input ui.
  227. * * clear user input
  228. * * enable user input
  229. * * set focus to user input
  230. */
  231. ui_reset_userinput() {
  232. this.elInUser.value = "";
  233. this.elInUser.disabled = false;
  234. this.elInUser.focus();
  235. }
  236. /**
  237. * Setup the needed callbacks wrt UI, curChatId to defaultChatId and
  238. * optionally switch to specified defaultChatId.
  239. * @param {string} defaultChatId
  240. * @param {boolean} bSwitchSession
  241. */
  242. setup_ui(defaultChatId, bSwitchSession=false) {
  243. this.curChatId = defaultChatId;
  244. if (bSwitchSession) {
  245. this.handle_session_switch(this.curChatId);
  246. }
  247. this.elBtnUser.addEventListener("click", (ev)=>{
  248. if (this.elInUser.disabled) {
  249. return;
  250. }
  251. this.handle_user_submit(this.curChatId, this.elSelectApiEP.value).catch((/** @type{Error} */reason)=>{
  252. let msg = `ERRR:SimpleChat\nMCUI:HandleUserSubmit:${this.curChatId}\n${reason.name}:${reason.message}`;
  253. console.debug(msg.replace("\n", ":"));
  254. alert(msg);
  255. this.ui_reset_userinput();
  256. });
  257. });
  258. this.elInUser.addEventListener("keyup", (ev)=> {
  259. // allow user to insert enter into their message using shift+enter.
  260. // while just pressing enter key will lead to submitting.
  261. if ((ev.key === "Enter") && (!ev.shiftKey)) {
  262. this.elBtnUser.click();
  263. ev.preventDefault();
  264. }
  265. });
  266. this.elInSystem.addEventListener("keyup", (ev)=> {
  267. // allow user to insert enter into the system prompt using shift+enter.
  268. // while just pressing enter key will lead to setting the system prompt.
  269. if ((ev.key === "Enter") && (!ev.shiftKey)) {
  270. let chat = this.simpleChats[this.curChatId];
  271. chat.add_system_anytime(this.elInSystem.value, this.curChatId);
  272. chat.show(this.elDivChat);
  273. ev.preventDefault();
  274. }
  275. });
  276. }
  277. /**
  278. * Setup a new chat session and optionally switch to it.
  279. * @param {string} chatId
  280. * @param {boolean} bSwitchSession
  281. */
  282. new_chat_session(chatId, bSwitchSession=false) {
  283. this.simpleChats[chatId] = new SimpleChat();
  284. if (bSwitchSession) {
  285. this.handle_session_switch(chatId);
  286. }
  287. }
  288. /**
  289. * Handle user query submit request, wrt specified chat session.
  290. * @param {string} chatId
  291. * @param {string} apiEP
  292. */
  293. async handle_user_submit(chatId, apiEP) {
  294. let chat = this.simpleChats[chatId];
  295. chat.add_system_anytime(this.elInSystem.value, chatId);
  296. let content = this.elInUser.value;
  297. if (!chat.add(Roles.User, content)) {
  298. console.debug(`WARN:SimpleChat:MCUI:${chatId}:HandleUserSubmit:Ignoring empty user input...`);
  299. return;
  300. }
  301. chat.show(this.elDivChat);
  302. let theBody;
  303. let theUrl = gChatURL[apiEP]
  304. if (apiEP == ApiEP.Chat) {
  305. theBody = chat.request_messages_jsonstr();
  306. } else {
  307. theBody = chat.request_prompt_jsonstr();
  308. }
  309. this.elInUser.value = "working...";
  310. this.elInUser.disabled = true;
  311. console.debug(`DBUG:SimpleChat:MCUI:${chatId}:HandleUserSubmit:${theUrl}:ReqBody:${theBody}`);
  312. let resp = await fetch(theUrl, {
  313. method: "POST",
  314. headers: {
  315. "Content-Type": "application/json",
  316. },
  317. body: theBody,
  318. });
  319. let respBody = await resp.json();
  320. console.debug(`DBUG:SimpleChat:MCUI:${chatId}:HandleUserSubmit:RespBody:${JSON.stringify(respBody)}`);
  321. let assistantMsg;
  322. if (apiEP == ApiEP.Chat) {
  323. assistantMsg = respBody["choices"][0]["message"]["content"];
  324. } else {
  325. try {
  326. assistantMsg = respBody["choices"][0]["text"];
  327. } catch {
  328. assistantMsg = respBody["content"];
  329. }
  330. }
  331. chat.add(Roles.Assistant, assistantMsg);
  332. if (chatId == this.curChatId) {
  333. chat.show(this.elDivChat);
  334. } else {
  335. console.debug(`DBUG:SimpleChat:MCUI:HandleUserSubmit:ChatId has changed:[${chatId}] [${this.curChatId}]`);
  336. }
  337. // Purposefully clear at end rather than begin of this function
  338. // so that one can switch from chat to completion mode and sequece
  339. // in a completion mode with multiple user-assistant chat data
  340. // from before to be sent/occur once.
  341. if ((apiEP == ApiEP.Completion) && (gbCompletionFreshChatAlways)) {
  342. chat.xchat.length = 0;
  343. }
  344. this.ui_reset_userinput();
  345. }
  346. /**
  347. * Show buttons for NewChat and available chat sessions, in the passed elDiv.
  348. * If elDiv is undefined/null, then use this.elDivSessions.
  349. * Take care of highlighting the selected chat-session's btn.
  350. * @param {HTMLDivElement | undefined} elDiv
  351. */
  352. show_sessions(elDiv=undefined) {
  353. if (!elDiv) {
  354. elDiv = this.elDivSessions;
  355. }
  356. elDiv.replaceChildren();
  357. // Btn for creating new chat session
  358. let btnNew = el_create_button("New CHAT", (ev)=> {
  359. if (this.elInUser.disabled) {
  360. console.error(`ERRR:SimpleChat:MCUI:NewChat:Current session [${this.curChatId}] awaiting response, ignoring request...`);
  361. alert("ERRR:SimpleChat\nMCUI:NewChat\nWait for response to pending query, before starting new chat session");
  362. return;
  363. }
  364. let chatId = `Chat${Object.keys(this.simpleChats).length}`;
  365. let chatIdGot = prompt("INFO:SimpleChat\nMCUI:NewChat\nEnter id for new chat session", chatId);
  366. if (!chatIdGot) {
  367. console.error("ERRR:SimpleChat:MCUI:NewChat:Skipping based on user request...");
  368. return;
  369. }
  370. this.new_chat_session(chatIdGot, true);
  371. this.create_session_btn(elDiv, chatIdGot);
  372. el_children_config_class(elDiv, chatIdGot, "session-selected", "");
  373. });
  374. elDiv.appendChild(btnNew);
  375. // Btns for existing chat sessions
  376. let chatIds = Object.keys(this.simpleChats);
  377. for(let cid of chatIds) {
  378. let btn = this.create_session_btn(elDiv, cid);
  379. if (cid == this.curChatId) {
  380. btn.className = "session-selected";
  381. }
  382. }
  383. }
  384. create_session_btn(elDiv, cid) {
  385. let btn = el_create_button(cid, (ev)=>{
  386. let target = /** @type{HTMLButtonElement} */(ev.target);
  387. console.debug(`DBUG:SimpleChat:MCUI:SessionClick:${target.id}`);
  388. if (this.elInUser.disabled) {
  389. console.error(`ERRR:SimpleChat:MCUI:SessionClick:${target.id}:Current session [${this.curChatId}] awaiting response, ignoring switch...`);
  390. alert("ERRR:SimpleChat\nMCUI:SessionClick\nWait for response to pending query, before switching");
  391. return;
  392. }
  393. this.handle_session_switch(target.id);
  394. el_children_config_class(elDiv, target.id, "session-selected", "");
  395. });
  396. elDiv.appendChild(btn);
  397. return btn;
  398. }
  399. /**
  400. * Switch ui to the specified chatId and set curChatId to same.
  401. * @param {string} chatId
  402. */
  403. async handle_session_switch(chatId) {
  404. let chat = this.simpleChats[chatId];
  405. if (chat == undefined) {
  406. console.error(`ERRR:SimpleChat:MCUI:HandleSessionSwitch:${chatId} missing...`);
  407. return;
  408. }
  409. this.elInSystem.value = chat.get_system_latest();
  410. this.elInUser.value = "";
  411. chat.show(this.elDivChat);
  412. this.elInUser.focus();
  413. this.curChatId = chatId;
  414. console.log(`INFO:SimpleChat:MCUI:HandleSessionSwitch:${chatId} entered...`);
  415. }
  416. }
  417. let gMuitChat;
  418. const gChatIds = [ "Default", "Other" ];
  419. function startme() {
  420. console.log("INFO:SimpleChat:StartMe:Starting...");
  421. gMuitChat = new MultiChatUI();
  422. for (let cid of gChatIds) {
  423. gMuitChat.new_chat_session(cid);
  424. }
  425. gMuitChat.setup_ui(gChatIds[0]);
  426. gMuitChat.show_sessions();
  427. }
  428. document.addEventListener("DOMContentLoaded", startme);