Skip to content

Commit 83938ef

Browse files
committed
feat: add operation
1 parent daeb169 commit 83938ef

File tree

10 files changed

+274
-16
lines changed

10 files changed

+274
-16
lines changed

src/assets/svgs/header/checked.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/svgs/header/copy.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/svgs/header/re-send.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/svgs/header/thumb-down.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/svgs/header/thumb-up.svg

Lines changed: 3 additions & 0 deletions
Loading

src/components/AIModal/AssistantMessage.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
1+
import React, { useState } from "react"
12
import ReactMarkdown from "react-markdown"
23
import remarkGfm from "remark-gfm"
34

5+
import { Box } from "@mui/material"
6+
7+
import Operation from "./Operation"
8+
49
const AssistantMessage = props => {
5-
const { children } = props
10+
const { children, feedback, allowOperation, isLast, onRetry, onThumbUp, onThumbDown } = props
11+
12+
const [operationVisible, setOperationVisible] = useState<boolean>(false)
13+
14+
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
15+
setOperationVisible(true)
16+
}
17+
18+
const handlePopoverClose = () => {
19+
setOperationVisible(false)
20+
}
21+
622
return (
7-
<ReactMarkdown
8-
children={children as string}
9-
remarkPlugins={[remarkGfm]}
10-
components={{
11-
a: ({ node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
12-
}}
13-
className="assistant-message"
14-
/>
23+
<>
24+
<Box onMouseEnter={handlePopoverOpen} onMouseLeave={handlePopoverClose}>
25+
<ReactMarkdown
26+
children={children as string}
27+
remarkPlugins={[remarkGfm]}
28+
components={{
29+
a: ({ node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
30+
}}
31+
className="assistant-message"
32+
/>
33+
<Box sx={{ height: "2rem", marginTop: "-0.4rem", marginBottom: "1.6rem" }}>
34+
{allowOperation && (isLast || operationVisible) && (
35+
<Operation message={children as string} feedback={feedback} onThumbUp={onThumbUp} onThumbDown={onThumbDown} onRetry={onRetry}></Operation>
36+
)}
37+
</Box>
38+
</Box>
39+
</>
1540
)
1641
}
1742

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { AnimatePresence, motion } from "motion/react"
2+
import { useEffect } from "react"
3+
4+
import { Box } from "@mui/material"
5+
6+
const MotionBox = motion(Box)
7+
const FeedbackAlert = props => {
8+
const { sx, open, duration, children, onClose } = props
9+
10+
useEffect(() => {
11+
if (open) {
12+
const timer = setTimeout(() => {
13+
onClose?.()
14+
}, duration || 2e3) // Auto close after 3 seconds
15+
return () => clearTimeout(timer)
16+
}
17+
}, [open])
18+
19+
return (
20+
<AnimatePresence>
21+
{open && (
22+
<MotionBox
23+
initial={{ opacity: 0, y: -20 }}
24+
animate={{ opacity: 1, y: 0 }}
25+
exit={{ opacity: 0, y: 20 }}
26+
transition={{ duration: 0.3 }}
27+
sx={{
28+
borderRadius: "0.4rem",
29+
backgroundColor: "#101010",
30+
color: "#fff",
31+
fontSize: "1.2rem",
32+
padding: "0.4rem 1.2rem",
33+
34+
...sx,
35+
}}
36+
>
37+
{children}
38+
</MotionBox>
39+
)}
40+
</AnimatePresence>
41+
)
42+
}
43+
44+
export default FeedbackAlert

src/components/AIModal/MessagePanel.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import clsx from "clsx"
2-
import { useEffect, useRef } from "react"
2+
import { useEffect, useRef, useState } from "react"
33

44
import { Box } from "@mui/material"
55

66
import SpinSvg from "@/assets/svgs/header/spin.svg"
77

88
import AssistantMessage from "./AssistantMessage"
9+
import FeedbackAlert from "./FeedbackAlert"
910
import UserMessage from "./UserMessage"
1011

1112
const MessagePanel = props => {
12-
const { data, loading } = props
13+
const { data, fetching, streaming, onRetry, onUpdateData } = props
1314

15+
const [feedbackAlertVisible, setFeedbackAlertVisible] = useState(false)
16+
// Reference to the bottom of the message panel for auto-scrolling
1417
const bottomRef = useRef<HTMLDivElement>(null)
1518

1619
useEffect(() => {
@@ -21,9 +24,19 @@ const MessagePanel = props => {
2124
return null
2225
}
2326

27+
const handleThumbUp = id => {
28+
setFeedbackAlertVisible(true)
29+
onUpdateData({ id, feedback: "good" })
30+
}
31+
32+
const handleThumbDown = id => {
33+
setFeedbackAlertVisible(true)
34+
onUpdateData({ id, feedback: "bad" })
35+
}
2436
return (
2537
<Box
2638
sx={{
39+
position: "relative",
2740
flex: 1,
2841
overflowY: "auto",
2942
padding: ["1.6rem 2rem 0", "1.6rem 1.6rem 0"],
@@ -32,14 +45,33 @@ const MessagePanel = props => {
3245
}}
3346
>
3447
<Box component="ul">
35-
{data.map(message => (
48+
{data.map((message, index) => (
3649
<Box component="li" key={message.id}>
37-
{message.type === "input_text" ? <UserMessage>{message.text}</UserMessage> : <AssistantMessage>{message.text}</AssistantMessage>}
50+
{message.type === "input_text" ? (
51+
<UserMessage>{message.text}</UserMessage>
52+
) : message.text ? (
53+
<AssistantMessage
54+
allowOperation={!streaming && !fetching}
55+
isLast={index === data.length - 1}
56+
feedback={message.feedback}
57+
onThumbUp={() => handleThumbUp(message.id)}
58+
onThumbDown={() => handleThumbDown(message.id)}
59+
onRetry={() => onRetry(message.id)}
60+
>
61+
{message.text}
62+
</AssistantMessage>
63+
) : null}
3864
</Box>
3965
))}
4066
</Box>
41-
<SpinSvg className={clsx(loading ? "visible mb-[1.6rem]" : "invisible")} />
67+
<SpinSvg className={clsx(fetching ? "visible mb-[1.6rem]" : "invisible")} />
4268
<div ref={bottomRef}></div>
69+
70+
<Box sx={{ position: "absolute", top: "2.4rem", left: "50%", transform: "translateX(-50%)" }}>
71+
<FeedbackAlert open={feedbackAlertVisible} duration={3e3} onClose={() => setFeedbackAlertVisible(false)}>
72+
Thanks for your feedback!
73+
</FeedbackAlert>
74+
</Box>
4375
</Box>
4476
)
4577
}

src/components/AIModal/Operation.tsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { useState } from "react"
2+
3+
import { Box, IconButton, Stack, Tooltip } from "@mui/material"
4+
5+
import CheckedSvg from "@/assets/svgs/header/checked.svg"
6+
import CopySvg from "@/assets/svgs/header/copy.svg"
7+
import ReSendSvg from "@/assets/svgs/header/re-send.svg"
8+
import ThumbDownSvg from "@/assets/svgs/header/thumb-down.svg"
9+
import ThumbUpSvg from "@/assets/svgs/header/thumb-up.svg"
10+
11+
const Operation = props => {
12+
const { sx, feedback, message, onRetry, onThumbUp, onThumbDown } = props
13+
const [tip, setTip] = useState<string>("")
14+
15+
const [copied, setCopied] = useState<boolean>(false)
16+
17+
const operations = [
18+
{
19+
icon: copied ? CheckedSvg : CopySvg,
20+
21+
tooltip: "Copy",
22+
onClick: () => {
23+
navigator.clipboard.writeText(message)
24+
setCopied(true)
25+
setTip("Copied")
26+
setTimeout(() => {
27+
setCopied(false)
28+
setTip("")
29+
}, 2e3)
30+
},
31+
},
32+
{
33+
icon: ThumbUpSvg,
34+
hidden: feedback === "bad",
35+
tooltip: "Good Response",
36+
disabled: feedback === "good",
37+
onClick: () => {
38+
onThumbUp?.()
39+
},
40+
},
41+
{
42+
icon: ThumbDownSvg,
43+
hidden: feedback === "good",
44+
tooltip: "Bad Response",
45+
disabled: feedback === "bad",
46+
onClick: () => {
47+
onThumbDown?.()
48+
},
49+
},
50+
{
51+
icon: ReSendSvg,
52+
tooltip: "Try Again",
53+
onClick: () => {
54+
onRetry?.()
55+
},
56+
},
57+
]
58+
59+
return (
60+
<Box sx={{ position: "relative", ...sx }}>
61+
<Stack direction="row" gap="1.6rem">
62+
{operations
63+
.filter(({ hidden }) => !hidden)
64+
.map(({ icon: Icon, tooltip, disabled, onClick }, index) => (
65+
<Tooltip
66+
key={index}
67+
title={tip || tooltip}
68+
placement="bottom"
69+
arrow
70+
slotProps={{
71+
tooltip: {
72+
sx: {
73+
borderRadius: "0.4rem",
74+
fontSize: "1.2rem",
75+
lineHeight: "1.6rem",
76+
padding: "0.4rem 0.8rem",
77+
backgroundColor: "text.primary",
78+
marginTop: "0.8rem",
79+
"& .MuiTooltip-arrow": {
80+
color: "#101010",
81+
},
82+
},
83+
},
84+
}}
85+
>
86+
<IconButton
87+
sx={{
88+
p: 0,
89+
color: "#10101099",
90+
"&:hover": {
91+
color: "text.primary",
92+
backgroundColor: "unset",
93+
},
94+
height: "2rem",
95+
width: "2rem",
96+
}}
97+
disabled={disabled}
98+
onClick={onClick}
99+
>
100+
<Icon></Icon>
101+
</IconButton>
102+
</Tooltip>
103+
))}
104+
</Stack>
105+
</Box>
106+
)
107+
}
108+
109+
export default Operation

src/components/AIModal/index.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface Message {
2424
id: string
2525
type: "input_text" | "output_text" | "output_text_error"
2626
text: string
27+
feedback?: "good" | "bad"
2728
}
2829

2930
type LoadingStatus = "none" | "fetching" | "streaming"
@@ -66,13 +67,23 @@ const AIModal = () => {
6667
})
6768
})
6869
setSearchText("")
70+
chatWithScrollAI(userMessage)
71+
}
72+
73+
const handleReSendMessage = async (id: string) => {
74+
const messageIndex = messages.findIndex(message => message.id === id)
75+
const reservedMessage = messages.slice(0, messageIndex)
76+
setMessages(reservedMessage)
77+
chatWithScrollAI(reservedMessage[messageIndex - 1].text)
78+
}
6979

80+
const chatWithScrollAI = async (message: string) => {
7081
setLoadingStatus("fetching")
7182

7283
let stream
7384
try {
7485
stream = await chatWithAI({
75-
message: userMessage,
86+
message,
7687
prevId: responseId,
7788
})
7889
} catch (error) {
@@ -149,12 +160,28 @@ const AIModal = () => {
149160
} as Message
150161
return [...preValue.slice(0, -1), newMessage]
151162
})
163+
} else if (event.type === "response.completed") {
164+
setLoadingStatus("none")
152165
} else if (event.type === "error") {
153166
throw new Error("Connection error, please try again.")
154167
}
155168
}
156169
}
157170

171+
const handleUpdateData = ({ id, feedback }) => {
172+
setMessages(preValue => {
173+
return preValue.map(message => {
174+
if (message.id === id) {
175+
return {
176+
...message,
177+
feedback: feedback,
178+
}
179+
}
180+
return message
181+
})
182+
})
183+
}
184+
158185
return (
159186
<AnimatePresence>
160187
{aiModalVisible ? (
@@ -207,7 +234,13 @@ const AIModal = () => {
207234
</IconButton>
208235
</Stack>
209236
{messages?.length ? (
210-
<MessagePanel data={messages} loading={loadingStatus === "fetching"}></MessagePanel>
237+
<MessagePanel
238+
data={messages}
239+
fetching={loadingStatus === "fetching"}
240+
streaming={loadingStatus === "streaming"}
241+
onUpdateData={handleUpdateData}
242+
onRetry={handleReSendMessage}
243+
></MessagePanel>
211244
) : (
212245
<InitialPanel onChat={handleSendMessage}></InitialPanel>
213246
)}

0 commit comments

Comments
 (0)