@@ -24,6 +24,7 @@ export interface Message {
2424 content : string | MessageContent [ ] ;
2525 model ?: string ;
2626 timestamp ?: string ;
27+ loading ?: boolean ;
2728}
2829
2930interface MessageBubbleProps {
@@ -100,119 +101,129 @@ const MessageBubble: React.FC<MessageBubbleProps> = React.memo(({ message, role
100101 { ! isUser && (
101102 < img src = { botAvatar } alt = "bot avatar" className = "avatar" />
102103 ) }
103- < div className = { `message-bubble ${ isUser ? 'user' : 'bot' } ` } >
104- { images . length > 0 && (
105- < div className = "message-images" >
106- { images . map ( ( image , index ) => (
107- < div
108- key = { index }
109- className = "message-image"
110- onClick = { ( ) => handleImageClick ( image ) }
111- >
112- < img src = { image } alt = { `User uploaded ${ index + 1 } ` } />
113- </ div >
114- ) ) }
104+ < div className = { `message-bubble ${ isUser ? 'user' : 'bot' } ${ message . loading ? 'loading' : '' } ` } >
105+ { message . loading ? (
106+ < div className = "typing-indicator" >
107+ < span > </ span >
108+ < span > </ span >
109+ < span > </ span >
115110 </ div >
116- ) }
117- { text && (
118- < ReactMarkdown
119- remarkPlugins = { [ remarkGfm ] }
120- components = { {
121- code ( { node, inline, className, children, ...props } ) {
122- const match = / l a n g u a g e - ( \w + ) / . exec ( className || '' ) ;
123- const codeString = String ( children ) . replace ( / \n $ / , '' ) ;
124-
125- if ( ! inline && match ) {
126- const lineNumber = node ?. position ?. start . line ;
127- return (
128- < div className = "code-block-wrapper" >
129- < div className = "code-block-header" >
130- < span className = "code-block-language" >
131- { match [ 1 ] }
132- </ span >
133- < button
134- className = "copy-button"
135- onClick = { ( ) => lineNumber && copyToClipboard ( codeString , lineNumber ) }
136- title = { copiedIndex === lineNumber ? 'Copied!' : 'Copy code' }
137- >
138- < svg
139- width = "16"
140- height = "16"
141- viewBox = "0 0 24 24"
142- fill = "none"
143- stroke = "currentColor"
144- strokeWidth = "2"
145- strokeLinecap = "round"
146- strokeLinejoin = "round"
147- >
148- { copiedIndex === lineNumber ? (
149- < path d = "M20 6L9 17l-5-5" />
150- ) : (
151- < >
152- < rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" />
153- < path d = "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
154- </ >
155- ) }
156- </ svg >
157- </ button >
158- </ div >
159- < div className = "syntax-highlighter-wrapper" >
160- < SyntaxHighlighter
161- style = { oneDark }
162- language = { match [ 1 ] }
163- PreTag = "div"
164- { ...props }
165- customStyle = { {
166- margin : 0 ,
167- background : '#282c34' ,
168- padding : '16px' ,
169- } }
170- >
171- { codeString }
172- </ SyntaxHighlighter >
173- </ div >
174- </ div >
175- ) ;
176- }
177- return (
178- < code className = { className } { ...props } >
179- { children }
180- </ code >
181- ) ;
182- } ,
183- } }
184- >
185- { text }
186- </ ReactMarkdown >
187- ) }
188- { ! isUser && (
189- < div className = "message-actions" >
190- < button
191- className = "message-copy-button"
192- onClick = { copyMessage }
193- title = { copiedMessage ? 'Copied!' : 'Copy response' }
194- >
195- < svg
196- width = "16"
197- height = "16"
198- viewBox = "0 0 24 24"
199- fill = "none"
200- stroke = "currentColor"
201- strokeWidth = "2"
202- strokeLinecap = "round"
203- strokeLinejoin = "round"
111+ ) : (
112+ < >
113+ { images . length > 0 && (
114+ < div className = "message-images" >
115+ { images . map ( ( image , index ) => (
116+ < div
117+ key = { index }
118+ className = "message-image"
119+ onClick = { ( ) => handleImageClick ( image ) }
120+ >
121+ < img src = { image } alt = { `User uploaded ${ index + 1 } ` } />
122+ </ div >
123+ ) ) }
124+ </ div >
125+ ) }
126+ { text && (
127+ < ReactMarkdown
128+ remarkPlugins = { [ remarkGfm ] }
129+ components = { {
130+ code ( { node, inline, className, children, ...props } ) {
131+ const match = / l a n g u a g e - ( \w + ) / . exec ( className || '' ) ;
132+ const codeString = String ( children ) . replace ( / \n $ / , '' ) ;
133+
134+ if ( ! inline && match ) {
135+ const lineNumber = node ?. position ?. start . line ;
136+ return (
137+ < div className = "code-block-wrapper" >
138+ < div className = "code-block-header" >
139+ < span className = "code-block-language" >
140+ { match [ 1 ] }
141+ </ span >
142+ < button
143+ className = "copy-button"
144+ onClick = { ( ) => lineNumber && copyToClipboard ( codeString , lineNumber ) }
145+ title = { copiedIndex === lineNumber ? 'Copied!' : 'Copy code' }
146+ >
147+ < svg
148+ width = "16"
149+ height = "16"
150+ viewBox = "0 0 24 24"
151+ fill = "none"
152+ stroke = "currentColor"
153+ strokeWidth = "2"
154+ strokeLinecap = "round"
155+ strokeLinejoin = "round"
156+ >
157+ { copiedIndex === lineNumber ? (
158+ < path d = "M20 6L9 17l-5-5" />
159+ ) : (
160+ < >
161+ < rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" />
162+ < path d = "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
163+ </ >
164+ ) }
165+ </ svg >
166+ </ button >
167+ </ div >
168+ < div className = "syntax-highlighter-wrapper" >
169+ < SyntaxHighlighter
170+ style = { oneDark }
171+ language = { match [ 1 ] }
172+ PreTag = "div"
173+ { ...props }
174+ customStyle = { {
175+ margin : 0 ,
176+ background : '#282c34' ,
177+ padding : '16px' ,
178+ } }
179+ >
180+ { codeString }
181+ </ SyntaxHighlighter >
182+ </ div >
183+ </ div >
184+ ) ;
185+ }
186+ return (
187+ < code className = { className } { ...props } >
188+ { children }
189+ </ code >
190+ ) ;
191+ } ,
192+ } }
204193 >
205- { copiedMessage ? (
206- < path d = "M20 6L9 17l-5-5" />
207- ) : (
208- < >
209- < rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" />
210- < path d = "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
211- </ >
212- ) }
213- </ svg >
214- </ button >
215- </ div >
194+ { text }
195+ </ ReactMarkdown >
196+ ) }
197+ { ! isUser && (
198+ < div className = "message-actions" >
199+ < button
200+ className = "message-copy-button"
201+ onClick = { copyMessage }
202+ title = { copiedMessage ? 'Copied!' : 'Copy response' }
203+ >
204+ < svg
205+ width = "16"
206+ height = "16"
207+ viewBox = "0 0 24 24"
208+ fill = "none"
209+ stroke = "currentColor"
210+ strokeWidth = "2"
211+ strokeLinecap = "round"
212+ strokeLinejoin = "round"
213+ >
214+ { copiedMessage ? (
215+ < path d = "M20 6L9 17l-5-5" />
216+ ) : (
217+ < >
218+ < rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" />
219+ < path d = "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
220+ </ >
221+ ) }
222+ </ svg >
223+ </ button >
224+ </ div >
225+ ) }
226+ </ >
216227 ) }
217228 </ div >
218229 { selectedImage && (
0 commit comments