feat: 多会话基础逻辑梳理

main
ChenZhaoYu 2 years ago
parent 33c02cfe10
commit de34af8747

@ -0,0 +1,30 @@
import { useHistoryStore } from '@/store'
export function useChat() {
const historyStore = useHistoryStore()
function addChat(message: string, args?: { reversal?: boolean; error?: boolean; options?: Chat.ChatOptions }) {
if (historyStore.historyChat.length === 0) {
historyStore.addHistory({
title: message,
isEdit: false,
data: [],
})
historyStore.chooseHistory(historyStore.historyChat.length - 1)
}
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 }
}

@ -1,27 +1,26 @@
<script setup lang='ts'> <script setup lang='ts'>
import { computed, nextTick, onMounted, ref } from 'vue' import { computed, nextTick, ref } from 'vue'
import { NButton, NInput, useMessage } from 'naive-ui' import { NButton, NInput, useMessage } from 'naive-ui'
import type { ChatOptions, ChatProps } from './types'
import { Message } from './components' import { Message } from './components'
import { Layout } from './layout' import { Layout } from './layout'
import { useChat } from './hooks/useChat'
import { fetchChatAPI } from '@/api' import { fetchChatAPI } from '@/api'
import { HoverButton, SvgIcon } from '@/components/common' import { HoverButton, SvgIcon } from '@/components/common'
import { useHistoryStore } from '@/store'
const ms = useMessage()
const historyStore = useHistoryStore()
const scrollRef = ref<HTMLDivElement>() const scrollRef = ref<HTMLDivElement>()
const ms = useMessage() const { addChat, clearChat } = useChat()
const prompt = ref('') const prompt = ref('')
const loading = ref(false) const loading = ref(false)
const list = ref<ChatProps[]>([]) const list = computed<Chat.Chat[]>(() => historyStore.getCurrentChat)
const chatList = computed(() => list.value.filter(item => (!item.reversal && !item.error))) const chatList = computed<Chat.Chat[]>(() => list.value.filter(item => (!item.reversal && !item.error)))
function initChat() {
addMessage('Hi, I am ChatGPT, a chatbot based on GPT-3.')
}
onMounted(initChat)
async function handleSubmit() { async function handleSubmit() {
if (loading.value) if (loading.value)
@ -37,7 +36,7 @@ async function handleSubmit() {
addMessage(message, { reversal: true }) addMessage(message, { reversal: true })
prompt.value = '' prompt.value = ''
let options: ChatOptions = {} let options: Chat.ChatOptions = {}
const lastContext = chatList.value[chatList.value.length - 1]?.options const lastContext = chatList.value[chatList.value.length - 1]?.options
if (lastContext) if (lastContext)
@ -63,21 +62,14 @@ function handleEnter(event: KeyboardEvent) {
function addMessage( function addMessage(
message: string, message: string,
args?: { reversal?: boolean; error?: boolean; options?: ChatOptions }, args?: { reversal?: boolean; error?: boolean; options?: Chat.ChatOptions },
) { ) {
list.value.push({ addChat(message, args)
dateTime: new Date().toLocaleString(),
message,
reversal: args?.reversal ?? false,
error: args?.error ?? false,
options: args?.options ?? undefined,
})
nextTick(() => scrollRef.value && (scrollRef.value.scrollTop = scrollRef.value.scrollHeight)) nextTick(() => scrollRef.value && (scrollRef.value.scrollTop = scrollRef.value.scrollHeight))
} }
function handleClear() { function handleClear() {
list.value = [] clearChat()
setTimeout(initChat, 100)
} }
</script> </script>
@ -96,8 +88,8 @@ function handleClear() {
</main> </main>
<footer class="p-4"> <footer class="p-4">
<div class="flex items-center justify-between space-x-2"> <div class="flex items-center justify-between space-x-2">
<HoverButton tooltip="Clear conversations" @click="handleClear"> <HoverButton tooltip="Clear conversations">
<span class="text-xl text-[#4f555e]"> <span class="text-xl text-[#4f555e]" @click="handleClear">
<SvgIcon icon="ri:delete-bin-line" /> <SvgIcon icon="ri:delete-bin-line" />
</span> </span>
</HoverButton> </HoverButton>

@ -1,50 +1,65 @@
<script setup lang='ts'> <script setup lang='ts'>
import { NScrollbar } from 'naive-ui' import { ref } from 'vue'
import type { HistoryChatProps } from '../../types' import { NInput, NScrollbar } from 'naive-ui'
import { SvgIcon } from '@/components/common' import { SvgIcon } from '@/components/common'
import { useHistoryStore } from '@/store'
interface Props { const historyStore = useHistoryStore()
data: HistoryChatProps[]
}
interface Emit { const dataSources = ref(historyStore.historyChat)
(ev: 'delete', index: number): void
(ev: 'edit', index: number): void
}
defineProps<Props>() function handleSelect(index: number) {
historyStore.chooseHistory(index)
}
const emit = defineEmits<Emit>() function handleEdit(index: number, isEdit: boolean) {
historyStore.editHistory(index, isEdit)
}
function handleEdit(index: number) { function handleRemove(index: number) {
emit('delete', index) historyStore.removeHistory(index)
} }
function handleDelete(index: number) { function handleEnter(index: number, isEdit: boolean, event: KeyboardEvent) {
emit('delete', index) if (event.key === 'Enter')
handleEdit(index, isEdit)
} }
</script> </script>
<template> <template>
<NScrollbar class="px-4"> <NScrollbar class="px-4">
<div class="flex flex-col gap-2 text-sm"> <div class="flex flex-col gap-2 text-sm">
<div v-for="(item, index) of data" :key="index"> <div v-for="(item, index) of dataSources" :key="index">
<a <a
class="relative flex items-center gap-3 px-3 py-3 break-all rounded-md cursor-pointer bg-neutral-50 pr-14 hover:bg-neutral-100 group" class="relative flex items-center gap-3 px-3 py-3 break-all rounded-md cursor-pointer bg-neutral-50 pr-14 hover:bg-neutral-100 group"
@click="handleSelect(index)"
> >
<span> <span>
<SvgIcon icon="ri:message-3-line" /> <SvgIcon icon="ri:message-3-line" />
</span> </span>
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap max-h-5"> <div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap">
<span>{{ item.title }}</span> <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>
<div class="absolute z-10 flex visible right-1"> <div class="absolute z-10 flex visible right-1">
<template v-if="item.isEdit">
<button class="p-1" @click="handleEdit(index, false)">
<SvgIcon icon="ri:save-line" />
</button>
</template>
<template v-else>
<button class="p-1"> <button class="p-1">
<SvgIcon icon="ri:edit-line" @click="handleEdit(index)" /> <SvgIcon icon="ri:edit-line" @click="handleEdit(index, true)" />
</button> </button>
<button class="p-1" @click="handleDelete(index)"> <button class="p-1" @click="handleRemove(index)">
<SvgIcon icon="ri:delete-bin-line" /> <SvgIcon icon="ri:delete-bin-line" />
</button> </button>
</template>
</div> </div>
</a> </a>
</div> </div>

@ -1,33 +1,23 @@
<script setup lang='ts'> <script setup lang='ts'>
import { ref } from 'vue' import { ref } from 'vue'
import { NButton, NLayoutSider } from 'naive-ui' import { NButton, NLayoutSider } from 'naive-ui'
import type { HistoryChatProps } from '../../types'
import List from './List.vue' import List from './List.vue'
import Footer from './Footer.vue' import Footer from './Footer.vue'
import { useAppStore } from '@/store' import { useAppStore, useHistoryStore } from '@/store'
const appStore = useAppStore() const appStore = useAppStore()
const historyStore = useHistoryStore()
const collapsed = ref(appStore.siderCollapsed ?? false) const collapsed = ref(appStore.siderCollapsed ?? false)
const history = ref<HistoryChatProps[]>([])
function handleAdd() { function handleAdd() {
history.value.push({ historyStore.addHistory({
title: 'New chat', title: '',
edit: false, isEdit: false,
data: [], data: [],
}) })
} }
function handleEdit(index: number) {
history.value[index].edit = true
}
function handleDelete(index: number) {
history.value.splice(index, 1)
}
function handleCollapsed() { function handleCollapsed() {
collapsed.value = !collapsed.value collapsed.value = !collapsed.value
appStore.setSiderCollapsed(collapsed.value) appStore.setSiderCollapsed(collapsed.value)
@ -51,7 +41,7 @@ function handleCollapsed() {
New chat New chat
</NButton> </NButton>
</div> </div>
<List :data="history" @edit="handleEdit" @delete="handleDelete" /> <List />
</main> </main>
<Footer /> <Footer />
</div> </div>

@ -1,18 +0,0 @@
export interface ChatOptions {
conversationId?: string
parentMessageId?: string
}
export interface ChatProps {
dateTime: string
message: string
reversal?: boolean
error?: boolean
options?: ChatOptions
}
export interface HistoryChatProps {
title: string
edit: boolean
data: ChatProps[]
}

@ -1,4 +1,6 @@
import { ls } from '@/utils/storage' import { ss } from '@/utils/storage'
const LOCAL_NAME = 'appSetting'
export interface AppState { export interface AppState {
siderCollapsed: boolean siderCollapsed: boolean
@ -8,11 +10,11 @@ export function defaultSetting() {
return { siderCollapsed: false } return { siderCollapsed: false }
} }
export function getAppSetting() { export function getLocalSetting() {
const localSetting: AppState = ls.get('appSetting') const localSetting: AppState | undefined = ss.get(LOCAL_NAME)
return localSetting ?? defaultSetting() return localSetting ?? defaultSetting()
} }
export function setAppSetting(setting: AppState) { export function setLocalSetting(setting: AppState) {
ls.set('appSetting', setting) ss.set(LOCAL_NAME, setting)
} }

@ -1,13 +1,13 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { AppState } from './helper' import type { AppState } from './helper'
import { getAppSetting, setAppSetting } from './helper' import { getLocalSetting, setLocalSetting } from './helper'
export const useAppStore = defineStore('app-store', { export const useAppStore = defineStore('app-store', {
state: (): AppState => getAppSetting(), state: (): AppState => getLocalSetting(),
actions: { actions: {
setSiderCollapsed(collapsed: boolean) { setSiderCollapsed(collapsed: boolean) {
this.siderCollapsed = collapsed this.siderCollapsed = collapsed
setAppSetting(this.$state) setLocalSetting(this.$state)
}, },
toggleSiderCollapse() { toggleSiderCollapse() {
this.setSiderCollapsed(!this.siderCollapsed) this.setSiderCollapsed(!this.siderCollapsed)

@ -0,0 +1,21 @@
import { ss } from '@/utils/storage'
const LOCAL_NAME = 'historyChat'
export interface HistoryState {
historyChat: Chat.HistoryChat[]
active: number | null
}
export function defaultSetting() {
return { historyChat: [], active: null }
}
export function getLocalHistory() {
const localSetting: HistoryState | undefined = ss.get(LOCAL_NAME)
return localSetting ?? defaultSetting()
}
export function setLocalHistory(data: HistoryState) {
ss.set(LOCAL_NAME, data)
}

@ -1,12 +1,55 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { HistoryState } from './helper'
interface HistoryState { import { getLocalHistory, setLocalHistory } from './helper'
list: any[]
}
export const useHistoryStore = defineStore('history-store', { export const useHistoryStore = defineStore('history-store', {
state: (): HistoryState => ({ state: (): HistoryState => getLocalHistory(),
list: [], getters: {
}), getCurrentChat(state): Chat.Chat[] {
actions: {}, if (state.historyChat.length === 0)
return []
if (state.active === null)
state.active = state.historyChat.length - 1
return state.historyChat[state.active].data
},
},
actions: {
addChat(data: Chat.Chat) {
if (this.active !== null) {
this.historyChat[this.active].data.push(data)
this.active = this.historyChat.length - 1
setLocalHistory(this.$state)
}
},
clearChat() {
if (this.active !== null) {
this.historyChat[this.active].data = []
setLocalHistory(this.$state)
}
},
chooseHistory(index: number) {
this.active = index
setLocalHistory(this.$state)
},
addHistory(data: Chat.HistoryChat) {
this.historyChat.push(data)
this.active = this.historyChat.length - 1
setLocalHistory(this.$state)
},
editHistory(index: number, isEdit: boolean) {
this.historyChat[index].isEdit = isEdit
setLocalHistory(this.$state)
},
removeHistory(index: number) {
this.historyChat.splice(index, 1)
setLocalHistory(this.$state)
},
},
}) })

@ -0,0 +1,20 @@
declare namespace Chat{
interface ChatOptions {
conversationId?: string
parentMessageId?: string
}
interface Chat {
dateTime: string
message: string
reversal?: boolean
error?: boolean
options?: ChatOptions
}
interface HistoryChat {
title: string
isEdit: boolean
data: Chat[]
}
}

@ -1,19 +1,28 @@
import { deCrypto, enCrypto } from '../crypto' import { deCrypto, enCrypto } from '../crypto'
interface StorageData<T = any> { interface StorageData<T = any> {
value: T data: T
expire: number | null expire: number | null
} }
function createLocalStorage() { export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) {
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 // 7 days const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
function set<T = any>(key: string, value: T, expire: number | null = DEFAULT_CACHE_TIME) { const { expire, crypto } = Object.assign(
{
expire: DEFAULT_CACHE_TIME,
crypto: true,
},
options,
)
function set<T = any>(key: string, data: T) {
const storageData: StorageData<T> = { const storageData: StorageData<T> = {
value, data,
expire: expire !== null ? new Date().getTime() + expire * 1000 : null, expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
} }
const json = enCrypto(storageData)
const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData)
window.localStorage.setItem(key, json) window.localStorage.setItem(key, json)
} }
@ -23,16 +32,16 @@ function createLocalStorage() {
let storageData: StorageData | null = null let storageData: StorageData | null = null
try { try {
storageData = deCrypto(json) storageData = crypto ? deCrypto(json) : JSON.parse(json)
} }
catch { catch {
// Prevent failure // Prevent failure
} }
if (storageData) { if (storageData) {
const { value, expire } = storageData const { data, expire } = storageData
if (expire === null || expire >= Date.now()) if (expire === null || expire >= Date.now())
return value return data
} }
remove(key) remove(key)
@ -57,3 +66,5 @@ function createLocalStorage() {
} }
export const ls = createLocalStorage() export const ls = createLocalStorage()
export const ss = createLocalStorage({ expire: null, crypto: false })

Loading…
Cancel
Save