index.html 27 KB

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