index.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. <html>
  2. <head>
  3. <meta charset="UTF-8">
  4. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  5. <title>llama.cpp - chat</title>
  6. <style>
  7. body {
  8. background-color: #fff;
  9. color: #000;
  10. font-family: system-ui;
  11. font-size: 90%;
  12. }
  13. #container {
  14. margin: 0em auto;
  15. display: flex;
  16. flex-direction: column;
  17. justify-content: space-between;
  18. height: 100%;
  19. }
  20. main {
  21. margin: 3px;
  22. display: flex;
  23. flex-direction: column;
  24. justify-content: space-between;
  25. gap: 1em;
  26. flex-grow: 1;
  27. overflow-y: auto;
  28. border: 1px solid #ccc;
  29. border-radius: 5px;
  30. padding: 0.5em;
  31. }
  32. body {
  33. max-width: 600px;
  34. min-width: 300px;
  35. line-height: 1.2;
  36. margin: 0 auto;
  37. padding: 0 0.5em;
  38. }
  39. p {
  40. overflow-wrap: break-word;
  41. word-wrap: break-word;
  42. hyphens: auto;
  43. margin-top: 0.5em;
  44. margin-bottom: 0.5em;
  45. }
  46. #write form {
  47. margin: 1em 0 0 0;
  48. display: flex;
  49. flex-direction: column;
  50. gap: 0.5em;
  51. align-items: stretch;
  52. }
  53. .right {
  54. display: flex;
  55. flex-direction: row;
  56. gap: 0.5em;
  57. justify-content: flex-end;
  58. }
  59. fieldset {
  60. border: none;
  61. padding: 0;
  62. margin: 0;
  63. }
  64. fieldset.two {
  65. display: grid;
  66. grid-template: "a a";
  67. gap: 1em;
  68. }
  69. fieldset.three {
  70. display: grid;
  71. grid-template: "a a a";
  72. gap: 1em;
  73. }
  74. details {
  75. border: 1px solid #aaa;
  76. border-radius: 4px;
  77. padding: 0.5em 0.5em 0;
  78. margin-top: 0.5em;
  79. }
  80. summary {
  81. font-weight: bold;
  82. margin: -0.5em -0.5em 0;
  83. padding: 0.5em;
  84. cursor: pointer;
  85. }
  86. details[open] {
  87. padding: 0.5em;
  88. }
  89. textarea {
  90. padding: 5px;
  91. flex-grow: 1;
  92. width: 100%;
  93. }
  94. pre code {
  95. display: block;
  96. background-color: #222;
  97. color: #ddd;
  98. }
  99. code {
  100. font-family: monospace;
  101. padding: 0.1em 0.3em;
  102. border-radius: 3px;
  103. }
  104. fieldset label {
  105. margin: 0.5em 0;
  106. display: block;
  107. }
  108. header, footer {
  109. text-align: center;
  110. }
  111. footer {
  112. font-size: 80%;
  113. color: #888;
  114. }
  115. </style>
  116. <script type="module">
  117. import {
  118. html, h, signal, effect, computed, render, useSignal, useEffect, useRef
  119. } from '/index.js';
  120. import { llama } from '/completion.js';
  121. const session = signal({
  122. prompt: "This is a conversation between user and llama, a friendly chatbot. respond in simple markdown.",
  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. })
  145. const llamaStats = signal(null)
  146. const controller = signal(null)
  147. const generating = computed(() => controller.value == null )
  148. const chatStarted = computed(() => session.value.transcript.length > 0)
  149. const transcriptUpdate = (transcript) => {
  150. session.value = {
  151. ...session.value,
  152. transcript
  153. }
  154. }
  155. // simple template replace
  156. const template = (str, extraSettings) => {
  157. let settings = session.value;
  158. if (extraSettings) {
  159. settings = { ...settings, ...extraSettings };
  160. }
  161. return String(str).replaceAll(/\{\{(.*?)\}\}/g, (_, key) => template(settings[key]));
  162. }
  163. // send message to server
  164. const chat = async (msg) => {
  165. if (controller.value) {
  166. console.log('already running...');
  167. return;
  168. }
  169. controller.value = new AbortController();
  170. transcriptUpdate([...session.value.transcript, ["{{user}}", msg]])
  171. const prompt = template(session.value.template, {
  172. message: msg,
  173. history: session.value.transcript.flatMap(([name, message]) => template(session.value.historyTemplate, {name, message})).join("\n"),
  174. });
  175. let currentMessage = '';
  176. const history = session.value.transcript
  177. const llamaParams = {
  178. ...params.value,
  179. stop: ["</s>", template("{{char}}:"), template("{{user}}:")],
  180. }
  181. for await (const chunk of llama(prompt, llamaParams, { controller: controller.value })) {
  182. const data = chunk.data;
  183. currentMessage += data.content;
  184. // remove leading whitespace
  185. currentMessage = currentMessage.replace(/^\s+/, "")
  186. transcriptUpdate([...history, ["{{char}}", currentMessage]])
  187. if (data.stop) {
  188. console.log("Completion finished: '", currentMessage, "', summary: ", data);
  189. }
  190. if (data.timings) {
  191. llamaStats.value = data.timings;
  192. }
  193. }
  194. controller.value = null;
  195. }
  196. function MessageInput() {
  197. const message = useSignal("")
  198. const stop = (e) => {
  199. e.preventDefault();
  200. if (controller.value) {
  201. controller.value.abort();
  202. controller.value = null;
  203. }
  204. }
  205. const reset = (e) => {
  206. stop(e);
  207. transcriptUpdate([]);
  208. }
  209. const submit = (e) => {
  210. stop(e);
  211. chat(message.value);
  212. message.value = "";
  213. }
  214. const enterSubmits = (event) => {
  215. if (event.which === 13 && !event.shiftKey) {
  216. submit(event);
  217. }
  218. }
  219. return html`
  220. <form onsubmit=${submit}>
  221. <div>
  222. <textarea type="text" rows=2 onkeypress=${enterSubmits} value="${message}" oninput=${(e) => message.value = e.target.value} placeholder="Say something..."/>
  223. </div>
  224. <div class="right">
  225. <button type="submit" disabled=${!generating.value} >Send</button>
  226. <button onclick=${stop} disabled=${generating}>Stop</button>
  227. <button onclick=${reset}>Reset</button>
  228. </div>
  229. </form>
  230. `
  231. }
  232. const ChatLog = (props) => {
  233. const messages = session.value.transcript;
  234. const container = useRef(null)
  235. useEffect(() => {
  236. // scroll to bottom (if needed)
  237. if (container.current && container.current.scrollHeight <= container.current.scrollTop + container.current.offsetHeight + 300) {
  238. container.current.scrollTo(0, container.current.scrollHeight)
  239. }
  240. }, [messages])
  241. const chatLine = ([user, msg]) => {
  242. return html`<p key=${msg}><strong>${template(user)}:</strong> <${Markdownish} text=${template(msg)} /></p>`
  243. };
  244. return html`
  245. <section id="chat" ref=${container}>
  246. ${messages.flatMap(chatLine)}
  247. </section>`;
  248. };
  249. const ConfigForm = (props) => {
  250. const updateSession = (el) => session.value = { ...session.value, [el.target.name]: el.target.value }
  251. const updateParams = (el) => params.value = { ...params.value, [el.target.name]: el.target.value }
  252. const updateParamsFloat = (el) => params.value = { ...params.value, [el.target.name]: parseFloat(el.target.value) }
  253. const updateParamsInt = (el) => params.value = { ...params.value, [el.target.name]: Math.floor(parseFloat(el.target.value)) }
  254. const FloatField = ({label, max, min, name, step, value}) => {
  255. return html`
  256. <div>
  257. <label for="${name}">${label}</label>
  258. <input type="range" id="${name}" min="${min}" max="${max}" step="${step}" name="${name}" value="${value}" oninput=${updateParamsFloat} />
  259. <span>${value}</span>
  260. </div>
  261. `
  262. };
  263. const IntField = ({label, max, min, name, value}) => {
  264. return html`
  265. <div>
  266. <label for="${name}">${label}</label>
  267. <input type="range" id="${name}" min="${min}" max="${max}" name="${name}" value="${value}" oninput=${updateParamsInt} />
  268. <span>${value}</span>
  269. </div>
  270. `
  271. };
  272. return html`
  273. <form>
  274. <fieldset>
  275. <div>
  276. <label for="prompt">Prompt</label>
  277. <textarea type="text" name="prompt" value="${session.value.prompt}" rows=4 oninput=${updateSession}/>
  278. </div>
  279. </fieldset>
  280. <fieldset class="two">
  281. <div>
  282. <label for="user">User name</label>
  283. <input type="text" name="user" value="${session.value.user}" oninput=${updateSession} />
  284. </div>
  285. <div>
  286. <label for="bot">Bot name</label>
  287. <input type="text" name="char" value="${session.value.char}" oninput=${updateSession} />
  288. </div>
  289. </fieldset>
  290. <fieldset>
  291. <div>
  292. <label for="template">Prompt template</label>
  293. <textarea id="template" name="template" value="${session.value.template}" rows=4 oninput=${updateSession}/>
  294. </div>
  295. <div>
  296. <label for="template">Chat history template</label>
  297. <textarea id="template" name="historyTemplate" value="${session.value.historyTemplate}" rows=1 oninput=${updateSession}/>
  298. </div>
  299. </fieldset>
  300. <fieldset class="two">
  301. ${IntField({label: "Predictions", max: 2048, min: -1, name: "n_predict", value: params.value.n_predict})}
  302. ${FloatField({label: "Temperature", max: 1.5, min: 0.0, name: "temperature", step: 0.01, value: params.value.temperature})}
  303. ${FloatField({label: "Penalize repeat sequence", max: 2.0, min: 0.0, name: "repeat_penalty", step: 0.01, value: params.value.repeat_penalty})}
  304. ${IntField({label: "Consider N tokens for penalize", max: 2048, min: 0, name: "repeat_last_n", value: params.value.repeat_last_n})}
  305. ${IntField({label: "Top-K sampling", max: 100, min: -1, name: "top_k", value: params.value.top_k})}
  306. ${FloatField({label: "Top-P sampling", max: 1.0, min: 0.0, name: "top_p", step: 0.01, value: params.value.top_p})}
  307. </fieldset>
  308. <details>
  309. <summary>More options</summary>
  310. <fieldset class="two">
  311. ${FloatField({label: "TFS-Z", max: 1.0, min: 0.0, name: "tfs_z", step: 0.01, value: params.value.tfs_z})}
  312. ${FloatField({label: "Typical P", max: 1.0, min: 0.0, name: "typical_p", step: 0.01, value: params.value.typical_p})}
  313. ${FloatField({label: "Presence penalty", max: 1.0, min: 0.0, name: "presence_penalty", step: 0.01, value: params.value.presence_penalty})}
  314. ${FloatField({label: "Frequency penalty", max: 1.0, min: 0.0, name: "frequency_penalty", step: 0.01, value: params.value.frequency_penalty})}
  315. </fieldset>
  316. <hr />
  317. <fieldset class="three">
  318. <div>
  319. <label><input type="radio" name="mirostat" value="0" checked=${params.value.mirostat == 0} oninput=${updateParamsInt} /> no Mirostat</label>
  320. <label><input type="radio" name="mirostat" value="1" checked=${params.value.mirostat == 1} oninput=${updateParamsInt} /> Mirostat v1</label>
  321. <label><input type="radio" name="mirostat" value="2" checked=${params.value.mirostat == 2} oninput=${updateParamsInt} /> Mirostat v2</label>
  322. </div>
  323. ${FloatField({label: "Mirostat tau", max: 10.0, min: 0.0, name: "mirostat_tau", step: 0.01, value: params.value.mirostat_tau})}
  324. ${FloatField({label: "Mirostat eta", max: 1.0, min: 0.0, name: "mirostat_eta", step: 0.01, value: params.value.mirostat_eta})}
  325. </fieldset>
  326. </details>
  327. </form>
  328. `
  329. }
  330. // poor mans markdown replacement
  331. const Markdownish = (params) => {
  332. const md = params.text
  333. .replace(/&/g, '&amp;')
  334. .replace(/</g, '&lt;')
  335. .replace(/>/g, '&gt;')
  336. .replace(/^#{1,6} (.*)$/gim, '<h3>$1</h3>')
  337. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  338. .replace(/__(.*?)__/g, '<strong>$1</strong>')
  339. .replace(/\*(.*?)\*/g, '<em>$1</em>')
  340. .replace(/_(.*?)_/g, '<em>$1</em>')
  341. .replace(/```.*?\n([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
  342. .replace(/`(.*?)`/g, '<code>$1</code>')
  343. .replace(/\n/gim, '<br />');
  344. return html`<span dangerouslySetInnerHTML=${{ __html: md }} />`;
  345. };
  346. const ModelGenerationInfo = (params) => {
  347. if (!llamaStats.value) {
  348. return html`<span/>`
  349. }
  350. return html`
  351. <span>
  352. ${llamaStats.value.predicted_per_token_ms.toFixed()}ms per token, ${llamaStats.value.predicted_per_second.toFixed(2)} tokens per second
  353. </span>
  354. `
  355. }
  356. function App(props) {
  357. return html`
  358. <div id="container">
  359. <header>
  360. <h1>llama.cpp</h1>
  361. </header>
  362. <main id="content">
  363. <${chatStarted.value ? ChatLog : ConfigForm} />
  364. </main>
  365. <section id="write">
  366. <${MessageInput} />
  367. </section>
  368. <footer>
  369. <p><${ModelGenerationInfo} /></p>
  370. <p>Powered by <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> and <a href="https://ggml.ai">ggml.ai</a>.</p>
  371. </footer>
  372. </div>
  373. `;
  374. }
  375. render(h(App), document.body);
  376. </script>
  377. </head>
  378. <body>
  379. </body>
  380. </html>