11/* eslint-disable no-confusing-arrow */
22import { useEffect , useRef , useState } from 'react' ;
3+ import ReactMarkdown from 'react-markdown' ;
4+ import rehypeRaw from 'rehype-raw' ;
5+ import remarkGfm from 'remark-gfm' ;
36import {
47 Button ,
58 Widget ,
@@ -10,6 +13,8 @@ import {
1013 useCopyToClipboard ,
1114 Modal ,
1215 Drawer ,
16+ LoadingSpinner ,
17+ TextLink ,
1318} from '@neo4j-ndl/react' ;
1419
1520import ChatBotAvatar from '../assets/chatbot-ai.png' ;
@@ -86,6 +91,7 @@ export default function Chatbot(props: ChatbotProps) {
8691
8792 const [ typingMessageId , setTypingMessageId ] = useState < number | null > ( null ) ;
8893 const [ currentTypingText , setCurrentTypingText ] = useState < string > ( '' ) ;
94+ const [ isLoading , setIsLoading ] = useState < boolean > ( false ) ;
8995
9096 const handleCloseModal = ( ) => setIsOpenModal ( false ) ;
9197
@@ -129,30 +135,38 @@ export default function Chatbot(props: ChatbotProps) {
129135 } , 20 ) ;
130136 } ;
131137
132- const handleSubmit = ( e : { preventDefault : ( ) => void } ) => {
138+ const handleSubmit = async ( e : { preventDefault : ( ) => void } ) => {
133139 e . preventDefault ( ) ;
134140 if ( ! inputMessage . trim ( ) || ! currentSession ) {
135141 return ;
136142 }
137-
143+
138144 const date = new Date ( ) ;
139145 const datetime = `${ date . toLocaleDateString ( ) } ${ date . toLocaleTimeString ( ) } ` ;
140- const userMessage : ChatMessage = {
141- id : Date . now ( ) ,
142- user : 'user' ,
143- message : inputMessage ,
144- datetime : datetime ,
146+ const userMessage : ChatMessage = {
147+ id : Date . now ( ) ,
148+ user : 'user' ,
149+ message : inputMessage ,
150+ datetime : datetime
145151 } ;
146-
152+
147153 addMessageToCurrentSession ( userMessage ) ;
148154 setInputMessage ( '' ) ;
149155
156+ setIsLoading ( true ) ;
157+
158+ // Simulate API delay (~2 seconds)
159+ // This is where you would call your backend API to get the chatbot response
160+ await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
161+
150162 const chatbotReply = {
151163 response :
152164 'Hello, here is an example response with sources. To use the chatbot, plug this to your backend with a fetch containing an object response of type: {response: string, src: Array<string>}' ,
153165 src : [ '1:1234-abcd-efgh-ijkl-5678:2' , '3:8765-zyxw-vuts-rqpo-4321:4' ] ,
154166 } ; // Replace with getting a response from your chatbot through your APIs
155167
168+ setIsLoading ( false ) ;
169+
156170 simulateTypingEffect ( chatbotReply ) ;
157171 } ;
158172
@@ -162,7 +176,7 @@ export default function Chatbot(props: ChatbotProps) {
162176
163177 useEffect ( ( ) => {
164178 scrollToBottom ( ) ;
165- } , [ currentSession ?. messages , typingMessageId , currentTypingText ] ) ;
179+ } , [ currentSession ?. messages , typingMessageId , currentTypingText , isLoading ] ) ;
166180
167181 const handleNewSession = ( ) => {
168182 createNewSession ( ) ;
@@ -355,7 +369,6 @@ export default function Chatbot(props: ChatbotProps) {
355369 </ Typography >
356370 </ div >
357371 </ div >
358-
359372 < div className = 'flex-1 overflow-y-auto pb-6 n-bg-palette-neutral-bg-default' >
360373 < div className = 'flex flex-col gap-3 p-3 min-h-full' >
361374 { currentMessages . map ( ( chat ) => (
@@ -396,15 +409,22 @@ export default function Chatbot(props: ChatbotProps) {
396409 } `}
397410 >
398411 < div >
399- { chat . message . split ( / ` ( .+ ?) ` / ) . map ( ( part , index ) =>
400- index % 2 === 1 ? (
401- < span key = { index } style = { formattedTextStyle } >
402- { part }
403- </ span >
404- ) : (
405- part
406- )
407- ) }
412+ < ReactMarkdown
413+ components = { {
414+ code : ( { children } ) => (
415+ < span style = { formattedTextStyle } >
416+ { children }
417+ </ span >
418+ ) ,
419+ a : ( { ...props } ) => (
420+ < TextLink type = "external" href = { props . href } target = "_blank" > { props . children } </ TextLink >
421+ )
422+ } }
423+ remarkPlugins = { [ remarkGfm ] }
424+ rehypePlugins = { [ rehypeRaw ] }
425+ >
426+ { chat . message }
427+ </ ReactMarkdown >
408428 </ div >
409429 < div className = 'text-right align-bottom pt-3' >
410430 < Typography variant = 'body-small' > { chat . datetime } </ Typography >
@@ -449,6 +469,29 @@ export default function Chatbot(props: ChatbotProps) {
449469 </ div >
450470 ) ) }
451471
472+ { isLoading && (
473+ < div ref = { messagesEndRef } className = 'flex gap-2.5 items-end flex-row' >
474+ < div className = 'w-8 h-8 mr-4 ml-4' >
475+ < Avatar
476+ className = '-ml-4'
477+ hasStatus
478+ name = 'KM'
479+ size = 'x-large'
480+ source = { ChatBotAvatar }
481+ status = 'online'
482+ type = 'image'
483+ shape = 'square'
484+ />
485+ </ div >
486+ < Widget header = '' isElevated = { true } className = 'p-4 self-start max-w-[55%] n-bg-palette-neutral-bg-weak' >
487+ < div className = 'flex items-center gap-2' >
488+ < LoadingSpinner size = 'small' />
489+ < Typography variant = 'body-medium' > Thinking...</ Typography >
490+ </ div >
491+ </ Widget >
492+ </ div >
493+ ) }
494+
452495 { typingMessageId && currentTypingText && (
453496 < div ref = { messagesEndRef } className = 'flex gap-2.5 items-end flex-row' >
454497 < div className = 'w-8 h-8 mr-4 ml-4' >
@@ -465,16 +508,22 @@ export default function Chatbot(props: ChatbotProps) {
465508 </ div >
466509 < Widget header = '' isElevated = { true } className = 'p-4 self-start max-w-[55%] n-bg-palette-neutral-bg-weak' >
467510 < div >
468- { currentTypingText . split ( / ` ( .+ ?) ` / ) . map ( ( part , index ) =>
469- index % 2 === 1 ? (
470- < span key = { index } style = { formattedTextStyle } >
471- { part }
472- </ span >
473- ) : (
474- part
475- )
476- ) }
477- < span className = 'animate-pulse' > |</ span >
511+ < ReactMarkdown
512+ components = { {
513+ code : ( { children } ) => (
514+ < span style = { formattedTextStyle } >
515+ { children }
516+ </ span >
517+ ) ,
518+ a : ( { ...props } ) => (
519+ < TextLink type = "external" href = { props . href } target = "_blank" > { props . children } </ TextLink >
520+ )
521+ } }
522+ remarkPlugins = { [ remarkGfm ] }
523+ rehypePlugins = { [ rehypeRaw ] }
524+ >
525+ { currentTypingText }
526+ </ ReactMarkdown >
478527 </ div >
479528 < div className = 'text-right align-bottom pt-3' >
480529 < Typography variant = 'body-small' > Typing...</ Typography >
0 commit comments