2024-05-22 23:23:21 +05:30
// @ts-check
// A simple completions and chat/completions test related web front end logic
// by Humans for All
2024-06-01 21:50:18 +05:30
import * as du from "./datautils.mjs" ;
import * as ui from "./ui.mjs"
2024-05-22 23:23:21 +05:30
class Roles {
static System = "system" ;
static User = "user" ;
static Assistant = "assistant" ;
}
class ApiEP {
2024-06-01 21:50:18 +05:30
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 ] } ` ;
}
2024-05-22 23:23:21 +05:30
}
2024-06-01 21:50:18 +05:30
2024-05-22 23:23:21 +05:30
let gUsageMsg = `
2024-05-26 06:26:34 +05:30
<p class="role-system">Usage</p>
<ul class="ul1">
2024-06-01 21:50:18 +05:30
<li> System prompt above, to try control ai response characteristics.</li>
2024-05-26 06:26:34 +05:30
<ul class="ul2">
2024-06-01 21:50:18 +05:30
<li> Completion mode - no system prompt normally.</li>
2024-05-26 06:26:34 +05:30
</ul>
2024-06-01 21:50:18 +05:30
<li> Use shift+enter for inserting enter/newline.</li>
2024-05-26 06:26:34 +05:30
<li> Enter your query to ai assistant below.</li>
<li> Default ContextWindow = [System, Last Query+Resp, Cur Query].</li>
<ul class="ul2">
2024-06-01 21:50:18 +05:30
<li> ChatHistInCtxt, MaxTokens, ModelCtxt window to expand</li>
2024-05-26 06:26:34 +05:30
</ul>
</ul>
2024-05-22 23:23:21 +05:30
` ;
2024-06-01 21:50:18 +05:30
2024-05-26 06:26:34 +05:30
/** @typedef {{role: string, content: string}[]} ChatMessages */
2024-06-01 21:50:18 +05:30
/** @typedef {{iLastSys: number, xchat: ChatMessages}} SimpleChatODS */
2024-05-22 23:23:21 +05:30
class SimpleChat {
2024-06-01 21:50:18 +05:30
/**
* @param {string} chatId
*/
constructor ( chatId ) {
this . chatId = chatId ;
2024-05-22 23:23:21 +05:30
/**
* Maintain in a form suitable for common LLM web service chat/completions' messages entry
2024-05-26 06:26:34 +05:30
* @type {ChatMessages}
2024-05-22 23:23:21 +05:30
*/
this . xchat = [];
this . iLastSys = - 1 ;
2024-06-01 21:50:18 +05:30
this . latestResponse = "" ;
2024-05-22 23:23:21 +05:30
}
2024-05-26 06:26:34 +05:30
clear () {
this . xchat = [];
this . iLastSys = - 1 ;
}
2024-06-01 21:50:18 +05:30
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 ;
}
2024-05-26 06:26:34 +05:30
/**
* 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 ;
}
2024-06-01 21:50:18 +05:30
/**
* 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 ;
}
2024-05-22 23:23:21 +05:30
/**
* 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 ;
}
2024-06-01 21:50:18 +05:30
this . save ();
2024-05-22 23:23:21 +05:30
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 ;
2024-05-26 06:26:34 +05:30
for ( const x of this . recent_chat ( gMe . iRecentUserMsgCnt )) {
2024-06-01 21:50:18 +05:30
let entry = ui . el_create_append_p ( ` ${ x . role } : ${ x . content } ` , div );
2024-05-22 23:23:21 +05:30
entry . className = `role- ${ x . role } ` ;
last = entry ;
}
if ( last !== undefined ) {
last . scrollIntoView ( false );
} else {
if ( bClear ) {
div . innerHTML = gUsageMsg ;
2024-06-01 21:50:18 +05:30
gMe . setup_load ( div , this );
2024-05-26 06:26:34 +05:30
gMe . show_info ( div );
2024-05-22 23:23:21 +05:30
}
}
2024-06-01 21:50:18 +05:30
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 ;
2024-05-22 23:23:21 +05:30
}
/**
2024-05-26 06:26:34 +05:30
* 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.
2024-06-01 21:50:18 +05:30
* Add optional stream flag, if required.
2024-05-22 23:23:21 +05:30
* Convert the json into string.
* @param {Object} obj
*/
2024-06-01 21:50:18 +05:30
request_jsonstr_extend ( obj ) {
2024-06-25 16:57:35 +05:30
for ( let k in gMe . apiRequestOptions ) {
obj [ k ] = gMe . apiRequestOptions [ k ];
2024-05-26 06:26:34 +05:30
}
2024-06-01 21:50:18 +05:30
if ( gMe . bStream ) {
obj [ "stream" ] = true ;
}
2024-05-22 23:23:21 +05:30
return JSON . stringify ( obj );
}
/**
* Return a string form of json object suitable for chat/completions
*/
request_messages_jsonstr () {
let req = {
2024-05-26 06:26:34 +05:30
messages : this . recent_chat ( gMe . iRecentUserMsgCnt ),
2024-05-22 23:23:21 +05:30
}
2024-06-01 21:50:18 +05:30
return this . request_jsonstr_extend ( req );
2024-05-22 23:23:21 +05:30
}
/**
* Return a string form of json object suitable for /completions
2024-05-26 06:26:34 +05:30
* @param {boolean} bInsertStandardRolePrefix Insert "<THE_ROLE>: " as prefix wrt each role's message
2024-05-22 23:23:21 +05:30
*/
2024-05-26 06:26:34 +05:30
request_prompt_jsonstr ( bInsertStandardRolePrefix ) {
2024-05-22 23:23:21 +05:30
let prompt = "" ;
2024-05-26 06:26:34 +05:30
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 } ` ;
2024-05-22 23:23:21 +05:30
}
let req = {
prompt : prompt ,
}
2024-06-01 21:50:18 +05:30
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 ;
2024-05-22 23:23:21 +05:30
}
/**
* 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 ;
}
2024-06-01 21:50:18 +05:30
/**
* 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..." );
2024-05-22 23:23:21 +05:30
}
2024-06-01 21:50:18 +05:30
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 );
}
2024-12-05 23:36:41 +02:00
if ( curLine . trim () === "[DONE]" ) {
break ;
}
2024-06-01 21:50:18 +05:30
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 ;
2024-05-22 23:23:21 +05:30
}
2024-06-01 21:50:18 +05:30
/**
* 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 );
2024-05-22 23:23:21 +05:30
}
2024-06-01 21:50:18 +05:30
/**
* 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 ;
2024-05-22 23:23:21 +05:30
}
2024-06-01 21:50:18 +05:30
2024-05-22 23:23:21 +05:30
}
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" ));
2024-06-01 21:50:18 +05:30
this . elDivHeading = /** @type{HTMLSelectElement} */ ( document . getElementById ( "heading" ));
2024-05-22 23:23:21 +05:30
this . elDivSessions = /** @type{HTMLDivElement} */ ( document . getElementById ( "sessions-div" ));
2024-06-01 21:50:18 +05:30
this . elBtnSettings = /** @type{HTMLButtonElement} */ ( document . getElementById ( "settings" ));
2024-05-22 23:23:21 +05:30
this . validate_element ( this . elInSystem , "system-in" );
this . validate_element ( this . elDivChat , "chat-div" );
this . validate_element ( this . elInUser , "user-in" );
2024-06-01 21:50:18 +05:30
this . validate_element ( this . elDivHeading , "heading" );
2024-05-22 23:23:21 +05:30
this . validate_element ( this . elDivChat , "sessions-div" );
2024-06-01 21:50:18 +05:30
this . validate_element ( this . elBtnSettings , "settings" );
2024-05-22 23:23:21 +05:30
}
/**
* 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 );
}
2024-06-01 21:50:18 +05:30
this . elBtnSettings . addEventListener ( "click" , ( ev )=>{
this . elDivChat . replaceChildren ();
gMe . show_settings ( this . elDivChat );
});
2024-05-22 23:23:21 +05:30
this . elBtnUser . addEventListener ( "click" , ( ev )=>{
if ( this . elInUser . disabled ) {
return ;
}
2024-06-01 21:50:18 +05:30
this . handle_user_submit ( this . curChatId , gMe . apiEP ). catch (( /** @type{Error} */ reason )=>{
2024-05-22 23:23:21 +05:30
let msg = `ERRR:SimpleChat\nMCUI:HandleUserSubmit: ${ this . curChatId } \n ${ reason . name } : ${ reason . message } ` ;
2024-06-01 21:50:18 +05:30
console . error ( msg . replace ( "\n" , ":" ));
2024-05-22 23:23:21 +05:30
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 )) {
2024-05-26 06:26:34 +05:30
let value = this . elInUser . value ;
this . elInUser . value = value . substring ( 0 , value . length - 1 );
2024-05-22 23:23:21 +05:30
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 )) {
2024-06-01 21:50:18 +05:30
let value = this . elInSystem . value ;
this . elInSystem . value = value . substring ( 0 , value . length - 1 );
2024-05-22 23:23:21 +05:30
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 ) {
2024-06-01 21:50:18 +05:30
this . simpleChats [ chatId ] = new SimpleChat ( chatId );
2024-05-22 23:23:21 +05:30
if ( bSwitchSession ) {
this . handle_session_switch ( chatId );
}
}
2024-05-26 06:26:34 +05:30
2024-05-22 23:23:21 +05:30
/**
* 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 ];
2024-05-26 06:26:34 +05:30
// 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.
2024-06-01 21:50:18 +05:30
if (( apiEP == ApiEP . Type . Completion ) && ( gMe . bCompletionFreshChatAlways )) {
2024-05-26 06:26:34 +05:30
chat . clear ();
}
2024-05-22 23:23:21 +05:30
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 );
2024-06-01 21:50:18 +05:30
let theUrl = ApiEP . Url ( gMe . baseURL , apiEP );
let theBody = chat . request_jsonstr ( apiEP );
2024-05-22 23:23:21 +05:30
this . elInUser . value = "working..." ;
this . elInUser . disabled = true ;
console . debug ( `DBUG:SimpleChat:MCUI: ${ chatId } :HandleUserSubmit: ${ theUrl } :ReqBody: ${ theBody } ` );
2024-06-01 21:50:18 +05:30
let theHeaders = chat . fetch_headers ( apiEP );
2024-05-22 23:23:21 +05:30
let resp = await fetch ( theUrl , {
method : "POST" ,
2024-06-01 21:50:18 +05:30
headers : theHeaders ,
2024-05-22 23:23:21 +05:30
body : theBody ,
});
2024-06-01 21:50:18 +05:30
let theResp = await chat . handle_response ( resp , apiEP , this . elDivChat );
2024-05-22 23:23:21 +05:30
if ( chatId == this . curChatId ) {
chat . show ( this . elDivChat );
2024-06-01 21:50:18 +05:30
if ( theResp . trimmed . length > 0 ) {
let p = ui . el_create_append_p ( `TRIMMED: ${ theResp . trimmed } ` , this . elDivChat );
p . className = "role-trim" ;
}
2024-05-22 23:23:21 +05:30
} 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
2024-06-01 21:50:18 +05:30
let btnNew = ui . el_create_button ( "New CHAT" , ( ev )=> {
2024-05-22 23:23:21 +05:30
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 );
2024-06-01 21:50:18 +05:30
ui . el_children_config_class ( elDiv , chatIdGot , "session-selected" , "" );
2024-05-22 23:23:21 +05:30
});
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 ) {
2024-06-01 21:50:18 +05:30
let btn = ui . el_create_button ( cid , ( ev )=>{
2024-05-22 23:23:21 +05:30
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 );
2024-06-01 21:50:18 +05:30
ui . el_children_config_class ( elDiv , target . id , "session-selected" , "" );
2024-05-22 23:23:21 +05:30
});
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...` );
}
}
2024-05-26 06:26:34 +05:30
class Me {
constructor () {
2024-06-01 21:50:18 +05:30
this . baseURL = "http://127.0.0.1:8080" ;
2024-05-26 06:26:34 +05:30
this . defaultChatIds = [ "Default" , "Other" ];
this . multiChat = new MultiChatUI ();
2024-06-01 21:50:18 +05:30
this . bStream = true ;
2024-05-26 06:26:34 +05:30
this . bCompletionFreshChatAlways = true ;
this . bCompletionInsertStandardRolePrefix = false ;
2024-06-01 21:50:18 +05:30
this . bTrimGarbage = true ;
2024-05-26 06:26:34 +05:30
this . iRecentUserMsgCnt = 2 ;
2024-06-01 21:50:18 +05:30
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
}
2024-05-26 06:26:34 +05:30
// Add needed fields wrt json object to be sent wrt LLM web services completions endpoint.
2024-06-25 16:57:35 +05:30
this . apiRequestOptions = {
2024-06-01 21:50:18 +05:30
"model" : "gpt-3.5-turbo" ,
2024-05-26 06:26:34 +05:30
"temperature" : 0.7 ,
"max_tokens" : 1024 ,
2024-06-01 21:50:18 +05:30
"n_predict" : 1024 ,
2024-06-25 16:57:35 +05:30
"cache_prompt" : false ,
2024-06-01 21:50:18 +05:30
//"frequency_penalty": 1.2,
//"presence_penalty": 1.2,
2024-05-26 06:26:34 +05:30
};
}
/**
2024-06-01 21:50:18 +05:30
* 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 );
2024-06-25 16:57:35 +05:30
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 );
2024-06-01 21:50:18 +05:30
ui . el_create_append_p ( `bCompletionFreshChatAlways: ${ this . bCompletionFreshChatAlways } ` , elDiv );
ui . el_create_append_p ( `bCompletionInsertStandardRolePrefix: ${ this . bCompletionInsertStandardRolePrefix } ` , elDiv );
}
2024-06-25 16:57:35 +05:30
ui . el_create_append_p ( `apiRequestOptions: ${ JSON . stringify ( this . apiRequestOptions , null , " - " ) } ` , elDiv );
2024-06-01 21:50:18 +05:30
ui . el_create_append_p ( `headers: ${ JSON . stringify ( this . headers , null , " - " ) } ` , elDiv );
}
/**
2024-06-25 16:57:35 +05:30
* Auto create ui input elements for fields in apiRequestOptions
2024-06-01 21:50:18 +05:30
* Currently supports text and number field types.
2024-05-26 06:26:34 +05:30
* @param {HTMLDivElement} elDiv
*/
2024-06-25 16:57:35 +05:30
show_settings_apirequestoptions ( elDiv ) {
2024-06-01 21:50:18 +05:30
let typeDict = {
"string" : "text" ,
"number" : "number" ,
};
let fs = document . createElement ( "fieldset" );
let legend = document . createElement ( "legend" );
2024-06-25 16:57:35 +05:30
legend . innerText = "ApiRequestOptions" ;
2024-06-01 21:50:18 +05:30
fs . appendChild ( legend );
elDiv . appendChild ( fs );
2024-06-25 16:57:35 +05:30
for ( const k in this . apiRequestOptions ) {
let val = this . apiRequestOptions [ k ];
2024-06-01 21:50:18 +05:30
let type = typeof ( val );
2024-06-25 16:57:35 +05:30
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 );
2024-06-01 21:50:18 +05:30
}
}
}
2024-05-26 06:26:34 +05:30
2024-06-01 21:50:18 +05:30
/**
* Show settings ui for configurable parameters, in the passed Div element.
* @param {HTMLDivElement} elDiv
*/
show_settings ( elDiv ) {
2024-05-26 06:26:34 +05:30
2024-06-01 21:50:18 +05:30
let inp = ui . el_creatediv_input ( "SetBaseURL" , "BaseURL" , "text" , this . baseURL , ( val )=>{
this . baseURL = val ;
});
elDiv . appendChild ( inp . div );
2024-05-26 06:26:34 +05:30
2024-06-01 21:50:18 +05:30
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 );
2024-05-26 06:26:34 +05:30
2024-06-01 21:50:18 +05:30
let bb = ui . el_creatediv_boolbutton ( "SetStream" , "Stream" , { true : "[+] yes stream" , false : "[-] do oneshot" }, this . bStream , ( val )=>{
this . bStream = val ;
});
elDiv . appendChild ( bb . div );
2024-05-26 06:26:34 +05:30
2024-06-25 16:57:35 +05:30
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 );
2024-06-01 21:50:18 +05:30
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 );
2024-05-26 06:26:34 +05:30
}
}
/** @type {Me} */
let gMe ;
2024-05-22 23:23:21 +05:30
function startme () {
console . log ( "INFO:SimpleChat:StartMe:Starting..." );
2024-05-26 06:26:34 +05:30
gMe = new Me ();
2024-06-01 21:50:18 +05:30
gMe . debug_disable ();
document [ "gMe" ] = gMe ;
document [ "du" ] = du ;
2024-05-26 06:26:34 +05:30
for ( let cid of gMe . defaultChatIds ) {
gMe . multiChat . new_chat_session ( cid );
2024-05-22 23:23:21 +05:30
}
2024-05-26 06:26:34 +05:30
gMe . multiChat . setup_ui ( gMe . defaultChatIds [ 0 ], true );
gMe . multiChat . show_sessions ();
2024-05-22 23:23:21 +05:30
}
document . addEventListener ( "DOMContentLoaded" , startme );