From 6ea3315334f2d9d87e73297e9ccfa58e2f5aae38 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Tue, 5 Nov 2024 19:52:00 +0100 Subject: [PATCH] regenerate, edit, copy buttons --- examples/server/public/index.html | 207 ++++++++++++++++++++++++++---- 1 file changed, 181 insertions(+), 26 deletions(-) diff --git a/examples/server/public/index.html b/examples/server/public/index.html index e9e590711a3ee..2aa1bd4e599a0 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 @@

Conversations

🦙 llama.cpp - chat
- + + -
+
- +
+ + + + +
-
- Edit + + +
+ + + + +
@@ -127,12 +163,39 @@

Conversations

placeholder="Type a message..." v-model="inputMsg" @keydown.enter="sendMessage" - v-bind:disabled="isGenerating" + :disabled="isGenerating" id="msg-input" > - + +
+ + + + + +
@@ -142,6 +205,16 @@

Conversations

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 @@

Conversations

}, // 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 @@

Conversations

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 @@

Conversations

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 @@

Conversations

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

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 @@

Conversations

}); 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

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