index.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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. textarea {
  65. padding: 5px;
  66. flex-grow: 1;
  67. width: 100%;
  68. }
  69. pre code {
  70. display: block;
  71. background-color: #222;
  72. color: #ddd;
  73. }
  74. code {
  75. font-family: monospace;
  76. padding: 0.1em 0.3em;
  77. border-radius: 3px;
  78. }
  79. fieldset label {
  80. margin: 0.5em 0;
  81. display: block;
  82. }
  83. header, footer {
  84. text-align: center;
  85. }
  86. footer {
  87. font-size: 80%;
  88. color: #888;
  89. }
  90. </style>
  91. <script type="module">
  92. import {
  93. html, h, signal, effect, computed, render, useSignal, useEffect, useRef
  94. } from '/index.js';
  95. import { llama } from '/completion.js';
  96. const session = signal({
  97. prompt: "This is a conversation between user and llama, a friendly chatbot. respond in simple markdown.",
  98. template: "{{prompt}}\n\n{{history}}\n{{char}}:",
  99. historyTemplate: "{{name}}: {{message}}",
  100. transcript: [],
  101. type: "chat",
  102. char: "llama",
  103. user: "User",
  104. })
  105. const params = signal({
  106. n_predict: 400,
  107. temperature: 0.7,
  108. repeat_last_n: 256,
  109. repeat_penalty: 1.18,
  110. top_k: 40,
  111. top_p: 0.5,
  112. })
  113. const llamaStats = signal(null)
  114. const controller = signal(null)
  115. const generating = computed(() => controller.value == null )
  116. const chatStarted = computed(() => session.value.transcript.length > 0)
  117. const transcriptUpdate = (transcript) => {
  118. session.value = {
  119. ...session.value,
  120. transcript
  121. }
  122. }
  123. // simple template replace
  124. const template = (str, extraSettings) => {
  125. let settings = session.value;
  126. if (extraSettings) {
  127. settings = { ...settings, ...extraSettings };
  128. }
  129. return String(str).replaceAll(/\{\{(.*?)\}\}/g, (_, key) => template(settings[key]));
  130. }
  131. // send message to server
  132. const chat = async (msg) => {
  133. if (controller.value) {
  134. console.log('already running...');
  135. return;
  136. }
  137. controller.value = new AbortController();
  138. transcriptUpdate([...session.value.transcript, ["{{user}}", msg]])
  139. const prompt = template(session.value.template, {
  140. message: msg,
  141. history: session.value.transcript.flatMap(([name, message]) => template(session.value.historyTemplate, {name, message})).join("\n"),
  142. });
  143. let currentMessage = '';
  144. const history = session.value.transcript
  145. const llamaParams = {
  146. ...params.value,
  147. stop: ["</s>", template("{{char}}:"), template("{{user}}:")],
  148. }
  149. for await (const chunk of llama(prompt, llamaParams, { controller: controller.value })) {
  150. const data = chunk.data;
  151. currentMessage += data.content;
  152. // remove leading whitespace
  153. currentMessage = currentMessage.replace(/^\s+/, "")
  154. transcriptUpdate([...history, ["{{char}}", currentMessage]])
  155. if (data.stop) {
  156. console.log("Completion finished: '", currentMessage, "', summary: ", data);
  157. }
  158. if (data.timings) {
  159. llamaStats.value = data.timings;
  160. }
  161. }
  162. controller.value = null;
  163. }
  164. function MessageInput() {
  165. const message = useSignal("")
  166. const stop = (e) => {
  167. e.preventDefault();
  168. if (controller.value) {
  169. controller.value.abort();
  170. controller.value = null;
  171. }
  172. }
  173. const reset = (e) => {
  174. stop(e);
  175. transcriptUpdate([]);
  176. }
  177. const submit = (e) => {
  178. stop(e);
  179. chat(message.value);
  180. message.value = "";
  181. }
  182. const enterSubmits = (event) => {
  183. if (event.which === 13 && !event.shiftKey) {
  184. submit(event);
  185. }
  186. }
  187. return html`
  188. <form onsubmit=${submit}>
  189. <div>
  190. <textarea type="text" rows=2 onkeypress=${enterSubmits} value="${message}" oninput=${(e) => message.value = e.target.value} placeholder="Say something..."/>
  191. </div>
  192. <div class="right">
  193. <button type="submit" disabled=${!generating.value} >Send</button>
  194. <button onclick=${stop} disabled=${generating}>Stop</button>
  195. <button onclick=${reset}>Reset</button>
  196. </div>
  197. </form>
  198. `
  199. }
  200. const ChatLog = (props) => {
  201. const messages = session.value.transcript;
  202. const container = useRef(null)
  203. useEffect(() => {
  204. // scroll to bottom (if needed)
  205. if (container.current && container.current.scrollHeight <= container.current.scrollTop + container.current.offsetHeight + 300) {
  206. container.current.scrollTo(0, container.current.scrollHeight)
  207. }
  208. }, [messages])
  209. const chatLine = ([user, msg]) => {
  210. return html`<p key=${msg}><strong>${template(user)}:</strong> <${Markdownish} text=${template(msg)} /></p>`
  211. };
  212. return html`
  213. <section id="chat" ref=${container}>
  214. ${messages.flatMap(chatLine)}
  215. </section>`;
  216. };
  217. const ConfigForm = (props) => {
  218. const updateSession = (el) => session.value = { ...session.value, [el.target.name]: el.target.value }
  219. const updateParams = (el) => params.value = { ...params.value, [el.target.name]: el.target.value }
  220. const updateParamsFloat = (el) => params.value = { ...params.value, [el.target.name]: parseFloat(el.target.value) }
  221. return html`
  222. <form>
  223. <fieldset>
  224. <div>
  225. <label for="prompt">Prompt</label>
  226. <textarea type="text" name="prompt" value="${session.value.prompt}" rows=4 oninput=${updateSession}/>
  227. </div>
  228. <div>
  229. <label for="user">User name</label>
  230. <input type="text" name="user" value="${session.value.user}" oninput=${updateSession} />
  231. </div>
  232. <div>
  233. <label for="bot">Bot name</label>
  234. <input type="text" name="char" value="${session.value.char}" oninput=${updateSession} />
  235. </div>
  236. <div>
  237. <label for="template">Prompt template</label>
  238. <textarea id="template" name="template" value="${session.value.template}" rows=4 oninput=${updateSession}/>
  239. </div>
  240. <div>
  241. <label for="template">Chat history template</label>
  242. <textarea id="template" name="historyTemplate" value="${session.value.historyTemplate}" rows=1 oninput=${updateSession}/>
  243. </div>
  244. <div>
  245. <label for="temperature">Temperature</label>
  246. <input type="range" id="temperature" min="0.0" max="1.0" step="0.01" name="temperature" value="${params.value.temperature}" oninput=${updateParamsFloat} />
  247. <span>${params.value.temperature}</span>
  248. </div>
  249. <div>
  250. <label for="nPredict">Predictions</label>
  251. <input type="range" id="nPredict" min="1" max="2048" step="1" name="n_predict" value="${params.value.n_predict}" oninput=${updateParamsFloat} />
  252. <span>${params.value.n_predict}</span>
  253. </div>
  254. <div>
  255. <label for="repeat_penalty">Penalize repeat sequence</label>
  256. <input type="range" id="repeat_penalty" min="0.0" max="2.0" step="0.01" name="repeat_penalty" value="${params.value.repeat_penalty}" oninput=${updateParamsFloat} />
  257. <span>${params.value.repeat_penalty}</span>
  258. </div>
  259. <div>
  260. <label for="repeat_last_n">Consider N tokens for penalize</label>
  261. <input type="range" id="repeat_last_n" min="0.0" max="2048" name="repeat_last_n" value="${params.value.repeat_last_n}" oninput=${updateParamsFloat} />
  262. <span>${params.value.repeat_last_n}</span>
  263. </div>
  264. </fieldset>
  265. </form>
  266. `
  267. }
  268. // poor mans markdown replacement
  269. const Markdownish = (params) => {
  270. const md = params.text
  271. .replace(/^#{1,6} (.*)$/gim, '<h3>$1</h3>')
  272. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  273. .replace(/__(.*?)__/g, '<strong>$1</strong>')
  274. .replace(/\*(.*?)\*/g, '<em>$1</em>')
  275. .replace(/_(.*?)_/g, '<em>$1</em>')
  276. .replace(/```.*?\n([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
  277. .replace(/`(.*?)`/g, '<code>$1</code>')
  278. .replace(/\n/gim, '<br />');
  279. return html`<span dangerouslySetInnerHTML=${{ __html: md }} />`;
  280. };
  281. const ModelGenerationInfo = (params) => {
  282. if (!llamaStats.value) {
  283. return html`<span/>`
  284. }
  285. return html`
  286. <span>
  287. ${llamaStats.value.predicted_per_token_ms.toFixed()}ms per token, ${llamaStats.value.predicted_per_second.toFixed(2)} tokens per second
  288. </span>
  289. `
  290. }
  291. function App(props) {
  292. return html`
  293. <div id="container">
  294. <header>
  295. <h1>llama.cpp</h1>
  296. </header>
  297. <main id="content">
  298. <${chatStarted.value ? ChatLog : ConfigForm} />
  299. </main>
  300. <section id="write">
  301. <${MessageInput} />
  302. </section>
  303. <footer>
  304. <p><${ModelGenerationInfo} /></p>
  305. <p>Powered by <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> and <a href="https://ggml.ai">ggml.ai</a>.</p>
  306. </footer>
  307. </div>
  308. `;
  309. }
  310. render(h(App), document.body);
  311. </script>
  312. </head>
  313. <body>
  314. </body>
  315. </html>