Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
sdream-ai-fe
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
侯明涛
sdream-ai-fe
Commits
6383ef2c
Commit
6383ef2c
authored
Aug 14, 2024
by
HoMeTown
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 收藏列表
parent
1949d403
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
186 additions
and
40 deletions
+186
-40
src/assets/svg/delete.svg
+22
-0
src/components/ChatEditor/index.tsx
+6
-1
src/pages/Chat/Chat.tsx
+3
-1
src/pages/Chat/components/ChatItem/ChatAnswerBox.tsx
+9
-1
src/pages/Chat/components/ChatItem/ChatAnswerParser.tsx
+5
-1
src/pages/Collect/Collect.module.less
+1
-1
src/pages/Collect/Collect.module.less.d.ts
+1
-1
src/pages/Collect/Collect.tsx
+113
-34
src/store/chatSlice.ts
+24
-0
src/store/index.ts
+2
-0
No files found.
src/assets/svg/delete.svg
0 → 100644
View file @
6383ef2c
<?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>
src/components/ChatEditor/index.tsx
View file @
6383ef2c
...
...
@@ -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
>
...
...
src/pages/Chat/Chat.tsx
View file @
6383ef2c
...
...
@@ -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
,
...
...
src/pages/Chat/components/ChatItem/ChatAnswerBox.tsx
View file @
6383ef2c
...
...
@@ -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
>
)
...
...
src/pages/Chat/components/ChatItem/ChatAnswerParser.tsx
View file @
6383ef2c
...
...
@@ -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
>
)
...
...
src/pages/Collect/Collect.module.less
View file @
6383ef2c
...
...
@@ -21,7 +21,7 @@
justify-content: flex-start;
overflow: hidden;
}
.
s
crollable {
.
collectS
crollable {
flex-direction: column;
align-items: center;
display: flex;
...
...
src/pages/Collect/Collect.module.less.d.ts
View file @
6383ef2c
...
...
@@ -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
src/pages/Collect/Collect.tsx
View file @
6383ef2c
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
>
)
}
src/store/chatSlice.ts
0 → 100644
View file @
6383ef2c
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
src/store/index.ts
View file @
6383ef2c
import
{
configureStore
}
from
'@reduxjs/toolkit'
import
conversationReducer
from
'./conversationSlice'
import
chatReducer
from
'./chatSlice'
export
const
store
=
configureStore
({
reducer
:
{
conversation
:
conversationReducer
,
chat
:
chatReducer
,
},
})
// 为了在TypeScript中使用,我们导出这些类型
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment