extract chat bubble to a component
This commit is contained in:
parent
bd2f59e50a
commit
95e294b19d
2 changed files with 102 additions and 54 deletions
|
@ -120,45 +120,16 @@
|
||||||
{{ messages.length === 0 ? 'Send a message to start' : '' }}
|
{{ messages.length === 0 ? 'Send a message to start' : '' }}
|
||||||
</div>
|
</div>
|
||||||
<div v-for="msg in messages" class="group">
|
<div v-for="msg in messages" class="group">
|
||||||
<div :class="{
|
<message-bubble
|
||||||
'chat': true,
|
:msg="msg"
|
||||||
'chat-start': msg.role !== 'user',
|
:key="msg.id"
|
||||||
'chat-end': msg.role === 'user',
|
:is-generating="isGenerating"
|
||||||
}">
|
:edit-user-msg-and-regenerate="editUserMsgAndRegenerate"
|
||||||
<div :class="{
|
:regenerate-msg="regenerateMsg"></message-bubble>
|
||||||
'chat-bubble markdown': true,
|
|
||||||
'chat-bubble-base-300': msg.role !== 'user',
|
|
||||||
}">
|
|
||||||
<!-- textarea for editing message -->
|
|
||||||
<template v-if="editingMsg && editingMsg.id === msg.id">
|
|
||||||
<textarea
|
|
||||||
class="textarea textarea-bordered bg-base-100 text-base-content w-[calc(90vw-8em)] lg:w-96"
|
|
||||||
v-model="msg.content"></textarea>
|
|
||||||
<br/>
|
|
||||||
<button class="btn btn-ghost mt-2 mr-2" @click="editingMsg = null">Cancel</button>
|
|
||||||
<button class="btn mt-2" @click="editUserMsgAndRegenerate(msg)">Submit</button>
|
|
||||||
</template>
|
|
||||||
<!-- render message as markdown -->
|
|
||||||
<vue-markdown v-else :source="msg.content" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- actions for each message -->
|
|
||||||
<div :class="{'text-right': msg.role === 'user'}" class="mx-4 mt-2 mb-2">
|
|
||||||
<!-- user message -->
|
|
||||||
<button v-if="msg.role === 'user'" class="badge btn-mini show-on-hover" @click="editingMsg = msg" :disabled="isGenerating">
|
|
||||||
✍️ Edit
|
|
||||||
</button>
|
|
||||||
<!-- assistant message -->
|
|
||||||
<button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="regenerateMsg(msg)" :disabled="isGenerating">
|
|
||||||
🔄 Regenerate
|
|
||||||
</button>
|
|
||||||
<button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="copyMsg(msg)" :disabled="isGenerating">
|
|
||||||
📋 Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- pending (ongoing) assistant message -->
|
<!-- pending (ongoing) assistant message -->
|
||||||
<div id="pending-msg" class="chat chat-start">
|
<div id="pending-msg" class="chat chat-start">
|
||||||
<div v-if="pendingMsg" class="chat-bubble markdown chat-bubble-base-300">
|
<div v-if="pendingMsg" class="chat-bubble markdown chat-bubble-base-300">
|
||||||
|
@ -227,6 +198,10 @@
|
||||||
<details class="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
|
<details class="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
|
||||||
<summary class="collapse-title font-bold">Advanced config</summary>
|
<summary class="collapse-title font-bold">Advanced config</summary>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
|
<div class="flex flex-row items-center mb-2">
|
||||||
|
<input type="checkbox" class="checkbox" v-model="config.show_tokens_per_second" />
|
||||||
|
<span class="ml-4">Show tokens per second</span>
|
||||||
|
</div>
|
||||||
<label class="form-control mb-2">
|
<label class="form-control mb-2">
|
||||||
<!-- Custom parameters input -->
|
<!-- Custom parameters input -->
|
||||||
<div class="label inline">Custom JSON config (For more info, refer to <a class="underline" href="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md" target="_blank" rel="noopener noreferrer">server documentation</a>)</div>
|
<div class="label inline">Custom JSON config (For more info, refer to <a class="underline" href="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md" target="_blank" rel="noopener noreferrer">server documentation</a>)</div>
|
||||||
|
@ -247,6 +222,48 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Template to be used as message bubble -->
|
||||||
|
<template id="message-bubble">
|
||||||
|
<div :class="{
|
||||||
|
'chat': true,
|
||||||
|
'chat-start': msg.role !== 'user',
|
||||||
|
'chat-end': msg.role === 'user',
|
||||||
|
}">
|
||||||
|
<div :class="{
|
||||||
|
'chat-bubble markdown': true,
|
||||||
|
'chat-bubble-base-300': msg.role !== 'user',
|
||||||
|
}">
|
||||||
|
<!-- textarea for editing message -->
|
||||||
|
<template v-if="editingContent !== null">
|
||||||
|
<textarea
|
||||||
|
class="textarea textarea-bordered bg-base-100 text-base-content w-[calc(90vw-8em)] lg:w-96"
|
||||||
|
v-model="editingContent"></textarea>
|
||||||
|
<br/>
|
||||||
|
<button class="btn btn-ghost mt-2 mr-2" @click="editingContent = null">Cancel</button>
|
||||||
|
<button class="btn mt-2" @click="editMsg()">Submit</button>
|
||||||
|
</template>
|
||||||
|
<!-- render message as markdown -->
|
||||||
|
<vue-markdown v-else :source="msg.content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- actions for each message -->
|
||||||
|
<div :class="{'text-right': msg.role === 'user'}" class="mx-4 mt-2 mb-2">
|
||||||
|
<!-- user message -->
|
||||||
|
<button v-if="msg.role === 'user'" class="badge btn-mini show-on-hover" @click="editingContent = msg.content" :disabled="isGenerating">
|
||||||
|
✍️ Edit
|
||||||
|
</button>
|
||||||
|
<!-- assistant message -->
|
||||||
|
<button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="regenerateMsg(msg)" :disabled="isGenerating">
|
||||||
|
🔄 Regenerate
|
||||||
|
</button>
|
||||||
|
<button v-if="msg.role === 'assistant'" class="badge btn-mini show-on-hover mr-2" @click="copyMsg()" :disabled="isGenerating">
|
||||||
|
📋 Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<!-- Template to be used by settings modal -->
|
<!-- Template to be used by settings modal -->
|
||||||
<template id="settings-modal-short-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">
|
||||||
|
|
|
@ -3,9 +3,12 @@ import { createApp, defineComponent, shallowRef, computed, h } from 'vue/dist/vu
|
||||||
import MarkdownIt from 'markdown-it';
|
import MarkdownIt from 'markdown-it';
|
||||||
import TextLineStream from 'textlinestream';
|
import TextLineStream from 'textlinestream';
|
||||||
|
|
||||||
|
const isDev = import.meta.env.MODE === 'development';
|
||||||
|
|
||||||
// utility functions
|
// utility functions
|
||||||
const isString = (x) => !!x.toLowerCase;
|
const isString = (x) => !!x.toLowerCase;
|
||||||
const isNumeric = (n) => !isString(n) && !isNaN(n);
|
const isBoolean = (x) => x === true || x === false;
|
||||||
|
const isNumeric = (n) => !isString(n) && !isNaN(n) && !isBoolean(n);
|
||||||
const escapeAttr = (str) => str.replace(/>/g, '>').replace(/"/g, '"');
|
const escapeAttr = (str) => str.replace(/>/g, '>').replace(/"/g, '"');
|
||||||
const copyStr = (str) => navigator.clipboard.writeText(str);
|
const copyStr = (str) => navigator.clipboard.writeText(str);
|
||||||
|
|
||||||
|
@ -36,6 +39,7 @@ const CONFIG_DEFAULT = {
|
||||||
dry_allowed_length: 2,
|
dry_allowed_length: 2,
|
||||||
dry_penalty_last_n: -1,
|
dry_penalty_last_n: -1,
|
||||||
max_tokens: -1,
|
max_tokens: -1,
|
||||||
|
show_tokens_per_second: false,
|
||||||
custom: '', // custom json-stringified object
|
custom: '', // custom json-stringified object
|
||||||
};
|
};
|
||||||
const CONFIG_INFO = {
|
const CONFIG_INFO = {
|
||||||
|
@ -101,6 +105,37 @@ const SettingsModalShortInput = defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// message bubble component
|
||||||
|
const MessageBubble = defineComponent({
|
||||||
|
components: {
|
||||||
|
VueMarkdown
|
||||||
|
},
|
||||||
|
template: document.getElementById('message-bubble').innerHTML,
|
||||||
|
props: {
|
||||||
|
msg: Object,
|
||||||
|
isGenerating: Boolean,
|
||||||
|
editUserMsgAndRegenerate: Function,
|
||||||
|
regenerateMsg: Function,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editingContent: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copyMsg() {
|
||||||
|
copyStr(this.msg.content);
|
||||||
|
},
|
||||||
|
editMsg() {
|
||||||
|
this.editUserMsgAndRegenerate({
|
||||||
|
...this.msg,
|
||||||
|
content: this.editingContent,
|
||||||
|
});
|
||||||
|
this.editingContent = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// coversations is stored in localStorage
|
// coversations is stored in localStorage
|
||||||
// format: { [convId]: { id: string, lastModified: number, messages: [...] } }
|
// format: { [convId]: { id: string, lastModified: number, messages: [...] } }
|
||||||
// convId is a string prefixed with 'conv-'
|
// convId is a string prefixed with 'conv-'
|
||||||
|
@ -199,6 +234,7 @@ async function* sendSSEPostRequest(url, fetchOptions) {
|
||||||
.pipeThrough(new TextDecoderStream())
|
.pipeThrough(new TextDecoderStream())
|
||||||
.pipeThrough(new TextLineStream());
|
.pipeThrough(new TextLineStream());
|
||||||
for await (const line of lines) {
|
for await (const line of lines) {
|
||||||
|
if (isDev) console.log({line});
|
||||||
if (line.startsWith('data:') && !line.endsWith('[DONE]')) {
|
if (line.startsWith('data:') && !line.endsWith('[DONE]')) {
|
||||||
const data = JSON.parse(line.slice(5));
|
const data = JSON.parse(line.slice(5));
|
||||||
yield data;
|
yield data;
|
||||||
|
@ -213,6 +249,7 @@ const mainApp = createApp({
|
||||||
components: {
|
components: {
|
||||||
VueMarkdown,
|
VueMarkdown,
|
||||||
SettingsModalShortInput,
|
SettingsModalShortInput,
|
||||||
|
MessageBubble,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -226,7 +263,6 @@ const mainApp = createApp({
|
||||||
selectedTheme: StorageUtils.getTheme(),
|
selectedTheme: StorageUtils.getTheme(),
|
||||||
config: StorageUtils.getConfig(),
|
config: StorageUtils.getConfig(),
|
||||||
showConfigDialog: false,
|
showConfigDialog: false,
|
||||||
editingMsg: null,
|
|
||||||
// const
|
// const
|
||||||
themes: THEMES,
|
themes: THEMES,
|
||||||
configDefault: {...CONFIG_DEFAULT},
|
configDefault: {...CONFIG_DEFAULT},
|
||||||
|
@ -243,6 +279,15 @@ const mainApp = createApp({
|
||||||
});
|
});
|
||||||
resizeObserver.observe(pendingMsgElem);
|
resizeObserver.observe(pendingMsgElem);
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
viewingConvId: function(val, oldVal) {
|
||||||
|
if (val != oldVal) {
|
||||||
|
this.fetchMessages();
|
||||||
|
chatScrollToBottom();
|
||||||
|
this.hideSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hideSidebar() {
|
hideSidebar() {
|
||||||
document.getElementById('toggle-drawer').checked = false;
|
document.getElementById('toggle-drawer').checked = false;
|
||||||
|
@ -254,18 +299,10 @@ const mainApp = createApp({
|
||||||
newConversation() {
|
newConversation() {
|
||||||
if (this.isGenerating) return;
|
if (this.isGenerating) return;
|
||||||
this.viewingConvId = StorageUtils.getNewConvId();
|
this.viewingConvId = StorageUtils.getNewConvId();
|
||||||
this.editingMsg = null;
|
|
||||||
this.fetchMessages();
|
|
||||||
chatScrollToBottom();
|
|
||||||
this.hideSidebar();
|
|
||||||
},
|
},
|
||||||
setViewingConv(convId) {
|
setViewingConv(convId) {
|
||||||
if (this.isGenerating) return;
|
if (this.isGenerating) return;
|
||||||
this.viewingConvId = convId;
|
this.viewingConvId = convId;
|
||||||
this.editingMsg = null;
|
|
||||||
this.fetchMessages();
|
|
||||||
chatScrollToBottom();
|
|
||||||
this.hideSidebar();
|
|
||||||
},
|
},
|
||||||
deleteConv(convId) {
|
deleteConv(convId) {
|
||||||
if (this.isGenerating) return;
|
if (this.isGenerating) return;
|
||||||
|
@ -273,7 +310,6 @@ const mainApp = createApp({
|
||||||
StorageUtils.remove(convId);
|
StorageUtils.remove(convId);
|
||||||
if (this.viewingConvId === convId) {
|
if (this.viewingConvId === convId) {
|
||||||
this.viewingConvId = StorageUtils.getNewConvId();
|
this.viewingConvId = StorageUtils.getNewConvId();
|
||||||
this.editingMsg = null;
|
|
||||||
}
|
}
|
||||||
this.fetchConversation();
|
this.fetchConversation();
|
||||||
this.fetchMessages();
|
this.fetchMessages();
|
||||||
|
@ -308,7 +344,6 @@ const mainApp = createApp({
|
||||||
this.fetchConversation();
|
this.fetchConversation();
|
||||||
this.fetchMessages();
|
this.fetchMessages();
|
||||||
this.inputMsg = '';
|
this.inputMsg = '';
|
||||||
this.editingMsg = null;
|
|
||||||
this.generateMessage(currConvId);
|
this.generateMessage(currConvId);
|
||||||
chatScrollToBottom();
|
chatScrollToBottom();
|
||||||
},
|
},
|
||||||
|
@ -316,7 +351,6 @@ const mainApp = createApp({
|
||||||
if (this.isGenerating) return;
|
if (this.isGenerating) return;
|
||||||
this.pendingMsg = { id: Date.now()+1, role: 'assistant', content: null };
|
this.pendingMsg = { id: Date.now()+1, role: 'assistant', content: null };
|
||||||
this.isGenerating = true;
|
this.isGenerating = true;
|
||||||
this.editingMsg = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
@ -347,6 +381,7 @@ const mainApp = createApp({
|
||||||
dry_allowed_length: this.config.dry_allowed_length,
|
dry_allowed_length: this.config.dry_allowed_length,
|
||||||
dry_penalty_last_n: this.config.dry_penalty_last_n,
|
dry_penalty_last_n: this.config.dry_penalty_last_n,
|
||||||
max_tokens: this.config.max_tokens,
|
max_tokens: this.config.max_tokens,
|
||||||
|
timings_per_token: !!this.config.show_tokens_per_second,
|
||||||
...(this.config.custom.length ? JSON.parse(this.config.custom) : {}),
|
...(this.config.custom.length ? JSON.parse(this.config.custom) : {}),
|
||||||
};
|
};
|
||||||
const chunks = sendSSEPostRequest(`${BASE_URL}/v1/chat/completions`, {
|
const chunks = sendSSEPostRequest(`${BASE_URL}/v1/chat/completions`, {
|
||||||
|
@ -407,14 +442,10 @@ const mainApp = createApp({
|
||||||
this.fetchMessages();
|
this.fetchMessages();
|
||||||
this.generateMessage(currConvId);
|
this.generateMessage(currConvId);
|
||||||
},
|
},
|
||||||
copyMsg(msg) {
|
|
||||||
copyStr(msg.content);
|
|
||||||
},
|
|
||||||
editUserMsgAndRegenerate(msg) {
|
editUserMsgAndRegenerate(msg) {
|
||||||
if (this.isGenerating) return;
|
if (this.isGenerating) return;
|
||||||
const currConvId = this.viewingConvId;
|
const currConvId = this.viewingConvId;
|
||||||
const newContent = msg.content;
|
const newContent = msg.content;
|
||||||
this.editingMsg = null;
|
|
||||||
StorageUtils.filterAndKeepMsgs(currConvId, (m) => m.id < msg.id);
|
StorageUtils.filterAndKeepMsgs(currConvId, (m) => m.id < msg.id);
|
||||||
StorageUtils.appendMsg(currConvId, {
|
StorageUtils.appendMsg(currConvId, {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue