chore: version 2.8.3 (#175)
* feat: 保留已存在的内容直到手动操作 * feat: 支持复制文本 * chore: version 2.8.3
This commit is contained in:
parent
94e23bb916
commit
42e320fe35
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,3 +1,14 @@
|
|||
## v2.8.3
|
||||
|
||||
`2023-03-01`
|
||||
|
||||
### Feature
|
||||
- 消息已输出内容不会因为中断而消失[#167]
|
||||
- 添加复制消息按钮[#133]
|
||||
|
||||
### Other
|
||||
- `README` 添加声明内容
|
||||
|
||||
## v2.8.2
|
||||
|
||||
`2023-02-28`
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# ChatGPT Web
|
||||
|
||||
> 使用 `express` 和 `vue3` 搭建的支持 `ChatGPT` 双模型演示网页
|
||||
> 声明:此项目只发布于 Github,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。
|
||||
|
||||
![cover](./docs/c1-2.8.0.png)
|
||||
![cover2](./docs/c2-2.8.0.png)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chatgpt-web",
|
||||
"version": "2.8.2",
|
||||
"version": "2.8.3",
|
||||
"private": false,
|
||||
"description": "ChatGPT Web",
|
||||
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
|
||||
|
|
|
@ -8,4 +8,4 @@ OPENAI_ACCESS_TOKEN=
|
|||
API_REVERSE_PROXY=
|
||||
|
||||
# timeout
|
||||
TIMEOUT_MS=60000
|
||||
TIMEOUT_MS=100000
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { h } from 'vue'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
|
||||
export const useIconRender = () => {
|
||||
interface IconConfig {
|
||||
icon?: string
|
||||
color?: string
|
||||
fontSize?: number
|
||||
}
|
||||
|
||||
interface IconStyle {
|
||||
color?: string
|
||||
fontSize?: string
|
||||
}
|
||||
|
||||
const iconRender = (config: IconConfig) => {
|
||||
const { color, fontSize, icon } = config
|
||||
|
||||
const style: IconStyle = {}
|
||||
|
||||
if (color)
|
||||
style.color = color
|
||||
|
||||
if (fontSize)
|
||||
style.fontSize = `${fontSize}px`
|
||||
|
||||
if (!icon)
|
||||
window.console.warn('iconRender: icon is required')
|
||||
|
||||
return () => h(SvgIcon, { icon, style })
|
||||
}
|
||||
|
||||
return {
|
||||
iconRender,
|
||||
}
|
||||
}
|
|
@ -129,6 +129,22 @@ export const useChatStore = defineStore('chat-store', {
|
|||
}
|
||||
},
|
||||
|
||||
updateChatSomeByUuid(uuid: number, index: number, chat: Partial<Chat.Chat>) {
|
||||
if (!uuid || uuid === 0) {
|
||||
if (this.chat.length) {
|
||||
this.chat[0].data[index] = { ...this.chat[0].data[index], ...chat }
|
||||
this.recordState()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const chatIndex = this.chat.findIndex(item => item.uuid === uuid)
|
||||
if (chatIndex !== -1) {
|
||||
this.chat[chatIndex].data[index] = { ...this.chat[chatIndex].data[index], ...chat }
|
||||
this.recordState()
|
||||
}
|
||||
},
|
||||
|
||||
deleteChatByUuid(uuid: number, index: number) {
|
||||
if (!uuid || uuid === 0) {
|
||||
if (this.chat.length) {
|
||||
|
|
|
@ -13,3 +13,15 @@ export function includeCode(text: string | null | undefined) {
|
|||
const regexp = /^(?:\s{4}|\t).+/gm
|
||||
return !!(text?.includes(' = ') || text?.match(regexp))
|
||||
}
|
||||
|
||||
// 复制文本
|
||||
export function copyText(text: string) {
|
||||
const input = document.createElement('input')
|
||||
input.setAttribute('readonly', 'readonly')
|
||||
input.setAttribute('value', text)
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
if (document.execCommand('copy'))
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<script setup lang='ts'>
|
||||
import { NDropdown, useMessage } from 'naive-ui'
|
||||
import AvatarComponent from './Avatar.vue'
|
||||
import TextComponent from './Text.vue'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { copyText } from '@/utils/format'
|
||||
import { useIconRender } from '@/hooks/useIconRender'
|
||||
|
||||
interface Props {
|
||||
dateTime?: string
|
||||
|
@ -16,14 +19,42 @@ interface Emit {
|
|||
(ev: 'delete'): void
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const ms = useMessage()
|
||||
|
||||
const { iconRender } = useIconRender()
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: 'Copy',
|
||||
key: 'copy',
|
||||
icon: iconRender({ icon: 'ri:file-copy-2-line' }),
|
||||
}, {
|
||||
label: 'Delete',
|
||||
key: 'delete',
|
||||
icon: iconRender({ icon: 'ri:delete-bin-line' }),
|
||||
},
|
||||
]
|
||||
|
||||
function handleSelect(key: 'copy' | 'delete') {
|
||||
if (key === 'copy')
|
||||
handleCopy()
|
||||
else
|
||||
handleDelete()
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
emit('delete')
|
||||
}
|
||||
|
||||
function handleCopy() {
|
||||
copyText(props.text ?? '')
|
||||
ms.success('Copied')
|
||||
}
|
||||
|
||||
function handleRegenerate() {
|
||||
emit('regenerate')
|
||||
}
|
||||
|
@ -59,12 +90,11 @@ function handleRegenerate() {
|
|||
>
|
||||
<SvgIcon icon="ri:restart-line" />
|
||||
</button>
|
||||
<button
|
||||
class="mb-1 transition text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<SvgIcon icon="ri:delete-bin-6-line" />
|
||||
</button>
|
||||
<NDropdown :options="options" @select="handleSelect">
|
||||
<button class="transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-200">
|
||||
<SvgIcon icon="ri:function-line" />
|
||||
</button>
|
||||
</NDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,10 @@ import { useChatStore } from '@/store'
|
|||
export function useChat() {
|
||||
const chatStore = useChatStore()
|
||||
|
||||
const getChatByUuidAndIndex = (uuid: number, index: number) => {
|
||||
return chatStore.getChatByUuidAndIndex(uuid, index)
|
||||
}
|
||||
|
||||
const addChat = (uuid: number, chat: Chat.Chat) => {
|
||||
chatStore.addChatByUuid(uuid, chat)
|
||||
}
|
||||
|
@ -11,8 +15,14 @@ export function useChat() {
|
|||
chatStore.updateChatByUuid(uuid, index, chat)
|
||||
}
|
||||
|
||||
const updateChatSome = (uuid: number, index: number, chat: Partial<Chat.Chat>) => {
|
||||
chatStore.updateChatSomeByUuid(uuid, index, chat)
|
||||
}
|
||||
|
||||
return {
|
||||
addChat,
|
||||
updateChat,
|
||||
updateChatSome,
|
||||
getChatByUuidAndIndex,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ const ms = useMessage()
|
|||
const chatStore = useChatStore()
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
const { addChat, updateChat } = useChat()
|
||||
const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
|
||||
const { scrollRef, scrollToBottom } = useScroll()
|
||||
|
||||
const { uuid } = route.params as { uuid: string }
|
||||
|
@ -71,7 +71,7 @@ async function onConversation() {
|
|||
+uuid,
|
||||
{
|
||||
dateTime: new Date().toLocaleString(),
|
||||
text: 'Aha, Thinking...',
|
||||
text: '',
|
||||
loading: true,
|
||||
inversion: false,
|
||||
error: false,
|
||||
|
@ -118,10 +118,34 @@ async function onConversation() {
|
|||
})
|
||||
}
|
||||
catch (error: any) {
|
||||
let errorMessage = error?.message ?? 'Something went wrong, please try again later.'
|
||||
const errorMessage = error?.message ?? 'Something went wrong, please try again later.'
|
||||
|
||||
if (error.message === 'canceled')
|
||||
errorMessage = 'Request canceled. Please try again.'
|
||||
if (error.message === 'canceled') {
|
||||
updateChatSome(
|
||||
+uuid,
|
||||
dataSources.value.length - 1,
|
||||
{
|
||||
loading: false,
|
||||
},
|
||||
)
|
||||
scrollToBottom()
|
||||
return
|
||||
}
|
||||
|
||||
const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)
|
||||
|
||||
if (currentChat?.text && currentChat.text !== '') {
|
||||
updateChatSome(
|
||||
+uuid,
|
||||
dataSources.value.length - 1,
|
||||
{
|
||||
text: `${currentChat.text}\n[${errorMessage}]`,
|
||||
error: false,
|
||||
loading: false,
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
updateChat(
|
||||
+uuid,
|
||||
|
@ -165,7 +189,7 @@ async function onRegenerate(index: number) {
|
|||
index,
|
||||
{
|
||||
dateTime: new Date().toLocaleString(),
|
||||
text: 'Aha, Let me think again...',
|
||||
text: '',
|
||||
inversion: false,
|
||||
error: false,
|
||||
loading: true,
|
||||
|
@ -210,10 +234,18 @@ async function onRegenerate(index: number) {
|
|||
})
|
||||
}
|
||||
catch (error: any) {
|
||||
let errorMessage = error?.message ?? 'Something went wrong, please try again later.'
|
||||
if (error.message === 'canceled') {
|
||||
updateChatSome(
|
||||
+uuid,
|
||||
index,
|
||||
{
|
||||
loading: false,
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (error.message === 'canceled')
|
||||
errorMessage = 'Request canceled. Please try again.'
|
||||
const errorMessage = error?.message ?? 'Something went wrong, please try again later.'
|
||||
|
||||
updateChat(
|
||||
+uuid,
|
||||
|
@ -343,8 +375,8 @@ onUnmounted(() => {
|
|||
@regenerate="onRegenerate(index)"
|
||||
@delete="handleDelete(index)"
|
||||
/>
|
||||
<div class="flex justify-center">
|
||||
<NButton v-if="loading" ghost @click="handleStop">
|
||||
<div class="sticky bottom-0 left-0 flex justify-center">
|
||||
<NButton v-if="loading" type="warning" @click="handleStop">
|
||||
<template #icon>
|
||||
<SvgIcon icon="ri:stop-circle-line" />
|
||||
</template>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<script setup lang='ts'>
|
||||
import { computed, h, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { NDropdown } from 'naive-ui'
|
||||
import { HoverButton, Setting, SvgIcon, UserAvatar } from '@/components/common'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useIconRender } from '@/hooks/useIconRender'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const { iconRender } = useIconRender()
|
||||
|
||||
const show = ref(false)
|
||||
|
||||
const theme = computed(() => appStore.theme)
|
||||
|
@ -14,26 +17,20 @@ const options = [
|
|||
{
|
||||
label: 'Dark',
|
||||
key: 'dark',
|
||||
icon: renderIcon('ri:moon-foggy-line'),
|
||||
icon: iconRender({ icon: 'ri:moon-foggy-line' }),
|
||||
},
|
||||
{
|
||||
label: 'Light',
|
||||
key: 'light',
|
||||
icon: renderIcon('ri:sun-foggy-line'),
|
||||
icon: iconRender({ icon: 'ri:sun-foggy-line' }),
|
||||
},
|
||||
{
|
||||
label: 'Auto',
|
||||
key: 'auto',
|
||||
icon: renderIcon('ri:contrast-line'),
|
||||
icon: iconRender({ icon: 'ri:contrast-line' }),
|
||||
},
|
||||
]
|
||||
|
||||
function renderIcon(icon: string) {
|
||||
return () => {
|
||||
return h(SvgIcon, { icon })
|
||||
}
|
||||
}
|
||||
|
||||
function handleThemeChange(key: 'light' | 'dark' | 'auto') {
|
||||
appStore.setTheme(key)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
// @vueuse/core 不能通过 vue-tsc 检查,所以这里需要忽略,以后将移除
|
||||
"types": ["vite/client", "node", "naive-ui/volar", "web-bluetooth"]
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "service"]
|
||||
|
|
Loading…
Reference in New Issue