Commit e6941309 by HoMeTown

feat: 历史聊天记录

parent bd13d886
...@@ -19,7 +19,6 @@ export const item = { ...@@ -19,7 +19,6 @@ export const item = {
} }
export const variants = { export const variants = {
hidden: { hidden: {
opacity: 0,
scale: 0, scale: 0,
x: '-50%', // 起始位置向左偏移 x: '-50%', // 起始位置向左偏移
}, },
...@@ -39,7 +38,7 @@ export const variants = { ...@@ -39,7 +38,7 @@ export const variants = {
x: '-50%', x: '-50%',
transition: { transition: {
duration: 0.2, duration: 0.2,
ease: 'easeIn', ease: 'easeOut', // 改为 easeOut
}, },
}, },
} }
......
...@@ -2,13 +2,15 @@ import React from 'react' ...@@ -2,13 +2,15 @@ import React from 'react'
import styles from './Chat.module.less' import styles from './Chat.module.less'
import { ChatSlogan } from './components/ChatSlogan' import { ChatSlogan } from './components/ChatSlogan'
import { ChatContent } from './components/ChatContent' import { ChatContent } from './components/ChatContent'
import { ChatMaskBar } from './components/ChatMaskBar'
import { RECOMMEND_QUESTIONS_OTHER } from '@/config/recommendQuestion' import { RECOMMEND_QUESTIONS_OTHER } from '@/config/recommendQuestion'
import { ChatEditor } from '@/components/ChatEditor' import { ChatEditor } from '@/components/ChatEditor'
export const Chat: React.FC = () => { export const Chat: React.FC = () => {
return ( return (
<div className={styles.chatPage}> <div className={`${styles.chatPage} relative`}>
<ChatSlogan /> <ChatSlogan />
<ChatMaskBar />
<div className={styles.content}> <div className={styles.content}>
<ChatContent /> <ChatContent />
</div> </div>
......
import { useEffect } from 'react' import { useEffect } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { Spinner } from '@nextui-org/react' import { Spinner } from '@nextui-org/react'
import { ChatWelcome } from '../ChatWelcome' import { Virtuoso } from 'react-virtuoso'
import { motion } from 'framer-motion'
import { ChatItem } from '../ChatItem' import { ChatItem } from '../ChatItem'
import { useAppDispatch, useAppSelector } from '@/store/hook' import { useAppDispatch, useAppSelector } from '@/store/hook'
import { fetchChatRecords } from '@/store/chatSlice' import { fetchChatRecords } from '@/store/chatSlice'
import type { ChatRecord } from '@/types/chat'
export const ChatContent: React.FC = () => { export const ChatContent: React.FC = () => {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { records, isLoading, error } = useAppSelector(state => state.chat) const { records, isLoading, error } = useAppSelector(state => state.chat)
const allItems: ChatRecord[] = [{ type: 'system' }, ...records]
useEffect(() => { useEffect(() => {
if (id) { if (id) {
dispatch(fetchChatRecords(id)) dispatch(fetchChatRecords(id))
...@@ -18,7 +22,7 @@ export const ChatContent: React.FC = () => { ...@@ -18,7 +22,7 @@ export const ChatContent: React.FC = () => {
}, [id, dispatch]) }, [id, dispatch])
if (isLoading) if (isLoading)
return <div className="w-full flex justify-center"><Spinner /></div> return <div className="w-full h-full flex justify-center"><Spinner /></div>
if (error) { if (error) {
return ( return (
<div> <div>
...@@ -28,15 +32,26 @@ export const ChatContent: React.FC = () => { ...@@ -28,15 +32,26 @@ export const ChatContent: React.FC = () => {
) )
} }
return ( return (
<div className="max-w-[1000px] mx-auto box-border py-[32px] flex flex-col gap-[32px]"> <motion.div
<ChatWelcome /> initial={{ opacity: 0, y: -50 }}
{ animate={{ opacity: 1, y: 0 }}
records.map((record) => { transition={{
return ( duration: 0.4,
<ChatItem record={record} key={record.groupId} /> x: { type: 'spring', stiffness: 50 },
) scale: { type: 'spring', stiffness: 50 },
}) opacity: { duration: 0.7 },
} }}
</div> className="w-full h-full mx-auto"
>
<Virtuoso
style={{ height: '100%' }} // 设置一个固定高度或使用动态高度
data={allItems}
itemContent={(index, record) => (
<ChatItem record={record} key={record.originalData?.groupId} />
)}
initialTopMostItemIndex={records.length - 1} // 初始滚动到底部
followOutput="smooth" // 新消息时平滑滚动到底部
/>
</motion.div>
) )
} }
import { ChatWelcome } from '../ChatWelcome'
import { ChatItemBot } from './ChatItemBot' import { ChatItemBot } from './ChatItemBot'
import { ChatItemUser } from './ChatItemUser' import { ChatItemUser } from './ChatItemUser'
import type { ChatRecord } from '@/store/chatSlice' import type { ChatRecord } from '@/types/chat'
interface ChatItemProps { interface ChatItemProps {
record: ChatRecord record: ChatRecord
...@@ -8,9 +9,14 @@ interface ChatItemProps { ...@@ -8,9 +9,14 @@ interface ChatItemProps {
export const ChatItem: React.FC<ChatItemProps> = ({ record }) => { export const ChatItem: React.FC<ChatItemProps> = ({ record }) => {
return ( return (
<> <div className="chatItem max-w-[1000px] mx-auto">
<ChatItemUser record={record} /> {
<ChatItemBot record={record} /> record.type === 'system'
</> ? <ChatWelcome />
: record.type === 'question'
? <ChatItemUser record={record} />
: record.type === 'answer' ? <ChatItemBot record={record} /> : null
}
</div>
) )
} }
...@@ -5,7 +5,7 @@ import rehypeRaw from 'rehype-raw' ...@@ -5,7 +5,7 @@ import rehypeRaw from 'rehype-raw'
import rehypeSanitize from 'rehype-sanitize' import rehypeSanitize from 'rehype-sanitize'
import { formatMarkdown } from './markdownFormatter' import { formatMarkdown } from './markdownFormatter'
import AvatarBot from '@/assets/avatarBot.png' import AvatarBot from '@/assets/avatarBot.png'
import type { ChatRecord } from '@/store/chatSlice' import type { ChatRecord } from '@/types/chat'
interface ChatItemBotProps { interface ChatItemBotProps {
record: ChatRecord record: ChatRecord
...@@ -24,11 +24,13 @@ export const ChatItemBot: React.FC<ChatItemBotProps> = ({ record }) => { ...@@ -24,11 +24,13 @@ export const ChatItemBot: React.FC<ChatItemBotProps> = ({ record }) => {
rehypePlugins={[rehypeRaw, rehypeSanitize]} rehypePlugins={[rehypeRaw, rehypeSanitize]}
className="markdown-content" className="markdown-content"
> >
{formatMarkdown(record.answerList[0].answer)} {formatMarkdown(record.originalData?.answerList[0].answer || '')}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
</motion.div> </motion.div>
<div className="w-[130px]"></div>
</div> </div>
<div className="h-[32px] w-full"></div>
</div> </div>
) )
} }
import { Avatar } from '@nextui-org/react' import { Avatar } from '@nextui-org/react'
import type { ChatRecord } from '@/store/chatSlice'
import AvatarUser from '@/assets/avatarUser.png' import AvatarUser from '@/assets/avatarUser.png'
import type { ChatRecord } from '@/types/chat'
interface ChatItemUserProps { interface ChatItemUserProps {
record: ChatRecord record: ChatRecord
...@@ -8,9 +8,12 @@ interface ChatItemUserProps { ...@@ -8,9 +8,12 @@ interface ChatItemUserProps {
export const ChatItemUser: React.FC<ChatItemUserProps> = ({ record }) => { export const ChatItemUser: React.FC<ChatItemUserProps> = ({ record }) => {
return ( return (
<div className="chatItemUser flex justify-end"> <div className="chatItemUser">
<div className="mr-[20px] bg-[#BFE9FE] rounded-[20px] box-border px-[24px] py-[20px] text-[#27353C]">{record.question}</div> <div className="flex justify-end">
<div className="mr-[20px] bg-[#BFE9FE] rounded-[20px] box-border px-[24px] py-[20px] text-[#27353C]">{record.originalData?.question}</div>
<Avatar className="flex-shrink-0" src={AvatarUser} /> <Avatar className="flex-shrink-0" src={AvatarUser} />
</div> </div>
<div className="h-[32px] w-full"></div>
</div>
) )
} }
export const ChatMaskBar: React.FC = () => {
return (
<div className="absolute w-full h-[32px] z-[100] top-[110px] bg-gradient-to-b from-[hsl(var(--sdream-background))] to-transparent"></div>
)
}
...@@ -5,6 +5,7 @@ import AvatarBot from '@/assets/avatarBot.png' ...@@ -5,6 +5,7 @@ import AvatarBot from '@/assets/avatarBot.png'
export const ChatWelcome: React.FC = () => { export const ChatWelcome: React.FC = () => {
return ( return (
<div className="chatWelcomeContainer w-full"> <div className="chatWelcomeContainer w-full">
<div className="h-[32px] w-full"></div>
<div className="flex"> <div className="flex">
<Avatar className="flex-shrink-0" src={AvatarBot} /> <Avatar className="flex-shrink-0" src={AvatarBot} />
<motion.div <motion.div
...@@ -16,6 +17,7 @@ export const ChatWelcome: React.FC = () => { ...@@ -16,6 +17,7 @@ export const ChatWelcome: React.FC = () => {
</div> </div>
</motion.div> </motion.div>
</div> </div>
<div className="h-[32px] w-full"></div>
</div> </div>
) )
} }
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { fetchUserQaRecordPage } from '@/api/conversation' import { fetchUserQaRecordPage } from '@/api/conversation'
import type { ChatRecord, ChatState, OriginalRecord } from '@/types/chat'
interface Attachment {
type: string
name: string
description: string
}
interface Answer {
answer: string
collectionFlag: boolean
feedbackStatus: string
groupId: string
question: string
recordId: string
terminateFlag: boolean
toolName: string
attachmentList: Attachment[]
}
export interface ChatRecord {
groupId: string
question: string
answerList: Answer[]
productCode: string
qaTime: string
}
interface ChatState {
records: ChatRecord[]
isLoading: boolean
error: string | null
}
const initialState: ChatState = { const initialState: ChatState = {
records: [], records: [],
...@@ -39,11 +8,30 @@ const initialState: ChatState = { ...@@ -39,11 +8,30 @@ const initialState: ChatState = {
error: null, error: null,
} }
function processApiResponse(data: OriginalRecord[]): ChatRecord[] {
const chatRecord: ChatRecord[] = []
data.forEach((record) => {
chatRecord.push({
type: 'question',
originalData: record,
})
if (record.answerList && record.answerList.length > 0) {
chatRecord.push({
type: 'answer',
originalData: record,
})
}
})
return chatRecord
}
export const fetchChatRecords = createAsyncThunk( export const fetchChatRecords = createAsyncThunk(
'chat/fetchRecords', 'chat/fetchRecords',
async (conversationId: string) => { async (conversationId: string) => {
const response = await fetchUserQaRecordPage(conversationId) const response = await fetchUserQaRecordPage(conversationId)
return response.data return processApiResponse(response.data)
}, },
) )
......
export interface Attachment {
type: string
name: string
description: string
}
export interface Answer {
answer: string
collectionFlag: boolean
feedbackStatus: string
groupId: string
question: string
recordId: string
terminateFlag: boolean
toolName: string
attachmentList: Attachment[]
}
export interface OriginalRecord {
groupId: string
question: string
answerList: Answer[]
productCode: string
qaTime: string
}
export interface ChatRecord {
type: 'question' | 'answer' | 'system'
originalData?: OriginalRecord
}
export interface ChatState {
records: ChatRecord[]
isLoading: boolean
error: string | null
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment