Compare commits

...

10 Commits

Author SHA1 Message Date
Rafi
27c5e2a3ac Get settings from backend, added web search functionality 2023-03-23 11:45:56 +08:00
Rafi
e90dc0c12b web_search toolbar 2023-03-22 23:29:58 +08:00
Rafi
837fd8c9ff update readme 2023-03-22 17:26:22 +08:00
Rafi
ce0b1004f3 Remove the parent_message_id constraint 2023-03-22 16:17:46 +08:00
Rafi
1ff1c46e37 Fix the bug of being unable to delete messages. 2023-03-22 15:55:06 +08:00
Rafi
afa3e499dc add DEBUT_PWA env variable 2023-03-22 14:12:49 +08:00
Rafi
70ce5746bc Merge remote-tracking branch 'origin/main' into main
# Conflicts:
#	nuxt.config.ts
#	yarn.lock
2023-03-22 13:50:53 +08:00
Rafi
8bbc44e7bf update nuxt.config.ts 2023-03-21 18:48:35 +08:00
Rafi
3dcb4be6e4 add robots.txt 2023-03-21 18:06:44 +08:00
Rafi
83f8072625 mv @vite-pwa/nuxt to devDependencies 2023-03-21 13:46:02 +08:00
16 changed files with 2120 additions and 72 deletions

View File

@@ -156,6 +156,16 @@ Before you can start chatting, you need to add an OpenAI API key. In the Setting
Now you can access the web client at `http(s)://your.domain` or `http://123.123.123.123` to start chatting. Now you can access the web client at `http(s)://your.domain` or `http://123.123.123.123` to start chatting.
## Donation
> If it is helpful to you, it is also helping me.
If you want to support me, Buy me a coffee ❤️ [https://www.buymeacoffee.com/WongSaang](https://www.buymeacoffee.com/WongSaang)
<p align="center">
<img height="150" src="https://github.com/WongSaang/chatgpt-ui/blob/main/demos/bmc_qr.png?raw=true"/>
</p>
## Development ## Development
### Setup ### Setup

View File

@@ -41,7 +41,7 @@ const deleteMessage = async () => {
method: 'DELETE' method: 'DELETE'
}) })
if (!error.value) { if (!error.value) {
this.$emit('deleteMessage', props.messageIndex) props.deleteMessage(props.messageIndex)
showSnackbar('Deleted!') showSnackbar('Deleted!')
} }
showSnackbar('Delete failed') showSnackbar('Delete failed')

View File

@@ -12,14 +12,19 @@ const md = new MarkdownIt({
}, },
}) })
const props = defineProps(['content']) const props = defineProps({
message: {
type: Object,
required: true
}
})
const contentHtml = ref('') const contentHtml = ref('')
const contentElm = ref(null) const contentElm = ref(null)
watchEffect(() => { watchEffect(() => {
contentHtml.value = props.content ? md.render(props.content) : '' contentHtml.value = props.message.message ? md.render(props.message.message) : ''
}) })
const bindCopyCodeToButtons = () => { const bindCopyCodeToButtons = () => {
@@ -54,11 +59,19 @@ onUpdated(() => {
</script> </script>
<template> <template>
<v-card
:color="message.is_bot ? '' : 'primary'"
rounded="lg"
elevation="2"
>
<v-card-text>
<div <div
ref="contentElm" ref="contentElm"
v-html="contentHtml" v-html="contentHtml"
class="chat-msg-content" class="chat-msg-content"
></div> ></div>
</v-card-text>
</v-card>
</template> </template>
<style> <style>

View File

@@ -96,10 +96,12 @@ onMounted( () => {
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn <v-btn
v-bind="props" v-bind="props"
icon
>
<v-icon
icon="speaker_notes" icon="speaker_notes"
title="Common prompts" ></v-icon>
class="mr-3" </v-btn>
></v-btn>
</template> </template>
<v-container> <v-container>

View File

@@ -8,3 +8,5 @@ export const useApiKey = () => useState('apiKey', () => getStoredApiKey())
export const useConversion = () => useState('conversion', () => getDefaultConversionData()) export const useConversion = () => useState('conversion', () => getDefaultConversionData())
export const useConversions = () => useState('conversions', () => []) export const useConversions = () => useState('conversions', () => [])
export const useSettings = () => useState('settings', () => {})

BIN
demos/bmc_qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -154,6 +154,16 @@ networks:
现在可以访问客户端地址 `http(s)://your.domain` / `http://123.123.123.123` 开始聊天。 现在可以访问客户端地址 `http(s)://your.domain` / `http://123.123.123.123` 开始聊天。
## 续杯咖啡
> 如果对您有帮助,也是在帮助我自己.
如果你想支持我,给我续杯咖啡吧 ❤️ [https://www.buymeacoffee.com/WongSaang](https://www.buymeacoffee.com/WongSaang)
<p align="center">
<img height="150" src="https://github.com/WongSaang/chatgpt-ui/blob/main/demos/bmc_qr.png?raw=true"/>
</p>
## Development ## Development
### Setup ### Setup

View File

@@ -34,6 +34,7 @@
"copied": "Copied", "copied": "Copied",
"delete": "Delete", "delete": "Delete",
"signOut": "Sign out", "signOut": "Sign out",
"webSearch": "Web Search",
"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

@@ -34,6 +34,7 @@
"copied": "已复制", "copied": "已复制",
"delete": "删除", "delete": "删除",
"signOut": "退出登录", "signOut": "退出登录",
"webSearch": "网页搜索",
"welcomeScreen": { "welcomeScreen": {
"introduction1": "是一个非官方的ChatGPT客户端但使用OpenAI的官方API", "introduction1": "是一个非官方的ChatGPT客户端但使用OpenAI的官方API",
"introduction2": "在使用本客户端之前您需要一个OpenAI API密钥。", "introduction2": "在使用本客户端之前您需要一个OpenAI API密钥。",

View File

@@ -84,6 +84,7 @@ const loadConversations = async () => {
const {mdAndUp} = useDisplay() const {mdAndUp} = useDisplay()
const drawerPermanent = computed(() => { const drawerPermanent = computed(() => {
return mdAndUp.value return mdAndUp.value
}) })
@@ -97,8 +98,9 @@ const signOut = async () => {
} }
} }
onNuxtReady(async () => { onMounted(async () => {
loadConversations() loadConversations()
loadSettings()
}) })
</script> </script>

View File

@@ -36,7 +36,7 @@ export default defineNuxtConfig({
description: 'A ChatGPT web Client' description: 'A ChatGPT web Client'
}, },
workbox: { workbox: {
enabled: true enabled: process.env.DEBUT_PWA === 'true',
} }
}, },
i18n: { i18n: {

View File

@@ -11,6 +11,7 @@
"@kevinmarrec/nuxt-pwa": "^0.17.0", "@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",
"@vite-pwa/nuxt": "^0.0.7",
"material-design-icons-iconfont": "^6.7.0", "material-design-icons-iconfont": "^6.7.0",
"nuxt": "^3.2.0" "nuxt": "^3.2.0"
}, },

View File

@@ -5,8 +5,6 @@ definePageMeta({
middleware: ["auth"] middleware: ["auth"]
}) })
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source' import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
import { nextTick } from 'vue'
import MessageActions from "~/components/MessageActions.vue";
const { $i18n, $auth } = useNuxtApp() const { $i18n, $auth } = useNuxtApp()
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
@@ -50,15 +48,22 @@ const abortFetch = () => {
} }
fetchingResponse.value = false fetchingResponse.value = false
} }
const fetchReply = async (message, parentMessageId) => { const fetchReply = async (message) => {
ctrl = new AbortController() ctrl = new AbortController()
let webSearchParams = {}
console.log(enableWebSearch.value)
if (enableWebSearch.value) {
webSearchParams['web_search'] = {
ua: navigator.userAgent
}
}
const data = Object.assign({}, currentModel.value, { const data = Object.assign({}, currentModel.value, {
openaiApiKey: openaiApiKey.value, openaiApiKey: openaiApiKey.value,
message: message, message: message,
parentMessageId: parentMessageId,
conversationId: currentConversation.value.id conversationId: currentConversation.value.id
}) }, webSearchParams)
try { try {
await fetchEventSource('/api/conversation/', { await fetchEventSource('/api/conversation/', {
@@ -93,6 +98,11 @@ const fetchReply = async (message, parentMessageId) => {
throw new Error(data.error); throw new Error(data.error);
} }
if (event === 'userMessageId') {
currentConversation.value.messages[currentConversation.value.messages.length - 1].id = data.userMessageId
return;
}
if (event === 'done') { if (event === 'done') {
if (currentConversation.value.id === null) { if (currentConversation.value.id === null) {
currentConversation.value.id = data.conversationId currentConversation.value.id = data.conversationId
@@ -129,15 +139,8 @@ const scrollChatWindow = () => {
const send = (message) => { const send = (message) => {
fetchingResponse.value = true fetchingResponse.value = true
let parentMessageId = null currentConversation.value.messages.push({message: message})
if (currentConversation.value.messages.length > 0) { fetchReply(message)
const lastMessage = currentConversation.value.messages[currentConversation.value.messages.length - 1]
if (lastMessage.is_bot && lastMessage.id !== null) {
parentMessageId = lastMessage.id
}
}
currentConversation.value.messages.push({parentMessageId: parentMessageId, message: message})
fetchReply(message, parentMessageId)
scrollChatWindow() scrollChatWindow()
} }
const stop = () => { const stop = () => {
@@ -160,6 +163,18 @@ const deleteMessage = (index) => {
currentConversation.value.messages.splice(index, 1) currentConversation.value.messages.splice(index, 1)
} }
const showWebSearchToggle = ref(false)
const enableWebSearch = ref(false)
const settings = useSettings()
watchEffect(() => {
if (settings.value) {
const settingsValue = toRaw(settings.value)
showWebSearchToggle.value = settingsValue.open_web_search && settingsValue.open_web_search === 'True'
}
})
</script> </script>
<template> <template>
@@ -184,15 +199,7 @@ const deleteMessage = (index) => {
:use-prompt="usePrompt" :use-prompt="usePrompt"
:delete-message="deleteMessage" :delete-message="deleteMessage"
/> />
<v-card <MsgContent :message="message" />
:color="message.is_bot ? '' : 'primary'"
rounded="lg"
elevation="2"
>
<v-card-text>
<MsgContent :content="message.message" />
</v-card-text>
</v-card>
<MessageActions <MessageActions
v-if="message.is_bot" v-if="message.is_bot"
:message="message" :message="message"
@@ -208,9 +215,9 @@ const deleteMessage = (index) => {
<div ref="grab" class="w-100" style="height: 200px;"></div> <div ref="grab" class="w-100" style="height: 200px;"></div>
</div> </div>
<Welcome v-else /> <Welcome v-else />
<v-footer app class="d-flex flex-column"> <v-footer app>
<div class="px-md-16 w-100 d-flex align-center"> <div class="px-md-16 w-100 d-flex flex-column">
<Prompt v-show="!fetchingResponse" :use-prompt="usePrompt" /> <div class="d-flex align-center">
<v-btn <v-btn
v-show="fetchingResponse" v-show="fetchingResponse"
icon="close" icon="close"
@@ -220,9 +227,24 @@ const deleteMessage = (index) => {
></v-btn> ></v-btn>
<MsgEditor ref="editor" :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" /> <MsgEditor ref="editor" :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
</div> </div>
<v-toolbar
density="comfortable"
color="transparent"
>
<Prompt v-show="!fetchingResponse" :use-prompt="usePrompt" />
<v-switch
v-if="showWebSearchToggle"
v-model="enableWebSearch"
hide-details
color="primary"
:label="$t('webSearch')"
></v-switch>
<v-spacer></v-spacer>
</v-toolbar>
<div class="px-4 py-2 text-disabled text-caption font-weight-light text-center w-100"> <!-- <div class="py-2 text-disabled text-caption font-weight-light text-center">-->
© {{ new Date().getFullYear() }} {{ runtimeConfig.public.appName }} <!-- © {{ new Date().getFullYear() }} {{ runtimeConfig.public.appName }}-->
<!-- </div>-->
</div> </div>
</v-footer> </v-footer>
<v-snackbar <v-snackbar

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Allow: /

View File

@@ -51,3 +51,22 @@ export const genTitle = async (conversationId) => {
} }
return null return null
} }
const transformData = (list) => {
const result = {};
for (let i = 0; i < list.length; i++) {
const item = list[i];
result[item.name] = item.value;
}
return result;
}
export const loadSettings = async () => {
const settings = useSettings()
const { data, error } = await useAuthFetch('/api/chat/settings/', {
method: 'GET'
})
if (!error.value) {
settings.value = transformData(data.value)
}
}

2003
yarn.lock

File diff suppressed because it is too large Load Diff