├── app ├── favicon.ico ├── layout.tsx ├── api │ └── route.ts ├── config.ts ├── global.css └── page.tsx ├── public ├── icon.png ├── social.png ├── shutter.mp3 ├── color-wash-bg.png ├── logo-anthropic.svg ├── logo-openai.svg └── logo-together.svg ├── components ├── Setup │ ├── index.tsx │ ├── AudioIndicator │ │ ├── styles.module.css │ │ └── index.tsx │ ├── DeviceSelect.tsx │ └── Configure.tsx ├── ui │ ├── label.tsx │ ├── field.tsx │ ├── helptip.tsx │ ├── textarea.tsx │ ├── switch.tsx │ ├── tooltip.tsx │ ├── input.tsx │ ├── alert.tsx │ ├── select.tsx │ ├── button.tsx │ └── card.tsx ├── Header │ ├── ExpiryTimer │ │ ├── styles.module.css │ │ └── index.tsx │ └── index.tsx ├── Session │ ├── Agent │ │ ├── avatar.tsx │ │ ├── model.tsx │ │ ├── face.svg │ │ ├── index.tsx │ │ ├── styles.module.css │ │ └── waveform.tsx │ ├── TranscriptOverlay │ │ ├── styles.module.css │ │ └── index.tsx │ ├── UserMicBubble │ │ ├── index.tsx │ │ └── styles.module.css │ ├── Stats │ │ ├── styles.module.css │ │ └── index.tsx │ └── index.tsx ├── context.tsx ├── Splash.tsx └── App.tsx ├── next.config.mjs ├── utils ├── tailwind.ts └── stats_aggregator.ts ├── postcss.config.mjs ├── .gitignore ├── tsconfig.json ├── types └── stats_aggregator.d.ts ├── package.json ├── lib └── stores │ └── languageStore.ts ├── rtvi.config.ts ├── tailwind.config.ts └── Readme.md /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanbvadassery/ai-teacher/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanbvadassery/ai-teacher/HEAD/public/icon.png -------------------------------------------------------------------------------- /public/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanbvadassery/ai-teacher/HEAD/public/social.png -------------------------------------------------------------------------------- /components/Setup/index.tsx: -------------------------------------------------------------------------------- 1 | import { Configure } from "./Configure"; 2 | 3 | export { Configure }; 4 | -------------------------------------------------------------------------------- /public/shutter.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanbvadassery/ai-teacher/HEAD/public/shutter.mp3 -------------------------------------------------------------------------------- /public/color-wash-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanbvadassery/ai-teacher/HEAD/public/color-wash-bg.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | "tailwindcss/nesting": "postcss-nesting", 5 | tailwindcss: {}, 6 | }, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /components/Setup/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 | .bar 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /components/Header/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: 80px; 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/Setup/AudioIndicator/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from "react"; 2 | import { VoiceEvent } from "realtime-ai"; 3 | import { useVoiceClientEvent } 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 | useVoiceClientEvent( 11 | VoiceEvent.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import ExpiryTimer from "@/components/Header/ExpiryTimer"; 2 | 3 | const aCx = 4 | "underline decoration-primary-400/0 hover:decoration-primary-400 underline-offset-4 transition-all duration-300"; 5 | 6 | export function Header() { 7 | return ( 8 | 25 | ); 26 | } 27 | 28 | export default Header; 29 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter, Space_Mono } from "next/font/google"; 3 | 4 | import "./global.css"; 5 | 6 | // Font 7 | const fontSans = Inter({ 8 | subsets: ["latin"], 9 | display: "swap", 10 | variable: "--font-sans", 11 | }); 12 | 13 | const fontMono = Space_Mono({ 14 | subsets: ["latin"], 15 | weight: ["400", "700"], 16 | variable: "--font-mono", 17 | }); 18 | 19 | export const metadata: Metadata = { 20 | title: "AI Vision Teacher", 21 | description: "An AI vision teacher app using Next.js", 22 | }; 23 | 24 | export default function RootLayout({ 25 | children, 26 | }: Readonly<{ 27 | children: React.ReactNode; 28 | }>) { 29 | return ( 30 | 31 | 32 | {children} 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /components/Session/Agent/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from "react"; 2 | import Image from "next/image"; 3 | import { VoiceEvent } from "realtime-ai"; 4 | import { useVoiceClientEvent } from "realtime-ai-react"; 5 | 6 | import FaceSVG from "./face.svg"; 7 | 8 | import styles from "./styles.module.css"; 9 | 10 | export const Avatar: React.FC = () => { 11 | const volRef = useRef(null); 12 | 13 | useVoiceClientEvent( 14 | VoiceEvent.RemoteAudioLevel, 15 | useCallback((volume: number) => { 16 | if (!volRef.current) return; 17 | volRef.current.style.transform = `scale(${Math.max(1, 1 + volume)})`; 18 | }, []) 19 | ); 20 | 21 | return ( 22 | <> 23 | Face 24 |
25 | 26 | ); 27 | }; 28 | 29 | export default Avatar; 30 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/utils/tailwind"; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |