mirror of
https://github.com/ggerganov/llama.cpp.git
synced 2024-12-27 06:39:25 +01:00
server : (web UI) add copy button for code block, fix api key (#10242)
* server : (web ui) add copy btn for code blocks * fix problem with api key * use settings-modal-short-input component * always show copy btn for code snippet
This commit is contained in:
parent
231f9360d9
commit
9901068ac7
@ -12,7 +12,7 @@
|
|||||||
.markdown {
|
.markdown {
|
||||||
h1, h2, h3, h4, h5, h6, ul, ol, li { all: revert; }
|
h1, h2, h3, h4, h5, h6, ul, ol, li { all: revert; }
|
||||||
pre {
|
pre {
|
||||||
@apply whitespace-pre-wrap my-4 rounded-lg p-2;
|
@apply whitespace-pre-wrap rounded-lg p-2;
|
||||||
border: 1px solid currentColor;
|
border: 1px solid currentColor;
|
||||||
}
|
}
|
||||||
/* TODO: fix markdown table */
|
/* TODO: fix markdown table */
|
||||||
@ -25,8 +25,11 @@
|
|||||||
.bg-base-200 {background-color: var(--fallback-b2,oklch(var(--b2)/1))}
|
.bg-base-200 {background-color: var(--fallback-b2,oklch(var(--b2)/1))}
|
||||||
.bg-base-300 {background-color: var(--fallback-b3,oklch(var(--b3)/1))}
|
.bg-base-300 {background-color: var(--fallback-b3,oklch(var(--b3)/1))}
|
||||||
.text-base-content {color: var(--fallback-bc,oklch(var(--bc)/1))}
|
.text-base-content {color: var(--fallback-bc,oklch(var(--bc)/1))}
|
||||||
|
.show-on-hover {
|
||||||
|
@apply opacity-0 group-hover:opacity-100;
|
||||||
|
}
|
||||||
.btn-mini {
|
.btn-mini {
|
||||||
@apply cursor-pointer opacity-0 group-hover:opacity-100 hover:shadow-md;
|
@apply cursor-pointer hover:shadow-md;
|
||||||
}
|
}
|
||||||
.chat-screen { max-width: 900px; }
|
.chat-screen { max-width: 900px; }
|
||||||
/* because the default bubble color is quite dark, we will make a custom one using bg-base-300 */
|
/* because the default bubble color is quite dark, we will make a custom one using bg-base-300 */
|
||||||
@ -152,14 +155,14 @@
|
|||||||
<!-- actions for each message -->
|
<!-- actions for each message -->
|
||||||
<div :class="{'text-right': msg.role === 'user'}" class="mx-4 mt-2 mb-2">
|
<div :class="{'text-right': msg.role === 'user'}" class="mx-4 mt-2 mb-2">
|
||||||
<!-- user message -->
|
<!-- user message -->
|
||||||
<button v-if="msg.role === 'user'" class="badge btn-mini" @click="editingMsg = msg" :disabled="isGenerating">
|
<button v-if="msg.role === 'user'" class="badge btn-minishow-on-hover " @click="editingMsg = msg" :disabled="isGenerating">
|
||||||
✍️ Edit
|
✍️ Edit
|
||||||
</button>
|
</button>
|
||||||
<!-- assistant message -->
|
<!-- assistant message -->
|
||||||
<button v-if="msg.role === 'assistant'" class="badge btn-mini mr-2" @click="regenerateMsg(msg)" :disabled="isGenerating">
|
<button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="regenerateMsg(msg)" :disabled="isGenerating">
|
||||||
🔄 Regenerate
|
🔄 Regenerate
|
||||||
</button>
|
</button>
|
||||||
<button v-if="msg.role === 'assistant'" class="badge btn-mini mr-2" @click="copyMsg(msg)" :disabled="isGenerating">
|
<button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="copyMsg(msg)" :disabled="isGenerating">
|
||||||
📋 Copy
|
📋 Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -196,12 +199,13 @@
|
|||||||
<h3 class="text-lg font-bold mb-6">Settings</h3>
|
<h3 class="text-lg font-bold mb-6">Settings</h3>
|
||||||
<div class="h-[calc(90vh-12rem)] overflow-y-auto">
|
<div class="h-[calc(90vh-12rem)] overflow-y-auto">
|
||||||
<p class="opacity-40 mb-6">Settings below are saved in browser's localStorage</p>
|
<p class="opacity-40 mb-6">Settings below are saved in browser's localStorage</p>
|
||||||
|
<settings-modal-short-input :config-key="'apiKey'" :config-default="configDefault" :config-info="configInfo" v-model="config.apiKey"></settings-modal-short-input>
|
||||||
<label class="form-control mb-2">
|
<label class="form-control mb-2">
|
||||||
<div class="label">System Message</div>
|
<div class="label">System Message</div>
|
||||||
<textarea class="textarea textarea-bordered h-24" :placeholder="'Default: ' + configDefault.systemMessage" v-model="config.systemMessage"></textarea>
|
<textarea class="textarea textarea-bordered h-24" :placeholder="'Default: ' + configDefault.systemMessage" v-model="config.systemMessage"></textarea>
|
||||||
</label>
|
</label>
|
||||||
<template v-for="configKey in ['temperature', 'top_k', 'top_p', 'min_p', 'max_tokens']">
|
<template v-for="configKey in ['temperature', 'top_k', 'top_p', 'min_p', 'max_tokens']">
|
||||||
<settings-modal-numeric-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
|
<settings-modal-short-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
|
||||||
</template>
|
</template>
|
||||||
<!-- TODO: add more sampling-related configs, please regroup them into different "collapse" sections -->
|
<!-- TODO: add more sampling-related configs, please regroup them into different "collapse" sections -->
|
||||||
<!-- Section: Other sampler settings -->
|
<!-- Section: Other sampler settings -->
|
||||||
@ -209,7 +213,7 @@
|
|||||||
<summary class="collapse-title font-bold">Other sampler settings</summary>
|
<summary class="collapse-title font-bold">Other sampler settings</summary>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<template v-for="configKey in ['dynatemp_range', 'dynatemp_exponent', 'typical_p', 'xtc_probability', 'xtc_threshold']">
|
<template v-for="configKey in ['dynatemp_range', 'dynatemp_exponent', 'typical_p', 'xtc_probability', 'xtc_threshold']">
|
||||||
<settings-modal-numeric-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
|
<settings-modal-short-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@ -218,7 +222,7 @@
|
|||||||
<summary class="collapse-title font-bold">Penalties settings</summary>
|
<summary class="collapse-title font-bold">Penalties settings</summary>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<template v-for="configKey in ['repeat_last_n', 'repeat_penalty', 'presence_penalty', 'frequency_penalty', 'dry_multiplier', 'dry_base', 'dry_allowed_length', 'dry_penalty_last_n']">
|
<template v-for="configKey in ['repeat_last_n', 'repeat_penalty', 'presence_penalty', 'frequency_penalty', 'dry_multiplier', 'dry_base', 'dry_allowed_length', 'dry_penalty_last_n']">
|
||||||
<settings-modal-numeric-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
|
<settings-modal-short-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@ -245,7 +249,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Template to be used by settings modal -->
|
<!-- Template to be used by settings modal -->
|
||||||
<template id="settings-modal-numeric-input">
|
<template id="settings-modal-short-input">
|
||||||
<label class="input input-bordered join-item grow flex items-center gap-2 mb-2">
|
<label class="input input-bordered join-item grow flex items-center gap-2 mb-2">
|
||||||
<!-- Show help message on hovering on the input label -->
|
<!-- Show help message on hovering on the input label -->
|
||||||
<div class="dropdown dropdown-hover">
|
<div class="dropdown dropdown-hover">
|
||||||
@ -264,9 +268,13 @@
|
|||||||
import { createApp, defineComponent, shallowRef, computed, h } from './deps_vue.esm-browser.js';
|
import { createApp, defineComponent, shallowRef, computed, h } from './deps_vue.esm-browser.js';
|
||||||
import { llama } from './completion.js';
|
import { llama } from './completion.js';
|
||||||
|
|
||||||
|
// utility functions
|
||||||
const isString = (x) => !!x.toLowerCase;
|
const isString = (x) => !!x.toLowerCase;
|
||||||
const isNumeric = (n) => !isString(n) && !isNaN(n);
|
const isNumeric = (n) => !isString(n) && !isNaN(n);
|
||||||
|
const escapeAttr = (str) => str.replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
const copyStr = (str) => navigator.clipboard.writeText(str);
|
||||||
|
|
||||||
|
// constants
|
||||||
const BASE_URL = localStorage.getItem('base') // for debugging
|
const BASE_URL = localStorage.getItem('base') // for debugging
|
||||||
|| (new URL('.', document.baseURI).href).toString(); // for production
|
|| (new URL('.', document.baseURI).href).toString(); // for production
|
||||||
const CONFIG_DEFAULT = {
|
const CONFIG_DEFAULT = {
|
||||||
@ -295,7 +303,7 @@
|
|||||||
custom: '', // custom json-stringified object
|
custom: '', // custom json-stringified object
|
||||||
};
|
};
|
||||||
const CONFIG_INFO = {
|
const CONFIG_INFO = {
|
||||||
apiKey: '',
|
apiKey: 'Set the API Key if you are using --api-key option for the server.',
|
||||||
systemMessage: 'The starting message that defines how model should behave.',
|
systemMessage: 'The starting message that defines how model should behave.',
|
||||||
temperature: 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.',
|
temperature: 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.',
|
||||||
dynatemp_range: 'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.',
|
dynatemp_range: 'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.',
|
||||||
@ -325,19 +333,28 @@
|
|||||||
// markdown support
|
// markdown support
|
||||||
const VueMarkdown = defineComponent(
|
const VueMarkdown = defineComponent(
|
||||||
(props) => {
|
(props) => {
|
||||||
const md = shallowRef(new markdownit(props.options ?? { breaks: true }));
|
const md = shallowRef(new markdownit({ breaks: true }));
|
||||||
for (const plugin of props.plugins ?? []) {
|
const origFenchRenderer = md.value.renderer.rules.fence;
|
||||||
md.value.use(plugin);
|
md.value.renderer.rules.fence = (tokens, idx, ...args) => {
|
||||||
}
|
const content = tokens[idx].content;
|
||||||
|
const origRendered = origFenchRenderer(tokens, idx, ...args);
|
||||||
|
return `<div class="relative my-4">
|
||||||
|
<div class="text-right sticky top-4 mb-2 mr-2 h-0">
|
||||||
|
<button class="badge btn-mini" onclick="copyStr(${escapeAttr(JSON.stringify(content))})">📋 Copy</button>
|
||||||
|
</div>
|
||||||
|
${origRendered}
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
window.copyStr = copyStr;
|
||||||
const content = computed(() => md.value.render(props.source));
|
const content = computed(() => md.value.render(props.source));
|
||||||
return () => h("div", { innerHTML: content.value });
|
return () => h("div", { innerHTML: content.value });
|
||||||
},
|
},
|
||||||
{ props: ["source", "options", "plugins"] }
|
{ props: ["source"] }
|
||||||
);
|
);
|
||||||
|
|
||||||
// inout field to be used by settings modal
|
// inout field to be used by settings modal
|
||||||
const SettingsModalNumericInput = defineComponent({
|
const SettingsModalShortInput = defineComponent({
|
||||||
template: document.getElementById('settings-modal-numeric-input').innerHTML,
|
template: document.getElementById('settings-modal-short-input').innerHTML,
|
||||||
props: ['configKey', 'configDefault', 'configInfo', 'modelValue'],
|
props: ['configKey', 'configDefault', 'configInfo', 'modelValue'],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -390,7 +407,11 @@
|
|||||||
if (!conv) return;
|
if (!conv) return;
|
||||||
const msg = conv.messages.pop();
|
const msg = conv.messages.pop();
|
||||||
conv.lastModified = Date.now();
|
conv.lastModified = Date.now();
|
||||||
localStorage.setItem(convId, JSON.stringify(conv));
|
if (conv.messages.length === 0) {
|
||||||
|
StorageUtils.remove(convId);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(convId, JSON.stringify(conv));
|
||||||
|
}
|
||||||
return msg;
|
return msg;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -431,7 +452,7 @@
|
|||||||
const mainApp = createApp({
|
const mainApp = createApp({
|
||||||
components: {
|
components: {
|
||||||
VueMarkdown,
|
VueMarkdown,
|
||||||
SettingsModalNumericInput,
|
SettingsModalShortInput,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -587,6 +608,7 @@
|
|||||||
this.isGenerating = false;
|
this.isGenerating = false;
|
||||||
this.stopGeneration = () => {};
|
this.stopGeneration = () => {};
|
||||||
this.fetchMessages();
|
this.fetchMessages();
|
||||||
|
chatScrollToBottom();
|
||||||
},
|
},
|
||||||
|
|
||||||
// message actions
|
// message actions
|
||||||
@ -600,7 +622,7 @@
|
|||||||
this.generateMessage(currConvId);
|
this.generateMessage(currConvId);
|
||||||
},
|
},
|
||||||
copyMsg(msg) {
|
copyMsg(msg) {
|
||||||
navigator.clipboard.writeText(msg.content);
|
copyStr(msg.content);
|
||||||
},
|
},
|
||||||
editUserMsgAndRegenerate(msg) {
|
editUserMsgAndRegenerate(msg) {
|
||||||
if (this.isGenerating) return;
|
if (this.isGenerating) return;
|
||||||
|
@ -102,6 +102,12 @@ struct server_task_result {
|
|||||||
bool error;
|
bool error;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct server_static_file {
|
||||||
|
const unsigned char * data;
|
||||||
|
unsigned int size;
|
||||||
|
const char * mime_type;
|
||||||
|
};
|
||||||
|
|
||||||
struct slot_params {
|
struct slot_params {
|
||||||
bool stream = true;
|
bool stream = true;
|
||||||
bool cache_prompt = false; // remember the prompt to avoid reprocessing all prompt
|
bool cache_prompt = false; // remember the prompt to avoid reprocessing all prompt
|
||||||
@ -2259,6 +2265,16 @@ int main(int argc, char ** argv) {
|
|||||||
LOG_INF("%s\n", common_params_get_system_info(params).c_str());
|
LOG_INF("%s\n", common_params_get_system_info(params).c_str());
|
||||||
LOG_INF("\n");
|
LOG_INF("\n");
|
||||||
|
|
||||||
|
// static files
|
||||||
|
std::map<std::string, server_static_file> static_files = {
|
||||||
|
{ "/", { index_html, index_html_len, "text/html; charset=utf-8" }},
|
||||||
|
{ "/completion.js", { completion_js, completion_js_len, "text/javascript; charset=utf-8" }},
|
||||||
|
{ "/deps_daisyui.min.css", { deps_daisyui_min_css, deps_daisyui_min_css_len, "text/css; charset=utf-8" }},
|
||||||
|
{ "/deps_markdown-it.js", { deps_markdown_it_js, deps_markdown_it_js_len, "text/javascript; charset=utf-8" }},
|
||||||
|
{ "/deps_tailwindcss.js", { deps_tailwindcss_js, deps_tailwindcss_js_len, "text/javascript; charset=utf-8" }},
|
||||||
|
{ "/deps_vue.esm-browser.js", { deps_vue_esm_browser_js, deps_vue_esm_browser_js_len, "text/javascript; charset=utf-8" }},
|
||||||
|
};
|
||||||
|
|
||||||
std::unique_ptr<httplib::Server> svr;
|
std::unique_ptr<httplib::Server> svr;
|
||||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||||
if (params.ssl_file_key != "" && params.ssl_file_cert != "") {
|
if (params.ssl_file_key != "" && params.ssl_file_cert != "") {
|
||||||
@ -2339,7 +2355,7 @@ int main(int argc, char ** argv) {
|
|||||||
// Middlewares
|
// Middlewares
|
||||||
//
|
//
|
||||||
|
|
||||||
auto middleware_validate_api_key = [¶ms, &res_error](const httplib::Request & req, httplib::Response & res) {
|
auto middleware_validate_api_key = [¶ms, &res_error, &static_files](const httplib::Request & req, httplib::Response & res) {
|
||||||
static const std::unordered_set<std::string> public_endpoints = {
|
static const std::unordered_set<std::string> public_endpoints = {
|
||||||
"/health",
|
"/health",
|
||||||
"/models",
|
"/models",
|
||||||
@ -2351,8 +2367,8 @@ int main(int argc, char ** argv) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If path is public, skip validation
|
// If path is public or is static file, skip validation
|
||||||
if (public_endpoints.find(req.path) != public_endpoints.end()) {
|
if (public_endpoints.find(req.path) != public_endpoints.end() || static_files.find(req.path) != static_files.end()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3096,13 +3112,6 @@ int main(int argc, char ** argv) {
|
|||||||
res.status = 200; // HTTP OK
|
res.status = 200; // HTTP OK
|
||||||
};
|
};
|
||||||
|
|
||||||
auto handle_static_file = [](unsigned char * content, size_t len, const char * mime_type) {
|
|
||||||
return [content, len, mime_type](const httplib::Request &, httplib::Response & res) {
|
|
||||||
res.set_content(reinterpret_cast<const char*>(content), len, mime_type);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Router
|
// Router
|
||||||
//
|
//
|
||||||
@ -3117,12 +3126,13 @@ int main(int argc, char ** argv) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// using embedded static files
|
// using embedded static files
|
||||||
svr->Get("/", handle_static_file(index_html, index_html_len, "text/html; charset=utf-8"));
|
for (const auto & it : static_files) {
|
||||||
svr->Get("/completion.js", handle_static_file(completion_js, completion_js_len, "text/javascript; charset=utf-8"));
|
const server_static_file & static_file = it.second;
|
||||||
svr->Get("/deps_daisyui.min.css", handle_static_file(deps_daisyui_min_css, deps_daisyui_min_css_len, "text/css; charset=utf-8"));
|
svr->Get(it.first.c_str(), [&static_file](const httplib::Request &, httplib::Response & res) {
|
||||||
svr->Get("/deps_markdown-it.js", handle_static_file(deps_markdown_it_js, deps_markdown_it_js_len, "text/javascript; charset=utf-8"));
|
res.set_content(reinterpret_cast<const char*>(static_file.data), static_file.size, static_file.mime_type);
|
||||||
svr->Get("/deps_tailwindcss.js", handle_static_file(deps_tailwindcss_js, deps_tailwindcss_js_len, "text/javascript; charset=utf-8"));
|
return false;
|
||||||
svr->Get("/deps_vue.esm-browser.js", handle_static_file(deps_vue_esm_browser_js, deps_vue_esm_browser_js_len, "text/javascript; charset=utf-8"));
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// register API routes
|
// register API routes
|
||||||
|
Loading…
Reference in New Issue
Block a user