Compare commits

..

13 Commits

Author SHA1 Message Date
Rafi
35d4292d29 Import the @kevinmarrec/nuxt-pwa module to fix the related bugs of PWA feature. 2023-03-21 22:13:02 +08:00
Rafi
3992121b71 update: docker-compose.yml 2023-03-21 10:20:08 +08:00
Rafi
d08806f0c9 update readme 2023-03-20 22:15:13 +08:00
Rafi
85ac73efcc Add email verification requirement judgment after completing registration 2023-03-20 22:03:53 +08:00
Rafi
7cc5a6b347 Fix: the language settings dialog not displaying the close button. 2023-03-20 20:13:23 +08:00
Rafi
983e4d436d update: deployment.sh 2023-03-20 12:54:11 +08:00
Rafi
727826f1b1 Added a Sign-out button 2023-03-19 14:26:46 +08:00
Rafi
386659109c Added a new message action: delete 2023-03-19 13:49:12 +08:00
Rafi
bd9e8bf45e Optimize the editor and enhance the user experience. 2023-03-19 13:39:20 +08:00
Rafi
4e40530a8c Added a new message action: edit 2023-03-19 13:13:27 +08:00
Rafi
ea69a350f4 add environment variable NUXT_DEV_SERVER 2023-03-19 12:53:44 +08:00
Rafi
18a4251714 feat: Message actions 2023-03-17 18:27:07 +08:00
Rafi
878fda0054 Support configuring model parameters in the front-end and storing them in localStorage. 2023-03-17 17:01:18 +08:00
21 changed files with 672 additions and 2173 deletions

View File

@@ -108,6 +108,7 @@ services:
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name - DJANGO_SUPERUSER_USERNAME=admin # default superuser name
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password - DJANGO_SUPERUSER_PASSWORD=password # default superuser password
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email - DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
- ACCOUNT_EMAIL_VERIFICATION=none # Determines the e-mail verification method during signup choose one of "none", "optional", or "mandatory". Default is "optional". If you don't need to verify the email, you can set it to "none".
# If you want to use the email verification function, you need to configure the following parameters # If you want to use the email verification function, you need to configure the following parameters
# - EMAIL_HOST=SMTP server address # - EMAIL_HOST=SMTP server address
# - EMAIL_PORT=SMTP server port # - EMAIL_PORT=SMTP server port

View File

@@ -1,6 +1,5 @@
<template> <template>
<div> <div>
<VitePwaManifest />
<NuxtLoadingIndicator /> <NuxtLoadingIndicator />
<NuxtLayout> <NuxtLayout>
<NuxtPage /> <NuxtPage />

View File

@@ -0,0 +1,98 @@
<script setup>
import copy from 'copy-to-clipboard'
const props = defineProps({
message: {
type: Object,
required: true
},
messageIndex: {
type: Number,
required: true
},
usePrompt: {
type: Function,
required: true
},
deleteMessage: {
type: Function,
required: true
}
})
const snackbar = ref(false)
const snackbarText = ref('')
const showSnackbar = (text) => {
snackbarText.value = text
snackbar.value = true
}
const copyMessage = () => {
copy(props.message.message)
showSnackbar('Copied!')
}
const editMessage = () => {
props.usePrompt(props.message.message)
}
const deleteMessage = async () => {
const { data, error } = await useAuthFetch(`/api/chat/messages/${props.message.id}/`, {
method: 'DELETE'
})
if (!error.value) {
this.$emit('deleteMessage', props.messageIndex)
showSnackbar('Deleted!')
}
showSnackbar('Delete failed')
}
</script>
<template>
<v-menu
>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon
variant="text"
class="mx-1"
>
<v-icon icon="more_horiz"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
@click="copyMessage()"
:title="$t('copy')"
prepend-icon="content_copy"
>
</v-list-item>
<v-list-item
@click="editMessage()"
:title="$t('edit')"
prepend-icon="edit"
>
</v-list-item>
<v-list-item
@click="deleteMessage()"
:title="$t('delete')"
prepend-icon="delete"
>
</v-list-item>
</v-list>
</v-menu>
<v-snackbar
v-model="snackbar"
location="top"
timeout="2000"
>
{{ snackbarText }}
</v-snackbar>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,191 @@
<script setup>
const dialog = ref(false)
const currentModel = useCurrentModel()
const availableModels = [
DEFAULT_MODEL.name
]
watch(currentModel, (newVal, oldVal) => {
saveCurrentModel(newVal)
}, { deep: true })
</script>
<template>
<v-dialog
v-model="dialog"
persistent
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
rounded="xl"
prepend-icon="tune"
:title="$t('modelParameters')"
></v-list-item>
</template>
<v-card>
<v-toolbar
density="compact"
>
<v-toolbar-title>{{ $t('modelParameters') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon="close" @click="dialog = false"></v-btn>
</v-toolbar>
<v-card-text>
<v-select
v-model="currentModel.name"
:label="$t('model')"
:items="availableModels"
variant="underlined"
></v-select>
<v-row
no-gutters
>
<v-col cols="12">
<div class="d-flex justify-space-between align-center">
<v-list-subheader>{{ $t('temperature') }}</v-list-subheader>
<v-text-field
v-model="currentModel.temperature"
hide-details
single-line
density="compact"
type="number"
max="1"
step="0.01"
style="width: 100px"
class="flex-grow-0"
></v-text-field>
</div>
</v-col>
<v-col cols="12">
<v-slider
v-model="currentModel.temperature"
:max="1"
:step="0.01"
hide-details
>
</v-slider>
</v-col>
</v-row>
<v-row
no-gutters
>
<v-col cols="12">
<div class="d-flex justify-space-between align-center">
<v-list-subheader>{{ $t('maxTokens') }}</v-list-subheader>
<v-text-field
v-model="currentModel.max_tokens"
hide-details
single-line
density="compact"
type="number"
max="2048"
step="1"
style="width: 100px"
class="flex-grow-0"
></v-text-field>
</div>
</v-col>
<v-col cols="12">
<v-slider
v-model="currentModel.max_tokens"
:max="2048"
:step="1"
hide-details
>
</v-slider>
</v-col>
</v-row>
<v-row
no-gutters
>
<v-col cols="12">
<div class="d-flex justify-space-between align-center">
<v-list-subheader>{{ $t('topP') }}</v-list-subheader>
<v-text-field
v-model="currentModel.top_p"
hide-details
single-line
density="compact"
type="number"
max="1"
step="0.01"
style="width: 100px"
class="flex-grow-0"
></v-text-field>
</div>
</v-col>
<v-col cols="12">
<v-slider
v-model="currentModel.top_p"
:max="1"
:step="0.01"
hide-details
>
</v-slider>
</v-col>
</v-row>
<v-row no-gutters>
<v-col cols="12">
<div class="d-flex justify-space-between align-center">
<v-list-subheader>{{ $t('frequencyPenalty') }}</v-list-subheader>
<v-text-field
v-model="currentModel.frequency_penalty"
hide-details
single-line
density="compact"
type="number"
max="2"
step="0.01"
style="width: 100px"
class="flex-grow-0"
></v-text-field>
</div>
</v-col>
<v-col cols="12">
<v-slider
v-model="currentModel.frequency_penalty"
:max="2"
:step="0.01"
hide-details
></v-slider>
</v-col>
</v-row>
<v-row no-gutters>
<v-col cols="12">
<div class="d-flex justify-space-between align-center">
<v-list-subheader>{{ $t('presencePenalty') }}</v-list-subheader>
<v-text-field
v-model="currentModel.presence_penalty"
hide-details
single-line
density="compact"
type="number"
max="2"
step="0.01"
style="width: 100px"
class="flex-grow-0"
></v-text-field>
</div>
</v-col>
<v-col cols="12">
<v-slider
v-model="currentModel.presence_penalty"
:max="2"
:step="0.01"
hide-details
></v-slider>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template>
<style scoped>
</style>

View File

@@ -1,17 +1,29 @@
<template> <template>
<v-textarea <div
v-model="message" class="flex-grow-1 d-flex align-center justify-space-between"
:label="$t('writeAMessage')" >
:placeholder="hint" <v-textarea
rows="1" v-model="message"
:auto-grow="autoGrow" :label="$t('writeAMessage')"
:disabled="disabled" :placeholder="hint"
:loading="loading" :rows="rows"
:hide-details="true" max-rows="8"
append-inner-icon="send" :auto-grow="autoGrow"
@keyup.enter.exact="enterOnly" :disabled="disabled"
@click:appendInner="clickSendBtn" :loading="loading"
></v-textarea> :hide-details="true"
clearable
variant="outlined"
@keydown.enter.exact="enterOnly"
></v-textarea>
<v-btn
:disabled="loading"
icon="send"
title="Send"
class="ml-3"
@click="clickSendBtn"
></v-btn>
</div>
</template> </template>
<script> <script>
@@ -39,7 +51,7 @@ export default {
message(val) { message(val) {
const lines = val.split(/\r\n|\r|\n/).length; const lines = val.split(/\r\n|\r|\n/).length;
if (lines > 8) { if (lines > 8) {
this.rows = lines; this.rows = 8;
this.autoGrow = false; this.autoGrow = false;
} else { } else {
this.rows = 1; this.rows = 1;
@@ -65,7 +77,8 @@ export default {
clickSendBtn () { clickSendBtn () {
this.send() this.send()
}, },
enterOnly () { enterOnly (event) {
event.preventDefault();
if (!isMobile()) { if (!isMobile()) {
this.send() this.send()
} }

View File

@@ -15,26 +15,23 @@
</template> </template>
<v-card> <v-card>
<v-toolbar <v-toolbar
dark
color="primary"
> >
<v-btn <v-btn
icon icon
dark
@click="dialog = false" @click="dialog = false"
> >
<v-icon>close</v-icon> <v-icon icon="close"></v-icon>
</v-btn> </v-btn>
<v-toolbar-title>{{ $t('language') }}</v-toolbar-title> <v-toolbar-title>{{ $t('language') }}</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<!-- <v-toolbar-items>--> <v-toolbar-items>
<!-- <v-btn--> <v-btn
<!-- variant="text"--> variant="text"
<!-- @click="dialog = false"--> @click="dialog = false"
<!-- >--> >
<!-- Save--> Save
<!-- </v-btn>--> </v-btn>
<!-- </v-toolbar-items>--> </v-toolbar-items>
</v-toolbar> </v-toolbar>
<v-list <v-list
> >

View File

@@ -65,6 +65,6 @@ sudo curl -L "https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker
echo "Starting services..." echo "Starting services..."
sudo APP_DOMAIN="${APP_DOMAIN}:${SERVER_PORT}" CLIENT_PORT=${CLIENT_PORT} SERVER_PORT=${SERVER_PORT} WSGI_PORT=${WSGI_PORT} docker-compose up --pull -d sudo APP_DOMAIN="${APP_DOMAIN}:${SERVER_PORT}" CLIENT_PORT=${CLIENT_PORT} SERVER_PORT=${SERVER_PORT} WSGI_PORT=${WSGI_PORT} docker-compose up --pull always -d
echo "Done" echo "Done"

View File

@@ -22,6 +22,7 @@ services:
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name - DJANGO_SUPERUSER_USERNAME=admin # default superuser name
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password - DJANGO_SUPERUSER_PASSWORD=password # default superuser password
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email - DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
- ACCOUNT_EMAIL_VERIFICATION=${ACCOUNT_EMAIL_VERIFICATION:-none} # Determines the e-mail verification method during signup choose one of "none", "optional", or "mandatory". Default is "optional". If you don't need to verify the email, you can set it to "none".
# If you want to use the email verification function, you need to configure the following parameters # If you want to use the email verification function, you need to configure the following parameters
# - EMAIL_HOST=SMTP server address # - EMAIL_HOST=SMTP server address
# - EMAIL_PORT=SMTP server port # - EMAIL_PORT=SMTP server port

View File

@@ -106,6 +106,7 @@ services:
- DJANGO_SUPERUSER_USERNAME=admin # 默认超级用户 - DJANGO_SUPERUSER_USERNAME=admin # 默认超级用户
- DJANGO_SUPERUSER_PASSWORD=password # 默认超级用户的密码 - DJANGO_SUPERUSER_PASSWORD=password # 默认超级用户的密码
- DJANGO_SUPERUSER_EMAIL=admin@example.com # 默认超级用户邮箱 - DJANGO_SUPERUSER_EMAIL=admin@example.com # 默认超级用户邮箱
- ACCOUNT_EMAIL_VERIFICATION=none # 邮箱验证方式,可选值: none, optional, mandatory. 默认为 optional。如果你不需要验证用户的邮箱可以设置为 none。
# 如果您想使用电子邮件验证功能,需要配置以下参数: # 如果您想使用电子邮件验证功能,需要配置以下参数:
# - EMAIL_HOST=SMTP server address # - EMAIL_HOST=SMTP server address
# - EMAIL_PORT=SMTP server port # - EMAIL_PORT=SMTP server port

View File

@@ -18,10 +18,22 @@
"feedback": "Feedback", "feedback": "Feedback",
"newConversation": "New conversation", "newConversation": "New conversation",
"clearConversations": "Clear conversations", "clearConversations": "Clear conversations",
"modelParameters": "Model Parameters",
"model": "Model",
"temperature": "Temperature",
"topP": "Top P",
"frequencyPenalty": "Frequency Penalty",
"presencePenalty": "Presence Penalty",
"maxTokens": "Max Tokens",
"roles": { "roles": {
"me": "Me", "me": "Me",
"ai": "AI" "ai": "AI"
}, },
"edit": "Edit",
"copy": "Copy",
"copied": "Copied",
"delete": "Delete",
"signOut": "Sign out",
"welcomeScreen": { "welcomeScreen": {
"introduction1": "is an unofficial client for ChatGPT, but uses the official OpenAI API.", "introduction1": "is an unofficial client for ChatGPT, but uses the official OpenAI API.",
"introduction2": "You will need an OpenAI API Key before you can use this client.", "introduction2": "You will need an OpenAI API Key before you can use this client.",

View File

@@ -18,10 +18,22 @@
"feedback": "反馈", "feedback": "反馈",
"newConversation": "新的对话", "newConversation": "新的对话",
"clearConversations": "清除对话", "clearConversations": "清除对话",
"modelParameters": "模型参数",
"model": "模型",
"temperature": "Temperature",
"topP": "Top P",
"frequencyPenalty": "Frequency Penalty",
"presencePenalty": "Presence Penalty",
"maxTokens": "Max Tokens",
"roles": { "roles": {
"me": "我", "me": "我",
"ai": "AI" "ai": "AI"
}, },
"edit": "编辑",
"copy": "复制",
"copied": "已复制",
"delete": "删除",
"signOut": "退出登录",
"welcomeScreen": { "welcomeScreen": {
"introduction1": "是一个非官方的ChatGPT客户端但使用OpenAI的官方API", "introduction1": "是一个非官方的ChatGPT客户端但使用OpenAI的官方API",
"introduction2": "在使用本客户端之前您需要一个OpenAI API密钥。", "introduction2": "在使用本客户端之前您需要一个OpenAI API密钥。",

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import {useDisplay} from "vuetify"; import {useDisplay} from "vuetify";
const { $i18n } = useNuxtApp() const { $i18n, $auth } = useNuxtApp()
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const colorMode = useColorMode() const colorMode = useColorMode()
const drawer = ref(null) const drawer = ref(null)
@@ -88,6 +88,15 @@ const drawerPermanent = computed(() => {
return mdAndUp.value return mdAndUp.value
}) })
const signOut = async () => {
const { data, error } = await useFetch('/api/account/logout/', {
method: 'POST'
})
if (!error.value) {
await $auth.logout()
}
}
onNuxtReady(async () => { onNuxtReady(async () => {
loadConversations() loadConversations()
}) })
@@ -228,6 +237,8 @@ onNuxtReady(async () => {
</v-card> </v-card>
</v-dialog> </v-dialog>
<ModelParameters/>
<v-menu <v-menu
> >
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
@@ -259,6 +270,14 @@ onNuxtReady(async () => {
:title="$t('feedback')" :title="$t('feedback')"
@click="feedback" @click="feedback"
></v-list-item> ></v-list-item>
<v-list-item
rounded="xl"
prepend-icon="logout"
:title="$t('signOut')"
@click="signOut"
></v-list-item>
</v-list> </v-list>
</div> </div>
</template> </template>
@@ -279,72 +298,12 @@ onNuxtReady(async () => {
@click="createNewConversion()" @click="createNewConversion()"
></v-btn> ></v-btn>
<!-- <v-menu-->
<!-- >-->
<!-- <template v-slot:activator="{ props }">-->
<!-- <v-btn-->
<!-- v-bind="props"-->
<!-- icon="help_outline"-->
<!-- title="Feedback"-->
<!-- ></v-btn>-->
<!-- </template>-->
<!-- <v-list-->
<!-- >-->
<!-- <v-list-item-->
<!-- @click="feedback"-->
<!-- >-->
<!-- <v-list-item-title>{{ $t('feedback') }}</v-list-item-title>-->
<!-- </v-list-item>-->
<!-- </v-list>-->
<!-- </v-menu>-->
</v-app-bar> </v-app-bar>
<v-main> <v-main>
<NuxtPage/> <NuxtPage/>
</v-main> </v-main>
<div>
<div
v-if="$pwa?.offlineReady || $pwa?.needRefresh"
class="pwa-toast"
role="alert"
>
<div class="message">
<span v-if="$pwa.offlineReady">
App ready to work offline
</span>
<span v-else>
New content available, click on reload button to update.
</span>
</div>
<button
v-if="$pwa.needRefresh"
@click="$pwa.updateServiceWorker()"
>
Reload
</button>
<button @click="$pwa.cancelPrompt()">
Close
</button>
</div>
<div
v-if="$pwa?.showInstallPrompt && !$pwa?.offlineReady && !$pwa?.needRefresh"
class="pwa-toast"
role="alert"
>
<div class="message">
<span>
Install PWA
</span>
</div>
<button @click="$pwa.install()">
Install
</button>
<button @click="$pwa.cancelInstall()">
Cancel
</button>
</div>
</div>
</v-app> </v-app>
</template> </template>
@@ -360,26 +319,4 @@ onNuxtReady(async () => {
border-radius: 3px; border-radius: 3px;
} }
.pwa-toast {
position: fixed;
right: 0;
bottom: 0;
margin: 16px;
padding: 12px;
border: 1px solid #8885;
border-radius: 4px;
z-index: 1;
text-align: left;
box-shadow: 3px 4px 5px 0 #8885;
}
.pwa-toast .message {
margin-bottom: 8px;
}
.pwa-toast button {
border: 1px solid #8885;
outline: none;
margin-right: 5px;
border-radius: 2px;
padding: 3px 10px;
}
</style> </style>

View File

@@ -25,36 +25,18 @@ export default defineNuxtConfig({
'highlight.js/styles/panda-syntax-dark.css', 'highlight.js/styles/panda-syntax-dark.css',
], ],
modules: [ modules: [
'@vite-pwa/nuxt', '@kevinmarrec/nuxt-pwa',
'@nuxtjs/color-mode', '@nuxtjs/color-mode',
'@nuxtjs/i18n', '@nuxtjs/i18n',
], ],
pwa: { pwa: {
registerType: 'autoUpdate',
manifest: { manifest: {
name: appName, name: appName,
short_name: appName, short_name: appName,
icons: [ description: 'A ChatGPT web Client'
{
src: 'icon-black.svg',
sizes: '900x900',
purpose: 'any maskable',
}
],
}, },
workbox: { workbox: {
navigateFallback: '/', enabled: true
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
},
client: {
installPrompt: true,
// you don't need to include this: only for testing purposes
// if enabling periodic sync for update use 1 hour or so (periodicSyncForUpdates: 3600)
periodicSyncForUpdates: 20,
},
devOptions: {
enabled: true,
type: 'module',
} }
}, },
i18n: { i18n: {
@@ -83,7 +65,7 @@ export default defineNuxtConfig({
nitro: { nitro: {
devProxy: { devProxy: {
"/api": { "/api": {
target: "http://localhost:8000/api", target: process.env.NUXT_DEV_SERVER ?? 'http://localhost:8000/api',
prependPath: true, prependPath: true,
changeOrigin: true, changeOrigin: true,
} }

View File

@@ -8,6 +8,7 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"devDependencies": { "devDependencies": {
"@kevinmarrec/nuxt-pwa": "^0.17.0",
"@nuxtjs/color-mode": "^3.2.0", "@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/i18n": "^8.0.0-beta.9", "@nuxtjs/i18n": "^8.0.0-beta.9",
"material-design-icons-iconfont": "^6.7.0", "material-design-icons-iconfont": "^6.7.0",
@@ -15,7 +16,6 @@
}, },
"dependencies": { "dependencies": {
"@microsoft/fetch-event-source": "^2.0.1", "@microsoft/fetch-event-source": "^2.0.1",
"@vite-pwa/nuxt": "^0.0.7",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"is-mobile": "^3.1.1", "is-mobile": "^3.1.1",

View File

@@ -45,24 +45,32 @@ onNuxtReady(() => {
elevation="0" elevation="0"
> >
<div class="text-center"> <div class="text-center">
<h2 class="text-h4">Verify your email</h2> <div v-if="route.query.email_verification_required && route.query.email_verification_required === 'none'">
<p class="text-body-2 mt-5"> <h2 class="text-h4">Your registration is successful</h2>
We've sent a verification email to <strong>{{ $auth.user.email }}</strong>. <br> <p class="mt-5">
Please check your inbox and click the link to verify your email address. You can now <NuxtLink to="/account/signin">login</NuxtLink> to your account.
</p> </p>
<p v-if="errorMsg" </div>
class="text-red" <div v-else>
>{{ errorMsg }}</p> <h2 class="text-h4">Verify your email</h2>
<v-btn <p class="mt-5">
variant="text" We've sent a verification email to <strong>{{ $auth.user.email }}</strong>. <br>
class="mt-5" Please check your inbox and click the link to verify your email address.
color="primary" </p>
:loading="sending" <p v-if="errorMsg"
@click="resendEmail" class="text-red"
:disabled="resent" >{{ errorMsg }}</p>
> <v-btn
{{ resent ? 'Resent' : 'Resend email'}} variant="text"
</v-btn> class="mt-5"
color="primary"
:loading="sending"
@click="resendEmail"
:disabled="resent"
>
{{ resent ? 'Resent' : 'Resend email'}}
</v-btn>
</div>
</div> </div>
</v-card> </v-card>
</v-col> </v-col>

View File

@@ -75,7 +75,7 @@ const submit = async () => {
} }
} else { } else {
$auth.setUser(data.value.user) $auth.setUser(data.value.user)
navigateTo('/account/onboarding') navigateTo('/account/onboarding?email_verification_required='+data.value.email_verification_required)
} }
submitting.value = false submitting.value = false

View File

@@ -6,6 +6,7 @@ definePageMeta({
}) })
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source' import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import MessageActions from "~/components/MessageActions.vue";
const { $i18n, $auth } = useNuxtApp() const { $i18n, $auth } = useNuxtApp()
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
@@ -51,6 +52,14 @@ const abortFetch = () => {
} }
const fetchReply = async (message, parentMessageId) => { const fetchReply = async (message, parentMessageId) => {
ctrl = new AbortController() ctrl = new AbortController()
const data = Object.assign({}, currentModel.value, {
openaiApiKey: openaiApiKey.value,
message: message,
parentMessageId: parentMessageId,
conversationId: currentConversation.value.id
})
try { try {
await fetchEventSource('/api/conversation/', { await fetchEventSource('/api/conversation/', {
signal: ctrl.signal, signal: ctrl.signal,
@@ -59,13 +68,7 @@ const fetchReply = async (message, parentMessageId) => {
'accept': 'application/json', 'accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify(data),
model: currentModel.value,
openaiApiKey: openaiApiKey.value,
message: message,
parentMessageId: parentMessageId,
conversationId: currentConversation.value.id
}),
onopen(response) { onopen(response) {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) { if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
return; return;
@@ -153,6 +156,10 @@ const usePrompt = (prompt) => {
editor.value.usePrompt(prompt) editor.value.usePrompt(prompt)
} }
const deleteMessage = (index) => {
currentConversation.value.messages.splice(index, 1)
}
</script> </script>
<template> <template>
@@ -167,9 +174,16 @@ const usePrompt = (prompt) => {
cols="12" cols="12"
> >
<div <div
class="d-flex" class="d-flex align-center"
:class="message.is_bot ? 'justify-start mr-16' : 'justify-end ml-16'" :class="message.is_bot ? 'justify-start' : 'justify-end'"
> >
<MessageActions
v-if="!message.is_bot"
:message="message"
:message-index="index"
:use-prompt="usePrompt"
:delete-message="deleteMessage"
/>
<v-card <v-card
:color="message.is_bot ? '' : 'primary'" :color="message.is_bot ? '' : 'primary'"
rounded="lg" rounded="lg"
@@ -178,18 +192,14 @@ const usePrompt = (prompt) => {
<v-card-text> <v-card-text>
<MsgContent :content="message.message" /> <MsgContent :content="message.message" />
</v-card-text> </v-card-text>
<!-- <v-card-actions-->
<!-- v-if="message.is_bot"-->
<!-- >-->
<!-- <v-spacer></v-spacer>-->
<!-- <v-tooltip text="Copy">-->
<!-- <template v-slot:activator="{ props }">-->
<!-- <v-btn v-bind="props" icon="content_copy"></v-btn>-->
<!-- </template>-->
<!-- </v-tooltip>-->
<!-- </v-card-actions>-->
</v-card> </v-card>
<MessageActions
v-if="message.is_bot"
:message="message"
:message-index="index"
:use-prompt="usePrompt"
:delete-message="deleteMessage"
/>
</div> </div>
</v-col> </v-col>
</v-row> </v-row>

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,6 +1,15 @@
export const STORAGE_KEY = { export const STORAGE_KEY = {
OPENAI_MODELS: 'openai_models', MODELS: 'models',
CURRENT_OPENAI_MODEL: 'current_openai_model', CURRENT_MODEL: 'current_model',
OPENAI_API_KEY: 'openai_api_key', OPENAI_API_KEY: 'openai_api_key',
}
export const DEFAULT_MODEL = {
name: 'gpt-3.5-turbo',
frequency_penalty: 0.0,
presence_penalty: 0.0,
max_tokens: 1000,
temperature: 0.7,
top_p: 1.0
} }

View File

@@ -11,32 +11,28 @@ const set = (key, val) => {
localStorage.setItem(key, JSON.stringify(val)) localStorage.setItem(key, JSON.stringify(val))
} }
const DEFAULT_OPENAI_MODEL = 'text-davinci-003'
export const setModels = (val) => { export const setModels = (val) => {
const models = useModels() const models = useModels()
set(STORAGE_KEY.OPENAI_MODELS, val) set(STORAGE_KEY.MODELS, val)
models.value = val models.value = val
} }
export const getStoredModels = () => { export const getStoredModels = () => {
let models = get(STORAGE_KEY.OPENAI_MODELS) let models = get(STORAGE_KEY.MODELS)
if (!models) { if (!models) {
models = [DEFAULT_OPENAI_MODEL] models = [DEFAULT_MODEL]
} }
return models return models
} }
export const setCurrentModel = (val) => { export const saveCurrentModel = (val) => {
const model = useCurrentModel() set(STORAGE_KEY.CURRENT_MODEL, val)
set(STORAGE_KEY.CURRENT_OPENAI_MODEL, val)
model.value = val
} }
export const getCurrentModel = () => { export const getCurrentModel = () => {
let model = get(STORAGE_KEY.CURRENT_OPENAI_MODEL) let model = get(STORAGE_KEY.CURRENT_MODEL)
if (!model) { if (!model) {
model = DEFAULT_OPENAI_MODEL model = DEFAULT_MODEL
} }
return model return model
} }

2204
yarn.lock

File diff suppressed because it is too large Load Diff