├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── app ├── (sidebar-layout) │ ├── components │ │ └── [...slug] │ │ │ └── page.tsx │ ├── docs │ │ ├── [slug] │ │ │ └── page.tsx │ │ └── page.tsx │ └── layout.tsx ├── api │ ├── og │ │ └── route.tsx │ └── vapi │ │ ├── book-appointment │ │ └── route.tsx │ │ ├── check-availability │ │ └── route.tsx │ │ └── make-call │ │ └── route.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── mdx.css ├── opengraph-image.jpg ├── opengraph-image.png └── page.tsx ├── assets └── fonts │ └── Inter-Bold.ttf ├── components.json ├── components ├── code-block-wrapper.tsx ├── code-preview.tsx ├── component-preview.tsx ├── examples │ ├── 3d-orb.tsx │ ├── 3d-orb2.tsx │ ├── abstract-ball.tsx │ ├── bigblock.tsx │ ├── builder-demo.tsx │ ├── circlewaveform.tsx │ ├── coder.tsx │ ├── config-drawer.tsx │ ├── dock-example.tsx │ ├── dynamic-island.tsx │ ├── family-button.tsx │ ├── floaty.tsx │ ├── form.tsx │ ├── glitch.tsx │ ├── glob.tsx │ ├── logo-roll.tsx │ ├── minimal-component.tsx │ ├── outbound-dial.tsx │ ├── phone.tsx │ ├── reserved.tsx │ ├── scheduler.tsx │ ├── shine-card.tsx │ ├── shine.tsx │ ├── showcase-siri.tsx │ ├── siri.tsx │ ├── testimonials.tsx │ ├── transcribe.tsx │ ├── transcriber.tsx │ ├── visualizer.tsx │ └── waveform.tsx ├── footer.tsx ├── header.tsx ├── hero.tsx ├── icons.tsx ├── logos.tsx ├── mdx-components.tsx ├── mobile-nav.tsx ├── sidebar.tsx ├── theme-provider.tsx ├── theme-switcher.tsx └── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── dock.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── dynamic-island.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input-otp.tsx │ ├── input.tsx │ ├── label.tsx │ ├── menubar.tsx │ ├── navigation-menu.tsx │ ├── pagination.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── resizable.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── sparkle-text.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ ├── tooltip.tsx │ └── use-toast.ts ├── config └── site.ts ├── content ├── docs │ ├── changelog.mdx │ ├── creating-a-new-project.mdx │ ├── introduction.mdx │ ├── quickstart.mdx │ └── templates.mdx └── library │ ├── 3d-orb.mdx │ ├── circlewaveform.mdx │ ├── demos │ ├── coder.mdx │ ├── logo-roll.mdx │ ├── meeting.mdx │ ├── shine.mdx │ └── testimonials.mdx │ ├── dynamic-island.mdx │ ├── floaty.mdx │ ├── glitch.mdx │ ├── glob.mdx │ ├── minimal-component.mdx │ ├── outbound-phone-dial.mdx │ ├── shine-card.mdx │ ├── siri.mdx │ ├── transcribe.mdx │ ├── visualizer.mdx │ └── waveform.mdx ├── hooks ├── appointment-scheduler.ts ├── code-assistant.ts ├── create-assistant-vapi.ts ├── use-multi-vapi.ts ├── use-vapi-factory.ts └── use-vapi.ts ├── lib └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── images │ ├── appt.png │ ├── hero-img.png │ ├── hero-img.webp │ └── image.png └── logos │ ├── aws.svg │ ├── babel.svg │ ├── coursera.svg │ ├── css.svg │ ├── git.svg │ ├── html5.svg │ ├── javascript.svg │ ├── nextjs_logo_dark.svg │ ├── prime-video.svg │ ├── react.svg │ ├── sanity.svg │ ├── stripe.svg │ ├── tailwindcss.svg │ ├── typescript.svg │ ├── vscode.svg │ └── webflow.svg ├── tailwind.config.ts ├── tsconfig.json └── velite.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # velite files 40 | .velite 41 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # ignore all mdx files in the root directory 2 | *.mdx -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Gonzalo Chalé 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vapi Blocks - UI Library for Vapi AI 2 | Vapi Blocks is a collection of animated components & api snippets to integrate Vapi AI into your Next.js app. Copy and paste the components and hooks to get started. 3 | 4 | ## Intro 5 | [![VapiBlocks UI Library & API Snips for Vapi Voice AI](https://res.cloudinary.com/marcomontalbano/image/upload/v1720129151/video_to_markdown/images/youtube--gjtUfjiRgAs-c05b58ac6eb4c4700831b2b3070cd403.jpg)](https://www.youtube.com/watch?v=gjtUfjiRgAs "VapiBlocks UI Library & API Snips for Vapi Voice AI") 6 | 7 | ## CustomGPT Helper in ChatGPT 8 | Use the [VapiBlocks GPT](https://chatgpt.com/g/g-VcpINtsho-vapiblocks-cookbook-for-vapi-voice-ai-platform) to get help implementing Vapi in general and VapiBlocks in your React / Next app. 9 | 10 | ## Features 11 | 12 | - **Simple**: Vapi Blocks is designed to be simple and easy to use. It is built on top of TailwindCSS, which makes it easy to integrate into your projects. 13 | 14 | - **Customizable**: Vapi Blocks is highly customizable. You can easily change the colors, fonts, and other styles to match your brand. 15 | 16 | - **Responsive**: Vapi Blocks is designed to be responsive. It works on all devices, from mobile to desktop. 17 | 18 | - **Open Source**: Vapi Blocks is open source. You can use it for free in your personal and commercial projects and contribute to its development. 19 | 20 | ## Voice & Mic Reactive Components [view here](https://vapiblocks.com) 21 | 22 | - **Siri** 23 | - **Classic** 24 | - **Orb** 25 | - **Glob** 26 | Ω- **Minimal** 27 | - **Floaty** 28 | 29 | ## Snippets & Examples 30 | 31 | - **Meeting Scheduler**: [view here](https://vapiblocks.com) 32 | - **Outbound Phone Call from Web**: [view here](https://vapiblocks.com](https://www.vapiblocks.com/components/outbound-phone-dial)) 33 | - **SMS Examples**: Coming soon. 34 | - **Coding Assistant**: [view here](https://www.vapiblocks.com/components/demos/builder) 35 | - **Tavily AI Web Search**: In progress. 36 | - **Data Retrieval**: Coming soon. 37 | 38 | ## Installation 39 | 40 | You only need to install the dependencies and import the components that you want to use in your project. Sopecific components may require installation of framer-motion. 41 | 42 | Dependencies: 43 | 44 | - **ReactJS**: Vapi Blocks is built on top of ReactJS, so your project needs to have ReactJS installed, for example you can use it with NextJS, Astro or Create React App. 45 | 46 | - **TailwindCSS**: Vapi Blocks uses TailwindCSS for styling, so you need to have TailwindCSS installed in your project. 47 | 48 | ## Credits 49 | 50 | Vapi Blocks is inspired by other libraries like **shadcn ui**, **Aceternity**, and **MagicUI** so I want to give them credit for their work and inspiration, also I want to thank the **TailwindCSS** team for their amazing work. The UI for the site was made possible by @chonza! 51 | -------------------------------------------------------------------------------- /app/(sidebar-layout)/components/[...slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { components } from "#site/content"; 2 | import { MDXContent } from "@/components/mdx-components"; 3 | import { notFound } from "next/navigation"; 4 | import { Metadata } from "next"; 5 | import { siteConfig } from "@/config/site"; 6 | 7 | interface ComponentPageProps { 8 | params: { slug: string[] }; 9 | } 10 | 11 | async function getComponentFromParams(params: ComponentPageProps["params"]) { 12 | const slug = params?.slug.join("/"); 13 | const component = components.find( 14 | (component) => component.slugAsParams === slug 15 | ); 16 | 17 | return component; 18 | } 19 | 20 | export async function generateMetadata({ 21 | params, 22 | }: ComponentPageProps): Promise { 23 | const component = await getComponentFromParams(params); 24 | 25 | if (!component) { 26 | return {}; 27 | } 28 | 29 | const ogSearchParams = new URLSearchParams(); 30 | ogSearchParams.set("title", component.title); 31 | 32 | return { 33 | title: component.title, 34 | description: component.description, 35 | authors: { name: siteConfig.author }, 36 | openGraph: { 37 | title: component.title, 38 | description: component.description, 39 | type: "article", 40 | url: component.slug, 41 | images: [ 42 | { 43 | url: `/api/og?${ogSearchParams.toString()}`, 44 | width: 1200, 45 | height: 630, 46 | alt: component.title, 47 | }, 48 | ], 49 | }, 50 | twitter: { 51 | card: "summary_large_image", 52 | title: component.title, 53 | description: component.description, 54 | images: [`/api/og?${ogSearchParams.toString()}`], 55 | }, 56 | }; 57 | } 58 | 59 | export async function generateStaticParams(): Promise< 60 | ComponentPageProps["params"][] 61 | > { 62 | return components.map((component) => ({ 63 | slug: component.slugAsParams.split("/"), 64 | })); 65 | } 66 | 67 | export default async function ComponentPage({ params }: ComponentPageProps) { 68 | const component = await getComponentFromParams(params); 69 | 70 | if (!component || !component.published) { 71 | return notFound(); 72 | } 73 | 74 | return ( 75 |
76 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /app/(sidebar-layout)/docs/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { docs } from "#site/content"; 2 | import { MDXContent } from "@/components/mdx-components"; 3 | import { notFound } from "next/navigation"; 4 | 5 | interface DocsPageProps { 6 | params: { slug: string[] }; 7 | } 8 | 9 | async function getDocsFromParams(params: DocsPageProps["params"]) { 10 | const slug = params?.slug as unknown as string; 11 | const doc = docs.find((doc) => doc.slugAsParams.includes(slug)); 12 | 13 | return doc; 14 | } 15 | 16 | export default async function DocsPage({ params }: DocsPageProps) { 17 | const doc = await getDocsFromParams(params); 18 | 19 | if (!doc || !doc.published) { 20 | return notFound(); 21 | } 22 | 23 | return ( 24 |
25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/(sidebar-layout)/docs/page.tsx: -------------------------------------------------------------------------------- 1 | import { docs } from "#site/content"; 2 | import { MDXContent } from "@/components/mdx-components"; 3 | 4 | export default function DocsPage() { 5 | const introduction = docs.find((doc) => doc.slugAsParams === "introduction"); 6 | 7 | if (!introduction || !introduction.published) { 8 | return null; 9 | } 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/(sidebar-layout)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Sidebar } from "@/components/sidebar"; 2 | import "@/app/mdx.css"; 3 | 4 | export default function SidebarLayout({ 5 | children, 6 | }: Readonly<{ 7 | children: React.ReactNode; 8 | }>) { 9 | return ( 10 |
11 | 12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/api/og/route.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { NextRequest } from "next/server"; 3 | import { ImageResponse } from "next/og"; 4 | import { siteConfig } from "@/config/site"; 5 | 6 | export const runtime = "edge"; 7 | 8 | const interBold = fetch( 9 | new URL("../../../assets/fonts/Inter-Bold.ttf", import.meta.url) 10 | ).then((res) => res.arrayBuffer()); 11 | 12 | export async function GET(req: NextRequest) { 13 | try { 14 | const fontBold = await interBold; 15 | 16 | const { searchParams } = req.nextUrl; 17 | const title = searchParams.get("title"); 18 | 19 | if (!title) { 20 | return new Response("No title provided", { status: 500 }); 21 | } 22 | 23 | const heading = 24 | title.length > 140 ? `${title.substring(0, 140)}...` : title; 25 | 26 | return new ImageResponse( 27 | ( 28 |
29 |
30 |

{siteConfig.name}

31 |
32 |
33 |
{heading}
34 |
35 |
36 |
{siteConfig.url}
37 |
38 |
{siteConfig.links.github}
39 |
40 |
41 |
42 | ), 43 | { 44 | width: 1200, 45 | height: 630, 46 | fonts: [ 47 | { 48 | name: "Inter", 49 | data: fontBold, 50 | style: "normal", 51 | weight: 700, 52 | }, 53 | ], 54 | } 55 | ); 56 | } catch (error) { 57 | return new Response("Failed to generate image", { status: 500 }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/api/vapi/book-appointment/route.tsx: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import axios from 'axios'; 3 | 4 | const apiKey = process.env.CAL_API_KEY; 5 | 6 | async function createBooking({ start, name, email, smsReminderNumber }: { start: string; name: string; email: string; smsReminderNumber?: string }) { 7 | const startDateTime = new Date(start); 8 | const endDateTime = new Date(startDateTime.getTime() + 30 * 60000); // Add 30 minutes to start time 9 | const end = endDateTime.toISOString(); 10 | 11 | const response = await axios.post( 12 | `https://api.cal.com/v1/bookings?apiKey=${apiKey}`, 13 | { 14 | eventTypeId: 873076, 15 | start: start, 16 | end: end, 17 | responses: { 18 | name: name, 19 | email: email, 20 | metadata: {}, 21 | location: "Google Meet" 22 | }, 23 | timeZone: "America/Los_Angeles", 24 | language: "en", 25 | title: "30min Meeting with Cam (Founder of VapiBlocks)", 26 | description: "Discuss partnership with VapiBlocks or ways to add Voice AI to your app", 27 | status: "ACCEPTED", 28 | smsReminderNumber: smsReminderNumber || null, 29 | metadata: {} 30 | } 31 | ); 32 | 33 | return response.data; 34 | } 35 | 36 | export async function POST(request: NextRequest) { 37 | try { 38 | const { message } = await request.json(); 39 | console.log(message); 40 | if (message.type === 'function-call' && message.functionCall) { 41 | const { parameters } = message.functionCall; 42 | 43 | const result = await createBooking(parameters); 44 | const jsonResponse = NextResponse.json({ result: "The booking was created successfully.", response: result }, { status: 200 }); 45 | jsonResponse.headers.set('Access-Control-Allow-Origin', '*'); 46 | jsonResponse.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); 47 | jsonResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 48 | return jsonResponse; 49 | } else { 50 | const jsonResponse = NextResponse.json({ message: `Unhandled message type: ${message.type}` }, { status: 400 }); 51 | jsonResponse.headers.set('Access-Control-Allow-Origin', '*'); 52 | jsonResponse.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); 53 | jsonResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 54 | return jsonResponse; 55 | } 56 | } catch (error) { 57 | console.error('Error processing request:', error); 58 | const jsonResponse = NextResponse.json({ message: error }, { status: 500 }); 59 | jsonResponse.headers.set('Access-Control-Allow-Origin', '*'); 60 | jsonResponse.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); 61 | jsonResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 62 | return jsonResponse; 63 | } 64 | } 65 | 66 | export async function OPTIONS() { 67 | const response = NextResponse.json({}, { status: 200 }); 68 | response.headers.set('Access-Control-Allow-Origin', '*'); 69 | response.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); 70 | response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 71 | return response; 72 | } 73 | -------------------------------------------------------------------------------- /app/api/vapi/check-availability/route.tsx: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import axios from 'axios'; 3 | 4 | const bearerToken = process.env.CAL_API_KEY; 5 | const username = process.env.CAL_USERNAME; 6 | const userId = process.env.CAL_USERID; 7 | 8 | async function checkAvailability(parameters: { dateFrom: string; dateTo: string; }) { 9 | const { dateFrom, dateTo } = parameters; 10 | const response = await axios.get('https://api.cal.com/v1/availability', { 11 | params: { 12 | apiKey: bearerToken, 13 | userId: userId, 14 | username: username, 15 | dateFrom: dateFrom, 16 | dateTo: dateTo, 17 | eventTypeId: '873076' 18 | } 19 | }); 20 | return response.data; 21 | } 22 | 23 | export async function POST(request: NextRequest) { 24 | console.log(request); 25 | try { 26 | const { message } = await request.json(); 27 | console.log(message); 28 | if (message.type === 'function-call' && message.functionCall) { 29 | const { parameters } = message.functionCall; 30 | 31 | const result = await checkAvailability(parameters); 32 | const response = NextResponse.json({ result }, { status: 200 }); 33 | response.headers.set('Access-Control-Allow-Origin', '*'); 34 | response.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); 35 | response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 36 | return response; 37 | } else { 38 | const response = NextResponse.json({ message: `Unhandled message type: ${message.type}` }, { status: 400 }); 39 | response.headers.set('Access-Control-Allow-Origin', '*'); 40 | response.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); 41 | response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 42 | return response; 43 | } 44 | } catch (error) { 45 | console.error('Error processing request:', error); 46 | const response = NextResponse.json({ message: error }, { status: 500 }); 47 | response.headers.set('Access-Control-Allow-Origin', '*'); 48 | response.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); 49 | response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 50 | return response; 51 | } 52 | } 53 | 54 | export async function OPTIONS() { 55 | const response = NextResponse.json({}, { status: 200 }); 56 | response.headers.set('Access-Control-Allow-Origin', '*'); 57 | response.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); 58 | response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 59 | return response; 60 | } 61 | -------------------------------------------------------------------------------- /app/api/vapi/make-call/route.tsx: -------------------------------------------------------------------------------- 1 | // app/api/vapi/make-call/route.ts 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | import axios from 'axios'; 5 | 6 | export async function POST(request: NextRequest) { 7 | const API_KEY = process.env.VAPI_API_KEY; 8 | 9 | if (!API_KEY) { 10 | return NextResponse.json({ message: 'API key not found' }, { status: 500 }); 11 | } 12 | 13 | const { phoneNumberId, assistantId, customerNumber } = await request.json(); 14 | 15 | if (!phoneNumberId || !assistantId || !customerNumber) { 16 | return NextResponse.json({ message: 'Missing required fields' }, { status: 400 }); 17 | } 18 | 19 | const headers = { 20 | Authorization: `Bearer ${API_KEY}`, 21 | 'Content-Type': 'application/json', 22 | }; 23 | 24 | const data = { 25 | phoneNumberId: phoneNumberId, 26 | assistantId: assistantId, 27 | customer: { 28 | number: customerNumber, 29 | }, 30 | }; 31 | 32 | try { 33 | const response = await axios.post('https://api.vapi.ai/call/phone', data, { headers }); 34 | if (response.status === 201) { 35 | return NextResponse.json({ message: 'Call created successfully', data: response.data }); 36 | } else { 37 | return NextResponse.json({ message: 'Failed to create call', error: response.data }, { status: response.status }); 38 | } 39 | } catch (error) { 40 | return NextResponse.json({ message: 'Internal Server Error', error: JSON.stringify(error, null, 2) }, { status: 500 }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cameronking4/VapiBlocks/850c4d227c13d254068b06d5c0a98774f3eac6bf/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 0 0% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 0 0% 3.9%; 15 | 16 | --primary: 0 0% 9%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | 22 | --muted: 0 0% 96.1%; 23 | --muted-foreground: 0 0% 45.1%; 24 | 25 | --accent: 0 0% 96.1%; 26 | --accent-foreground: 0 0% 9%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 0 0% 89.8%; 32 | --input: 0 0% 89.8%; 33 | --ring: 0 0% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 0 0% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 0 0% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 0 0% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 0 0% 9%; 50 | 51 | --secondary: 0 0% 14.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 0 0% 14.9%; 55 | --muted-foreground: 0 0% 63.9%; 56 | 57 | --accent: 0 0% 14.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 0 0% 14.9%; 64 | --input: 0 0% 14.9%; 65 | --ring: 0 0% 83.1%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | 74 | body { 75 | @apply bg-background text-foreground; 76 | font-feature-settings: "rlig" 1, "calt" 1; 77 | } 78 | } 79 | 80 | @layer utilities { 81 | .step { 82 | counter-increment: step; 83 | } 84 | 85 | .step:before { 86 | @apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background; 87 | @apply ml-[-50px] mt-[-4px]; 88 | content: counter(step); 89 | } 90 | } 91 | 92 | .text-gradient_indigo-purple { 93 | background: linear-gradient(90deg, #6366f1 0%, rgb(168 85 247 / 0.8) 100%); 94 | -webkit-background-clip: text; 95 | -webkit-text-fill-color: transparent; 96 | background-clip: text; 97 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter as FontSans } from "next/font/google"; 3 | import "./globals.css"; 4 | import { siteConfig } from "@/config/site"; 5 | import { ThemeProvider } from "@/components/theme-provider"; 6 | import { Analytics } from "@vercel/analytics/react"; 7 | import { cn } from "@/lib/utils"; 8 | import { Header } from "@/components/header"; 9 | import { Footer } from "@/components/footer"; 10 | import Script from 'next/script'; 11 | 12 | const fontSans = FontSans({ 13 | subsets: ["latin"], 14 | variable: "--font-sans", 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: siteConfig.name, 19 | description: siteConfig.description, 20 | authors: [{ name: siteConfig.author, url: siteConfig.links.twitter }], 21 | creator: siteConfig.author, 22 | metadataBase: new URL(siteConfig.url), 23 | openGraph: { 24 | images: "/opengraph-image.png", 25 | }, 26 | icons: { 27 | icon: "/favicon.ico", 28 | }, 29 | keywords: ["Vapi Blocks", "Vapi", "Blocks", "Vapiblocks", "VapiBlocks", "Voice AI", "Voice AI components", "web components", "UI components", "UI Library", "shadcn", "aceternity", "AI", "Next.js", "React", "Tailwind CSS", "Framer Motion", "TypeScript", "Design engineer", "Vapi AI"], 30 | }; 31 | 32 | export default function RootLayout({ 33 | children, 34 | }: Readonly<{ 35 | children: React.ReactNode; 36 | }>) { 37 | return ( 38 | 39 | 45 | 51 |
52 |
53 |
54 | {children} 55 |
56 |
57 |
58 | 59 |
60 | 61 |