diff --git a/examples/server/public/index.html b/examples/server/public/index.html
index e9e590711..2aa1bd4e5 100644
--- a/examples/server/public/index.html
+++ b/examples/server/public/index.html
@@ -18,6 +18,9 @@
.bg-base-100 {background-color: var(--fallback-b1,oklch(var(--b1)/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))}
+ .btn-mini {
+ @apply cursor-pointer opacity-0 group-hover:opacity-100 hover:shadow-md;
+ }
@@ -49,7 +52,7 @@
🦙 llama.cpp - chat
-
@@ -142,6 +205,16 @@
const BASE_URL = localStorage.getItem('base') // for debugging
|| (new URL('.', document.baseURI).href).toString(); // for production
+ const CONFIG_DEFAULT = {
+ apiKey: '',
+ systemMessage: 'You are a helpful assistant.',
+ temperature: 0.8,
+ top_k: 40,
+ top_p: 0.95,
+ max_tokens: -1,
+ custom: '', // custom json object
+ };
+ const THEMES = ['light', 'dark', 'cupcake', 'bumblebee', 'emerald', 'corporate', 'synthwave', 'retro', 'cyberpunk', 'valentine', 'halloween', 'garden', 'forest', 'aqua', 'lofi', 'pastel', 'fantasy', 'wireframe', 'black', 'luxury', 'dracula', 'cmyk', 'autumn', 'business', 'acid', 'lemonade', 'night', 'coffee', 'winter', 'dim', 'nord', 'sunset'];
// markdown support
const VueMarkdown = defineComponent(
@@ -177,6 +250,7 @@
},
// if convId does not exist, create one
appendMsg(convId, msg) {
+ if (msg.content === null) return;
const conv = Conversations.getOne(convId) || {
id: convId,
lastModified: Date.now(),
@@ -192,6 +266,13 @@
remove(convId) {
localStorage.removeItem(convId);
},
+ filterAndKeepMsgs(convId, predicate) {
+ const conv = Conversations.getOne(convId);
+ if (!conv) return;
+ conv.messages = conv.messages.filter(predicate);
+ conv.lastModified = Date.now();
+ localStorage.setItem(convId, JSON.stringify(conv));
+ },
};
// scroll to bottom of chat messages
@@ -212,10 +293,14 @@
inputMsg: '',
isGenerating: false,
pendingMsg: null, // the on-going message from assistant
- abortController: null,
+ stopGeneration: () => {},
selectedTheme: localStorage.getItem('theme') || 'auto',
+ config: JSON.parse(localStorage.getItem('config') || 'null') || {...CONFIG_DEFAULT},
+ showConfigDialog: false,
+ editingMsg: null,
// const
- themes: ['light', 'dark', 'cupcake', 'bumblebee', 'emerald', 'corporate', 'synthwave', 'retro', 'cyberpunk', 'valentine', 'halloween', 'garden', 'forest', 'aqua', 'lofi', 'pastel', 'fantasy', 'wireframe', 'black', 'luxury', 'dracula', 'cmyk', 'autumn', 'business', 'acid', 'lemonade', 'night', 'coffee', 'winter', 'dim', 'nord', 'sunset'],
+ themes: THEMES,
+ configDefault: {...CONFIG_DEFAULT},
}
},
computed: {},
@@ -240,12 +325,14 @@
newConversation() {
if (this.isGenerating) return;
this.viewingConvId = Conversations.getNewConvId();
+ this.editingMsg = null;
this.fetchMessages();
chatScrollToBottom();
},
setViewingConv(convId) {
if (this.isGenerating) return;
this.viewingConvId = convId;
+ this.editingMsg = null;
this.fetchMessages();
chatScrollToBottom();
},
@@ -255,11 +342,28 @@
Conversations.remove(convId);
if (this.viewingConvId === convId) {
this.viewingConvId = Conversations.getNewConvId();
+ this.editingMsg = null;
}
this.fetchConversation();
this.fetchMessages();
}
},
+ closeSaveAndConfigDialog() {
+ try {
+ if (this.config.custom.length) JSON.parse(this.config.custom);
+ } catch (error) {
+ alert('Invalid JSON for custom config. Please either fix it or leave it empty.');
+ return;
+ }
+ for (const key of ['temperature', 'top_k', 'top_p', 'max_tokens']) {
+ if (isNaN(this.config[key])) {
+ alert('Invalid number for ' + key);
+ return;
+ }
+ }
+ this.showConfigDialog = false;
+ localStorage.setItem('config', JSON.stringify(this.config));
+ },
async sendMessage() {
if (!this.inputMsg) return;
const currConvId = this.viewingConvId;
@@ -271,20 +375,34 @@
});
this.fetchConversation();
this.fetchMessages();
-
this.inputMsg = '';
+ this.editingMsg = null;
+ this.generateMessage(currConvId);
+ },
+ async generateMessage(currConvId) {
+ if (this.isGenerating) return;
this.pendingMsg = { id: Date.now()+1, role: 'assistant', content: null };
this.isGenerating = true;
+ this.editingMsg = null;
try {
- this.abortController = new AbortController();
+ const abortController = new AbortController();
+ this.stopGeneration = () => abortController.abort();
const params = {
- messages: this.messages,
+ messages: [
+ { role: 'system', content: this.config.systemMessage },
+ ...this.messages,
+ ],
stream: true,
cache_prompt: true,
+ temperature: this.config.temperature,
+ top_k: this.config.top_k,
+ top_p: this.config.top_p,
+ max_tokens: this.config.max_tokens,
+ ...(this.config.custom.length ? JSON.parse(this.config.custom) : {}),
};
const config = {
- controller: this.abortController,
+ controller: abortController,
api_url: BASE_URL,
endpoint: '/chat/completions',
};
@@ -304,15 +422,52 @@
Conversations.appendMsg(currConvId, this.pendingMsg);
this.fetchConversation();
this.fetchMessages();
- this.pendingMsg = null;
- this.isGenerating = false;
setTimeout(() => document.getElementById('msg-input').focus(), 1);
} catch (error) {
- console.error(error);
- alert(error);
- this.pendingMsg = null;
- this.isGenerating = false;
+ if (error.name === 'AbortError') {
+ // user stopped the generation via stopGeneration() function
+ Conversations.appendMsg(currConvId, this.pendingMsg);
+ this.fetchConversation();
+ this.fetchMessages();
+ } else {
+ console.error(error);
+ alert(error);
+ this.inputMsg = this.pendingMsg.content || '';
+ }
}
+
+ this.pendingMsg = null;
+ this.isGenerating = false;
+ this.stopGeneration = () => {};
+ },
+
+ // message actions
+ regenerateMsg(msg) {
+ if (this.isGenerating) return;
+ // TODO: somehow keep old history (like how ChatGPT has different "tree")
+ const currConvId = this.viewingConvId;
+ Conversations.filterAndKeepMsgs(currConvId, (m) => m.id < msg.id);
+ this.fetchConversation();
+ this.fetchMessages();
+ this.generateMessage(currConvId);
+ },
+ copyMsg(msg) {
+ navigator.clipboard.writeText(msg.content);
+ },
+ editUserMsgAndRegenerate(msg) {
+ if (this.isGenerating) return;
+ const currConvId = this.viewingConvId;
+ const newContent = msg.content;
+ this.editingMsg = null;
+ Conversations.filterAndKeepMsgs(currConvId, (m) => m.id < msg.id);
+ Conversations.appendMsg(currConvId, {
+ id: Date.now(),
+ role: 'user',
+ content: newContent,
+ });
+ this.fetchConversation();
+ this.fetchMessages();
+ this.generateMessage(currConvId);
},
// sync state functions