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'
import { Button } from '@nextui-org/react'
import { useToggle } from 'ahooks'
import { LoginModal } from '../LoginModal'
import type { RootState } from '@/store'
import SendIcon from '@/assets/svg/send.svg?react'
import { type WithAuthProps, withAuth } from '@/auth/withAuth'
import { useAppSelector } from '@/store/hook'
interface ChatEditorProps {
onChange?: (value: string) => void
......@@ -19,6 +21,7 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
const [currentPlaceholder, setCurrentPlaceholder] = useState(0)
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const [isOpenLoginModal, isOpenLoginModalActions] = useToggle()
const isAsking = useAppSelector((state: RootState) => state.chat.isAsking)
const startAnimation = () => {
intervalRef.current = setInterval(() => {
......@@ -44,6 +47,8 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
}
const handleSubmit = () => {
if (isAsking)
return
if (checkAuth()) {
if (content.trim()) {
onSubmit?.(content.trim())
......@@ -103,7 +108,7 @@ const ChatEditorBase: React.FC<ChatEditorProps & WithAuthProps> = ({ checkAuth,
resize: 'none',
}}
/>
<Button onClick={handleSubmit} radius="full" isDisabled={!content} isIconOnly color="primary">
<Button onClick={handleSubmit} radius="full" isDisabled={!content || isAsking} isIconOnly color="primary">
<SendIcon />
</Button>
......
......@@ -16,9 +16,10 @@ import type { ChatRecord } from '@/types/chat'
import { fetchUserQaRecordPage } from '@/api/conversation'
import { fetchCheckTokenApi, fetchStreamResponse } from '@/api/chat'
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 ScrollBtoIcon from '@/assets/svg/scrollBto.svg?react'
import { setIsAsking } from '@/store/chatSlice'
export const Chat: React.FC = () => {
let ignore = false
......@@ -32,6 +33,7 @@ export const Chat: React.FC = () => {
const position = useScroll(scrollableRef)
const handleSubmitQuestion = async (question: string) => {
dispatch(setIsAsking(true))
// 添加用户提问的问题
setAllItems(prevItems => [
...prevItems,
......
......@@ -6,6 +6,8 @@ import { ChatAnswerParser } from './ChatAnswerParser'
import { ChatAnswerRecommend } from './ChatAnswerRecommend'
import type { Answer, ChatRecord } from '@/types/chat'
import AvatarBot from '@/assets/avatarBot.png'
import { useAppDispatch } from '@/store/hook'
import { setIsAsking } from '@/store/chatSlice'
interface ChatAnswerBoxProps {
record: ChatRecord
......@@ -18,9 +20,12 @@ interface ChatAnswerBoxProps {
export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex, isLastAnswer, onSubmitQuestion }) => {
const [isShowRecommend, setIsShowRecommend] = useState(false)
const [recommendUseAnswer, setRecommendUseAnswer] = useState<Answer>()
const dispatch = useAppDispatch()
const handleComplate = (answer: Answer) => {
setIsShowRecommend(true)
setRecommendUseAnswer(answer)
dispatch(setIsAsking(false))
}
return (
<div>
......@@ -45,7 +50,10 @@ export const ChatAnswerBox: React.FC<ChatAnswerBoxProps> = ({ record, showIndex,
</motion.div>
<div className="w-[130px]"></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>
)
......
......@@ -44,7 +44,11 @@ export const ChatAnswerParser: React.FC<ChatAnswerParserProps> = ({ onComplate,
>
{displayedText}
</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} />}
</div>
)
......
......@@ -21,7 +21,7 @@
justify-content: flex-start;
overflow: hidden;
}
.scrollable {
.collectScrollable {
flex-direction: column;
align-items: center;
display: flex;
......
......@@ -2,10 +2,10 @@
// Please do not change this file!
interface CssExports {
collectPage: string
collectScrollable: string
content: string
inter: string
scrollView: string
scrollable: string
}
declare const cssExports: CssExports
export default cssExports
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 rehypeRaw from 'rehype-raw'
import rehypeSanitize from 'rehype-sanitize'
......@@ -11,63 +11,142 @@ import { formatMarkdown } from '../Chat/components/ChatItem/markdownFormatter'
import { ChatAnswerAttachment } from '../Chat/components/ChatItem/ChatAnswerAttchment'
import styles from './Collect.module.less'
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 = () => {
const [isLoading, setIsLoading] = useState(false)
const [isOpen, setIsOpen] = useState(false)
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 () => {
setIsLoading(true)
const params = {
pageNum: 1,
pageSize: 5,
pageNum,
pageSize,
}
const res = await fetchQueryCollectionList(params)
setCollectList(res.data.records)
setCollectList([...collectList, ...res.data.records])
setTotal(res.data.total)
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(() => {
getCollectList()
}, [])
}, [pageNum])
return (
<div className={styles.scrollView}>
<div className={`${styles.collectPage} relative`}>
<ChatSlogan />
<ChatMaskBar />
<div className="content h-full overflow-y-auto">
{isLoading && <div className="w-full h-full flex justify-center"><Spinner /></div>}
{!isLoading && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
opacity: { duration: 0.1 },
}}
className={`${styles.scrollable} scrollbar-hide`}
>
<div className={`${styles.inter} gap-[32px]`}>
{
collectList.map((item: any) => (
<div className="w-full max-w-[900px] mx-auto bg-white rounded-[20px] box-border px-[24px] py-[20px]" key={item.conversationId}>
<ReactMarkdown
rehypePlugins={[rehypeRaw, rehypeSanitize]}
remarkPlugins={[remarkGfm]}
className="markdown-content"
>
{formatMarkdown(item.answer || '')}
</ReactMarkdown>
{item.attachmentList && item.attachmentList?.length !== 0 && <ChatAnswerAttachment answer={item} />}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
opacity: { duration: 0.1 },
}}
className={`${styles.collectScrollable} scrollbar-hide`}
>
<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}`}>
<ReactMarkdown
rehypePlugins={[rehypeRaw, rehypeSanitize]}
remarkPlugins={[remarkGfm]}
className="markdown-content"
>
{formatMarkdown(item.answer || '')}
</ReactMarkdown>
{item.attachmentList && item.attachmentList?.length !== 0 && <ChatAnswerAttachment answer={item} />}
<div className="mt-[12px] flex gap-[4px] justify-end">
<Tooltip color="foreground" content="复制" className="capitalize">
<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>
</motion.div>
)}
</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>
<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>
)
}
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 conversationReducer from './conversationSlice'
import chatReducer from './chatSlice'
export const store = configureStore({
reducer: {
conversation: conversationReducer,
chat: chatReducer,
},
})
// 为了在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