diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-02-12 20:57:17 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-02-12 20:57:17 +0100 |
| commit | b333b06772c89d96aacb5490d6a219fba7c09cc6 (patch) | |
| tree | 211df60083a5946baa2ed61d33d8121b7e251b06 /llama.cpp/tools/server/themes/wild | |
| download | llmnpc-b333b06772c89d96aacb5490d6a219fba7c09cc6.tar.gz | |
Engage!
Diffstat (limited to 'llama.cpp/tools/server/themes/wild')
| -rw-r--r-- | llama.cpp/tools/server/themes/wild/README.md | 5 | ||||
| -rw-r--r-- | llama.cpp/tools/server/themes/wild/favicon.ico | bin | 0 -> 4122 bytes | |||
| -rw-r--r-- | llama.cpp/tools/server/themes/wild/index.html | 1056 | ||||
| -rw-r--r-- | llama.cpp/tools/server/themes/wild/llama_cpp.png | bin | 0 -> 76484 bytes | |||
| -rw-r--r-- | llama.cpp/tools/server/themes/wild/llamapattern.png | bin | 0 -> 259586 bytes | |||
| -rw-r--r-- | llama.cpp/tools/server/themes/wild/wild.png | bin | 0 -> 496463 bytes |
6 files changed, 1061 insertions, 0 deletions
diff --git a/llama.cpp/tools/server/themes/wild/README.md b/llama.cpp/tools/server/themes/wild/README.md new file mode 100644 index 0000000..560bcc8 --- /dev/null +++ b/llama.cpp/tools/server/themes/wild/README.md | |||
| @@ -0,0 +1,5 @@ | |||
| 1 | # LLaMA.cpp Server Wild Theme | ||
| 2 | |||
| 3 | Simple tweaks to the UI. To use simply run server with `--path=themes/wild` | ||
| 4 | |||
| 5 |  | ||
diff --git a/llama.cpp/tools/server/themes/wild/favicon.ico b/llama.cpp/tools/server/themes/wild/favicon.ico new file mode 100644 index 0000000..89e154a --- /dev/null +++ b/llama.cpp/tools/server/themes/wild/favicon.ico | |||
| Binary files differ | |||
diff --git a/llama.cpp/tools/server/themes/wild/index.html b/llama.cpp/tools/server/themes/wild/index.html new file mode 100644 index 0000000..601f776 --- /dev/null +++ b/llama.cpp/tools/server/themes/wild/index.html | |||
| @@ -0,0 +1,1056 @@ | |||
| 1 | <html> | ||
| 2 | |||
| 3 | <head> | ||
| 4 | <meta charset="UTF-8"> | ||
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> | ||
| 6 | <meta name="color-scheme" content="light dark"> | ||
| 7 | <title>llama.cpp - chat</title> | ||
| 8 | |||
| 9 | <style> | ||
| 10 | body { | ||
| 11 | font-family: system-ui; | ||
| 12 | font-size: 90%; | ||
| 13 | background-image: url('llamapattern.png'); | ||
| 14 | } | ||
| 15 | |||
| 16 | #container { | ||
| 17 | margin: 0em auto; | ||
| 18 | display: flex; | ||
| 19 | flex-direction: column; | ||
| 20 | justify-content: space-between; | ||
| 21 | height: 100%; | ||
| 22 | } | ||
| 23 | |||
| 24 | main { | ||
| 25 | margin: 3px; | ||
| 26 | display: flex; | ||
| 27 | flex-direction: column; | ||
| 28 | justify-content: space-between; | ||
| 29 | gap: 1em; | ||
| 30 | |||
| 31 | flex-grow: 1; | ||
| 32 | overflow-y: auto; | ||
| 33 | |||
| 34 | border: 1px solid #ccc; | ||
| 35 | border-radius: 5px; | ||
| 36 | padding: 0.5em; | ||
| 37 | |||
| 38 | background-color: rgba(255,255,255,0.9); | ||
| 39 | } | ||
| 40 | |||
| 41 | body { | ||
| 42 | max-width: 600px; | ||
| 43 | min-width: 300px; | ||
| 44 | line-height: 1.2; | ||
| 45 | margin: 0 auto; | ||
| 46 | padding: 0 0.5em; | ||
| 47 | } | ||
| 48 | |||
| 49 | p { | ||
| 50 | overflow-wrap: break-word; | ||
| 51 | word-wrap: break-word; | ||
| 52 | hyphens: auto; | ||
| 53 | margin-top: 0.5em; | ||
| 54 | margin-bottom: 0.5em; | ||
| 55 | } | ||
| 56 | |||
| 57 | #write form { | ||
| 58 | margin: 1em 0 0 0; | ||
| 59 | display: flex; | ||
| 60 | flex-direction: column; | ||
| 61 | gap: 0.5em; | ||
| 62 | align-items: stretch; | ||
| 63 | } | ||
| 64 | |||
| 65 | .right { | ||
| 66 | display: flex; | ||
| 67 | flex-direction: row; | ||
| 68 | gap: 0.5em; | ||
| 69 | justify-content: flex-end; | ||
| 70 | } | ||
| 71 | |||
| 72 | fieldset { | ||
| 73 | border: none; | ||
| 74 | padding: 0; | ||
| 75 | margin: 0; | ||
| 76 | } | ||
| 77 | |||
| 78 | fieldset.two { | ||
| 79 | display: grid; | ||
| 80 | grid-template: "a a"; | ||
| 81 | gap: 1em; | ||
| 82 | } | ||
| 83 | |||
| 84 | fieldset.three { | ||
| 85 | display: grid; | ||
| 86 | grid-template: "a a a"; | ||
| 87 | gap: 1em; | ||
| 88 | } | ||
| 89 | |||
| 90 | details { | ||
| 91 | border: 1px solid #aaa; | ||
| 92 | border-radius: 4px; | ||
| 93 | padding: 0.5em 0.5em 0; | ||
| 94 | margin-top: 0.5em; | ||
| 95 | } | ||
| 96 | |||
| 97 | summary { | ||
| 98 | font-weight: bold; | ||
| 99 | margin: -0.5em -0.5em 0; | ||
| 100 | padding: 0.5em; | ||
| 101 | cursor: pointer; | ||
| 102 | } | ||
| 103 | |||
| 104 | details[open] { | ||
| 105 | padding: 0.5em; | ||
| 106 | } | ||
| 107 | |||
| 108 | .prob-set { | ||
| 109 | padding: 0.3em; | ||
| 110 | border-bottom: 1px solid #ccc; | ||
| 111 | } | ||
| 112 | |||
| 113 | .popover-content { | ||
| 114 | position: absolute; | ||
| 115 | background-color: white; | ||
| 116 | padding: 0.2em; | ||
| 117 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); | ||
| 118 | } | ||
| 119 | |||
| 120 | textarea { | ||
| 121 | padding: 5px; | ||
| 122 | flex-grow: 1; | ||
| 123 | width: 100%; | ||
| 124 | } | ||
| 125 | |||
| 126 | pre code { | ||
| 127 | display: block; | ||
| 128 | background-color: #222; | ||
| 129 | color: #ddd; | ||
| 130 | } | ||
| 131 | |||
| 132 | code { | ||
| 133 | font-family: monospace; | ||
| 134 | padding: 0.1em 0.3em; | ||
| 135 | border-radius: 3px; | ||
| 136 | } | ||
| 137 | |||
| 138 | fieldset label { | ||
| 139 | margin: 0.5em 0; | ||
| 140 | display: block; | ||
| 141 | } | ||
| 142 | |||
| 143 | fieldset label.slim { | ||
| 144 | margin: 0 0.5em; | ||
| 145 | display: inline; | ||
| 146 | } | ||
| 147 | |||
| 148 | header, | ||
| 149 | footer { | ||
| 150 | text-align: center; | ||
| 151 | } | ||
| 152 | |||
| 153 | footer { | ||
| 154 | font-size: 80%; | ||
| 155 | color: #888; | ||
| 156 | } | ||
| 157 | |||
| 158 | .mode-chat textarea[name=prompt] { | ||
| 159 | height: 4.5em; | ||
| 160 | } | ||
| 161 | |||
| 162 | .mode-completion textarea[name=prompt] { | ||
| 163 | height: 10em; | ||
| 164 | } | ||
| 165 | |||
| 166 | [contenteditable] { | ||
| 167 | display: inline-block; | ||
| 168 | white-space: pre-wrap; | ||
| 169 | outline: 0px solid transparent; | ||
| 170 | } | ||
| 171 | |||
| 172 | @keyframes loading-bg-wipe { | ||
| 173 | 0% { | ||
| 174 | background-position: 0%; | ||
| 175 | } | ||
| 176 | |||
| 177 | 100% { | ||
| 178 | background-position: 100%; | ||
| 179 | } | ||
| 180 | } | ||
| 181 | |||
| 182 | .loading { | ||
| 183 | --loading-color-1: #eeeeee00; | ||
| 184 | --loading-color-2: #eeeeeeff; | ||
| 185 | background-size: 50% 100%; | ||
| 186 | background-image: linear-gradient(90deg, var(--loading-color-1), var(--loading-color-2), var(--loading-color-1)); | ||
| 187 | animation: loading-bg-wipe 2s linear infinite; | ||
| 188 | } | ||
| 189 | |||
| 190 | @media (prefers-color-scheme: dark) { | ||
| 191 | .loading { | ||
| 192 | --loading-color-1: #22222200; | ||
| 193 | --loading-color-2: #222222ff; | ||
| 194 | } | ||
| 195 | |||
| 196 | .popover-content { | ||
| 197 | background-color: black; | ||
| 198 | } | ||
| 199 | } | ||
| 200 | </style> | ||
| 201 | |||
| 202 | <script type="module"> | ||
| 203 | import { | ||
| 204 | html, h, signal, effect, computed, render, useSignal, useEffect, useRef, Component | ||
| 205 | } from './index.js'; | ||
| 206 | |||
| 207 | import { llama } from './completion.js'; | ||
| 208 | import { SchemaConverter } from './json-schema-to-grammar.mjs'; | ||
| 209 | let selected_image = false; | ||
| 210 | var slot_id = -1; | ||
| 211 | |||
| 212 | const session = signal({ | ||
| 213 | 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.", | ||
| 214 | template: "{{prompt}}\n\n{{history}}\n{{char}}:", | ||
| 215 | historyTemplate: "{{name}}: {{message}}", | ||
| 216 | transcript: [], | ||
| 217 | type: "chat", // "chat" | "completion" | ||
| 218 | char: "Llama", | ||
| 219 | user: "User", | ||
| 220 | image_selected: '' | ||
| 221 | }) | ||
| 222 | |||
| 223 | const params = signal({ | ||
| 224 | n_predict: 400, | ||
| 225 | temperature: 0.7, | ||
| 226 | repeat_last_n: 256, // 0 = disable penalty, -1 = context size | ||
| 227 | repeat_penalty: 1.18, // 1.0 = disabled | ||
| 228 | top_k: 40, // <= 0 to use vocab size | ||
| 229 | top_p: 0.95, // 1.0 = disabled | ||
| 230 | min_p: 0.05, // 0 = disabled | ||
| 231 | typical_p: 1.0, // 1.0 = disabled | ||
| 232 | presence_penalty: 0.0, // 0.0 = disabled | ||
| 233 | frequency_penalty: 0.0, // 0.0 = disabled | ||
| 234 | mirostat: 0, // 0/1/2 | ||
| 235 | mirostat_tau: 5, // target entropy | ||
| 236 | mirostat_eta: 0.1, // learning rate | ||
| 237 | grammar: '', | ||
| 238 | n_probs: 0, // no completion_probabilities, | ||
| 239 | min_keep: 0, // min probs from each sampler, | ||
| 240 | image_data: [], | ||
| 241 | cache_prompt: true, | ||
| 242 | api_key: '' | ||
| 243 | }) | ||
| 244 | |||
| 245 | /* START: Support for storing prompt templates and parameters in browsers LocalStorage */ | ||
| 246 | |||
| 247 | const local_storage_storageKey = "llamacpp_server_local_storage"; | ||
| 248 | |||
| 249 | function local_storage_setDataFromObject(tag, content) { | ||
| 250 | localStorage.setItem(local_storage_storageKey + '/' + tag, JSON.stringify(content)); | ||
| 251 | } | ||
| 252 | |||
| 253 | function local_storage_setDataFromRawText(tag, content) { | ||
| 254 | localStorage.setItem(local_storage_storageKey + '/' + tag, content); | ||
| 255 | } | ||
| 256 | |||
| 257 | function local_storage_getDataAsObject(tag) { | ||
| 258 | const item = localStorage.getItem(local_storage_storageKey + '/' + tag); | ||
| 259 | if (!item) { | ||
| 260 | return null; | ||
| 261 | } else { | ||
| 262 | return JSON.parse(item); | ||
| 263 | } | ||
| 264 | } | ||
| 265 | |||
| 266 | function local_storage_getDataAsRawText(tag) { | ||
| 267 | const item = localStorage.getItem(local_storage_storageKey + '/' + tag); | ||
| 268 | if (!item) { | ||
| 269 | return null; | ||
| 270 | } else { | ||
| 271 | return item; | ||
| 272 | } | ||
| 273 | } | ||
| 274 | |||
| 275 | // create a container for user templates and settings | ||
| 276 | |||
| 277 | const savedUserTemplates = signal({}) | ||
| 278 | const selectedUserTemplate = signal({ name: '', template: { session: {}, params: {} } }) | ||
| 279 | |||
| 280 | // let's import locally saved templates and settings if there are any | ||
| 281 | // user templates and settings are stored in one object | ||
| 282 | // in form of { "templatename": "templatedata" } and { "settingstemplatename":"settingsdata" } | ||
| 283 | |||
| 284 | console.log('Importing saved templates') | ||
| 285 | |||
| 286 | let importedTemplates = local_storage_getDataAsObject('user_templates') | ||
| 287 | |||
| 288 | if (importedTemplates) { | ||
| 289 | // saved templates were successfully imported. | ||
| 290 | |||
| 291 | console.log('Processing saved templates and updating default template') | ||
| 292 | params.value = { ...params.value, image_data: [] }; | ||
| 293 | |||
| 294 | //console.log(importedTemplates); | ||
| 295 | savedUserTemplates.value = importedTemplates; | ||
| 296 | |||
| 297 | //override default template | ||
| 298 | savedUserTemplates.value.default = { session: session.value, params: params.value } | ||
| 299 | local_storage_setDataFromObject('user_templates', savedUserTemplates.value) | ||
| 300 | } else { | ||
| 301 | // no saved templates detected. | ||
| 302 | |||
| 303 | console.log('Initializing LocalStorage and saving default template') | ||
| 304 | |||
| 305 | savedUserTemplates.value = { "default": { session: session.value, params: params.value } } | ||
| 306 | local_storage_setDataFromObject('user_templates', savedUserTemplates.value) | ||
| 307 | } | ||
| 308 | |||
| 309 | function userTemplateResetToDefault() { | ||
| 310 | console.log('Resetting template to default') | ||
| 311 | selectedUserTemplate.value.name = 'default'; | ||
| 312 | selectedUserTemplate.value.data = savedUserTemplates.value['default']; | ||
| 313 | } | ||
| 314 | |||
| 315 | function userTemplateApply(t) { | ||
| 316 | session.value = t.data.session; | ||
| 317 | session.value = { ...session.value, image_selected: '' }; | ||
| 318 | params.value = t.data.params; | ||
| 319 | params.value = { ...params.value, image_data: [] }; | ||
| 320 | } | ||
| 321 | |||
| 322 | function userTemplateResetToDefaultAndApply() { | ||
| 323 | userTemplateResetToDefault() | ||
| 324 | userTemplateApply(selectedUserTemplate.value) | ||
| 325 | } | ||
| 326 | |||
| 327 | function userTemplateLoadAndApplyAutosaved() { | ||
| 328 | // get autosaved last used template | ||
| 329 | let lastUsedTemplate = local_storage_getDataAsObject('user_templates_last') | ||
| 330 | |||
| 331 | if (lastUsedTemplate) { | ||
| 332 | |||
| 333 | console.log('Autosaved template found, restoring') | ||
| 334 | |||
| 335 | selectedUserTemplate.value = lastUsedTemplate | ||
| 336 | } | ||
| 337 | else { | ||
| 338 | |||
| 339 | console.log('No autosaved template found, using default template') | ||
| 340 | // no autosaved last used template was found, so load from default. | ||
| 341 | |||
| 342 | userTemplateResetToDefault() | ||
| 343 | } | ||
| 344 | |||
| 345 | console.log('Applying template') | ||
| 346 | // and update internal data from templates | ||
| 347 | |||
| 348 | userTemplateApply(selectedUserTemplate.value) | ||
| 349 | } | ||
| 350 | |||
| 351 | //console.log(savedUserTemplates.value) | ||
| 352 | //console.log(selectedUserTemplate.value) | ||
| 353 | |||
| 354 | function userTemplateAutosave() { | ||
| 355 | console.log('Template Autosave...') | ||
| 356 | if (selectedUserTemplate.value.name == 'default') { | ||
| 357 | // we don't want to save over default template, so let's create a new one | ||
| 358 | let newTemplateName = 'UserTemplate-' + Date.now().toString() | ||
| 359 | let newTemplate = { 'name': newTemplateName, 'data': { 'session': session.value, 'params': params.value } } | ||
| 360 | |||
| 361 | console.log('Saving as ' + newTemplateName) | ||
| 362 | |||
| 363 | // save in the autosave slot | ||
| 364 | local_storage_setDataFromObject('user_templates_last', newTemplate) | ||
| 365 | |||
| 366 | // and load it back and apply | ||
| 367 | userTemplateLoadAndApplyAutosaved() | ||
| 368 | } else { | ||
| 369 | local_storage_setDataFromObject('user_templates_last', { 'name': selectedUserTemplate.value.name, 'data': { 'session': session.value, 'params': params.value } }) | ||
| 370 | } | ||
| 371 | } | ||
| 372 | |||
| 373 | console.log('Checking for autosaved last used template') | ||
| 374 | userTemplateLoadAndApplyAutosaved() | ||
| 375 | |||
| 376 | /* END: Support for storing prompt templates and parameters in browsers LocalStorage */ | ||
| 377 | |||
| 378 | const llamaStats = signal(null) | ||
| 379 | const controller = signal(null) | ||
| 380 | |||
| 381 | // currently generating a completion? | ||
| 382 | const generating = computed(() => controller.value != null) | ||
| 383 | |||
| 384 | // has the user started a chat? | ||
| 385 | const chatStarted = computed(() => session.value.transcript.length > 0) | ||
| 386 | |||
| 387 | const transcriptUpdate = (transcript) => { | ||
| 388 | session.value = { | ||
| 389 | ...session.value, | ||
| 390 | transcript | ||
| 391 | } | ||
| 392 | } | ||
| 393 | |||
| 394 | // simple template replace | ||
| 395 | const template = (str, extraSettings) => { | ||
| 396 | let settings = session.value; | ||
| 397 | if (extraSettings) { | ||
| 398 | settings = { ...settings, ...extraSettings }; | ||
| 399 | } | ||
| 400 | return String(str).replaceAll(/\{\{(.*?)\}\}/g, (_, key) => template(settings[key])); | ||
| 401 | } | ||
| 402 | |||
| 403 | async function runLlama(prompt, llamaParams, char) { | ||
| 404 | const currentMessages = []; | ||
| 405 | const history = session.value.transcript; | ||
| 406 | if (controller.value) { | ||
| 407 | throw new Error("already running"); | ||
| 408 | } | ||
| 409 | controller.value = new AbortController(); | ||
| 410 | for await (const chunk of llama(prompt, llamaParams, { controller: controller.value, api_url: location.pathname.replace(/\/+$/, '') })) { | ||
| 411 | const data = chunk.data; | ||
| 412 | |||
| 413 | if (data.stop) { | ||
| 414 | while ( | ||
| 415 | currentMessages.length > 0 && | ||
| 416 | currentMessages[currentMessages.length - 1].content.match(/\n$/) != null | ||
| 417 | ) { | ||
| 418 | currentMessages.pop(); | ||
| 419 | } | ||
| 420 | transcriptUpdate([...history, [char, currentMessages]]) | ||
| 421 | console.log("Completion finished: '", currentMessages.map(msg => msg.content).join(''), "', summary: ", data); | ||
| 422 | } else { | ||
| 423 | currentMessages.push(data); | ||
| 424 | slot_id = data.slot_id; | ||
| 425 | if (selected_image && !data.multimodal) { | ||
| 426 | alert("The server was not compiled for multimodal or the model projector can't be loaded."); | ||
| 427 | return; | ||
| 428 | } | ||
| 429 | transcriptUpdate([...history, [char, currentMessages]]) | ||
| 430 | } | ||
| 431 | |||
| 432 | if (data.timings) { | ||
| 433 | llamaStats.value = data; | ||
| 434 | } | ||
| 435 | } | ||
| 436 | |||
| 437 | controller.value = null; | ||
| 438 | } | ||
| 439 | |||
| 440 | // send message to server | ||
| 441 | const chat = async (msg) => { | ||
| 442 | if (controller.value) { | ||
| 443 | console.log('already running...'); | ||
| 444 | return; | ||
| 445 | } | ||
| 446 | |||
| 447 | transcriptUpdate([...session.value.transcript, ["{{user}}", msg]]) | ||
| 448 | |||
| 449 | let prompt = template(session.value.template, { | ||
| 450 | message: msg, | ||
| 451 | history: session.value.transcript.flatMap( | ||
| 452 | ([name, data]) => | ||
| 453 | template( | ||
| 454 | session.value.historyTemplate, | ||
| 455 | { | ||
| 456 | name, | ||
| 457 | message: Array.isArray(data) ? | ||
| 458 | data.map(msg => msg.content).join('').replace(/^\s/, '') : | ||
| 459 | data, | ||
| 460 | } | ||
| 461 | ) | ||
| 462 | ).join("\n"), | ||
| 463 | }); | ||
| 464 | if (selected_image) { | ||
| 465 | prompt = `A chat between a curious human and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the human's questions.\nUSER:[img-10]${msg}\nASSISTANT:`; | ||
| 466 | } | ||
| 467 | await runLlama(prompt, { | ||
| 468 | ...params.value, | ||
| 469 | slot_id: slot_id, | ||
| 470 | stop: ["</s>", template("{{char}}:"), template("{{user}}:")], | ||
| 471 | }, "{{char}}"); | ||
| 472 | } | ||
| 473 | |||
| 474 | const runCompletion = () => { | ||
| 475 | if (controller.value) { | ||
| 476 | console.log('already running...'); | ||
| 477 | return; | ||
| 478 | } | ||
| 479 | const { prompt } = session.value; | ||
| 480 | transcriptUpdate([...session.value.transcript, ["", prompt]]); | ||
| 481 | runLlama(prompt, { | ||
| 482 | ...params.value, | ||
| 483 | slot_id: slot_id, | ||
| 484 | stop: [], | ||
| 485 | }, "").finally(() => { | ||
| 486 | session.value.prompt = session.value.transcript.map(([_, data]) => | ||
| 487 | Array.isArray(data) ? data.map(msg => msg.content).join('') : data | ||
| 488 | ).join(''); | ||
| 489 | session.value.transcript = []; | ||
| 490 | }) | ||
| 491 | } | ||
| 492 | |||
| 493 | const stop = (e) => { | ||
| 494 | e.preventDefault(); | ||
| 495 | if (controller.value) { | ||
| 496 | controller.value.abort(); | ||
| 497 | controller.value = null; | ||
| 498 | } | ||
| 499 | } | ||
| 500 | |||
| 501 | const reset = (e) => { | ||
| 502 | stop(e); | ||
| 503 | transcriptUpdate([]); | ||
| 504 | } | ||
| 505 | |||
| 506 | const uploadImage = (e) => { | ||
| 507 | e.preventDefault(); | ||
| 508 | document.getElementById("fileInput").click(); | ||
| 509 | document.getElementById("fileInput").addEventListener("change", function (event) { | ||
| 510 | const selectedFile = event.target.files[0]; | ||
| 511 | if (selectedFile) { | ||
| 512 | const reader = new FileReader(); | ||
| 513 | reader.onload = function () { | ||
| 514 | const image_data = reader.result; | ||
| 515 | session.value = { ...session.value, image_selected: image_data }; | ||
| 516 | params.value = { | ||
| 517 | ...params.value, image_data: [ | ||
| 518 | { data: image_data.replace(/data:image\/[^;]+;base64,/, ''), id: 10 }] | ||
| 519 | } | ||
| 520 | }; | ||
| 521 | selected_image = true; | ||
| 522 | reader.readAsDataURL(selectedFile); | ||
| 523 | } | ||
| 524 | }); | ||
| 525 | } | ||
| 526 | |||
| 527 | function MessageInput() { | ||
| 528 | const message = useSignal("") | ||
| 529 | |||
| 530 | const submit = (e) => { | ||
| 531 | stop(e); | ||
| 532 | chat(message.value); | ||
| 533 | message.value = ""; | ||
| 534 | } | ||
| 535 | |||
| 536 | const enterSubmits = (event) => { | ||
| 537 | if (event.which === 13 && !event.shiftKey) { | ||
| 538 | submit(event); | ||
| 539 | } | ||
| 540 | } | ||
| 541 | |||
| 542 | return html` | ||
| 543 | <form onsubmit=${submit}> | ||
| 544 | <div> | ||
| 545 | <textarea | ||
| 546 | className=${generating.value ? "loading" : null} | ||
| 547 | oninput=${(e) => message.value = e.target.value} | ||
| 548 | onkeypress=${enterSubmits} | ||
| 549 | placeholder="Say something..." | ||
| 550 | rows=2 | ||
| 551 | type="text" | ||
| 552 | value="${message}" | ||
| 553 | /> | ||
| 554 | </div> | ||
| 555 | <div class="right"> | ||
| 556 | <button type="submit" disabled=${generating.value}>Send</button> | ||
| 557 | <button onclick=${uploadImage}>Upload Image</button> | ||
| 558 | <button onclick=${stop} disabled=${!generating.value}>Stop</button> | ||
| 559 | <button onclick=${reset}>Reset</button> | ||
| 560 | </div> | ||
| 561 | </form> | ||
| 562 | ` | ||
| 563 | } | ||
| 564 | |||
| 565 | function CompletionControls() { | ||
| 566 | const submit = (e) => { | ||
| 567 | stop(e); | ||
| 568 | runCompletion(); | ||
| 569 | } | ||
| 570 | return html` | ||
| 571 | <div> | ||
| 572 | <button onclick=${submit} type="button" disabled=${generating.value}>Start</button> | ||
| 573 | <button onclick=${stop} disabled=${!generating.value}>Stop</button> | ||
| 574 | <button onclick=${reset}>Reset</button> | ||
| 575 | </div>`; | ||
| 576 | } | ||
| 577 | |||
| 578 | const ChatLog = (props) => { | ||
| 579 | const messages = session.value.transcript; | ||
| 580 | const container = useRef(null) | ||
| 581 | |||
| 582 | useEffect(() => { | ||
| 583 | // scroll to bottom (if needed) | ||
| 584 | const parent = container.current.parentElement; | ||
| 585 | if (parent && parent.scrollHeight <= parent.scrollTop + parent.offsetHeight + 300) { | ||
| 586 | parent.scrollTo(0, parent.scrollHeight) | ||
| 587 | } | ||
| 588 | }, [messages]) | ||
| 589 | |||
| 590 | const isCompletionMode = session.value.type === 'completion' | ||
| 591 | const chatLine = ([user, data], index) => { | ||
| 592 | let message | ||
| 593 | const isArrayMessage = Array.isArray(data) | ||
| 594 | if (params.value.n_probs > 0 && isArrayMessage) { | ||
| 595 | message = html`<${Probabilities} data=${data} />` | ||
| 596 | } else { | ||
| 597 | const text = isArrayMessage ? | ||
| 598 | data.map(msg => msg.content).join('').replace(/^\s+/, '') : | ||
| 599 | data; | ||
| 600 | message = isCompletionMode ? | ||
| 601 | text : | ||
| 602 | html`<${Markdownish} text=${template(text)} />` | ||
| 603 | } | ||
| 604 | if (user) { | ||
| 605 | return html`<p key=${index}><strong>${template(user)}:</strong> ${message}</p>` | ||
| 606 | } else { | ||
| 607 | return isCompletionMode ? | ||
| 608 | html`<span key=${index}>${message}</span>` : | ||
| 609 | html`<p key=${index}>${message}</p>` | ||
| 610 | } | ||
| 611 | }; | ||
| 612 | |||
| 613 | const handleCompletionEdit = (e) => { | ||
| 614 | session.value.prompt = e.target.innerText; | ||
| 615 | session.value.transcript = []; | ||
| 616 | } | ||
| 617 | |||
| 618 | return html` | ||
| 619 | <div id="chat" ref=${container} key=${messages.length}> | ||
| 620 | <img style="width: 60%;${!session.value.image_selected ? `display: none;` : ``}" src="${session.value.image_selected}"/> | ||
| 621 | <span contenteditable=${isCompletionMode} ref=${container} oninput=${handleCompletionEdit}> | ||
| 622 | ${messages.flatMap(chatLine)} | ||
| 623 | </span> | ||
| 624 | </div>`; | ||
| 625 | }; | ||
| 626 | |||
| 627 | const ConfigForm = (props) => { | ||
| 628 | const updateSession = (el) => session.value = { ...session.value, [el.target.name]: el.target.value } | ||
| 629 | const updateParams = (el) => params.value = { ...params.value, [el.target.name]: el.target.value } | ||
| 630 | const updateParamsFloat = (el) => params.value = { ...params.value, [el.target.name]: parseFloat(el.target.value) } | ||
| 631 | const updateParamsInt = (el) => params.value = { ...params.value, [el.target.name]: Math.floor(parseFloat(el.target.value)) } | ||
| 632 | const updateParamsBool = (el) => params.value = { ...params.value, [el.target.name]: el.target.checked } | ||
| 633 | |||
| 634 | const grammarJsonSchemaPropOrder = signal('') | ||
| 635 | const updateGrammarJsonSchemaPropOrder = (el) => grammarJsonSchemaPropOrder.value = el.target.value | ||
| 636 | const convertJSONSchemaGrammar = async () => { | ||
| 637 | try { | ||
| 638 | let schema = JSON.parse(params.value.grammar) | ||
| 639 | const converter = new SchemaConverter({ | ||
| 640 | prop_order: grammarJsonSchemaPropOrder.value | ||
| 641 | .split(',') | ||
| 642 | .reduce((acc, cur, i) => ({ ...acc, [cur.trim()]: i }), {}), | ||
| 643 | allow_fetch: true, | ||
| 644 | }) | ||
| 645 | schema = await converter.resolveRefs(schema, 'input') | ||
| 646 | converter.visit(schema, '') | ||
| 647 | params.value = { | ||
| 648 | ...params.value, | ||
| 649 | grammar: converter.formatGrammar(), | ||
| 650 | } | ||
| 651 | } catch (e) { | ||
| 652 | alert(`Convert failed: ${e.message}`) | ||
| 653 | } | ||
| 654 | } | ||
| 655 | |||
| 656 | const FloatField = ({ label, max, min, name, step, value }) => { | ||
| 657 | return html` | ||
| 658 | <div> | ||
| 659 | <label for="${name}">${label}</label> | ||
| 660 | <input type="range" id="${name}" min="${min}" max="${max}" step="${step}" name="${name}" value="${value}" oninput=${updateParamsFloat} /> | ||
| 661 | <span>${value}</span> | ||
| 662 | </div> | ||
| 663 | ` | ||
| 664 | }; | ||
| 665 | |||
| 666 | const IntField = ({ label, max, min, name, value }) => { | ||
| 667 | return html` | ||
| 668 | <div> | ||
| 669 | <label for="${name}">${label}</label> | ||
| 670 | <input type="range" id="${name}" min="${min}" max="${max}" name="${name}" value="${value}" oninput=${updateParamsInt} /> | ||
| 671 | <span>${value}</span> | ||
| 672 | </div> | ||
| 673 | ` | ||
| 674 | }; | ||
| 675 | |||
| 676 | const BoolField = ({ label, name, value }) => { | ||
| 677 | return html` | ||
| 678 | <div> | ||
| 679 | <label for="${name}">${label}</label> | ||
| 680 | <input type="checkbox" id="${name}" name="${name}" checked="${value}" onclick=${updateParamsBool} /> | ||
| 681 | </div> | ||
| 682 | ` | ||
| 683 | }; | ||
| 684 | |||
| 685 | const userTemplateReset = (e) => { | ||
| 686 | e.preventDefault(); | ||
| 687 | userTemplateResetToDefaultAndApply() | ||
| 688 | } | ||
| 689 | |||
| 690 | const UserTemplateResetButton = () => { | ||
| 691 | if (selectedUserTemplate.value.name == 'default') { | ||
| 692 | return html` | ||
| 693 | <button disabled>Using default template</button> | ||
| 694 | ` | ||
| 695 | } | ||
| 696 | |||
| 697 | return html` | ||
| 698 | <button onclick=${userTemplateReset}>Reset all to default</button> | ||
| 699 | ` | ||
| 700 | }; | ||
| 701 | |||
| 702 | useEffect(() => { | ||
| 703 | // autosave template on every change | ||
| 704 | userTemplateAutosave() | ||
| 705 | }, [session.value, params.value]) | ||
| 706 | |||
| 707 | const GrammarControl = () => ( | ||
| 708 | html` | ||
| 709 | <div> | ||
| 710 | <label for="template">Grammar</label> | ||
| 711 | <textarea id="grammar" name="grammar" placeholder="Use gbnf or JSON Schema+convert" value="${params.value.grammar}" rows=4 oninput=${updateParams}/> | ||
| 712 | <input type="text" name="prop-order" placeholder="order: prop1,prop2,prop3" oninput=${updateGrammarJsonSchemaPropOrder} /> | ||
| 713 | <button type="button" onclick=${convertJSONSchemaGrammar}>Convert JSON Schema</button> | ||
| 714 | </div> | ||
| 715 | ` | ||
| 716 | ); | ||
| 717 | |||
| 718 | const PromptControlFieldSet = () => ( | ||
| 719 | html` | ||
| 720 | <fieldset> | ||
| 721 | <div> | ||
| 722 | <label htmlFor="prompt">Prompt</label> | ||
| 723 | <textarea type="text" name="prompt" value="${session.value.prompt}" oninput=${updateSession}/> | ||
| 724 | </div> | ||
| 725 | </fieldset> | ||
| 726 | ` | ||
| 727 | ); | ||
| 728 | |||
| 729 | const ChatConfigForm = () => ( | ||
| 730 | html` | ||
| 731 | ${PromptControlFieldSet()} | ||
| 732 | |||
| 733 | <fieldset class="two"> | ||
| 734 | <div> | ||
| 735 | <label for="user">User name</label> | ||
| 736 | <input type="text" name="user" value="${session.value.user}" oninput=${updateSession} /> | ||
| 737 | </div> | ||
| 738 | |||
| 739 | <div> | ||
| 740 | <label for="bot">Bot name</label> | ||
| 741 | <input type="text" name="char" value="${session.value.char}" oninput=${updateSession} /> | ||
| 742 | </div> | ||
| 743 | </fieldset> | ||
| 744 | |||
| 745 | <fieldset> | ||
| 746 | <div> | ||
| 747 | <label for="template">Prompt template</label> | ||
| 748 | <textarea id="template" name="template" value="${session.value.template}" rows=4 oninput=${updateSession}/> | ||
| 749 | </div> | ||
| 750 | |||
| 751 | <div> | ||
| 752 | <label for="template">Chat history template</label> | ||
| 753 | <textarea id="template" name="historyTemplate" value="${session.value.historyTemplate}" rows=1 oninput=${updateSession}/> | ||
| 754 | </div> | ||
| 755 | ${GrammarControl()} | ||
| 756 | </fieldset> | ||
| 757 | ` | ||
| 758 | ); | ||
| 759 | |||
| 760 | const CompletionConfigForm = () => ( | ||
| 761 | html` | ||
| 762 | ${PromptControlFieldSet()} | ||
| 763 | <fieldset>${GrammarControl()}</fieldset> | ||
| 764 | ` | ||
| 765 | ); | ||
| 766 | |||
| 767 | return html` | ||
| 768 | <form> | ||
| 769 | <fieldset class="two"> | ||
| 770 | <${UserTemplateResetButton}/> | ||
| 771 | <div> | ||
| 772 | <label class="slim"><input type="radio" name="type" value="chat" checked=${session.value.type === "chat"} oninput=${updateSession} /> Chat</label> | ||
| 773 | <label class="slim"><input type="radio" name="type" value="completion" checked=${session.value.type === "completion"} oninput=${updateSession} /> Completion</label> | ||
| 774 | </div> | ||
| 775 | </fieldset> | ||
| 776 | |||
| 777 | ${session.value.type === 'chat' ? ChatConfigForm() : CompletionConfigForm()} | ||
| 778 | |||
| 779 | <fieldset class="two"> | ||
| 780 | ${IntField({ label: "Predictions", max: 2048, min: -1, name: "n_predict", value: params.value.n_predict })} | ||
| 781 | ${FloatField({ label: "Temperature", max: 2.0, min: 0.0, name: "temperature", step: 0.01, value: params.value.temperature })} | ||
| 782 | ${FloatField({ label: "Penalize repeat sequence", max: 2.0, min: 0.0, name: "repeat_penalty", step: 0.01, value: params.value.repeat_penalty })} | ||
| 783 | ${IntField({ label: "Consider N tokens for penalize", max: 2048, min: 0, name: "repeat_last_n", value: params.value.repeat_last_n })} | ||
| 784 | ${IntField({ label: "Top-K sampling", max: 100, min: -1, name: "top_k", value: params.value.top_k })} | ||
| 785 | ${FloatField({ label: "Top-P sampling", max: 1.0, min: 0.0, name: "top_p", step: 0.01, value: params.value.top_p })} | ||
| 786 | ${FloatField({ label: "Min-P sampling", max: 1.0, min: 0.0, name: "min_p", step: 0.01, value: params.value.min_p })} | ||
| 787 | </fieldset> | ||
| 788 | <details> | ||
| 789 | <summary>More options</summary> | ||
| 790 | <fieldset class="two"> | ||
| 791 | ${FloatField({ label: "Typical P", max: 1.0, min: 0.0, name: "typical_p", step: 0.01, value: params.value.typical_p })} | ||
| 792 | ${FloatField({ label: "Presence penalty", max: 1.0, min: 0.0, name: "presence_penalty", step: 0.01, value: params.value.presence_penalty })} | ||
| 793 | ${FloatField({ label: "Frequency penalty", max: 1.0, min: 0.0, name: "frequency_penalty", step: 0.01, value: params.value.frequency_penalty })} | ||
| 794 | </fieldset> | ||
| 795 | <hr /> | ||
| 796 | <fieldset class="three"> | ||
| 797 | <div> | ||
| 798 | <label><input type="radio" name="mirostat" value="0" checked=${params.value.mirostat == 0} oninput=${updateParamsInt} /> no Mirostat</label> | ||
| 799 | <label><input type="radio" name="mirostat" value="1" checked=${params.value.mirostat == 1} oninput=${updateParamsInt} /> Mirostat v1</label> | ||
| 800 | <label><input type="radio" name="mirostat" value="2" checked=${params.value.mirostat == 2} oninput=${updateParamsInt} /> Mirostat v2</label> | ||
| 801 | </div> | ||
| 802 | ${FloatField({ label: "Mirostat tau", max: 10.0, min: 0.0, name: "mirostat_tau", step: 0.01, value: params.value.mirostat_tau })} | ||
| 803 | ${FloatField({ label: "Mirostat eta", max: 1.0, min: 0.0, name: "mirostat_eta", step: 0.01, value: params.value.mirostat_eta })} | ||
| 804 | </fieldset> | ||
| 805 | <fieldset> | ||
| 806 | ${IntField({ label: "Show Probabilities", max: 10, min: 0, name: "n_probs", value: params.value.n_probs })} | ||
| 807 | </fieldset> | ||
| 808 | <fieldset> | ||
| 809 | ${IntField({ label: "Min Probabilities from each Sampler", max: 10, min: 0, name: "min_keep", value: params.value.min_keep })} | ||
| 810 | </fieldset> | ||
| 811 | <fieldset> | ||
| 812 | <label for="api_key">API Key</label> | ||
| 813 | <input type="text" name="api_key" value="${params.value.api_key}" placeholder="Enter API key" oninput=${updateParams} /> | ||
| 814 | </fieldset> | ||
| 815 | </details> | ||
| 816 | </form> | ||
| 817 | ` | ||
| 818 | } | ||
| 819 | |||
| 820 | const probColor = (p) => { | ||
| 821 | const r = Math.floor(192 * (1 - p)); | ||
| 822 | const g = Math.floor(192 * p); | ||
| 823 | return `rgba(${r},${g},0,0.3)`; | ||
| 824 | } | ||
| 825 | |||
| 826 | const Probabilities = (params) => { | ||
| 827 | return params.data.map(msg => { | ||
| 828 | const { completion_probabilities } = msg; | ||
| 829 | if ( | ||
| 830 | !completion_probabilities || | ||
| 831 | completion_probabilities.length === 0 | ||
| 832 | ) return msg.content | ||
| 833 | |||
| 834 | if (completion_probabilities.length > 1) { | ||
| 835 | // Not for byte pair | ||
| 836 | if (completion_probabilities[0].content.startsWith('byte: \\')) return msg.content | ||
| 837 | |||
| 838 | const splitData = completion_probabilities.map(prob => ({ | ||
| 839 | content: prob.content, | ||
| 840 | completion_probabilities: [prob] | ||
| 841 | })) | ||
| 842 | return html`<${Probabilities} data=${splitData} />` | ||
| 843 | } | ||
| 844 | |||
| 845 | const { probs, content } = completion_probabilities[0] | ||
| 846 | const found = probs.find(p => p.tok_str === msg.content) | ||
| 847 | const pColor = found ? probColor(found.prob) : 'transparent' | ||
| 848 | |||
| 849 | const popoverChildren = html` | ||
| 850 | <div class="prob-set"> | ||
| 851 | ${probs.map((p, index) => { | ||
| 852 | return html` | ||
| 853 | <div | ||
| 854 | key=${index} | ||
| 855 | title=${`prob: ${p.prob}`} | ||
| 856 | style=${{ | ||
| 857 | padding: '0.3em', | ||
| 858 | backgroundColor: p.tok_str === content ? probColor(p.prob) : 'transparent' | ||
| 859 | }} | ||
| 860 | > | ||
| 861 | <span>${p.tok_str}: </span> | ||
| 862 | <span>${Math.floor(p.prob * 100)}%</span> | ||
| 863 | </div> | ||
| 864 | ` | ||
| 865 | })} | ||
| 866 | </div> | ||
| 867 | ` | ||
| 868 | |||
| 869 | return html` | ||
| 870 | <${Popover} style=${{ backgroundColor: pColor }} popoverChildren=${popoverChildren}> | ||
| 871 | ${msg.content.match(/\n/gim) ? html`<br />` : msg.content} | ||
| 872 | </> | ||
| 873 | ` | ||
| 874 | }); | ||
| 875 | } | ||
| 876 | |||
| 877 | // poor mans markdown replacement | ||
| 878 | const Markdownish = (params) => { | ||
| 879 | const md = params.text | ||
| 880 | .replace(/&/g, '&') | ||
| 881 | .replace(/</g, '<') | ||
| 882 | .replace(/>/g, '>') | ||
| 883 | .replace(/^#{1,6} (.*)$/gim, '<h3>$1</h3>') | ||
| 884 | .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') | ||
| 885 | .replace(/__(.*?)__/g, '<strong>$1</strong>') | ||
| 886 | .replace(/\*(.*?)\*/g, '<em>$1</em>') | ||
| 887 | .replace(/_(.*?)_/g, '<em>$1</em>') | ||
| 888 | .replace(/```.*?\n([\s\S]*?)```/g, '<pre><code>$1</code></pre>') | ||
| 889 | .replace(/`(.*?)`/g, '<code>$1</code>') | ||
| 890 | .replace(/\n/gim, '<br />'); | ||
| 891 | return html`<span dangerouslySetInnerHTML=${{ __html: md }} />`; | ||
| 892 | }; | ||
| 893 | |||
| 894 | const ModelGenerationInfo = (params) => { | ||
| 895 | if (!llamaStats.value) { | ||
| 896 | return html`<span/>` | ||
| 897 | } | ||
| 898 | return html` | ||
| 899 | <span> | ||
| 900 | ${llamaStats.value.tokens_predicted} predicted, ${llamaStats.value.tokens_cached} cached, ${llamaStats.value.timings.predicted_per_token_ms.toFixed()}ms per token, ${llamaStats.value.timings.predicted_per_second.toFixed(2)} tokens per second | ||
| 901 | </span> | ||
| 902 | ` | ||
| 903 | } | ||
| 904 | |||
| 905 | // simple popover impl | ||
| 906 | const Popover = (props) => { | ||
| 907 | const isOpen = useSignal(false); | ||
| 908 | const position = useSignal({ top: '0px', left: '0px' }); | ||
| 909 | const buttonRef = useRef(null); | ||
| 910 | const popoverRef = useRef(null); | ||
| 911 | |||
| 912 | const togglePopover = () => { | ||
| 913 | if (buttonRef.current) { | ||
| 914 | const rect = buttonRef.current.getBoundingClientRect(); | ||
| 915 | position.value = { | ||
| 916 | top: `${rect.bottom + window.scrollY}px`, | ||
| 917 | left: `${rect.left + window.scrollX}px`, | ||
| 918 | }; | ||
| 919 | } | ||
| 920 | isOpen.value = !isOpen.value; | ||
| 921 | }; | ||
| 922 | |||
| 923 | const handleClickOutside = (event) => { | ||
| 924 | if (popoverRef.current && !popoverRef.current.contains(event.target) && !buttonRef.current.contains(event.target)) { | ||
| 925 | isOpen.value = false; | ||
| 926 | } | ||
| 927 | }; | ||
| 928 | |||
| 929 | useEffect(() => { | ||
| 930 | document.addEventListener('mousedown', handleClickOutside); | ||
| 931 | return () => { | ||
| 932 | document.removeEventListener('mousedown', handleClickOutside); | ||
| 933 | }; | ||
| 934 | }, []); | ||
| 935 | |||
| 936 | return html` | ||
| 937 | <span style=${props.style} ref=${buttonRef} onClick=${togglePopover}>${props.children}</span> | ||
| 938 | ${isOpen.value && html` | ||
| 939 | <${Portal} into="#portal"> | ||
| 940 | <div | ||
| 941 | ref=${popoverRef} | ||
| 942 | class="popover-content" | ||
| 943 | style=${{ | ||
| 944 | top: position.value.top, | ||
| 945 | left: position.value.left, | ||
| 946 | }} | ||
| 947 | > | ||
| 948 | ${props.popoverChildren} | ||
| 949 | </div> | ||
| 950 | </${Portal}> | ||
| 951 | `} | ||
| 952 | `; | ||
| 953 | }; | ||
| 954 | |||
| 955 | // Source: preact-portal (https://github.com/developit/preact-portal/blob/master/src/preact-portal.js) | ||
| 956 | /** Redirect rendering of descendants into the given CSS selector */ | ||
| 957 | class Portal extends Component { | ||
| 958 | componentDidUpdate(props) { | ||
| 959 | for (let i in props) { | ||
| 960 | if (props[i] !== this.props[i]) { | ||
| 961 | return setTimeout(this.renderLayer); | ||
| 962 | } | ||
| 963 | } | ||
| 964 | } | ||
| 965 | |||
| 966 | componentDidMount() { | ||
| 967 | this.isMounted = true; | ||
| 968 | this.renderLayer = this.renderLayer.bind(this); | ||
| 969 | this.renderLayer(); | ||
| 970 | } | ||
| 971 | |||
| 972 | componentWillUnmount() { | ||
| 973 | this.renderLayer(false); | ||
| 974 | this.isMounted = false; | ||
| 975 | if (this.remote && this.remote.parentNode) this.remote.parentNode.removeChild(this.remote); | ||
| 976 | } | ||
| 977 | |||
| 978 | findNode(node) { | ||
| 979 | return typeof node === 'string' ? document.querySelector(node) : node; | ||
| 980 | } | ||
| 981 | |||
| 982 | renderLayer(show = true) { | ||
| 983 | if (!this.isMounted) return; | ||
| 984 | |||
| 985 | // clean up old node if moving bases: | ||
| 986 | if (this.props.into !== this.intoPointer) { | ||
| 987 | this.intoPointer = this.props.into; | ||
| 988 | if (this.into && this.remote) { | ||
| 989 | this.remote = render(html`<${PortalProxy} />`, this.into, this.remote); | ||
| 990 | } | ||
| 991 | this.into = this.findNode(this.props.into); | ||
| 992 | } | ||
| 993 | |||
| 994 | this.remote = render(html` | ||
| 995 | <${PortalProxy} context=${this.context}> | ||
| 996 | ${show && this.props.children || null} | ||
| 997 | </${PortalProxy}> | ||
| 998 | `, this.into, this.remote); | ||
| 999 | } | ||
| 1000 | |||
| 1001 | render() { | ||
| 1002 | return null; | ||
| 1003 | } | ||
| 1004 | } | ||
| 1005 | // high-order component that renders its first child if it exists. | ||
| 1006 | // used as a conditional rendering proxy. | ||
| 1007 | class PortalProxy extends Component { | ||
| 1008 | getChildContext() { | ||
| 1009 | return this.props.context; | ||
| 1010 | } | ||
| 1011 | render({ children }) { | ||
| 1012 | return children || null; | ||
| 1013 | } | ||
| 1014 | } | ||
| 1015 | |||
| 1016 | function App(props) { | ||
| 1017 | useEffect(() => { | ||
| 1018 | const query = new URLSearchParams(location.search).get("q"); | ||
| 1019 | if (query) chat(query); | ||
| 1020 | }, []); | ||
| 1021 | |||
| 1022 | return html` | ||
| 1023 | <div class="mode-${session.value.type}"> | ||
| 1024 | <header> | ||
| 1025 | <img src="llama_cpp.png" style="width:100%"/> | ||
| 1026 | </header> | ||
| 1027 | |||
| 1028 | <section id="write"> | ||
| 1029 | <${session.value.type === 'chat' ? MessageInput : CompletionControls} /> | ||
| 1030 | </section> | ||
| 1031 | |||
| 1032 | <main id="content"> | ||
| 1033 | <${chatStarted.value ? ChatLog : ConfigForm} /> | ||
| 1034 | </main> | ||
| 1035 | |||
| 1036 | |||
| 1037 | <footer> | ||
| 1038 | <p><${ModelGenerationInfo} /></p> | ||
| 1039 | <p>Powered by <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a> and <a href="https://ggml.ai">ggml.ai</a>.</p> | ||
| 1040 | </footer> | ||
| 1041 | </div> | ||
| 1042 | `; | ||
| 1043 | } | ||
| 1044 | |||
| 1045 | render(h(App), document.querySelector('#container')); | ||
| 1046 | </script> | ||
| 1047 | </head> | ||
| 1048 | |||
| 1049 | <body> | ||
| 1050 | <div id="container"> | ||
| 1051 | <input type="file" id="fileInput" accept="image/*" style="display: none;"> | ||
| 1052 | </div> | ||
| 1053 | <div id="portal"></div> | ||
| 1054 | </body> | ||
| 1055 | |||
| 1056 | </html> | ||
diff --git a/llama.cpp/tools/server/themes/wild/llama_cpp.png b/llama.cpp/tools/server/themes/wild/llama_cpp.png new file mode 100644 index 0000000..bad1dc9 --- /dev/null +++ b/llama.cpp/tools/server/themes/wild/llama_cpp.png | |||
| Binary files differ | |||
diff --git a/llama.cpp/tools/server/themes/wild/llamapattern.png b/llama.cpp/tools/server/themes/wild/llamapattern.png new file mode 100644 index 0000000..2a159ce --- /dev/null +++ b/llama.cpp/tools/server/themes/wild/llamapattern.png | |||
| Binary files differ | |||
diff --git a/llama.cpp/tools/server/themes/wild/wild.png b/llama.cpp/tools/server/themes/wild/wild.png new file mode 100644 index 0000000..46ffa0f --- /dev/null +++ b/llama.cpp/tools/server/themes/wild/wild.png | |||
| Binary files differ | |||
