Skip to content

Commit

Permalink
chore: added speech to text support!
Browse files Browse the repository at this point in the history
  • Loading branch information
Redskull-127 committed Jul 4, 2024
1 parent 33c73ba commit 0214f00
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 89 deletions.
Binary file modified bun.lockb
Binary file not shown.
208 changes: 127 additions & 81 deletions components/Dialogs/chat/ChatAI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,131 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { Icons } from '../../icons/icons';
import { Textarea } from '../../ui/textarea';
import { Button } from '../../ui/button';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ChatAPIMaker } from '@/lib/server/functions/chatapi';
import SpeechRecognition, {
useSpeechRecognition,
} from 'react-speech-recognition';
import { toast } from 'sonner';

const TestData = [
const initialMessageHistory = [
{
message:
'Hey there! You can ask me anything about Meer Tarbani. For example: Who is Meer Tarbani?, Skills, Projects, LinkedIn, Mail, etc.',
sender: 'bot',
},
];

export default function ChatAI() {
const useChatAI = () => {
const submitRef = useRef<HTMLButtonElement>(null);
const [pending, setPending] = useState(false);
const [messages, setMessages] = useState(TestData);
const [messages, setMessages] = useState(initialMessageHistory);
const [input, setInput] = useState('');
const bottomRef = useRef<HTMLDivElement>(null);
const [shouldScroll, setShouldScroll] = useState<boolean>(false);
const [shouldScroll, setShouldScroll] = useState(false);
const {
isMicrophoneAvailable,
listening,
resetTranscript,
browserSupportsSpeechRecognition,
transcript,
finalTranscript,
} = useSpeechRecognition();

useEffect(() => {
const timer = setTimeout(() => {
bottomRef.current?.scrollIntoView();
}, 500);
if (shouldScroll && bottomRef.current) {
timer;
const timer = setTimeout(() => bottomRef.current?.scrollIntoView(), 500);
return () => clearTimeout(timer);
}
return () => clearTimeout(timer);
}, [shouldScroll, messages]);

useEffect(() => {
const handleEnter = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
return submitRef.current?.click();
submitRef.current?.click();
}
};
window.addEventListener('keydown', handleEnter);
return () => window.removeEventListener('keydown', handleEnter);
}, []);

useEffect(() => {
if (finalTranscript.length > 0) {
setInput(finalTranscript);
}
if (finalTranscript === input) resetTranscript();
}, [input, finalTranscript, resetTranscript]);

const handleSpeech = useMemo(
() => async () => {
if (!isMicrophoneAvailable) {
return toast.warning('Microphone is not available');
}
if (!browserSupportsSpeechRecognition) {
return toast.warning(
'Your browser does not support speech recognition',
);
}
try {
if (!listening) {
await SpeechRecognition.startListening();
toast.info('Listening...');
} else {
await SpeechRecognition.stopListening();
toast.info('Stopped listening');
}
} catch (err: any) {
toast.error(err);
}
},
[isMicrophoneAvailable, browserSupportsSpeechRecognition, listening],
);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setPending(true);

const request = await ChatAPIMaker(input);
const newMessages = [
...messages,
{ message: input, sender: 'user' },
request.status === 'error'
? { message: 'Something went wrong, Please try again.', sender: 'bot' }
: { message: request.data?.message || 'I am a bot', sender: 'bot' },
];

setMessages(newMessages);
setInput('');
setPending(false);
};

return {
submitRef,
pending,
messages,
input,
setInput,
bottomRef,
setShouldScroll,
handleSpeech,
handleSubmit,
};
};

export default function ChatAI() {
const {
submitRef,
pending,
messages,
input,
setInput,
bottomRef,
setShouldScroll,
handleSpeech,
handleSubmit,
} = useChatAI();

return (
<Dialog>
<DialogTrigger asChild>
Expand All @@ -74,79 +159,30 @@ export default function ChatAI() {
</DialogHeader>

<ScrollArea
onLoad={() => {
setShouldScroll(true);
}}
className="flex flex-col min-h-[30vh] max-h-[50vh] w-full "
onLoad={() => setShouldScroll(true)}
className="flex flex-col min-h-[30vh] max-h-[50vh] w-full"
>
{messages.map((data, index) => {
return (
<div
key={index}
className={
data.sender === 'bot'
? `flex h-fit w-full justify-start items-start gap-3`
: `flex h-fit w-full float-end justify-end items-start gap-3 my-3`
}
>
{data.sender === 'bot' && (
<Icons.Bot className="h-5 max-w-5 my-3" />
)}
<h1 className="text-md p-3 border rounded-md max-w-96">
{data.message}
</h1>
{data.sender === 'user' && (
<Icons.User className="h-5 w-5 my-3" />
)}
</div>
);
})}
<div
onLoad={(e) => {
e.currentTarget.scrollIntoView({ behavior: 'smooth' });
}}
ref={bottomRef}
></div>
{messages.map((data, index) => (
<div
key={index}
className={`flex h-fit w-full justify-${data.sender === 'bot' ? 'start' : 'end'} items-start gap-3 my-3`}
>
{data.sender === 'bot' && (
<Icons.Bot className="h-5 max-w-5 my-3" />
)}
<h1 className="text-md p-3 border rounded-md max-w-96">
{data.message}
</h1>
{data.sender === 'user' && (
<Icons.User className="h-5 w-5 my-3" />
)}
</div>
))}
<div ref={bottomRef}></div>
</ScrollArea>
<form
onSubmit={async (e) => {
e.preventDefault();
setPending(true);
const request = await ChatAPIMaker(input);

if (request.status === 'error') {
setMessages([
...messages,
{ message: input, sender: 'user' },
{
message: 'Something went wrong, Please try again.',
sender: 'bot',
},
]);
setInput('');
setPending(false);
return;
}

if (request.status === 'success' && request.data?.message) {
setMessages([
...messages,
{ message: input, sender: 'user' },
{ message: request.data?.message, sender: 'bot' },
]);
setInput('');
setPending(false);
return;
}

setMessages([
...messages,
{ message: input, sender: 'user' },
{ message: 'I am a bot', sender: 'bot' },
]);
setInput('');
setPending(false);
}}
<form
onSubmit={handleSubmit}
className="w-full flex items-center gap-2"
>
<Textarea
Expand All @@ -160,6 +196,7 @@ export default function ChatAI() {
/>
<Button
ref={submitRef}
type="submit"
disabled={pending}
className="disabled:opacity-60"
>
Expand All @@ -169,6 +206,15 @@ export default function ChatAI() {
'Send'
)}
</Button>
<Button
type="button"
size="icon"
variant="ghost"
className="p-2"
onClick={handleSpeech}
>
<Icons.Mic className="size-5" />
</Button>
</form>
</DialogContent>
</Dialog>
Expand Down
2 changes: 2 additions & 0 deletions components/icons/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
TrendingUp,
Radio,
Disc,
Mic,
} from 'lucide-react';

export type LogoIcon = {
Expand All @@ -39,6 +40,7 @@ export const Icons = {
TrendingUp: TrendingUp,
Radio: Radio,
Disc: Disc,
Mic: Mic,
logo: (props: LogoIcon) => {
return (
<Image
Expand Down
20 changes: 12 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,13 @@
"postgres": "^3.4.4",
"posthog-js": "^1.142.1",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-speech-recognition": "^3.10.0",
"sharp": "^0.33.3",
"signals-react-safe": "^1.1.2",
"socket.io-client": "^4.7.5",
"sonner": "^1.4.41",
"sonner": "^1.5.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.0",
Expand All @@ -71,21 +72,24 @@
"devDependencies": {
"@types/bun": "^1.1.5",
"@types/node": "^20.12.7",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-speech-recognition": "^3.9.5",
"autoprefixer": "^10.4.19",
"drizzle-kit": "^0.20.16",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.2",
"husky": "^8.0.0",
"eslint-config-next": "^14.2.4",
"husky": "^8.0.3",
"lint-staged": "^15.2.7",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5"
},
"trustedDependencies": [
"@vercel/speed-insights",
"es5-ext"
"es5-ext",
"esbuild",
"sharp"
],
"lint-staged": {
"**/*.{ts,tsx,mdx,js,jsx,md,css,yaml,yml}": [
Expand Down

0 comments on commit 0214f00

Please sign in to comment.