2024-01-30 12:15:05 -06:00
const char index_html [] = R "LITERAL(
< html >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, maximum-scale=1" />
< meta name = "color-scheme" content = "light dark" >
< title > llama . cpp - chat </ title >
< style >
body {
font - family : system - ui ;
font - size : 90 % ;
}
#container {
margin : 0 em auto ;
display : flex ;
flex - direction : column ;
justify - content : space - between ;
height : 100 % ;
}
main {
margin : 3 px ;
display : flex ;
flex - direction : column ;
justify - content : space - between ;
gap : 1 em ;
flex - grow : 1 ;
overflow - y : auto ;
border : 1 px solid # ccc ;
border - radius : 5 px ;
padding : 0.5 em ;
}
body {
max - width : 600 px ;
min - width : 300 px ;
line - height : 1.2 ;
margin : 0 auto ;
padding : 0 0.5 em ;
}
p {
overflow - wrap : break - word ;
word - wrap : break - word ;
hyphens : auto ;
margin - top : 0.5 em ;
margin - bottom : 0.5 em ;
}
#write form {
margin : 1 em 0 0 0 ;
display : flex ;
flex - direction : column ;
gap : 0.5 em ;
align - items : stretch ;
}
. right {
display : flex ;
flex - direction : row ;
gap : 0.5 em ;
justify - content : flex - end ;
}
fieldset {
border : none ;
padding : 0 ;
margin : 0 ;
}
fieldset . two {
display : grid ;
grid - template : "a a" ;
gap : 1 em ;
}
fieldset . three {
display : grid ;
grid - template : "a a a" ;
gap : 1 em ;
}
details {
border : 1 px solid # aaa ;
border - radius : 4 px ;
padding : 0.5 em 0.5 em 0 ;
margin - top : 0.5 em ;
}
summary {
font - weight : bold ;
margin : - 0.5 em - 0.5 em 0 ;
padding : 0.5 em ;
cursor : pointer ;
}
details [ open ] {
padding : 0.5 em ;
}
. prob - set {
padding : 0.3 em ;
border - bottom : 1 px solid # ccc ;
}
. popover - content {
position : absolute ;
background - color : white ;
padding : 0.2 em ;
box - shadow : 0 0 10 px rgba ( 0 , 0 , 0 , 0.1 );
}
textarea {
padding : 5 px ;
flex - grow : 1 ;
width : 100 % ;
}
pre code {
display : block ;
background - color : # 222 ;
color : # ddd ;
}
code {
font - family : monospace ;
padding : 0.1 em 0.3 em ;
border - radius : 3 px ;
}
fieldset label {
margin : 0.5 em 0 ;
display : block ;
}
fieldset label . slim {
margin : 0 0.5 em ;
display : inline ;
}
header ,
footer {
text - align : center ;
}
footer {
font - size : 80 % ;
color : # 888 ;
}
. mode - chat textarea [ name = prompt ] {
height : 4.5 em ;
}
. mode - completion textarea [ name = prompt ] {
height : 10 em ;
}
[ contenteditable ] {
display : inline - block ;
white - space : pre - wrap ;
outline : 0 px solid transparent ;
}
@ keyframes loading - bg - wipe {
0 % {
background - position : 0 % ;
}
100 % {
background - position : 100 % ;
}
}
. loading {
-- loading - color - 1 : # eeeeee00 ;
-- loading - color - 2 : # eeeeeeff ;
background - size : 50 % 100 % ;
background - image : linear - gradient ( 90 deg , var ( -- loading - color - 1 ), var ( -- loading - color - 2 ), var ( -- loading - color - 1 ));
animation : loading - bg - wipe 2 s linear infinite ;
}
@ media ( prefers - color - scheme : dark ) {
. loading {
-- loading - color - 1 : # 22222200 ;
-- loading - color - 2 : # 222222ff ;
}
. popover - content {
background - color : black ;
}
}
</ style >
< script type = "module" >
import {
html , h , signal , effect , computed , render , useSignal , useEffect , useRef , Component
} from ' / index . js ' ;
import { llama } from ' / completion . js ' ;
import { SchemaConverter } from ' / json - schema - to - grammar . mjs ' ;
let selected_image = false ;
var slot_id = - 1 ;
const session = signal ({
prompt : "This is a conversation between User and Llama, a friendly chatbot. Llama is helpful, kind, honest, good at writing, and never fails to answer any requests immediately and with precision." ,
template : "{{prompt}} \n\n {{history}} \n {{char}}:" ,
historyTemplate : "{{name}}: {{message}}" ,
transcript : [],
type : "chat" , // "chat" | "completion"
char : "Llama" ,
user : "User" ,
image_selected : ''
})
const params = signal ({
n_predict : 400 ,
temperature : 0.7 ,
repeat_last_n : 256 , // 0 = disable penalty, -1 = context size
repeat_penalty : 1.18 , // 1.0 = disabled
top_k : 40 , // <= 0 to use vocab size
top_p : 0.95 , // 1.0 = disabled
min_p : 0.05 , // 0 = disabled
tfs_z : 1.0 , // 1.0 = disabled
typical_p : 1.0 , // 1.0 = disabled
presence_penalty : 0.0 , // 0.0 = disabled
frequency_penalty : 0.0 , // 0.0 = disabled
mirostat : 0 , // 0/1/2
mirostat_tau : 5 , // target entropy
mirostat_eta : 0.1 , // learning rate
grammar : '' ,
n_probs : 0 , // no completion_probabilities,
image_data : [],
cache_prompt : true ,
api_key : ''
})
/* START: Support for storing prompt templates and parameters in browsers LocalStorage */
const local_storage_storageKey = "llamacpp_server_local_storage" ;
function local_storage_setDataFromObject ( tag , content ) {
localStorage . setItem ( local_storage_storageKey + '/' + tag , JSON . stringify ( content ));
}
function local_storage_setDataFromRawText ( tag , content ) {
localStorage . setItem ( local_storage_storageKey + '/' + tag , content );
}
function local_storage_getDataAsObject ( tag ) {
const item = localStorage . getItem ( local_storage_storageKey + '/' + tag );
if ( ! item ) {
return null ;
} else {
return JSON . parse ( item );
}
}
function local_storage_getDataAsRawText ( tag ) {
const item = localStorage . getItem ( local_storage_storageKey + '/' + tag );
if ( ! item ) {
return null ;
} else {
return item ;
}
}
// create a container for user templates and settings
const savedUserTemplates = signal ({})
const selectedUserTemplate = signal ({ name : '' , template : { session : {}, params : {} } })
// let's import locally saved templates and settings if there are any
// user templates and settings are stored in one object
// in form of { "templatename": "templatedata" } and { "settingstemplatename":"settingsdata" }
console . log ( ' Importing saved templates ' )
let importedTemplates = local_storage_getDataAsObject ( ' user_templates ' )
if ( importedTemplates ) {
// saved templates were successfully imported.
console . log ( ' Processing saved templates and updating default template ' )
params . value = { ... params . value , image_data : [] };
//console.log(importedTemplates);
savedUserTemplates . value = importedTemplates ;
//override default template
savedUserTemplates . value . default = { session : session . value , params : params . value }
local_storage_setDataFromObject ( ' user_templates ' , savedUserTemplates . value )
} else {
// no saved templates detected.
console . log ( ' Initializing LocalStorage and saving default template ' )
savedUserTemplates . value = { "default" : { session : session . value , params : params . value } }
local_storage_setDataFromObject ( ' user_templates ' , savedUserTemplates . value )
}
function userTemplateResetToDefault () {
console . log ( ' Resetting template to default ' )
selectedUserTemplate . value . name = ' default ' ;
selectedUserTemplate . value . data = savedUserTemplates . value [ ' default ' ];
}
function userTemplateApply ( t ) {
session . value = t . data . session ;
session . value = { ... session . value , image_selected : '' };
params . value = t . data . params ;
params . value = { ... params . value , image_data : [] };
}
function userTemplateResetToDefaultAndApply () {
userTemplateResetToDefault ()
userTemplateApply ( selectedUserTemplate . value )
}
function userTemplateLoadAndApplyAutosaved () {
// get autosaved last used template
let lastUsedTemplate = local_storage_getDataAsObject ( ' user_templates_last ' )
if ( lastUsedTemplate ) {
console . log ( ' Autosaved template found , restoring ' )
selectedUserTemplate . value = lastUsedTemplate
}
else {
console . log ( ' No autosaved template found , using default template ' )
// no autosaved last used template was found, so load from default.
userTemplateResetToDefault ()
}
console . log ( ' Applying template ' )
// and update internal data from templates
userTemplateApply ( selectedUserTemplate . value )
}
//console.log(savedUserTemplates.value)
//console.log(selectedUserTemplate.value)
function userTemplateAutosave () {
console . log ( ' Template Autosave ... ' )
if ( selectedUserTemplate . value . name == ' default ' ) {
// we don't want to save over default template, so let's create a new one
let newTemplateName = ' UserTemplate - ' + Date . now (). toString ()
let newTemplate = { ' name ' : newTemplateName , ' data ' : { ' session ' : session . value , ' params ' : params . value } }
console . log ( ' Saving as ' + newTemplateName )
// save in the autosave slot
local_storage_setDataFromObject ( ' user_templates_last ' , newTemplate )
// and load it back and apply
userTemplateLoadAndApplyAutosaved ()
} else {
local_storage_setDataFromObject ( ' user_templates_last ' , { ' name ' : selectedUserTemplate . value . name , ' data ' : { ' session ' : session . value , ' params ' : params . value } })
}
}
console . log ( ' Checking for autosaved last used template ' )
userTemplateLoadAndApplyAutosaved ()
/* END: Support for storing prompt templates and parameters in browsers LocalStorage */
const llamaStats = signal ( null )
const controller = signal ( null )
// currently generating a completion?
const generating = computed (() => controller . value != null )
// has the user started a chat?
const chatStarted = computed (() => session . value . transcript . length > 0 )
const transcriptUpdate = ( transcript ) => {
session . value = {
... session . value ,
transcript
}
}
// simple template replace
const template = ( str , extraSettings ) => {
let settings = session . value ;
if ( extraSettings ) {
settings = { ... settings , ... extraSettings };
}
return String ( str ). replaceAll ( / \ { \ {(. *? ) \ } \ } / g , ( _ , key ) => template ( settings [ key ]));
}
async function runLlama ( prompt , llamaParams , char ) {
const currentMessages = [];
const history = session . value . transcript ;
if ( controller . value ) {
throw new Error ( "already running" );
}
controller . value = new AbortController ();
for await ( const chunk of llama ( prompt , llamaParams , { controller : controller . value })) {
const data = chunk . data ;
if ( data . stop ) {
while (
currentMessages . length > 0 &&
currentMessages [ currentMessages . length - 1 ]. content . match ( / \ n $ / ) != null
) {
currentMessages . pop ();
}
transcriptUpdate ([... history , [ char , currentMessages ]])
console . log ( "Completion finished: '" , currentMessages . map ( msg => msg . content ). join ( '' ), "', summary: " , data );
} else {
currentMessages . push ( data );
slot_id = data . slot_id ;
if ( selected_image && ! data . multimodal ) {
alert ( "The server was not compiled for multimodal or the model projector can't be loaded." );
return ;
}
transcriptUpdate ([... history , [ char , currentMessages ]])
}
if ( data . timings ) {
llamaStats . value = data ;
}
}
controller . value = null ;
}
// send message to server
const chat = async ( msg ) => {
if ( controller . value ) {
console . log ( ' already running ... ' );
return ;
}
transcriptUpdate ([... session . value . transcript , [ "{{user}}" , msg ]])
let prompt = template ( session . value . template , {
message : msg ,
history : session . value . transcript . flatMap (
([ name , data ]) =>
template (
session . value . historyTemplate ,
{
name ,
message : Array . isArray ( data ) ?
data . map ( msg => msg . content ). join ( '' ). replace ( /^ \ s / , '' ) :
data ,
}
)
). join ( " \n " ),
});
if ( selected_image ) {
prompt = ` A chat between a curious human and an artificial intelligence assistant . The assistant gives helpful , detailed , and polite answers to the human ' s questions . \ nUSER :[ img - 10 ] $ { msg } \ nASSISTANT : ` ;
}
await runLlama ( prompt , {
... params . value ,
slot_id : slot_id ,
stop : [ "</s>" , template ( "{{char}}:" ), template ( " {{ user }} : ")],
}, "{{char}}" );
}
const runCompletion = () => {
if ( controller . value ) {
console . log ( ' already running ... ' );
return ;
}
const { prompt } = session . value ;
transcriptUpdate ([... session . value . transcript , [ "" , prompt ]]);
runLlama ( prompt , {
... params . value ,
slot_id : slot_id ,
stop : [],
}, "" ). finally (() => {
session . value . prompt = session . value . transcript . map (([ _ , data ]) =>
Array . isArray ( data ) ? data . map ( msg => msg . content ). join ( '' ) : data
). join ( '' );
session . value . transcript = [];
})
}
const stop = ( e ) => {
e . preventDefault ();
if ( controller . value ) {
controller . value . abort ();
controller . value = null ;
}
}
const reset = ( e ) => {
stop ( e );
transcriptUpdate ([]);
}
const uploadImage = ( e ) => {
e . preventDefault ();
document . getElementById ( "fileInput" ). click ();
document . getElementById ( "fileInput" ). addEventListener ( "change" , function ( event ) {
const selectedFile = event . target . files [ 0 ];
if ( selectedFile ) {
const reader = new FileReader ();
reader . onload = function () {
const image_data = reader . result ;
session . value = { ... session . value , image_selected : image_data };
params . value = {
... params . value , image_data : [
{ data : image_data . replace ( / data : image \ / [ ^ ;] + ; base64 , / , '' ), id : 10 }]
}
};
selected_image = true ;
reader . readAsDataURL ( selectedFile );
}
});
}
function MessageInput () {
const message = useSignal ( "" )
const submit = ( e ) => {
stop ( e );
chat ( message . value );
message . value = "" ;
}
const enterSubmits = ( event ) => {
if ( event . which === 13 && ! event . shiftKey ) {
submit ( event );
}
}
return html `
< form onsubmit = $ { submit } >
< div >
< textarea
className = $ { generating . value ? "loading" : null }
oninput = $ {( e ) => message . value = e . target . value }
onkeypress = $ { enterSubmits }
placeholder = "Say something..."
rows = 2
type = "text"
value = "${message}"
/>
</ div >
< div class =" right ">
< button type = "submit" disabled = $ { generating . value } > Send </ button >
< button onclick = $ { uploadImage } > Upload Image </ button >
< button onclick = $ { stop } disabled = $ { ! generating . value } > Stop </ button >
< button onclick = $ { reset } > Reset </ button >
</ div >
</ form >
`
}
function CompletionControls () {
const submit = ( e ) => {
stop ( e );
runCompletion ();
}
return html `
< div >
< button onclick = $ { submit } type = "button" disabled = $ { generating . value } > Start </ button >
< button onclick = $ { stop } disabled = $ { ! generating . value } > Stop </ button >
< button onclick = $ { reset } > Reset </ button >
</ div > ` ;
}
const ChatLog = ( props ) => {
const messages = session . value . transcript ;
const container = useRef ( null )
useEffect (() => {
// scroll to bottom (if needed)
const parent = container . current . parentElement ;
if ( parent && parent . scrollHeight <= parent . scrollTop + parent . offsetHeight + 300 ) {
parent . scrollTo ( 0 , parent . scrollHeight )
}
}, [ messages ])
const isCompletionMode = session . value . type === ' completion '
const chatLine = ([ user , data ], index ) => {
let message
const isArrayMessage = Array . isArray ( data )
if ( params . value . n_probs > 0 && isArrayMessage ) {
message = html ` < $ { Probabilities } data = $ { data } /> `
} else {
const text = isArrayMessage ?
data . map ( msg => msg . content ). join ( '' ). replace ( /^ \ s +/ , '' ) :
data ;
message = isCompletionMode ?
text :
html ` < $ { Markdownish } text = $ { template ( text )} /> `
}
if ( user ) {
return html ` < p key = $ { index } >< strong > $ { template ( user )} :</ strong > $ { message } </ p > `
} else {
return isCompletionMode ?
html ` < span key = $ { index } > $ { message } </ span > ` :
html ` < p key = $ { index } > $ { message } </ p > `
}
};
const handleCompletionEdit = ( e ) => {
session . value . prompt = e . target . innerText ;
session . value . transcript = [];
}
return html `
< div id = "chat" ref = $ { container } key = $ { messages . length } >
< img style = "width: 60%;${!session.value.image_selected ? `display: none;` : ``}" src = "${session.value.image_selected}" />
< span contenteditable = $ { isCompletionMode } ref = $ { container } oninput = $ { handleCompletionEdit } >
$ { messages . flatMap ( chatLine )}
</ span >
</ div > ` ;
};
const ConfigForm = ( props ) => {
const updateSession = ( el ) => session . value = { ... session . value , [ el . target . name ] : el . target . value }
const updateParams = ( el ) => params . value = { ... params . value , [ el . target . name ] : el . target . value }
const updateParamsFloat = ( el ) => params . value = { ... params . value , [ el . target . name ] : parseFloat ( el . target . value ) }
const updateParamsInt = ( el ) => params . value = { ... params . value , [ el . target . name ] : Math . floor ( parseFloat ( el . target . value )) }
const grammarJsonSchemaPropOrder = signal ( '' )
const updateGrammarJsonSchemaPropOrder = ( el ) => grammarJsonSchemaPropOrder . value = el . target . value
const convertJSONSchemaGrammar = () => {
try {
const schema = JSON . parse ( params . value . grammar )
const converter = new SchemaConverter (
grammarJsonSchemaPropOrder . value
. split ( ',' )
. reduce (( acc , cur , i ) => ({ ... acc , [ cur . trim ()] : i }), {})
)
converter . visit ( schema , '' )
params . value = {
... params . value ,
grammar : converter . formatGrammar (),
}
} catch ( e ) {
alert ( ` Convert failed : $ { e . message } ` )
}
}
const FloatField = ({ label , max , min , name , step , value }) => {
return html `
< div >
< label for = "${name}" > $ { label } </ label >
< input type = "range" id = "${name}" min = "${min}" max = "${max}" step = "${step}" name = "${name}" value = "${value}" oninput = $ { updateParamsFloat } />
< span > $ { value } </ span >
</ div >
`
};
const IntField = ({ label , max , min , name , value }) => {
return html `
< div >
< label for = "${name}" > $ { label } </ label >
< input type = "range" id = "${name}" min = "${min}" max = "${max}" name = "${name}" value = "${value}" oninput = $ { updateParamsInt } />
< span > $ { value } </ span >
</ div >
`
};
const userTemplateReset = ( e ) => {
e . preventDefault ();
userTemplateResetToDefaultAndApply ()
}
const UserTemplateResetButton = () => {
if ( selectedUserTemplate . value . name == ' default ' ) {
return html `
< button disabled > Using default template </ button >
`
}
return html `
< button onclick = $ { userTemplateReset } > Reset all to default </ button >
`
};
useEffect (() => {
// autosave template on every change
userTemplateAutosave ()
}, [ session . value , params . value ])
const GrammarControl = () => (
html `
< div >
< label for = "template" > Grammar </ label >
< textarea id = "grammar" name = "grammar" placeholder = "Use gbnf or JSON Schema+convert" value = "${params.value.grammar}" rows = 4 oninput = $ { updateParams } />
< input type = "text" name = "prop-order" placeholder = "order: prop1,prop2,prop3" oninput = $ { updateGrammarJsonSchemaPropOrder } />
< button type = "button" onclick = $ { convertJSONSchemaGrammar } > Convert JSON Schema </ button >
</ div >
`
);
const PromptControlFieldSet = () => (
html `
< fieldset >
< div >
< label htmlFor = "prompt" > Prompt </ label >
< textarea type = "text" name = "prompt" value = "${session.value.prompt}" oninput = $ { updateSession } />
</ div >
</ fieldset >
`
);
const ChatConfigForm = () => (
html `
$ { PromptControlFieldSet ()}
< fieldset class =" two ">
< div >
< label for = "user" > User name </ label >
< input type = "text" name = "user" value = "${session.value.user}" oninput = $ { updateSession } />
</ div >
< div >
< label for = "bot" > Bot name </ label >
< input type = "text" name = "char" value = "${session.value.char}" oninput = $ { updateSession } />
</ div >
</ fieldset >
< fieldset >
< div >
< label for = "template" > Prompt template </ label >
< textarea id = "template" name = "template" value = "${session.value.template}" rows = 4 oninput = $ { updateSession } />
</ div >
< div >
< label for = "template" > Chat history template </ label >
< textarea id = "template" name = "historyTemplate" value = "${session.value.historyTemplate}" rows = 1 oninput = $ { updateSession } />
</ div >
$ { GrammarControl ()}
</ fieldset >
`
);
const CompletionConfigForm = () => (
html `
$ { PromptControlFieldSet ()}
< fieldset > $ { GrammarControl ()} </ fieldset >
`
);
return html `
< form >
< fieldset class =" two ">
< $ { UserTemplateResetButton } />
< div >
< label class =" slim "><input type=" radio " name=" type " value=" chat " checked=${session.value.type === " chat "} oninput=${updateSession} /> Chat</label>
< label class =" slim "><input type=" radio " name=" type " value=" completion " checked=${session.value.type === " completion "} oninput=${updateSession} /> Completion</label>
</ div >
</ fieldset >
$ { session . value . type === ' chat ' ? ChatConfigForm () : CompletionConfigForm ()}
< fieldset class =" two ">
$ { IntField ({ label : "Predictions" , max : 2048 , min : - 1 , name : "n_predict" , value : params . value . n_predict })}
$ { FloatField ({ label : "Temperature" , max : 2.0 , min : 0.0 , name : "temperature" , step : 0.01 , value : params . value . temperature })}
$ { FloatField ({ label : "Penalize repeat sequence" , max : 2.0 , min : 0.0 , name : "repeat_penalty" , step : 0.01 , value : params . value . repeat_penalty })}
$ { IntField ({ label : "Consider N tokens for penalize" , max : 2048 , min : 0 , name : "repeat_last_n" , value : params . value . repeat_last_n })}
$ { IntField ({ label : "Top-K sampling" , max : 100 , min : - 1 , name : "top_k" , value : params . value . top_k })}
$ { FloatField ({ label : "Top-P sampling" , max : 1.0 , min : 0.0 , name : "top_p" , step : 0.01 , value : params . value . top_p })}
$ { FloatField ({ label : "Min-P sampling" , max : 1.0 , min : 0.0 , name : "min_p" , step : 0.01 , value : params . value . min_p })}
</ fieldset >
< details >
< summary > More options </ summary >
< fieldset class =" two ">
$ { FloatField ({ label : "TFS-Z" , max : 1.0 , min : 0.0 , name : "tfs_z" , step : 0.01 , value : params . value . tfs_z })}
$ { FloatField ({ label : "Typical P" , max : 1.0 , min : 0.0 , name : "typical_p" , step : 0.01 , value : params . value . typical_p })}
$ { FloatField ({ label : "Presence penalty" , max : 1.0 , min : 0.0 , name : "presence_penalty" , step : 0.01 , value : params . value . presence_penalty })}
$ { FloatField ({ label : "Frequency penalty" , max : 1.0 , min : 0.0 , name : "frequency_penalty" , step : 0.01 , value : params . value . frequency_penalty })}
</ fieldset >
< hr />
< fieldset class =" three ">
< div >
< label >< input type = "radio" name = "mirostat" value = "0" checked = $ { params . value . mirostat == 0 } oninput = $ { updateParamsInt } /> no Mirostat </ label >
< label >< input type = "radio" name = "mirostat" value = "1" checked = $ { params . value . mirostat == 1 } oninput = $ { updateParamsInt } /> Mirostat v1 </ label >
< label >< input type = "radio" name = "mirostat" value = "2" checked = $ { params . value . mirostat == 2 } oninput = $ { updateParamsInt } /> Mirostat v2 </ label >
</ div >
$ { FloatField ({ label : "Mirostat tau" , max : 10.0 , min : 0.0 , name : "mirostat_tau" , step : 0.01 , value : params . value . mirostat_tau })}
$ { FloatField ({ label : "Mirostat eta" , max : 1.0 , min : 0.0 , name : "mirostat_eta" , step : 0.01 , value : params . value . mirostat_eta })}
</ fieldset >
< fieldset >
$ { IntField ({ label : "Show Probabilities" , max : 10 , min : 0 , name : "n_probs" , value : params . value . n_probs })}
</ fieldset >
< fieldset >
< label for = "api_key" > API Key </ label >
< input type = "text" name = "api_key" value = "${params.value.api_key}" placeholder = "Enter API key" oninput = $ { updateParams } />
</ fieldset >
</ details >
</ form >
`
}
const probColor = ( p ) => {
const r = Math . floor ( 192 * ( 1 - p ));
const g = Math . floor ( 192 * p );
return ` rgba ( $ { r }, $ { g }, 0 , 0.3 ) ` ;
}
const Probabilities = ( params ) => {
return params . data . map ( msg => {
const { completion_probabilities } = msg ;
if (
! completion_probabilities ||
completion_probabilities . length === 0
) return msg . content
if ( completion_probabilities . length > 1 ) {
// Not for byte pair
if ( completion_probabilities [ 0 ]. content . startsWith ( ' byte : \\' )) return msg . content
const splitData = completion_probabilities . map ( prob => ({
content : prob . content ,
completion_probabilities : [ prob ]
}))
return html ` < $ { Probabilities } data = $ { splitData } /> `
}
const { probs , content } = completion_probabilities [ 0 ]
const found = probs . find ( p => p . tok_str === msg . content )
const pColor = found ? probColor ( found . prob ) : ' transparent '
const popoverChildren = html `
< div class =" prob - set ">
$ { probs . map (( p , index ) => {
return html `
< div
key = $ { index }
title = $ { ` prob : $ { p . prob } ` }
style = $ {{
padding : ' 0.3 em ' ,
backgroundColor : p . tok_str === content ? probColor ( p . prob ) : ' transparent '
}}
>
< span > $ { p . tok_str } : </ span >
< span > $ { Math . floor ( p . prob * 100 )} %</ span >
</ div >
`
})}
</ div >
`
return html `
< $ { Popover } style = $ {{ backgroundColor : pColor }} popoverChildren = $ { popoverChildren } >
$ { msg . content . match ( / \ n / gim ) ? html ` < br /> ` : msg . content }
</>
`
});
}
// poor mans markdown replacement
const Markdownish = ( params ) => {
const md = params . text
. replace ( /&/ g , ' & amp ; ' )
. replace ( /</ g , ' & lt ; ' )
. replace ( />/ g , ' & gt ; ' )
. replace ( /^ # { 1 , 6 } (. * ) $ / gim , ' < h3 > $ 1 </ h3 > ' )
. replace ( / \ * \ * (. *? ) \ * \*/ g , ' < strong > $ 1 </ strong > ' )
. replace ( / __ (. *? ) __ / g , ' < strong > $ 1 </ strong > ' )
. replace ( / \ * (. *? ) \*/ g , ' < em > $ 1 </ em > ' )
. replace ( / _ (. *? ) _ / g , ' < em > $ 1 </ em > ' )
. replace ( / ``` . *? \ n ([ \ s \ S ] *? ) ``` / g , ' < pre >< code > $ 1 </ code ></ pre > ' )
. replace ( / ` (. *? ) ` / g , ' < code > $ 1 </ code > ' )
. replace ( / \ n / gim , ' < br /> ' );
return html ` < span dangerouslySetInnerHTML = $ {{ __html : md }} /> ` ;
};
const ModelGenerationInfo = ( params ) => {
if ( ! llamaStats . value ) {
return html ` < span /> `
}
return html `
< span >
$ { llamaStats . value . tokens_predicted } predicted , $ { llamaStats . value . tokens_cached } cached , $ { llamaStats . value . timings . predicted_per_token_ms . toFixed ()} ms per token , $ { llamaStats . value . timings . predicted_per_second . toFixed ( 2 )} tokens per second
</ span >
`
}
// simple popover impl
const Popover = ( props ) => {
const isOpen = useSignal ( false );
const position = useSignal ({ top : ' 0 px ' , left : ' 0 px ' });
const buttonRef = useRef ( null );
const popoverRef = useRef ( null );
const togglePopover = () => {
if ( buttonRef . current ) {
const rect = buttonRef . current . getBoundingClientRect ();
position . value = {
top : `$ { rect . bottom + window . scrollY } px ` ,
left : `$ { rect . left + window . scrollX } px ` ,
};
}
isOpen . value = ! isOpen . value ;
};
const handleClickOutside = ( event ) => {
if ( popoverRef . current && ! popoverRef . current . contains ( event . target ) && ! buttonRef . current . contains ( event . target )) {
isOpen . value = false ;
}
};
useEffect (() => {
document . addEventListener ( ' mousedown ' , handleClickOutside );
return () => {
document . removeEventListener ( ' mousedown ' , handleClickOutside );
};
}, []);
return html `
< span style = $ { props . style } ref = $ { buttonRef } onClick = $ { togglePopover } > $ { props . children } </ span >
$ { isOpen . value && html `
< $ { Portal } into = "#portal" >
< div
ref = $ { popoverRef }
class =" popover - content "
style = $ {{
top : position . value . top ,
left : position . value . left ,
}}
>
$ { props . popoverChildren }
</ div >
</ $ { Portal } >
` }
` ;
};
// Source: preact-portal (https://github.com/developit/preact-portal/blob/master/src/preact-portal.js)
/** Redirect rendering of descendants into the given CSS selector */
class Portal extends Component {
componentDidUpdate ( props ) {
for ( let i in props ) {
if ( props [ i ] !== this . props [ i ]) {
return setTimeout ( this . renderLayer );
}
}
}
componentDidMount () {
this . isMounted = true ;
this . renderLayer = this . renderLayer . bind ( this );
this . renderLayer ();
}
componentWillUnmount () {
this . renderLayer ( false );
this . isMounted = false ;
if ( this . remote && this . remote . parentNode ) this . remote . parentNode . removeChild ( this . remote );
}
findNode ( node ) {
return typeof node === ' string ' ? document . querySelector ( node ) : node ;
}
renderLayer ( show = true ) {
if ( ! this . isMounted ) return ;
// clean up old node if moving bases:
if ( this . props . into !== this . intoPointer ) {
this . intoPointer = this . props . into ;
if ( this . into && this . remote ) {
this . remote = render ( html ` < $ { PortalProxy } /> ` , this . into , this . remote );
}
this . into = this . findNode ( this . props . into );
}
this . remote = render ( html `
< $ { PortalProxy } context = $ { this . context } >
$ { show && this . props . children || null }
</ $ { PortalProxy } >
` , this . into , this . remote );
}
render () {
return null ;
}
}
// high-order component that renders its first child if it exists.
// used as a conditional rendering proxy.
class PortalProxy extends Component {
getChildContext () {
return this . props . context ;
}
render ({ children }) {
return children || null ;
}
}
function App ( props ) {
return html `
< div class =" mode - $ { session . value . type } ">
< header >
< h1 > llama . cpp </ h1 >
</ header >
< main id = "content" >
< $ { chatStarted . value ? ChatLog : ConfigForm } />
</ main >
< section id = "write" >
< $ { session . value . type === ' chat ' ? MessageInput : CompletionControls } />
</ section >
< footer >
< p >< $ { ModelGenerationInfo } /></ p >
< p > Powered by < a href = "https://github.com/ggerganov/llama.cpp" > llama . cpp </ a > and < a href = "https://ggml.ai" > ggml . ai </ a > . </ p >
</ footer >
</ div >
` ;
}
render ( h ( App ), document . querySelector ( '# container ' ));
</ script >
</ head >
< body >
< div id = "container" >
< input type = "file" id = "fileInput" accept = "image/*" style = "display: none;" >
</ div >
< div id = "portal" ></ div >
</ body >
</ html >
) LITERAL ";
unsigned int index_html_len = sizeof ( index_html );