├── .gitignore
├── LICENSE
├── README.md
├── app
├── api
│ └── session
│ │ └── route.ts
├── favicon.ico
├── globals.css
├── layout.tsx
└── page.tsx
├── components.json
├── components
├── banner.tsx
├── broadcast-button.tsx
├── header.tsx
├── language-switcher.tsx
├── message-controls.tsx
├── mobile-nav.tsx
├── status.tsx
├── text-input.tsx
├── theme-provider.tsx
├── theme-switcher.tsx
├── token-usage.tsx
├── tools-education.tsx
├── translations-context.tsx
├── ui
│ ├── accordion.tsx
│ ├── alert-dialog.tsx
│ ├── alert.tsx
│ ├── aspect-ratio.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── breadcrumb.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── carousel.tsx
│ ├── chart.tsx
│ ├── checkbox.tsx
│ ├── collapsible.tsx
│ ├── command.tsx
│ ├── confetti.tsx
│ ├── context-menu.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── dropdown-menu.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
│ ├── sidebar.tsx
│ ├── skeleton.tsx
│ ├── slider.tsx
│ ├── sonner.tsx
│ ├── switch.tsx
│ ├── table.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── three-dots-wave.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ ├── toggle-group.tsx
│ ├── toggle.tsx
│ ├── tooltip.tsx
│ └── transcriber.tsx
├── voice-select.tsx
└── welcome.tsx
├── config
└── site.ts
├── deno.json
├── deno.lock
├── eslint.config.mjs
├── hooks
├── use-mobile.tsx
├── use-toast.ts
├── use-tools.ts
└── use-webrtc.ts
├── lib
├── conversations.ts
├── tools.ts
├── translations
│ ├── en.ts
│ ├── es.ts
│ ├── fr.ts
│ └── zh.ts
└── utils.ts
├── next-env.d.ts
├── next.config.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── public
├── demo.gif
├── demo.mp4
├── file.svg
├── globe.svg
├── next.svg
├── ui.png
├── vercel.svg
└── window.svg
├── tailwind.config.ts
├── tsconfig.json
└── types
└── index.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 skrivov
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 | # OpenAI WebRTC Shadcn Next15 Starter
2 | This is a WebRTC-based Voice AI stream application using `OpenAI`'s `Realtime API` and `WebRTC`. Project contains `/api` route and UI components developed with `Next.js` and `shadcn/ui`. It supports real-time audio conversations implented in [skrivov/openai-voice-webrtc-next](https://github.com/skrivov/openai-voice-webrtc-next) with the addition of a hook to abstract the WebRTC handling.
3 |
4 | https://github.com/user-attachments/assets/ea9324af-5c18-48d2-b980-2b81baeea4c0
5 |
6 | ## Features
7 | - **Next.js Framework**: Built with Next.js for server-side rendering and API routes.
8 | - **Modern UI**: Animated using Tailwind CSS & Framer Motion & shadcn/ui.
9 | - **Use-WebRTC Hook**: A hook to abstract the OpenAI WebRTC handling.
10 | - **Tool Calling**: 6 example functions to demonstrate client side tools along with Realtime API: `getCurrentTime`, `partyMode`, `changeBackground`, `launchWebsite`, `copyToClipboard`, `scrapeWebsite` (requires FireCrawl API key)
11 | - **Localization**: Select language for app strings and the voice agent (English, Spanish, French, Chinese)
12 | - **Type Safety**: TypeScript with strict eslint rules (optional)
13 |
14 |
15 | ## Requirements
16 | - **Deno runtime** or **Node.js**
17 | - OpenAI API Key or Azure OpenAI API Key in `.env` file
18 |
19 | ## Installation
20 |
21 | ### 1. Clone the Repository
22 | ```bash
23 | git clone https://github.com/cameronking4/openai-realtime-api-nextjs.git
24 | cd openai-realtime-api-nextjs
25 | ```
26 |
27 | ### 2. Environment Setup
28 | Create a `.env` file in the root directory:
29 | ```env
30 | OPENAI_API_KEY=your-openai-api-key
31 | ```
32 |
33 | ### 3. Install Dependencies
34 | If using **Node.js**:
35 | ```bash
36 | npm install
37 | ```
38 |
39 | If using **Deno**:
40 | ```bash
41 | deno install
42 | ```
43 |
44 | ### 4. Run the Application
45 |
46 | #### Using Node.js:
47 | ```bash
48 | npm run dev
49 | ```
50 |
51 | #### Using Deno:
52 | ```bash
53 | deno task start
54 | ```
55 |
56 | The application will be available at `http://localhost:3000`.
57 |
58 | ## Usage
59 | 1. Open the app in your browser: `http://localhost:3000`.
60 | 3. Select a voice and start the audio session.
61 |
62 | ## Deploy to Vercel
63 | **Deploy in one-click**
64 |
65 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcameronking4%2Fopenai-realtime-api-nextjs&env=OPENAI_API_KEY&envDescription=OpenAI%20Key%20(Realtime%20API%20Beta%20access)&envLink=https%3A%2F%2Fplatform.openai.com%2Fapi-keys&project-name=openai-rt-shadcn&repository-name=openai-realtime-api-nextjs-clone&demo-title=OpenAI%20Realtime%20API%20(WebRTC)%20x%20shadcn%2Fui&demo-description=Next.js%2015%20template%20to%20create%20beautiful%20Voice%20AI%20applications%20with%20OpenAI%20Realtime%20API%20Beta&demo-url=https%3A%2F%2Fopenai-rt-shadcn.vercel.app&demo-image=http%3A%2F%2Fopenai-rt-shadcn.vercel.app%2Fdemo.gif)
66 |
67 | ## License
68 | This project is licensed under the MIT License. See the `LICENSE` file for details.
69 |
70 | ## Acknowledgements
71 | - [OpenAI](https://openai.com/) for their API and models.
72 | - [Next.js](https://nextjs.org/) for the framework.
73 | - [Tailwind CSS](https://tailwindcss.com/) for styling.
74 | - [Simon Willison’s Weblog](https://simonwillison.net/2024/Dec/17/openai-webrtc/) for inspiration
75 | - [Originator: skrivov](https://github.com/skrivov/openai-voice-webrtc-next) for the WebRTC and Nextjs implementation
76 |
--------------------------------------------------------------------------------
/app/api/session/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | export async function POST() {
4 | try {
5 | if (!process.env.OPENAI_API_KEY){
6 | throw new Error(`OPENAI_API_KEY is not set`);
7 |
8 | }
9 | const response = await fetch("https://api.openai.com/v1/realtime/sessions", {
10 | method: "POST",
11 | headers: {
12 | Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
13 | "Content-Type": "application/json",
14 | },
15 | body: JSON.stringify({
16 | model: "gpt-4o-realtime-preview-2024-12-17",
17 | voice: "alloy",
18 | modalities: ["audio", "text"],
19 | instructions:"Start conversation with the user by saying 'Hello, how can I help you today?' Use the available tools when relevant. After executing a tool, you will need to respond (create a subsequent conversation item) to the user sharing the function result or error. If you do not respond with additional message with function result, user will not know you successfully executed the tool. Speak and respond in the language of the user.",
20 | tool_choice: "auto",
21 | }),
22 | });
23 |
24 | if (!response.ok) {
25 | throw new Error(`API request failed with status ${JSON.stringify(response)}`);
26 | }
27 |
28 | const data = await response.json();
29 |
30 | // Return the JSON response to the client
31 | return NextResponse.json(data);
32 | } catch (error) {
33 | console.error("Error fetching session data:", error);
34 | return NextResponse.json({ error: "Failed to fetch session data" }, { status: 500 });
35 | }
36 | }
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/openai-realtime-api-nextjs/3093bffb1ca0dae2df66b3e0f81ca2336f220a25/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .audio-indicator {
6 | @apply inline-block w-5 h-5 rounded-full align-middle bg-gray-400;
7 | }
8 |
9 | .audio-indicator.active {
10 | @apply bg-green-500;
11 | animation: pulse 1s infinite;
12 | }
13 |
14 | @keyframes pulse {
15 | 0% {
16 | opacity: 1;
17 | }
18 |
19 | 50% {
20 | opacity: 0.5;
21 | }
22 |
23 | 100% {
24 | opacity: 1;
25 | }
26 | }
27 |
28 | .controls {
29 | @apply my-5;
30 | }
31 |
32 | .form-group {
33 | @apply mb-4;
34 | }
35 |
36 |
37 | @layer base {
38 | :root {
39 | --background: 0 0% 100%;
40 | --foreground: 222.2 84% 4.9%;
41 | --card: 0 0% 100%;
42 | --card-foreground: 222.2 84% 4.9%;
43 | --popover: 0 0% 100%;
44 | --popover-foreground: 222.2 84% 4.9%;
45 | --primary: 222.2 47.4% 11.2%;
46 | --primary-foreground: 210 40% 98%;
47 | --secondary: 210 40% 96.1%;
48 | --secondary-foreground: 222.2 47.4% 11.2%;
49 | --muted: 210 40% 96.1%;
50 | --muted-foreground: 215.4 16.3% 46.9%;
51 | --accent: 210 40% 96.1%;
52 | --accent-foreground: 222.2 47.4% 11.2%;
53 | --destructive: 0 84.2% 60.2%;
54 | --destructive-foreground: 210 40% 98%;
55 | --border: 214.3 31.8% 91.4%;
56 | --input: 214.3 31.8% 91.4%;
57 | --ring: 222.2 84% 4.9%;
58 | --radius: 0.5rem;
59 | }
60 |
61 | .dark {
62 | --background: 222.2 84% 4.9%;
63 | --foreground: 210 40% 98%;
64 | --card: 222.2 84% 4.9%;
65 | --card-foreground: 210 40% 98%;
66 | --popover: 222.2 84% 4.9%;
67 | --popover-foreground: 210 40% 98%;
68 | --primary: 210 40% 98%;
69 | --primary-foreground: 222.2 47.4% 11.2%;
70 | --secondary: 217.2 32.6% 17.5%;
71 | --secondary-foreground: 210 40% 98%;
72 | --muted: 217.2 32.6% 17.5%;
73 | --muted-foreground: 215 20.2% 65.1%;
74 | --accent: 217.2 32.6% 17.5%;
75 | --accent-foreground: 210 40% 98%;
76 | --destructive: 0 62.8% 30.6%;
77 | --destructive-foreground: 210 40% 98%;
78 | --border: 217.2 32.6% 17.5%;
79 | --input: 217.2 32.6% 17.5%;
80 | --ring: 212.7 26.8% 83.9%;
81 | }
82 | }
83 |
84 | @layer base {
85 | * {
86 | @apply border-border;
87 | }
88 | body {
89 | @apply bg-background text-foreground;
90 | }
91 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist } from "next/font/google";
3 | import "./globals.css";
4 | import { Header } from "@/components/header";
5 | import { cn } from "@/lib/utils";
6 | import { ThemeProvider } from "@/components/theme-provider";
7 | import { siteConfig } from "@/config/site";
8 | import { Toaster } from "@/components/ui/sonner"
9 | import { Analytics } from "@vercel/analytics/react"
10 | import { TranslationsProvider } from "@/components/translations-context"
11 | import { Banner } from "@/components/banner";
12 |
13 | const geistSans = Geist({
14 | variable: "--font-geist-sans",
15 | subsets: ["latin"],
16 | });
17 |
18 | export const metadata: Metadata = {
19 | title: "Next.js + OpenAI Realtime API (WebRTC)",
20 | description: "Next.js Starter for using the OpenAI Realtime API WebRTC method. Starter showcases capabilities of OpenAI's latest Realtime API (12/17/2024). It has all shadcn/ui components to build your own real-time voice AI application. Fastest & latest way to do Voice AI (Dec 2024), implementing API advancements of Day of OpenAI's 12 days of Christmas.",
21 | authors: [{ name: siteConfig.author, url: siteConfig.links.twitter }],
22 | creator: siteConfig.author,
23 | metadataBase: new URL(siteConfig.url),
24 | openGraph: {
25 | images: "/opengraph-image.png",
26 | },
27 | icons: {
28 | icon: "/favicon.ico",
29 | },
30 | keywords: ["AI Blocks", "OpenAI Blocks", "Blocks", "OpenAI Realtime API", "OpenAI Realtime", "OpenAI WebRTC", "Livekit", "OpenAI Realtime WebRTC", "OpenAI Realtime Starter", "Voice AI", "Voice AI components", "web components", "UI components", "UI Library", "shadcn", "aceternity", "AI", "Next.js", "React", "Tailwind CSS", "Framer Motion", "TypeScript", "Design engineer", "shadcn ai"],
31 | };
32 |
33 | export default function RootLayout({
34 | children,
35 | }: Readonly<{
36 | children: React.ReactNode;
37 | }>) {
38 | return (
39 |
40 |
46 |
52 |
53 |
54 |
55 |
56 |
57 | {children}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, { useEffect, useState } from "react"
4 | import useWebRTCAudioSession from "@/hooks/use-webrtc"
5 | import { tools } from "@/lib/tools"
6 | import { Welcome } from "@/components/welcome"
7 | import { VoiceSelector } from "@/components/voice-select"
8 | import { BroadcastButton } from "@/components/broadcast-button"
9 | import { StatusDisplay } from "@/components/status"
10 | import { TokenUsageDisplay } from "@/components/token-usage"
11 | import { MessageControls } from "@/components/message-controls"
12 | import { ToolsEducation } from "@/components/tools-education"
13 | import { TextInput } from "@/components/text-input"
14 | import { motion } from "framer-motion"
15 | import { useToolsFunctions } from "@/hooks/use-tools"
16 |
17 | const App: React.FC = () => {
18 | // State for voice selection
19 | const [voice, setVoice] = useState("ash")
20 |
21 | // WebRTC Audio Session Hook
22 | const {
23 | status,
24 | isSessionActive,
25 | registerFunction,
26 | handleStartStopClick,
27 | msgs,
28 | conversation,
29 | sendTextMessage
30 | } = useWebRTCAudioSession(voice, tools)
31 |
32 | // Get all tools functions
33 | const toolsFunctions = useToolsFunctions();
34 |
35 | useEffect(() => {
36 | // Register all functions by iterating over the object
37 | Object.entries(toolsFunctions).forEach(([name, func]) => {
38 | const functionNames: Record = {
39 | timeFunction: 'getCurrentTime',
40 | backgroundFunction: 'changeBackgroundColor',
41 | partyFunction: 'partyMode',
42 | launchWebsite: 'launchWebsite',
43 | copyToClipboard: 'copyToClipboard',
44 | scrapeWebsite: 'scrapeWebsite'
45 | };
46 |
47 | registerFunction(functionNames[name], func);
48 | });
49 | }, [registerFunction, toolsFunctions])
50 |
51 | return (
52 |
53 |
59 |
60 |
61 |
67 |
68 |
69 |
70 |
74 |
75 | {msgs.length > 4 && }
76 | {status && (
77 |
84 |
85 |
89 |
90 | )}
91 |
92 |
93 | {status && }
94 |
95 |
96 |
97 |
98 |
99 | )
100 | }
101 |
102 | export default App;
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "stone",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/components/banner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTranslations } from "@/components/translations-context"
4 |
5 | export const Banner = () => {
6 | const { t } = useTranslations();
7 |
8 | return (
9 |
20 | );
21 | };
22 |
23 | export default Banner;
24 |
--------------------------------------------------------------------------------
/components/broadcast-button.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import { Badge } from "@/components/ui/badge"
3 | import { useTranslations } from "@/components/translations-context";
4 |
5 | interface BroadcastButtonProps {
6 | isSessionActive: boolean
7 | onClick: () => void
8 | }
9 |
10 | export function BroadcastButton({ isSessionActive, onClick }: BroadcastButtonProps) {
11 | const { t } = useTranslations();
12 | return (
13 |
25 | )
26 | }
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import Link from "next/link";
5 | import { ThemeSwitcher } from "@/components/theme-switcher";
6 | import { MobileNav } from "./mobile-nav";
7 | import { Badge } from "./ui/badge";
8 | import { siteConfig } from "@/config/site";
9 | import { TwitterIcon, StarIcon } from "lucide-react";
10 | import { motion } from "framer-motion";
11 | import { LanguageSwitcher } from "@/components/language-switcher";
12 | import { useTranslations } from "@/components/translations-context";
13 |
14 | export function Header() {
15 | const { t } = useTranslations()
16 | return (
17 |
23 |
24 |
25 |
31 |
32 |
36 | {t('header.logo')}
37 |
38 |
43 |
44 | {t('header.beta')}
45 |
46 |
47 |
48 |
49 |
55 |
56 |
62 |
63 |
72 |
73 |
74 |
80 |
81 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/components/language-switcher.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectItem,
7 | SelectTrigger,
8 | SelectValue,
9 | } from "@/components/ui/select"
10 | import { toast } from "sonner"
11 | import { Languages } from "lucide-react"
12 | import { useTranslations } from "@/components/translations-context"
13 |
14 | export function LanguageSwitcher() {
15 | const { t, locale, setLocale } = useTranslations()
16 |
17 | const languages = [
18 | { code: 'en', label: 'English', icon: '🇬🇧' },
19 | { code: 'es', label: 'Español', icon: '🇪🇸' },
20 | { code: 'fr', label: 'Français', icon: '🇫🇷' },
21 | { code: 'zh', label: '中文', icon: '🇨🇳' },
22 | ]
23 |
24 | const selectedLanguage = languages.find(lang => lang.code === locale)
25 |
26 | const onSelect = (value: string) => {
27 | setLocale(value);
28 | toast.success(`${t('status.language')} ${locale}`)
29 | }
30 |
31 | return (
32 |
47 | )
48 | }
--------------------------------------------------------------------------------
/components/message-controls.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import Transcriber from "@/components/ui/transcriber"
3 | import { Conversation } from "@/lib/conversations"
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogHeader,
8 | DialogTitle,
9 | DialogTrigger,
10 | } from "@/components/ui/dialog"
11 | import {
12 | Table,
13 | TableBody,
14 | TableCell,
15 | TableHead,
16 | TableHeader,
17 | TableRow,
18 | } from "@/components/ui/table"
19 | import { Message as MessageType } from "@/types"
20 | import { ScrollArea } from "@/components/ui/scroll-area"
21 | import { Input } from "@/components/ui/input"
22 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
23 | import { useState } from "react"
24 | import { Terminal } from "lucide-react"
25 | import { useTranslations } from "@/components/translations-context"
26 |
27 | function FilterControls({
28 | typeFilter,
29 | setTypeFilter,
30 | searchQuery,
31 | setSearchQuery,
32 | messageTypes,
33 | messages,
34 | }: {
35 | typeFilter: string
36 | setTypeFilter: (value: string) => void
37 | searchQuery: string
38 | setSearchQuery: (value: string) => void
39 | messageTypes: string[]
40 | messages: MessageType[]
41 | }) {
42 | const { t } = useTranslations();
43 |
44 | return (
45 |
46 |
58 | setSearchQuery(e.target.value)}
62 | className="flex-1"
63 | />
64 |
68 |
69 | )
70 | }
71 |
72 | export function MessageControls({ conversation, msgs }: { conversation: Conversation[], msgs: MessageType[] }) {
73 | const { t } = useTranslations();
74 | const [typeFilter, setTypeFilter] = useState("all")
75 | const [searchQuery, setSearchQuery] = useState("")
76 |
77 | if (conversation.length === 0) return null
78 |
79 | // Get unique message types
80 | const messageTypes = ["all", ...new Set(msgs.map(msg => msg.type))]
81 |
82 | // Filter messages based on type and search query
83 | const filteredMsgs = msgs.filter(msg => {
84 | const matchesType = typeFilter === "all" || msg.type === typeFilter
85 | const matchesSearch = searchQuery === "" ||
86 | JSON.stringify(msg).toLowerCase().includes(searchQuery.toLowerCase())
87 | return matchesType && matchesSearch
88 | })
89 |
90 | return (
91 |
92 |
93 |
{t('messageControls.logs')}
94 |
136 |
137 |
138 |
139 |
140 | )
141 | }
--------------------------------------------------------------------------------
/components/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogHeader,
7 | DialogTitle,
8 | DialogTrigger,
9 | } from "@/components/ui/dialog";
10 | import { useState } from "react";
11 | import { Button } from "@/components/ui/button";
12 | import { MenuIcon } from "lucide-react";
13 | import Link from "next/link";
14 | import { siteConfig } from "@/config/site";
15 | import { Badge } from "./ui/badge";
16 | import { useTranslations } from "@/components/translations-context"
17 |
18 | export function MobileNav() {
19 | const { t } = useTranslations();
20 | const [open, setOpen] = useState(false);
21 |
22 | return (
23 |
24 |
51 |
52 | );
53 | }
--------------------------------------------------------------------------------
/components/status.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect } from 'react'
4 | import { toast } from 'sonner'
5 | import { useTranslations } from "@/components/translations-context"
6 |
7 | interface StatusDisplayProps {
8 | status: string
9 | }
10 |
11 | export function StatusDisplay({ status }: StatusDisplayProps) {
12 | const { t } = useTranslations();
13 | useEffect(() => {
14 | if (status.startsWith("Error")) {
15 | toast.error(t('status.error'), {
16 | description: status,
17 | duration: 3000,
18 | })
19 | }
20 | else if (status.startsWith("Session established")) {
21 | toast.success(t('status.success'), {
22 | description: status,
23 | duration: 5000,
24 | })
25 | }
26 | else {
27 | toast.info(t('status.info'), {
28 | description: status,
29 | duration: 3000,
30 | })
31 | }
32 | }, [status, t])
33 | return null
34 | }
--------------------------------------------------------------------------------
/components/text-input.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, { useState } from "react"
4 | import { Button } from "@/components/ui/button"
5 | import { Input } from "@/components/ui/input"
6 | import { Send } from "lucide-react"
7 |
8 | interface TextInputProps {
9 | onSubmit: (text: string) => void
10 | disabled?: boolean
11 | }
12 |
13 | export function TextInput({ onSubmit, disabled = false }: TextInputProps) {
14 | const [text, setText] = useState("")
15 |
16 | const handleSubmit = (e: React.FormEvent) => {
17 | e.preventDefault()
18 | if (text.trim()) {
19 | onSubmit(text.trim())
20 | setText("")
21 | }
22 | }
23 |
24 | return (
25 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { ThemeProvider as NextThemesProvider } from "next-themes";
3 | import { type ThemeProviderProps } from "next-themes";
4 |
5 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
6 | return {children};
7 | }
8 |
--------------------------------------------------------------------------------
/components/theme-switcher.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useTheme } from "next-themes";
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuTrigger,
9 | } from "@/components/ui/dropdown-menu";
10 | import { SunIcon, MoonIcon } from "lucide-react";
11 | import { useTranslations } from "@/components/translations-context";
12 |
13 | export function ThemeSwitcher() {
14 | const { t } = useTranslations()
15 | const { setTheme } = useTheme();
16 |
17 | return (
18 |
19 |
20 |
25 |
26 |
27 | setTheme("light")}>
28 | {t('header.light')}
29 |
30 | setTheme("dark")}>
31 | {t('header.dark')}
32 |
33 | setTheme("system")}>
34 | {t('header.system')}
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/components/token-usage.tsx:
--------------------------------------------------------------------------------
1 | import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
2 | import { Card, CardContent, CardTitle } from "@/components/ui/card"
3 | import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"
4 | import { useTranslations } from "@/components/translations-context"
5 | import { Message } from "@/types"
6 |
7 | interface TokenUsageDisplayProps {
8 | messages: Message[]
9 | }
10 |
11 | export function TokenUsageDisplay({ messages }: TokenUsageDisplayProps) {
12 | const { t } = useTranslations();
13 | return (
14 | <>
15 | { messages.length > 0 && (
16 |
17 |
18 |
19 | {t('tokenUsage.usage')}
20 |
21 |
22 |
23 |
24 |
25 | {messages
26 | .filter((msg) => msg.type === 'response.done')
27 | .slice(-1)
28 | .map((msg) => {
29 | const tokenData = [
30 | { label: t('tokenUsage.total'), value: msg.response?.usage?.total_tokens },
31 | { label: t('tokenUsage.input'), value: msg.response?.usage?.input_tokens },
32 | { label: t('tokenUsage.output'), value: msg.response?.usage?.output_tokens }
33 | ];
34 |
35 | return (
36 |
37 |
38 | {tokenData.map(({label, value}) => (
39 |
40 | {label}
41 | {value}
42 |
43 | ))}
44 |
45 |
46 | );
47 | })}
48 |
49 |
50 |
51 |
52 |
53 |
54 | )
55 | }
56 | >
57 | )
58 | }
--------------------------------------------------------------------------------
/components/tools-education.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Accordion,
5 | AccordionContent,
6 | AccordionItem,
7 | AccordionTrigger,
8 | } from "@/components/ui/accordion"
9 | import {
10 | Table,
11 | TableBody,
12 | TableCell,
13 | TableRow,
14 | } from "@/components/ui/table"
15 | import { useTranslations } from "@/components/translations-context"
16 |
17 |
18 | export function ToolsEducation() {
19 | const { t } = useTranslations();
20 |
21 | const AVAILABLE_TOOLS = [
22 | {
23 | name: t('tools.availableTools.copyFn.name'),
24 | description: t('tools.availableTools.copyFn.description'),
25 | },
26 | {
27 | name: t('tools.availableTools.getTime.name'),
28 | description: t('tools.availableTools.getTime.description'),
29 | },
30 | {
31 | name: t('tools.availableTools.themeSwitcher.name'),
32 | description: t('tools.availableTools.themeSwitcher.description'),
33 | },
34 | {
35 | name: t('tools.availableTools.partyMode.name'),
36 | description: t('tools.availableTools.partyMode.description'),
37 | },
38 | {
39 | name: t('tools.availableTools.launchWebsite.name'),
40 | description: t('tools.availableTools.launchWebsite.description'),
41 | },
42 | {
43 | name: t('tools.availableTools.scrapeWebsite.name'),
44 | description: t('tools.availableTools.scrapeWebsite.description'),
45 | },
46 | ] as const;
47 |
48 | return (
49 |
50 |
51 |
52 | {t('tools.availableTools.title')}
53 |
54 |
55 |
56 | {AVAILABLE_TOOLS.map((tool) => (
57 |
58 | {tool.name}
59 |
60 | {tool.description}
61 |
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 |
69 |
70 | )
71 | }
--------------------------------------------------------------------------------
/components/translations-context.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createContext, useContext, useState, ReactNode } from 'react'
4 | import { en } from '@/lib/translations/en'
5 | import { es } from '@/lib/translations/es'
6 | import { fr } from '@/lib/translations/fr'
7 | import { zh } from '@/lib/translations/zh'
8 |
9 | type TranslationValue = string | { [key: string]: TranslationValue }
10 |
11 | type Translations = {
12 | [key: string]: TranslationValue
13 | }
14 |
15 | const translations: { [key: string]: Translations } = {
16 | en,
17 | es,
18 | fr,
19 | zh
20 | }
21 |
22 | type TranslationsContextType = {
23 | t: (key: string) => string
24 | locale: string
25 | setLocale: (locale: string) => void
26 | }
27 |
28 | const TranslationsContext = createContext(null)
29 |
30 | export function TranslationsProvider({ children }: { children: ReactNode }) {
31 | const [locale, setLocale] = useState('en')
32 |
33 | const t = (key: string): string => {
34 | const keys = key.split('.')
35 | let value: TranslationValue = translations[locale]
36 |
37 | for (const k of keys) {
38 | if (value === undefined) return key
39 | value = typeof value === 'object' ? value[k] : key
40 | }
41 |
42 | return typeof value === 'string' ? value : key
43 | }
44 |
45 | return (
46 |
47 | {children}
48 |
49 | )
50 | }
51 |
52 | export function useTranslations() {
53 | const context = useContext(TranslationsContext)
54 | if (!context) {
55 | throw new Error('useTranslations must be used within a TranslationsProvider')
56 | }
57 | return context
58 | }
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ))
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
59 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:w-3.5 [&>svg]:h-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLDivElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/carousel.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import useEmblaCarousel, {
5 | type UseEmblaCarouselType,
6 | } from "embla-carousel-react"
7 | import { ArrowLeft, ArrowRight } from "lucide-react"
8 |
9 | import { cn } from "@/lib/utils"
10 | import { Button } from "@/components/ui/button"
11 |
12 | type CarouselApi = UseEmblaCarouselType[1]
13 | type UseCarouselParameters = Parameters
14 | type CarouselOptions = UseCarouselParameters[0]
15 | type CarouselPlugin = UseCarouselParameters[1]
16 |
17 | type CarouselProps = {
18 | opts?: CarouselOptions
19 | plugins?: CarouselPlugin
20 | orientation?: "horizontal" | "vertical"
21 | setApi?: (api: CarouselApi) => void
22 | }
23 |
24 | type CarouselContextProps = {
25 | carouselRef: ReturnType[0]
26 | api: ReturnType[1]
27 | scrollPrev: () => void
28 | scrollNext: () => void
29 | canScrollPrev: boolean
30 | canScrollNext: boolean
31 | } & CarouselProps
32 |
33 | const CarouselContext = React.createContext(null)
34 |
35 | function useCarousel() {
36 | const context = React.useContext(CarouselContext)
37 |
38 | if (!context) {
39 | throw new Error("useCarousel must be used within a ")
40 | }
41 |
42 | return context
43 | }
44 |
45 | const Carousel = React.forwardRef<
46 | HTMLDivElement,
47 | React.HTMLAttributes & CarouselProps
48 | >(
49 | (
50 | {
51 | orientation = "horizontal",
52 | opts,
53 | setApi,
54 | plugins,
55 | className,
56 | children,
57 | ...props
58 | },
59 | ref
60 | ) => {
61 | const [carouselRef, api] = useEmblaCarousel(
62 | {
63 | ...opts,
64 | axis: orientation === "horizontal" ? "x" : "y",
65 | },
66 | plugins
67 | )
68 | const [canScrollPrev, setCanScrollPrev] = React.useState(false)
69 | const [canScrollNext, setCanScrollNext] = React.useState(false)
70 |
71 | const onSelect = React.useCallback((api: CarouselApi) => {
72 | if (!api) {
73 | return
74 | }
75 |
76 | setCanScrollPrev(api.canScrollPrev())
77 | setCanScrollNext(api.canScrollNext())
78 | }, [])
79 |
80 | const scrollPrev = React.useCallback(() => {
81 | api?.scrollPrev()
82 | }, [api])
83 |
84 | const scrollNext = React.useCallback(() => {
85 | api?.scrollNext()
86 | }, [api])
87 |
88 | const handleKeyDown = React.useCallback(
89 | (event: React.KeyboardEvent) => {
90 | if (event.key === "ArrowLeft") {
91 | event.preventDefault()
92 | scrollPrev()
93 | } else if (event.key === "ArrowRight") {
94 | event.preventDefault()
95 | scrollNext()
96 | }
97 | },
98 | [scrollPrev, scrollNext]
99 | )
100 |
101 | React.useEffect(() => {
102 | if (!api || !setApi) {
103 | return
104 | }
105 |
106 | setApi(api)
107 | }, [api, setApi])
108 |
109 | React.useEffect(() => {
110 | if (!api) {
111 | return
112 | }
113 |
114 | onSelect(api)
115 | api.on("reInit", onSelect)
116 | api.on("select", onSelect)
117 |
118 | return () => {
119 | api?.off("select", onSelect)
120 | }
121 | }, [api, onSelect])
122 |
123 | return (
124 |
137 |
145 | {children}
146 |
147 |
148 | )
149 | }
150 | )
151 | Carousel.displayName = "Carousel"
152 |
153 | const CarouselContent = React.forwardRef<
154 | HTMLDivElement,
155 | React.HTMLAttributes
156 | >(({ className, ...props }, ref) => {
157 | const { carouselRef, orientation } = useCarousel()
158 |
159 | return (
160 |
171 | )
172 | })
173 | CarouselContent.displayName = "CarouselContent"
174 |
175 | const CarouselItem = React.forwardRef<
176 | HTMLDivElement,
177 | React.HTMLAttributes
178 | >(({ className, ...props }, ref) => {
179 | const { orientation } = useCarousel()
180 |
181 | return (
182 |
193 | )
194 | })
195 | CarouselItem.displayName = "CarouselItem"
196 |
197 | const CarouselPrevious = React.forwardRef<
198 | HTMLButtonElement,
199 | React.ComponentProps
200 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
201 | const { orientation, scrollPrev, canScrollPrev } = useCarousel()
202 |
203 | return (
204 |
222 | )
223 | })
224 | CarouselPrevious.displayName = "CarouselPrevious"
225 |
226 | const CarouselNext = React.forwardRef<
227 | HTMLButtonElement,
228 | React.ComponentProps
229 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
230 | const { orientation, scrollNext, canScrollNext } = useCarousel()
231 |
232 | return (
233 |
251 | )
252 | })
253 | CarouselNext.displayName = "CarouselNext"
254 |
255 | export {
256 | type CarouselApi,
257 | Carousel,
258 | CarouselContent,
259 | CarouselItem,
260 | CarouselPrevious,
261 | CarouselNext,
262 | }
263 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | const CommandDialog = ({ children, ...props }: DialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/components/ui/confetti.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import React, {
3 | createContext,
4 | forwardRef,
5 | useCallback,
6 | useEffect,
7 | useImperativeHandle,
8 | useMemo,
9 | useRef,
10 | } from "react";
11 | import type {
12 | GlobalOptions as ConfettiGlobalOptions,
13 | CreateTypes as ConfettiInstance,
14 | Options as ConfettiOptions,
15 | } from "canvas-confetti";
16 | import confetti from "canvas-confetti";
17 |
18 | import { Button, ButtonProps } from "@/components/ui/button";
19 |
20 | type Api = {
21 | fire: (options?: ConfettiOptions) => void;
22 | };
23 |
24 | type Props = React.ComponentPropsWithRef<"canvas"> & {
25 | options?: ConfettiOptions;
26 | globalOptions?: ConfettiGlobalOptions;
27 | manualstart?: boolean;
28 | children?: ReactNode;
29 | };
30 |
31 | export type ConfettiRef = Api | null;
32 |
33 | const ConfettiContext = createContext({} as Api);
34 |
35 | // Define component first
36 | const ConfettiComponent = forwardRef((props, ref) => {
37 | const {
38 | options,
39 | globalOptions = { resize: true, useWorker: true },
40 | manualstart = false,
41 | children,
42 | ...rest
43 | } = props;
44 | const instanceRef = useRef(null);
45 |
46 | const canvasRef = useCallback(
47 | (node: HTMLCanvasElement) => {
48 | if (node !== null) {
49 | if (instanceRef.current) return;
50 | instanceRef.current = confetti.create(node, {
51 | ...globalOptions,
52 | resize: true,
53 | });
54 | } else {
55 | if (instanceRef.current) {
56 | instanceRef.current.reset();
57 | instanceRef.current = null;
58 | }
59 | }
60 | },
61 | [globalOptions],
62 | );
63 |
64 | const fire = useCallback(
65 | async (opts = {}) => {
66 | try {
67 | await instanceRef.current?.({ ...options, ...opts });
68 | } catch (error) {
69 | console.error("Confetti error:", error);
70 | }
71 | },
72 | [options],
73 | );
74 |
75 | const api = useMemo(
76 | () => ({
77 | fire,
78 | }),
79 | [fire],
80 | );
81 |
82 | useImperativeHandle(ref, () => api, [api]);
83 |
84 | useEffect(() => {
85 | if (!manualstart) {
86 | (async () => {
87 | try {
88 | await fire();
89 | } catch (error) {
90 | console.error("Confetti effect error:", error);
91 | }
92 | })();
93 | }
94 | }, [manualstart, fire]);
95 |
96 | return (
97 |
98 |
99 | {children}
100 |
101 | );
102 | });
103 |
104 | // Set display name immediately
105 | ConfettiComponent.displayName = "Confetti";
106 |
107 | // Export as Confetti
108 | export const Confetti = ConfettiComponent;
109 |
110 | interface ConfettiButtonProps extends ButtonProps {
111 | options?: ConfettiOptions &
112 | ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };
113 | children?: React.ReactNode;
114 | }
115 |
116 | const ConfettiButtonComponent = ({
117 | options,
118 | children,
119 | ...props
120 | }: ConfettiButtonProps) => {
121 | const handleClick = async (event: React.MouseEvent) => {
122 | try {
123 | const rect = event.currentTarget.getBoundingClientRect();
124 | const x = rect.left + rect.width / 2;
125 | const y = rect.top + rect.height / 2;
126 | await confetti({
127 | ...options,
128 | origin: {
129 | x: x / window.innerWidth,
130 | y: y / window.innerHeight,
131 | },
132 | });
133 | } catch (error) {
134 | console.error("Confetti button error:", error);
135 | }
136 | };
137 |
138 | return (
139 |
142 | );
143 | };
144 |
145 | ConfettiButtonComponent.displayName = "ConfettiButton";
146 |
147 | export const ConfettiButton = ConfettiButtonComponent;
148 |
149 | export default Confetti;
150 |
--------------------------------------------------------------------------------
/components/ui/context-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const ContextMenu = ContextMenuPrimitive.Root
10 |
11 | const ContextMenuTrigger = ContextMenuPrimitive.Trigger
12 |
13 | const ContextMenuGroup = ContextMenuPrimitive.Group
14 |
15 | const ContextMenuPortal = ContextMenuPrimitive.Portal
16 |
17 | const ContextMenuSub = ContextMenuPrimitive.Sub
18 |
19 | const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
20 |
21 | const ContextMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
41 |
42 | const ContextMenuSubContent = React.forwardRef<
43 | React.ElementRef,
44 | React.ComponentPropsWithoutRef
45 | >(({ className, ...props }, ref) => (
46 |
54 | ))
55 | ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
56 |
57 | const ContextMenuContent = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
62 |
70 |
71 | ))
72 | ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
73 |
74 | const ContextMenuItem = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef & {
77 | inset?: boolean
78 | }
79 | >(({ className, inset, ...props }, ref) => (
80 |
89 | ))
90 | ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
91 |
92 | const ContextMenuCheckboxItem = React.forwardRef<
93 | React.ElementRef,
94 | React.ComponentPropsWithoutRef
95 | >(({ className, children, checked, ...props }, ref) => (
96 |
105 |
106 |
107 |
108 |
109 |
110 | {children}
111 |
112 | ))
113 | ContextMenuCheckboxItem.displayName =
114 | ContextMenuPrimitive.CheckboxItem.displayName
115 |
116 | const ContextMenuRadioItem = React.forwardRef<
117 | React.ElementRef,
118 | React.ComponentPropsWithoutRef
119 | >(({ className, children, ...props }, ref) => (
120 |
128 |
129 |
130 |
131 |
132 |
133 | {children}
134 |
135 | ))
136 | ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
137 |
138 | const ContextMenuLabel = React.forwardRef<
139 | React.ElementRef,
140 | React.ComponentPropsWithoutRef & {
141 | inset?: boolean
142 | }
143 | >(({ className, inset, ...props }, ref) => (
144 |
153 | ))
154 | ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
155 |
156 | const ContextMenuSeparator = React.forwardRef<
157 | React.ElementRef,
158 | React.ComponentPropsWithoutRef
159 | >(({ className, ...props }, ref) => (
160 |
165 | ))
166 | ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
167 |
168 | const ContextMenuShortcut = ({
169 | className,
170 | ...props
171 | }: React.HTMLAttributes) => {
172 | return (
173 |
180 | )
181 | }
182 | ContextMenuShortcut.displayName = "ContextMenuShortcut"
183 |
184 | export {
185 | ContextMenu,
186 | ContextMenuTrigger,
187 | ContextMenuContent,
188 | ContextMenuItem,
189 | ContextMenuCheckboxItem,
190 | ContextMenuRadioItem,
191 | ContextMenuLabel,
192 | ContextMenuSeparator,
193 | ContextMenuShortcut,
194 | ContextMenuGroup,
195 | ContextMenuPortal,
196 | ContextMenuSub,
197 | ContextMenuSubContent,
198 | ContextMenuSubTrigger,
199 | ContextMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
47 | {children}
48 |
49 |
50 | Close
51 |
52 |
53 |
54 | ))
55 | DialogContent.displayName = DialogPrimitive.Content.displayName
56 |
57 | const DialogHeader = ({
58 | className,
59 | ...props
60 | }: React.HTMLAttributes) => (
61 |
68 | )
69 | DialogHeader.displayName = "DialogHeader"
70 |
71 | const DialogFooter = ({
72 | className,
73 | ...props
74 | }: React.HTMLAttributes) => (
75 |
82 | )
83 | DialogFooter.displayName = "DialogFooter"
84 |
85 | const DialogTitle = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 | DialogTitle.displayName = DialogPrimitive.Title.displayName
99 |
100 | const DialogDescription = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
110 | ))
111 | DialogDescription.displayName = DialogPrimitive.Description.displayName
112 |
113 | export {
114 | Dialog,
115 | DialogPortal,
116 | DialogOverlay,
117 | DialogClose,
118 | DialogTrigger,
119 | DialogContent,
120 | DialogHeader,
121 | DialogFooter,
122 | DialogTitle,
123 | DialogDescription,
124 | }
125 |
--------------------------------------------------------------------------------
/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | )
17 | Drawer.displayName = "Drawer"
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal
22 |
23 | const DrawerClose = DrawerPrimitive.Close
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ))
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ))
56 | DrawerContent.displayName = "DrawerContent"
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | )
67 | DrawerHeader.displayName = "DrawerHeader"
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | )
78 | DrawerFooter.displayName = "DrawerFooter"
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ))
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ))
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | }
119 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent }
30 |
--------------------------------------------------------------------------------
/components/ui/input-otp.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { OTPInput, OTPInputContext } from "input-otp"
5 | import { Dot } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const InputOTP = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, containerClassName, ...props }, ref) => (
13 |
22 | ))
23 | InputOTP.displayName = "InputOTP"
24 |
25 | const InputOTPGroup = React.forwardRef<
26 | React.ElementRef<"div">,
27 | React.ComponentPropsWithoutRef<"div">
28 | >(({ className, ...props }, ref) => (
29 |
30 | ))
31 | InputOTPGroup.displayName = "InputOTPGroup"
32 |
33 | const InputOTPSlot = React.forwardRef<
34 | React.ElementRef<"div">,
35 | React.ComponentPropsWithoutRef<"div"> & { index: number }
36 | >(({ index, className, ...props }, ref) => {
37 | const inputOTPContext = React.useContext(OTPInputContext)
38 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
39 |
40 | return (
41 |
50 | {char}
51 | {hasFakeCaret && (
52 |
55 | )}
56 |
57 | )
58 | })
59 | InputOTPSlot.displayName = "InputOTPSlot"
60 |
61 | const InputOTPSeparator = React.forwardRef<
62 | React.ElementRef<"div">,
63 | React.ComponentPropsWithoutRef<"div">
64 | >(({ ...props }, ref) => (
65 |
66 |
67 |
68 | ))
69 | InputOTPSeparator.displayName = "InputOTPSeparator"
70 |
71 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
72 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { ButtonProps, buttonVariants } from "@/components/ui/button"
6 |
7 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8 |
14 | )
15 | Pagination.displayName = "Pagination"
16 |
17 | const PaginationContent = React.forwardRef<
18 | HTMLUListElement,
19 | React.ComponentProps<"ul">
20 | >(({ className, ...props }, ref) => (
21 |
26 | ))
27 | PaginationContent.displayName = "PaginationContent"
28 |
29 | const PaginationItem = React.forwardRef<
30 | HTMLLIElement,
31 | React.ComponentProps<"li">
32 | >(({ className, ...props }, ref) => (
33 |
34 | ))
35 | PaginationItem.displayName = "PaginationItem"
36 |
37 | type PaginationLinkProps = {
38 | isActive?: boolean
39 | } & Pick &
40 | React.ComponentProps<"a">
41 |
42 | const PaginationLink = ({
43 | className,
44 | isActive,
45 | size = "icon",
46 | ...props
47 | }: PaginationLinkProps) => (
48 |
59 | )
60 | PaginationLink.displayName = "PaginationLink"
61 |
62 | const PaginationPrevious = ({
63 | className,
64 | ...props
65 | }: React.ComponentProps) => (
66 |
72 |
73 | Previous
74 |
75 | )
76 | PaginationPrevious.displayName = "PaginationPrevious"
77 |
78 | const PaginationNext = ({
79 | className,
80 | ...props
81 | }: React.ComponentProps) => (
82 |
88 | Next
89 |
90 |
91 | )
92 | PaginationNext.displayName = "PaginationNext"
93 |
94 | const PaginationEllipsis = ({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"span">) => (
98 |
103 |
104 | More pages
105 |
106 | )
107 | PaginationEllipsis.displayName = "PaginationEllipsis"
108 |
109 | export {
110 | Pagination,
111 | PaginationContent,
112 | PaginationEllipsis,
113 | PaginationItem,
114 | PaginationLink,
115 | PaginationNext,
116 | PaginationPrevious,
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { GripVertical } from "lucide-react"
4 | import * as ResizablePrimitive from "react-resizable-panels"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | )
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean
29 | }) => (
30 | div]:rotate-90",
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | )
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
46 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/components/ui/three-dots-wave.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 |
4 | const loadingContainer = {
5 | width: "2rem",
6 | height: "2rem",
7 | display: "flex",
8 | justifyContent: "space-around",
9 | };
10 |
11 | const loadingCircle = {
12 | display: "block",
13 | width: "0.4rem",
14 | height: "0.4rem",
15 | borderRadius: "0.25rem",
16 | };
17 |
18 | const loadingContainerVariants = {
19 | start: {
20 | transition: {
21 | staggerChildren: 0.2,
22 | },
23 | },
24 | end: {
25 | transition: {
26 | staggerChildren: 0.2,
27 | },
28 | },
29 | };
30 |
31 | const loadingCircleVariants = {
32 | start: {
33 | y: "50%",
34 | },
35 | end: {
36 | y: "150%",
37 | },
38 | };
39 |
40 | const loadingCircleTransition = {
41 | duration: 0.5,
42 | yoyo: Infinity,
43 | ease: "easeInOut",
44 | };
45 |
46 | interface ThreeDotsWaveProps {
47 | colorVariable?: string; // CSS variable for background color
48 | }
49 |
50 | export default function ThreeDotsWave({
51 | colorVariable = "--card",
52 | }: ThreeDotsWaveProps) {
53 | return (
54 |
60 |
68 |
76 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full mb-2",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { toggleVariants } from "@/components/ui/toggle"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default",
15 | })
16 |
17 | const ToggleGroup = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef &
20 | VariantProps
21 | >(({ className, variant, size, children, ...props }, ref) => (
22 |
27 |
28 | {children}
29 |
30 |
31 | ))
32 |
33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
34 |
35 | const ToggleGroupItem = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef &
38 | VariantProps
39 | >(({ className, children, variant, size, ...props }, ref) => {
40 | const context = React.useContext(ToggleGroupContext)
41 |
42 | return (
43 |
54 | {children}
55 |
56 | )
57 | })
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60 |
61 | export { ToggleGroup, ToggleGroupItem }
62 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-10 px-3 min-w-10",
20 | sm: "h-9 px-2.5 min-w-9",
21 | lg: "h-11 px-5 min-w-11",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/ui/transcriber.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 | import { motion, AnimatePresence } from "framer-motion";
6 | import { cn } from "@/lib/utils";
7 | import ThreeDotsWave from "@/components/ui/three-dots-wave";
8 | import { Conversation } from "@/lib/conversations";
9 | import { useTranslations } from "@/components/translations-context";
10 |
11 | /**
12 | * Avatar building blocks with Radix
13 | */
14 | const Avatar = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, ...props }, ref) => (
18 |
26 | ));
27 | Avatar.displayName = AvatarPrimitive.Root.displayName;
28 |
29 | const AvatarImage = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef
32 | >(({ className, ...props }, ref) => (
33 |
38 | ));
39 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
40 |
41 | const AvatarFallback = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, ...props }, ref) => (
45 |
53 | ));
54 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
55 |
56 | /**
57 | * Decide if a conversation item should be displayed or filtered out.
58 | * Optional, this is used to filter out empty or useless user messages (e.g., final + empty text)
59 | */
60 | function shouldDisplayMessage(msg: Conversation): boolean {
61 | const { role, text, status, isFinal } = msg;
62 |
63 | if (role === "assistant") {
64 | // Always display assistant messages (even if they're empty, though that’s rare).
65 | return true;
66 | } else {
67 | // User role
68 | // 1) If user is currently speaking or processing, we show it (wave or “Processing…”).
69 | if (status === "speaking" || status === "processing") {
70 | return true;
71 | }
72 | // 2) If user is final, only show if the transcript is non-empty.
73 | if (isFinal && text.trim().length > 0) {
74 | return true;
75 | }
76 | // Otherwise, skip.
77 | return false;
78 | }
79 | }
80 |
81 | /**
82 | * Single conversation item
83 | */
84 | function ConversationItem({ message }: { message: Conversation }) {
85 | const isUser = message.role === "user";
86 | const isAssistant = message.role === "assistant";
87 | const msgStatus = message.status;
88 |
89 | return (
90 |
96 | {/* Assistant Avatar */}
97 | {isAssistant && (
98 |
99 | {/* */}
100 | AI
101 |
102 | )}
103 |
104 | {/* Message Bubble */}
105 |
112 | {(isUser && msgStatus === "speaking") || msgStatus === "processing" ? (
113 | // Show wave animation for "speaking" status
114 |
115 | ) : (
116 | // Otherwise, show the message text or final text)
117 |
{message.text}
118 | )}
119 |
120 | {/* Timestamp below */}
121 |
122 | {new Date(message.timestamp).toLocaleTimeString("en-US", {
123 | hour: "numeric",
124 | minute: "numeric",
125 | })}
126 |
127 |
128 |
129 | {/* User Avatar */}
130 | {isUser && (
131 |
132 | {/* */}
133 | You
134 |
135 | )}
136 |
137 | );
138 | }
139 |
140 | interface TranscriberProps {
141 | conversation: Conversation[];
142 | }
143 |
144 |
145 | export default function Transcriber({ conversation }: TranscriberProps) {
146 | const scrollRef = React.useRef(null);
147 | const { t } = useTranslations();
148 |
149 | // Scroll to bottom whenever conversation updates
150 | React.useEffect(() => {
151 | if (scrollRef.current) {
152 | scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
153 | }
154 | }, [conversation]);
155 |
156 | // Filter out messages that we do not want to display
157 | const displayableMessages = React.useMemo(() => {
158 | return conversation.filter(shouldDisplayMessage);
159 | }, [conversation]);
160 |
161 | return (
162 |
163 | {/* Header */}
164 |
165 |
166 | {t('transcriber.title')}
167 |
168 |
169 |
170 | {/* Body */}
171 |
175 |
176 | {displayableMessages.map((message) => (
177 |
178 | ))}
179 |
180 |
181 |
182 | );
183 | }
184 |
185 | export { Avatar, AvatarImage, AvatarFallback };
186 |
--------------------------------------------------------------------------------
/components/voice-select.tsx:
--------------------------------------------------------------------------------
1 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
2 | import { useTranslations } from "@/components/translations-context"
3 | import { Label } from "@/components/ui/label"
4 |
5 | interface VoiceSelectorProps {
6 | value: string
7 | onValueChange: (value: string) => void
8 | }
9 |
10 | export function VoiceSelector({ value, onValueChange }: VoiceSelectorProps) {
11 | const { t } = useTranslations()
12 | return (
13 |
14 |
15 |
27 |
28 | )
29 | }
--------------------------------------------------------------------------------
/components/welcome.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge"
2 | import { Button } from "@/components/ui/button"
3 | import { GithubIcon } from "lucide-react"
4 | import Link from "next/link"
5 | import { useTranslations } from "@/components/translations-context"
6 |
7 | export const Welcome = () => {
8 | const { t } = useTranslations()
9 |
10 | return (
11 |
12 |
13 |
14 | {t('hero.badge')}
15 |
16 |
17 |
20 |
21 |
22 |
23 | {t('hero.title')}
24 |
25 |
26 | {t('hero.subtitle')}
27 |
28 |
29 | )
30 | }
31 |
32 | export default Welcome;
--------------------------------------------------------------------------------
/config/site.ts:
--------------------------------------------------------------------------------
1 | export const siteConfig = {
2 | name: "OpenAI Realtime Starter",
3 | url: "https://openai-rt-shadcn.vercel.app",
4 | description: "OpenAI Realtime Starter is a template for Realtime Voice AI with Shadcn UI components. It demonstrates tool calling with a WebRTC hook.",
5 | author: "cameronking4",
6 | links: {
7 | twitter: "https://x.com/cameronyking4",
8 | github: "https://github.com/cameronking4/shadcn-openai-realtime-api",
9 | portfolio: "https://www.linkedin.com/in/cameronyking",
10 | }
11 | };
12 |
13 | export type SiteConfig = typeof siteConfig;
14 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "unstable": [
3 | "unsafe-proto"
4 | ]
5 | }
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 4
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/hooks/use-tools.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { toast } from "sonner"
4 | import confetti from 'canvas-confetti'
5 | import { animate as framerAnimate } from "framer-motion"
6 | import { useTranslations } from "@/components/translations-context"
7 | import FirecrawlApp, { ScrapeResponse } from '@mendable/firecrawl-js';
8 |
9 | export const useToolsFunctions = () => {
10 | const { t } = useTranslations();
11 |
12 | const timeFunction = () => {
13 | const now = new Date()
14 | return {
15 | success: true,
16 | time: now.toLocaleTimeString(),
17 | timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
18 | message: t('tools.time') + now.toLocaleTimeString() + " in " + Intl.DateTimeFormat().resolvedOptions().timeZone + " timezone."
19 | }
20 | }
21 |
22 | const backgroundFunction = () => {
23 | try {
24 | const html = document.documentElement;
25 | const currentTheme = html.classList.contains('dark') ? 'dark' : 'light';
26 | const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
27 |
28 | html.classList.remove(currentTheme);
29 | html.classList.add(newTheme);
30 |
31 | toast(`Switched to ${newTheme} mode! 🌓`, {
32 | description: t('tools.switchTheme') + newTheme + ".",
33 | })
34 |
35 | return {
36 | success: true,
37 | theme: newTheme,
38 | message: t('tools.switchTheme') + newTheme + "."
39 | };
40 | } catch (error) {
41 | return {
42 | success: false,
43 | message: t('tools.themeFailed') + ": " + error
44 | };
45 | }
46 | }
47 |
48 | const partyFunction = () => {
49 | try {
50 | const duration = 5 * 1000
51 | const colors = ["#a786ff", "#fd8bbc", "#eca184", "#f8deb1", "#3b82f6", "#14b8a6", "#f97316", "#10b981", "#facc15"]
52 |
53 | const confettiConfig = {
54 | particleCount: 30,
55 | spread: 100,
56 | startVelocity: 90,
57 | colors,
58 | gravity: 0.5
59 | }
60 |
61 | const shootConfetti = (angle: number, origin: { x: number, y: number }) => {
62 | confetti({
63 | ...confettiConfig,
64 | angle,
65 | origin
66 | })
67 | }
68 |
69 | const animate = () => {
70 | const now = Date.now()
71 | const end = now + duration
72 |
73 | const elements = document.querySelectorAll('div, p, button, h1, h2, h3')
74 | elements.forEach((element) => {
75 | framerAnimate(element,
76 | {
77 | scale: [1, 1.1, 1],
78 | rotate: [0, 5, -5, 0],
79 | },
80 | {
81 | duration: 0.5,
82 | repeat: 10,
83 | ease: "easeInOut"
84 | }
85 | )
86 | })
87 |
88 | const frame = () => {
89 | if (Date.now() > end) return
90 | shootConfetti(60, { x: 0, y: 0.5 })
91 | shootConfetti(120, { x: 1, y: 0.5 })
92 | requestAnimationFrame(frame)
93 | }
94 |
95 | const mainElement = document.querySelector('main')
96 | if (mainElement) {
97 | mainElement.classList.remove('bg-gradient-to-b', 'from-gray-50', 'to-white')
98 | const originalBg = mainElement.style.backgroundColor
99 |
100 | const changeColor = () => {
101 | const now = Date.now()
102 | const end = now + duration
103 |
104 | const colorCycle = () => {
105 | if (Date.now() > end) {
106 | framerAnimate(mainElement,
107 | { backgroundColor: originalBg },
108 | { duration: 0.5 }
109 | )
110 | return
111 | }
112 | const newColor = colors[Math.floor(Math.random() * colors.length)]
113 | framerAnimate(mainElement,
114 | { backgroundColor: newColor },
115 | { duration: 0.2 }
116 | )
117 | setTimeout(colorCycle, 200)
118 | }
119 |
120 | colorCycle()
121 | }
122 |
123 | changeColor()
124 | }
125 |
126 | frame()
127 | }
128 |
129 | animate()
130 | toast.success(t('tools.partyMode.toast') + " 🎉", {
131 | description: t('tools.partyMode.description'),
132 | })
133 | return { success: true, message: t('tools.partyMode.success') + " 🎉" }
134 | } catch (error) {
135 | return { success: false, message: t('tools.partyMode.failed') + ": " + error }
136 | }
137 | }
138 |
139 | const launchWebsite = ({ url }: { url: string }) => {
140 | window.open(url, '_blank')
141 | toast(t('tools.launchWebsite') + " 🌐", {
142 | description: t('tools.launchWebsiteSuccess') + url + ", tell the user it's been launched.",
143 | })
144 | return {
145 | success: true,
146 | message: `Launched the site${url}, tell the user it's been launched.`
147 | }
148 | }
149 |
150 | const copyToClipboard = ({ text }: { text: string }) => {
151 | navigator.clipboard.writeText(text)
152 | toast(t('tools.clipboard.toast') + " 📋", {
153 | description: t('tools.clipboard.description'),
154 | })
155 | return {
156 | success: true,
157 | text,
158 | message: t('tools.clipboard.success')
159 | }
160 | }
161 |
162 | const scrapeWebsite = async ({ url }: { url: string }) => {
163 | const apiKey = process.env.NEXT_PUBLIC_FIRECRAWL_API_KEY;
164 | try {
165 | const app = new FirecrawlApp({ apiKey: apiKey });
166 | const scrapeResult = await app.scrapeUrl(url, { formats: ['markdown', 'html'] }) as ScrapeResponse;
167 |
168 | if (!scrapeResult.success) {
169 | console.log(scrapeResult.error)
170 | return {
171 | success: false,
172 | message: `Failed to scrape: ${scrapeResult.error}`
173 | };
174 | }
175 |
176 | toast.success(t('tools.scrapeWebsite.toast') + " 📋", {
177 | description: t('tools.scrapeWebsite.success'),
178 | })
179 |
180 | return {
181 | success: true,
182 | message: "Here is the scraped website content: " + JSON.stringify(scrapeResult.markdown) + "Summarize and explain it to the user now in a response."
183 | };
184 |
185 | } catch (error) {
186 | return {
187 | success: false,
188 | message: `Error scraping website: ${error}`
189 | };
190 | }
191 | }
192 |
193 | return {
194 | timeFunction,
195 | backgroundFunction,
196 | partyFunction,
197 | launchWebsite,
198 | copyToClipboard,
199 | scrapeWebsite
200 | }
201 | }
--------------------------------------------------------------------------------
/lib/conversations.ts:
--------------------------------------------------------------------------------
1 | interface Conversation {
2 | id: string; // Unique ID for react rendering and loggin purposes
3 | role: string; // "user" or "assistant"
4 | text: string; // User or assistant message
5 | timestamp: string; // ISO string for message time
6 | isFinal: boolean; // Whether the transcription is final
7 | status?: "speaking" | "processing" | "final"; // Status for real-time conversation states
8 | }
9 |
10 | export type { Conversation };
11 |
--------------------------------------------------------------------------------
/lib/tools.ts:
--------------------------------------------------------------------------------
1 | // Add interface for tools
2 | interface Tool {
3 | type: 'function';
4 | name: string;
5 | description: string;
6 | parameters?: {
7 | type: string;
8 | properties: Record;
12 | };
13 | }
14 |
15 | const toolDefinitions = {
16 | getCurrentTime: {
17 | description: 'Gets the current time in the user\'s timezone',
18 | parameters: {}
19 | },
20 | changeBackgroundColor: {
21 | description: 'Changes the background color of the page',
22 | parameters: {
23 | color: {
24 | type: 'string',
25 | description: 'Color value (hex, rgb, or color name)'
26 | }
27 | }
28 | },
29 | partyMode: {
30 | description: 'Triggers a confetti animation on the page',
31 | parameters: {}
32 | },
33 | launchWebsite: {
34 | description: 'Launches a website in the user\'s browser',
35 | parameters: {
36 | url: {
37 | type: 'string',
38 | description: 'The URL to launch'
39 | }
40 | }
41 | },
42 | copyToClipboard: {
43 | description: 'Copies text to the user\'s clipboard',
44 | parameters: {
45 | text: {
46 | type: 'string',
47 | description: 'The text to copy'
48 | }
49 | }
50 | },
51 | takeScreenshot: {
52 | description: 'Takes a screenshot of the current page',
53 | parameters: {}
54 | },
55 | scrapeWebsite: {
56 | description: 'Scrapes a URL and returns content in markdown and HTML formats',
57 | parameters: {
58 | url: {
59 | type: 'string',
60 | description: 'The URL to scrape'
61 | }
62 | }
63 | }
64 | } as const;
65 |
66 | const tools: Tool[] = Object.entries(toolDefinitions).map(([name, config]) => ({
67 | type: "function",
68 | name,
69 | description: config.description,
70 | parameters: {
71 | type: 'object',
72 | properties: config.parameters
73 | }
74 | }));
75 |
76 |
77 | export type { Tool };
78 | export { tools };
--------------------------------------------------------------------------------
/lib/translations/en.ts:
--------------------------------------------------------------------------------
1 | export const en = {
2 | broadcast: {
3 | end: "End Broadcasting",
4 | live: "Live",
5 | start: "Start Broadcasting"
6 | },
7 | header: {
8 | title: "About",
9 | about: "This is a project that aims to demonstrate how to use OpenAI Realtime API with WebRTC in a modern Next 15 project. It has shadcn/ui components already installed and the WebRTC audio session hook already implemented. Clone the project and define your own tools.",
10 | banner: "🎉 Check out the new OpenAI Realtime Blocks UI Library for Next.js!",
11 | bannerLink: "Learn more →",
12 | beta: "Beta",
13 | dark: "Dark",
14 | github: "Star on GitHub",
15 | language: "Language",
16 | light: "Light",
17 | logo: "OpenAI Realtime Starter",
18 | system: "System",
19 | theme: "Toggle theme",
20 | twitter: "Follow on"
21 | },
22 | hero: {
23 | badge: "Next.js + shadcn/ui",
24 | subtitle: "Demo by clicking the button below and try available tools",
25 | title: "OpenAI Realtime API (WebRTC)"
26 | },
27 | messageControls: {
28 | content: "Content",
29 | filter: "Filter by type",
30 | log: "Log to Console",
31 | logs: "Conversation Logs",
32 | search: "Search messages...",
33 | type: "Type",
34 | view: "View Logs"
35 | },
36 | status: {
37 | error: "Whoops!",
38 | info: "Toggling Voice Assistant...",
39 | language: "Language switched from",
40 | session: "Session established",
41 | success: "We're live, baby!",
42 | toggle: "Toggling Voice Assistant..."
43 | },
44 | tokenUsage: {
45 | input: "Input Tokens",
46 | output: "Output Tokens",
47 | total: "Total Tokens",
48 | usage: "Token Usage"
49 | },
50 | tools: {
51 | availableTools: {
52 | title: "Available Tools",
53 | copyFn: {
54 | description: 'Say "Copy that to clipboard" to paste it somewhere.',
55 | name: "Copy Fn"
56 | },
57 | getTime: {
58 | description: 'Ask "Tell me what time is it?" to get current time.',
59 | name: "Get Time"
60 | },
61 | launchWebsite: {
62 | description: '"Take me to [website]" to launch a site in a new tab.',
63 | name: "Launch Website"
64 | },
65 | partyMode: {
66 | description: 'Say "Start party mode" for a dynamic confetti animation!',
67 | name: "Party Mode"
68 | },
69 | themeSwitcher: {
70 | description: 'Say "Change background" or "Switch to dark mode" or "Switch to light mode".',
71 | name: "Theme Switcher"
72 | },
73 | scrapeWebsite: {
74 | name: "Website Scraper",
75 | description: 'Say "Scrape [website URL]" to extract content from a webpage.'
76 | }
77 | },
78 | clipboard: {
79 | description: "You can now paste it somewhere.",
80 | success: "Text copied to clipboard. Ask the user to paste it somewhere.",
81 | toast: "Text copied to clipboard!"
82 | },
83 | launchWebsite: {
84 | description: "Failed to launch website",
85 | success: "Launched the site! Tell the user it's been launched.",
86 | toast: "Launching website "
87 | },
88 | partyMode: {
89 | description: "Failed to activate party mode",
90 | success: "Party mode activated",
91 | toast: "Party mode!",
92 | failed: "Failed to activate party mode",
93 | },
94 | switchTheme: "Theme switched to ",
95 | themeFailed: "Failed to switch theme",
96 | time: "Announce to user: The current time is ",
97 | scrapeWebsite: {
98 | success: "Website content extracted successfully",
99 | description: "Failed to scrape website content",
100 | toast: "Scraping website..."
101 | }
102 | },
103 | transcriber: {
104 | title: "Live Transcript"
105 | },
106 | voice: {
107 | select: "Select a voice",
108 | ash: "Ash - Gentle & Professional",
109 | ballad: "Ballad - Warm & Engaging",
110 | coral: "Coral - Clear & Friendly",
111 | sage: "Sage - Authoritative & Calm",
112 | verse: "Verse - Dynamic & Expressive"
113 | },
114 | language: "English",
115 | languagePrompt: "Speak and respond only in English. It is crucial that you maintain your responses in English. If the user speaks in other languages, you should still respond in English."
116 | }
--------------------------------------------------------------------------------
/lib/translations/es.ts:
--------------------------------------------------------------------------------
1 | export const es = {
2 | broadcast: {
3 | end: "Finalizar Transmisión",
4 | live: "En Vivo",
5 | start: "Iniciar Transmisión"
6 | },
7 | header: {
8 | title: "Acerca de",
9 | about: "Este es un proyecto que pretende demostrar cómo usar la API en tiempo real de OpenAI con WebRTC en un proyecto moderno de Next 15. Tiene componentes shadcn/ui ya instalados y el hook de sesión de audio WebRTC ya implementado. Clona el proyecto y define tus propias herramientas.",
10 | banner: "🎉 ¡Descubre la nueva biblioteca OpenAI Realtime Blocks UI para Next.js!",
11 | bannerLink: "Saber más →",
12 | beta: "Beta",
13 | dark: "Oscuro",
14 | github: "Estrella en GitHub",
15 | language: "Idioma",
16 | light: "Claro",
17 | logo: "OpenAI Realtime Starter",
18 | system: "Sistema",
19 | theme: "Cambiar tema",
20 | twitter: "Seguir en"
21 | },
22 | hero: {
23 | badge: "Next.js + shadcn/ui",
24 | subtitle: "Haga una demostración haciendo clic en el botón de abajo y pruebe las herramientas disponibles",
25 | title: "API en tiempo real de OpenAI (WebRTC)"
26 | },
27 | messageControls: {
28 | content: "Contenido",
29 | filter: "Filtrar por tipo",
30 | log: "Registrar en Consola",
31 | logs: "Registros de Conversación",
32 | search: "Buscar mensajes...",
33 | type: "Tipo",
34 | view: "Ver Registros"
35 | },
36 | status: {
37 | error: "¡Ups!",
38 | info: "Alternando Asistente de Voz...",
39 | language: "Idioma cambiado de",
40 | session: "Sesión establecida",
41 | success: "¡Estamos en vivo!",
42 | toggle: "Alternando Asistente de Voz..."
43 | },
44 | tokenUsage: {
45 | input: "Tokens de Entrada",
46 | output: "Tokens de Salida",
47 | total: "Tokens Totales",
48 | usage: "Uso de Tokens"
49 | },
50 | tools: {
51 | availableTools: {
52 | title: "Herramientas Disponibles",
53 | copyFn: {
54 | description: 'Di "Copiar eso al portapapeles" para pegarlo en algún lugar.',
55 | name: "Función Copiar"
56 | },
57 | getTime: {
58 | description: 'Pregunta "¿Qué hora es?" para obtener la hora actual.',
59 | name: "Obtener Hora"
60 | },
61 | launchWebsite: {
62 | description: '"Llévame a [sitio web]" para abrir un sitio en una nueva pestaña.',
63 | name: "Abrir Sitio Web"
64 | },
65 | partyMode: {
66 | description: '¡Di "Iniciar modo fiesta" para una animación dinámica de confeti!',
67 | name: "Modo Fiesta"
68 | },
69 | themeSwitcher: {
70 | description: 'Di "Cambiar fondo" o "Cambiar a modo oscuro" o "Cambiar a modo claro".',
71 | name: "Cambiar Tema"
72 | },
73 | scrapeWebsite: {
74 | name: "Extractor de Sitios Web",
75 | description: 'Di "Extraer contenido de [URL del sitio]" para obtener contenido de una página web.'
76 | }
77 | },
78 | clipboard: {
79 | description: "Ahora puedes pegarlo en algún lugar.",
80 | success: "Texto copiado al portapapeles. Pide al usuario que lo pegue en algún lugar.",
81 | toast: "¡Texto copiado al portapapeles!"
82 | },
83 | launchWebsite: {
84 | description: "Error al abrir el sitio web",
85 | success: "¡Sitio web abierto! Informa al usuario que se ha abierto.",
86 | toast: "Abriendo sitio web "
87 | },
88 | partyMode: {
89 | description: "Error al activar el modo fiesta",
90 | success: "Modo fiesta activado",
91 | toast: "¡Modo fiesta!"
92 | },
93 | switchTheme: "Tema cambiado a ",
94 | themeFailed: "Error al cambiar el tema",
95 | time: "Anunciar al usuario: La hora actual es ",
96 | scrapeWebsite: {
97 | success: "Contenido del sitio web extraído exitosamente",
98 | description: "Error al extraer contenido del sitio web",
99 | toast: "Extrayendo contenido del sitio web..."
100 | }
101 | },
102 | transcriber: {
103 | title: "Transcripción en Vivo"
104 | },
105 | voice: {
106 | select: "Seleccionar una voz",
107 | ash: "Ash - Suave y Profesional",
108 | ballad: "Ballad - Cálida y Cautivadora",
109 | coral: "Coral - Clara y Amigable",
110 | sage: "Sage - Autoritaria y Tranquila",
111 | verse: "Verse - Dinámica y Expresiva"
112 | },
113 | language: "Spanish",
114 | languagePrompt: "Habla y responde solo en español. Es crucial que mantengas tus respuestas en español. Si el usuario habla en otros idiomas, deberías responder en español. (Spanish only)"
115 | }
--------------------------------------------------------------------------------
/lib/translations/fr.ts:
--------------------------------------------------------------------------------
1 | export const fr = {
2 | broadcast: {
3 | end: "Arrêter la Diffusion",
4 | live: "En Direct",
5 | start: "Démarrer la Diffusion"
6 | },
7 | header: {
8 | title: "À propos",
9 | about: "Ce projet vise à démontrer comment utiliser l'API OpenAI Realtime avec WebRTC dans un projet Next 15 moderne. Il dispose déjà des composants shadcn/ui installés et du hook de session audio WebRTC implémenté. Clonez le projet et définissez vos propres outils.",
10 | banner: "🎉 Découvrez la nouvelle bibliothèque OpenAI Realtime Blocks UI pour Next.js !",
11 | bannerLink: "En savoir plus →",
12 | beta: "Bêta",
13 | dark: "Sombre",
14 | github: "Étoile sur GitHub",
15 | language: "Langue",
16 | light: "Clair",
17 | logo: "OpenAI Realtime Starter",
18 | system: "Système",
19 | theme: "Changer le thème",
20 | twitter: "Suivre sur"
21 | },
22 | hero: {
23 | badge: "Next.js + shadcn/ui",
24 | subtitle: "Faites une démo en cliquant sur le bouton ci-dessous et essayez les outils disponibles",
25 | title: "OpenAI Realtime API (WebRTC)"
26 | },
27 | messageControls: {
28 | content: "Contenu",
29 | filter: "Filtrer par type",
30 | log: "Journal dans la Console",
31 | logs: "Journaux de Conversation",
32 | search: "Rechercher des messages...",
33 | type: "Type",
34 | view: "Voir les Journaux"
35 | },
36 | status: {
37 | error: "Oups !",
38 | info: "Basculement de l'Assistant Vocal...",
39 | language: "Langue changée de",
40 | session: "Session établie",
41 | success: "Nous sommes en direct !",
42 | toggle: "Basculement de l'Assistant Vocal..."
43 | },
44 | tokenUsage: {
45 | input: "Tokens d'Entrée",
46 | output: "Tokens de Sortie",
47 | total: "Tokens Totaux",
48 | usage: "Utilisation des Tokens"
49 | },
50 | tools: {
51 | availableTools: {
52 | title: "Outils Disponibles",
53 | copyFn: {
54 | description: 'Dites "Copier ça dans le presse-papiers" pour le coller quelque part.',
55 | name: "Fonction Copier"
56 | },
57 | getTime: {
58 | description: 'Demandez "Quelle heure est-il ?" pour obtenir l\'heure actuelle.',
59 | name: "Obtenir l'Heure"
60 | },
61 | launchWebsite: {
62 | description: '"Emmène-moi sur [site web]" pour ouvrir un site dans un nouvel onglet.',
63 | name: "Lancer un Site Web"
64 | },
65 | partyMode: {
66 | description: 'Dites "Activer le mode fête" pour une animation de confettis dynamique !',
67 | name: "Mode Fête"
68 | },
69 | themeSwitcher: {
70 | description: 'Dites "Changer le fond" ou "Passer en mode sombre" ou "Passer en mode clair".',
71 | name: "Changeur de Thème"
72 | },
73 | scrapeWebsite: {
74 | name: "Extracteur de Site Web",
75 | description: 'Dites "Extraire le contenu de [URL du site]" pour récupérer le contenu d\'une page web.'
76 | }
77 | },
78 | clipboard: {
79 | description: "Vous pouvez maintenant le coller quelque part.",
80 | success: "Texte copié dans le presse-papiers. Demandez à l'utilisateur de le coller quelque part.",
81 | toast: "Texte copié dans le presse-papiers !"
82 | },
83 | launchWebsite: {
84 | description: "Échec du lancement du site web",
85 | success: "Site lancé ! Informez l'utilisateur qu'il a été lancé.",
86 | toast: "Lancement du site web "
87 | },
88 | partyMode: {
89 | description: "Échec de l'activation du mode fête",
90 | success: "Mode fête activé",
91 | toast: "Mode fête !"
92 | },
93 | switchTheme: "Thème changé en ",
94 | themeFailed: "Échec du changement de thème",
95 | time: "Annoncer à l'utilisateur : L'heure actuelle est ",
96 | scrapeWebsite: {
97 | success: "Contenu du site web extrait avec succès",
98 | description: "Échec de l'extraction du contenu du site web",
99 | toast: "Extraction du contenu du site web..."
100 | }
101 | },
102 | transcriber: {
103 | title: "Transcription en Direct"
104 | },
105 | voice: {
106 | select: "Sélectionner une voix",
107 | ash: "Ash - Douce et Professionnelle",
108 | ballad: "Ballad - Chaleureuse et Engageante",
109 | coral: "Coral - Claire et Amicale",
110 | sage: "Sage - Autoritaire et Calme",
111 | verse: "Verse - Dynamique et Expressive"
112 | },
113 | language: "Français",
114 | languagePrompt: "Parlez et répondez uniquement en français. Il est crucial que vous mainteniez vos réponses en français. Si l'utilisateur parle dans d'autres langues, vous devriez toujours répondre en français. (French only)"
115 | }
--------------------------------------------------------------------------------
/lib/translations/zh.ts:
--------------------------------------------------------------------------------
1 | export const zh = {
2 | broadcast: {
3 | end: "结束广播",
4 | live: "直播中",
5 | start: "开始广播"
6 | },
7 | header: {
8 | title: "关于",
9 | about: "这是一个旨在演示如何在现代 Next 15 项目中使用 OpenAI 实时 API 和 WebRTC 的项目。它已安装了 shadcn/ui 组件并实现了 WebRTC 音频会话钩子。克隆项目并定义您自己的工具。",
10 | banner: "🎉 快来看看全新的 Next.js OpenAI 实时块用户界面库!",
11 | bannerLink: "了解更多 →",
12 | beta: "测试版",
13 | dark: "深色",
14 | github: "在 GitHub 上标星",
15 | language: "语言",
16 | light: "浅色",
17 | logo: "OpenAI 实时启动器",
18 | system: "系统",
19 | theme: "切换主题",
20 | twitter: "在推特上关注"
21 | },
22 | hero: {
23 | badge: "Next.js + shadcn/ui",
24 | subtitle: "点击下方按钮进行演示并尝试可用工具",
25 | title: "OpenAI 实时 API (WebRTC)"
26 | },
27 | messageControls: {
28 | content: "内容",
29 | filter: "按类型筛选",
30 | log: "控制台日志",
31 | logs: "对话记录",
32 | search: "搜索消息...",
33 | type: "类型",
34 | view: "查看日志"
35 | },
36 | status: {
37 | error: "哎呀!",
38 | info: "正在切换语音助手...",
39 | language: "语言已从以下更改",
40 | session: "会话已建立",
41 | success: "我们已经开始直播了!",
42 | toggle: "正在切换语音助手..."
43 | },
44 | tokenUsage: {
45 | input: "输入令牌",
46 | output: "输出令牌",
47 | total: "总令牌数",
48 | usage: "令牌使用量"
49 | },
50 | tools: {
51 | availableTools: {
52 | title: "可用工具",
53 | copyFn: {
54 | description: '说"复制到剪贴板"以将其粘贴到某处。',
55 | name: "复制功能"
56 | },
57 | getTime: {
58 | description: '问"现在几点了?"以获取当前时间。',
59 | name: "获取时间"
60 | },
61 | launchWebsite: {
62 | description: '说"带我去[网站]"以在新标签页中打开网站。',
63 | name: "启动网站"
64 | },
65 | partyMode: {
66 | description: '说"开启派对模式"以启动动态彩带动画!',
67 | name: "派对模式"
68 | },
69 | themeSwitcher: {
70 | description: '说"更换背景"或"切换到深色模式"或"切换到浅色模式"。',
71 | name: "主题切换器"
72 | },
73 | scrapeWebsite: {
74 | name: "网页内容提取器",
75 | description: '说"提取[网站URL]的内容"来获取网页内容。'
76 | }
77 | },
78 | clipboard: {
79 | description: "您现在可以将其粘贴到某处。",
80 | success: "文本已复制到剪贴板。请让用户将其粘贴到某处。",
81 | toast: "文本已复制到剪贴板!"
82 | },
83 | launchWebsite: {
84 | description: "启动网站失败",
85 | success: "网站已启动!告诉用户网站已经打开。",
86 | toast: "正在启动网站 "
87 | },
88 | partyMode: {
89 | description: "无法激活派对模式",
90 | success: "派对模式已激活",
91 | toast: "派对模式!"
92 | },
93 | switchTheme: "主题已切换至 ",
94 | themeFailed: "主题切换失败",
95 | time: "向用户播报:当前时间是 ",
96 | scrapeWebsite: {
97 | success: "网页内容提取成功",
98 | description: "网页内容提取失败",
99 | toast: "正在提取网页内容..."
100 | }
101 | },
102 | transcriber: {
103 | title: "实时转录"
104 | },
105 | voice: {
106 | select: "选择语音",
107 | ash: "Ash - 温和专业",
108 | ballad: "Ballad - 温暖动人",
109 | coral: "Coral - 清晰友好",
110 | sage: "Sage - 权威平静",
111 | verse: "Verse - 富有表现力"
112 | },
113 | language: "Chinese",
114 | languagePrompt: "只用中文说话和回答。你一定要保持你的回答是中文的。如果用户说其他语言,你应该用中文回答。(Speak Chinese only)"
115 | }
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///