chore: v2.5.0 整体优化 (#70)

* feat: locale language

* refactor: 页面暂存

* feat: 逻辑判断

* feat: 分组消息

* feat: 实验场

* feat: 重新请求结果

* feat: 基础问答逻辑和重新询问

* feat: 上下文消息删除确认

* feat: 处理类型报错

* chore: 更新 deps 和移除 i18n

* feat: 路由页面切换终止请求

* feat: let me think

* feat: 信息更新代码高亮匹配

* feat: 加载时添加光标

* feat: 错误提示

* feat: 历史记录删除确认

* fix: 侧边栏高度不正确的问题

* chore: version 2.5.0

* chore: update deps
This commit is contained in:
Redon
2023-02-20 14:10:51 +08:00
committed by GitHub
parent 6216d84ecd
commit fda6c6bb6a
35 changed files with 1101 additions and 691 deletions

View File

@@ -1,16 +0,0 @@
<script lang="ts" setup>
interface Props {
image?: boolean
}
defineProps<Props>()
</script>
<template>
<img v-if="image" src="@/assets/avatar.jpg" class="object-cover w-full h-full " alt="avatar">
<span v-else class="text-[27px]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" width="1em" height="1em">
<path d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z" />
</svg>
</span>
</template>

View File

@@ -1,22 +0,0 @@
<script lang="ts" setup>
interface Props {
reversal?: boolean
error?: boolean
}
defineProps<Props>()
</script>
<template>
<div class="p-2 mt-2 rounded-md" :class="[reversal ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]']">
<span v-highlight class="leading-relaxed whitespace-pre-wrap">
<slot />
</span>
</div>
</template>
<style>
.hljs {
background-color: #fff0 !important;
}
</style>

View File

@@ -1,32 +0,0 @@
<script setup lang='ts'>
import Avatar from './Avatar.vue'
import Text from './Text.vue'
interface Props {
message?: string
dateTime?: string
reversal?: boolean
error?: boolean
}
defineProps<Props>()
</script>
<template>
<div class="flex w-full mb-6" :class="[{ 'flex-row-reverse': reversal }]">
<div
class="flex items-center justify-center rounded-full overflow-hidden w-[32px] h-[32px]"
:class="[reversal ? 'ml-3' : 'mr-3']"
>
<Avatar :image="reversal" />
</div>
<div class="flex flex-col flex-1 text-sm" :class="[reversal ? 'items-end' : 'items-start']">
<span class="text-xs text-[#b4bbc4]">
{{ dateTime }}
</span>
<Text :reversal="reversal" :error="error">
{{ message }}
</Text>
</div>
</div>
</template>

View File

@@ -1,3 +0,0 @@
import Message from './Message/index.vue'
export { Message }

View File

@@ -1,26 +0,0 @@
import { useHistoryStore } from '@/store'
export function useChat() {
const historyStore = useHistoryStore()
function addChat(
message: string,
args?: { reversal?: boolean; error?: boolean; options?: Chat.ChatOptions },
) {
historyStore.addChat(
{
dateTime: new Date().toLocaleString(),
message,
reversal: args?.reversal ?? false,
error: args?.error ?? false,
options: args?.options ?? undefined,
},
)
}
function clearChat() {
historyStore.clearChat()
}
return { addChat, clearChat }
}

View File

@@ -1,208 +0,0 @@
<script setup lang='ts'>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { MessageReactive } from 'naive-ui'
import { NButton, NInput, useMessage } from 'naive-ui'
import { Message } from './components'
import { Layout } from './layout'
import { useChat } from './hooks/useChat'
import { fetchChatAPI } from '@/api'
import { HoverButton, SvgIcon } from '@/components/common'
import { useHistoryStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
let controller = new AbortController()
const ms = useMessage()
const historyStore = useHistoryStore()
const { isMobile } = useBasicLayout()
let messageReactive: MessageReactive | null = null
const scrollRef = ref<HTMLDivElement>()
const { addChat, clearChat } = useChat()
const prompt = ref('')
const loading = ref(false)
const currentActive = computed(() => historyStore.active)
const heartbeat = computed(() => historyStore.heartbeat)
const list = computed<Chat.Chat[]>(() => historyStore.getCurrentChat)
const chatList = computed<Chat.Chat[]>(() => list.value.filter(item => (!item.reversal && !item.error)))
const footerMobileStyle = computed(() => {
if (isMobile.value)
return ['pl-2', 'pt-2', 'pb-6', 'fixed', 'bottom-0', 'left-0', 'right-0', 'z-30']
return []
})
async function handleSubmit() {
if (loading.value)
return
controller = new AbortController()
const message = prompt.value.trim()
if (!message || !message.length) {
ms.warning('Please enter a message')
return
}
addMessage(message, { reversal: true })
prompt.value = ''
let options: Chat.ChatOptions = {}
const lastContext = chatList.value[chatList.value.length - 1]?.options
if (lastContext)
options = { ...lastContext }
try {
loading.value = true
createMessage()
const { data } = await fetchChatAPI(message, options, controller.signal)
addMessage(data?.text ?? '', { options: { conversationId: data.conversationId, parentMessageId: data.id } })
}
catch (error: any) {
if (error.message !== 'canceled')
addMessage(`${error.message ?? 'Request failed, please try again later.'}`, { error: true })
}
finally {
loading.value = false
removeMessage()
}
}
function handleEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSubmit()
}
}
function addMessage(
message: string,
args?: { reversal?: boolean; error?: boolean; options?: Chat.ChatOptions },
) {
addChat(message, args)
scrollToBottom()
}
function scrollToBottom() {
nextTick(() => scrollRef.value && (scrollRef.value.scrollTop = scrollRef.value.scrollHeight))
}
function handleClear() {
handleCancel()
clearChat()
}
function handleCancel() {
controller.abort()
controller = new AbortController()
loading.value = false
removeMessage()
}
function createMessage() {
if (!messageReactive) {
messageReactive = ms.loading('Thinking...', {
duration: 0,
})
}
}
function removeMessage() {
if (messageReactive) {
messageReactive.destroy()
messageReactive = null
}
}
onMounted(() => {
scrollToBottom()
})
onBeforeUnmount(() => {
handleCancel()
})
watch(
heartbeat,
() => {
handleCancel()
scrollToBottom()
},
)
watch(
currentActive,
(_, oldActive) => {
if (oldActive !== null) {
handleCancel()
scrollToBottom()
}
},
)
</script>
<template>
<Layout>
<div class="flex flex-col h-full">
<main class="flex-1 overflow-hidden">
<div
ref="scrollRef"
class="h-full p-4 overflow-hidden overflow-y-auto"
:class="[{ 'p-2': isMobile }]"
>
<template v-if="!list.length">
<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
<span>Aha~</span>
</div>
</template>
<template v-else>
<div>
<Message
v-for="(item, index) of list"
:key="index"
:date-time="item.dateTime"
:message="item.message"
:reversal="item.reversal"
:error="item.error"
/>
</div>
</template>
</div>
</main>
<footer
class="p-4"
:class="footerMobileStyle"
>
<div class="flex items-center justify-between space-x-2">
<HoverButton tooltip="Clear conversations">
<span class="text-xl text-[#4f555e]" @click="handleClear">
<SvgIcon icon="ri:delete-bin-line" />
</span>
</HoverButton>
<NInput
v-model:value="prompt"
type="textarea"
:autosize="{ minRows: 1, maxRows: 2 }"
placeholder="Ask me anything..."
@keypress="handleEnter"
/>
<NButton type="primary" :disabled="loading" @click="handleSubmit">
<template #icon>
<SvgIcon icon="ri:send-plane-fill" />
</template>
</NButton>
</div>
</footer>
</div>
</Layout>
</template>

View File

@@ -1,43 +0,0 @@
<script setup lang='ts'>
import { computed } from 'vue'
import { NLayout, NLayoutContent } from 'naive-ui'
import Sider from './sider/index.vue'
import Header from './header/index.vue'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { useAppStore } from '@/store'
const appStore = useAppStore()
const { isMobile } = useBasicLayout()
const collapsed = computed(() => appStore.siderCollapsed)
const getMobileClass = computed(() => {
if (isMobile.value)
return ['rounded-none', 'shadow-none']
return ['border', 'rounded-md', 'shadow-md']
})
const getContainerClass = computed(() => {
return [
'h-full',
{ 'pt-14': isMobile.value },
{ 'pb-[70px]': isMobile.value },
{ 'pl-[260px]': !isMobile.value && !collapsed.value },
]
})
</script>
<template>
<div class="h-screen" :class="[isMobile ? 'p-0' : 'p-4']">
<div class="h-full overflow-hidden" :class="getMobileClass">
<NLayout class="z-40 transition" :class="getContainerClass" has-sider>
<Sider />
<Header v-if="isMobile" />
<NLayoutContent class="h-full">
<slot />
</NLayoutContent>
</NLayout>
</div>
</div>
</template>

View File

@@ -1,36 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { SvgIcon } from '@/components/common'
import { useAppStore, useHistoryStore } from '@/store'
const appStore = useAppStore()
const historyStore = useHistoryStore()
const collapsed = computed(() => appStore.siderCollapsed)
function handleAdd() {
historyStore.addHistory({
title: 'New Chat',
isEdit: false,
data: [],
})
}
function handleUpdateCollapsed() {
appStore.setSiderCollapsed(!collapsed.value)
}
</script>
<template>
<header class="fixed top-0 left-0 right-0 z-50 border-b bg-white/80 backdrop-blur">
<div class="relative flex items-center justify-between h-14">
<button class="flex items-center justify-center w-11 h-11" @click="handleUpdateCollapsed">
<SvgIcon v-if="collapsed" class="text-2xl" icon="ri:align-justify" />
<SvgIcon v-else class="text-2xl" icon="ri:align-right" />
</button>
<button class="flex items-center justify-center w-11 h-11" @click="handleAdd">
<SvgIcon class="text-2xl" icon="ri:add-fill" />
</button>
</div>
</header>
</template>

View File

@@ -1,3 +0,0 @@
import Layout from './Layout.vue'
export { Layout }

View File

@@ -1,79 +0,0 @@
<script setup lang='ts'>
import { ref } from 'vue'
import { NInput, NScrollbar } from 'naive-ui'
import { SvgIcon } from '@/components/common'
import { useHistoryStore } from '@/store'
const historyStore = useHistoryStore()
const dataSources = ref(historyStore.historyChat)
function handleSelect(index: number) {
historyStore.chooseHistory(index)
}
function handleEdit(index: number, isEdit: boolean, event?: MouseEvent) {
historyStore.editHistory(index, isEdit)
event?.stopPropagation()
}
function handleRemove(index: number, event?: MouseEvent) {
historyStore.removeHistory(index)
event?.stopPropagation()
}
function handleEnter(index: number, isEdit: boolean, event: KeyboardEvent) {
if (event.key === 'Enter') {
handleEdit(index, isEdit)
event.stopPropagation()
}
}
</script>
<template>
<NScrollbar class="px-4">
<div class="flex flex-col gap-2 text-sm">
<template v-if="!dataSources.length">
<div class="flex flex-col items-center mt-4 text-center text-neutral-300">
<SvgIcon icon="ri:inbox-line" class="mb-2 text-3xl" />
<span>No history</span>
</div>
</template>
<template v-else>
<div v-for="(item, index) of dataSources" :key="index">
<a
class="relative flex items-center gap-3 px-3 py-3 break-all border rounded-md cursor-pointer hover:bg-neutral-100 group"
:class="historyStore.active === index && ['border-[#4b9e5f]', 'bg-neutral-100', 'text-[#4b9e5f]', 'pr-14']"
@click="handleSelect(index)"
>
<span>
<SvgIcon icon="ri:message-3-line" />
</span>
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap">
<NInput
v-if="item.isEdit" v-model:value="item.title" size="tiny"
@keypress="handleEnter(index, false, $event)"
/>
<span v-else>{{ item.title }}</span>
</div>
<div v-if="historyStore.active === index" class="absolute z-10 flex visible right-1">
<template v-if="item.isEdit">
<button class="p-1" @click="handleEdit(index, false, $event)">
<SvgIcon icon="ri:save-line" />
</button>
</template>
<template v-else>
<button class="p-1">
<SvgIcon icon="ri:edit-line" @click="handleEdit(index, true, $event)" />
</button>
<button class="p-1" @click="handleRemove(index, $event)">
<SvgIcon icon="ri:delete-bin-line" />
</button>
</template>
</div>
</a>
</div>
</template>
</div>
</NScrollbar>
</template>

View File

@@ -1,73 +0,0 @@
<script setup lang='ts'>
import { computed, watch } from 'vue'
import { NButton, NLayoutSider } from 'naive-ui'
import List from './List.vue'
import { HoverButton, SvgIcon, UserAvatar } from '@/components/common'
import { useAppStore, useHistoryStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
const appStore = useAppStore()
const historyStore = useHistoryStore()
const { isMobile } = useBasicLayout()
const collapsed = computed(() => appStore.siderCollapsed)
function handleAdd() {
historyStore.addHistory({
title: 'New Chat',
isEdit: false,
data: [],
})
}
function handleUpdateCollapsed() {
appStore.setSiderCollapsed(!collapsed.value)
}
watch(
isMobile,
(val) => {
appStore.setSiderCollapsed(val)
},
{
immediate: true,
flush: 'post',
},
)
</script>
<template>
<NLayoutSider
:collapsed="collapsed"
:collapsed-width="0"
:width="260"
:show-trigger="isMobile ? false : 'arrow-circle'"
collapse-mode="transform"
position="absolute"
bordered
style="z-index: 50;"
@update-collapsed="handleUpdateCollapsed"
>
<div class="flex flex-col h-full" :class="[{ 'pt-14': isMobile }]">
<main class="flex-1 min-h-0 overflow-hidden">
<div class="p-4">
<NButton dashed block @click="handleAdd">
New chat
</NButton>
</div>
<List />
</main>
<footer class="flex items-center justify-between min-w-0 p-4 overflow-hidden border-t h-[70px]">
<UserAvatar />
<HoverButton tooltip="Setting">
<span class="text-xl text-[#4f555e]">
<SvgIcon icon="ri:settings-4-line" />
</span>
</HoverButton>
</footer>
</div>
</NLayoutSider>
<template v-if="isMobile">
<div v-show="!collapsed" class="absolute inset-0 z-40 bg-black/40" @click="handleUpdateCollapsed" />
</template>
</template>

View File

@@ -1,3 +1 @@
import Chat from './Chat/index.vue'
export { Chat }
export { }