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 HeyGen Logo; +} + +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"] +}