diff options
Diffstat (limited to 'examples/server/public/index.html')
-rw-r--r-- | examples/server/public/index.html | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/examples/server/public/index.html b/examples/server/public/index.html new file mode 100644 index 0000000..6393e2e --- /dev/null +++ b/examples/server/public/index.html @@ -0,0 +1,359 @@ +<html> + +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> + <title>llama.cpp - chat</title> + + <style> + + body { + background-color: #fff; + color: #000; + font-family: system-ui; + font-size: 90%; + } + + #container { + margin: 0em auto; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + } + + header, footer { + text-align: center; + } + + main { + margin: 3px; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1em; + + flex-grow: 1; + overflow-y: auto; + + border: 1px solid #ccc; + border-radius: 5px; + padding: 0.5em; + } + + body { + max-width: 600px; + min-width: 300px; + line-height: 1.2; + margin: 0 auto; + padding: 0 0.5em; + } + + p { + overflow-wrap: break-word; + word-wrap: break-word; + hyphens: auto; + margin-top: 0.5em; + margin-bottom: 0.5em; + } + + #write form { + margin: 1em 0 0 0; + display: flex; + flex-direction: column; + gap: 0.5em; + align-items: stretch; + } + + .right { + display: flex; + flex-direction: row; + gap: 0.5em; + justify-content: flex-end; + } + + fieldset { + border: none; + padding: 0; + margin: 0; + } + + textarea { + padding: 5px; + flex-grow: 1; + width: 100%; + } + + pre code { + display: block; + background-color: #222; + color: #ddd; + } + code { + font-family: monospace; + padding: 0.1em 0.3em; + border-radius: 3px; + } + + fieldset label { + margin: 0.5em 0; + display: block; + } + </style> + + <script type="module"> + import { + html, h, signal, effect, computed, render, useSignal, useEffect, useRef + } from '/index.js'; + + import { llamaComplete } from '/completion.js'; + + const session = signal({ + prompt: "This is a conversation between user and llama, a friendly chatbot. respond in markdown.", + template: "{{prompt}}\n\n{{history}}\n{{char}}:", + historyTemplate: "{{name}}: {{message}}", + transcript: [], + type: "chat", + char: "llama", + user: "User", + }) + + const transcriptUpdate = (transcript) => { + session.value = { + ...session.value, + transcript + } + } + + const chatStarted = computed(() => session.value.transcript.length > 0) + + const params = signal({ + n_predict: 400, + temperature: 0.7, + repeat_last_n: 256, + repeat_penalty: 1.18, + top_k: 40, + top_p: 0.5, + }) + + const controller = signal(null) + const generating = computed(() => controller.value == null ) + + // simple template replace + const template = (str, extraSettings) => { + let settings = session.value; + if (extraSettings) { + settings = { ...settings, ...extraSettings }; + } + return String(str).replaceAll(/\{\{(.*?)\}\}/g, (_, key) => template(settings[key])); + } + + // send message to server + const chat = async (msg) => { + if (controller.value) { + console.log('already running...'); + return; + } + controller.value = new AbortController(); + + transcriptUpdate([...session.value.transcript, ["{{user}}", msg]]) + + const payload = template(session.value.template, { + message: msg, + history: session.value.transcript.flatMap(([name, message]) => template(session.value.historyTemplate, {name, message})).join("\n"), + }); + + let currentMessage = ''; + const history = session.value.transcript + + const llamaParams = { + ...params.value, + prompt: payload, + stop: ["</s>", template("{{char}}:"), template("{{user}}:")], + } + + await llamaComplete(llamaParams, controller.value, (message) => { + const data = message.data; + currentMessage += data.content; + // remove leading whitespace + currentMessage = currentMessage.replace(/^\s+/, "") + + transcriptUpdate([...history, ["{{char}}", currentMessage]]) + + if (data.stop) { + console.log("-->", data, ' response was:', currentMessage, 'transcript state:', session.value.transcript); + } + }) + + controller.value = null; + } + + function MessageInput() { + const message = useSignal("") + + const stop = (e) => { + e.preventDefault(); + if (controller.value) { + controller.value.abort(); + controller.value = null; + } + } + + const reset = (e) => { + stop(e); + transcriptUpdate([]); + } + + const submit = (e) => { + stop(e); + chat(message.value); + message.value = ""; + } + + const enterSubmits = (event) => { + if (event.which === 13 && !event.shiftKey) { + submit(event); + } + } + + return html` + <form onsubmit=${submit}> + <div> + <textarea type="text" rows=2 onkeypress=${enterSubmits} value="${message}" oninput=${(e) => message.value = e.target.value} placeholder="Say something..."/> + + </div> + <div class="right"> + <button type="submit" disabled=${!generating.value} >Send</button> + <button onclick=${stop} disabled=${generating}>Stop</button> + <button onclick=${reset}>Reset</button> + </div> + </form> + ` + } + + const ChatLog = (props) => { + const messages = session.value.transcript; + const container = useRef(null) + + useEffect(() => { + // scroll to bottom (if needed) + if (container.current && container.current.scrollHeight <= container.current.scrollTop + container.current.offsetHeight + 300) { + container.current.scrollTo(0, container.current.scrollHeight) + } + }, [messages]) + + const chatLine = ([user, msg]) => { + return html`<p key=${msg}><strong>${template(user)}:</strong> <${Markdown} text=${template(msg)} /></p>` + }; + + return html` + <section id="chat" ref=${container}> + ${messages.flatMap(chatLine)} + </section>`; + }; + + const ConfigForm = (props) => { + const updateSession = (el) => session.value = { ...session.value, [el.target.name]: el.target.value } + const updateParams = (el) => params.value = { ...params.value, [el.target.name]: el.target.value } + const updateParamsFloat = (el) => params.value = { ...params.value, [el.target.name]: parseFloat(el.target.value) } + + return html` + <form> + <fieldset> + <div> + <label for="prompt">Prompt</label> + <textarea type="text" name="prompt" value="${session.value.prompt}" rows=4 oninput=${updateSession}/> + </div> + + <div> + <label for="user">User name</label> + <input type="text" name="user" value="${session.value.user}" oninput=${updateSession} /> + </div> + + <div> + <label for="bot">Bot name</label> + <input type="text" name="char" value="${session.value.char}" oninput=${updateSession} /> + </div> + + <div> + <label for="template">Prompt template</label> + <textarea id="template" name="template" value="${session.value.template}" rows=4 oninput=${updateSession}/> + </div> + + <div> + <label for="template">Chat history template</label> + <textarea id="template" name="historyTemplate" value="${session.value.historyTemplate}" rows=1 oninput=${updateSession}/> + </div> + + <div> + <label for="temperature">Temperature</label> + <input type="range" id="temperature" min="0.0" max="1.0" step="0.01" name="temperature" value="${params.value.temperature}" oninput=${updateParamsFloat} /> + <span>${params.value.temperature}</span> + </div> + + <div> + <label for="nPredict">Predictions</label> + <input type="range" id="nPredict" min="1" max="2048" step="1" name="n_predict" value="${params.value.n_predict}" oninput=${updateParamsFloat} /> + <span>${params.value.n_predict}</span> + </div> + + <div> + <label for="repeat_penalty">Penalize repeat sequence</label> + <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} /> + <span>${params.value.repeat_penalty}</span> + </div> + + <div> + <label for="repeat_last_n">Consider N tokens for penalize</label> + <input type="range" id="repeat_last_n" min="0.0" max="2048" name="repeat_last_n" value="${params.value.repeat_last_n}" oninput=${updateParamsFloat} /> + <span>${params.value.repeat_last_n}</span> + </div> + + </fieldset> + </form> + ` + } +const Markdown = (params) => { + const md = params.text + .replace(/^#{1,6} (.*)$/gim, '<h3>$1</h3>') + .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') + .replace(/__(.*?)__/g, '<strong>$1</strong>') + .replace(/\*(.*?)\*/g, '<em>$1</em>') + .replace(/_(.*?)_/g, '<em>$1</em>') + .replace(/```.*?\n([\s\S]*?)```/g, '<pre><code>$1</code></pre>') + .replace(/`(.*?)`/g, '<code>$1</code>') + .replace(/\n/gim, '<br />'); + return html`<span dangerouslySetInnerHTML=${{ __html: md }} />`; +}; + + function App(props) { + + return html` + <div id="container"> + <header> + <h1>llama.cpp</h1> + </header> + + <main id="content"> + <${chatStarted.value ? ChatLog : ConfigForm} /> + </main> + + <footer id="write"> + <${MessageInput} /> + </footer> + + <footer> + <p>Powered by <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> and <a href="https://ggml.ai">ggml.ai</a></p> + </footer> + </div> + `; + } + + render(h(App), document.body); + </script> +</head> + +<body> +</body> + +</html> |