added examples back

This commit is contained in:
Jon Taylor
2024-05-13 17:09:46 +01:00
parent 27ba50cbbf
commit f1c02f8554
154 changed files with 7918 additions and 1287 deletions

View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# 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

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,109 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--primary: 220.9 39.3% 11%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--accent: 220 14.3% 95.9%;
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 20% 98%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 224 71.4% 4.1%;
--radius: 0.5rem;
}
.dark {
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%;
--primary: 210 20% 98%;
--primary-foreground: 220.9 39.3% 11%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--ring: 216 12.2% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
body{
background: url("/bg.jpg") no-repeat center center;
background-size: cover;
}
.cardShadow{
box-shadow: 0px 124px 35px 0px rgba(0, 0, 0, 0.00), 0px 79px 32px 0px rgba(0, 0, 0, 0.01), 0px 45px 27px 0px rgba(0, 0, 0, 0.05), 0px 20px 20px 0px rgba(0, 0, 0, 0.09), 0px 5px 11px 0px rgba(0, 0, 0, 0.10);
}
@keyframes fadeInSlideUp {
0% {
opacity: 0;
transform: translateY(50px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 1s ease-out;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@@ -0,0 +1,46 @@
import React from "react";
import "./globals.css";
import type { Metadata } from "next";
import { Space_Grotesk, Space_Mono } from "next/font/google";
import { cn } from "@/app/utils";
// Font
const sans = Space_Grotesk({
subsets: ["latin"],
weight: ["400", "500", "600"],
variable: "--font-sans",
});
const mono = Space_Mono({
subsets: ["latin"],
weight: ["400", "700"],
variable: "--font-mono",
});
export const metadata: Metadata = {
title: "Storytelling Chatbot - Daily AI",
description: "Built with git.new/ai",
metadataBase: new URL(process.env.SITE_URL || "http://localhost:3000"),
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={cn(
"min-h-screen bg-background font-sans antialiased flex flex-col",
sans.variable,
mono.variable
)}
>
<main className="flex flex-1">{children}</main>
</body>
</html>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,16 @@
"use client";
import React from "react";
import { DailyProvider, useCallObject } from "@daily-co/daily-react";
import App from "../components/App";
export default function Home() {
const callObject = useCallObject({});
return (
<DailyProvider callObject={callObject}>
<App />
</DailyProvider>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "gray",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/app"
}
}

View File

@@ -0,0 +1,90 @@
"use client";
import React, { useState } from "react";
import { useDaily } from "@daily-co/daily-react";
import Setup from "./Setup";
import Story from "./Story";
type State =
| "idle"
| "connecting"
| "connected"
| "started"
| "finished"
| "error";
export default function Call() {
const daily = useDaily();
const [state, setState] = useState<State>("idle");
const [room, setRoom] = useState<string | null>(null);
async function start() {
setState("connecting");
if (!daily) return;
// Create a new room for the story session
try {
const response = await fetch("/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
room_url: process.env.NEXT_PUBLIC_ROOM_URL || null,
}),
});
const { room_url, token } = await response.json();
// Keep a reference to the room url for later
setRoom(room_url);
// Join the WebRTC session
await daily.join({
url: room_url,
token,
videoSource: false,
startAudioOff: true,
});
setState("connected");
// Disable local audio, the bot will say hello first
daily.setLocalAudio(false);
// Start the bot
const resp = await fetch("/start", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
room_url,
}),
});
setState("started");
} catch (error) {
setState("error");
leave();
}
}
async function leave() {
await daily?.leave();
setState("finished");
}
if (state === "error") {
return <div>An Error occured</div>;
}
if (state === "started") {
return <Story handleLeave={() => leave()} />;
}
return <Setup handleStart={() => start()} />;
}

View File

@@ -0,0 +1,69 @@
import {
useAudioLevel,
useAudioTrack,
useLocalSessionId,
} from "@daily-co/daily-react";
import { useCallback, useRef } from "react";
export const AudioIndicator: React.FC = () => {
const localSessionId = useLocalSessionId();
const audioTrack = useAudioTrack(localSessionId);
const volRef = useRef<HTMLDivElement>(null);
useAudioLevel(
audioTrack?.persistentTrack,
useCallback((volume) => {
// this volume number will be between 0 and 1
// give it a minimum scale of 0.15 to not completely disappear 👻
if (volRef.current) {
const v = volume * 1.75;
volRef.current.style.transform = `scale(${Math.max(0.1, v)})`;
}
}, [])
);
// Your audio track's audio volume visualized in a small circle,
// whose size changes depending on the volume level
return (
<>
<div className="vol bg-teal-700" ref={volRef} />
<style jsx>{`
.vol {
position: absolute;
overflow: hidden;
inset: 0px;
z-index: 0;
border-radius: 999px;
transition: all 0.1s ease;
transform: scale(0);
}
`}</style>
</>
);
};
export const AudioIndicatorBar: React.FC = () => {
const localSessionId = useLocalSessionId();
const audioTrack = useAudioTrack(localSessionId);
const volRef = useRef<HTMLDivElement>(null);
useAudioLevel(
audioTrack?.persistentTrack,
useCallback((volume) => {
if (volRef.current)
volRef.current.style.width = Math.max(2, volume * 100) + "%";
}, [])
);
return (
<div className="flex-1 bg-gray-200 h-[8px] rounded-full overflow-hidden">
<div
className="bg-green-500 h-[8px] w-[0] rounded-full transition-all duration-100 ease"
ref={volRef}
/>
</div>
);
};
export default AudioIndicator;

View File

@@ -0,0 +1,139 @@
"use client";
import { useEffect } from "react";
import { DailyMeetingState } from "@daily-co/daily-js";
import { useDaily, useDevices } from "@daily-co/daily-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { IconMicrophone, IconDeviceSpeaker } from "@tabler/icons-react";
import { AudioIndicatorBar } from "../AudioIndicator";
interface Props {}
export default function DevicePicker({}: Props) {
const daily = useDaily();
const {
currentMic,
hasMicError,
micState,
microphones,
setMicrophone,
currentSpeaker,
speakers,
setSpeaker,
} = useDevices();
const handleMicrophoneChange = (value: string) => {
setMicrophone(value);
};
const handleSpeakerChange = (value: string) => {
setSpeaker(value);
};
useEffect(() => {
if (microphones.length > 0 || !daily || daily.isDestroyed()) return;
const meetingState = daily.meetingState();
const meetingStatesBeforeJoin: DailyMeetingState[] = [
"new",
"loading",
"loaded",
];
if (meetingStatesBeforeJoin.includes(meetingState)) {
daily.startCamera({ startVideoOff: true, startAudioOff: false });
}
}, [daily, microphones]);
return (
<div className="flex flex-col gap-5">
<section>
<label className="uppercase text-sm tracking-wider text-gray-500">
Microphone:
</label>
<div className="flex flex-row gap-4 items-center mt-2">
<IconMicrophone size={24} />
<div className="flex flex-col flex-1 gap-3">
<Select onValueChange={handleMicrophoneChange}>
<SelectTrigger className="">
<SelectValue
placeholder={
hasMicError ? "error" : currentMic?.device?.label
}
/>
</SelectTrigger>
<SelectContent>
{hasMicError && (
<option value="error" disabled>
No microphone access.
</option>
)}
{microphones.map((m) => (
<SelectItem key={m.device.deviceId} value={m.device.deviceId}>
{m.device.label}
</SelectItem>
))}
</SelectContent>
</Select>
<AudioIndicatorBar />
</div>
</div>
</section>
<section>
<label className="uppercase text-sm tracking-wider text-gray-500">
Speakers:
</label>
<div className="flex flex-row gap-4 items-center mt-2">
<IconDeviceSpeaker size={24} />
<Select onValueChange={handleSpeakerChange}>
<SelectTrigger className="">
<SelectValue placeholder={currentSpeaker?.device?.label} />
</SelectTrigger>
<SelectContent>
{speakers.map((m) => (
<SelectItem key={m.device.deviceId} value={m.device.deviceId}>
{m.device.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</section>
{hasMicError && (
<div className="error">
{micState === "blocked" ? (
<p>
Please check your browser and system permissions. Make sure that
this app is allowed to access your microphone.
</p>
) : micState === "in-use" ? (
<p>
Your microphone is being used by another app. Please close any
other apps using your microphone and restart this app.
</p>
) : micState === "not-found" ? (
<p>
No microphone seems to be connected. Please connect a microphone.
</p>
) : micState === "not-supported" ? (
<p>
This app is not supported on your device. Please update your
software or use a different device.
</p>
) : (
<p>
There seems to be an issue accessing your microphone. Try
restarting the app or consult a system administrator.
</p>
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,34 @@
import {
useDaily,
useLocalSessionId,
useMediaTrack,
} from "@daily-co/daily-react";
import { useCallback } from "react";
import { IconMicrophone, IconMicrophoneOff } from "@tabler/icons-react";
export const MicToggle: React.FC = () => {
const daily = useDaily();
const localSessionId = useLocalSessionId();
const audioTrack = useMediaTrack(localSessionId, "audio");
const isMicMuted =
audioTrack.state === "blocked" || audioTrack.state === "off";
const handleClick = useCallback(() => {
if (!daily) return;
daily.setLocalAudio(isMicMuted);
}, [daily, isMicMuted]);
const text = isMicMuted ? (
<IconMicrophone size={21} />
) : (
<IconMicrophoneOff size={21} />
);
return (
<button className="MicToggle UIButton" onClick={handleClick}>
{text}
</button>
);
};
export default MicToggle;

View File

@@ -0,0 +1,93 @@
import React from "react";
import { Button } from "@/components/ui/button";
import DevicePicker from "@/components/DevicePicker";
import { IconEar, IconLoader2 } from "@tabler/icons-react";
type SetupProps = {
handleStart: () => void;
};
const buttonLabel = {
intro: "Next",
setup: "Let's begin!",
loading: "Joining...",
};
export const Setup: React.FC<SetupProps> = ({ handleStart }) => {
const [state, setState] = React.useState<"intro" | "setup" | "loading">(
"intro"
);
return (
<div className="w-full flex flex-col items-center justify-between">
<div className="bg-white rounded-3xl cardAnim cardShadow p-9 max-w-screen-sm mx-auto outline outline-[5px] outline-gray-600/10 my-auto">
<div className="flex flex-col gap-6">
<h1 className="text-4xl font-bold text-pretty tracking-tighter mb-4">
Welcome to <span className="text-sky-500">Storytime</span>
</h1>
{state === "intro" ? (
<>
<p className="text-gray-600 leading-relaxed text-pretty">
This app demos a voice-controlled storytelling chatbot. It will
start with the bot asking you what kind of story you&apos;d like
to hear (e.g. a fairy tale, a mystery, etc.). After each scene,
the bot will pause to ask for your input. Direct the story any
way you choose!
</p>
<p className="flex flex-row gap-2 text-gray-600 font-medium">
<IconEar size={24} /> For best results, try in a quiet
environment!
</p>
</>
) : (
<>
<p className="text-gray-600 leading-relaxed text-pretty">
Since you&apos;ll be talking to Storybot, we need to make sure
it can hear you! Please configure your microphone and speakers
below.
</p>
<DevicePicker />
</>
)}
<hr className="border-gray-150 my-2" />
<Button
size="lg"
disabled={state === "loading"}
onClick={() => {
if (state === "intro") {
setState("setup");
} else {
setState("loading");
handleStart();
}
}}
>
{state === "loading" && (
<IconLoader2
size={21}
stroke={2}
className="mr-2 h-4 w-4 animate-spin"
/>
)}
{buttonLabel[state]}
</Button>
</div>
</div>
<footer className="flex-0 text-center font-mono text-sm text-gray-100 py-6">
<span className="bg-gray-800/70 px-3 py-1 rounded-md">
Created with{" "}
<a
href="https://git.new/ai"
className="text-violet-300 underline decoration-violet-400 hover:text-violet-100"
>
git.new/ai
</a>
</span>
</footer>
</div>
);
};
export default Setup;

View File

@@ -0,0 +1,79 @@
import React, { useState } from "react";
import {
useDaily,
useParticipantIds,
useAppMessage,
DailyAudio,
} from "@daily-co/daily-react";
import { IconLogout, IconLoader2 } from "@tabler/icons-react";
import VideoTile from "@/components/VideoTile";
import { Button } from "@/components/ui/button";
import UserInputIndicator from "@/components/UserInputIndicator";
import WaveText from "@/components/WaveText";
interface StoryProps {
handleLeave: () => void;
}
const Story: React.FC<StoryProps> = ({ handleLeave }) => {
const daily = useDaily();
const participantIds = useParticipantIds({ filter: "remote" });
const [storyState, setStoryState] = useState<"user" | "assistant">(
"assistant"
);
useAppMessage({
onAppMessage: (e) => {
if (!daily || !e.data?.cue) return;
// Determine the UI state from the cue sent by the bot
if (e.data?.cue === "user_turn") {
// Delay enabling local mic input to avoid feedback from LLM
setTimeout(() => daily.setLocalAudio(true), 500);
setStoryState("user");
} else {
daily.setLocalAudio(false);
setStoryState("assistant");
}
},
});
return (
<div className="w-full flex flex-col flex-1 self-stretch">
{/* Absolute elements */}
<div className="absolute top-20 w-full text-center z-50">
<WaveText active={storyState === "user"} />
</div>
<header className="flex absolute top-0 w-full z-50 p-6 justify-end">
<Button variant="secondary" onClick={() => handleLeave()}>
<IconLogout size={21} className="mr-2" />
Exit
</Button>
</header>
<div className="absolute inset-0 bg-gray-800 bg-opacity-90 z-10 fade-in"></div>
{/* Static elements */}
<div className="relative z-20 flex-1 flex items-center justify-center">
{participantIds.length >= 1 ? (
<VideoTile
sessionId={participantIds[0]}
inactive={storyState === "user"}
/>
) : (
<span className="p-3 rounded-full bg-gray-900/60 animate-pulse">
<IconLoader2
size={42}
stroke={2}
className="animate-spin text-white z-20 self-center"
/>
</span>
)}
<DailyAudio />
</div>
<UserInputIndicator active={storyState === "user"} />
</div>
);
};
export default Story;

View File

@@ -0,0 +1,42 @@
.container{
position: absolute;
color:white;
z-index: 50;
margin: 0 auto;
display: flex;
flex-direction: column;
@apply gap-4 inset-6;
align-items: center;
justify-content: end;
text-align: center;
}
.transcript{
@apply font-semibold;
}
.transcript span{
box-decoration-break: clone;
@apply bg-gray-900/80 rounded-md px-4 py-2;
}
.sentence{
opacity: 1;
animation: fadeOut 2.5s linear forwards;
animation-delay: 1s;
}
@keyframes fadeOut {
0% {
opacity: 1;
transform: scale(1);
}
20% {
transform: scale(1);
filter: blur(0);
}
100% {
transform: scale(0.8) translateY(-50%);
filter: blur(25px);
opacity: 0;
}
}

View File

@@ -0,0 +1,55 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useAppMessage } from "@daily-co/daily-react";
import { DailyEventObjectAppMessage } from "@daily-co/daily-js";
import styles from "./StoryTranscript.module.css";
export default function StoryTranscript() {
const [partialText, setPartialText] = useState<string>("");
const [sentences, setSentences] = useState<string[]>([]);
const intervalRef = useRef<any | null>(null);
useEffect(() => {
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
if (sentences.length > 2) {
setSentences((s) => s.slice(1));
}
}, 2500);
return () => clearInterval(intervalRef.current);
}, [sentences]);
useAppMessage({
onAppMessage: (e: DailyEventObjectAppMessage<any>) => {
if (e.fromId && e.fromId === "transcription") {
// Check for LLM transcripts only
if (e.data.user_id !== "") {
setPartialText(e.data.text);
if (e.data.is_final) {
setPartialText("");
setSentences((s) => [...s, e.data.text]);
}
}
}
},
});
return (
<div className={styles.container}>
{sentences.map((sentence, index) => (
<p key={index} className={`${styles.transcript} ${styles.sentence}`}>
<span>{sentence}</span>
</p>
))}
{partialText && (
<p className={`${styles.transcript}`}>
<span>{partialText}</span>
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,82 @@
.panel{
width: 100%;
pointer-events: none;
color: #ffffff;
text-align: center;
position: relative;
@apply pb-8;
}
.panel::after{
content: "";
position: absolute;
inset: 0px;
z-index: 10;
opacity: 0;
transition: all 0.3s ease;
@apply bg-gradient-to-t from-gray-950 to-transparent;
}
.active::after{ opacity: 1;}
.micIcon{
position: relative;
width: 120px;
height: 120px;
border-radius: 120px;
border: 6px solid;
outline: 6px solid;
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto;
z-index: 20;
transition: all 0.5s ease;
@apply bg-gray-700/60 border-gray-600 outline-gray-900/20;
}
.micIcon svg{
position: relative;
z-index: 20;
opacity: 0.25;
transition: opacity 0.5s ease;
}
@keyframes pulse {
0% {
outline-width: 6px;
@apply outline-teal-500/10;
}
50% {
outline-width: 24px;
@apply outline-teal-500/50;
}
100% {
outline-width: 6px;
@apply outline-teal-500/10;
}
}
.micIconActive{
@apply bg-teal-950 border-teal-500 outline-teal-500/20;
animation: pulse 2s infinite ease-in-out;
}
.micIconActive svg{
opacity: 1;
}
.transcript{
flex: 0;
align-self: center;
opacity: 0.25;
transition: opacity 1s ease;
transition-delay: 2.5s;
@apply bg-gray-900/90 font-medium py-1 px-2 rounded-sm mt-4;
}
.active .transcript{
opacity: 1;
}

View File

@@ -0,0 +1,48 @@
import React, { useState, useEffect } from "react";
import { useAppMessage } from "@daily-co/daily-react";
import { DailyEventObjectAppMessage } from "@daily-co/daily-js";
import styles from "./UserInputIndicator.module.css";
import { IconMicrophone } from "@tabler/icons-react";
import { TypewriterEffect } from "../ui/typewriter";
import AudioIndicator from "../AudioIndicator";
interface Props {
active: boolean;
}
export default function UserInputIndicator({ active }: Props) {
const [transcription, setTranscription] = useState<string[]>([]);
useAppMessage({
onAppMessage: (e: DailyEventObjectAppMessage<any>) => {
if (e.fromId && e.fromId === "transcription") {
if (e.data.user_id === "" && e.data.is_final) {
setTranscription((t) => [...t, ...e.data.text.split(" ")]);
}
}
},
});
useEffect(() => {
if (active) return;
const t = setTimeout(() => setTranscription([]), 4000);
return () => clearTimeout(t);
}, [active]);
return (
<div className={`${styles.panel} ${active ? styles.active : ""}`}>
<div className="relative z-20 flex flex-col">
<div
className={`${styles.micIcon} ${active ? styles.micIconActive : ""}`}
>
<IconMicrophone size={42} />
{active && <AudioIndicator />}
</div>
<footer className={styles.transcript}>
<TypewriterEffect words={transcription} />
</footer>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
.container{
position: relative;
animation: fadeIn 3s ease;
transition: all 3s ease-out;
}
.videoTile{
@apply bg-gray-950;
width: 560px;
height: 560px;
mask-image: url('/alpha-mask.gif');
mask-size: cover;
mask-repeat: no-repeat;
margin:0 auto;
z-index: 10;
position: relative;
}
.inactive{
opacity: 0.7;
filter:blur(3px);
transform: scale(0.95)
}
@keyframes fadeIn {
0% {
filter: blur(100px);
opacity: 0;
}
100% {
filter: blur(0px);
opacity: 1;
}
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import styles from "./VideoTile.module.css";
import { DailyVideo } from "@daily-co/daily-react";
import StoryTranscript from "@/components/StoryTranscript";
interface Props {
sessionId: string;
inactive: boolean;
}
const VideoTile = ({ sessionId, inactive }: Props) => {
return (
<div className={`${styles.container} ${inactive ? styles.inactive : ""} `}>
<StoryTranscript />
<div className={styles.videoTile}>
<DailyVideo
sessionId={sessionId}
type={"video"}
className="aspect-square"
/>
</div>
</div>
);
};
export default VideoTile;

View File

@@ -0,0 +1,68 @@
.waveText{
color: white;
text-shadow: 3px 3px 0px rgba(0,0,0,0.5);
opacity: 0;
transition: opacity 2s ease;
position: relative;
@apply text-4xl font-bold;
}
.active{
opacity: 1;
}
@keyframes jump {
0% {
opacity: 0.5;
transform:translateY(0px)
}
50% {
opacity: 1;
transform:translateY(-30px);
}
100% {
opacity: 0.55;
transform:translateY(0px)
}
}
.waveText span{
display:inline-block;
animation:jump 2s infinite ease-in-out;
}
.waveText span:nth-child(1) {
animation-delay:0s;
}
.waveText span:nth-child(1) {
animation-delay:0.1s;
}
.waveText span:nth-child(2) {
animation-delay:0.2s;
}
.waveText span:nth-child(3) {
animation-delay:0.3s;
}
.waveText span:nth-child(4) {
animation-delay:0.4s;
}
.waveText span:nth-child(5) {
animation-delay:0.5s;
}
.waveText span:nth-child(6) {
animation-delay:0.6s;
}
.waveText span:nth-child(7) {
animation-delay:0.7s;
}
.waveText span:nth-child(8) {
animation-delay:0.8s;
}
.waveText span:nth-child(9) {
animation-delay:0.9s;
}
.waveText span:nth-child(10) {
animation-delay:1s;
}

View File

@@ -0,0 +1,23 @@
import React from "react";
import styles from "./WaveText.module.css";
interface Props {
active: boolean;
}
export default function WaveText({ active }: Props) {
return (
<div className={`${styles.waveText} ${active ? styles.active : ""}`}>
<span>W</span>
<span>h</span>
<span>a</span>
<span>t</span>
<span>&nbsp;&nbsp;</span>
<span>n</span>
<span>e</span>
<span>x</span>
<span>t</span>
<span>?</span>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/app/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-11 rounded-lg px-6 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-12 rounded-xl px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -0,0 +1,160 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { IconCheck, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { cn } from "@/app/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex gap-3 h-12 w-full items-center justify-between rounded-xl border border-input bg-background px-4 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<IconChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<IconChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<IconChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<IconCheck className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View File

@@ -0,0 +1,61 @@
"use client";
import { cn } from "@/app/utils";
import { motion } from "framer-motion";
export const TypewriterEffect = ({
words,
className,
}: {
words: string[];
className?: string;
cursorClassName?: string;
}) => {
const renderWords = () => {
return (
<div>
{words.map((word, idx) => {
return (
<div key={`word-${idx}`} className="inline-block">
{word.split("").map((char, index) => (
<span key={`char-${index}`}>{char}</span>
))}
&nbsp;
</div>
);
})}
</div>
);
};
return (
<div className={cn("flex", className)}>
{words.length < 1 ? (
<span>...</span>
) : (
<motion.div
className="overflow-hidden"
initial={{
width: "0%",
}}
whileInView={{
width: "fit-content",
}}
transition={{
duration: 0.5,
ease: "linear",
delay: 1,
}}
>
<div
style={{
whiteSpace: "nowrap",
}}
>
{renderWords()}
</div>
</motion.div>
)}
</div>
);
};

View File

@@ -0,0 +1,2 @@
NEXT_PUBLIC_ROOM_URL=
SITE_URL=

View File

@@ -0,0 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
async rewrites() {
return [
{
source: "/:path*",
destination: "http://localhost:7860/:path*",
},
];
},
};
export default nextConfig;

View File

@@ -0,0 +1,38 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@daily-co/daily-js": "^0.62.0",
"@daily-co/daily-react": "^0.18.0",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@tabler/icons-react": "^3.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"framer-motion": "^11.0.27",
"next": "14.1.4",
"react": "^18",
"react-dom": "^18",
"recoil": "^0.7.7",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.4",
"postcss": "^8",
"tailwindcss": "^3.4.3",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

View File

@@ -0,0 +1,86 @@
import type { Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";
const config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
mono: ["var(--font-mono)", ...fontFamily.mono],
},
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;
export default config;

View File

@@ -0,0 +1,40 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"build/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

File diff suppressed because it is too large Load Diff