Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jptaylor committed Jun 6, 2024
0 parents commit 128eaf5
Show file tree
Hide file tree
Showing 41 changed files with 5,428 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
**/.DS_Store
node_modules
dist
dist-ssr
*.lock
*.local
*.*.local
18 changes: 18 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Pipecat Runner + Bot + WebUI Template

Some docs regarding how all this fits together: [here](/docs/)

## Getting setup

Create a virtualenv

`pip install requirements.txt`

`mv env.example .env` (enter keys)

`python bot_runner.py --host localhost`

## Adding the test UI

Set `SERVE_STATIC` to `True` in `bot_runner.py` (L37)

Build the UI:

```
cd web-ui/
yarn
yarn run build
cd ..
python bot_runner.py --host localhost
```

Navigate to https://localhost:7860

## Serving UI externally from Bot Runner

Set `SERVE_STATIC` to `False` in `bot_runner.py` (L37)

Run the UI:

```
cd web-ui/
yarn
mv env.example .env.development.local
```

Set `VITE_SERVER_URL` in `.env` to the URL of the API

```
yarn run dev
```

Navigate to https://localhost:5173 (ensure the API is running on `VITE_SERVER_URL` including the specified port)
5 changes: 5 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
VITE_APP_TITLE=Simple Chatbot
VITE_SERVER_URL=... #optional: if serving frontend externally from backend
VITE_MANUAL_ROOM_ENTRY=... #optional: require user to specify a room URL to join
VITE_OPEN_MIC= # Can the user speak freely, or do they need to wait their turn?
VITE_USER_VIDEO= #Does the app require the user's webcam?
17 changes: 17 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:[email protected]&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">

<title>Pipecat - %VITE_APP_TITLE%</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "pipecatdemo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@daily-co/daily-js": "^0.64.0",
"@daily-co/daily-react": "^0.19.0",
"class-variance-authority": "^0.7.0",
"lucide-react": "^0.378.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"recoil": "^0.7.7"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"i": "^0.3.7",
"npm": "^10.8.0",
"postcss-custom-media": "^10.0.6",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vite-plugin-webfont-dl": "^3.9.4"
}
}
Binary file added public/favicon.ico
Binary file not shown.
187 changes: 187 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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";
import { RoomInput } from "./components/RoomInput";
import { SettingList } from "./components/SettingList/SettingList";

type State =
| "idle"
| "configuring"
| "requesting_agent"
| "connecting"
| "connected"
| "started"
| "finished"
| "error";

// Server URL (ensure trailing slash)
let serverUrl = import.meta.env.VITE_SERVER_URL || import.meta.env.BASE_URL;
if (!serverUrl.endsWith("/")) serverUrl += "/";

// Query string for room URL
const roomQs = new URLSearchParams(window.location.search).get("room_url");
const checkRoomUrl = (url: string | null): boolean =>
!!(url && /^(https?:\/\/[^.]+\.daily\.co\/[^/]+)$/.test(url));

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 [config, setConfig] = useState<{ open_mic?: boolean }>({});
const [roomUrl, setRoomUrl] = useState<string | null>(roomQs || null);
const [roomError, setRoomError] = useState<boolean>(
(roomQs && checkRoomUrl(roomQs)) || false
);

function handleRoomUrl() {
if (checkRoomUrl(roomUrl)) {
setRoomError(false);
setState("configuring");
} else {
setRoomError(true);
}
}

async function start() {
if (!daily) return;

setState("requesting_agent");

// Request a bot to join your session
let data;

try {
const res = await fetch(`${serverUrl}start_bot`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ room_url: roomUrl }),
});

data = await res.json();
setConfig(data.config || {});

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");

// Join the daily session, passing through the url and token
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()} openMic={config?.open_mic} />;
}

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">
<div className="card-header">
<h1>Configure your devices</h1>
<p> Please configure your microphone and speakers below</p>
</div>
<DeviceSelect />
<div className="card-footer">
<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>
</div>
);
}

return (
<div className="card card-appear">
<div className="card-inner">
<div className="card-header">
<h1>Pipecat {import.meta.env.VITE_APP_TITLE}</h1>
<p>Review and configure your bot experience</p>
</div>

{import.meta.env.DEV && !import.meta.env.VITE_SERVER_URL && (
<Alert title="Missing server URL environment" intent="danger">
<p>
You have not set a server URL for local development. Please set{" "}
<samp>VITE_SERVER_URL</samp> in{" "}
<samp>.env.development.local</samp>.
</p>
<p>
Without this, the client will attempt to start the bot by calling
localhost on the same port.
</p>
</Alert>
)}
<SettingList
serverUrl={serverUrl}
roomQueryString={roomQs}
roomQueryStringValid={checkRoomUrl(roomQs)}
/>

{import.meta.env.VITE_MANUAL_ROOM_ENTRY && !roomQs && (
<RoomInput onChange={(url) => setRoomUrl(url)} error={roomError} />
)}
<div className="card-footer">
<Button
key="next"
disabled={!!(roomQs && !roomError)}
onClick={() => handleRoomUrl()}
>
Next <ArrowRight />
</Button>
</div>
</div>
</div>
);
}
7 changes: 7 additions & 0 deletions src/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions src/components/AudioIndicator/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 128eaf5

Please sign in to comment.