Skip to content

Commit f669c37

Browse files
prakriti-solankeyvasanthasaikallurikartikpersistent
authored andcommitted
speech and copy (#422)
* speech and copy * startTime * added chunk properties * tooltips --------- Co-authored-by: vasanthasaikalluri <165021735+vasanthasaikalluri@users.noreply.github.com> Co-authored-by: kartikpersistent <101251502+kartikpersistent@users.noreply.github.com>
1 parent 4c2d767 commit f669c37

20 files changed

+295
-128
lines changed

backend/src/chunkid_entities.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,27 @@ def process_record(record, elements_data):
5454
logging.error(f"chunkid_entities module: An error occurred while extracting the nodes and relationships from records: {e}")
5555

5656

57+
def time_to_seconds(time_str):
58+
h, m, s = map(int, time_str.split(':'))
59+
return h * 3600 + m * 60 + s
60+
5761
def process_chunk_data(chunk_data):
5862
"""
5963
Processes a record to extract chunk_text
6064
"""
6165
try:
62-
chunk_properties = list()
66+
required_doc_properties = ["fileSource", "fileType", "url"]
67+
chunk_properties = []
68+
6369
for record in chunk_data:
64-
chunks = record["chunks"]
65-
for chunk in chunks:
70+
doc_properties = {prop: record["doc"].get(prop, None) for prop in required_doc_properties}
71+
for chunk in record["chunks"]:
72+
chunk.update(doc_properties)
73+
if chunk["fileSource"] == "youtube":
74+
chunk["start_time"] = time_to_seconds(chunk["start_time"])
75+
chunk["end_time"] = time_to_seconds(chunk["end_time"])
6676
chunk_properties.append(chunk)
77+
6778
return chunk_properties
6879
except Exception as e:
6980
logging.error(f"chunkid_entities module: An error occurred while extracting the Chunk text from records: {e}")

frontend/src/HOC/CustomModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Banner, Button, Dialog } from '@neo4j-ndl/react';
22
import { CustomModalProps } from '../types';
3+
import { buttonCaptions } from '../utils/Constants';
34

45
const CustomModal: React.FC<CustomModalProps> = ({
56
open,
67
onClose,
78
children,
8-
submitLabel = 'Submit',
9+
submitLabel = buttonCaptions.submit,
910
submitHandler,
1011
statusMessage,
1112
status,
@@ -35,7 +36,7 @@ const CustomModal: React.FC<CustomModalProps> = ({
3536
<div className='n-flex n-flex-row n-flex-wrap'>{children}</div>
3637
<Dialog.Actions className='mt-4'>
3738
<Button color='neutral' fill='outlined' onClick={onClose} size='medium'>
38-
Cancel
39+
{buttonCaptions.cancel}
3940
</Button>
4041
<Button onClick={submitHandler} size='medium' disabled={isDisabled}>
4142
{submitLabel}

frontend/src/components/Chatbot.tsx

Lines changed: 105 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import 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';
104
import ChatBotAvatar from '../assets/images/chatbot-ai.png';
115
import { ChatbotProps, Source, UserCredentials } from '../types';
126
import { useCredentials } from '../context/UserCredentials';
@@ -17,7 +11,9 @@ import InfoModal from './InfoModal';
1711
import clsx from 'clsx';
1812
import ReactMarkdown from 'react-markdown';
1913
import IconButtonWithToolTip from './IconButtonToolTip';
20-
// import { tooltips } from '../utils/Constants';
14+
import { buttonCaptions, tooltips } from '../utils/Constants';
15+
import useSpeechSynthesis from '../hooks/useSpeech';
16+
2117
const 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

frontend/src/components/ConnectionModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } f
33
import connectAPI from '../services/ConnectAPI';
44
import { useCredentials } from '../context/UserCredentials';
55
import { useSearchParams } from 'react-router-dom';
6+
import { buttonCaptions } from '../utils/Constants';
67

78
interface Message {
89
type: 'success' | 'info' | 'warning' | 'danger' | 'unknown';
@@ -190,7 +191,7 @@ export default function ConnectionModal({ open, setOpenConnection, setConnection
190191
<div className='n-flex max-h-44'>
191192
<Dropzone
192193
isTesting={false}
193-
customTitle={<>Drop your neo4j credentials file here</>}
194+
customTitle={<>{buttonCaptions.dropYourCreds}</>}
194195
className='n-p-6 end-0 top-0 w-full h-full'
195196
acceptedFileExtensions={['.txt', '.env']}
196197
dropZoneOptions={{
@@ -275,7 +276,7 @@ export default function ConnectionModal({ open, setOpenConnection, setConnection
275276
</div>
276277
</div>
277278
<Button loading={isLoading} disabled={isDisabled} onClick={() => submitConnection()}>
278-
Connect
279+
{buttonCaptions.connect}
279280
</Button>
280281
</Dialog.Content>
281282
</Dialog>

0 commit comments

Comments
 (0)