fixed runner

This commit is contained in:
Jon Taylor
2024-05-16 18:57:34 +01:00
parent 72da9320da
commit dab01e0d58
37 changed files with 214 additions and 109 deletions

140
examples/web-ui/src/App.tsx Normal file
View File

@@ -0,0 +1,140 @@
import { useState } from "react";
import { useDaily } from "@daily-co/daily-react";
import { Alert } from "./components/alert";
import { Button } from "./components/button";
import { ArrowRight, Loader2 } from "lucide-react";
import { DeviceSelect } from "./components/DeviceSelect";
import Session from "./components/Session";
type State =
| "idle"
| "configuring"
| "requesting_agent"
| "connecting"
| "connected"
| "started"
| "finished"
| "error";
export default function App() {
// Use Daily as our agent transport
const daily = useDaily();
const [state, setState] = useState<State>("idle");
const [error, setError] = useState<string | null>(null);
//const [room, setRoom] = useState<string | null>(null);
async function start() {
if (!daily) return;
setState("requesting_agent");
const serverUrl =
import.meta.env.VITE_SERVER_URL || import.meta.env.BASE_URL;
// Request a bot to join your session
let data;
try {
const res = await fetch(`${serverUrl}start_bot`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
data = await res.json();
if (!res.ok) {
setError(data.detail);
setState("error");
return;
}
} catch (e) {
setError(
`Unable to connect to the server at '${serverUrl}' - is it running?`
);
setState("error");
return;
}
setState("connecting");
await daily.join({
url: data.room_url,
token: data.token,
videoSource: false,
startAudioOff: true,
});
setState("connected");
}
async function leave() {
await daily?.leave();
setState("idle");
}
if (state === "error") {
return (
<Alert intent="danger" title="An error occurred">
{error}
</Alert>
);
}
if (state === "connected") {
return <Session onLeave={() => leave()} />;
}
const status_text = {
configuring: "Start",
requesting_agent: "Requesting agent...",
connecting: "Connecting to agent...",
};
if (state !== "idle") {
return (
<div className="card card-appear">
<div className="card-inner">
<h1 className="card-header">Configure your devices</h1>
<p className="card-text">
Please configure your microphone and speakers below
</p>
<DeviceSelect />
<Button
key="start"
onClick={() => start()}
disabled={state !== "configuring"}
>
{state !== "configuring" && <Loader2 className="animate-spin" />}
{status_text[state as keyof typeof status_text]}
</Button>
</div>
</div>
);
}
return (
<div className="card card-appear">
<div className="card-inner">
<h1 className="card-header">Pipecat Simple Chatbot</h1>
<p className="card-text">
Please ensure you microphone and speakers are connected and ready to
go
</p>
{import.meta.env.DEV && !import.meta.env.VITE_SERVER_URL && (
<div>
Warning: you have not set a server URL for local development. Please
set <code>VITE_SERVER_URL</code> in{" "}
<code>.env.development.local</code>
</div>
)}
<Button key="next" onClick={() => setState("configuring")}>
Next <ArrowRight />
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
<svg width="332" height="192" viewBox="0 0 332 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.7718 0.769635C50.4477 -0.990844 55.7252 0.330188 59.0204 4.08595L101.936 53H230.064L272.98 4.08595C276.275 0.330188 281.552 -0.990844 286.228 0.769635C290.904 2.53011 294 7.00367 294 12V120H332V144H270V43.8728L244.52 72.9141C242.242 75.5111 238.955 77 235.5 77H96.5C93.0452 77 89.7581 75.5111 87.4796 72.9141L62 43.8728V144H0V120H38V12C38 7.00367 41.0958 2.53011 45.7718 0.769635Z" fill="black"/>
<path d="M270 168H332V192H270V168Z" fill="black"/>
<path d="M0 168H62V192H0V168Z" fill="black"/>
<path d="M128 128C128 136.837 120.837 144 112 144C103.163 144 96 136.837 96 128C96 119.164 103.163 112 112 112C120.837 112 128 119.164 128 128Z" fill="black"/>
<path d="M236 128C236 136.837 228.837 144 220 144C211.163 144 204 136.837 204 128C204 119.164 211.163 112 220 112C228.837 112 236 119.164 236 128Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 937 B

View File

@@ -0,0 +1,31 @@
import {
useAudioLevel,
useAudioTrack,
useLocalSessionId,
} from "@daily-co/daily-react";
import { useCallback, useRef } from "react";
import styles from "./styles.module.css";
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={styles.bar}>
<div ref={volRef} />
</div>
);
};
export default AudioIndicatorBar;

View File

@@ -0,0 +1,15 @@
.bar {
background: var(--color-gray-200);
height: 8px;
width: 100%;
border-radius: 999px;
overflow: hidden;
> div {
background: var(--color-green-500);
height: 8px;
width: 0px;
border-radius: 999px;
transition: width 0.1s ease;
}
}

View File

@@ -0,0 +1,127 @@
"use client";
import { useEffect } from "react";
import { DailyMeetingState } from "@daily-co/daily-js";
import { useDaily, useDevices } from "@daily-co/daily-react";
import { Mic, Speaker } from "lucide-react";
import { AudioIndicatorBar } from "../AudioIndicator";
import styles from "./styles.module.css";
import { Alert } from "../alert";
export function DeviceSelect() {
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={styles.deviceSelect}>
{hasMicError && (
<Alert intent="danger" title="Device error">
{micState === "blocked" ? (
<>
Please check your browser and system permissions. Make sure that
this app is allowed to access your microphone and refresh the
page.
</>
) : micState === "in-use" ? (
<>
Your microphone is being used by another app. Please close any
other apps using your microphone and restart this app.
</>
) : micState === "not-found" ? (
<>
No microphone seems to be connected. Please connect a microphone.
</>
) : micState === "not-supported" ? (
<>
This app is not supported on your device. Please update your
software or use a different device.
</>
) : (
<>
There seems to be an issue accessing your microphone. Try
restarting the app or consult a system administrator.
</>
)}
</Alert>
)}
<section className={styles.field}>
<label className={styles.label}>Microphone:</label>
<div className={styles.selectContainer}>
<Mic size={24} />
<select
onChange={(e) => handleMicrophoneChange(e.target.value)}
defaultValue={currentMic?.device.deviceId}
className={styles.deviceSelectField}
>
{microphones.length === 0 ? (
<option value="">Loading devices...</option>
) : (
microphones.map((m) => (
<option key={m.device.deviceId} value={m.device.deviceId}>
{m.device.label}
</option>
))
)}
</select>
</div>
<AudioIndicatorBar />
</section>
<section className={styles.field}>
<label className={styles.label}>Speakers:</label>
<div className={styles.selectContainer}>
<Speaker size={24} />
<select
onChange={(e) => handleSpeakerChange(e.target.value)}
defaultValue={currentSpeaker?.device.deviceId}
className={styles.deviceSelectField}
>
{speakers.length === 0 ? (
<option value="">Loading devices...</option>
) : (
speakers.map((m) => (
<option key={m.device.deviceId} value={m.device.deviceId}>
{m.device.label}
</option>
))
)}
</select>
</div>
</section>
</div>
);
}
export default DeviceSelect;

View File

@@ -0,0 +1,39 @@
.deviceSelect {
display: flex;
flex-flow: column wrap;
width: 100%;
gap: 1rem;
}
.field {
display: flex;
flex-flow: column wrap;
align-items: flex-start;
gap: 0.5rem;
}
.label {
font-weight: 600;
font-size: 0.875rem;
}
.selectContainer {
position: relative;
width: 100%;
> svg {
position: absolute;
size: 24px;
top: 0;
bottom: 0;
margin: auto 0;
left: 0.75rem;
color: var(--color-gray-400);
}
}
.deviceSelectField {
width: 100%;
padding-left: 2.875rem;
margin-top: auto;
}

View File

@@ -0,0 +1,22 @@
import React from "react";
import Status from "./status";
import styles from "./styles.module.css";
import { useParticipantIds } from "@daily-co/daily-react";
export const Agent: React.FC = () => {
const participantIds = useParticipantIds({ filter: "remote" });
const status = participantIds.length > 0 ? "connected" : "connecting";
return (
<div className={styles.agent}>
<div className={styles.agentWindow}></div>
<footer className={styles.agentFooter}>
<Status>User status</Status>
<Status variant={status}>Agent status</Status>
</footer>
</div>
);
};
export default Agent;

View File

@@ -0,0 +1,78 @@
import React, { useEffect, useRef, useState } from "react";
import { LogOut, Settings } from "lucide-react";
import { DailyAudio, useAppMessage, useDaily } from "@daily-co/daily-react";
import DeviceSelect from "../DeviceSelect";
import Agent from "./agent";
import { Button } from "../button";
import UserMicBubble from "../UserMicBubble";
import styles from "./styles.module.css";
export const Session: React.FC<{ onLeave: () => void }> = ({ onLeave }) => {
const daily = useDaily();
const [showDevices, setShowDevices] = useState(false);
const modalRef = useRef<HTMLDialogElement>(null);
const [talkState, setTalkState] = 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);
setTalkState("user");
} else {
daily.setLocalAudio(false);
setTalkState("assistant");
}
},
});
useEffect(() => {
const current = modalRef.current;
// Backdrop doesn't currently work with dialog open, so we use setModal instead
if (current && showDevices) {
current.inert = true;
current.showModal();
current.inert = false;
}
return () => current?.close();
}, [showDevices]);
return (
<>
<dialog ref={modalRef}>
<h2>Configure devices</h2>
<DeviceSelect />
<Button onClick={() => setShowDevices(false)}>Close</Button>
</dialog>
<div className={styles.agentContainer}>
<Agent />
<UserMicBubble active={talkState === "user"} />
<DailyAudio />
</div>
<footer className={styles.footer}>
<div className={styles.controls}>
<Button
variant="ghost"
size="icon"
onClick={() => setShowDevices(true)}
>
<Settings />
</Button>
<Button onClick={() => onLeave()}>
<LogOut size={16} />
End
</Button>
</div>
</footer>
</>
);
};
export default Session;

View File

@@ -0,0 +1,42 @@
import { VariantProps, cva } from "class-variance-authority";
import styles from "./styles.module.css";
const statusVariants = cva(styles.statusIndicator, {
variants: {
variant: {
default: styles.statusDefault,
connecting: styles.statusOrange,
connected: styles.statusGreen,
},
},
defaultVariants: {
variant: "default",
},
});
export interface StatusProps
extends React.HTMLAttributes<HTMLElement>,
VariantProps<typeof statusVariants> {}
const status_text = {
default: "Idle",
connecting: "Connecting",
connected: "Connected",
};
export const Status: React.FC<StatusProps> = ({
children,
variant = "default",
}) => {
return (
<div className={styles.status}>
<span>{children}</span>
<div className={statusVariants({ variant })}>
<span />
{status_text[variant as keyof typeof status_text]}
</div>
</div>
);
};
export default Status;

View File

@@ -0,0 +1,110 @@
.footer {
display: flex;
flex-flow: row nowrap;
margin-top: auto;
align-self: flex-end;
}
.controls {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
gap: 0.75rem;
}
.agentContainer {
flex: 1;
display: flex;
flex-flow: column wrap;
align-items: center;
justify-content: center;
}
.agent {
margin-top: auto;
min-width: 420px;
padding: 0.75rem;
border-radius: var(--borderRadius-lg);
border-radius: 24px;
border: 1px solid #e2e8f0;
background: #fff;
box-shadow: 0px 360px 101px 0px rgba(0, 0, 0, 0),
0px 231px 92px 0px rgba(0, 0, 0, 0), 0px 130px 78px 0px rgba(0, 0, 0, 0.02),
0px 58px 58px 0px rgba(0, 0, 0, 0.03), 0px 14px 32px 0px rgba(0, 0, 0, 0.03);
background-image: linear-gradient(90deg, white, white),
linear-gradient(0deg, var(--color-gray-300), var(--color-gray-200));
background-clip: padding-box, border-box;
background-origin: border-box;
border: 1px solid transparent;
}
.agentWindow {
min-width: 400px;
aspect-ratio: 1;
background: var(--color-gray-200);
border-radius: var(--borderRadius-md);
}
.agentFooter {
margin: 1.5rem 0 0.75rem 0;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
width: 100%;
> :first-child {
border-right: 1px solid var(--color-gray-200);
}
}
.status {
flex: 1;
line-height: 1;
display: flex;
flex-flow: column wrap;
align-items: center;
gap: 0.5rem;
> span {
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
line-height: 1;
letter-spacing: 0.7px;
}
}
.statusIndicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
font-weight: 700;
padding: 0.5rem 0.75rem;
border-radius: var(--borderRadius-xs);
> span {
display: block;
width: 9px;
height: 9px;
border-radius: 9px;
background: red;
}
}
.statusDefault {
color: var(--color-gray-500);
background: var(--color-gray-100);
> span {
background: var(--color-gray-400);
}
}
.statusOrange {
color: var(--color-orange-800);
background: var(--color-orange-100);
> span {
background: var(--color-orange-400);
}
}

View File

@@ -0,0 +1,70 @@
import React, { useCallback, useRef } from "react";
import {
useAudioLevel,
useAudioTrack,
useLocalSessionId,
//useAppMessage,
} from "@daily-co/daily-react";
//import { DailyEventObjectAppMessage } from "@daily-co/daily-js";
import { Mic, MicOff } from "lucide-react";
//import { TypewriterEffect } from "../ui/typewriter";
import styles from "./styles.module.css";
const AudioIndicatorBubble: 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 ref={volRef} className={styles.volume} />;
};
interface Props {
active: boolean;
}
export default function UserMicBubble({ 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.bubbleContainer} ${active ? styles.active : ""}`}>
<div
className={`${styles.micIcon} ${active ? styles.micIconActive : ""}`}
>
{active ? <Mic size={42} /> : <MicOff size={42} />}
{active && <AudioIndicatorBubble />}
</div>
<footer className={styles.transcript}></footer>
</div>
);
}

View File

@@ -0,0 +1,96 @@
.active::after {
opacity: 1;
}
.bubbleContainer {
color: #ffffff;
position: relative;
z-index: 20;
display: flex;
flex-direction: column;
margin: auto;
}
.micIcon {
position: relative;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
width: 120px;
height: 120px;
border-radius: 120px;
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto;
z-index: 20;
transition: all 0.5s ease;
border: 6px solid color-mix(in srgb, var(--color-gray-300), transparent 70%);
outline: 6px solid color-mix(in srgb, var(--color-gray-300), transparent 70%);
background-color: var(--color-gray-500);
background-image: radial-gradient(
var(--color-gray-300),
var(--color-gray-400)
);
}
.micIcon svg {
position: relative;
z-index: 20;
opacity: 0.3;
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 {
background-color: var(--color-green-500);
background-image: radial-gradient(
var(--color-green-500),
var(--color-green-600)
);
border: 6px solid color-mix(in srgb, var(--color-green-200), transparent 60%);
outline: 6px solid color-mix(in srgb, var(--color-green-400), transparent 70%);
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;
}
.active .transcript {
opacity: 1;
}
.volume {
position: absolute;
overflow: hidden;
inset: 0px;
z-index: 0;
border-radius: 999px;
transition: all 0.1s ease;
transform: scale(0);
opacity: 0.4;
background-color: var(--color-green-200);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import { cva, VariantProps } from "class-variance-authority";
const alertVariants = cva("alert", {
variants: {
intent: {
info: "alert-info",
danger: "alert-danger",
},
},
defaultVariants: {
intent: "info",
},
});
export interface AlertProps
extends React.HTMLAttributes<HTMLElement>,
VariantProps<typeof alertVariants> {}
export const Alert: React.FC<AlertProps> = ({ children, intent, title }) => {
return (
<div className={alertVariants({ intent })}>
<span>{title}</span>
{children}
</div>
);
};

View File

@@ -0,0 +1,27 @@
import React from "react";
import { cva, VariantProps } from "class-variance-authority";
const buttonVariants = cva("button", {
variants: {
variant: {
primary: "button-primary",
ghost: "button-ghost",
},
size: {
base: "",
icon: "button-icon",
},
},
defaultVariants: {
variant: "primary",
size: "base",
},
});
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export const Button: React.FC<ButtonProps> = ({ variant, size, ...props }) => {
return <button className={buttonVariants({ variant, size })} {...props} />;
};

View File

@@ -0,0 +1,22 @@
import Logo from "./logo";
function Header() {
return (
<header className="header">
<span className="logo-button">
<Logo />
</span>
<nav>
<a href="https://git.new/ai" target="_blank">
GitHub
</a>
<a href="https://discord.gg/pipecat" target="_blank">
Discord
</a>
</nav>
</header>
);
}
export default Header;

View File

@@ -0,0 +1,46 @@
import React from "react";
const Logo: React.FC = () => {
return (
<svg
width="332"
height="192"
viewBox="0 0 332 192"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="logo"
>
<path
d="M45.7718 0.769635C50.4477 -0.990844 55.7252 0.330188 59.0204 4.08595L101.936 53H230.064L272.98 4.08595C276.275 0.330188 281.552 -0.990844 286.228 0.769635C290.904 2.53011 294 7.00367 294 12V120H332V144H270V43.8728L244.52 72.9141C242.242 75.5111 238.955 77 235.5 77H96.5C93.0452 77 89.7581 75.5111 87.4796 72.9141L62 43.8728V144H0V120H38V12C38 7.00367 41.0958 2.53011 45.7718 0.769635Z"
fill="black"
/>
<path d="M270 168H332V192H270V168Z" fill="black" />
<path d="M0 168H62V192H0V168Z" fill="black" />
<g id="eyes_1">
<path
d="M128 128C128 136.837 120.837 144 112 144C103.163 144 96 136.837 96 128C96 119.164 103.163 112 112 112C120.837 112 128 119.164 128 128Z"
fill="black"
/>
<path
d="M236 128C236 136.837 228.837 144 220 144C211.163 144 204 136.837 204 128C204 119.164 211.163 112 220 112C228.837 112 236 119.164 236 128Z"
fill="black"
/>
</g>
<g id="eyes_2" visibility="hidden">
<path
d="M128 128C128 136.837 120.837 144 112 144C103.163 144 96 136.837 96 128C96 119.163 103.163 112 112 112C120.837 112 128 119.163 128 128Z"
stroke="black"
strokeWidth="12"
/>
<path
d="M236 128C236 136.837 228.837 144 220 144C211.163 144 204 136.837 204 128C204 119.163 211.163 112 220 112C228.837 112 236 119.163 236 128Z"
stroke="black"
strokeWidth="12"
/>
</g>
</svg>
);
};
export default Logo;

View File

@@ -0,0 +1,387 @@
:root {
--color-gray-50: #f8fafc;
--color-gray-100: #f1f5f9;
--color-gray-200: #e2e8f0;
--color-gray-300: #cbd5e1;
--color-gray-400: #94a3b8;
--color-gray-500: #64748b;
--color-gray-600: #475569;
--color-gray-700: #334155;
--color-gray-800: #1e293b;
--color-gray-900: #0f172a;
--color-gray-950: #020617;
--color-orange-50: #fff7ed;
--color-orange-100: #ffedd5;
--color-orange-200: #fed7aa;
--color-orange-300: #fdba74;
--color-orange-400: #fb923c;
--color-orange-500: #f97316;
--color-orange-600: #ea580c;
--color-orange-700: #c2410c;
--color-orange-800: #9a3412;
--color-orange-900: #7c2d12;
--color-orange-950: #431407;
--color-green-50: #f0fdf4;
--color-green-100: #dcfce7;
--color-green-200: #bbf7d0;
--color-green-300: #86efac;
--color-green-400: #4ade80;
--color-green-500: #22c55e;
--color-green-600: #16a34a;
--color-green-700: #15803d;
--color-green-800: #166534;
--color-green-900: #14532d;
--color-green-950: #052e16;
--borderRadius-xs: 6px;
--borderRadius-sm: 9px;
--borderRadius-md: 12px;
--borderRadius-lg: 24px;
--input-height: 48px;
--font-sans: "Inter", system-ui, Avenir, Helvetica, Arial, sans-serif;
--font-mono: "Space Mono", monospace;
--font-size-base: 1rem;
--font-size-sm: 0.875rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
}
* {
font-family: var(--font-sans);
}
#root {
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0 auto;
padding: 1rem;
text-align: center;
display: flex;
flex-direction: column;
flex: 1;
}
* {
outline-color: black;
outline-offset: 4px;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
display: flex;
color: var(--color-gray-950);
background-color: var(--color-gray-50);
}
a {
color: var(--color-gray-950);
text-decoration: inherit;
}
a:hover {
text-decoration-line: underline;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.header {
display: flex;
align-self: flex-start;
align-items: center;
gap: 2rem;
}
.header .logo-button {
box-sizing: border-box;
border: 1px solid var(--color-gray-200);
padding: 0.5rem;
display: flex;
place-items: center;
border-radius: var(--borderRadius-md);
transition: all 0.3s ease;
background: white;
box-shadow: 0px 7px 2px 0px rgba(0, 0, 0, 0),
0px 5px 2px 0px rgba(0, 0, 0, 0.01), 0px 3px 2px 0px rgba(0, 0, 0, 0.03),
0px 1px 1px 0px rgba(0, 0, 0, 0.04), 0px 0px 1px 0px rgba(0, 0, 0, 0.05);
}
.header .logo {
width: 42px;
height: auto;
aspect-ratio: 1;
}
.header nav {
pointer-events: none;
flex-flow: row nowrap;
align-items: center;
gap: 2rem;
display: none;
a {
font-size: 1.125rem;
line-height: 1.75rem;
text-underline-offset: 4px;
text-decoration: underline solid transparent;
transition: text-decoration 0.5s ease;
&:hover {
text-decoration: underline solid var(--color-gray-400);
}
}
}
.header:hover {
.logo {
animation: wiggle 0.2s 1;
}
#eyes_2 {
visibility: visible;
}
#eyes_1 {
visibility: hidden;
}
nav {
display: flex;
pointer-events: all;
display: flex;
animation: fadeIn 0.5s ease;
}
.logo-button {
border-color: var(--color-gray-200);
box-shadow: 0px 100px 28px 0px rgba(0, 0, 0, 0),
0px 64px 26px 0px rgba(0, 0, 0, 0.01),
0px 36px 22px 0px rgba(0, 0, 0, 0.03),
0px 16px 16px 0px rgba(0, 0, 0, 0.04), 0px 4px 9px 0px rgba(0, 0, 0, 0.05);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes wiggle {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
50% {
transform: translateX(5px);
}
75% {
transform: translateX(-5px);
}
100% {
transform: translateX(0);
}
}
.button {
border-radius: var(--borderRadius-md);
border: 1px solid transparent;
padding: 0 1.5rem;
height: var(--input-height);
font-size: 1rem;
font-weight: 600;
font-family: inherit;
background-color: var(--color-gray-950);
color: white;
cursor: pointer;
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: center;
}
.button > svg {
width: 20px;
height: 20px;
}
.button:hover:not(:disabled) {
background-color: var(--color-gray-600);
}
.button:disabled {
cursor: not-allowed;
background-color: var(--color-gray-300);
color: var(--color-gray-100);
}
.button-ghost {
border: 1px solid var(--color-gray-300);
background-color: white;
color: var(--color-gray-950);
}
.button-ghost:hover:not(:disabled) {
background-color: white;
border-color: var(--color-gray-600);
}
.button-icon {
padding: 1rem;
}
.alert {
border: 1px solid black;
border-radius: var(--borderRadius-md);
padding: 1rem;
display: flex;
flex-flow: column wrap;
gap: 2;
font-size: 0.875rem;
span {
font-size: 1rem;
font-weight: bold;
}
}
.alert-danger {
border-color: red;
color: red;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
.controls {
}
.card {
padding: 12px;
border-radius: var(--borderRadius-lg);
background: #fff;
box-shadow: 0px 360px 101px 0px rgba(0, 0, 0, 0),
0px 231px 92px 0px rgba(0, 0, 0, 0), 0px 130px 78px 0px rgba(0, 0, 0, 0.02),
0px 58px 58px 0px rgba(0, 0, 0, 0.03), 0px 14px 32px 0px rgba(0, 0, 0, 0.03);
background-image: linear-gradient(90deg, white, white),
linear-gradient(0deg, var(--color-gray-300), var(--color-gray-200));
background-clip: padding-box, border-box;
background-origin: border-box;
border: 1px solid transparent;
}
.card-appear {
animation: appear 0.5s ease-out forwards;
}
.card-inner {
padding: 20px;
max-width: 420px;
display: flex;
gap: 24px;
flex-direction: column;
justify-content: center;
place-items: center;
}
.card-header {
font-size: 1.5rem;
font-weight: 600;
margin: 0px;
}
.card-text {
font-size: 1.125rem;
color: var(--color-gray-500);
text-wrap: pretty;
margin: 0px;
}
@keyframes appear {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
select {
appearance: none;
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQgNkw4IDEwTDEyIDYiIHN0cm9rZT0iIzY0NzQ4QiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=");
background-repeat: no-repeat;
background-position: right 1rem top 50%;
background-size: 16px auto;
border-radius: var(--borderRadius-md);
border: 1px solid var(--color-gray-300);
padding: 0 1rem;
height: var(--input-height);
font-size: var(--font-size-sm);
white-space: nowrap;
text-overflow: ellipsis;
padding-right: 3rem;
}
dialog {
max-width: 420px;
padding: 2rem;
border-radius: var(--borderRadius-lg);
background: #fff;
box-shadow: 0px 360px 101px 0px rgba(0, 0, 0, 0),
0px 231px 92px 0px rgba(0, 0, 0, 0), 0px 130px 78px 0px rgba(0, 0, 0, 0.02),
0px 58px 58px 0px rgba(0, 0, 0, 0.03), 0px 14px 32px 0px rgba(0, 0, 0, 0.03);
background-image: linear-gradient(90deg, white, white),
linear-gradient(0deg, var(--color-gray-300), var(--color-gray-200));
background-clip: padding-box, border-box;
background-origin: border-box;
border: 1px solid transparent;
animation: appear 0.5s ease-out forwards;
> h2 {
font-size: var(--font-size-xl);
margin: 0;
margin-bottom: 2rem;
}
> button {
margin: 2rem auto 0 auto;
}
}
dialog::backdrop {
background-color: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(5px);
}

View File

@@ -0,0 +1,18 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import Header from "./components/header.tsx";
import { DailyProvider } from "@daily-co/daily-react";
import "./global.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Header />
<main>
<DailyProvider>
<App />
</DailyProvider>
</main>
</React.StrictMode>
);

View File

1
examples/web-ui/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />