Skip to content

Commit 9901068

Browse files
authored
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
1 parent 231f936 commit 9901068

File tree

2 files changed

+68
-36
lines changed

2 files changed

+68
-36
lines changed

examples/server/public/index.html

+42-20
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
.markdown {
1313
h1, h2, h3, h4, h5, h6, ul, ol, li { all: revert; }
1414
pre {
15-
@apply whitespace-pre-wrap my-4 rounded-lg p-2;
15+
@apply whitespace-pre-wrap rounded-lg p-2;
1616
border: 1px solid currentColor;
1717
}
1818
/* TODO: fix markdown table */
@@ -25,8 +25,11 @@
2525
.bg-base-200 {background-color: var(--fallback-b2,oklch(var(--b2)/1))}
2626
.bg-base-300 {background-color: var(--fallback-b3,oklch(var(--b3)/1))}
2727
.text-base-content {color: var(--fallback-bc,oklch(var(--bc)/1))}
28+
.show-on-hover {
29+
@apply opacity-0 group-hover:opacity-100;
30+
}
2831
.btn-mini {
29-
@apply cursor-pointer opacity-0 group-hover:opacity-100 hover:shadow-md;
32+
@apply cursor-pointer hover:shadow-md;
3033
}
3134
.chat-screen { max-width: 900px; }
3235
/* because the default bubble color is quite dark, we will make a custom one using bg-base-300 */
@@ -152,14 +155,14 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
152155
<!-- actions for each message -->
153156
<div :class="{'text-right': msg.role === 'user'}" class="mx-4 mt-2 mb-2">
154157
<!-- user message -->
155-
<button v-if="msg.role === 'user'" class="badge btn-mini" @click="editingMsg = msg" :disabled="isGenerating">
158+
<button v-if="msg.role === 'user'" class="badge btn-minishow-on-hover " @click="editingMsg = msg" :disabled="isGenerating">
156159
✍️ Edit
157160
</button>
158161
<!-- assistant message -->
159-
<button v-if="msg.role === 'assistant'" class="badge btn-mini mr-2" @click="regenerateMsg(msg)" :disabled="isGenerating">
162+
<button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="regenerateMsg(msg)" :disabled="isGenerating">
160163
🔄 Regenerate
161164
</button>
162-
<button v-if="msg.role === 'assistant'" class="badge btn-mini mr-2" @click="copyMsg(msg)" :disabled="isGenerating">
165+
<button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="copyMsg(msg)" :disabled="isGenerating">
163166
📋 Copy
164167
</button>
165168
</div>
@@ -196,20 +199,21 @@ <h2 class="font-bold mb-4 ml-4">Conversations</h2>
196199
<h3 class="text-lg font-bold mb-6">Settings</h3>
197200
<div class="h-[calc(90vh-12rem)] overflow-y-auto">
198201
<p class="opacity-40 mb-6">Settings below are saved in browser's localStorage</p>
202+
<settings-modal-short-input :config-key="'apiKey'" :config-default="configDefault" :config-info="configInfo" v-model="config.apiKey"></settings-modal-short-input>
199203
<label class="form-control mb-2">
200204
<div class="label">System Message</div>
201205
<textarea class="textarea textarea-bordered h-24" :placeholder="'Default: ' + configDefault.systemMessage" v-model="config.systemMessage"></textarea>
202206
</label>
203207
<template v-for="configKey in ['temperature', 'top_k', 'top_p', 'min_p', 'max_tokens']">
204-
<settings-modal-numeric-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
208+
<settings-modal-short-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
205209
</template>
206210
<!-- TODO: add more sampling-related configs, please regroup them into different "collapse" sections -->
207211
<!-- Section: Other sampler settings -->
208212
<details class="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
209213
<summary class="collapse-title font-bold">Other sampler settings</summary>
210214
<div class="collapse-content">
211215
<template v-for="configKey in ['dynatemp_range', 'dynatemp_exponent', 'typical_p', 'xtc_probability', 'xtc_threshold']">
212-
<settings-modal-numeric-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
216+
<settings-modal-short-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
213217
</template>
214218
</div>
215219
</details>
@@ -218,7 +222,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
218222
<summary class="collapse-title font-bold">Penalties settings</summary>
219223
<div class="collapse-content">
220224
<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']">
221-
<settings-modal-numeric-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
225+
<settings-modal-short-input :config-key="configKey" :config-default="configDefault" :config-info="configInfo" v-model="config[configKey]" />
222226
</template>
223227
</div>
224228
</details>
@@ -245,7 +249,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
245249
</div>
246250

247251
<!-- Template to be used by settings modal -->
248-
<template id="settings-modal-numeric-input">
252+
<template id="settings-modal-short-input">
249253
<label class="input input-bordered join-item grow flex items-center gap-2 mb-2">
250254
<!-- Show help message on hovering on the input label -->
251255
<div class="dropdown dropdown-hover">
@@ -264,9 +268,13 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
264268
import { createApp, defineComponent, shallowRef, computed, h } from './deps_vue.esm-browser.js';
265269
import { llama } from './completion.js';
266270

271+
// utility functions
267272
const isString = (x) => !!x.toLowerCase;
268273
const isNumeric = (n) => !isString(n) && !isNaN(n);
274+
const escapeAttr = (str) => str.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
275+
const copyStr = (str) => navigator.clipboard.writeText(str);
269276

277+
// constants
270278
const BASE_URL = localStorage.getItem('base') // for debugging
271279
|| (new URL('.', document.baseURI).href).toString(); // for production
272280
const CONFIG_DEFAULT = {
@@ -295,7 +303,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
295303
custom: '', // custom json-stringified object
296304
};
297305
const CONFIG_INFO = {
298-
apiKey: '',
306+
apiKey: 'Set the API Key if you are using --api-key option for the server.',
299307
systemMessage: 'The starting message that defines how model should behave.',
300308
temperature: 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.',
301309
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 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
325333
// markdown support
326334
const VueMarkdown = defineComponent(
327335
(props) => {
328-
const md = shallowRef(new markdownit(props.options ?? { breaks: true }));
329-
for (const plugin of props.plugins ?? []) {
330-
md.value.use(plugin);
331-
}
336+
const md = shallowRef(new markdownit({ breaks: true }));
337+
const origFenchRenderer = md.value.renderer.rules.fence;
338+
md.value.renderer.rules.fence = (tokens, idx, ...args) => {
339+
const content = tokens[idx].content;
340+
const origRendered = origFenchRenderer(tokens, idx, ...args);
341+
return `<div class="relative my-4">
342+
<div class="text-right sticky top-4 mb-2 mr-2 h-0">
343+
<button class="badge btn-mini" onclick="copyStr(${escapeAttr(JSON.stringify(content))})">📋 Copy</button>
344+
</div>
345+
${origRendered}
346+
</div>`;
347+
};
348+
window.copyStr = copyStr;
332349
const content = computed(() => md.value.render(props.source));
333350
return () => h("div", { innerHTML: content.value });
334351
},
335-
{ props: ["source", "options", "plugins"] }
352+
{ props: ["source"] }
336353
);
337354

338355
// inout field to be used by settings modal
339-
const SettingsModalNumericInput = defineComponent({
340-
template: document.getElementById('settings-modal-numeric-input').innerHTML,
356+
const SettingsModalShortInput = defineComponent({
357+
template: document.getElementById('settings-modal-short-input').innerHTML,
341358
props: ['configKey', 'configDefault', 'configInfo', 'modelValue'],
342359
});
343360

@@ -390,7 +407,11 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
390407
if (!conv) return;
391408
const msg = conv.messages.pop();
392409
conv.lastModified = Date.now();
393-
localStorage.setItem(convId, JSON.stringify(conv));
410+
if (conv.messages.length === 0) {
411+
StorageUtils.remove(convId);
412+
} else {
413+
localStorage.setItem(convId, JSON.stringify(conv));
414+
}
394415
return msg;
395416
},
396417

@@ -431,7 +452,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
431452
const mainApp = createApp({
432453
components: {
433454
VueMarkdown,
434-
SettingsModalNumericInput,
455+
SettingsModalShortInput,
435456
},
436457
data() {
437458
return {
@@ -587,6 +608,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
587608
this.isGenerating = false;
588609
this.stopGeneration = () => {};
589610
this.fetchMessages();
611+
chatScrollToBottom();
590612
},
591613

592614
// message actions
@@ -600,7 +622,7 @@ <h3 class="text-lg font-bold mb-6">Settings</h3>
600622
this.generateMessage(currConvId);
601623
},
602624
copyMsg(msg) {
603-
navigator.clipboard.writeText(msg.content);
625+
copyStr(msg.content);
604626
},
605627
editUserMsgAndRegenerate(msg) {
606628
if (this.isGenerating) return;

examples/server/server.cpp

+26-16
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ struct server_task_result {
102102
bool error;
103103
};
104104

105+
struct server_static_file {
106+
const unsigned char * data;
107+
unsigned int size;
108+
const char * mime_type;
109+
};
110+
105111
struct slot_params {
106112
bool stream = true;
107113
bool cache_prompt = false; // remember the prompt to avoid reprocessing all prompt
@@ -2259,6 +2265,16 @@ int main(int argc, char ** argv) {
22592265
LOG_INF("%s\n", common_params_get_system_info(params).c_str());
22602266
LOG_INF("\n");
22612267

2268+
// static files
2269+
std::map<std::string, server_static_file> static_files = {
2270+
{ "/", { index_html, index_html_len, "text/html; charset=utf-8" }},
2271+
{ "/completion.js", { completion_js, completion_js_len, "text/javascript; charset=utf-8" }},
2272+
{ "/deps_daisyui.min.css", { deps_daisyui_min_css, deps_daisyui_min_css_len, "text/css; charset=utf-8" }},
2273+
{ "/deps_markdown-it.js", { deps_markdown_it_js, deps_markdown_it_js_len, "text/javascript; charset=utf-8" }},
2274+
{ "/deps_tailwindcss.js", { deps_tailwindcss_js, deps_tailwindcss_js_len, "text/javascript; charset=utf-8" }},
2275+
{ "/deps_vue.esm-browser.js", { deps_vue_esm_browser_js, deps_vue_esm_browser_js_len, "text/javascript; charset=utf-8" }},
2276+
};
2277+
22622278
std::unique_ptr<httplib::Server> svr;
22632279
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
22642280
if (params.ssl_file_key != "" && params.ssl_file_cert != "") {
@@ -2339,7 +2355,7 @@ int main(int argc, char ** argv) {
23392355
// Middlewares
23402356
//
23412357

2342-
auto middleware_validate_api_key = [&params, &res_error](const httplib::Request & req, httplib::Response & res) {
2358+
auto middleware_validate_api_key = [&params, &res_error, &static_files](const httplib::Request & req, httplib::Response & res) {
23432359
static const std::unordered_set<std::string> public_endpoints = {
23442360
"/health",
23452361
"/models",
@@ -2351,8 +2367,8 @@ int main(int argc, char ** argv) {
23512367
return true;
23522368
}
23532369

2354-
// If path is public, skip validation
2355-
if (public_endpoints.find(req.path) != public_endpoints.end()) {
2370+
// If path is public or is static file, skip validation
2371+
if (public_endpoints.find(req.path) != public_endpoints.end() || static_files.find(req.path) != static_files.end()) {
23562372
return true;
23572373
}
23582374

@@ -3096,13 +3112,6 @@ int main(int argc, char ** argv) {
30963112
res.status = 200; // HTTP OK
30973113
};
30983114

3099-
auto handle_static_file = [](unsigned char * content, size_t len, const char * mime_type) {
3100-
return [content, len, mime_type](const httplib::Request &, httplib::Response & res) {
3101-
res.set_content(reinterpret_cast<const char*>(content), len, mime_type);
3102-
return false;
3103-
};
3104-
};
3105-
31063115
//
31073116
// Router
31083117
//
@@ -3117,12 +3126,13 @@ int main(int argc, char ** argv) {
31173126
}
31183127
} else {
31193128
// using embedded static files
3120-
svr->Get("/", handle_static_file(index_html, index_html_len, "text/html; charset=utf-8"));
3121-
svr->Get("/completion.js", handle_static_file(completion_js, completion_js_len, "text/javascript; charset=utf-8"));
3122-
svr->Get("/deps_daisyui.min.css", handle_static_file(deps_daisyui_min_css, deps_daisyui_min_css_len, "text/css; charset=utf-8"));
3123-
svr->Get("/deps_markdown-it.js", handle_static_file(deps_markdown_it_js, deps_markdown_it_js_len, "text/javascript; charset=utf-8"));
3124-
svr->Get("/deps_tailwindcss.js", handle_static_file(deps_tailwindcss_js, deps_tailwindcss_js_len, "text/javascript; charset=utf-8"));
3125-
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"));
3129+
for (const auto & it : static_files) {
3130+
const server_static_file & static_file = it.second;
3131+
svr->Get(it.first.c_str(), [&static_file](const httplib::Request &, httplib::Response & res) {
3132+
res.set_content(reinterpret_cast<const char*>(static_file.data), static_file.size, static_file.mime_type);
3133+
return false;
3134+
});
3135+
}
31263136
}
31273137

31283138
// register API routes

0 commit comments

Comments
 (0)