Commit e6941309 by HoMeTown

feat: 历史聊天记录

parent bd13d886
......@@ -19,7 +19,6 @@ export const item = {
}
export const variants = {
hidden: {
opacity: 0,
scale: 0,
x: '-50%', // 起始位置向左偏移
},
......@@ -39,7 +38,7 @@ export const variants = {
x: '-50%',
transition: {
duration: 0.2,
ease: 'easeIn',
ease: 'easeOut', // 改为 easeOut
},
},
}
......
......@@ -2,13 +2,15 @@ import React from 'react'
import styles from './Chat.module.less'
import { ChatSlogan } from './components/ChatSlogan'
import { ChatContent } from './components/ChatContent'
import { ChatMaskBar } from './components/ChatMaskBar'
import { RECOMMEND_QUESTIONS_OTHER } from '@/config/recommendQuestion'
import { ChatEditor } from '@/components/ChatEditor'
export const Chat: React.FC = () => {
return (
<div className={styles.chatPage}>
<div className={`${styles.chatPage} relative`}>
<ChatSlogan />
<ChatMaskBar />
<div className={styles.content}>
<ChatContent />
</div>
......
import { useEffect } from 'react'
import { useParams } from 'react-router-dom'
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 { useAppDispatch, useAppSelector } from '@/store/hook'
import { fetchChatRecords } from '@/store/chatSlice'
import type { ChatRecord } from '@/types/chat'
export const ChatContent: React.FC = () => {
const { id } = useParams<{ id: string }>()
const dispatch = useAppDispatch()
const { records, isLoading, error } = useAppSelector(state => state.chat)
const allItems: ChatRecord[] = [{ type: 'system' }, ...records]
useEffect(() => {
if (id) {
dispatch(fetchChatRecords(id))
......@@ -18,7 +22,7 @@ export const ChatContent: React.FC = () => {
}, [id, dispatch])
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) {
return (
<div>
......@@ -28,15 +32,26 @@ export const ChatContent: React.FC = () => {
)
}
return (
<div className="max-w-[1000px] mx-auto box-border py-[32px] flex flex-col gap-[32px]">
<ChatWelcome />
{
records.map((record) => {
return (
<ChatItem record={record} key={record.groupId} />
)
})
}
</div>
<motion.div
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.4,
x: { type: 'spring', stiffness: 50 },
scale: { type: 'spring', stiffness: 50 },
opacity: { duration: 0.7 },
}}
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 { ChatItemUser } from './ChatItemUser'
import type { ChatRecord } from '@/store/chatSlice'
import type { ChatRecord } from '@/types/chat'
interface ChatItemProps {
record: ChatRecord
......@@ -8,9 +9,14 @@ interface ChatItemProps {
export const ChatItem: React.FC<ChatItemProps> = ({ record }) => {
return (
<>
<ChatItemUser record={record} />
<ChatItemBot record={record} />
</>
<div className="chatItem max-w-[1000px] mx-auto">
{
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'
import rehypeSanitize from 'rehype-sanitize'
import { formatMarkdown } from './markdownFormatter'
import AvatarBot from '@/assets/avatarBot.png'
import type { ChatRecord } from '@/store/chatSlice'
import type { ChatRecord } from '@/types/chat'
interface ChatItemBotProps {
record: ChatRecord
......@@ -24,11 +24,13 @@ export const ChatItemBot: React.FC<ChatItemBotProps> = ({ record }) => {
rehypePlugins={[rehypeRaw, rehypeSanitize]}
className="markdown-content"
>
{formatMarkdown(record.answerList[0].answer)}
{formatMarkdown(record.originalData?.answerList[0].answer || '')}
</ReactMarkdown>
</div>
</motion.div>
<div className="w-[130px]"></div>
</div>
<div className="h-[32px] w-full"></div>
</div>
)
}
import { Avatar } from '@nextui-org/react'
import type { ChatRecord } from '@/store/chatSlice'
import AvatarUser from '@/assets/avatarUser.png'
import type { ChatRecord } from '@/types/chat'
interface ChatItemUserProps {
record: ChatRecord
......@@ -8,9 +8,12 @@ interface ChatItemUserProps {
export const ChatItemUser: React.FC<ChatItemUserProps> = ({ record }) => {
return (
<div className="chatItemUser flex justify-end">
<div className="mr-[20px] bg-[#BFE9FE] rounded-[20px] box-border px-[24px] py-[20px] text-[#27353C]">{record.question}</div>
<Avatar className="flex-shrink-0" src={AvatarUser} />
<div className="chatItemUser">
<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} />
</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'
export const ChatWelcome: React.FC = () => {
return (
<div className="chatWelcomeContainer w-full">
<div className="h-[32px] w-full"></div>
<div className="flex">
<Avatar className="flex-shrink-0" src={AvatarBot} />
<motion.div
......@@ -16,6 +17,7 @@ export const ChatWelcome: React.FC = () => {
</div>
</motion.div>
</div>
<div className="h-[32px] w-full"></div>
</div>
)
}
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { fetchUserQaRecordPage } from '@/api/conversation'
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
}
import type { ChatRecord, ChatState, OriginalRecord } from '@/types/chat'
const initialState: ChatState = {
records: [],
......@@ -39,11 +8,30 @@ const initialState: ChatState = {
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(
'chat/fetchRecords',
async (conversationId: string) => {
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