Skip to content

Commit ef19a73

Browse files
committed
feat: display thoughts.
1 parent fcfdeb8 commit ef19a73

File tree

4 files changed

+191
-20
lines changed

4 files changed

+191
-20
lines changed

web/src/components/ChatMessage/index.jsx

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ThemeContext } from "@/contexts/theme";
2222
import { MessageContext } from "@/contexts/message";
2323
import { UserContext } from "@/contexts/user";
2424
import { stringToColor } from "@/commons";
25+
import PeekDetails from "@/components/PeekDetails";
2526

2627

2728
/**
@@ -64,11 +65,11 @@ const ChatMessage = ({ convId, message }) => {
6465
}, [theme]);
6566

6667
/**
67-
* Handles the copying of message content to the clipboard.
68-
* Updates the tooltip title to indicate the content has been copied.
69-
*
70-
* @param {string} content - The content to be copied to the clipboard.
71-
*/
68+
* Handles the copying of message content to the clipboard.
69+
* Updates the tooltip title to indicate the content has been copied.
70+
*
71+
* @param {string} content - The content to be copied to the clipboard.
72+
*/
7273
const onCopyClick = (content) => {
7374
navigator.clipboard.writeText(content);
7475
setCopyTooltipTitle("copied!");
@@ -78,9 +79,9 @@ const ChatMessage = ({ convId, message }) => {
7879
};
7980

8081
/**
81-
* Handles the thumbs up action by sending a PUT request to register the feedback.
82-
* Updates the tooltip and dispatches a message update to the context.
83-
*/
82+
* Handles the thumbs up action by sending a PUT request to register the feedback.
83+
* Updates the tooltip and dispatches a message update to the context.
84+
*/
8485
const onThumbUpClick = () => {
8586
fetch(`/api/conversations/${convId}/messages/${message.id}/thumbup`, {
8687
method: "PUT",
@@ -94,9 +95,9 @@ const ChatMessage = ({ convId, message }) => {
9495
};
9596

9697
/**
97-
* Handles the thumbs down action by sending a PUT request to register the feedback.
98-
* Updates the tooltip and dispatches a message update to the context.
99-
*/
98+
* Handles the thumbs down action by sending a PUT request to register the feedback.
99+
* Updates the tooltip and dispatches a message update to the context.
100+
*/
100101
const onThumbDownClick = () => {
101102
fetch(`/api/conversations/${convId}/messages/${message.id}/thumbdown`, {
102103
method: "PUT",
@@ -110,11 +111,11 @@ const ChatMessage = ({ convId, message }) => {
110111
};
111112

112113
/**
113-
* Determines if the message was sent by the current user.
114-
*
115-
* @param {Object} message - The message object.
116-
* @returns {boolean} True if the message was sent by the current user, otherwise false.
117-
*/
114+
* Determines if the message was sent by the current user.
115+
*
116+
* @param {Object} message - The message object.
117+
* @returns {boolean} True if the message was sent by the current user, otherwise false.
118+
*/
118119
const myMessage = message.from && message.from.toLowerCase() === username;
119120

120121
return (
@@ -123,14 +124,20 @@ const ChatMessage = ({ convId, message }) => {
123124
{/* NOTE: className not working on Avatar */}
124125
{myMessage ?
125126
<Avatar src={avatar} /> :
126-
// TODO: give AI an avatar
127+
// TODO: give AI an avatar
127128
<Avatar sx={{ bgcolor: stringToColor(message.from) }}>
128-
AI
129+
AI
129130
</Avatar>
130131
}
131132
<div className={styles.messageTitleName}>{myMessage ? "You" : "AI"}</div>
132133
</div>
133134
<div className={styles.messageBody}>
135+
{message.additional_kwargs && message.additional_kwargs.thought && (
136+
<PeekDetails
137+
summary="Thoughts"
138+
content={message.additional_kwargs.thought}
139+
/>
140+
)}
134141
<Markdown
135142
remarkPlugins={[remarkGfm, remarkMath]}
136143
rehypePlugins={[rehypeKatex]}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import styles from "./index.module.css";
2+
3+
import { useContext, useEffect, useRef, useState } from "react";
4+
import Markdown from "react-markdown";
5+
import SyntaxHighlighter from "react-syntax-highlighter";
6+
import remarkGfm from "remark-gfm";
7+
import remarkMath from "remark-math";
8+
import rehypeKatex from "rehype-katex";
9+
import { darcula, googlecode } from "react-syntax-highlighter/dist/esm/styles/hljs";
10+
import PropTypes from "prop-types";
11+
12+
import { ThemeContext } from "@/contexts/theme";
13+
14+
15+
const PeekDetails = ({ summary, content, peekHeight = "5rem" }) => {
16+
const { theme } = useContext(ThemeContext);
17+
const [markdownTheme, setMarkdownTheme] = useState(darcula);
18+
const contentRef = useRef(null);
19+
const [isOpen, setIsOpen] = useState(false);
20+
21+
const toggleOpen = () => {
22+
setIsOpen(!isOpen);
23+
};
24+
25+
// Update markdown theme based on the current theme
26+
useEffect(() => {
27+
switch (theme) {
28+
case "dark":
29+
setMarkdownTheme(darcula);
30+
break;
31+
case "light":
32+
setMarkdownTheme(googlecode);
33+
break;
34+
default: {
35+
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
36+
setMarkdownTheme(darcula);
37+
} else {
38+
setMarkdownTheme(googlecode);
39+
}
40+
}
41+
}
42+
}, [theme]);
43+
44+
useEffect(() => {
45+
if (!isOpen && contentRef.current) {
46+
contentRef.current.scrollTop = contentRef.current.scrollHeight - contentRef.current.clientHeight;
47+
} else if (isOpen && contentRef.current) {
48+
contentRef.current.scrollTop = 0;
49+
}
50+
}, [isOpen, content]);
51+
52+
return (
53+
<div className={styles.peekDetails}>
54+
<div className={styles.summaryBar} onClick={toggleOpen}>
55+
{summary}
56+
<span className={styles.toggleIcon}>{isOpen ? "-" : "+"}</span>
57+
</div>
58+
<div
59+
className={styles.content}
60+
style={{
61+
maxHeight: isOpen ? "200rem" : peekHeight // I must use a numeric value for transition to work
62+
}}
63+
ref={contentRef}
64+
>
65+
<Markdown
66+
remarkPlugins={[remarkGfm, remarkMath]}
67+
rehypePlugins={[rehypeKatex]}
68+
components={{
69+
code({ inline, className, children, ...props }) {
70+
const match = /language-(\w+)/.exec(className || "");
71+
return !inline && match ? (
72+
<div>
73+
<div className={styles.codeTitle}>
74+
<div>{match[1]}</div>
75+
</div>
76+
<SyntaxHighlighter
77+
{...props}
78+
style={markdownTheme}
79+
language={match[1]}
80+
PreTag="div"
81+
>
82+
{/* remove the last line separator, is it necessary? */}
83+
{String(children).replace(/\n$/, "")}
84+
</SyntaxHighlighter>
85+
</div>
86+
) : (
87+
<code {...props} className={className}>
88+
{children}
89+
</code>
90+
);
91+
},
92+
}}
93+
>
94+
{content}
95+
</Markdown>
96+
</div>
97+
</div>
98+
);
99+
};
100+
101+
PeekDetails.propTypes = {
102+
summary: PropTypes.string.isRequired,
103+
content: PropTypes.string,
104+
peekHeight: PropTypes.string,
105+
};
106+
107+
export default PeekDetails;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.peekDetails {
2+
border: 1px solid var(--border-color);
3+
border-radius: 5px;
4+
margin-bottom: 10px;
5+
}
6+
7+
.summaryBar {
8+
padding: 10px;
9+
background-color: var(--bg-primary);
10+
cursor: pointer;
11+
display: flex;
12+
justify-content: space-between;
13+
align-items: center;
14+
border-radius: 5px 5px 0 0;
15+
border-bottom: 1px solid var(--border-color);
16+
}
17+
18+
.toggleIcon {
19+
font-weight: bold;
20+
margin-left: 10px;
21+
}
22+
23+
.content {
24+
overflow: hidden;
25+
border-radius: 0 05px 5px;
26+
padding: 10px;
27+
margin: 0;
28+
overflow-y: auto;
29+
scroll-behavior: smooth;
30+
scrollbar-color: var(--border-color) transparent;
31+
scrollbar-width: thin;
32+
}
33+
34+
.codeTitle {
35+
padding: 5px;
36+
padding-left: 10px;
37+
padding-right: 10px;
38+
display: flex;
39+
flex-direction: row;
40+
justify-content: space-between;
41+
background-color: var(--bg-primary);
42+
}

web/src/contexts/message.jsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,18 @@ export const messagesReducer = (messages, action) => {
4646
// this could happen when the user switch to another conversation and switch back
4747
return messages;
4848
}
49-
return [...messages.slice(0, match), { ...messages[match], content: messages[match].content + action.message.content }, ...messages.slice(match + 1)];
49+
return [
50+
...messages.slice(0, match),
51+
{
52+
...messages[match],
53+
content: messages[match].content + action.message.content,
54+
additional_kwargs: {
55+
... (messages[match].additional_kwargs || {}),
56+
thought: ((messages[match].additional_kwargs || {}).thought ?? "") + ((action.message.additional_kwargs || {}).thought ?? ""),
57+
}
58+
},
59+
...messages.slice(match + 1)
60+
];
5061
}
5162
case "updated": {
5263
// find reversly could potentially be faster as the full message usually is the last one (streamed).
@@ -55,7 +66,11 @@ export const messagesReducer = (messages, action) => {
5566
// message does not exist, ignore it
5667
return messages;
5768
}
58-
return [...messages.slice(0, match), { ...messages[match], ...action.message }, ...messages.slice(match + 1)];
69+
return [
70+
...messages.slice(0, match),
71+
{ ...messages[match], ...action.message },
72+
...messages.slice(match + 1)
73+
];
5974
}
6075
case "replaceAll": {
6176
return [...action.messages]

0 commit comments

Comments
 (0)