Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c5e2a3ac | ||
|
|
e90dc0c12b | ||
|
|
837fd8c9ff | ||
|
|
ce0b1004f3 | ||
|
|
1ff1c46e37 | ||
|
|
afa3e499dc | ||
|
|
70ce5746bc | ||
|
|
8bbc44e7bf | ||
|
|
3dcb4be6e4 | ||
|
|
83f8072625 |
10
README.md
10
README.md
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
BIN
demos/bmc_qr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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密钥。",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user