Compare commits

...

4 Commits

Author SHA1 Message Date
Rafi
052f5299a0 Add frequently used prompt function. 2023-03-09 17:39:45 +08:00
Rafi
8340edbf40 Add typewriter effect to the messages of the model. 2023-03-09 15:05:40 +08:00
Rafi
7bff84638e update demo.png 2023-03-08 16:43:32 +08:00
Rafi
54660706e3 update demo.png 2023-03-08 16:41:58 +08:00
5 changed files with 271 additions and 13 deletions

View File

@@ -13,7 +13,7 @@ hljs.addPlugin({
}
header = Object.assign(document.createElement("div"), {
className: "hljs-code-header d-flex align-center justify-space-between bg-black pa-1",
className: "hljs-code-header d-flex align-center justify-space-between bg-grey-darken-3 pa-1",
innerHTML: `<div class="pl-2 text-caption">${result.language}</div>`
});
@@ -56,6 +56,9 @@ const contentHtml = ref('')
const contentElm = ref(null)
const highlightCode = () => {
if (!contentElm.value) {
return
}
contentElm.value.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block)
})
@@ -63,9 +66,11 @@ const highlightCode = () => {
watchEffect(() => {
contentHtml.value = props.content ? marked(props.content) : ''
nextTick(() => {
highlightCode()
})
if (props.content && props.content.endsWith('```')) {
nextTick(() => {
highlightCode()
})
}
})
</script>

View File

@@ -2,13 +2,12 @@
<v-textarea
v-model="message"
:label="$t('writeAMessage')"
:placeholder="$t('writeAMessage') + '...'"
:placeholder="hint"
rows="1"
:auto-grow="autoGrow"
:disabled="disabled"
:loading="loading"
:hint="hint"
:hide-details="loading"
:hide-details="true"
append-inner-icon="send"
@keyup.enter.exact="enterOnly"
@click:appendInner="clickSendBtn"
@@ -60,6 +59,9 @@ export default {
}
this.message = ""
},
usePrompt(prompt) {
this.message = prompt
},
clickSendBtn () {
this.send()
},

224
components/Prompt.vue Normal file
View File

@@ -0,0 +1,224 @@
<script setup>
const menu = ref(false)
const prompts = ref([])
const editingPrompt = ref(null)
const newPrompt = ref('')
const submittingNewPrompt = ref(false)
const promptInputErrorMessage = ref('')
const loadingPrompts = ref(false)
const deletingPromptIndex = ref(null)
const props = defineProps({
usePrompt: {
type: Function,
required: true
}
})
const addPrompt = async () => {
if (!newPrompt.value) {
promptInputErrorMessage.value = 'Please enter a prompt'
return
}
submittingNewPrompt.value = true
const { data, error } = await useAuthFetch('/api/chat/prompts/', {
method: 'POST',
body: JSON.stringify({
prompt: newPrompt.value
})
})
if (!error.value) {
prompts.value.push(data.value)
newPrompt.value = ''
}
submittingNewPrompt.value = false
}
const editPrompt = (index) => {
editingPrompt.value = Object.assign({}, prompts.value[index])
}
const updatePrompt = async (index) => {
editingPrompt.value.updating = true
const { data, error } = await useAuthFetch(`/api/chat/prompts/${editingPrompt.value.id}/`, {
method: 'PUT',
body: JSON.stringify({
prompt: editingPrompt.value.prompt
})
})
if (!error.value) {
prompts.value[index] = editingPrompt.value
}
editingPrompt.value.updating = false
editingPrompt.value = null
}
const cancelEditPrompt = () => {
editingPrompt.value = null
}
const deletePrompt = async (index) => {
deletingPromptIndex.value = index
const { data, error } = await useAuthFetch(`/api/chat/prompts/${prompts.value[index].id}/`, {
method: 'DELETE'
})
deletingPromptIndex.value = null
if (!error.value) {
prompts.value.splice(index, 1)
}
}
const loadPrompts = async () => {
loadingPrompts.value = true
const { data, error } = await useAuthFetch('/api/chat/prompts/')
if (!error.value) {
prompts.value = data.value
}
loadingPrompts.value = false
}
const selectPrompt = (prompt) => {
props.usePrompt(prompt.prompt)
menu.value = false
}
onMounted( () => {
loadPrompts()
})
</script>
<template>
<div>
<v-menu
v-model="menu"
:close-on-content-click="false"
>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon="speaker_notes"
title="Common prompts"
class="mr-3"
></v-btn>
</template>
<v-container>
<v-card
min-width="300"
max-width="500"
>
<v-card-title>
<span class="headline">Frequently prompts</span>
</v-card-title>
<v-divider></v-divider>
<v-list>
<v-list-item v-show="loadingPrompts">
<v-list-item-title class="d-flex justify-center">
<v-progress-circular indeterminate></v-progress-circular>
</v-list-item-title>
</v-list-item>
<template
v-for="(prompt, idx) in prompts"
:key="prompt.id"
>
<v-list-item
active-color="primary"
rounded="xl"
v-if="editingPrompt && editingPrompt.id === prompt.id"
>
<v-textarea
rows="2"
v-model="editingPrompt.prompt"
:loading="editingPrompt.updating"
variant="underlined"
hide-details
density="compact"
>
<template v-slot:append>
<div class="d-flex flex-column">
<v-btn
icon="done"
variant="text"
:loading="editingPrompt.updating"
@click="updatePrompt(idx)"
>
</v-btn>
<v-btn
icon="close"
variant="text"
@click="cancelEditPrompt()"
>
</v-btn>
</div>
</template>
</v-textarea>
</v-list-item>
<v-list-item
v-if="!editingPrompt || editingPrompt.id !== prompt.id"
rounded="xl"
active-color="primary"
@click="selectPrompt(prompt)"
>
<v-list-item-title>{{ prompt.prompt }}</v-list-item-title>
<template v-slot:append>
<v-btn
icon="edit"
size="small"
variant="text"
@click="editPrompt(idx)"
>
</v-btn>
<v-btn
icon="delete"
size="small"
variant="text"
:loading="deletingPromptIndex === idx"
@click="deletePrompt(idx)"
>
</v-btn>
</template>
</v-list-item>
</template>
<v-list-item
active-color="primary"
>
<div
class="pt-3"
>
<v-textarea
rows="2"
v-model="newPrompt"
label="Add a new prompt"
variant="outlined"
density="compact"
:error-messages="promptInputErrorMessage"
@update:modelValue="promptInputErrorMessage = ''"
clearable
>
</v-textarea>
</div>
</v-list-item>
<v-list-item>
<v-btn
variant="text"
block
:loading="submittingNewPrompt"
@click="addPrompt()"
>
<v-icon icon="add"></v-icon>
Add prompt
</v-btn>
</v-list-item>
</v-list>
</v-card>
</v-container>
</v-menu>
</div>
</template>
<style scoped>
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1,4 +1,6 @@
<script setup>
import Prompt from "~/components/Prompt.vue";
definePageMeta({
middleware: ["auth"]
})
@@ -10,6 +12,29 @@ const runtimeConfig = useRuntimeConfig()
const currentModel = useCurrentModel()
const openaiApiKey = useApiKey()
const fetchingResponse = ref(false)
const messageQueue = []
let isProcessingQueue = false
const processMessageQueue = () => {
if (isProcessingQueue || messageQueue.length === 0) {
return
}
if (!currentConversation.value.messages[currentConversation.value.messages.length - 1].is_bot) {
currentConversation.value.messages.push({id: null, is_bot: true, message: ''})
}
isProcessingQueue = true
const nextMessage = messageQueue.shift()
let wordIndex = 0;
const intervalId = setInterval(() => {
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage[wordIndex]
wordIndex++
if (wordIndex === nextMessage.length) {
clearInterval(intervalId)
isProcessingQueue = false
processMessageQueue()
}
}, 50)
}
let ctrl
const abortFetch = () => {
@@ -69,11 +94,8 @@ const fetchReply = async (message, parentMessageId) => {
return;
}
if (currentConversation.value.messages[currentConversation.value.messages.length - 1].is_bot) {
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += data.content
} else {
currentConversation.value.messages.push({id: null, is_bot: true, message: data.content})
}
messageQueue.push(data.content)
processMessageQueue()
scrollChatWindow()
},
@@ -120,6 +142,10 @@ const showSnackbar = (text) => {
snackbar.value = true
}
const editor = ref(null)
const usePrompt = (prompt) => {
editor.value.usePrompt(prompt)
}
</script>
@@ -168,6 +194,7 @@ const showSnackbar = (text) => {
<Welcome v-else />
<v-footer app class="d-flex flex-column">
<div class="px-md-16 w-100 d-flex align-center">
<Prompt v-show="!fetchingResponse" :use-prompt="usePrompt" />
<v-btn
v-show="fetchingResponse"
icon="close"
@@ -175,7 +202,7 @@ const showSnackbar = (text) => {
class="mr-3"
@click="stop"
></v-btn>
<MsgEditor :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
<MsgEditor ref="editor" :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
</div>
<div class="px-4 py-2 text-disabled text-caption font-weight-light text-center w-100">