├── env.example ├── src ├── vite-env.d.ts ├── assets │ ├── logos │ │ ├── daily.png │ │ ├── llama3.png │ │ ├── cerebrium.png │ │ └── deepgram.png │ └── logo.svg ├── utils │ ├── tailwind.ts │ └── stats_aggregator.ts ├── components │ ├── Setup │ │ ├── index.tsx │ │ ├── RoomInput.tsx │ │ ├── Configure.tsx │ │ ├── RoomSetup.tsx │ │ ├── DeviceSelect.tsx │ │ └── SettingsList.tsx │ ├── AudioIndicator │ │ ├── styles.module.css │ │ └── index.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── header.tsx │ │ ├── field.tsx │ │ ├── helptip.tsx │ │ ├── switch.tsx │ │ ├── tooltip.tsx │ │ ├── input.tsx │ │ ├── alert.tsx │ │ ├── select.tsx │ │ ├── button.tsx │ │ └── card.tsx │ ├── Session │ │ ├── ExpiryTimer │ │ │ ├── styles.module.css │ │ │ └── index.tsx │ │ ├── Agent │ │ │ ├── avatar.tsx │ │ │ ├── model.tsx │ │ │ ├── face.svg │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── TranscriptOverlay │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── index.tsx │ ├── Latency │ │ ├── utils.ts │ │ ├── styles.module.css │ │ └── index.tsx │ ├── Configuration │ │ ├── ModelSelect.tsx │ │ ├── VoiceSelect.tsx │ │ ├── LanguageSelect.tsx │ │ └── index.tsx │ ├── UserMicBubble │ │ ├── index.tsx │ │ └── styles.module.css │ ├── logo.tsx │ └── Stats │ │ ├── styles.module.css │ │ └── index.tsx ├── actions.ts ├── types │ └── stats_aggregator.d.ts ├── Splash.tsx ├── main.tsx ├── config.ts ├── global.css └── App.tsx ├── public ├── social.png ├── silero_vad.onnx └── color-wash-bg.png ├── .dockerignore ├── .prettierrc ├── postcss.config.js ├── tsconfig.node.json ├── .gitignore ├── vercel.json ├── tsconfig.json ├── vite.config.ts ├── index.html ├── .eslintrc.cjs ├── README.md ├── package.json └── tailwind.config.js /env.example: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=https://rtvi.pipecat.bot/ -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-rtvi-web-demo/main/public/social.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | node_modules 3 | dist 4 | dist-ssr 5 | *.lock 6 | *.local 7 | *.*.local -------------------------------------------------------------------------------- /public/silero_vad.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-rtvi-web-demo/main/public/silero_vad.onnx -------------------------------------------------------------------------------- /public/color-wash-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-rtvi-web-demo/main/public/color-wash-bg.png -------------------------------------------------------------------------------- /src/assets/logos/daily.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-rtvi-web-demo/main/src/assets/logos/daily.png -------------------------------------------------------------------------------- /src/assets/logos/llama3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-rtvi-web-demo/main/src/assets/logos/llama3.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/logos/cerebrium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-rtvi-web-demo/main/src/assets/logos/cerebrium.png -------------------------------------------------------------------------------- /src/assets/logos/deepgram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-rtvi-web-demo/main/src/assets/logos/deepgram.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/tailwind.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Setup/index.tsx: -------------------------------------------------------------------------------- 1 | import { Configure } from "./Configure"; 2 | import { RoomInput } from "./RoomInput"; 3 | import { RoomSetup } from "./RoomSetup"; 4 | import { SettingsList } from "./SettingsList"; 5 | 6 | export { Configure, RoomInput, RoomSetup, SettingsList }; 7 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/AudioIndicator/styles.module.css: -------------------------------------------------------------------------------- 1 | .bar { 2 | background: theme(colors.gray.200); 3 | height: 8px; 4 | width: 100%; 5 | border-radius: 999px; 6 | overflow: hidden; 7 | 8 | > div { 9 | background: theme(colors.green.500); 10 | height: 8px; 11 | width: 0px; 12 | border-radius: 999px; 13 | transition: width 0.1s ease; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/utils/tailwind"; 4 | 5 | export const Label: React.FC<{ 6 | className?: string; 7 | children: React.ReactNode; 8 | }> = ({ className, children }) => ( 9 | 10 | ); 11 | 12 | Label.displayName = "Label"; 13 | -------------------------------------------------------------------------------- /src/components/ui/header.tsx: -------------------------------------------------------------------------------- 1 | import ExpiryTimer from "../Session/ExpiryTimer"; 2 | 3 | export function Header() { 4 | return ( 5 | 12 | ); 13 | } 14 | 15 | export default Header; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | /test-results/ 26 | /playwright-report/ 27 | /blob-report/ 28 | /playwright/.cache/ 29 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/", 5 | "headers": [ 6 | { 7 | "key": "Cross-Origin-Embedder-Policy", 8 | "value": "require-corp" 9 | }, 10 | { 11 | "key": "Cross-Origin-Opener-Policy", 12 | "value": "same-origin" 13 | } 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /src/actions.ts: -------------------------------------------------------------------------------- 1 | export const fetch_start_agent = async ( 2 | roomUrl: string | null, 3 | serverUrl: string 4 | ) => { 5 | const req = await fetch(`${serverUrl}`, { 6 | method: "POST", 7 | headers: { 8 | "Content-Type": "application/json", 9 | }, 10 | body: JSON.stringify({ room_url: roomUrl }), 11 | }); 12 | 13 | const data = await req.json(); 14 | 15 | if (!req.ok) { 16 | return { error: true, detail: data.detail }; 17 | } 18 | return data; 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Setup/RoomInput.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from "../ui/field"; 2 | import { Input } from "../ui/input"; 3 | 4 | interface RoomInputProps { 5 | onChange: (url: string) => void; 6 | error?: string | undefined | boolean; 7 | } 8 | 9 | export const RoomInput: React.FC = ({ onChange, error }) => { 10 | return ( 11 | 12 | onChange(e.target.value)} 17 | className="w-full" 18 | /> 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Session/ExpiryTimer/styles.module.css: -------------------------------------------------------------------------------- 1 | .expiry { 2 | margin-left: auto; 3 | display: flex; 4 | flex-flow: row nowrap; 5 | font-size: theme(fontSize.sm); 6 | background-color: theme(colors.primary.100); 7 | border-radius: theme(borderRadius.lg); 8 | padding: theme(spacing.2) theme(spacing.3); 9 | border-top: 1px solid theme(colors.primary.200); 10 | gap: theme(spacing[1.5]); 11 | 12 | svg { 13 | color: theme(colors.primary.400); 14 | } 15 | } 16 | 17 | .expired { 18 | color: theme(colors.primary.400); 19 | } 20 | 21 | .time { 22 | font-weight: theme(fontWeight.semibold); 23 | letter-spacing: theme(letterSpacing.wider); 24 | width: 60px; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ui/field.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/utils/tailwind"; 4 | 5 | import { Label } from "./label"; 6 | 7 | interface FieldProps extends React.HTMLAttributes { 8 | label: string; 9 | error?: string | undefined | boolean; 10 | } 11 | 12 | export const Field: React.FC = ({ 13 | className, 14 | label, 15 | error, 16 | children, 17 | }) => ( 18 |
19 | 20 | {children} 21 | {error &&

{error}

} 22 |
23 | ); 24 | 25 | Field.displayName = "Field"; 26 | -------------------------------------------------------------------------------- /src/components/AudioIndicator/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from "react"; 2 | import { RTVIEvent } from "realtime-ai"; 3 | import { useRTVIClientEvent } from "realtime-ai-react"; 4 | 5 | import styles from "./styles.module.css"; 6 | 7 | export const AudioIndicatorBar: React.FC = () => { 8 | const volRef = useRef(null); 9 | 10 | useRTVIClientEvent( 11 | RTVIEvent.LocalAudioLevel, 12 | useCallback((volume: number) => { 13 | if (volRef.current) 14 | volRef.current.style.width = Math.max(2, volume * 100) + "%"; 15 | }, []) 16 | ); 17 | 18 | return ( 19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default AudioIndicatorBar; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["src/*"] 12 | }, 13 | 14 | /* Bundler mode */ 15 | "moduleResolution": "bundler", 16 | "allowImportingTsExtensions": true, 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true 27 | }, 28 | "include": ["src"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ui/helptip.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CircleHelp } from "lucide-react"; 3 | 4 | import { cn } from "@/utils/tailwind"; 5 | 6 | import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; 7 | 8 | interface HelpTipProps { 9 | text: string; 10 | className?: string; 11 | } 12 | 13 | const HelpTip: React.FC = ({ text, className }) => { 14 | return ( 15 | 16 | 17 | 22 | 23 | {text} 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default HelpTip; 30 | -------------------------------------------------------------------------------- /src/types/stats_aggregator.d.ts: -------------------------------------------------------------------------------- 1 | type Stat = [string, string, number, number]; 2 | 3 | interface MetricValue { 4 | latest: number; 5 | timeseries: number[]; 6 | median: number | null; 7 | high: number | null; 8 | low: number | null; 9 | } 10 | 11 | interface Metric { 12 | [metric: string]: MetricValue; 13 | } 14 | 15 | interface StatsMap { 16 | [service: string]: Metric; 17 | } 18 | 19 | interface IStatsAggregator { 20 | statsMap: StatsMap; 21 | hasNewStats: boolean; 22 | turns: number; 23 | 24 | addStat(stat: Stat): void; 25 | getStats(): StatsMap | null; 26 | } 27 | 28 | declare class StatsAggregator implements IStatsAggregator { 29 | statsMap: StatsMap; 30 | hasNewStats: boolean; 31 | turns: number; 32 | 33 | constructor(); 34 | addStat(stat: Stat): void; 35 | getStats(): StatsMap | null; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Latency/utils.ts: -------------------------------------------------------------------------------- 1 | export function calculateMedian(deltaArray: number[]) { 2 | // sort array ascending 3 | const asc = (arr: number[]) => arr.sort((a, b) => a - b); 4 | 5 | const quantile = (arr: number[], q: number) => { 6 | const sorted = asc(arr); 7 | const pos = (sorted.length - 1) * q; 8 | const base = Math.floor(pos); 9 | const rest = pos - base; 10 | if (sorted[base + 1] !== undefined) { 11 | return sorted[base] + rest * (sorted[base + 1] - sorted[base]); 12 | } else { 13 | return sorted[base]; 14 | } 15 | }; 16 | 17 | //const q25 = (arr) => quantile(arr, 0.25); 18 | 19 | const q50 = (arr: number[]) => quantile(arr, 0.5); 20 | 21 | //const q75 = (arr) => quantile(arr, 0.75); 22 | 23 | const median = (arr: number[]) => q50(arr); 24 | 25 | return median(deltaArray); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Session/Agent/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from "react"; 2 | import { RTVIEvent } from "realtime-ai"; 3 | import { useRTVIClientEvent } from "realtime-ai-react"; 4 | 5 | import FaceSVG from "./face.svg"; 6 | 7 | import styles from "./styles.module.css"; 8 | 9 | export const Avatar: React.FC = () => { 10 | const volRef = useRef(null); 11 | 12 | useRTVIClientEvent( 13 | RTVIEvent.RemoteAudioLevel, 14 | useCallback((volume: number) => { 15 | if (!volRef.current) return; 16 | volRef.current.style.transform = `scale(${Math.max(1, 1 + volume)})`; 17 | }, []) 18 | ); 19 | 20 | return ( 21 | <> 22 | Face 23 |
24 | 25 | ); 26 | }; 27 | 28 | export default Avatar; 29 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import path from "path"; 3 | import { defineConfig } from "vite"; 4 | import { viteStaticCopy } from "vite-plugin-static-copy"; 5 | import webfontDownload from "vite-plugin-webfont-dl"; 6 | 7 | export default defineConfig({ 8 | assetsInclude: ["**/*.onnx"], 9 | server: { 10 | headers: { 11 | "Cross-Origin-Embedder-Policy": "require-corp", 12 | "Cross-Origin-Opener-Policy": "same-origin", 13 | }, 14 | }, 15 | resolve: { 16 | alias: { 17 | "@": path.resolve(__dirname, "./src"), 18 | }, 19 | }, 20 | plugins: [ 21 | react(), 22 | webfontDownload(), 23 | viteStaticCopy({ 24 | targets: [ 25 | { 26 | src: "node_modules/onnxruntime-web/dist/*.wasm", 27 | dest: "./", 28 | }, 29 | ], 30 | }), 31 | ], 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/Configuration/ModelSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Package } from "lucide-react"; 3 | 4 | import { LLMModel, llmModels } from "../../config"; 5 | import { Field } from "../ui/field"; 6 | import { Select } from "../ui/select"; 7 | 8 | type ModelSelectProps = { 9 | onSelect: (model: string) => void; 10 | }; 11 | 12 | const ModelSelect: React.FC = ({ onSelect }) => { 13 | return ( 14 | 15 | 25 | 26 | ); 27 | }; 28 | 29 | export default ModelSelect; 30 | -------------------------------------------------------------------------------- /src/components/Configuration/VoiceSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MessageCircle } from "lucide-react"; 3 | 4 | import { ttsVoices, Voice } from "../../config"; 5 | import { Field } from "../ui/field"; 6 | import { Select } from "../ui/select"; 7 | 8 | type VoiceSelectProps = { 9 | onSelect: (voice: Voice) => void; 10 | }; 11 | 12 | const VoiceSelect: React.FC = ({ onSelect }) => { 13 | return ( 14 | 15 | 25 | 26 | ); 27 | }; 28 | 29 | export default VoiceSelect; 30 | -------------------------------------------------------------------------------- /src/components/Configuration/LanguageSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MessageCircle } from "lucide-react"; 3 | 4 | import { ttsVoices, Voice } from "../../config"; 5 | import { Field } from "../ui/field"; 6 | import { Select } from "../ui/select"; 7 | 8 | type LanguageSelectProps = { 9 | onSelect: (voice: Voice) => void; 10 | }; 11 | 12 | const LanguageSelect: React.FC = ({ onSelect }) => { 13 | return ( 14 | 15 | 25 | 26 | ); 27 | }; 28 | 29 | export default LanguageSelect; 30 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/Session/Agent/model.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { RTVIClientConfigOption, RTVIEvent } from "realtime-ai"; 3 | import { useRTVIClient, useRTVIClientEvent } from "realtime-ai-react"; 4 | 5 | import styles from "./styles.module.css"; 6 | 7 | const ModelBadge: React.FC = () => { 8 | const rtviClient = useRTVIClient()!; 9 | const [model, setModel] = React.useState(); 10 | 11 | React.useEffect(() => { 12 | const getInitialModel = async () => { 13 | const modelValue = await rtviClient.getServiceOptionValueFromConfig( 14 | "llm", 15 | "model" 16 | ); 17 | setModel(modelValue as string); 18 | }; 19 | getInitialModel(); 20 | }, [rtviClient]); 21 | 22 | useRTVIClientEvent( 23 | RTVIEvent.Config, 24 | useCallback((config: RTVIClientConfigOption[]) => { 25 | const llmConfig = config.find((c) => c.service === "llm"); 26 | const modelOption = llmConfig?.options.find( 27 | (opt) => opt.name === "model" 28 | ); 29 | setModel(modelOption?.value as string); 30 | }, []) 31 | ); 32 | 33 | return
{model}
; 34 | }; 35 | 36 | export default ModelBadge; 37 | -------------------------------------------------------------------------------- /src/components/Session/Agent/face.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | RTVI Web Demo 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh", "simple-import-sort"], 12 | rules: { 13 | "simple-import-sort/imports": [ 14 | "error", 15 | { 16 | groups: [ 17 | // Packages `react` related packages come first. 18 | ["^react", "^@?\\w"], 19 | // Internal packages. 20 | ["^(@|components)(/.*|$)"], 21 | // Side effect imports. 22 | ["^\\u0000"], 23 | // Parent imports. Put `..` last. 24 | ["^\\.\\.(?!/?$)", "^\\.\\./?$"], 25 | // Other relative imports. Put same-folder imports and `.` last. 26 | ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], 27 | // Style imports. 28 | ["^.+\\.?(css)$"], 29 | ], 30 | }, 31 | ], 32 | "simple-import-sort/exports": "error", 33 | "react-refresh/only-export-components": [ 34 | "warn", 35 | { allowConstantExport: true }, 36 | ], 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 3 | 4 | import { cn } from "@/utils/tailwind"; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )); 25 | Switch.displayName = SwitchPrimitives.Root.displayName; 26 | 27 | export { Switch }; 28 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 3 | 4 | import { cn } from "../../utils/tailwind"; 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider; 7 | 8 | const Tooltip = TooltipPrimitive.Root; 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger; 11 | 12 | const TooltipContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, sideOffset = 4, ...props }, ref) => ( 16 | 25 | )); 26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 27 | 28 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; 29 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/utils/tailwind"; 5 | 6 | const inputVariants = cva( 7 | "flex h-12 px-3 w-full rounded-xl border border-primary-200 bg-background text-sm ring-ring file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 8 | { 9 | variants: { 10 | variant: { 11 | default: "", 12 | danger: 13 | "border-red-500 text-red-500 focus-visible:ring-red-500 placeholder:text-red-300", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ); 21 | 22 | export interface InputProps 23 | extends React.InputHTMLAttributes, 24 | VariantProps {} 25 | 26 | export const Input: React.FC = ({ 27 | variant, 28 | className, 29 | type, 30 | ...props 31 | }) => { 32 | return ( 33 | 38 | ); 39 | }; 40 | Input.displayName = "Input"; 41 | -------------------------------------------------------------------------------- /src/components/Session/TranscriptOverlay/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, useState } from "react"; 2 | import { BotLLMTextData, RTVIEvent } from "realtime-ai"; 3 | import { useRTVIClientEvent } from "realtime-ai-react"; 4 | 5 | import styles from "./styles.module.css"; 6 | 7 | const TranscriptOverlay: React.FC = () => { 8 | const [sentences, setSentences] = useState([]); 9 | const intervalRef = useRef(null); 10 | 11 | useEffect(() => { 12 | clearInterval(intervalRef.current!); 13 | 14 | intervalRef.current = setInterval(() => { 15 | if (sentences.length > 2) { 16 | setSentences((s) => s.slice(1)); 17 | } 18 | }, 2500); 19 | 20 | return () => clearInterval(intervalRef.current!); 21 | }, [sentences]); 22 | 23 | useRTVIClientEvent( 24 | RTVIEvent.BotTranscript, 25 | useCallback((data: BotLLMTextData) => { 26 | setSentences((s) => [...s, data.text]); 27 | }, []) 28 | ); 29 | 30 | return ( 31 |
32 | {sentences.map((sentence, index) => ( 33 | 34 | {sentence} 35 | 36 | ))} 37 |
38 | ); 39 | }; 40 | 41 | export default TranscriptOverlay; 42 | -------------------------------------------------------------------------------- /src/components/Setup/Configure.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Configuration from "../Configuration"; 4 | import HelpTip from "../ui/helptip"; 5 | import { Label } from "../ui/label"; 6 | import { Switch } from "../ui/switch"; 7 | 8 | import DeviceSelect from "./DeviceSelect"; 9 | 10 | interface ConfigureProps { 11 | startAudioOff: boolean; 12 | handleStartAudioOff: () => void; 13 | } 14 | 15 | export const Configure: React.FC = ({ 16 | startAudioOff, 17 | handleStartAudioOff, 18 | }) => { 19 | return ( 20 | <> 21 |
22 | 23 | 24 |
25 | 26 |
27 |
28 | 32 | 36 |
37 |
38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/stats_aggregator.ts: -------------------------------------------------------------------------------- 1 | class StatsAggregator implements IStatsAggregator { 2 | statsMap: StatsMap = {}; 3 | hasNewStats = false; 4 | turns = 0; 5 | 6 | constructor() {} 7 | 8 | addStat(stat: Stat) { 9 | const [service, metric, value] = stat; 10 | if (!service || !metric || value <= 0) { 11 | return; 12 | } 13 | 14 | // Ensure the service exists in statsMap 15 | if (!this.statsMap[service]) { 16 | this.statsMap[service] = {}; 17 | } 18 | 19 | const timeseries = [ 20 | ...(this.statsMap[service][metric]?.timeseries || []), 21 | value, 22 | ]; 23 | 24 | const median = 25 | timeseries.reduce((acc, curr) => acc + curr, 0) / timeseries.length; 26 | const high = timeseries.reduce((acc, curr) => Math.max(acc, curr), 0); 27 | const low = timeseries.reduce((acc, curr) => Math.min(acc, curr), Infinity); 28 | 29 | this.statsMap[service][metric] = { 30 | latest: value, 31 | timeseries, 32 | median, 33 | high, 34 | low, 35 | }; 36 | 37 | this.hasNewStats = true; 38 | } 39 | 40 | getStats(): StatsMap | null { 41 | if (this.hasNewStats) { 42 | this.hasNewStats = false; 43 | return this.statsMap; 44 | } else { 45 | return null; 46 | } 47 | } 48 | } 49 | 50 | export default StatsAggregator; 51 | -------------------------------------------------------------------------------- /src/components/Session/TranscriptOverlay/styles.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | left: 1rem; 4 | right: 1rem; 5 | bottom: 1.5rem; 6 | top: 1.5rem; 7 | color: white; 8 | z-index: 50; 9 | margin: 0 auto; 10 | display: flex; 11 | flex-direction: column; 12 | gap: 8px; 13 | align-items: center; 14 | justify-content: end; 15 | text-align: center; 16 | } 17 | 18 | .transcript { 19 | font-weight: 600; 20 | font-size: theme(fontSize.sm); 21 | max-width: 320px; 22 | } 23 | 24 | .transcript span { 25 | box-decoration-break: clone; 26 | -webkit-box-decoration-break: clone; 27 | -moz-box-decoration-break: clone; 28 | background-color: color-mix( 29 | in srgb, 30 | theme(colors.primary.800), 31 | transparent 30% 32 | ); 33 | border-radius: theme(borderRadius.md); 34 | padding: 4px 8px; 35 | line-height: 1; 36 | } 37 | 38 | .sentence { 39 | opacity: 1; 40 | margin: 0px; 41 | animation: fadeOut 2.5s linear forwards; 42 | animation-delay: 1s; 43 | line-height: 2; 44 | } 45 | 46 | @keyframes fadeOut { 47 | 0% { 48 | opacity: 1; 49 | transform: scale(1); 50 | } 51 | 20% { 52 | transform: scale(1); 53 | filter: blur(0); 54 | } 55 | 100% { 56 | transform: scale(0.8) translateY(-50%); 57 | filter: blur(25px); 58 | opacity: 0; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cva, VariantProps } from "class-variance-authority"; 3 | import { CircleAlert } from "lucide-react"; 4 | 5 | import { cn } from "@/utils/tailwind"; 6 | 7 | const alertVariants = cva("text-left border border-black rounded-lg p-4", { 8 | variants: { 9 | intent: { 10 | info: "alert-info", 11 | danger: "border-red-200 text-red-600 bg-red-50", 12 | }, 13 | }, 14 | defaultVariants: { 15 | intent: "info", 16 | }, 17 | }); 18 | 19 | export interface AlertProps 20 | extends React.HTMLAttributes, 21 | VariantProps {} 22 | 23 | export const Alert: React.FC = ({ children, intent, title }) => { 24 | return ( 25 |
26 | 27 | {intent === "danger" && } 28 | {title} 29 | 30 | {children} 31 |
32 | ); 33 | }; 34 | 35 | export const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |

47 | )); 48 | AlertTitle.displayName = "AlertTitle"; 49 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/utils/tailwind"; 5 | 6 | const selectVariants = cva( 7 | "appearance-none bg-none bg-white bg-selectArrow bg-no-repeat bg-selectArrow flex h-12 px-3 pr-10 w-full rounded-xl border border-primary-200 text-sm ring-ring file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 8 | { 9 | variants: { 10 | variant: { 11 | default: "", 12 | danger: 13 | "border-red-500 text-red-500 focus-visible:ring-red-500 placeholder:text-red-300", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ); 21 | 22 | export interface SelectProps 23 | extends React.SelectHTMLAttributes, 24 | VariantProps { 25 | icon: React.ReactNode; 26 | } 27 | 28 | export const Select: React.FC = ({ 29 | variant, 30 | className, 31 | children, 32 | icon, 33 | ...props 34 | }) => { 35 | return ( 36 |

37 | {icon && ( 38 |
39 | {icon} 40 |
41 | )} 42 | 52 |
53 | ); 54 | }; 55 | Select.displayName = "Input"; 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Temporarily archived while examples are updated. 2 | # RTVI Web Demo 3 | 4 | 5 | ## Getting setup 6 | 7 | Setup your .env.local 8 | 9 | ``` 10 | cp env.example .env.local 11 | ``` 12 | 13 | Install deps and build the UI: 14 | 15 | ``` 16 | yarn 17 | yarn run dev 18 | ``` 19 | 20 | Navigate to the URL shown in your terminal window. 21 | 22 | 23 | 24 | ## Configuring your env 25 | 26 | `VITE_BASE_URL` 27 | 28 | The location of your bot running infrastructure. A default is provided for you to test. 29 | 30 | If you want to run your own infrastructure, please be aware that `navigator` requires SSL / HTTPS when not targeting `localhost`. 31 | 32 | ## Regarding HMR 33 | 34 | Whilst this app works well with hot reloading, the underlying WebRTC dependencies on some transports will throw errors if they are reinitialized. Check your console for warnings if something doesn't appear to be working. 35 | 36 | ## User intent 37 | 38 | When not in local development, browsers require user intent before allowing access to media devices (such as webcams or mics.) We show an initial splash page which requires a user to click a button before requesting any devices from the navigator object. Removing the `Splash.tsx` willcause an error when the app is not served locally which you can see in the web console. Be sure to get user intent in your apps first! 39 | 40 | ## What libraries does this use? 41 | 42 | ### Vite / React 43 | 44 | We've used [Vite](https://vitejs.dev/) to simplify the development and build experience. 45 | 46 | ### Tailwind CSS 47 | 48 | We use [Tailwind](https://tailwindcss.com/) so the UI is easy to theme quickly, and reduce the number of CSS classes used throughout the project. 49 | 50 | ### Radix 51 | 52 | For interactive components, we make use of [Radix](https://www.radix-ui.com/). 53 | 54 | -------------------------------------------------------------------------------- /src/components/UserMicBubble/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef } from "react"; 2 | import clsx from "clsx"; 3 | import { Mic, MicOff, Pause } from "lucide-react"; 4 | import { RTVIEvent } from "realtime-ai"; 5 | import { useRTVIClientEvent } from "realtime-ai-react"; 6 | 7 | import styles from "./styles.module.css"; 8 | 9 | const AudioIndicatorBubble: React.FC = () => { 10 | const volRef = useRef(null); 11 | 12 | useRTVIClientEvent( 13 | RTVIEvent.LocalAudioLevel, 14 | useCallback((volume: number) => { 15 | if (volRef.current) { 16 | const v = Number(volume) * 1.75; 17 | volRef.current.style.transform = `scale(${Math.max(0.1, v)})`; 18 | } 19 | }, []) 20 | ); 21 | 22 | return
; 23 | }; 24 | 25 | interface Props { 26 | active: boolean; 27 | muted: boolean; 28 | handleMute: () => void; 29 | } 30 | 31 | export default function UserMicBubble({ 32 | active, 33 | muted = false, 34 | handleMute, 35 | }: Props) { 36 | const canTalk = !muted && active; 37 | 38 | const cx = clsx( 39 | muted && active && styles.muted, 40 | !active && styles.blocked, 41 | canTalk && styles.canTalk 42 | ); 43 | 44 | return ( 45 |
46 |
handleMute()}> 47 |
48 | {!active ? ( 49 | 50 | ) : canTalk ? ( 51 | 52 | ) : ( 53 | 54 | )} 55 |
56 | {canTalk && } 57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtvi-web-demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "license": "BSD-2-Clause", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "test": "npx playwright test", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "@daily-co/daily-js": ">=0.67.0", 16 | "@daily-co/realtime-ai-daily": "^0.2.1", 17 | "@radix-ui/react-switch": "^1.0.3", 18 | "@radix-ui/react-tooltip": "^1.0.7", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.1.1", 21 | "lucide-react": "^0.378.0", 22 | "onnxruntime-web": "^1.18.0", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-sparklines": "^1.7.0", 26 | "realtime-ai": "^0.2.1", 27 | "realtime-ai-react": "^0.2.1", 28 | "recoil": "^0.7.7", 29 | "tailwind-merge": "^2.3.0", 30 | "web-vad": "^0.0.6" 31 | }, 32 | "devDependencies": { 33 | "@types/audioworklet": "^0.0.55", 34 | "@types/node": "^20.14.5", 35 | "@types/react": "^18.2.66", 36 | "@types/react-dom": "^18.2.22", 37 | "@types/react-sparklines": "^1.7.5", 38 | "@typescript-eslint/eslint-plugin": "^7.2.0", 39 | "@typescript-eslint/parser": "^7.2.0", 40 | "@vitejs/plugin-react": "^4.2.1", 41 | "autoprefixer": "^10.4.19", 42 | "eslint": "^8.57.0", 43 | "eslint-plugin-react-hooks": "^4.6.0", 44 | "eslint-plugin-react-refresh": "^0.4.6", 45 | "eslint-plugin-simple-import-sort": "^12.1.0", 46 | "i": "^0.3.7", 47 | "npm": "^10.8.0", 48 | "postcss": "^8.4.38", 49 | "tailwindcss": "^3.4.4", 50 | "typescript": "^5.2.2", 51 | "vite": "^5.2.0", 52 | "vite-plugin-static-copy": "^1.0.5", 53 | "vite-plugin-webfont-dl": "^3.9.4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Logo: React.FC<{ className: string }> = ({ className }) => { 4 | return ( 5 | 13 | 17 | 18 | 19 | 20 | 24 | 28 | 29 | 30 | 31 | 36 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default Logo; 47 | -------------------------------------------------------------------------------- /src/components/Setup/RoomSetup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Alert } from "../ui/alert"; 4 | 5 | import { RoomInput } from "./RoomInput"; 6 | import { SettingsList } from "./SettingsList"; 7 | 8 | interface RoomSetupProps { 9 | serverUrl: string; 10 | roomQs: string | null; 11 | roomQueryStringValid: boolean; 12 | roomError: boolean; 13 | handleCheckRoomUrl: (url: string) => void; 14 | } 15 | 16 | const serverURL = import.meta.env.VITE_SERVER_URL; 17 | const manualRoomCreation = !!import.meta.env.VITE_MANUAL_ROOM_ENTRY; 18 | 19 | export const RoomSetup: React.FC = ({ 20 | serverUrl, 21 | roomQs, 22 | roomQueryStringValid, 23 | roomError, 24 | handleCheckRoomUrl, 25 | }) => { 26 | return ( 27 | <> 28 | {import.meta.env.DEV && !serverURL && ( 29 | 30 |

31 | You have not set a server URL for local development. Please set{" "} 32 | VITE_SERVER_URL in .env.local. You will 33 | need to launch your bot manually at the same room URL. 34 |

35 |
36 | )} 37 | {import.meta.env.DEV && !serverURL && !manualRoomCreation && ( 38 | 42 |

43 | You have not set `VITE_MANUAL_ROOM_ENTRY` (auto room creation mode) 44 | without setting `VITE_SERVER_URL` 45 |

46 |
47 | )} 48 | 54 | 55 | {manualRoomCreation && !roomQs && ( 56 | 60 | )} 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/Setup/DeviceSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Mic } from "lucide-react"; 3 | import { useRTVIClientMediaDevices } from "realtime-ai-react"; 4 | 5 | import { AudioIndicatorBar } from "../AudioIndicator"; 6 | import { Field } from "../ui/field"; 7 | import { Select } from "../ui/select"; 8 | 9 | interface DeviceSelectProps { 10 | hideMeter: boolean; 11 | } 12 | 13 | export const DeviceSelect: React.FC = ({ 14 | hideMeter = false, 15 | }) => { 16 | const { availableMics, selectedMic, updateMic } = useRTVIClientMediaDevices(); 17 | 18 | useEffect(() => { 19 | updateMic(selectedMic?.deviceId); 20 | }, [updateMic, selectedMic]); 21 | 22 | return ( 23 |
24 | 25 | 40 | {!hideMeter && } 41 | 42 | 43 | {/*} 44 | 45 | 60 | 61 | */} 62 |
63 | ); 64 | }; 65 | 66 | export default DeviceSelect; 67 | -------------------------------------------------------------------------------- /src/Splash.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Book, Info } from "lucide-react"; 3 | import { VAD } from "web-vad"; 4 | 5 | import { Button } from "./components/ui/button"; 6 | 7 | type SplashProps = { 8 | handleReady: () => void; 9 | }; 10 | 11 | export const Splash: React.FC = ({ handleReady }) => { 12 | const [isReady, setIsReady] = React.useState(false); 13 | 14 | useEffect(() => { 15 | const cacheVAD = async () => { 16 | await VAD.precacheModels("silero_vad.onnx"); 17 | setIsReady(true); 18 | }; 19 | cacheVAD(); 20 | }, []); 21 | 22 | return ( 23 |
24 |
25 |

26 | Groq & 27 |
28 | Llama 3.1 & 29 |
30 | Daily & 31 |
32 | RTVI 33 |
34 | Voice-to-Voice Demo 35 |

36 | 37 | 40 | 41 | 62 |
63 | ); 64 | }; 65 | 66 | export default Splash; 67 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/utils/tailwind"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex gap-2 items-center justify-center whitespace-nowrap rounded-xl border text-base font-semibold ring-ring 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]:size-5", 9 | { 10 | variants: { 11 | variant: { 12 | primary: 13 | "border-primary bg-primary text-primary-foreground hover:bg-primary/90 disabled:text-primary-foreground/50", 14 | ghost: 15 | "border-primary-200 bg-white text-primary hover:border-primary-300 hover:bg-white/0 disabled:text-primary-foreground/50", 16 | outline: "button-outline", 17 | light: "border-transparent bg-transparent hover:bg-primary-50/20", 18 | icon: "bg-transparent border-0 hover:bg-primary-200", 19 | }, 20 | size: { 21 | default: "h-12 px-6 py-2", 22 | sm: "h-9 rounded-md px-3", 23 | lg: "h-11 rounded-md px-8", 24 | icon: "h-12 w-12", 25 | iconSm: "h-9 w-9", 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: "primary", 30 | size: "default", 31 | }, 32 | } 33 | ); 34 | 35 | export interface ButtonProps 36 | extends React.ButtonHTMLAttributes, 37 | VariantProps { 38 | fullWidthMobile?: boolean; 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ( 44 | { variant, size, fullWidthMobile, className, asChild = false, ...props }, 45 | ref 46 | ) => { 47 | const Comp = asChild ? Slot : "button"; 48 | return ( 49 | 58 | ); 59 | } 60 | ); 61 | Button.displayName = "Button"; 62 | 63 | export { Button }; 64 | -------------------------------------------------------------------------------- /src/components/Session/Agent/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useState } from "react"; 2 | import clsx from "clsx"; 3 | import { Loader2 } from "lucide-react"; 4 | import { RTVIEvent } from "realtime-ai"; 5 | import { useRTVIClientEvent } from "realtime-ai-react"; 6 | 7 | import Latency from "@/components/Latency"; 8 | 9 | //import TranscriptOverlay from "../TranscriptOverlay"; 10 | import Avatar from "./avatar"; 11 | import ModelBadge from "./model"; 12 | 13 | import styles from "./styles.module.css"; 14 | 15 | export const Agent: React.FC<{ 16 | isReady: boolean; 17 | statsAggregator: StatsAggregator; 18 | }> = memo( 19 | ({ isReady, statsAggregator }) => { 20 | const [hasStarted, setHasStarted] = useState(false); 21 | const [botStatus, setBotStatus] = useState< 22 | "initializing" | "connected" | "disconnected" 23 | >("initializing"); 24 | 25 | useEffect(() => { 26 | // Update the started state when the transport enters the ready state 27 | if (!isReady) return; 28 | setHasStarted(true); 29 | setBotStatus("connected"); 30 | }, [isReady]); 31 | 32 | useRTVIClientEvent( 33 | RTVIEvent.BotDisconnected, 34 | useCallback(() => { 35 | setHasStarted(false); 36 | setBotStatus("disconnected"); 37 | }, []) 38 | ); 39 | 40 | // Cleanup 41 | useEffect(() => () => setHasStarted(false), []); 42 | 43 | const cx = clsx(styles.agentWindow, hasStarted && styles.ready); 44 | 45 | return ( 46 |
47 |
48 | 49 | {!hasStarted ? ( 50 | 51 | 52 | 53 | ) : ( 54 | 55 | )} 56 | {/**/} 57 |
58 |
59 | 64 |
65 |
66 | ); 67 | }, 68 | (p, n) => p.isReady === n.isReady 69 | ); 70 | 71 | export default Agent; 72 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { DailyTransport } from "@daily-co/realtime-ai-daily"; 4 | import { LLMHelper, RTVIClient, RTVIClientConfigOption } from "realtime-ai"; 5 | import { RTVIClientAudio, RTVIClientProvider } from "realtime-ai-react"; 6 | 7 | import { Header } from "./components/ui/header"; 8 | import { TooltipProvider } from "./components/ui/tooltip"; 9 | import App from "./App"; 10 | import { defaultConfig } from "./config"; 11 | import { Splash } from "./Splash"; 12 | 13 | import "./global.css"; // Note: Core app layout can be found here 14 | 15 | // Show warning on Firefox 16 | // @ts-expect-error - Firefox is not well support 17 | const isFirefox: boolean = typeof InstallTrigger !== "undefined"; 18 | 19 | const rtviClient = new RTVIClient({ 20 | transport: new DailyTransport(), 21 | params: { 22 | baseUrl: import.meta.env.VITE_BASE_URL, 23 | endpoints: { 24 | connect: "/start-bot", 25 | action: "/bot-action", 26 | }, 27 | config: defaultConfig as RTVIClientConfigOption[], 28 | }, 29 | enableMic: true, 30 | enableCam: false, 31 | }); 32 | 33 | const llmHelper = new LLMHelper({ 34 | callbacks: { 35 | // ... 36 | }, 37 | }); 38 | 39 | // Register the helper 40 | rtviClient.registerHelper("llm", llmHelper); 41 | 42 | export const Layout = () => { 43 | const [showSplash, setShowSplash] = useState(true); 44 | 45 | if (showSplash) { 46 | return setShowSplash(false)} />; 47 | } 48 | 49 | return ( 50 | 51 | 52 |
53 |
54 |
55 | 56 |
57 |
58 |