diff --git a/.env b/.env
new file mode 100644
index 0000000..cb4caa2
--- /dev/null
+++ b/.env
@@ -0,0 +1,3 @@
+HEYGEN_API_KEY=your Heygen API key
+OPENAI_API_KEY=your OpenAI API key
+NEXT_PUBLIC_OPENAI_API_KEY=your OpenAI API key
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..af6ab76
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,20 @@
+.now/*
+*.css
+.changeset
+dist
+esm/*
+public/*
+tests/*
+scripts/*
+*.config.js
+.DS_Store
+node_modules
+coverage
+.next
+build
+!.commitlintrc.cjs
+!.lintstagedrc.cjs
+!jest.config.js
+!plopfile.js
+!react-shim.js
+!tsup.config.ts
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..d3067d4
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,92 @@
+{
+ "$schema": "https://json.schemastore.org/eslintrc.json",
+ "env": {
+ "browser": false,
+ "es2021": true,
+ "node": true
+ },
+ "extends": [
+ "plugin:react/recommended",
+ "plugin:prettier/recommended",
+ "plugin:react-hooks/recommended",
+ "plugin:jsx-a11y/recommended"
+ ],
+ "plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaFeatures": {
+ "jsx": true
+ },
+ "ecmaVersion": 12,
+ "sourceType": "module"
+ },
+ "settings": {
+ "react": {
+ "version": "detect"
+ }
+ },
+ "rules": {
+ "no-console": "warn",
+ "react/prop-types": "off",
+ "react/jsx-uses-react": "off",
+ "react/react-in-jsx-scope": "off",
+ "react-hooks/exhaustive-deps": "off",
+ "jsx-a11y/click-events-have-key-events": "warn",
+ "jsx-a11y/interactive-supports-focus": "warn",
+ "prettier/prettier": "warn",
+ "no-unused-vars": "off",
+ "unused-imports/no-unused-vars": "off",
+ "unused-imports/no-unused-imports": "warn",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ "args": "after-used",
+ "ignoreRestSiblings": false,
+ "argsIgnorePattern": "^_.*?$"
+ }
+ ],
+ "import/order": [
+ "warn",
+ {
+ "groups": [
+ "type",
+ "builtin",
+ "object",
+ "external",
+ "internal",
+ "parent",
+ "sibling",
+ "index"
+ ],
+ "pathGroups": [
+ {
+ "pattern": "~/**",
+ "group": "external",
+ "position": "after"
+ }
+ ],
+ "newlines-between": "always"
+ }
+ ],
+ "react/self-closing-comp": "warn",
+ "react/jsx-sort-props": [
+ "warn",
+ {
+ "callbacksLast": true,
+ "shorthandFirst": true,
+ "noSortAlphabetically": false,
+ "reservedFirst": true
+ }
+ ],
+ "padding-line-between-statements": [
+ "warn",
+ {"blankLine": "always", "prev": "*", "next": "return"},
+ {"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"},
+ {
+ "blankLine": "any",
+ "prev": ["const", "let", "var"],
+ "next": ["const", "let", "var"]
+ }
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8f322f0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..43c97e7
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..3662b37
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..08df0a9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+# Next.js & NextUI Template
+
+This is a template for creating applications using Next.js 14 (app directory) and NextUI (v2).
+
+[Try it on CodeSandbox](https://githubbox.com/nextui-org/next-app-template)
+
+## Technologies Used
+
+- [Next.js 14](https://nextjs.org/docs/getting-started)
+- [NextUI v2](https://nextui.org/)
+- [Tailwind CSS](https://tailwindcss.com/)
+- [Tailwind Variants](https://tailwind-variants.org)
+- [TypeScript](https://www.typescriptlang.org/)
+- [Framer Motion](https://www.framer.com/motion/)
+- [next-themes](https://github.com/pacocoursey/next-themes)
+
+## How to Use
+
+### Use the template with create-next-app
+
+To create a new project based on this template using `create-next-app`, run the following command:
+
+```bash
+npx create-next-app -e https://github.com/nextui-org/next-app-template
+```
+
+### Install dependencies
+
+You can use one of them `npm`, `yarn`, `pnpm`, `bun`, Example using `npm`:
+
+```bash
+npm install
+```
+
+### Run the development server
+
+```bash
+npm run dev
+```
+
+### Setup pnpm (optional)
+
+If you are using `pnpm`, you need to add the following code to your `.npmrc` file:
+
+```bash
+public-hoist-pattern[]=*@nextui-org/*
+```
+
+After modifying the `.npmrc` file, you need to run `pnpm install` again to ensure that the dependencies are installed correctly.
+
+## License
+
+Licensed under the [MIT license](https://github.com/nextui-org/next-app-template/blob/main/LICENSE).
diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts
new file mode 100644
index 0000000..a085638
--- /dev/null
+++ b/app/api/chat/route.ts
@@ -0,0 +1,16 @@
+import { openai } from "@ai-sdk/openai";
+import { streamText } from "ai";
+
+// Allow streaming responses up to 30 seconds
+export const maxDuration = 30;
+
+export async function POST(req: Request) {
+ const { messages } = await req.json();
+
+ const result = await streamText({
+ model: openai("gpt-4-turbo"),
+ messages,
+ });
+
+ return result.toAIStreamResponse();
+}
diff --git a/app/api/get-access-token/route.ts b/app/api/get-access-token/route.ts
new file mode 100644
index 0000000..7d5ba94
--- /dev/null
+++ b/app/api/get-access-token/route.ts
@@ -0,0 +1,30 @@
+const HEYGEN_API_KEY = process.env.HEYGEN_API_KEY;
+
+export async function POST() {
+ try {
+ if (!HEYGEN_API_KEY) {
+ throw new Error("API key is missing from .env");
+ }
+
+ const res = await fetch(
+ "https://api.heygen.com/v1/streaming.create_token",
+ {
+ method: "POST",
+ headers: {
+ "x-api-key": HEYGEN_API_KEY,
+ },
+ }
+ );
+ const data = await res.json();
+
+ return new Response(data.data.token, {
+ status: 200,
+ });
+ } catch (error) {
+ console.error("Error retrieving access token:", error);
+
+ return new Response("Failed to retrieve access token", {
+ status: 500,
+ });
+ }
+}
diff --git a/app/error.tsx b/app/error.tsx
new file mode 100644
index 0000000..9ed5104
--- /dev/null
+++ b/app/error.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { useEffect } from "react";
+
+export default function Error({
+ error,
+ reset,
+}: {
+ error: Error;
+ reset: () => void;
+}) {
+ useEffect(() => {
+ // Log the error to an error reporting service
+ /* eslint-disable no-console */
+ console.error(error);
+ }, [error]);
+
+ return (
+
+
Something went wrong!
+
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..f1b1472
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,59 @@
+import "@/styles/globals.css";
+import clsx from "clsx";
+import { Metadata, Viewport } from "next";
+
+import { Providers } from "./providers";
+
+import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google";
+import NavBar from "@/components/NavBar";
+
+const fontSans = FontSans({
+ subsets: ["latin"],
+ variable: "--font-sans",
+});
+
+const fontMono = FontMono({
+ subsets: ["latin"],
+ variable: "--font-geist-mono",
+});
+
+export const metadata: Metadata = {
+ title: {
+ default: "HeyGen Streaming Avatar SDK Demo",
+ template: `%s - HeyGen Streaming Avatar SDK Demo`,
+ },
+ icons: {
+ icon: "/heygen-logo.png",
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "white" },
+ { media: "(prefers-color-scheme: dark)", color: "black" },
+ ],
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..6934227
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import NavBar from "@/components/NavBar";
+import StreamingAvatar from "@/components/StreamingAvatar";
+import StreamingAvatarCode from "@/components/StreamingAvatarCode";
+import { Tab, Tabs } from "@nextui-org/react";
+
+export default function App() {
+ const tabs = [
+ {
+ id: "demo",
+ label: "Demo",
+ content: ,
+ },
+ {
+ id: "code",
+ label: "Code",
+ content: ,
+ },
+ ];
+
+ return (
+
+
+
+
+ {(items) => (
+
+ {items.content}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/providers.tsx b/app/providers.tsx
new file mode 100644
index 0000000..9a1ac92
--- /dev/null
+++ b/app/providers.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import * as React from "react";
+import { NextUIProvider } from "@nextui-org/system";
+import { useRouter } from "next/navigation";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+import { ThemeProviderProps } from "next-themes/dist/types";
+
+export interface ProvidersProps {
+ children: React.ReactNode;
+ themeProps?: ThemeProviderProps;
+}
+
+export function Providers({ children, themeProps }: ProvidersProps) {
+ const router = useRouter();
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/Icons.tsx b/components/Icons.tsx
new file mode 100644
index 0000000..0e0d8d8
--- /dev/null
+++ b/components/Icons.tsx
@@ -0,0 +1,81 @@
+export function HeyGenLogo() {
+ return
;
+}
+
+type IconSvgProps = {
+ size?: number;
+ width?: number;
+ height?: number;
+ className?: string;
+};
+
+export function GithubIcon({
+ size = 24,
+ width,
+ height,
+ ...props
+}: IconSvgProps) {
+ return (
+
+ );
+}
+
+export function MoonFilledIcon({
+ size = 24,
+ width,
+ height,
+ ...props
+}: IconSvgProps) {
+ return (
+
+ );
+}
+
+export function SunFilledIcon({
+ size = 24,
+ width,
+ height,
+ ...props
+}: IconSvgProps) {
+ return (
+
+ );
+}
diff --git a/components/NavBar.tsx b/components/NavBar.tsx
new file mode 100644
index 0000000..f066a95
--- /dev/null
+++ b/components/NavBar.tsx
@@ -0,0 +1,46 @@
+import {
+ Link,
+ Navbar,
+ NavbarBrand,
+ NavbarContent,
+ NavbarItem,
+} from "@nextui-org/react";
+import { GithubIcon, HeyGenLogo } from "./Icons";
+import { ThemeSwitch } from "./ThemeSwitch";
+
+export default function NavBar() {
+ return (
+
+
+
+
+
+
+
+ HeyGen Streaming Avatar SDK NextJS Demo
+
+
+
+
+
+
+ API Docs
+
+
+
+ SDK Github
+
+
+
+
+
+ );
+}
diff --git a/components/StreamingAvatar.tsx b/components/StreamingAvatar.tsx
new file mode 100644
index 0000000..83b6084
--- /dev/null
+++ b/components/StreamingAvatar.tsx
@@ -0,0 +1,332 @@
+import {
+ Configuration,
+ NewSessionData,
+ StreamingAvatarApi,
+} from "@heygen/streaming-avatar";
+import {
+ Button,
+ Card,
+ CardBody,
+ CardFooter,
+ Divider,
+ Input,
+ Spinner,
+ Tooltip,
+} from "@nextui-org/react";
+import { useChat } from "ai/react";
+import OpenAI from "openai";
+import { useEffect, useRef, useState } from "react";
+import StreamingAvatarTextInput from "./StreamingAvatarTextInput";
+import {
+ Camera,
+ Microphone,
+ MicrophoneSlash,
+ MicrophoneStage,
+ Record,
+} from "@phosphor-icons/react";
+import clsx from "clsx";
+
+const openai = new OpenAI({
+ apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
+ dangerouslyAllowBrowser: true,
+});
+
+export default function StreamingAvatar() {
+ const [loading, setLoading] = useState(false);
+ const [stream, setStream] = useState();
+ const [debug, setDebug] = useState();
+ const [avatarId, setAvatarId] = useState("");
+ const [voiceId, setVoiceId] = useState("");
+ const [data, setData] = useState();
+ const [text, setText] = useState("");
+ const [initialized, setInitialized] = useState(false); // Track initialization
+ const [recording, setRecording] = useState(false); // Track recording state
+ const mediaStream = useRef(null);
+ const avatar = useRef(null);
+ const mediaRecorder = useRef(null);
+ const audioChunks = useRef([]);
+ const { input, setInput, isLoading, handleSubmit } = useChat({
+ onFinish: async (message) => {
+ console.log("ChatGPT Response:", message);
+
+ if (!initialized || !avatar.current) {
+ setDebug("Avatar API not initialized");
+ return;
+ }
+
+ //send the ChatGPT response to the Streaming Avatar
+ await avatar.current
+ .speak({
+ taskRequest: { text: message.content, sessionId: data?.sessionId },
+ })
+ .catch((e) => {
+ setDebug(e.message);
+ });
+ },
+ });
+
+ async function fetchAccessToken() {
+ try {
+ const response = await fetch("/api/get-access-token", {
+ method: "POST",
+ });
+ const token = await response.text();
+ console.log("Access Token:", token); // Log the token to verify
+ return token;
+ } catch (error) {
+ console.error("Error fetching access token:", error);
+ return "";
+ }
+ }
+
+ async function start() {
+ setLoading(true);
+ await updateToken();
+ if (!avatar.current) {
+ setDebug("Avatar API is not initialized");
+ return;
+ }
+ try {
+ const res = await avatar.current.createStartAvatar(
+ {
+ newSessionRequest: {
+ quality: "low",
+ avatarName: avatarId,
+ voice: { voiceId: voiceId },
+ },
+ },
+ setDebug
+ );
+ setData(res);
+ setStream(avatar.current.mediaStream);
+ setLoading(false);
+ } catch (error) {
+ console.error("Error starting avatar session:", error);
+ }
+ }
+
+ async function updateToken() {
+ const newToken = await fetchAccessToken();
+ console.log("Updating Access Token:", newToken); // Log token for debugging
+ avatar.current = new StreamingAvatarApi(
+ new Configuration({ accessToken: newToken })
+ );
+
+ const startTalkCallback = (e: any) => {
+ console.log("Avatar started talking", e);
+ };
+
+ const stopTalkCallback = (e: any) => {
+ console.log("Avatar stopped talking", e);
+ };
+
+ console.log("Adding event handlers:", avatar.current);
+ avatar.current.addEventHandler("avatar_start_talking", startTalkCallback);
+ avatar.current.addEventHandler("avatar_stop_talking", stopTalkCallback);
+
+ setInitialized(true);
+ }
+
+ async function stop() {
+ if (!initialized || !avatar.current) {
+ setDebug("Avatar API not initialized");
+ return;
+ }
+ await avatar.current.stopAvatar(
+ { stopSessionRequest: { sessionId: data?.sessionId } },
+ setDebug
+ );
+ }
+
+ async function handleSpeak() {
+ if (!initialized || !avatar.current) {
+ setDebug("Avatar API not initialized");
+ return;
+ }
+ await avatar.current
+ .speak({ taskRequest: { text: text, sessionId: data?.sessionId } })
+ .catch((e) => {
+ setDebug(e.message);
+ });
+ }
+
+ useEffect(() => {
+ async function init() {
+ const newToken = await fetchAccessToken();
+ console.log("Initializing with Access Token:", newToken); // Log token for debugging
+ avatar.current = new StreamingAvatarApi(
+ new Configuration({ accessToken: newToken, jitterBuffer: 200 })
+ );
+ setInitialized(true); // Set initialized to true
+ }
+ init();
+
+ return () => {
+ stop();
+ };
+ }, []);
+
+ useEffect(() => {
+ if (stream && mediaStream.current) {
+ mediaStream.current.srcObject = stream;
+ mediaStream.current.onloadedmetadata = () => {
+ mediaStream.current!.play();
+ setDebug("Playing");
+ };
+ }
+ }, [mediaStream, stream]);
+
+ function startRecording() {
+ navigator.mediaDevices
+ .getUserMedia({ audio: true })
+ .then((stream) => {
+ mediaRecorder.current = new MediaRecorder(stream);
+ mediaRecorder.current.ondataavailable = (event) => {
+ audioChunks.current.push(event.data);
+ };
+ mediaRecorder.current.onstop = () => {
+ const audioBlob = new Blob(audioChunks.current, {
+ type: "audio/wav",
+ });
+ audioChunks.current = [];
+ transcribeAudio(audioBlob);
+ };
+ mediaRecorder.current.start();
+ setRecording(true);
+ })
+ .catch((error) => {
+ console.error("Error accessing microphone:", error);
+ });
+ }
+
+ function stopRecording() {
+ if (mediaRecorder.current) {
+ mediaRecorder.current.stop();
+ setRecording(false);
+ }
+ }
+
+ async function transcribeAudio(audioBlob: Blob) {
+ try {
+ // Convert Blob to File
+ const audioFile = new File([audioBlob], "recording.wav", {
+ type: "audio/wav",
+ });
+ const response = await openai.audio.transcriptions.create({
+ model: "whisper-1",
+ file: audioFile,
+ });
+ const transcription = response.text;
+ console.log("Transcription: ", transcription);
+ setInput(transcription);
+ } catch (error) {
+ console.error("Error transcribing audio:", error);
+ }
+ }
+
+ return (
+
+
+
+ {stream ? (
+
+
+
+ ) : !loading ? (
+
+ setAvatarId(e.target.value)}
+ placeholder="Custom Avatar ID (optional)"
+ />
+ setVoiceId(e.target.value)}
+ placeholder="Custom Voice ID (optional)"
+ />
+
+
+ ) : (
+
+ )}
+
+
+
+
+ {
+ if (!input) {
+ setDebug("Please enter text to send to ChatGPT");
+ return;
+ }
+ handleSubmit();
+ }}
+ setInput={setInput}
+ endContent={
+
+
+
+ }
+ disabled={!stream}
+ />
+
+
+
+ Console:
+
+ {debug}
+
+
+ );
+}
diff --git a/components/StreamingAvatarCode.tsx b/components/StreamingAvatarCode.tsx
new file mode 100644
index 0000000..addbd47
--- /dev/null
+++ b/components/StreamingAvatarCode.tsx
@@ -0,0 +1,98 @@
+import { Card, CardBody } from "@nextui-org/react";
+import { langs } from "@uiw/codemirror-extensions-langs";
+import ReactCodeMirror from "@uiw/react-codemirror";
+
+export default function StreamingAvatarCode() {
+ return (
+
+
This SDK supports the following behavior:
+
+ -
+
+
Start:
Start the
+ streaming avatar session
+
+
+ -
+
+
Close:
Close the
+ streaming avatar session
+
+
+ -
+
+
Speak:
Repeat the
+ input
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const TEXT = `
+ export default function App() {
+ // Media stream used by the video player to display the avatar
+ const [stream, setStream] = useState ();
+ const mediaStream = useRef(null);
+
+ // Instantiate the streaming avatar api using your access token
+ const avatar = useRef(new StreamingAvatarApi(
+ new Configuration({accessToken: ''})
+ ));
+
+ // State holding streaming avatar session data
+ const [sessionData, setSessionData] = useState();
+
+ // Function to start the streaming avatar session
+ async function start(){
+ const res = await avatar.current.createStartAvatar(
+ { newSessionRequest:
+
+ // Define the session variables during creation
+ { quality: "medium", // low, medium, high
+ avatarName: ,
+ voice:{voiceId: }
+ }
+
+ });
+ setSessionData(res);
+ }
+
+ // Function to stop the streaming avatar session
+ async function stop(){
+ await avatar.current.stopAvatar({stopSessionRequest: {sessionId: sessionData?.sessionId}});
+ }
+
+ // Function which passes in text to the avatar to repeat
+ async function handleSpeak(){
+ await avatar.current.speak({taskRequest: {text: , sessionId: sessionData?.sessionId}}).catch((e) => {
+ });
+ }
+
+ useEffect(()=>{
+ // Handles the display of the streaming avatar
+ if(stream && mediaStream.current){
+ mediaStream.current.srcObject = stream;
+ mediaStream.current.onloadedmetadata = () => {
+ mediaStream.current!.play();
+ }
+ }
+ }, [mediaStream, stream])
+
+ return (
+
+
+ }`;
diff --git a/components/StreamingAvatarTextInput.tsx b/components/StreamingAvatarTextInput.tsx
new file mode 100644
index 0000000..56b0395
--- /dev/null
+++ b/components/StreamingAvatarTextInput.tsx
@@ -0,0 +1,67 @@
+import { Input, Tooltip } from "@nextui-org/react";
+import { Airplane, ArrowRight, PaperPlaneRight } from "@phosphor-icons/react";
+import clsx from "clsx";
+
+interface StreamingAvatarTextInputProps {
+ label: string;
+ placeholder: string;
+ input: string;
+ onSubmit: () => void;
+ setInput: (value: string) => void;
+ endContent?: React.ReactNode;
+ disabled?: boolean;
+}
+
+export default function StreamingAvatarTextInput({
+ label,
+ placeholder,
+ input,
+ onSubmit,
+ setInput,
+ endContent,
+ disabled = false,
+}: StreamingAvatarTextInputProps) {
+ function handleSubmit() {
+ if (input.trim() === "") {
+ return;
+ }
+ onSubmit();
+ setInput("");
+ }
+
+ return (
+
+ {endContent}
+
+
+
+
+ }
+ label={label}
+ placeholder={placeholder}
+ size="sm"
+ value={input}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ handleSubmit();
+ }
+ }}
+ onValueChange={setInput}
+ isDisabled={disabled}
+ />
+ );
+}
diff --git a/components/ThemeSwitch.tsx b/components/ThemeSwitch.tsx
new file mode 100644
index 0000000..3bbd529
--- /dev/null
+++ b/components/ThemeSwitch.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { FC } from "react";
+import { VisuallyHidden } from "@react-aria/visually-hidden";
+import { SwitchProps, useSwitch } from "@nextui-org/switch";
+import { useTheme } from "next-themes";
+import { useIsSSR } from "@react-aria/ssr";
+import clsx from "clsx";
+import { MoonFilledIcon, SunFilledIcon } from "./Icons";
+
+export interface ThemeSwitchProps {
+ className?: string;
+ classNames?: SwitchProps["classNames"];
+}
+
+export const ThemeSwitch: FC = ({
+ className,
+ classNames,
+}) => {
+ const { theme, setTheme } = useTheme();
+ const isSSR = useIsSSR();
+
+ const onChange = () => {
+ theme === "light" ? setTheme("dark") : setTheme("light");
+ };
+
+ const {
+ Component,
+ slots,
+ isSelected,
+ getBaseProps,
+ getInputProps,
+ getWrapperProps,
+ } = useSwitch({
+ isSelected: theme === "light" || isSSR,
+ "aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`,
+ onChange,
+ });
+
+ return (
+
+
+
+
+
+ {!isSelected || isSSR ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..767719f
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..5836935
--- /dev/null
+++ b/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "next-app-template",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "next dev -p 3234",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix"
+ },
+ "dependencies": {
+ "@ai-sdk/openai": "^0.0.34",
+ "@heygen/streaming-avatar": "^1.0.10",
+ "@nextui-org/button": "2.0.34",
+ "@nextui-org/code": "2.0.29",
+ "@nextui-org/input": "2.2.2",
+ "@nextui-org/kbd": "2.0.30",
+ "@nextui-org/link": "2.0.32",
+ "@nextui-org/listbox": "2.1.21",
+ "@nextui-org/navbar": "2.0.33",
+ "@nextui-org/react": "^2.4.2",
+ "@nextui-org/snippet": "2.0.38",
+ "@nextui-org/switch": "2.0.31",
+ "@nextui-org/system": "2.2.1",
+ "@nextui-org/theme": "2.2.5",
+ "@phosphor-icons/react": "^2.1.5",
+ "@react-aria/ssr": "3.9.4",
+ "@react-aria/visually-hidden": "3.8.12",
+ "@uiw/codemirror-extensions-langs": "^4.22.1",
+ "@uiw/react-codemirror": "^4.22.1",
+ "ai": "^3.2.15",
+ "clsx": "2.1.1",
+ "framer-motion": "~11.1.1",
+ "intl-messageformat": "^10.5.0",
+ "next": "14.2.4",
+ "next-themes": "^0.2.1",
+ "openai": "^4.52.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "zod": "^3.23.8"
+ },
+ "devDependencies": {
+ "@types/node": "20.5.7",
+ "@types/react": "18.3.3",
+ "@types/react-dom": "18.3.0",
+ "@typescript-eslint/eslint-plugin": "7.2.0",
+ "@typescript-eslint/parser": "7.2.0",
+ "autoprefixer": "10.4.19",
+ "eslint": "^8.57.0",
+ "eslint-config-next": "14.2.1",
+ "eslint-config-prettier": "^8.2.0",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-jsx-a11y": "^6.4.1",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-react": "^7.23.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-unused-imports": "^3.2.0",
+ "postcss": "8.4.38",
+ "tailwind-variants": "0.1.20",
+ "tailwindcss": "3.4.3",
+ "typescript": "5.0.4"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/heygen-logo.png b/public/heygen-logo.png
new file mode 100644
index 0000000..e9c1496
Binary files /dev/null and b/public/heygen-logo.png differ
diff --git a/public/next.svg b/public/next.svg
new file mode 100644
index 0000000..5174b28
--- /dev/null
+++ b/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
new file mode 100644
index 0000000..d2f8422
--- /dev/null
+++ b/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/styles/globals.css b/styles/globals.css
new file mode 100644
index 0000000..1f78d2b
--- /dev/null
+++ b/styles/globals.css
@@ -0,0 +1,9 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+li {
+ list-style-type: square;
+ margin-left: 30px;
+ padding: 2px;
+}
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..03a4e22
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,20 @@
+import {nextui} from '@nextui-org/theme'
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
+ './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
+ ],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ["var(--font-sans)"],
+ mono: ["var(--font-geist-mono)"],
+ },
+ },
+ },
+ darkMode: "class",
+ plugins: [nextui()],
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e06a445
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}