241 lines
7.0 KiB
Vue
241 lines
7.0 KiB
Vue
<script lang="ts" setup>
|
|
import ChatItem from "~/components/aigc/chat/ChatItem.vue";
|
|
import type {ChatMessage, ChatMessageId, ChatSessionId} from "~/components/aigc/chat";
|
|
import Message from "~/components/aigc/chat/Message.vue";
|
|
import {useIdGenerator} from "~/composables/useIdGenerator";
|
|
import {useHistory} from "~/composables/useHistory";
|
|
|
|
useHead({
|
|
title: '聊天 | XSH AI'
|
|
})
|
|
|
|
const dayjs = useDayjs()
|
|
const toast = useToast()
|
|
const {generateUUID} = useIdGenerator()
|
|
const historyStore = useHistory()
|
|
const {chatSessions} = storeToRefs(historyStore)
|
|
const {setChatSessions} = historyStore
|
|
|
|
// const chatSessions = ref<ChatSession[]>([])
|
|
const currentSessionId = ref<ChatSessionId | null>(null)
|
|
const messagesWrapperRef = ref<HTMLDivElement | null>(null)
|
|
|
|
const user_input = ref('')
|
|
const responding = ref(false)
|
|
|
|
const getSessionCopyById = (chatSessionId: ChatSessionId) => chatSessions.value.find(s => s.id === chatSessionId);
|
|
const selectCurrentSessionId = (chatSessionId?: ChatSessionId) => {
|
|
if (chatSessions.value.length > 0) {
|
|
if (chatSessionId) { // 切换到指定 ID
|
|
// 保存当前输入并清空输入框
|
|
setChatSessions(chatSessions.value.map(s => s.id === currentSessionId.value ? {
|
|
...s,
|
|
last_input: user_input.value,
|
|
} : s))
|
|
user_input.value = ''
|
|
// 切换到指定 ID 会话
|
|
currentSessionId.value = chatSessionId
|
|
// 恢复输入
|
|
user_input.value = getSessionCopyById(chatSessionId)?.last_input || ''
|
|
// 清除已恢复的输入
|
|
setChatSessions(chatSessions.value.map(s => s.id === chatSessionId ? {
|
|
...s,
|
|
last_input: '',
|
|
} : s))
|
|
} else { // 切换到第一个会话
|
|
currentSessionId.value = chatSessions.value[0].id
|
|
}
|
|
} else {
|
|
handleClickCreateSession()
|
|
}
|
|
}
|
|
|
|
const handleClickCreateSession = () => {
|
|
// 生成一个新的会话 ID
|
|
const sessionId = generateUUID()
|
|
// 插入新会话数据
|
|
setChatSessions([
|
|
{
|
|
id: sessionId,
|
|
subject: '新对话',
|
|
messages: [],
|
|
create_at: dayjs().unix(),
|
|
},
|
|
...chatSessions.value,
|
|
])
|
|
// 切换到新的会话
|
|
selectCurrentSessionId(sessionId)
|
|
}
|
|
|
|
const handleClickSend = (event: any) => {
|
|
if (event.ctrlKey) {
|
|
return;
|
|
}
|
|
if (responding.value) return;
|
|
if (!user_input.value) return
|
|
if (!currentSessionId.value) {
|
|
toast.add({
|
|
title: '发送失败',
|
|
description: '请先选择一个会话',
|
|
color: 'red',
|
|
icon: 'i-tabler-circle-x',
|
|
})
|
|
return;
|
|
}
|
|
// 插入用户消息
|
|
insetMessage({
|
|
id: generateUUID(),
|
|
role: 'user',
|
|
content: user_input.value,
|
|
create_at: dayjs().unix(),
|
|
})
|
|
user_input.value = ''
|
|
// 进入响应中状态
|
|
responding.value = true
|
|
// 插入空助手消息(加载状态)
|
|
const assistantReplyId = insetMessage({
|
|
id: generateUUID(),
|
|
role: 'assistant',
|
|
content: '',
|
|
create_at: dayjs().unix(),
|
|
})
|
|
// mock
|
|
setTimeout(() => {
|
|
modifyMessageContent(assistantReplyId, '草,走,忽略!ጿ ኈ ቼ ዽ ጿ')
|
|
responding.value = false
|
|
}, 1000)
|
|
}
|
|
|
|
const scrollToMessageListBottom = () => {
|
|
nextTick(() => {
|
|
messagesWrapperRef.value?.scrollTo({
|
|
top: messagesWrapperRef.value.scrollHeight,
|
|
behavior: 'smooth',
|
|
})
|
|
})
|
|
}
|
|
|
|
const insetMessage = (message: ChatMessage): ChatMessageId => {
|
|
setChatSessions(chatSessions.value.map(s => s.id === currentSessionId.value ? {
|
|
...s,
|
|
messages: [
|
|
...s.messages,
|
|
message
|
|
]
|
|
} : s))
|
|
scrollToMessageListBottom()
|
|
return message.id
|
|
}
|
|
|
|
const modifyMessageContent = (messageId: ChatMessageId, content: string) => {
|
|
setChatSessions(chatSessions.value.map(s => s.id === currentSessionId.value ? {
|
|
...s,
|
|
messages: s.messages.map(m => m.id === messageId ? {
|
|
...m,
|
|
content,
|
|
} : m)
|
|
} : s))
|
|
scrollToMessageListBottom()
|
|
}
|
|
|
|
onMounted(() => {
|
|
// 切换到第一个会话, 没有会话会自动创建
|
|
selectCurrentSessionId()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="w-full flex relative">
|
|
<div class="h-[calc(100vh-4rem)] bg-neutral-100 dark:bg-neutral-900 p-4 flex flex-col w-[300px]
|
|
shadow-sidebar border-r border-transparent dark:border-neutral-700">
|
|
<div class="flex-1 flex flex-col overflow-auto overflow-x-hidden">
|
|
<!-- list -->
|
|
<div class="flex flex-col gap-3">
|
|
<!-- ClientOnly avoids hydrate exception -->
|
|
<ClientOnly>
|
|
<ChatItem
|
|
v-for="(session, i) in chatSessions"
|
|
:chat-session="session" :key="i"
|
|
:active="session.id === currentSessionId"
|
|
@click="selectCurrentSessionId(session.id)"
|
|
@remove="() => {
|
|
chatSessions.splice(chatSessions.findIndex(s => s.id === session.id), 1)
|
|
session.id === currentSessionId && selectCurrentSessionId()
|
|
}"
|
|
/>
|
|
</ClientOnly>
|
|
</div>
|
|
</div>
|
|
<div class="pt-4 flex justify-between items-center">
|
|
<div></div>
|
|
<div>
|
|
<UButton
|
|
color="white"
|
|
variant="solid"
|
|
icon="i-tabler-message-circle-plus"
|
|
@click="handleClickCreateSession"
|
|
>
|
|
新建聊天
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="h-[calc(100vh-4rem)] bg-white dark:bg-neutral-900 flex-1 flex flex-col">
|
|
<div class="w-full p-4 bg-neutral-50 dark:bg-neutral-800/50 border-b dark:border-neutral-700/50">
|
|
{{ getSessionCopyById(currentSessionId!)?.subject || '新对话' }}
|
|
</div>
|
|
<div ref="messagesWrapperRef" class="flex-1 flex flex-col overflow-auto overflow-x-hidden">
|
|
<div class="flex flex-col gap-8 px-4 py-8">
|
|
<TransitionGroup name="message">
|
|
<Message
|
|
v-for="message in getSessionCopyById(currentSessionId!)?.messages || []"
|
|
:message="message"
|
|
:key="message.id"
|
|
/>
|
|
</TransitionGroup>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="w-full p-4 pt-2 flex flex-col gap-2 bg-neutral-50 dark:bg-neutral-800/50 border-t dark:border-neutral-700/50">
|
|
<div>action bar</div>
|
|
<div class="relative">
|
|
<UTextarea
|
|
v-model="user_input"
|
|
size="lg"
|
|
autoresize
|
|
:rows="5"
|
|
:maxrows="12"
|
|
class="font-sans"
|
|
placeholder="Enter 发送, Ctrl + Enter 换行"
|
|
@keydown.ctrl.enter="user_input += '\n'"
|
|
@keydown.enter.prevent="handleClickSend"
|
|
/>
|
|
<UButton
|
|
color="black"
|
|
variant="solid"
|
|
icon="i-tabler-send-2"
|
|
class="absolute bottom-2.5 right-3"
|
|
@click.stop="handleClickSend"
|
|
>
|
|
发送
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!--suppress CssUnusedSymbol -->
|
|
<style scoped>
|
|
.message-enter-active {
|
|
@apply transition duration-300;
|
|
}
|
|
|
|
.message-enter-from {
|
|
@apply translate-y-4 opacity-0;
|
|
}
|
|
|
|
.message-leave-to {
|
|
@apply -translate-y-4 opacity-0;
|
|
}
|
|
</style> |