Skip to content

Commit 2117ca0

Browse files
committed
add fform
1 parent 806cb75 commit 2117ca0

File tree

7 files changed

+432
-3
lines changed

7 files changed

+432
-3
lines changed

apps/agent-elements/components/demo-nav.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ const demoCategories = [
1212
icon: '/icons/agents.svg',
1313
},
1414
{
15-
name: 'Transcription',
16-
slug: 'transcription',
15+
name: 'Transcriber',
16+
slug: 'transcriber',
17+
icon: '/icons/speech-to-text.svg',
18+
},
19+
{
20+
name: 'Form Transcriber',
21+
slug: 'form-transcriber',
1722
icon: '/icons/speech-to-text.svg',
1823
},
1924
] as const;

apps/agent-elements/components/demo-showcase.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import SpeakerO1 from '@/registry/agent-elements/blocks/speaker-01';
55
import { DemoNav, type DemoCategory } from '@/components/demo-nav';
66
import Transcriber01 from '@/registry/agent-elements/blocks/transcriber-01/page';
77
import Agents01 from '@/registry/agent-elements/blocks/agents-01';
8+
import FormTranscriber01 from '@/registry/agent-elements/blocks/form-transcriber-01/page';
89

910
export function DemoShowcase() {
1011
const [activeCategory, setActiveCategory] = useState<DemoCategory>('music');
@@ -13,8 +14,10 @@ export function DemoShowcase() {
1314
switch (activeCategory) {
1415
case 'music':
1516
return <SpeakerO1 />;
16-
case 'transcription':
17+
case 'transcriber':
1718
return <Transcriber01 />;
19+
case 'form-transcriber':
20+
return <FormTranscriber01 />;
1821
case 'agents':
1922
return <Agents01 />;
2023
}

apps/agent-elements/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"react": "^19.1.1",
5353
"react-day-picker": "^9.7.0",
5454
"react-dom": "^19.1.1",
55+
"react-hook-form": "^7.62.0",
5556
"rehype-pretty-code": "^0.14.1",
5657
"rimraf": "^6.0.1",
5758
"shadcn": "3.2.1",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use server';
2+
3+
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js';
4+
import { SpeechToTextChunkResponseModel } from '@elevenlabs/elevenlabs-js/api/types/SpeechToTextChunkResponseModel';
5+
import { generateText } from 'ai';
6+
7+
export interface TranscriptionResult {
8+
text?: string;
9+
error?: string;
10+
transcriptionTime?: number;
11+
}
12+
export type TranscribeAudioInput = {
13+
audio: File;
14+
enhance?: boolean;
15+
};
16+
17+
export async function transcribeAudio({
18+
audio,
19+
enhance = false,
20+
}: TranscribeAudioInput): Promise<TranscriptionResult> {
21+
try {
22+
if (!audio) {
23+
return { error: 'No audio file provided' };
24+
}
25+
26+
const apiKey = process.env.ELEVENLABS_API_KEY;
27+
if (!process.env.ELEVENLABS_API_KEY) {
28+
return { error: 'Service not configured' };
29+
}
30+
31+
const client = new ElevenLabsClient({ apiKey });
32+
const audioBuffer = await audio.arrayBuffer();
33+
34+
const file = new File([audioBuffer], audio.name || 'audio.webm', {
35+
type: audio.type || 'audio/webm',
36+
});
37+
38+
const startTime = Date.now();
39+
const transcriptionResult = await client.speechToText.convert({
40+
file,
41+
modelId: 'scribe_v1',
42+
languageCode: 'en',
43+
});
44+
const transcriptionTime = Date.now() - startTime;
45+
46+
const rawText = (transcriptionResult as SpeechToTextChunkResponseModel)
47+
.text;
48+
49+
if (!rawText) {
50+
return { error: 'No transcription available' };
51+
}
52+
53+
if (!enhance) {
54+
return { text: rawText, transcriptionTime };
55+
}
56+
57+
const { text: enhancedText } = await generateText({
58+
model: 'openai/gpt-5-nano',
59+
system: `
60+
You are an enhancement layer for automatic speech transcription.
61+
62+
Your job:
63+
1) If the transcript is natural speech, CLEAN it (punctuation, casing, typos, disfluencies) without changing meaning.
64+
2) If the transcript is a DIRECT INSTRUCTION (imperative like "write/explain/summarize", or a question expecting an answer), EXECUTE it and return ONLY the requested content.
65+
66+
Decision rules:
67+
- EXECUTE when the speaker is clearly asking you to produce something (e.g., “Write a haiku…”, “Summarize this…”, “Explain X…”, “Translate into French…”, “List three reasons…”).
68+
- CLEAN when the text is chit-chat, notes, or narration (e.g., “hello world how are you…”, “today I met with…”).
69+
- If the instruction is quoted or meta (e.g., “Then I said, ‘write me a haiku’”), CLEAN rather than execute.
70+
- If safety policies would be violated, return a brief refusal: “I can’t help with that.”
71+
72+
CLEAN mode rules:
73+
- Fix obvious transcription errors and typos.
74+
- Add punctuation and capitalization.
75+
- Split run-ons; remove filler words (“um”, “uh”) unless meaningful.
76+
- Preserve technical terms and the original meaning.
77+
- Keep the conversational tone if present.
78+
79+
EXECUTE mode rules:
80+
- Return ONLY the requested content—no prefaces, no apologies, no extra commentary.
81+
- Use the minimal appropriate format (plain text, bullet list if asked, code block only if explicitly requested or clearly implied).
82+
- Be concise and accurate.
83+
84+
General:
85+
- Do not add facts not present or requested.
86+
- Keep proper nouns accurate.
87+
- Output must be a single final answer with no explanations.`,
88+
prompt: rawText,
89+
});
90+
91+
return { text: enhancedText, transcriptionTime };
92+
} catch (error) {
93+
console.error('Transcription error:', error);
94+
return {
95+
error:
96+
error instanceof Error ? error.message : 'Failed to transcribe audio',
97+
};
98+
}
99+
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
'use client';
2+
3+
import { zodResolver } from '@hookform/resolvers/zod';
4+
import { Button } from '@elevenlabs/ui/components/button';
5+
import {
6+
Card,
7+
CardContent,
8+
CardDescription,
9+
CardHeader,
10+
CardTitle,
11+
} from '@elevenlabs/ui/components/card';
12+
import {
13+
Form,
14+
FormControl,
15+
FormDescription,
16+
FormField,
17+
FormItem,
18+
FormLabel,
19+
FormMessage,
20+
} from '@elevenlabs/ui/components/form';
21+
import { Input } from '@elevenlabs/ui/components/input';
22+
import { Textarea } from '@elevenlabs/ui/components/textarea';
23+
import { cn } from '@/lib/utils';
24+
import { useForm } from 'react-hook-form';
25+
import {
26+
personaFormSchema,
27+
type PersonaFormValues,
28+
} from '../schema/persona-form.schema';
29+
30+
export function PersonaForm({
31+
className,
32+
...props
33+
}: React.ComponentProps<'div'>) {
34+
const form = useForm<PersonaFormValues>({
35+
resolver: zodResolver(personaFormSchema),
36+
defaultValues: {
37+
firstName: '',
38+
lastName: '',
39+
dateOfBirth: '',
40+
email: '',
41+
phone: '',
42+
description: '',
43+
occupation: '',
44+
interests: '',
45+
},
46+
});
47+
48+
function onSubmit(values: PersonaFormValues) {
49+
console.log('Form submitted with values:', values);
50+
// Handle form submission here
51+
}
52+
53+
return (
54+
<div className={cn('flex flex-col gap-6', className)} {...props}>
55+
<Card>
56+
<CardHeader>
57+
<CardTitle>Create Your Persona</CardTitle>
58+
<CardDescription>
59+
Fill out your personal information to create your profile. All
60+
fields marked with * are required.
61+
</CardDescription>
62+
</CardHeader>
63+
<CardContent>
64+
<Form {...form}>
65+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
66+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
67+
<FormField
68+
control={form.control}
69+
name="firstName"
70+
render={({ field }) => (
71+
<FormItem>
72+
<FormLabel>First Name *</FormLabel>
73+
<FormControl>
74+
<Input placeholder="John" {...field} />
75+
</FormControl>
76+
<FormMessage />
77+
</FormItem>
78+
)}
79+
/>
80+
<FormField
81+
control={form.control}
82+
name="lastName"
83+
render={({ field }) => (
84+
<FormItem>
85+
<FormLabel>Last Name *</FormLabel>
86+
<FormControl>
87+
<Input placeholder="Doe" {...field} />
88+
</FormControl>
89+
<FormMessage />
90+
</FormItem>
91+
)}
92+
/>
93+
</div>
94+
95+
<FormField
96+
control={form.control}
97+
name="dateOfBirth"
98+
render={({ field }) => (
99+
<FormItem>
100+
<FormLabel>Date of Birth *</FormLabel>
101+
<FormControl>
102+
<Input
103+
type="date"
104+
{...field}
105+
max={
106+
new Date(
107+
new Date().getFullYear() - 13,
108+
new Date().getMonth(),
109+
new Date().getDate(),
110+
)
111+
.toISOString()
112+
.split('T')[0]
113+
}
114+
min={
115+
new Date(
116+
new Date().getFullYear() - 120,
117+
new Date().getMonth(),
118+
new Date().getDate(),
119+
)
120+
.toISOString()
121+
.split('T')[0]
122+
}
123+
/>
124+
</FormControl>
125+
<FormDescription>
126+
You must be at least 13 years old
127+
</FormDescription>
128+
<FormMessage />
129+
</FormItem>
130+
)}
131+
/>
132+
133+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
134+
<FormField
135+
control={form.control}
136+
name="email"
137+
render={({ field }) => (
138+
<FormItem>
139+
<FormLabel>Email *</FormLabel>
140+
<FormControl>
141+
<Input
142+
type="email"
143+
placeholder="john.doe@example.com"
144+
{...field}
145+
/>
146+
</FormControl>
147+
<FormMessage />
148+
</FormItem>
149+
)}
150+
/>
151+
<FormField
152+
control={form.control}
153+
name="phone"
154+
render={({ field }) => (
155+
<FormItem>
156+
<FormLabel>Phone Number</FormLabel>
157+
<FormControl>
158+
<Input
159+
type="tel"
160+
placeholder="+1 (555) 123-4567"
161+
{...field}
162+
/>
163+
</FormControl>
164+
<FormMessage />
165+
</FormItem>
166+
)}
167+
/>
168+
</div>
169+
170+
<FormField
171+
control={form.control}
172+
name="occupation"
173+
render={({ field }) => (
174+
<FormItem>
175+
<FormLabel>Occupation *</FormLabel>
176+
<FormControl>
177+
<Input placeholder="Software Engineer" {...field} />
178+
</FormControl>
179+
<FormMessage />
180+
</FormItem>
181+
)}
182+
/>
183+
184+
<FormField
185+
control={form.control}
186+
name="description"
187+
render={({ field }) => (
188+
<FormItem>
189+
<FormLabel>Describe Yourself *</FormLabel>
190+
<FormControl>
191+
<Textarea
192+
placeholder="Tell us about yourself, your background, goals, and what makes you unique..."
193+
className="min-h-[120px] resize-none"
194+
{...field}
195+
/>
196+
</FormControl>
197+
<FormDescription>
198+
{field.value?.length || 0}/500 characters (minimum 50)
199+
</FormDescription>
200+
<FormMessage />
201+
</FormItem>
202+
)}
203+
/>
204+
205+
<FormField
206+
control={form.control}
207+
name="interests"
208+
render={({ field }) => (
209+
<FormItem>
210+
<FormLabel>Interests & Hobbies</FormLabel>
211+
<FormControl>
212+
<Textarea
213+
placeholder="Share your interests, hobbies, or activities you enjoy..."
214+
className="min-h-[80px] resize-none"
215+
{...field}
216+
/>
217+
</FormControl>
218+
<FormDescription>
219+
Optional - Tell us what you're passionate about
220+
</FormDescription>
221+
<FormMessage />
222+
</FormItem>
223+
)}
224+
/>
225+
226+
<div className="flex justify-end gap-4">
227+
<Button
228+
type="button"
229+
variant="outline"
230+
onClick={() => form.reset()}
231+
>
232+
Clear Form
233+
</Button>
234+
<Button type="submit">Create Persona</Button>
235+
</div>
236+
</form>
237+
</Form>
238+
</CardContent>
239+
</Card>
240+
<div className="text-muted-foreground text-center text-xs text-balance">
241+
By creating a persona, you agree to our{' '}
242+
<a href="#" className="underline underline-offset-4 hover:text-primary">
243+
Terms of Service
244+
</a>{' '}
245+
and{' '}
246+
<a href="#" className="underline underline-offset-4 hover:text-primary">
247+
Privacy Policy
248+
</a>
249+
.
250+
</div>
251+
</div>
252+
);
253+
}

0 commit comments

Comments
 (0)