Commit e2985b61 by HoMeTown

feat:修改chat区域的滚动方式

parent ceee81e4
...@@ -41,7 +41,6 @@ ...@@ -41,7 +41,6 @@
"dependencies": { "dependencies": {
"@nextui-org/react": "^2.4.6", "@nextui-org/react": "^2.4.6",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"@virtuoso.dev/message-list": "^1.8.3",
"ahooks": "^3.8.0", "ahooks": "^3.8.0",
"axios": "^1.7.3", "axios": "^1.7.3",
"clsx": "^2.1.1", "clsx": "^2.1.1",
...@@ -52,7 +51,6 @@ ...@@ -52,7 +51,6 @@
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"react-virtuoso": "^4.9.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
......
...@@ -4,6 +4,37 @@ ...@@ -4,6 +4,37 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.scrollView {
// display: flex;
// flex-direction: column;
// flex: 1 1;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.content { .content {
flex: 1 1; display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
justify-content: flex-start;
overflow: hidden;
}
.scrollable {
flex-direction: column-reverse;
align-items: center;
display: flex;
overflow-x: hidden;
overflow-y: scroll;
position: relative;
width: 100%;
}
.inter {
overflow-anchor: none;
display: flex;
flex-direction: column;
flex-shrink: 0;
height: fit-content;
width: 100%;
} }
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
interface CssExports { interface CssExports {
chatPage: string chatPage: string
content: string content: string
inter: string
scrollView: string
scrollable: string
} }
declare const cssExports: CssExports declare const cssExports: CssExports
export default cssExports export default cssExports
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } 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 { Virtuoso } from 'react-virtuoso'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import styles from './Chat.module.less' import styles from './Chat.module.less'
import { ChatSlogan } from './components/ChatSlogan' import { ChatSlogan } from './components/ChatSlogan'
...@@ -73,6 +72,7 @@ export const Chat: React.FC = () => { ...@@ -73,6 +72,7 @@ export const Chat: React.FC = () => {
answerList: [ answerList: [
{ {
...msg.content.data, ...msg.content.data,
isShow: false,
answer: (newItems[lastIndex].answerList[0].answer || '') + msg.content.data.answer, answer: (newItems[lastIndex].answerList[0].answer || '') + msg.content.data.answer,
}, },
], ],
...@@ -125,43 +125,39 @@ export const Chat: React.FC = () => { ...@@ -125,43 +125,39 @@ export const Chat: React.FC = () => {
}, [id]) }, [id])
return ( return (
<div className={`${styles.chatPage} relative`}> <div className={styles.scrollView}>
<ChatSlogan /> <div className={`${styles.chatPage} relative`}>
<ChatMaskBar /> <ChatSlogan />
<div className={styles.content}> <ChatMaskBar />
{isLoading && <div className="w-full h-full flex justify-center"><Spinner /></div>} <div className={styles.content}>
{!isLoading && ( {isLoading && <div className="w-full h-full flex justify-center"><Spinner /></div>}
<motion.div {!isLoading && (
initial={{ opacity: 0, y: -50 }} <motion.div
animate={{ opacity: 1, y: 0 }} initial={{ opacity: 0, y: -10 }}
transition={{ animate={{ opacity: 1, y: 0 }}
duration: 0.4, transition={{
x: { type: 'spring', stiffness: 50 }, duration: 0.3,
scale: { type: 'spring', stiffness: 50 }, opacity: { duration: 0.1 },
opacity: { duration: 0.7 }, }}
}} className={styles.scrollable}
className="w-full h-full mx-auto" >
> <div className={styles.inter}>
<Virtuoso {allItems.map((record, index) => (
style={{ height: '100%' }} // 设置一个固定高度或使用动态高度 <div className="w-full chatItem max-w-[1000px] mx-auto" key={index}>
data={allItems} {record.role === 'system' && <ChatWelcome />}
itemContent={(index, record) => ( {record.role === 'user' && <ChatItemUser record={record} />}
<div className="chatItem max-w-[1000px] mx-auto"> {record.role === 'ai' && <ChatAnswerBox isLastAnswer={index === allItems.length - 1} showIndex={0} record={record} index={index} />}
{record.role === 'system' && <ChatWelcome />} </div>
{record.role === 'user' && <ChatItemUser record={record} />} ))}
{record.role === 'ai' && <ChatAnswerBox isLastAnswer={index === allItems.length - 1} showIndex={0} record={record} index={index} />} </div>
</div> </motion.div>
)} )}
initialTopMostItemIndex={allItems.length - 1} // 初始滚动到底部 </div>
followOutput="smooth" // 新消息时平滑滚动到底部 <div className="box-border px-[0] mx-auto iptContainer w-full max-w-[1000px] flex-shrink-0 sm:px-0 pb-[18px]">
/> <ChatEditor onSubmit={handleSubmitQuestion} placeholders={RECOMMEND_QUESTIONS_OTHER} />
</motion.div> <div className="w-full text-center mt-[20px] text-[#3333334d] text-[12px]">
)} 内容由AI模型生成,其准确性和完整性无法保证,仅供参考
</div> </div>
<div className="box-border px-[0] mx-auto iptContainer w-full max-w-[1000px] flex-shrink-0 sm:px-0 pb-[18px]">
<ChatEditor onSubmit={handleSubmitQuestion} placeholders={RECOMMEND_QUESTIONS_OTHER} />
<div className="w-full text-center mt-[20px] text-[#3333334d] text-[12px]">
内容由AI模型生成,其准确性和完整性无法保证,仅供参考
</div> </div>
</div> </div>
</div> </div>
......
import { Avatar } from '@nextui-org/react' import { Avatar, Spinner } from '@nextui-org/react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import ReactMarkdown from 'react-markdown' import { ChatAnswerShower } from './ChatAnswerShower'
import rehypeRaw from 'rehype-raw' import { ChatAnswerParser } from './ChatAnswerParser'
import rehypeSanitize from 'rehype-sanitize'
import remarkGfm from 'remark-gfm'
import { formatMarkdown } from './markdownFormatter'
import type { ChatRecord } from '@/types/chat' import type { ChatRecord } from '@/types/chat'
import AvatarBot from '@/assets/avatarBot.png' import AvatarBot from '@/assets/avatarBot.png'
...@@ -29,17 +26,12 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex ...@@ -29,17 +26,12 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex
> >
{item.answer && ( {item.answer && (
<div className="content"> <div className="content">
<ReactMarkdown {item.isShow && <ChatAnswerShower answer={item} />}
rehypePlugins={[rehypeRaw, rehypeSanitize]} {!item.isShow && <ChatAnswerParser answer={item} />}
remarkPlugins={[remarkGfm]}
className="markdown-content"
>
{formatMarkdown(item.answer || '')}
</ReactMarkdown>
</div> </div>
)} )}
{!item.answer && ( {!item.answer && (
<span>loading</span> <Spinner size="sm" />
)} )}
</motion.div> </motion.div>
<div className="w-[130px]"></div> <div className="w-[130px]"></div>
......
import React, { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import rehypeSanitize from 'rehype-sanitize'
import remarkGfm from 'remark-gfm'
import type { Answer } from '@/types/chat'
interface ChatAnswerParserProps {
answer: Answer
}
export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ answer }) => {
const [displayedText, setDisplayedText] = useState('')
const [currentIndex, setCurrentIndex] = useState(0)
useEffect(() => {
if (currentIndex < answer.answer.length) {
const timer = setTimeout(() => {
setDisplayedText(answer.answer.slice(0, currentIndex + 1))
setCurrentIndex(prevIndex => prevIndex + 1)
}, 30) // 调整此值以改变打字速度
return () => clearTimeout(timer)
}
}, [answer, currentIndex])
return (
<div className="answerParser">
<ReactMarkdown
rehypePlugins={[rehypeRaw, rehypeSanitize]}
remarkPlugins={[remarkGfm]}
className="markdown-content"
>
{displayedText}
</ReactMarkdown>
</div>
)
}
import ReactMarkdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import rehypeSanitize from 'rehype-sanitize'
import remarkGfm from 'remark-gfm'
import { formatMarkdown } from './markdownFormatter'
import type { Answer } from '@/types/chat'
interface ChatAnswerShowerProps {
answer: Answer
}
export const ChatAnswerShower: React.FC<ChatAnswerShowerProps> = ({ answer }) => {
return (
<div className="answerShower">
<ReactMarkdown
rehypePlugins={[rehypeRaw, rehypeSanitize]}
remarkPlugins={[remarkGfm]}
className="markdown-content"
>
{formatMarkdown(answer.answer || '')}
</ReactMarkdown>
</div>
)
}
import React, { useEffect, useState } from 'react'
interface ChatAnswerTypeItProps {
content: string
typingSpeed?: number
}
const ChatAnswerTypeIt: React.FC<ChatAnswerTypeItProps> = ({ content, typingSpeed = 50 }) => {
const [displayedText, setDisplayedText] = useState('')
const [currentIndex, setCurrentIndex] = useState(0)
useEffect(() => {
if (currentIndex < content.length) {
const timer = setTimeout(() => {
setDisplayedText(prev => prev + content[currentIndex])
setCurrentIndex(prev => prev + 1)
}, typingSpeed)
return () => clearTimeout(timer)
}
}, [content, currentIndex, typingSpeed])
return <span>{displayedText}</span>
}
export default ChatAnswerTypeIt
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