index.html 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  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. textarea {
  89. padding: 5px;
  90. flex-grow: 1;
  91. width: 100%;
  92. }
  93. pre code {
  94. display: block;
  95. background-color: #222;
  96. color: #ddd;
  97. }
  98. code {
  99. font-family: monospace;
  100. padding: 0.1em 0.3em;
  101. border-radius: 3px;
  102. }
  103. fieldset label {
  104. margin: 0.5em 0;
  105. display: block;
  106. }
  107. header, footer {
  108. text-align: center;
  109. }
  110. footer {
  111. font-size: 80%;
  112. color: #888;
  113. }
  114. </style>
  115. <script type="module">
  116. import {
  117. html, h, signal, effect, computed, render, useSignal, useEffect, useRef
  118. } from '/index.js';
  119. import { llama } from '/completion.js';
  120. import { SchemaConverter } from '/json-schema-to-grammar.mjs';
  121. const session = signal({
  122. 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.",
  123. template: "{{prompt}}\n\n{{history}}\n{{char}}:",
  124. historyTemplate: "{{name}}: {{message}}",
  125. transcript: [],
  126. type: "chat",
  127. char: "Llama",
  128. user: "User",
  129. })
  130. const params = signal({
  131. n_predict: 400,
  132. temperature: 0.7,
  133. repeat_last_n: 256, // 0 = disable penalty, -1 = context size
  134. repeat_penalty: 1.18, // 1.0 = disabled
  135. top_k: 40, // <= 0 to use vocab size
  136. top_p: 0.5, // 1.0 = disabled
  137. tfs_z: 1.0, // 1.0 = disabled
  138. typical_p: 1.0, // 1.0 = disabled
  139. presence_penalty: 0.0, // 0.0 = disabled
  140. frequency_penalty: 0.0, // 0.0 = disabled
  141. mirostat: 0, // 0/1/2
  142. mirostat_tau: 5, // target entropy
  143. mirostat_eta: 0.1, // learning rate
  144. grammar: '',
  145. })
  146. /* START: Support for storing prompt templates and parameters in borwser LocalStorage */
  147. const local_storage_storageKey = "llamacpp_server_local_storage";
  148. function local_storage_setDataFromObject(tag, content) {
  149. localStorage.setItem(local_storage_storageKey + '/' + tag, JSON.stringify(content));
  150. }
  151. function local_storage_setDataFromRawText(tag, content) {
  152. localStorage.setItem(local_storage_storageKey + '/' + tag, content);
  153. }
  154. function local_storage_getDataAsObject(tag) {
  155. const item = localStorage.getItem(local_storage_storageKey + '/' + tag);
  156. if (!item) {
  157. return null;
  158. } else {
  159. return JSON.parse(item);
  160. }
  161. }
  162. function local_storage_getDataAsRawText(tag) {
  163. const item = localStorage.getItem(local_storage_storageKey + '/' + tag);
  164. if (!item) {
  165. return null;
  166. } else {
  167. return item;
  168. }
  169. }
  170. // create a container for user templates and settings
  171. const savedUserTemplates = signal({})
  172. const selectedUserTemplate = signal({ name: '', template: { session: {}, params: {} } })
  173. // let's import locally saved templates and settings if there are any
  174. // user templates and settings are stored in one object
  175. // in form of { "templatename": "templatedata" } and { "settingstemplatename":"settingsdata" }
  176. console.log('Importing saved templates')
  177. let importedTemplates = local_storage_getDataAsObject('user_templates')
  178. if (importedTemplates) {
  179. // saved templates were successfuly imported.
  180. console.log('Processing saved templates and updating default template')
  181. //console.log(importedTemplates);
  182. savedUserTemplates.value = importedTemplates;
  183. //override default template
  184. savedUserTemplates.value.default = { session: session.value, params: params.value }
  185. local_storage_setDataFromObject('user_templates', savedUserTemplates.value)
  186. } else {
  187. // no saved templates detected.
  188. console.log('Initializing LocalStorage and saving default template')
  189. savedUserTemplates.value = { "default": { session: session.value, params: params.value } }
  190. local_storage_setDataFromObject('user_templates', savedUserTemplates.value)
  191. }
  192. function userTemplateResetToDefault() {
  193. console.log('Reseting themplate to default')
  194. selectedUserTemplate.value.name = 'default';
  195. selectedUserTemplate.value.data = savedUserTemplates.value['default'];
  196. }
  197. function userTemplateApply(t) {
  198. session.value = t.data.session;
  199. params.value = t.data.params;
  200. }
  201. function userTemplateResetToDefaultAndApply() {
  202. userTemplateResetToDefault()
  203. userTemplateApply(selectedUserTemplate.value)
  204. }
  205. function userTemplateLoadAndApplyAutosaved() {
  206. // get autosaved last used template
  207. let lastUsedTemplate = local_storage_getDataAsObject('user_templates_last')
  208. if (lastUsedTemplate) {
  209. console.log('Autosaved template found, restoring')
  210. selectedUserTemplate.value = lastUsedTemplate
  211. }
  212. else {
  213. console.log('No autosaved template found, using default template')
  214. // no autosaved last used template was found, so load from default.
  215. userTemplateResetToDefault()
  216. }
  217. console.log('Applying template')
  218. // and update internal data from templates
  219. userTemplateApply(selectedUserTemplate.value)
  220. }
  221. //console.log(savedUserTemplates.value)
  222. //console.log(selectedUserTemplate.value)
  223. function userTemplateAutosave() {
  224. console.log('Template Autosave...')
  225. if (selectedUserTemplate.value.name == 'default') {
  226. // we don't want to save over default template, so let's create a new one
  227. let newTemplateName = 'UserTemplate-' + Date.now().toString()
  228. let newTemplate = { 'name': newTemplateName, 'data': { 'session': session.value, 'params': params.value } }
  229. console.log('Saving as ' + newTemplateName)
  230. // save in the autosave slot
  231. local_storage_setDataFromObject('user_templates_last', newTemplate)
  232. // and load it back and apply
  233. userTemplateLoadAndApplyAutosaved()
  234. } else {
  235. local_storage_setDataFromObject('user_templates_last', { 'name': selectedUserTemplate.value.name, 'data': { 'session': session.value, 'params': params.value } })
  236. }
  237. }
  238. console.log('Checking for autosaved last used template')
  239. userTemplateLoadAndApplyAutosaved()
  240. /* END: Support for storing prompt templates and parameters in browsers LocalStorage */
  241. const llamaStats = signal(null)
  242. const controller = signal(null)
  243. const generating = computed(() => controller.value == null )
  244. const chatStarted = computed(() => session.value.transcript.length > 0)
  245. const transcriptUpdate = (transcript) => {
  246. session.value = {
  247. ...session.value,
  248. transcript
  249. }
  250. }
  251. // simple template replace
  252. const template = (str, extraSettings) => {
  253. let settings = session.value;
  254. if (extraSettings) {
  255. settings = { ...settings, ...extraSettings };
  256. }
  257. return String(str).replaceAll(/\{\{(.*?)\}\}/g, (_, key) => template(settings[key]));
  258. }
  259. // send message to server
  260. const chat = async (msg) => {
  261. if (controller.value) {
  262. console.log('already running...');
  263. return;
  264. }
  265. controller.value = new AbortController();
  266. transcriptUpdate([...session.value.transcript, ["{{user}}", msg]])
  267. const prompt = template(session.value.template, {
  268. message: msg,
  269. history: session.value.transcript.flatMap(([name, message]) => template(session.value.historyTemplate, {name, message})).join("\n"),
  270. });
  271. let currentMessage = '';
  272. const history = session.value.transcript
  273. const llamaParams = {
  274. ...params.value,
  275. stop: ["</s>", template("{{char}}:"), template("{{user}}:")],
  276. }
  277. for await (const chunk of llama(prompt, llamaParams, { controller: controller.value })) {
  278. const data = chunk.data;
  279. currentMessage += data.content;
  280. // remove leading whitespace
  281. currentMessage = currentMessage.replace(/^\s+/, "")
  282. transcriptUpdate([...history, ["{{char}}", currentMessage]])
  283. if (data.stop) {
  284. console.log("Completion finished: '", currentMessage, "', summary: ", data);
  285. }
  286. if (data.timings) {
  287. llamaStats.value = data.timings;
  288. }
  289. }
  290. controller.value = null;
  291. }
  292. function MessageInput() {
  293. const message = useSignal("")
  294. const stop = (e) => {
  295. e.preventDefault();
  296. if (controller.value) {
  297. controller.value.abort();
  298. controller.value = null;
  299. }
  300. }
  301. const reset = (e) => {
  302. stop(e);
  303. transcriptUpdate([]);
  304. }
  305. const submit = (e) => {
  306. stop(e);
  307. chat(message.value);
  308. message.value = "";
  309. }
  310. const enterSubmits = (event) => {
  311. if (event.which === 13 && !event.shiftKey) {
  312. submit(event);
  313. }
  314. }
  315. return html`
  316. <form onsubmit=${submit}>
  317. <div>
  318. <textarea type="text" rows=2 onkeypress=${enterSubmits} value="${message}" oninput=${(e) => message.value = e.target.value} placeholder="Say something..."/>
  319. </div>
  320. <div class="right">
  321. <button type="submit" disabled=${!generating.value} >Send</button>
  322. <button onclick=${stop} disabled=${generating}>Stop</button>
  323. <button onclick=${reset}>Reset</button>
  324. </div>
  325. </form>
  326. `
  327. }
  328. const ChatLog = (props) => {
  329. const messages = session.value.transcript;
  330. const container = useRef(null)
  331. useEffect(() => {
  332. // scroll to bottom (if needed)
  333. const parent = container.current.parentElement;
  334. if (parent && parent.scrollHeight <= parent.scrollTop + parent.offsetHeight + 300) {
  335. parent.scrollTo(0, parent.scrollHeight)
  336. }
  337. }, [messages])
  338. const chatLine = ([user, msg]) => {
  339. return html`<p key=${msg}><strong>${template(user)}:</strong> <${Markdownish} text=${template(msg)} /></p>`
  340. };
  341. return html`
  342. <section id="chat" ref=${container}>
  343. ${messages.flatMap(chatLine)}
  344. </section>`;
  345. };
  346. const ConfigForm = (props) => {
  347. const updateSession = (el) => session.value = { ...session.value, [el.target.name]: el.target.value }
  348. const updateParams = (el) => params.value = { ...params.value, [el.target.name]: el.target.value }
  349. const updateParamsFloat = (el) => params.value = { ...params.value, [el.target.name]: parseFloat(el.target.value) }
  350. const updateParamsInt = (el) => params.value = { ...params.value, [el.target.name]: Math.floor(parseFloat(el.target.value)) }
  351. const grammarJsonSchemaPropOrder = signal('')
  352. const updateGrammarJsonSchemaPropOrder = (el) => grammarJsonSchemaPropOrder.value = el.target.value
  353. const convertJSONSchemaGrammar = () => {
  354. try {
  355. const schema = JSON.parse(params.value.grammar)
  356. const converter = new SchemaConverter(
  357. grammarJsonSchemaPropOrder.value
  358. .split(',')
  359. .reduce((acc, cur, i) => ({...acc, [cur.trim()]: i}), {})
  360. )
  361. converter.visit(schema, '')
  362. params.value = {
  363. ...params.value,
  364. grammar: converter.formatGrammar(),
  365. }
  366. } catch (e) {
  367. alert(`Convert failed: ${e.message}`)
  368. }
  369. }
  370. const FloatField = ({label, max, min, name, step, value}) => {
  371. return html`
  372. <div>
  373. <label for="${name}">${label}</label>
  374. <input type="range" id="${name}" min="${min}" max="${max}" step="${step}" name="${name}" value="${value}" oninput=${updateParamsFloat} />
  375. <span>${value}</span>
  376. </div>
  377. `
  378. };
  379. const IntField = ({label, max, min, name, value}) => {
  380. return html`
  381. <div>
  382. <label for="${name}">${label}</label>
  383. <input type="range" id="${name}" min="${min}" max="${max}" name="${name}" value="${value}" oninput=${updateParamsInt} />
  384. <span>${value}</span>
  385. </div>
  386. `
  387. };
  388. const userTemplateReset = (e) => {
  389. e.preventDefault();
  390. userTemplateResetToDefaultAndApply()
  391. }
  392. const UserTemplateResetButton = () => {
  393. if (selectedUserTemplate.value.name == 'default') {
  394. return html`
  395. <button disabled>Using default template</button>
  396. `
  397. }
  398. return html`
  399. <button onclick=${userTemplateReset}>Reset all to default</button>
  400. `
  401. };
  402. useEffect(() => {
  403. // autosave template on every change
  404. userTemplateAutosave()
  405. }, [session.value, params.value])
  406. return html`
  407. <form>
  408. <fieldset>
  409. <${UserTemplateResetButton}/>
  410. </fieldset>
  411. <fieldset>
  412. <div>
  413. <label for="prompt">Prompt</label>
  414. <textarea type="text" name="prompt" value="${session.value.prompt}" rows=4 oninput=${updateSession}/>
  415. </div>
  416. </fieldset>
  417. <fieldset class="two">
  418. <div>
  419. <label for="user">User name</label>
  420. <input type="text" name="user" value="${session.value.user}" oninput=${updateSession} />
  421. </div>
  422. <div>
  423. <label for="bot">Bot name</label>
  424. <input type="text" name="char" value="${session.value.char}" oninput=${updateSession} />
  425. </div>
  426. </fieldset>
  427. <fieldset>
  428. <div>
  429. <label for="template">Prompt template</label>
  430. <textarea id="template" name="template" value="${session.value.template}" rows=4 oninput=${updateSession}/>
  431. </div>
  432. <div>
  433. <label for="template">Chat history template</label>
  434. <textarea id="template" name="historyTemplate" value="${session.value.historyTemplate}" rows=1 oninput=${updateSession}/>
  435. </div>
  436. <div>
  437. <label for="template">Grammar</label>
  438. <textarea id="grammar" name="grammar" placeholder="Use gbnf or JSON Schema+convert" value="${params.value.grammar}" rows=4 oninput=${updateParams}/>
  439. <input type="text" name="prop-order" placeholder="order: prop1,prop2,prop3" oninput=${updateGrammarJsonSchemaPropOrder} />
  440. <button type="button" onclick=${convertJSONSchemaGrammar}>Convert JSON Schema</button>
  441. </div>
  442. </fieldset>
  443. <fieldset class="two">
  444. ${IntField({label: "Predictions", max: 2048, min: -1, name: "n_predict", value: params.value.n_predict})}
  445. ${FloatField({label: "Temperature", max: 1.5, min: 0.0, name: "temperature", step: 0.01, value: params.value.temperature})}
  446. ${FloatField({label: "Penalize repeat sequence", max: 2.0, min: 0.0, name: "repeat_penalty", step: 0.01, value: params.value.repeat_penalty})}
  447. ${IntField({label: "Consider N tokens for penalize", max: 2048, min: 0, name: "repeat_last_n", value: params.value.repeat_last_n})}
  448. ${IntField({label: "Top-K sampling", max: 100, min: -1, name: "top_k", value: params.value.top_k})}
  449. ${FloatField({label: "Top-P sampling", max: 1.0, min: 0.0, name: "top_p", step: 0.01, value: params.value.top_p})}
  450. </fieldset>
  451. <details>
  452. <summary>More options</summary>
  453. <fieldset class="two">
  454. ${FloatField({label: "TFS-Z", max: 1.0, min: 0.0, name: "tfs_z", step: 0.01, value: params.value.tfs_z})}
  455. ${FloatField({label: "Typical P", max: 1.0, min: 0.0, name: "typical_p", step: 0.01, value: params.value.typical_p})}
  456. ${FloatField({label: "Presence penalty", max: 1.0, min: 0.0, name: "presence_penalty", step: 0.01, value: params.value.presence_penalty})}
  457. ${FloatField({label: "Frequency penalty", max: 1.0, min: 0.0, name: "frequency_penalty", step: 0.01, value: params.value.frequency_penalty})}
  458. </fieldset>
  459. <hr />
  460. <fieldset class="three">
  461. <div>
  462. <label><input type="radio" name="mirostat" value="0" checked=${params.value.mirostat == 0} oninput=${updateParamsInt} /> no Mirostat</label>
  463. <label><input type="radio" name="mirostat" value="1" checked=${params.value.mirostat == 1} oninput=${updateParamsInt} /> Mirostat v1</label>
  464. <label><input type="radio" name="mirostat" value="2" checked=${params.value.mirostat == 2} oninput=${updateParamsInt} /> Mirostat v2</label>
  465. </div>
  466. ${FloatField({label: "Mirostat tau", max: 10.0, min: 0.0, name: "mirostat_tau", step: 0.01, value: params.value.mirostat_tau})}
  467. ${FloatField({label: "Mirostat eta", max: 1.0, min: 0.0, name: "mirostat_eta", step: 0.01, value: params.value.mirostat_eta})}
  468. </fieldset>
  469. </details>
  470. </form>
  471. `
  472. }
  473. // poor mans markdown replacement
  474. const Markdownish = (params) => {
  475. const md = params.text
  476. .replace(/&/g, '&amp;')
  477. .replace(/</g, '&lt;')
  478. .replace(/>/g, '&gt;')
  479. .replace(/^#{1,6} (.*)$/gim, '<h3>$1</h3>')
  480. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  481. .replace(/__(.*?)__/g, '<strong>$1</strong>')
  482. .replace(/\*(.*?)\*/g, '<em>$1</em>')
  483. .replace(/_(.*?)_/g, '<em>$1</em>')
  484. .replace(/```.*?\n([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
  485. .replace(/`(.*?)`/g, '<code>$1</code>')
  486. .replace(/\n/gim, '<br />');
  487. return html`<span dangerouslySetInnerHTML=${{ __html: md }} />`;
  488. };
  489. const ModelGenerationInfo = (params) => {
  490. if (!llamaStats.value) {
  491. return html`<span/>`
  492. }
  493. return html`
  494. <span>
  495. ${llamaStats.value.predicted_per_token_ms.toFixed()}ms per token, ${llamaStats.value.predicted_per_second.toFixed(2)} tokens per second
  496. </span>
  497. `
  498. }
  499. function App(props) {
  500. return html`
  501. <div id="container">
  502. <header>
  503. <h1>llama.cpp</h1>
  504. </header>
  505. <main id="content">
  506. <${chatStarted.value ? ChatLog : ConfigForm} />
  507. </main>
  508. <section id="write">
  509. <${MessageInput} />
  510. </section>
  511. <footer>
  512. <p><${ModelGenerationInfo} /></p>
  513. <p>Powered by <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> and <a href="https://ggml.ai">ggml.ai</a>.</p>
  514. </footer>
  515. </div>
  516. `;
  517. }
  518. render(h(App), document.body);
  519. </script>
  520. </head>
  521. <body>
  522. </body>
  523. </html>