| 'use client'; | |
| import { useActionState, useEffect, useRef, useState, type FC } from 'react'; | |
| import { useFormStatus } from 'react-dom'; | |
| import { convertTextToSpeech, type TtsState } from '@/app/actions'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |
| import { Textarea } from '@/components/ui/textarea'; | |
| import { useToast } from '@/hooks/use-toast'; | |
| import { Volume2, LoaderCircle, Copy, Mic } from 'lucide-react'; | |
| const WindowControls = () => ( | |
| <div className="flex space-x-1.5"> | |
| <div className="w-3 h-3 rounded-full bg-muted-foreground/50"></div> | |
| <div className="w-3 h-3 rounded-full bg-muted-foreground/50"></div> | |
| <div className="w-3 h-3 rounded-full bg-muted-foreground/50"></div> | |
| </div> | |
| ); | |
| interface TtsFormProps { | |
| examplePhrases: string[]; | |
| } | |
| function SubmitButton() { | |
| const { pending } = useFormStatus(); | |
| return ( | |
| <Button type="submit" disabled={pending} className="w-full" size="lg"> | |
| {pending ? ( | |
| <> | |
| <LoaderCircle className="animate-spin" /> | |
| Generating... | |
| </> | |
| ) : ( | |
| <> | |
| <Volume2 /> | |
| Generate Speech | |
| </> | |
| )} | |
| </Button> | |
| ); | |
| } | |
| export const TtsForm: FC<TtsFormProps> = ({ examplePhrases }) => { | |
| const initialState: TtsState = { success: false }; | |
| const [state, formAction] = useActionState(convertTextToSpeech, initialState); | |
| const { toast } = useToast(); | |
| const formRef = useRef<HTMLFormElement>(null); | |
| const [text, setText] = useState(''); | |
| useEffect(() => { | |
| if (state.error) { | |
| toast({ | |
| variant: 'destructive', | |
| title: 'Oops! Something went wrong.', | |
| description: state.error, | |
| }); | |
| } | |
| }, [state, toast]); | |
| const handleExampleClick = (phrase: string) => { | |
| setText(phrase); | |
| }; | |
| const handleCopyUrl = (url: string) => { | |
| navigator.clipboard.writeText(url).then(() => { | |
| toast({ | |
| title: 'Copied to clipboard!', | |
| description: 'Audio URL is ready to be shared.', | |
| }); | |
| }); | |
| }; | |
| return ( | |
| <div className="space-y-4 md:space-y-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Text Input</CardTitle> | |
| <WindowControls /> | |
| </CardHeader> | |
| <CardContent> | |
| <form ref={formRef} action={formAction} className="space-y-4 pt-4"> | |
| <Textarea | |
| name="text" | |
| placeholder="इहाँ अपन छत्तीसगढ़ी सब्द लिखव..." | |
| className="min-h-[100px] sm:min-h-[140px] resize-none" | |
| value={text} | |
| onChange={(e) => setText(e.target.value)} | |
| required | |
| /> | |
| <SubmitButton /> | |
| </form> | |
| </CardContent> | |
| </Card> | |
| {state.success && state.audioUrl && ( | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Audio Output</CardTitle> | |
| <WindowControls /> | |
| </CardHeader> | |
| <CardContent className="space-y-4 pt-4"> | |
| <audio key={state.key} controls src={state.audioUrl} className="w-full"> | |
| Your browser does not support the audio element. | |
| </audio> | |
| <Button | |
| variant="outline" | |
| className="w-full" | |
| onClick={() => handleCopyUrl(state.audioUrl!)} | |
| > | |
| <Copy /> | |
| Copy Audio URL | |
| </Button> | |
| </CardContent> | |
| </Card> | |
| )} | |
| {examplePhrases.length > 0 && ( | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Examples</CardTitle> | |
| <WindowControls /> | |
| </CardHeader> | |
| <CardContent className="pt-4"> | |
| <div className="flex flex-wrap gap-2"> | |
| {examplePhrases.map((phrase, index) => ( | |
| <Button | |
| key={index} | |
| variant="secondary" | |
| size="sm" | |
| className="h-auto items-start text-left font-body font-normal whitespace-normal text-xs sm:text-sm" | |
| onClick={() => handleExampleClick(phrase)}> | |
| <Mic className="mr-1 mt-0.5 h-4 w-4 shrink-0" /> | |
| <span>{phrase}</span> | |
| </Button> | |
| ))} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| </div> | |
| ); | |
| }; | |