demo: Restructure storytelling-chatbot directory, update README steps, link to vercel demo
This commit is contained in:
3
examples/storytelling-chatbot/client/.eslintrc.json
Normal file
3
examples/storytelling-chatbot/client/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
36
examples/storytelling-chatbot/client/.gitignore
vendored
Normal file
36
examples/storytelling-chatbot/client/.gitignore
vendored
Normal 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
|
||||
36
examples/storytelling-chatbot/client/README.md
Normal file
36
examples/storytelling-chatbot/client/README.md
Normal 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.
|
||||
BIN
examples/storytelling-chatbot/client/app/favicon.ico
Normal file
BIN
examples/storytelling-chatbot/client/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
109
examples/storytelling-chatbot/client/app/globals.css
Normal file
109
examples/storytelling-chatbot/client/app/globals.css
Normal 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;
|
||||
}
|
||||
}
|
||||
46
examples/storytelling-chatbot/client/app/layout.tsx
Normal file
46
examples/storytelling-chatbot/client/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
BIN
examples/storytelling-chatbot/client/app/opengraph-image.png
Normal file
BIN
examples/storytelling-chatbot/client/app/opengraph-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
16
examples/storytelling-chatbot/client/app/page.tsx
Normal file
16
examples/storytelling-chatbot/client/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
BIN
examples/storytelling-chatbot/client/app/twitter-image.png
Normal file
BIN
examples/storytelling-chatbot/client/app/twitter-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
6
examples/storytelling-chatbot/client/app/utils.ts
Normal file
6
examples/storytelling-chatbot/client/app/utils.ts
Normal 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))
|
||||
}
|
||||
17
examples/storytelling-chatbot/client/components.json
Normal file
17
examples/storytelling-chatbot/client/components.json
Normal 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"
|
||||
}
|
||||
}
|
||||
81
examples/storytelling-chatbot/client/components/App.tsx
Normal file
81
examples/storytelling-chatbot/client/components/App.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"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("/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
setState("started");
|
||||
} catch (error) {
|
||||
setState("error");
|
||||
}
|
||||
}
|
||||
|
||||
async function leave() {
|
||||
await daily?.leave();
|
||||
setState("finished");
|
||||
}
|
||||
|
||||
if (state === "error") {
|
||||
return (
|
||||
<div className="flex items-center mx-auto">
|
||||
<p className="text-red-500 font-semibold bg-white px-4 py-2 shadow-xl rounded-lg">
|
||||
This demo is currently at capacity. Please try again later.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === "started") {
|
||||
return <Story handleLeave={() => leave()} />;
|
||||
}
|
||||
|
||||
return <Setup handleStart={() => start()} />;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 className="text-red-500">
|
||||
Please check your browser and system permissions. Make sure that
|
||||
this app is allowed to access your microphone.
|
||||
</p>
|
||||
) : micState === "in-use" ? (
|
||||
<p className="text-red-500">
|
||||
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 className="text-red-500">
|
||||
No microphone seems to be connected. Please connect a microphone.
|
||||
</p>
|
||||
) : micState === "not-supported" ? (
|
||||
<p className="text-red-500">
|
||||
This app is not supported on your device. Please update your
|
||||
software or use a different device.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-red-500">
|
||||
There seems to be an issue accessing your microphone. Try
|
||||
restarting the app or consult a system administrator.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
94
examples/storytelling-chatbot/client/components/Setup.tsx
Normal file
94
examples/storytelling-chatbot/client/components/Setup.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import DevicePicker from "@/components/DevicePicker";
|
||||
import { IconAlertCircle, 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'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="flex flex-row gap-2 text-gray-600 font-medium text-red-500">
|
||||
<IconAlertCircle size={24} /> This demo expires after 5 minutes.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-gray-600 leading-relaxed text-pretty">
|
||||
Since you'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;
|
||||
81
examples/storytelling-chatbot/client/components/Story.tsx
Normal file
81
examples/storytelling-chatbot/client/components/Story.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { useState, useEffect } 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 {
|
||||
// Uncomment the next line to mute the mic while the
|
||||
// assistant it talking. Leave it commented to allow for interruptions
|
||||
// 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={false}
|
||||
/>
|
||||
) : (
|
||||
<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={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Story;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.micIconActive{
|
||||
@apply bg-teal-950 border-teal-500 outline-teal-500/20;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useState, useEffect, useRef } 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[]>([]);
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const resetTimeout = () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setTranscription([]);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
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(" ")]);
|
||||
resetTimeout();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.container{
|
||||
position: relative;
|
||||
animation: fadeIn 3s ease;
|
||||
transition: all 3s ease-out;
|
||||
}
|
||||
|
||||
.videoTile{
|
||||
@apply bg-gray-950;
|
||||
width: 760px;
|
||||
height: 760px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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> </span>
|
||||
<span>n</span>
|
||||
<span>e</span>
|
||||
<span>x</span>
|
||||
<span>t</span>
|
||||
<span>?</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
160
examples/storytelling-chatbot/client/components/ui/select.tsx
Normal file
160
examples/storytelling-chatbot/client/components/ui/select.tsx
Normal 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,
|
||||
};
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
</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>
|
||||
);
|
||||
};
|
||||
1
examples/storytelling-chatbot/client/env.example
Normal file
1
examples/storytelling-chatbot/client/env.example
Normal file
@@ -0,0 +1 @@
|
||||
SITE_URL=
|
||||
15
examples/storytelling-chatbot/client/next.config.mjs
Normal file
15
examples/storytelling-chatbot/client/next.config.mjs
Normal 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;
|
||||
6432
examples/storytelling-chatbot/client/package-lock.json
generated
Normal file
6432
examples/storytelling-chatbot/client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
examples/storytelling-chatbot/client/package.json
Normal file
38
examples/storytelling-chatbot/client/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "client",
|
||||
"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.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@tabler/icons-react": "^3.19.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.9.0",
|
||||
"next": "^14.2.25",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recoil": "^0.7.7",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.16.10",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "14.1.4",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
6
examples/storytelling-chatbot/client/postcss.config.js
Normal file
6
examples/storytelling-chatbot/client/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
examples/storytelling-chatbot/client/public/alpha-mask.gif
Normal file
BIN
examples/storytelling-chatbot/client/public/alpha-mask.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
examples/storytelling-chatbot/client/public/bg.jpg
Normal file
BIN
examples/storytelling-chatbot/client/public/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 788 KiB |
86
examples/storytelling-chatbot/client/tailwind.config.ts
Normal file
86
examples/storytelling-chatbot/client/tailwind.config.ts
Normal 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;
|
||||
40
examples/storytelling-chatbot/client/tsconfig.json
Normal file
40
examples/storytelling-chatbot/client/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user