Commit 6383ef2c by HoMeTown

feat: 收藏列表

parent 1949d403
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26px" height="26px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>矩形@2x</title>
<defs>
<rect id="path-1" x="0" y="0" width="22" height="22"></rect>
</defs>
<g id="晓得---PC端页面-草稿" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.800637381">
<g id="晓得-PC端---收藏" transform="translate(-1412.000000, -550.000000)">
<g id="1" transform="translate(498.000000, 144.000000)">
<g id="操作按钮" transform="translate(914.000000, 376.000000)">
<g id="删除" transform="translate(0.000000, 30.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="矩形"></g>
<path d="M16.7272727,6.8 L16.2273329,15.5989407 C16.1507677,16.9464888 15.0356987,18 13.6859772,18 L8.3140228,18 C6.96430127,18 5.84923232,16.9464888 5.77266708,15.5989407 L5.27272727,6.8 L16.7272727,6.8 Z M8.13636364,9.425 C7.7849097,9.425 7.5,9.7099097 7.5,10.0613636 L7.5,10.0613636 L7.5,15.1522727 C7.5,15.5037267 7.7849097,15.7886364 8.13636364,15.7886364 C8.48781757,15.7886364 8.77272727,15.5037267 8.77272727,15.1522727 L8.77272727,15.1522727 L8.77272727,10.0613636 C8.77272727,9.7099097 8.48781757,9.425 8.13636364,9.425 Z M11,9.425 C10.6485461,9.425 10.3636364,9.7099097 10.3636364,10.0613636 L10.3636364,10.0613636 L10.3636364,15.1522727 C10.3636364,15.5037267 10.6485461,15.7886364 11,15.7886364 C11.3514539,15.7886364 11.6363636,15.5037267 11.6363636,15.1522727 L11.6363636,15.1522727 L11.6363636,10.0613636 C11.6363636,9.7099097 11.3514539,9.425 11,9.425 Z M13.8636364,9.425 C13.5121824,9.425 13.2272727,9.7099097 13.2272727,10.0613636 L13.2272727,10.0613636 L13.2272727,15.1522727 C13.2272727,15.5037267 13.5121824,15.7886364 13.8636364,15.7886364 C14.2150903,15.7886364 14.5,15.5037267 14.5,15.1522727 L14.5,15.1522727 L14.5,10.0613636 C14.5,9.7099097 14.2150903,9.425 13.8636364,9.425 Z M12.2363707,4 L11.6363636,4.63636364 L16.5363636,4.63636364 C17.297158,4.63636364 17.9223772,5.21683019 17.9932999,5.95904205 L18,6.1 L4,6.1 C4,5.29165596 4.65529232,4.63636364 5.46363636,4.63636364 L10.3636364,4.63636364 L9.72727273,4 L12.2363707,4 Z" id="形状结合" fill="#D1D5DA" mask="url(#mask-2)"></path>
</g>
</g>
</g>
</g>
</g>
</svg>
...@@ -3,8 +3,10 @@ import { AnimatePresence, motion } from 'framer-motion' ...@@ -3,8 +3,10 @@ import { AnimatePresence, motion } from 'framer-motion'
import { Button } from '@nextui-org/react' import { Button } from '@nextui-org/react'
import { useToggle } from 'ahooks' import { useToggle } from 'ahooks'
import { LoginModal } from '../LoginModal' import { LoginModal } from '../LoginModal'
import type { RootState } from '@/store'
import SendIcon from '@/assets/svg/send.svg?react' import SendIcon from '@/assets/svg/send.svg?react'
import { type WithAuthProps, withAuth } from '@/auth/withAuth' import { type WithAuthProps, withAuth } from '@/auth/withAuth'
import { useAppSelector } from '@/store/hook'
interface ChatEditorProps { interface ChatEditorProps {
onChange?: (value: string) => void onChange?: (value: string) => void
...@@ -19,6 +21,7 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth, ...@@ -19,6 +21,7 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
const [currentPlaceholder, setCurrentPlaceholder] = useState(0) const [currentPlaceholder, setCurrentPlaceholder] = useState(0)
const intervalRef = useRef<NodeJS.Timeout | null>(null) const intervalRef = useRef<NodeJS.Timeout | null>(null)
const [isOpenLoginModal, isOpenLoginModalActions] = useToggle() const [isOpenLoginModal, isOpenLoginModalActions] = useToggle()
const isAsking = useAppSelector((state: RootState) => state.chat.isAsking)
const startAnimation = () => { const startAnimation = () => {
intervalRef.current = setInterval(() => { intervalRef.current = setInterval(() => {
...@@ -44,6 +47,8 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth, ...@@ -44,6 +47,8 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
} }
const handleSubmit = () => { const handleSubmit = () => {
if (isAsking)
return
if (checkAuth()) { if (checkAuth()) {
if (content.trim()) { if (content.trim()) {
onSubmit?.(content.trim()) onSubmit?.(content.trim())
...@@ -103,7 +108,7 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth, ...@@ -103,7 +108,7 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
resize: 'none', resize: 'none',
}} }}
/> />
<Button onClick={handleSubmit} radius="full" isDisabled={!content} isIconOnly color="primary"> <Button onClick={handleSubmit} radius="full" isDisabled={!content || isAsking} isIconOnly color="primary">
<SendIcon /> <SendIcon />
</Button> </Button>
......
...@@ -16,9 +16,10 @@ import type { ChatRecord } from '@/types/chat' ...@@ -16,9 +16,10 @@ import type { ChatRecord } from '@/types/chat'
import { fetchUserQaRecordPage } from '@/api/conversation' import { fetchUserQaRecordPage } from '@/api/conversation'
import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat' import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat'
import { clearShouldSendQuestion, fetchConversations } from '@/store/conversationSlice' import { clearShouldSendQuestion, fetchConversations } from '@/store/conversationSlice'
import type { RootState } from '@/store' // 假设你的 store 文件导出了 RootState 类型 import type { RootState } from '@/store'
import { useAppDispatch, useAppSelector } from '@/store/hook' import { useAppDispatch, useAppSelector } from '@/store/hook'
import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react' import ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react'
import { setIsAsking } from '@/store/chatSlice'
export const Chat: React.FC = () => { export const Chat: React.FC = () => {
let ignore = false let ignore = false
...@@ -32,6 +33,7 @@ export const Chat: React.FC = () => { ...@@ -32,6 +33,7 @@ export const Chat: React.FC = () => {
const position = useScroll(scrollableRef) const position = useScroll(scrollableRef)
const handleSubmitQuestion = async (question: string) => { const handleSubmitQuestion = async (question: string) => {
dispatch(setIsAsking(true))
// 添加用户提问的问题 // 添加用户提问的问题
setAllItems(prevItems => [ setAllItems(prevItems => [
...prevItems, ...prevItems,
......
...@@ -6,6 +6,8 @@ import { ChatAnswerParser } from './ChatAnswerParser' ...@@ -6,6 +6,8 @@ import { ChatAnswerParser } from './ChatAnswerParser'
import { ChatAnswerRecommend } from './ChatAnswerRecommend' import { ChatAnswerRecommend } from './ChatAnswerRecommend'
import type { Answer, ChatRecord } from '@/types/chat' import type { Answer, ChatRecord } from '@/types/chat'
import AvatarBot from '@/assets/avatarBot.png' import AvatarBot from '@/assets/avatarBot.png'
import { useAppDispatch } from '@/store/hook'
import { setIsAsking } from '@/store/chatSlice'
interface ChatAnswerBoxProps { interface ChatAnswerBoxProps {
record: ChatRecord record: ChatRecord
...@@ -18,9 +20,12 @@ interface ChatAnswerBoxProps { ...@@ -18,9 +20,12 @@ interface ChatAnswerBoxProps {
export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex, isLastAnswer, onSubmitQuestion }) => { export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex, isLastAnswer, onSubmitQuestion }) => {
const [isShowRecommend, setIsShowRecommend] = useState(false) const [isShowRecommend, setIsShowRecommend] = useState(false)
const [recommendUseAnswer, setRecommendUseAnswer] = useState<Answer>() const [recommendUseAnswer, setRecommendUseAnswer] = useState<Answer>()
const dispatch = useAppDispatch()
const handleComplate = (answer: Answer) => { const handleComplate = (answer: Answer) => {
setIsShowRecommend(true) setIsShowRecommend(true)
setRecommendUseAnswer(answer) setRecommendUseAnswer(answer)
dispatch(setIsAsking(false))
} }
return ( return (
<div> <div>
...@@ -45,7 +50,10 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex, ...@@ -45,7 +50,10 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex,
</motion.div> </motion.div>
<div className="w-[130px]"></div> <div className="w-[130px]"></div>
</div> </div>
{isLastAnswer && isShowRecommend && recommendUseAnswer && <ChatAnswerRecommend onSubmitQuestion={onSubmitQuestion} answer={recommendUseAnswer} />} {isLastAnswer
&& isShowRecommend
&& recommendUseAnswer
&& <ChatAnswerRecommend onSubmitQuestion={onSubmitQuestion} answer={recommendUseAnswer} />}
<div className="h-[32px] w-full"></div> <div className="h-[32px] w-full"></div>
</div> </div>
) )
......
...@@ -44,7 +44,11 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ onComplate, ...@@ -44,7 +44,11 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ onComplate,
> >
{displayedText} {displayedText}
</ReactMarkdown> </ReactMarkdown>
{!isTyping && answer.attachmentList && answer.attachmentList?.length !== 0 && <ChatAnswerAttachment answer={answer} />} {!isTyping
&& answer.attachmentList
&& answer.attachmentList?.length !== 0
&& <ChatAnswerAttachment answer={answer} />}
{!isTyping && <ChatAnswerOperate answer={answer} />} {!isTyping && <ChatAnswerOperate answer={answer} />}
</div> </div>
) )
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
justify-content: flex-start; justify-content: flex-start;
overflow: hidden; overflow: hidden;
} }
.scrollable { .collectScrollable {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
display: flex; display: flex;
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
collectPage: string collectPage: string
collectScrollable: string
content: string content: string
inter: string inter: string
scrollView: string scrollView: string
scrollable: string
} }
declare const cssExports: CssExports declare const cssExports: CssExports
export default cssExports export default cssExports
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Spinner } from '@nextui-org/react' import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, Spinner, Tooltip } from '@nextui-org/react'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import rehypeRaw from 'rehype-raw' import rehypeRaw from 'rehype-raw'
import rehypeSanitize from 'rehype-sanitize' import rehypeSanitize from 'rehype-sanitize'
...@@ -11,63 +11,142 @@ import { formatMarkdown } from '../Chat/components/ChatItem/markdownFormatter' ...@@ -11,63 +11,142 @@ import { formatMarkdown } from '../Chat/components/ChatItem/markdownFormatter'
import { ChatAnswerAttachment } from '../Chat/components/ChatItem/ChatAnswerAttchment' import { ChatAnswerAttachment } from '../Chat/components/ChatItem/ChatAnswerAttchment'
import styles from './Collect.module.less' import styles from './Collect.module.less'
import { fetchQueryCollectionList } from '@/api/collect' import { fetchQueryCollectionList } from '@/api/collect'
import CopyIcon from '@/assets/svg/copy.svg?react'
import DeleteIcon from '@/assets/svg/delete.svg?react'
import type { Answer } from '@/types/chat'
import useToast from '@/hooks/useToast'
import { fetchDelCollection } from '@/api/chat'
export const Collect: React.FC = () => { export const Collect: React.FC = () => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const [collectList, setCollectList] = useState<any>([]) const [collectList, setCollectList] = useState<any>([])
const [curCollectId, setCollectId] = useState('')
const [pageNum, setPageNum] = useState(1)
const [pageSize] = useState(5)
const [total, setTotal] = useState(0)
const showToast = useToast()
const getCollectList = async () => { const getCollectList = async () => {
setIsLoading(true) setIsLoading(true)
const params = { const params = {
pageNum: 1, pageNum,
pageSize: 5, pageSize,
} }
const res = await fetchQueryCollectionList(params) const res = await fetchQueryCollectionList(params)
setCollectList(res.data.records) setCollectList([...collectList, ...res.data.records])
setTotal(res.data.total)
setIsLoading(false) setIsLoading(false)
} }
const handleCopy = async (item: Answer) => {
if (!navigator.clipboard) {
showToast('您的浏览器不支持复制', 'error')
return
}
await navigator.clipboard.writeText(item.answer)
showToast('复制成功!快去分享吧!', 'success')
}
const handleDelete = (item: any) => {
setIsOpen(true)
setCollectId(item.collectionId)
}
const handleSureDel = async () => {
const res = await fetchDelCollection([curCollectId])
if (res.ok) {
setIsOpen(false)
getCollectList()
}
}
const handleLoadMore = () => {
setPageNum(pageNum + 1)
}
useEffect(() => { useEffect(() => {
getCollectList() getCollectList()
}, []) }, [pageNum])
return ( return (
<div className={styles.scrollView}> <div className={styles.scrollView}>
<div className={`${styles.collectPage} relative`}> <div className={`${styles.collectPage} relative`}>
<ChatSlogan /> <ChatSlogan />
<ChatMaskBar /> <ChatMaskBar />
<div className="content h-full overflow-y-auto"> <div className="content h-full overflow-y-auto">
{isLoading && <div className="w-full h-full flex justify-center"><Spinner /></div>} <motion.div
{!isLoading && ( initial={{ opacity: 0, y: -10 }}
<motion.div animate={{ opacity: 1, y: 0 }}
initial={{ opacity: 0, y: -10 }} transition={{
animate={{ opacity: 1, y: 0 }} duration: 0.3,
transition={{ opacity: { duration: 0.1 },
duration: 0.3, }}
opacity: { duration: 0.1 }, className={`${styles.collectScrollable} scrollbar-hide`}
}} >
className={`${styles.scrollable} scrollbar-hide`} <div className={`${styles.inter} gap-[32px]`}>
> {
<div className={`${styles.inter} gap-[32px]`}> collectList.map((item: any, index: number) => (
{ <div className="w-full max-w-[1000px] mx-auto bg-white rounded-[20px] box-border px-[24px] py-[20px]" key={`${item.collectionId}_${index}`}>
collectList.map((item: any) => ( <ReactMarkdown
<div className="w-full max-w-[900px] mx-auto bg-white rounded-[20px] box-border px-[24px] py-[20px]" key={item.conversationId}> rehypePlugins={[rehypeRaw, rehypeSanitize]}
<ReactMarkdown remarkPlugins={[remarkGfm]}
className="markdown-content"
rehypePlugins={[rehypeRaw, rehypeSanitize]} >
remarkPlugins={[remarkGfm]} {formatMarkdown(item.answer || '')}
className="markdown-content" </ReactMarkdown>
> {item.attachmentList && item.attachmentList?.length !== 0 && <ChatAnswerAttachment answer={item} />}
{formatMarkdown(item.answer || '')} <div className="mt-[12px] flex gap-[4px] justify-end">
</ReactMarkdown> <Tooltip color="foreground" content="复制" className="capitalize">
{item.attachmentList && item.attachmentList?.length !== 0 && <ChatAnswerAttachment answer={item} />} <Button variant="light" isIconOnly aria-label="CopyIcon" onClick={() => handleCopy(item)}><CopyIcon /></Button>
</Tooltip>
<Tooltip color="foreground" content="删除" className="capitalize">
<Button variant="light" isIconOnly aria-label="DeleteIcon" onClick={() => handleDelete(item)}><DeleteIcon /></Button>
</Tooltip>
</div> </div>
)) </div>
} ))
</div> }
</motion.div> {isLoading && <div className="w-full flex justify-center"><Spinner /></div>}
)} {
!isLoading && collectList.length < total && (
<div className="w-full max-w-[1000px] mx-auto flex justify-center">
<Button onClick={handleLoadMore} color="primary" variant="light">
加载更多
</Button>
</div>
)
}
{
collectList.length === total && collectList.length !== 0 && (
<div className="w-full max-w-[1000px] mx-auto flex justify-center text-[#8D9795]">到底啦~</div>
)
}
</div>
</motion.div>
</div> </div>
</div> </div>
<Modal backdrop="blur" isOpen={isOpen} onClose={() => setIsOpen(false)}>
<ModalContent>
{onClose => (
<>
<ModalHeader className="flex flex-col gap-1">删除提示</ModalHeader>
<ModalBody className="text-[#27353C]">
确认删除当前收藏内容吗?删除后,此条收藏的内容将不可恢复。
</ModalBody>
<ModalFooter>
<Button onPress={onClose}>
再想想
</Button>
<Button color="primary" onPress={handleSureDel}>
确认
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</div> </div>
) )
} }
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
interface ChatSlice {
isAsking: boolean
}
const initialState: ChatSlice = {
isAsking: false,
}
const chatSlice = createSlice({
name: 'chatSlice',
initialState,
reducers: {
setIsAsking: (state, action: PayloadAction<boolean>) => {
state.isAsking = action.payload
},
},
})
export const { setIsAsking } = chatSlice.actions
export default chatSlice.reducer
import { configureStore } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit'
import conversationReducer from './conversationSlice' import conversationReducer from './conversationSlice'
import chatReducer from './chatSlice'
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
conversation: conversationReducer, conversation: conversationReducer,
chat: chatReducer,
}, },
}) })
// 为了在TypeScript中使用,我们导出这些类型 // 为了在TypeScript中使用,我们导出这些类型
......
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