-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathChatWindow.tsx
237 lines (228 loc) · 9.03 KB
/
ChatWindow.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import React, {useContext} from 'react';
import {ThemeContext, removeUndefined} from '../../theme/ThemeContext';
import {useMessages} from '../../hooks';
import DocumentPlusIcon from '@heroicons/react/24/outline/esm/DocumentPlusIcon';
import {ResetWrapper} from '../../utils/ResetWrapper';
import {ChatWindowUserMessage} from "./ChatWindowUserMessage";
import {ChatWindowAssistantMessage} from "./ChatWindowAssistantMessage";
import {addOpacity, scaleFontSize} from '../../utils/scaleFontSize';
// todo: Add docu for new components, remove border around icon, add longer description
// Define a type for the shape of the overrides
export interface ChatWindowStyles extends React.CSSProperties {
backgroundColor?: string;
messageBackgroundColor?: string;
messageBorderRadius?: string;
messageFontSize?: string;
roleLabelFontSize?: string;
accent?: string;
color?: string;
padding?: string;
fontFamily?: string;
fontSize?: string;
borderRadius?: string;
}
/**
* Props for the ChatWindow component
* @see {@link ChatWindow}
*/
export interface ChatWindowProps {
/**
* Style overrides for the component
*/
styleOverrides?: ChatWindowStyles;
/**
* Whether to show role labels (User, Assistant) or Icons before messages
* @default true
*/
showRoleIndicator?: boolean;
/**
* Custom label for user messages
* @default "User"
*/
userLabel?: string;
/**
* Custom label for assistant messages
* @default "Assistant"
*/
assistantLabel?: string;
/**
* Custom icon for user messages. Must be a ReactElement. If provided, this will be shown instead of the role label
*/
userIcon?: React.ReactElement
/**
* Custom icon for assistant messages. Must be a ReactElement. If provided, this will be shown instead of the role label
*/
assistantIcon?: React.ReactElement;
/**
* Whether to render markdown in assistant messages
* @default true
*/
markdown?: boolean;
/**
* Whether to show the copy button in assistant messages
* @default true
*/
showCopy?: boolean;
componentKey?: string;
}
/**
* The ChatWindow component displays a conversation between a user and an assistant. It supports the display
* of **markdown-formatted** messages, **role indicators**, and **custom icons** for user and assistant messages.
*
* This component is responsible for rendering a dynamic chat interface using messages provided by
* the `LexioProvider` (via the `useMessages` hook). It supports both **static and streaming messages**,
* and automatically scrolls to the most recent message upon updates.
*
* The appearance of the ChatWindow is primarily determined by theme defaults from the `ThemeContext`
* but can be customized via the `styleOverrides` prop. These overrides merge with the default theme styles,
* ensuring a consistent yet flexible look and feel.
*
* @component
*
* Features:
* - Distinct styling for user and assistant messages
* - Markdown rendering for assistant messages
* - Code syntax highlighting with copy functionality
* - Automatic scrolling to the latest message
* - Customizable role labels and icons
* - Responsive design
*
* @example
*
* ```tsx
* <ChatWindow
* userLabel="Customer"
* userIcon={<UserIcon className="w-5 h-5" />}
* assistantLabel="Support"
* assistantIcon={<BotIcon className="w-5 h-5" />}
* styleOverrides={{
* backgroundColor: '#f5f5f5',
* padding: '1rem',
* messageBackgroundColor: '#ffffff',
* borderRadius: '8px',
* }}
* showRoleIndicator={true}
* markdown={true}
* showCopy={true}
* componentKey="support-chat"
* />
* ```
*
* @see ChatWindowUserMessage for details on rendering user messages.
* @see ChatWindowAssistantMessage for details on rendering assistant messages.
*/
const ChatWindow: React.FC<ChatWindowProps> = ({
userLabel = 'User',
userIcon = undefined,
assistantLabel = 'Assistant',
assistantIcon = undefined,
styleOverrides = {},
showRoleIndicator = true,
markdown = true,
showCopy = true,
componentKey = undefined,
}) => {
const {messages, currentStream, clearMessages} = useMessages(componentKey ? `ChatWindow-${componentKey}` : 'ChatWindow');
// Add ref for scrolling
const chatEndRef = React.useRef<HTMLDivElement>(null);
// Scroll to bottom whenever messages or currentStream changes
React.useEffect(() => {
// If there are no messages and no currentStream, don't scroll
if (messages.length === 0 && !currentStream) {
return;
}
// Get the parent element of the chatEndRef and scroll to the bottom
const container = chatEndRef.current?.parentElement;
if (container) {
// Scroll to the bottom of the container by setting scrollTop to scrollHeight
container.scrollTop = container.scrollHeight;
}
}, [messages, currentStream]);
// --- use theme ---
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error('ThemeContext is undefined');
}
const {colors, typography, componentDefaults} = theme.theme;
// Merge theme defaults + overrides
const style: ChatWindowStyles = {
backgroundColor: colors.background,
messageBackgroundColor: addOpacity(colors.primary, 0.12), // add opacity
messageBorderRadius: componentDefaults.borderRadius,
messageFontSize: typography.fontSizeBase,
roleLabelFontSize: scaleFontSize(typography.fontSizeBase, 0.8),
accent: addOpacity(colors.primary, 0.3), // add opacity
color: colors.text,
padding: componentDefaults.padding,
fontFamily: typography.fontFamily,
fontSize: typography.fontSizeBase,
borderRadius: componentDefaults.borderRadius,
...removeUndefined(styleOverrides), // ensure these override theme defaults
};
return (
<ResetWrapper>
<div
className="w-full h-full overflow-y-auto flex flex-col gap-y-6"
style={style}
>
{/* Fixed header */}
<div className="flex justify-end p-2 h-14 flex-none">
<button
onClick={clearMessages}
className="p-2 rounded-full hover:bg-gray-100 transition-colors"
style={{
color: colors.text,
backgroundColor: 'transparent',
border: 'none',
cursor: 'pointer',
}}
title="New conversation"
aria-label="New conversation"
>
<DocumentPlusIcon className="size-5" />
</button>
</div>
{messages.map((msg, index) => (
<React.Fragment key={msg.id || index}>
{msg.role == "user" && (
<ChatWindowUserMessage
message={msg.content}
style={style}
showRoleIndicator={showRoleIndicator}
roleLabel={userLabel}
icon={userIcon || undefined}
/>
)}
{msg.role == "assistant" && (
<ChatWindowAssistantMessage
message={msg.content}
messageId={msg.id}
style={style}
showRoleIndicator={showRoleIndicator}
roleLabel={assistantLabel}
icon={assistantIcon || undefined}
markdown={markdown}
showCopy={showCopy}
/>
)}
</React.Fragment>
))}
{currentStream && (
<ChatWindowAssistantMessage
key={'stream'}
messageId={null}
message={currentStream.content}
style={style}
showRoleIndicator={showRoleIndicator}
roleLabel={assistantLabel}
icon={assistantIcon || undefined}
markdown={markdown}
isStreaming={true}
/>
)}
<div ref={chatEndRef}/>
</div>
</ResetWrapper>
);
};
export {ChatWindow}