index.html 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  1. <html>
  2. <head>
  3. <meta charset="UTF-8">
  4. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  5. <meta name="color-scheme" content="light dark">
  6. <title>llama.cpp - chat</title>
  7. <style>
  8. body {
  9. font-family: system-ui;
  10. font-size: 90%;
  11. }
  12. #container {
  13. margin: 0em auto;
  14. display: flex;
  15. flex-direction: column;
  16. justify-content: space-between;
  17. height: 100%;
  18. }
  19. main {
  20. margin: 3px;
  21. display: flex;
  22. flex-direction: column;
  23. justify-content: space-between;
  24. gap: 1em;
  25. flex-grow: 1;
  26. overflow-y: auto;
  27. border: 1px solid #ccc;
  28. border-radius: 5px;
  29. padding: 0.5em;
  30. }
  31. body {
  32. max-width: 600px;
  33. min-width: 300px;
  34. line-height: 1.2;
  35. margin: 0 auto;
  36. padding: 0 0.5em;
  37. }
  38. p {
  39. overflow-wrap: break-word;
  40. word-wrap: break-word;
  41. hyphens: auto;
  42. margin-top: 0.5em;
  43. margin-bottom: 0.5em;
  44. }
  45. #write form {
  46. margin: 1em 0 0 0;
  47. display: flex;
  48. flex-direction: column;
  49. gap: 0.5em;
  50. align-items: stretch;
  51. }
  52. .right {
  53. display: flex;
  54. flex-direction: row;
  55. gap: 0.5em;
  56. justify-content: flex-end;
  57. }
  58. fieldset {
  59. border: none;
  60. padding: 0;
  61. margin: 0;
  62. }
  63. fieldset.two {
  64. display: grid;
  65. grid-template: "a a";
  66. gap: 1em;
  67. }
  68. fieldset.three {
  69. display: grid;
  70. grid-template: "a a a";
  71. gap: 1em;
  72. }
  73. details {
  74. border: 1px solid #aaa;
  75. border-radius: 4px;
  76. padding: 0.5em 0.5em 0;
  77. margin-top: 0.5em;
  78. }
  79. summary {
  80. font-weight: bold;
  81. margin: -0.5em -0.5em 0;
  82. padding: 0.5em;
  83. cursor: pointer;
  84. }
  85. details[open] {
  86. padding: 0.5em;
  87. }
  88. .prob-set {
  89. padding: 0.3em;
  90. border-bottom: 1px solid #ccc;
  91. }
  92. .popover-content {
  93. position: absolute;
  94. background-color: white;
  95. padding: 0.2em;
  96. box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  97. }
  98. textarea {
  99. padding: 5px;
  100. flex-grow: 1;
  101. width: 100%;
  102. }
  103. pre code {
  104. display: block;
  105. background-color: #222;
  106. color: #ddd;
  107. }
  108. code {
  109. font-family: monospace;
  110. padding: 0.1em 0.3em;
  111. border-radius: 3px;
  112. }
  113. fieldset label {
  114. margin: 0.5em 0;
  115. display: block;
  116. }
  117. header, footer {
  118. text-align: center;
  119. }
  120. footer {
  121. font-size: 80%;
  122. color: #888;
  123. }
  124. @keyframes loading-bg-wipe {
  125. 0% {
  126. background-position: 0%;
  127. }
  128. 100% {
  129. background-position: 100%;
  130. }
  131. }
  132. .loading {
  133. --loading-color-1: #eeeeee00;
  134. --loading-color-2: #eeeeeeff;
  135. background-size: 50% 100%;
  136. background-image: linear-gradient(90deg, var(--loading-color-1), var(--loading-color-2), var(--loading-color-1));
  137. animation: loading-bg-wipe 2s linear infinite;
  138. }
  139. @media (prefers-color-scheme: dark) {
  140. .loading {
  141. --loading-color-1: #22222200;
  142. --loading-color-2: #222222ff;
  143. }
  144. .popover-content {
  145. background-color: black;
  146. }
  147. }
  148. </style>
  149. <script type="module">
  150. import {
  151. html, h, signal, effect, computed, render, useSignal, useEffect, useRef, Component
  152. } from '/index.js';
  153. import { llama } from '/completion.js';
  154. import { SchemaConverter } from '/json-schema-to-grammar.mjs';
  155. const session = signal({
  156. 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.",
  157. template: "{{prompt}}\n\n{{history}}\n{{char}}:",
  158. historyTemplate: "{{name}}: {{message}}",
  159. transcript: [],
  160. type: "chat",
  161. char: "Llama",
  162. user: "User",
  163. })
  164. const params = signal({
  165. n_predict: 400,
  166. temperature: 0.7,
  167. repeat_last_n: 256, // 0 = disable penalty, -1 = context size
  168. repeat_penalty: 1.18, // 1.0 = disabled
  169. top_k: 40, // <= 0 to use vocab size
  170. top_p: 0.5, // 1.0 = disabled
  171. tfs_z: 1.0, // 1.0 = disabled
  172. typical_p: 1.0, // 1.0 = disabled
  173. presence_penalty: 0.0, // 0.0 = disabled
  174. frequency_penalty: 0.0, // 0.0 = disabled
  175. mirostat: 0, // 0/1/2
  176. mirostat_tau: 5, // target entropy
  177. mirostat_eta: 0.1, // learning rate
  178. grammar: '',
  179. n_probs: 0, // no completion_probabilities
  180. })
  181. /* START: Support for storing prompt templates and parameters in borwser LocalStorage */
  182. const local_storage_storageKey = "llamacpp_server_local_storage";
  183. function local_storage_setDataFromObject(tag, content) {
  184. localStorage.setItem(local_storage_storageKey + '/' + tag, JSON.stringify(content));
  185. }
  186. function local_storage_setDataFromRawText(tag, content) {
  187. localStorage.setItem(local_storage_storageKey + '/' + tag, content);
  188. }
  189. function local_storage_getDataAsObject(tag) {
  190. const item = localStorage.getItem(local_storage_storageKey + '/' + tag);
  191. if (!item) {
  192. return null;
  193. } else {
  194. return JSON.parse(item);
  195. }
  196. }
  197. function local_storage_getDataAsRawText(tag) {
  198. const item = localStorage.getItem(local_storage_storageKey + '/' + tag);
  199. if (!item) {
  200. return null;
  201. } else {
  202. return item;
  203. }
  204. }
  205. // create a container for user templates and settings
  206. const savedUserTemplates = signal({})
  207. const selectedUserTemplate = signal({ name: '', template: { session: {}, params: {} } })
  208. // let's import locally saved templates and settings if there are any
  209. // user templates and settings are stored in one object
  210. // in form of { "templatename": "templatedata" } and { "settingstemplatename":"settingsdata" }
  211. console.log('Importing saved templates')
  212. let importedTemplates = local_storage_getDataAsObject('user_templates')
  213. if (importedTemplates) {
  214. // saved templates were successfuly imported.
  215. console.log('Processing saved templates and updating default template')
  216. //console.log(importedTemplates);
  217. savedUserTemplates.value = importedTemplates;
  218. //override default template
  219. savedUserTemplates.value.default = { session: session.value, params: params.value }
  220. local_storage_setDataFromObject('user_templates', savedUserTemplates.value)
  221. } else {
  222. // no saved templates detected.
  223. console.log('Initializing LocalStorage and saving default template')
  224. savedUserTemplates.value = { "default": { session: session.value, params: params.value } }
  225. local_storage_setDataFromObject('user_templates', savedUserTemplates.value)
  226. }
  227. function userTemplateResetToDefault() {
  228. console.log('Reseting themplate to default')
  229. selectedUserTemplate.value.name = 'default';
  230. selectedUserTemplate.value.data = savedUserTemplates.value['default'];
  231. }
  232. function userTemplateApply(t) {
  233. session.value = t.data.session;
  234. params.value = t.data.params;
  235. }
  236. function userTemplateResetToDefaultAndApply() {
  237. userTemplateResetToDefault()
  238. userTemplateApply(selectedUserTemplate.value)
  239. }
  240. function userTemplateLoadAndApplyAutosaved() {
  241. // get autosaved last used template
  242. let lastUsedTemplate = local_storage_getDataAsObject('user_templates_last')
  243. if (lastUsedTemplate) {
  244. console.log('Autosaved template found, restoring')
  245. selectedUserTemplate.value = lastUsedTemplate
  246. }
  247. else {
  248. console.log('No autosaved template found, using default template')
  249. // no autosaved last used template was found, so load from default.
  250. userTemplateResetToDefault()
  251. }
  252. console.log('Applying template')
  253. // and update internal data from templates
  254. userTemplateApply(selectedUserTemplate.value)
  255. }
  256. //console.log(savedUserTemplates.value)
  257. //console.log(selectedUserTemplate.value)
  258. function userTemplateAutosave() {
  259. console.log('Template Autosave...')
  260. if (selectedUserTemplate.value.name == 'default') {
  261. // we don't want to save over default template, so let's create a new one
  262. let newTemplateName = 'UserTemplate-' + Date.now().toString()
  263. let newTemplate = { 'name': newTemplateName, 'data': { 'session': session.value, 'params': params.value } }
  264. console.log('Saving as ' + newTemplateName)
  265. // save in the autosave slot
  266. local_storage_setDataFromObject('user_templates_last', newTemplate)
  267. // and load it back and apply
  268. userTemplateLoadAndApplyAutosaved()
  269. } else {
  270. local_storage_setDataFromObject('user_templates_last', { 'name': selectedUserTemplate.value.name, 'data': { 'session': session.value, 'params': params.value } })
  271. }
  272. }
  273. console.log('Checking for autosaved last used template')
  274. userTemplateLoadAndApplyAutosaved()
  275. /* END: Support for storing prompt templates and parameters in browsers LocalStorage */
  276. const llamaStats = signal(null)
  277. const controller = signal(null)
  278. // currently generating a completion?
  279. const generating = computed(() => controller.value != null)
  280. // has the user started a chat?
  281. const chatStarted = computed(() => session.value.transcript.length > 0)
  282. const transcriptUpdate = (transcript) => {
  283. session.value = {
  284. ...session.value,
  285. transcript
  286. }
  287. }
  288. // simple template replace
  289. const template = (str, extraSettings) => {
  290. let settings = session.value;
  291. if (extraSettings) {
  292. settings = { ...settings, ...extraSettings };
  293. }
  294. return String(str).replaceAll(/\{\{(.*?)\}\}/g, (_, key) => template(settings[key]));
  295. }
  296. // send message to server
  297. const chat = async (msg) => {
  298. if (controller.value) {
  299. console.log('already running...');
  300. return;
  301. }
  302. controller.value = new AbortController();
  303. transcriptUpdate([...session.value.transcript, ["{{user}}", msg]])
  304. const prompt = template(session.value.template, {
  305. message: msg,
  306. history: session.value.transcript.flatMap(
  307. ([name, data]) =>
  308. template(
  309. session.value.historyTemplate,
  310. {
  311. name,
  312. message: Array.isArray(data) ?
  313. data.map(msg => msg.content).join('').replace(/^\s/, '') :
  314. data,
  315. }
  316. )
  317. ).join("\n"),
  318. });
  319. const currentMessages = [];
  320. const history = session.value.transcript
  321. const llamaParams = {
  322. ...params.value,
  323. stop: ["</s>", template("{{char}}:"), template("{{user}}:")],
  324. }
  325. for await (const chunk of llama(prompt, llamaParams, { controller: controller.value })) {
  326. const data = chunk.data;
  327. if (data.stop) {
  328. while (
  329. currentMessages.length > 0 &&
  330. currentMessages[currentMessages.length - 1].content.match(/\n$/) != null
  331. ) {
  332. currentMessages.pop();
  333. }
  334. transcriptUpdate([...history, ["{{char}}", currentMessages]])
  335. console.log("Completion finished: '", currentMessages.map(msg => msg.content).join(''), "', summary: ", data);
  336. } else {
  337. currentMessages.push(data);
  338. transcriptUpdate([...history, ["{{char}}", currentMessages]])
  339. }
  340. if (data.timings) {
  341. llamaStats.value = data.timings;
  342. }
  343. }
  344. controller.value = null;
  345. }
  346. function MessageInput() {
  347. const message = useSignal("")
  348. const stop = (e) => {
  349. e.preventDefault();
  350. if (controller.value) {
  351. controller.value.abort();
  352. controller.value = null;
  353. }
  354. }
  355. const reset = (e) => {
  356. stop(e);
  357. transcriptUpdate([]);
  358. }
  359. const submit = (e) => {
  360. stop(e);
  361. chat(message.value);
  362. message.value = "";
  363. }
  364. const enterSubmits = (event) => {
  365. if (event.which === 13 && !event.shiftKey) {
  366. submit(event);
  367. }
  368. }
  369. return html`
  370. <form onsubmit=${submit}>
  371. <div>
  372. <textarea
  373. className=${generating.value ? "loading" : null}
  374. oninput=${(e) => message.value = e.target.value}
  375. onkeypress=${enterSubmits}
  376. placeholder="Say something..."
  377. rows=2
  378. type="text"
  379. value="${message}"
  380. />
  381. </div>
  382. <div class="right">
  383. <button type="submit" disabled=${generating.value}>Send</button>
  384. <button onclick=${stop} disabled=${!generating.value}>Stop</button>
  385. <button onclick=${reset}>Reset</button>
  386. </div>
  387. </form>
  388. `
  389. }
  390. const ChatLog = (props) => {
  391. const messages = session.value.transcript;
  392. const container = useRef(null)
  393. useEffect(() => {
  394. // scroll to bottom (if needed)
  395. const parent = container.current.parentElement;
  396. if (parent && parent.scrollHeight <= parent.scrollTop + parent.offsetHeight + 300) {
  397. parent.scrollTo(0, parent.scrollHeight)
  398. }
  399. }, [messages])
  400. const chatLine = ([user, data], index) => {
  401. let message
  402. const isArrayMessage = Array.isArray(data)
  403. if (params.value.n_probs > 0 && isArrayMessage) {
  404. message = html`<${Probabilities} data=${data} />`
  405. } else {
  406. const text = isArrayMessage ?
  407. data.map(msg => msg.content).join('').replace(/^\s+/, '') :
  408. data;
  409. message = html`<${Markdownish} text=${template(text)} />`
  410. }
  411. return html`<p key=${index}><strong>${template(user)}:</strong> ${message}</p>`
  412. };
  413. return html`
  414. <section id="chat" ref=${container}>
  415. ${messages.flatMap(chatLine)}
  416. </section>`;
  417. };
  418. const ConfigForm = (props) => {
  419. const updateSession = (el) => session.value = { ...session.value, [el.target.name]: el.target.value }
  420. const updateParams = (el) => params.value = { ...params.value, [el.target.name]: el.target.value }
  421. const updateParamsFloat = (el) => params.value = { ...params.value, [el.target.name]: parseFloat(el.target.value) }
  422. const updateParamsInt = (el) => params.value = { ...params.value, [el.target.name]: Math.floor(parseFloat(el.target.value)) }
  423. const grammarJsonSchemaPropOrder = signal('')
  424. const updateGrammarJsonSchemaPropOrder = (el) => grammarJsonSchemaPropOrder.value = el.target.value
  425. const convertJSONSchemaGrammar = () => {
  426. try {
  427. const schema = JSON.parse(params.value.grammar)
  428. const converter = new SchemaConverter(
  429. grammarJsonSchemaPropOrder.value
  430. .split(',')
  431. .reduce((acc, cur, i) => ({...acc, [cur.trim()]: i}), {})
  432. )
  433. converter.visit(schema, '')
  434. params.value = {
  435. ...params.value,
  436. grammar: converter.formatGrammar(),
  437. }
  438. } catch (e) {
  439. alert(`Convert failed: ${e.message}`)
  440. }
  441. }
  442. const FloatField = ({label, max, min, name, step, value}) => {
  443. return html`
  444. <div>
  445. <label for="${name}">${label}</label>
  446. <input type="range" id="${name}" min="${min}" max="${max}" step="${step}" name="${name}" value="${value}" oninput=${updateParamsFloat} />
  447. <span>${value}</span>
  448. </div>
  449. `
  450. };
  451. const IntField = ({label, max, min, name, value}) => {
  452. return html`
  453. <div>
  454. <label for="${name}">${label}</label>
  455. <input type="range" id="${name}" min="${min}" max="${max}" name="${name}" value="${value}" oninput=${updateParamsInt} />
  456. <span>${value}</span>
  457. </div>
  458. `
  459. };
  460. const userTemplateReset = (e) => {
  461. e.preventDefault();
  462. userTemplateResetToDefaultAndApply()
  463. }
  464. const UserTemplateResetButton = () => {
  465. if (selectedUserTemplate.value.name == 'default') {
  466. return html`
  467. <button disabled>Using default template</button>
  468. `
  469. }
  470. return html`
  471. <button onclick=${userTemplateReset}>Reset all to default</button>
  472. `
  473. };
  474. useEffect(() => {
  475. // autosave template on every change
  476. userTemplateAutosave()
  477. }, [session.value, params.value])
  478. return html`
  479. <form>
  480. <fieldset>
  481. <${UserTemplateResetButton}/>
  482. </fieldset>
  483. <fieldset>
  484. <div>
  485. <label for="prompt">Prompt</label>
  486. <textarea type="text" name="prompt" value="${session.value.prompt}" rows=4 oninput=${updateSession}/>
  487. </div>
  488. </fieldset>
  489. <fieldset class="two">
  490. <div>
  491. <label for="user">User name</label>
  492. <input type="text" name="user" value="${session.value.user}" oninput=${updateSession} />
  493. </div>
  494. <div>
  495. <label for="bot">Bot name</label>
  496. <input type="text" name="char" value="${session.value.char}" oninput=${updateSession} />
  497. </div>
  498. </fieldset>
  499. <fieldset>
  500. <div>
  501. <label for="template">Prompt template</label>
  502. <textarea id="template" name="template" value="${session.value.template}" rows=4 oninput=${updateSession}/>
  503. </div>
  504. <div>
  505. <label for="template">Chat history template</label>
  506. <textarea id="template" name="historyTemplate" value="${session.value.historyTemplate}" rows=1 oninput=${updateSession}/>
  507. </div>
  508. <div>
  509. <label for="template">Grammar</label>
  510. <textarea id="grammar" name="grammar" placeholder="Use gbnf or JSON Schema+convert" value="${params.value.grammar}" rows=4 oninput=${updateParams}/>
  511. <input type="text" name="prop-order" placeholder="order: prop1,prop2,prop3" oninput=${updateGrammarJsonSchemaPropOrder} />
  512. <button type="button" onclick=${convertJSONSchemaGrammar}>Convert JSON Schema</button>
  513. </div>
  514. </fieldset>
  515. <fieldset class="two">
  516. ${IntField({label: "Predictions", max: 2048, min: -1, name: "n_predict", value: params.value.n_predict})}
  517. ${FloatField({label: "Temperature", max: 1.5, min: 0.0, name: "temperature", step: 0.01, value: params.value.temperature})}
  518. ${FloatField({label: "Penalize repeat sequence", max: 2.0, min: 0.0, name: "repeat_penalty", step: 0.01, value: params.value.repeat_penalty})}
  519. ${IntField({label: "Consider N tokens for penalize", max: 2048, min: 0, name: "repeat_last_n", value: params.value.repeat_last_n})}
  520. ${IntField({label: "Top-K sampling", max: 100, min: -1, name: "top_k", value: params.value.top_k})}
  521. ${FloatField({label: "Top-P sampling", max: 1.0, min: 0.0, name: "top_p", step: 0.01, value: params.value.top_p})}
  522. </fieldset>
  523. <details>
  524. <summary>More options</summary>
  525. <fieldset class="two">
  526. ${FloatField({label: "TFS-Z", max: 1.0, min: 0.0, name: "tfs_z", step: 0.01, value: params.value.tfs_z})}
  527. ${FloatField({label: "Typical P", max: 1.0, min: 0.0, name: "typical_p", step: 0.01, value: params.value.typical_p})}
  528. ${FloatField({label: "Presence penalty", max: 1.0, min: 0.0, name: "presence_penalty", step: 0.01, value: params.value.presence_penalty})}
  529. ${FloatField({label: "Frequency penalty", max: 1.0, min: 0.0, name: "frequency_penalty", step: 0.01, value: params.value.frequency_penalty})}
  530. </fieldset>
  531. <hr />
  532. <fieldset class="three">
  533. <div>
  534. <label><input type="radio" name="mirostat" value="0" checked=${params.value.mirostat == 0} oninput=${updateParamsInt} /> no Mirostat</label>
  535. <label><input type="radio" name="mirostat" value="1" checked=${params.value.mirostat == 1} oninput=${updateParamsInt} /> Mirostat v1</label>
  536. <label><input type="radio" name="mirostat" value="2" checked=${params.value.mirostat == 2} oninput=${updateParamsInt} /> Mirostat v2</label>
  537. </div>
  538. ${FloatField({label: "Mirostat tau", max: 10.0, min: 0.0, name: "mirostat_tau", step: 0.01, value: params.value.mirostat_tau})}
  539. ${FloatField({label: "Mirostat eta", max: 1.0, min: 0.0, name: "mirostat_eta", step: 0.01, value: params.value.mirostat_eta})}
  540. </fieldset>
  541. <fieldset>
  542. ${IntField({label: "Show Probabilities", max: 10, min: 0, name: "n_probs", value: params.value.n_probs})}
  543. </fieldset>
  544. </details>
  545. </form>
  546. `
  547. }
  548. const probColor = (p) => {
  549. const r = Math.floor(192 * (1 - p));
  550. const g = Math.floor(192 * p);
  551. return `rgba(${r},${g},0,0.3)`;
  552. }
  553. const Probabilities = (params) => {
  554. return params.data.map(msg => {
  555. const { completion_probabilities } = msg;
  556. if (
  557. !completion_probabilities ||
  558. completion_probabilities.length === 0
  559. ) return msg.content
  560. if (completion_probabilities.length > 1) {
  561. // Not for byte pair
  562. if (completion_probabilities[0].content.startsWith('byte: \\')) return msg.content
  563. const splitData = completion_probabilities.map(prob => ({
  564. content: prob.content,
  565. completion_probabilities: [prob]
  566. }))
  567. return html`<${Probabilities} data=${splitData} />`
  568. }
  569. const { probs, content } = completion_probabilities[0]
  570. const found = probs.find(p => p.tok_str === msg.content)
  571. const pColor = found ? probColor(found.prob) : 'transparent'
  572. const popoverChildren = html`
  573. <div class="prob-set">
  574. ${probs.map((p, index) => {
  575. return html`
  576. <div
  577. key=${index}
  578. title=${`prob: ${p.prob}`}
  579. style=${{
  580. padding: '0.3em',
  581. backgroundColor: p.tok_str === content ? probColor(p.prob) : 'transparent'
  582. }}
  583. >
  584. <span>${p.tok_str}: </span>
  585. <span>${Math.floor(p.prob * 100)}%</span>
  586. </div>
  587. `
  588. })}
  589. </div>
  590. `
  591. return html`
  592. <${Popover} style=${{ backgroundColor: pColor }} popoverChildren=${popoverChildren}>
  593. ${msg.content.match(/\n/gim) ? html`<br />` : msg.content}
  594. </>
  595. `
  596. });
  597. }
  598. // poor mans markdown replacement
  599. const Markdownish = (params) => {
  600. const md = params.text
  601. .replace(/&/g, '&amp;')
  602. .replace(/</g, '&lt;')
  603. .replace(/>/g, '&gt;')
  604. .replace(/^#{1,6} (.*)$/gim, '<h3>$1</h3>')
  605. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  606. .replace(/__(.*?)__/g, '<strong>$1</strong>')
  607. .replace(/\*(.*?)\*/g, '<em>$1</em>')
  608. .replace(/_(.*?)_/g, '<em>$1</em>')
  609. .replace(/```.*?\n([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
  610. .replace(/`(.*?)`/g, '<code>$1</code>')
  611. .replace(/\n/gim, '<br />');
  612. return html`<span dangerouslySetInnerHTML=${{ __html: md }} />`;
  613. };
  614. const ModelGenerationInfo = (params) => {
  615. if (!llamaStats.value) {
  616. return html`<span/>`
  617. }
  618. return html`
  619. <span>
  620. ${llamaStats.value.predicted_per_token_ms.toFixed()}ms per token, ${llamaStats.value.predicted_per_second.toFixed(2)} tokens per second
  621. </span>
  622. `
  623. }
  624. // simple popover impl
  625. const Popover = (props) => {
  626. const isOpen = useSignal(false);
  627. const position = useSignal({ top: '0px', left: '0px' });
  628. const buttonRef = useRef(null);
  629. const popoverRef = useRef(null);
  630. const togglePopover = () => {
  631. if (buttonRef.current) {
  632. const rect = buttonRef.current.getBoundingClientRect();
  633. position.value = {
  634. top: `${rect.bottom + window.scrollY}px`,
  635. left: `${rect.left + window.scrollX}px`,
  636. };
  637. }
  638. isOpen.value = !isOpen.value;
  639. };
  640. const handleClickOutside = (event) => {
  641. if (popoverRef.current && !popoverRef.current.contains(event.target) && !buttonRef.current.contains(event.target)) {
  642. isOpen.value = false;
  643. }
  644. };
  645. useEffect(() => {
  646. document.addEventListener('mousedown', handleClickOutside);
  647. return () => {
  648. document.removeEventListener('mousedown', handleClickOutside);
  649. };
  650. }, []);
  651. return html`
  652. <span style=${props.style} ref=${buttonRef} onClick=${togglePopover}>${props.children}</span>
  653. ${isOpen.value && html`
  654. <${Portal} into="#portal">
  655. <div
  656. ref=${popoverRef}
  657. class="popover-content"
  658. style=${{
  659. top: position.value.top,
  660. left: position.value.left,
  661. }}
  662. >
  663. ${props.popoverChildren}
  664. </div>
  665. </${Portal}>
  666. `}
  667. `;
  668. };
  669. // Source: preact-portal (https://github.com/developit/preact-portal/blob/master/src/preact-portal.js)
  670. /** Redirect rendering of descendants into the given CSS selector */
  671. class Portal extends Component {
  672. componentDidUpdate(props) {
  673. for (let i in props) {
  674. if (props[i] !== this.props[i]) {
  675. return setTimeout(this.renderLayer);
  676. }
  677. }
  678. }
  679. componentDidMount() {
  680. this.isMounted = true;
  681. this.renderLayer = this.renderLayer.bind(this);
  682. this.renderLayer();
  683. }
  684. componentWillUnmount() {
  685. this.renderLayer(false);
  686. this.isMounted = false;
  687. if (this.remote && this.remote.parentNode) this.remote.parentNode.removeChild(this.remote);
  688. }
  689. findNode(node) {
  690. return typeof node === 'string' ? document.querySelector(node) : node;
  691. }
  692. renderLayer(show = true) {
  693. if (!this.isMounted) return;
  694. // clean up old node if moving bases:
  695. if (this.props.into !== this.intoPointer) {
  696. this.intoPointer = this.props.into;
  697. if (this.into && this.remote) {
  698. this.remote = render(html`<${PortalProxy} />`, this.into, this.remote);
  699. }
  700. this.into = this.findNode(this.props.into);
  701. }
  702. this.remote = render(html`
  703. <${PortalProxy} context=${this.context}>
  704. ${show && this.props.children || null}
  705. </${PortalProxy}>
  706. `, this.into, this.remote);
  707. }
  708. render() {
  709. return null;
  710. }
  711. }
  712. // high-order component that renders its first child if it exists.
  713. // used as a conditional rendering proxy.
  714. class PortalProxy extends Component {
  715. getChildContext() {
  716. return this.props.context;
  717. }
  718. render({ children }) {
  719. return children || null;
  720. }
  721. }
  722. function App(props) {
  723. return html`
  724. <div>
  725. <header>
  726. <h1>llama.cpp</h1>
  727. </header>
  728. <main id="content">
  729. <${chatStarted.value ? ChatLog : ConfigForm} />
  730. </main>
  731. <section id="write">
  732. <${MessageInput} />
  733. </section>
  734. <footer>
  735. <p><${ModelGenerationInfo} /></p>
  736. <p>Powered by <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> and <a href="https://ggml.ai">ggml.ai</a>.</p>
  737. </footer>
  738. </div>
  739. `;
  740. }
  741. render(h(App), document.querySelector('#container'));
  742. </script>
  743. </head>
  744. <body>
  745. <div id="container"></div>
  746. <div id="portal"></div>
  747. </body>
  748. </html>