mirror of
https://github.com/Chanzhaoyu/chatgpt-web.git
synced 2025-10-22 20:26:31 +00:00
feat: 多会话基础逻辑梳理
This commit is contained in:
30
src/components/business/Chat/hooks/useChat.ts
Normal file
30
src/components/business/Chat/hooks/useChat.ts
Normal file
@@ -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[]
|
|
||||||
|
const dataSources = ref(historyStore.historyChat)
|
||||||
|
|
||||||
|
function handleSelect(index: number) {
|
||||||
|
historyStore.chooseHistory(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emit {
|
function handleEdit(index: number, isEdit: boolean) {
|
||||||
(ev: 'delete', index: number): void
|
historyStore.editHistory(index, isEdit)
|
||||||
(ev: 'edit', index: number): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>()
|
function handleRemove(index: number) {
|
||||||
|
historyStore.removeHistory(index)
|
||||||
const emit = defineEmits<Emit>()
|
|
||||||
|
|
||||||
function handleEdit(index: number) {
|
|
||||||
emit('delete', 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">
|
||||||
<button class="p-1">
|
<template v-if="item.isEdit">
|
||||||
<SvgIcon icon="ri:edit-line" @click="handleEdit(index)" />
|
<button class="p-1" @click="handleEdit(index, false)">
|
||||||
|
<SvgIcon icon="ri:save-line" />
|
||||||
</button>
|
</button>
|
||||||
<button class="p-1" @click="handleDelete(index)">
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button class="p-1">
|
||||||
|
<SvgIcon icon="ri:edit-line" @click="handleEdit(index, true)" />
|
||||||
|
</button>
|
||||||
|
<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)
|
||||||
|
21
src/store/modules/history/helper.ts
Normal file
21
src/store/modules/history/helper.ts
Normal file
@@ -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)
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
20
src/typings/chat.d.ts
vendored
Normal file
20
src/typings/chat.d.ts
vendored
Normal file
@@ -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 })
|
||||||
|
Reference in New Issue
Block a user