11import React , { useEffect , useRef , useState } from 'react' ;
2- import { Button , Widget , Typography , Avatar , TextInput , IconButton , Modal } from '@neo4j-ndl/react' ;
3- import {
4- InformationCircleIconOutline ,
5- XMarkIconOutline ,
6- // ClipboardDocumentIconOutline,
7- // SpeakerWaveIconOutline,
8- // SpeakerXMarkIconOutline,
9- } from '@neo4j-ndl/react/icons' ;
2+ import { Button , Widget , Typography , Avatar , TextInput , IconButton , Modal , useCopyToClipboard } from '@neo4j-ndl/react' ;
3+ import { InformationCircleIconOutline , XMarkIconOutline , ClipboardDocumentIconOutline , SpeakerWaveIconOutline , SpeakerXMarkIconOutline } from '@neo4j-ndl/react/icons' ;
104import ChatBotAvatar from '../assets/images/chatbot-ai.png' ;
115import { ChatbotProps , Source , UserCredentials } from '../types' ;
126import { useCredentials } from '../context/UserCredentials' ;
@@ -17,7 +11,9 @@ import InfoModal from './InfoModal';
1711import clsx from 'clsx' ;
1812import ReactMarkdown from 'react-markdown' ;
1913import IconButtonWithToolTip from './IconButtonToolTip' ;
20- // import { tooltips } from '../utils/Constants';
14+ import { buttonCaptions , tooltips } from '../utils/Constants' ;
15+ import useSpeechSynthesis from '../hooks/useSpeech' ;
16+
2117const Chatbot : React . FC < ChatbotProps > = ( props ) => {
2218 const { messages : listMessages , setMessages : setListMessages , isLoading, isFullScreen } = props ;
2319 const [ inputMessage , setInputMessage ] = useState ( '' ) ;
@@ -32,8 +28,16 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
3228 const [ responseTime , setResponseTime ] = useState < number > ( 0 ) ;
3329 const [ chunkModal , setChunkModal ] = useState < string [ ] > ( [ ] ) ;
3430 const [ tokensUsed , setTokensUsed ] = useState < number > ( 0 ) ;
35- // const [copyMessage, setCopyMessage] = useState<string>('');
36- // const [speaking, setSpeaking] = useState<boolean>(false);
31+ const [ copyMessageId , setCopyMessageId ] = useState < number | null > ( null ) ;
32+
33+ const [ value , copy ] = useCopyToClipboard ( ) ;
34+ const { speak, cancel, supported } = useSpeechSynthesis ( {
35+ onEnd : ( ) => {
36+ setListMessages ( ( msgs ) =>
37+ msgs . map ( ( msg ) => ( { ...msg , speaking : false } ) )
38+ ) ;
39+ } ,
40+ } ) ;
3741
3842 const handleInputChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
3943 setInputMessage ( e . target . value ) ;
@@ -53,6 +57,8 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
5357 chunk_ids ?: string [ ] ;
5458 total_tokens ?: number ;
5559 response_time ?: number ;
60+ speaking ?: boolean ;
61+ copying ?: boolean
5662 } ,
5763 index = 0
5864 ) => {
@@ -77,6 +83,8 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
7783 chunks : response ?. chunk_ids ,
7884 total_tokens : response . total_tokens ,
7985 response_time : response ?. response_time ,
86+ speaking : false ,
87+ copying : false ,
8088 } ,
8189 ] ) ;
8290 } else {
@@ -93,6 +101,8 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
93101 lastmsg . chunk_ids = response ?. chunk_ids ;
94102 lastmsg . total_tokens = response ?. total_tokens ;
95103 lastmsg . response_time = response ?. response_time ;
104+ lastmsg . speaking = false ;
105+ lastmsg . copying = false ;
96106 return msgs . map ( ( msg , index ) => {
97107 if ( index === msgs . length - 1 ) {
98108 return lastmsg ;
@@ -110,6 +120,7 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
110120 }
111121 } ;
112122 let date = new Date ( ) ;
123+
113124 const handleSubmit = async ( e : { preventDefault : ( ) => void } ) => {
114125 e . preventDefault ( ) ;
115126 if ( ! inputMessage . trim ( ) ) {
@@ -129,20 +140,24 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
129140 simulateTypingEffect ( { reply : ' ' } ) ;
130141 const chatbotAPI = await chatBotAPI ( userCredentials as UserCredentials , inputMessage , sessionId , model ) ;
131142 const chatresponse = chatbotAPI ?. response ;
143+ console . log ( 'api' , chatresponse ) ;
132144 chatbotReply = chatresponse ?. data ?. data ?. message ;
133145 chatSources = chatresponse ?. data ?. data ?. info . sources ;
134146 chatModel = chatresponse ?. data ?. data ?. info . model ;
135147 chatChunks = chatresponse ?. data ?. data ?. info . chunkids ;
136148 chatTokensUsed = chatresponse ?. data ?. data ?. info . total_tokens ;
137149 chatTimeTaken = chatresponse ?. data ?. data ?. info . response_time ;
138- simulateTypingEffect ( {
150+ const finalbotReply = {
139151 reply : chatbotReply ,
140152 sources : chatSources ,
141153 model : chatModel ,
142154 chunk_ids : chatChunks ,
143155 total_tokens : chatTokensUsed ,
144156 response_time : chatTimeTaken ,
145- } ) ;
157+ speaking : false ,
158+ copying : false ,
159+ } ;
160+ simulateTypingEffect ( finalbotReply ) ;
146161 } catch ( error ) {
147162 chatbotReply = "Oops! It seems we couldn't retrieve the answer. Please try again later" ;
148163 setInputMessage ( '' ) ;
@@ -155,33 +170,55 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
155170 useEffect ( ( ) => {
156171 scrollToBottom ( ) ;
157172 } , [ listMessages ] ) ;
173+
158174 useEffect ( ( ) => {
159175 setLoading ( ( ) => listMessages . some ( ( msg ) => msg . isLoading || msg . isTyping ) ) ;
160176 } , [ listMessages ] ) ;
161177
162- // const handleCopy = async (text: string) => {
163- // try {
164- // await navigator.clipboard.writeText(text);
165- // setCopyMessage('copied!');
166- // setTimeout(() => setCopyMessage(''), 2000);
167- // } catch (error) {
168- // console.error('Failed to copy text: ', error);
169- // }
170- // };
178+ const handleCopy = ( message : string , id : number ) => {
179+ copy ( message ) ;
180+ setListMessages ( ( msgs ) =>
181+ msgs . map ( ( msg ) => {
182+ if ( msg . id === id ) {
183+ msg . copying = true ;
184+ }
185+ return msg ;
186+ } )
187+ ) ;
188+ setCopyMessageId ( id ) ;
189+ setTimeout ( ( ) => {
190+ setCopyMessageId ( null ) ;
191+ setListMessages ( ( msgs ) =>
192+ msgs . map ( ( msg ) => {
193+ if ( msg . id === id ) {
194+ msg . copying = false ;
195+ }
196+ return msg ;
197+ } )
198+ ) ;
199+ } , 2000 ) ;
200+ } ;
201+
202+ const handleCancel = ( id : number ) => {
203+ cancel ( ) ;
204+ setListMessages ( ( msgs ) =>
205+ msgs . map ( ( msg ) =>
206+ ( msg . id
207+ === id ? { ...msg , speaking : false } : msg )
208+ )
209+ ) ;
210+ }
171211
172- // const handleSpeak = (text: string) => {
173- // if (speaking) {
174- // window.speechSynthesis.cancel();
175- // setSpeaking(false);
176- // } else {
177- // const utterance = new SpeechSynthesisUtterance(text);
178- // utterance.onend = () => {
179- // setSpeaking(false);
180- // };
181- // window.speechSynthesis.speak(utterance);
182- // setSpeaking(true);
183- // }
184- // };
212+ const handleSpeak = ( chatMessage : any , id : number ) => {
213+ speak ( { text : chatMessage } ) ;
214+ setListMessages ( ( msgs ) => {
215+ const messageWithSpeaking = msgs . find ( msg => msg . speaking ) ;
216+ return msgs . map ( ( msg ) =>
217+ ( msg . id
218+ === id && ! messageWithSpeaking ? { ...msg , speaking : true } : msg )
219+ ) ;
220+ } ) ;
221+ }
185222
186223 return (
187224 < div className = 'n-bg-palette-neutral-bg-weak flex flex-col justify-between min-h-full max-h-full overflow-hidden' >
@@ -224,16 +261,14 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
224261 < Widget
225262 header = ''
226263 isElevated = { true }
227- className = { `p-4 self-start ${ isFullScreen ? 'max-w-[55%]' : '' } ${
228- chat . user === 'chatbot' ? 'n-bg-palette-neutral-bg-strong' : 'n-bg-palette-primary-bg-weak'
229- } `}
264+ className = { `p-4 self-start ${ isFullScreen ? 'max-w-[55%]' : '' } ${ chat . user === 'chatbot' ? 'n-bg-palette-neutral-bg-strong' : 'n-bg-palette-primary-bg-weak'
265+ } `}
230266 >
231267 < div
232- className = { `${
233- listMessages [ index ] . isLoading && index === listMessages . length - 1 && chat . user == 'chatbot'
234- ? 'loader'
235- : ''
236- } `}
268+ className = { `${ listMessages [ index ] . isLoading && index === listMessages . length - 1 && chat . user == 'chatbot'
269+ ? 'loader'
270+ : ''
271+ } `}
237272 >
238273 < ReactMarkdown > { chat . message } </ ReactMarkdown >
239274 </ div >
@@ -262,40 +297,50 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
262297 >
263298 < InformationCircleIconOutline className = 'w-4 h-4 inline-block' />
264299 </ IconButtonWithToolTip >
265- { /* <IconButtonWithToolTip
300+ < IconButtonWithToolTip
266301 label = 'copy text'
267302 placement = 'top'
268303 clean
269- text={copyMessage ? tooltips.copied : tooltips.copy}
270- onClick={() => handleCopy(chat.message)}
304+ text = { chat . copying ? tooltips . copied : tooltips . copy }
305+ onClick = { ( ) => handleCopy ( chat . message , chat . id ) }
271306 disabled = { chat . isTyping || chat . isLoading }
272307 >
273308 < ClipboardDocumentIconOutline className = 'w-4 h-4 inline-block' />
274309 </ IconButtonWithToolTip >
275- {copyMessage && <span className='pt-4 text-xs'>{copyMessage}</span>}
276- <IconButtonWithToolTip
310+ { copyMessageId === chat . id && (
311+ < > < span className = 'pt-4 text-xs' > Copied!</ span >
312+ < span style = { { display :'none' } } > { value } </ span > </ >
313+ ) }
314+ { supported && chat . speaking ? < IconButtonWithToolTip
277315 placement = 'top'
278316 label = 'text to speak'
279317 clean
280- text={speaking ? tooltips.stopSpeaking : tooltips.textTospeech }
281- onClick={() => handleSpeak(chat.message) }
318+ onClick = { ( ) => handleCancel ( chat . id ) }
319+ text = { chat . speaking ? tooltips . stopSpeaking : tooltips . textTospeech }
282320 disabled = { chat . isTyping || chat . isLoading }
283321 >
284- {speaking ? (
285- <SpeakerXMarkIconOutline className='w-4 h-4 inline-block' />
286- ) : (
287- <SpeakerWaveIconOutline className='w-4 h-4 inline-block' />
288- )}
289- </IconButtonWithToolTip> */ }
322+ < SpeakerXMarkIconOutline className = "w-4 h-4 inline-block" />
323+ </ IconButtonWithToolTip > :
324+ < IconButtonWithToolTip
325+ placement = 'top'
326+ clean
327+ onClick = { ( ) => handleSpeak ( chat . message , chat . id ) }
328+ text = { chat . speaking ? tooltips . stopSpeaking : tooltips . textTospeech }
329+ disabled = { chat . isTyping || chat . isLoading }
330+ label = 'speech'
331+ >
332+ < SpeakerWaveIconOutline className = "w-4 h-4 inline-block" />
333+ </ IconButtonWithToolTip >
334+ }
290335 </ div >
291336 ) }
292337 </ div >
293338 </ Widget >
294339 </ div >
295340 ) ) }
296341 </ div >
297- </ Widget >
298- </ div >
342+ </ Widget >
343+ </ div >
299344 < div className = 'n-bg-palette-neutral-bg-weak flex gap-2.5 bottom-0 p-2.5 w-full' >
300345 < form onSubmit = { handleSubmit } className = 'flex gap-2.5 w-full' >
301346 < TextInput
@@ -307,7 +352,7 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
307352 onChange = { handleInputChange }
308353 />
309354 < Button type = 'submit' disabled = { loading } >
310- Submit
355+ { buttonCaptions . submit }
311356 </ Button >
312357 </ form >
313358 </ div >
@@ -338,7 +383,7 @@ const Chatbot: React.FC<ChatbotProps> = (props) => {
338383 total_tokens = { tokensUsed }
339384 />
340385 </ Modal >
341- </ div >
386+ </ div >
342387 ) ;
343388} ;
344389
0 commit comments