Nagesh Muralidhar commited on
Commit
d7a8925
·
1 Parent(s): a288236

midterm-submission

Browse files
.gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Node
24
+ node_modules/
25
+ npm-debug.log
26
+ yarn-debug.log
27
+ yarn-error.log
28
+
29
+ # Environment variables
30
+ .env
31
+ .env.local
32
+ .env.*.local
33
+
34
+ # IDE
35
+ .vscode/
36
+ .idea/
37
+ *.swp
38
+ *.swo
39
+
40
+ # OS
41
+ .DS_Store
42
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build frontend
2
+ FROM node:18-alpine as frontend-build
3
+
4
+ WORKDIR /frontend
5
+ COPY podcraft/package*.json ./
6
+ RUN npm install
7
+ COPY podcraft/ .
8
+ RUN npm run build
9
+
10
+ # Build backend
11
+ FROM python:3.11-slim
12
+
13
+ WORKDIR /app
14
+
15
+ # Install system dependencies
16
+ RUN apt-get update && apt-get install -y \
17
+ build-essential \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # Copy and install Python dependencies
21
+ COPY server/requirements.txt .
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+
24
+ # Copy backend code
25
+ COPY server/ .
26
+
27
+ # Create necessary directories
28
+ RUN mkdir -p audio_storage transcripts
29
+
30
+ # Copy frontend build to a static directory
31
+ COPY --from=frontend-build /frontend/dist /app/static
32
+
33
+ # Expose port
34
+ EXPOSE 7860
35
+
36
+ # Start FastAPI application
37
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
Screenshot 2025-02-23 210022.png ADDED
docker-compose.yml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ frontend:
5
+ build:
6
+ context: ./podcraft
7
+ dockerfile: Dockerfile
8
+ ports:
9
+ - "3000:80"
10
+ depends_on:
11
+ - backend
12
+ environment:
13
+ - VITE_API_URL=http://localhost:8000
14
+
15
+ backend:
16
+ build:
17
+ context: ./server
18
+ dockerfile: Dockerfile
19
+ ports:
20
+ - "8000:8000"
21
+ volumes:
22
+ - ./server/audio_storage:/app/audio_storage
23
+ - ./server/transcripts:/app/transcripts
24
+ environment:
25
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
26
+ - ELEVEN_API_KEY=${ELEVEN_API_KEY}
27
+ - TAVILY_API_KEY=${TAVILY_API_KEY}
28
+ healthcheck:
29
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
30
+ interval: 30s
31
+ timeout: 10s
32
+ retries: 3
podcraft/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
podcraft/Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build stage
2
+ FROM node:18-alpine as build
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy package files
7
+ COPY package*.json ./
8
+
9
+ # Install dependencies
10
+ RUN npm install
11
+
12
+ # Copy source code
13
+ COPY . .
14
+
15
+ # Build the application
16
+ RUN npm run build
17
+
18
+ # Production stage
19
+ FROM nginx:alpine
20
+
21
+ # Remove default nginx static assets
22
+ RUN rm -rf /usr/share/nginx/html/*
23
+
24
+ # Copy built files from build stage
25
+ COPY --from=build /app/dist /usr/share/nginx/html
26
+
27
+ # Copy nginx configuration
28
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
29
+
30
+ # Expose port
31
+ EXPOSE 80
32
+
33
+ # Start Nginx
34
+ CMD ["nginx", "-g", "daemon off;"]
podcraft/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
podcraft/eslint.config.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import react from 'eslint-plugin-react'
4
+ import reactHooks from 'eslint-plugin-react-hooks'
5
+ import reactRefresh from 'eslint-plugin-react-refresh'
6
+
7
+ export default [
8
+ { ignores: ['dist'] },
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ languageOptions: {
12
+ ecmaVersion: 2020,
13
+ globals: globals.browser,
14
+ parserOptions: {
15
+ ecmaVersion: 'latest',
16
+ ecmaFeatures: { jsx: true },
17
+ sourceType: 'module',
18
+ },
19
+ },
20
+ settings: { react: { version: '18.3' } },
21
+ plugins: {
22
+ react,
23
+ 'react-hooks': reactHooks,
24
+ 'react-refresh': reactRefresh,
25
+ },
26
+ rules: {
27
+ ...js.configs.recommended.rules,
28
+ ...react.configs.recommended.rules,
29
+ ...react.configs['jsx-runtime'].rules,
30
+ ...reactHooks.configs.recommended.rules,
31
+ 'react/jsx-no-target-blank': 'off',
32
+ 'react-refresh/only-export-components': [
33
+ 'warn',
34
+ { allowConstantExport: true },
35
+ ],
36
+ },
37
+ },
38
+ ]
podcraft/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + React</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
podcraft/nginx.conf ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 80;
3
+ server_name localhost;
4
+
5
+ location / {
6
+ root /usr/share/nginx/html;
7
+ index index.html;
8
+ try_files $uri $uri/ /index.html;
9
+ }
10
+
11
+ location /api/ {
12
+ proxy_pass http://backend:8000/;
13
+ proxy_http_version 1.1;
14
+ proxy_set_header Upgrade $http_upgrade;
15
+ proxy_set_header Connection 'upgrade';
16
+ proxy_set_header Host $host;
17
+ proxy_cache_bypass $http_upgrade;
18
+
19
+ # CORS headers
20
+ add_header 'Access-Control-Allow-Origin' '*' always;
21
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
22
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
23
+ add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
24
+
25
+ if ($request_method = 'OPTIONS') {
26
+ add_header 'Access-Control-Allow-Origin' '*';
27
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
28
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
29
+ add_header 'Access-Control-Max-Age' 1728000;
30
+ add_header 'Content-Type' 'text/plain; charset=utf-8';
31
+ add_header 'Content-Length' 0;
32
+ return 204;
33
+ }
34
+ }
35
+
36
+ # Audio files proxy
37
+ location /audio-files/ {
38
+ proxy_pass http://backend:8000/audio-files/;
39
+ proxy_http_version 1.1;
40
+ proxy_set_header Upgrade $http_upgrade;
41
+ proxy_set_header Connection 'upgrade';
42
+ proxy_set_header Host $host;
43
+ proxy_cache_bypass $http_upgrade;
44
+ }
45
+ }
podcraft/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
podcraft/package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "podcraft",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --port 3000",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0",
15
+ "react-router-dom": "^7.2.0"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^9.19.0",
19
+ "@types/react": "^19.0.8",
20
+ "@types/react-dom": "^19.0.3",
21
+ "@vitejs/plugin-react": "^4.3.4",
22
+ "eslint": "^9.19.0",
23
+ "eslint-plugin-react": "^7.37.4",
24
+ "eslint-plugin-react-hooks": "^5.0.0",
25
+ "eslint-plugin-react-refresh": "^0.4.18",
26
+ "globals": "^15.14.0",
27
+ "vite": "^6.1.0"
28
+ }
29
+ }
podcraft/public/vite.svg ADDED
podcraft/src/App.css ADDED
@@ -0,0 +1,1084 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Theme Variables */
2
+ :root {
3
+ --bg-primary: #013135; /* Dark teal for sidebar/main elements */
4
+ --bg-secondary: #AFDDE5; /* Light teal for background */
5
+ --text-primary: #ffffff; /* White text for contrast */
6
+ --text-secondary: #AFDDE5; /* Light teal for secondary text */
7
+ --accent-primary: #12A3B0; /* Bright teal for interactive elements */
8
+ --accent-secondary: #964834; /* Rust color for secondary accents */
9
+ --border-color: #014951; /* Medium teal for borders */
10
+ --card-bg: #014951; /* Medium teal for cards */
11
+ --card-shadow: 0 4px 6px rgba(1, 49, 53, 0.2); /* Teal-tinted shadow */
12
+ --hover-bg: #014951; /* Medium teal for hover states */
13
+ --chat-bg: #AFDDE5; /* Light teal for chat background */
14
+ --card-bg-rgb: 1, 73, 81; /* RGB values for #014951 */
15
+ }
16
+
17
+ [data-theme='dark'] {
18
+ --bg-primary: #1a1a1a;
19
+ --bg-secondary: #2d2d2d;
20
+ --text-primary: #ffffff;
21
+ --text-secondary: #a3a3a3;
22
+ --accent-primary: #818cf8;
23
+ --accent-secondary: #6366f1;
24
+ --border-color: #404040;
25
+ --card-bg: #1a1a1a;
26
+ --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
27
+ --hover-bg: #404040;
28
+ --chat-bg: #141414;
29
+ --card-bg-rgb: 26, 26, 26; /* RGB values for #1a1a1a */
30
+ }
31
+
32
+ /* Reset default margins and make app take full viewport */
33
+ * {
34
+ margin: 0;
35
+ padding: 0;
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ body, html {
40
+ height: 100vh;
41
+ width: 100%;
42
+ overflow-x: hidden;
43
+ background-color: var(--bg-secondary);
44
+ color: var(--text-primary);
45
+ transition: background-color 0.3s, color 0.3s;
46
+ }
47
+
48
+ #root {
49
+ min-height: 100vh;
50
+ width: 100%;
51
+ display: flex;
52
+ }
53
+
54
+ .app {
55
+ display: flex;
56
+ flex: 1;
57
+ position: relative;
58
+ }
59
+
60
+ .wave-canvas {
61
+ position: fixed;
62
+ top: 0;
63
+ left: 0;
64
+ width: 100%;
65
+ height: 100%;
66
+ z-index: -1;
67
+ background-color: var(--bg-primary);
68
+ }
69
+
70
+ /* Left Navigation Styles */
71
+ .leftnav {
72
+ width: 250px;
73
+ background-color: var(--bg-primary);
74
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
75
+ padding: 2rem 0;
76
+ height: 100vh;
77
+ position: fixed;
78
+ display: flex;
79
+ flex-direction: column;
80
+ }
81
+
82
+ .nav-brand {
83
+ font-size: 1.75rem;
84
+ font-weight: bold;
85
+ padding: 0 2rem 2rem 2rem;
86
+ border-bottom: 1px solid var(--border-color);
87
+ background: linear-gradient(
88
+ 135deg,
89
+ #12A3B0,
90
+ #AFDDE5,
91
+ #014951,
92
+ #013135,
93
+ #964834
94
+ );
95
+ -webkit-background-clip: text;
96
+ background-clip: text;
97
+ -webkit-text-fill-color: transparent;
98
+ animation: gradient-shift 8s ease infinite;
99
+ background-size: 300% auto;
100
+ }
101
+
102
+ @keyframes gradient-shift {
103
+ 0% {
104
+ background-position: 0% 50%;
105
+ }
106
+ 50% {
107
+ background-position: 100% 50%;
108
+ }
109
+ 100% {
110
+ background-position: 0% 50%;
111
+ }
112
+ }
113
+
114
+ .nav-links {
115
+ display: flex;
116
+ flex-direction: column;
117
+ padding: 2rem 0;
118
+ gap: 0.5rem;
119
+ flex: 1;
120
+ }
121
+
122
+ .nav-link {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 1rem;
126
+ padding: 1rem 2rem;
127
+ text-decoration: none;
128
+ color: var(--text-secondary);
129
+ font-weight: 500;
130
+ transition: all 0.2s;
131
+ }
132
+
133
+ .nav-link:hover {
134
+ background-color: var(--hover-bg);
135
+ color: var(--accent-primary);
136
+ }
137
+
138
+ .nav-link.active {
139
+ background-color: var(--hover-bg);
140
+ color: var(--accent-primary);
141
+ border-right: 3px solid var(--accent-primary);
142
+ }
143
+
144
+ .nav-icon {
145
+ font-size: 1.25rem;
146
+ }
147
+
148
+ /* Theme Toggle Styles */
149
+ .theme-toggle {
150
+ padding: 2rem;
151
+ border-top: 1px solid var(--border-color);
152
+ }
153
+
154
+ .theme-button {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 1rem;
158
+ width: 100%;
159
+ padding: 1rem;
160
+ background-color: var(--bg-primary);
161
+ color: var(--text-secondary);
162
+ border: 1px solid var(--border-color);
163
+ border-radius: 0.5rem;
164
+ cursor: pointer;
165
+ transition: all 0.2s;
166
+ font-size: 1rem;
167
+ }
168
+
169
+ .theme-button:hover {
170
+ background-color: var(--hover-bg);
171
+ color: var(--accent-primary);
172
+ }
173
+
174
+ /* Main Content Styles */
175
+ .main-content {
176
+ flex: 1;
177
+ margin-left: 250px;
178
+ display: flex;
179
+ flex-direction: column;
180
+ }
181
+
182
+ /* Chat Interface Styles */
183
+ .chat-container {
184
+ flex: 1;
185
+ display: flex;
186
+ flex-direction: column;
187
+ background-color: var(--bg-secondary);
188
+ height: 90vh;
189
+ max-width: 800px;
190
+ margin: 2rem auto;
191
+ border-radius: 1rem;
192
+ background: var(--bg-primary);
193
+ overflow: hidden;
194
+ position: relative;
195
+ width: 95%;
196
+ }
197
+
198
+ .chat-container::before {
199
+ content: '';
200
+ position: absolute;
201
+ inset: 0;
202
+ border-radius: 1rem;
203
+ padding: 2px;
204
+ background: linear-gradient(
205
+ 135deg,
206
+ #12A3B0,
207
+ #014951,
208
+ #964834
209
+ );
210
+ -webkit-mask: linear-gradient(#fff 0 0) content-box,
211
+ linear-gradient(#fff 0 0);
212
+ -webkit-mask-composite: xor;
213
+ mask-composite: exclude;
214
+ opacity: 0.8;
215
+ pointer-events: none;
216
+ }
217
+
218
+ .chat-messages {
219
+ flex: 1;
220
+ display: flex;
221
+ flex-direction: column;
222
+ padding: 1.5rem;
223
+ gap: 0.75rem;
224
+ overflow-y: auto;
225
+ background-color: var(--chat-bg, var(--bg-secondary));
226
+ max-height: calc(90vh - 80px);
227
+ scrollbar-width: thin;
228
+ scrollbar-color: var(--accent-primary) var(--bg-primary);
229
+ padding-bottom: 2rem;
230
+ width: 100%;
231
+ }
232
+
233
+ /* Webkit scrollbar styles */
234
+ .chat-messages::-webkit-scrollbar {
235
+ width: 8px;
236
+ }
237
+
238
+ .chat-messages::-webkit-scrollbar-track {
239
+ background: var(--bg-primary);
240
+ border-radius: 4px;
241
+ }
242
+
243
+ .chat-messages::-webkit-scrollbar-thumb {
244
+ background: var(--accent-primary);
245
+ border-radius: 4px;
246
+ }
247
+
248
+ .chat-messages::-webkit-scrollbar-thumb:hover {
249
+ background: var(--accent-secondary);
250
+ }
251
+
252
+ .message {
253
+ display: flex;
254
+ padding: 0.75rem 1rem;
255
+ border-radius: 0.75rem;
256
+ max-width: 95%;
257
+ word-wrap: break-word;
258
+ font-size: 0.95rem;
259
+ gap: 1rem;
260
+ align-items: flex-start;
261
+ overflow-wrap: break-word;
262
+ width: fit-content;
263
+ }
264
+
265
+ .message-content {
266
+ display: flex;
267
+ flex-direction: row;
268
+ align-items: flex-start;
269
+ gap: 0.75rem;
270
+ width: 100%;
271
+ }
272
+
273
+ .message-text-content {
274
+ display: flex;
275
+ flex-direction: column;
276
+ gap: 0.25rem;
277
+ flex: 1;
278
+ }
279
+
280
+ .agent-icon {
281
+ font-size: 1.25rem;
282
+ min-width: 1.5rem;
283
+ text-align: center;
284
+ }
285
+
286
+ .agent-name {
287
+ font-size: 0.8rem;
288
+ text-transform: capitalize;
289
+ color: var(--text-secondary);
290
+ font-weight: 500;
291
+ margin-bottom: 0.25rem;
292
+ }
293
+
294
+ .message-text {
295
+ line-height: 1.5;
296
+ overflow-wrap: break-word;
297
+ word-break: break-word;
298
+ max-width: 100%;
299
+ }
300
+
301
+ /* Agent-specific styles */
302
+ .extractor-message {
303
+ background-color: var(--card-bg);
304
+ border: 1px solid var(--border-color);
305
+ color: var(--text-primary);
306
+ }
307
+
308
+ .skeptic-message {
309
+ background-color: var(--card-bg);
310
+ border: 1px solid var(--border-color);
311
+ color: var(--text-primary);
312
+ }
313
+
314
+ .believer-message {
315
+ background-color: var(--card-bg);
316
+ border: 1px solid var(--border-color);
317
+ color: var(--text-primary);
318
+ }
319
+
320
+ .supervisor-message {
321
+ background-color: var(--accent-primary);
322
+ color: white;
323
+ opacity: 0.9;
324
+ }
325
+
326
+ .system-message {
327
+ max-width: 100%;
328
+ width: 100%;
329
+ background-color: var(--bg-primary);
330
+ border: 1px dashed var(--border-color);
331
+ color: var(--text-secondary);
332
+ font-style: italic;
333
+ margin: 0.5rem 0;
334
+ }
335
+
336
+ .bot-message {
337
+ align-self: flex-start;
338
+ background-color: var(--card-bg);
339
+ color: var(--text-primary);
340
+ border-bottom-left-radius: 0.25rem;
341
+ box-shadow: var(--card-shadow);
342
+ border: 1px solid var(--border-color);
343
+ }
344
+
345
+ .user-message {
346
+ align-self: flex-end;
347
+ background-color: var(--accent-primary);
348
+ color: white;
349
+ border-bottom-right-radius: 0.25rem;
350
+ opacity: 0.9;
351
+ }
352
+
353
+ .message .agent-icon {
354
+ display: inline-block;
355
+ margin-right: 0.5rem;
356
+ font-size: 1.25rem;
357
+ vertical-align: middle;
358
+ }
359
+
360
+ .chat-input-form {
361
+ display: flex;
362
+ padding: 1rem;
363
+ gap: 0.75rem;
364
+ background-color: var(--bg-primary);
365
+ border-top: 1px solid var(--border-color);
366
+ min-height: 80px; /* Fixed height for input form */
367
+ }
368
+
369
+ .chat-input {
370
+ flex: 1;
371
+ padding: 0.75rem 1rem;
372
+ border: 1px solid var(--border-color);
373
+ border-radius: 0.5rem;
374
+ font-size: 0.95rem;
375
+ outline: none;
376
+ transition: all 0.2s;
377
+ background-color: var(--bg-primary);
378
+ color: var(--text-primary);
379
+ }
380
+
381
+ .chat-input:focus {
382
+ border-color: var(--accent-primary);
383
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
384
+ }
385
+
386
+ .chat-send-button {
387
+ padding: 0 1.5rem;
388
+ background-color: var(--accent-primary);
389
+ color: white;
390
+ border: none;
391
+ border-radius: 0.5rem;
392
+ font-weight: 500;
393
+ cursor: pointer;
394
+ transition: all 0.2s;
395
+ font-size: 0.95rem;
396
+ }
397
+
398
+ .chat-send-button:hover {
399
+ background-color: var(--accent-secondary);
400
+ transform: translateY(-1px);
401
+ }
402
+
403
+ /* Podcasts Page Styles */
404
+ .podcasts-container {
405
+ flex: 1;
406
+ display: flex;
407
+ flex-direction: column;
408
+ align-items: center;
409
+ padding: 2rem;
410
+ position: relative;
411
+ z-index: 1;
412
+ background: transparent;
413
+ }
414
+
415
+ .podcasts-header {
416
+ text-align: center;
417
+ margin: 2rem 0 4rem 0;
418
+ width: 100%;
419
+ position: relative;
420
+ z-index: 2;
421
+ }
422
+
423
+ .podcasts-grid {
424
+ display: flex;
425
+ flex-wrap: wrap;
426
+ gap: 2rem;
427
+ max-width: 1400px;
428
+ width: 100%;
429
+ padding: 0 2rem;
430
+ justify-content: center;
431
+ position: relative;
432
+ z-index: 2;
433
+ }
434
+
435
+ .podcast-card {
436
+ flex: 1;
437
+ min-width: 320px;
438
+ max-width: 400px;
439
+ background-color: var(--card-bg);
440
+ border-radius: 1.5rem;
441
+ display: flex;
442
+ flex-direction: column;
443
+ position: relative;
444
+ transition: all 0.3s ease;
445
+ overflow: hidden;
446
+ backdrop-filter: blur(10px);
447
+ background: rgba(var(--card-bg-rgb), 0.8);
448
+ border: 2px solid transparent;
449
+ background-clip: padding-box;
450
+ }
451
+
452
+ .podcast-card::before {
453
+ content: '';
454
+ position: absolute;
455
+ inset: -2px;
456
+ border-radius: 1.5rem;
457
+ padding: 2px;
458
+ background: linear-gradient(
459
+ 135deg,
460
+ rgb(147, 51, 234) 0%, /* Start with Purple */
461
+ rgb(147, 51, 234) 20%, /* Hold Purple */
462
+ rgb(103, 90, 240) 40%, /* Mix Purple-Blue */
463
+ rgb(59, 130, 246) 60%, /* Mix Purple-Blue */
464
+ rgb(59, 130, 246) 80%, /* Hold Blue */
465
+ rgb(59, 130, 246) 100% /* End with Blue */
466
+ );
467
+ -webkit-mask: linear-gradient(#fff 0 0) content-box,
468
+ linear-gradient(#fff 0 0);
469
+ -webkit-mask-composite: xor;
470
+ mask-composite: exclude;
471
+ opacity: 1;
472
+ transition: opacity 0.3s ease;
473
+ }
474
+
475
+ .podcast-card:hover {
476
+ transform: translateY(-8px);
477
+ }
478
+
479
+ .podcast-card:hover::before {
480
+ opacity: 1;
481
+ }
482
+
483
+ .podcast-content {
484
+ flex: 1;
485
+ display: flex;
486
+ flex-direction: column;
487
+ padding: 2rem;
488
+ gap: 1.5rem;
489
+ background: linear-gradient(
490
+ to bottom,
491
+ var(--card-bg),
492
+ var(--bg-primary)
493
+ );
494
+ }
495
+
496
+ .podcast-title {
497
+ display: flex;
498
+ align-items: center;
499
+ gap: 0.75rem;
500
+ color: var(--text-primary);
501
+ font-size: 1.5rem;
502
+ font-weight: 600;
503
+ line-height: 1.4;
504
+ }
505
+
506
+ .podcast-title::before {
507
+ content: '🎧';
508
+ font-size: 1.75rem;
509
+ }
510
+
511
+ .description {
512
+ color: var(--text-secondary);
513
+ line-height: 1.6;
514
+ font-size: 0.95rem;
515
+ display: -webkit-box;
516
+ -webkit-line-clamp: 3;
517
+ -webkit-box-orient: vertical;
518
+ overflow: hidden;
519
+ }
520
+
521
+ .listen-button {
522
+ padding: 0.75rem 1.5rem;
523
+ background-color: var(--accent-primary);
524
+ color: white;
525
+ border: none;
526
+ border-radius: 0.75rem;
527
+ font-weight: 500;
528
+ cursor: pointer;
529
+ transition: all 0.2s;
530
+ font-size: 0.95rem;
531
+ display: flex;
532
+ align-items: center;
533
+ gap: 0.5rem;
534
+ align-self: flex-start;
535
+ }
536
+
537
+ .listen-button::after {
538
+ content: '▶';
539
+ font-size: 0.8rem;
540
+ }
541
+
542
+ .listen-button:hover {
543
+ background-color: var(--accent-secondary);
544
+ transform: translateY(-2px);
545
+ }
546
+
547
+ /* Podcast Form Page Styles */
548
+ .podcast-form-container {
549
+ display: flex;
550
+ padding: 2rem;
551
+ height: calc(100vh - 4rem);
552
+ max-width: 1200px;
553
+ margin: 0 auto;
554
+ width: 100%;
555
+ }
556
+
557
+ .chat-column {
558
+ flex: 1;
559
+ display: flex;
560
+ flex-direction: column;
561
+ width: 100%;
562
+ }
563
+
564
+ .podcast-chat-container {
565
+ flex: 1;
566
+ display: flex;
567
+ flex-direction: column;
568
+ background: var(--bg-primary);
569
+ border-radius: 1.5rem;
570
+ overflow: hidden;
571
+ position: relative;
572
+ }
573
+
574
+ .podcast-chat-container::before {
575
+ content: '';
576
+ position: absolute;
577
+ inset: 0;
578
+ border-radius: 1.5rem;
579
+ padding: 2px;
580
+ background: linear-gradient(
581
+ 135deg,
582
+ #12A3B0,
583
+ #014951,
584
+ #964834
585
+ );
586
+ -webkit-mask: linear-gradient(#fff 0 0) content-box,
587
+ linear-gradient(#fff 0 0);
588
+ -webkit-mask-composite: xor;
589
+ mask-composite: exclude;
590
+ opacity: 0.8;
591
+ pointer-events: none;
592
+ }
593
+
594
+ .podcast-chat-messages {
595
+ flex: 1;
596
+ display: flex;
597
+ flex-direction: column;
598
+ padding: 1.5rem;
599
+ gap: 1rem;
600
+ overflow-y: auto;
601
+ background: var(--chat-bg, var(--bg-secondary));
602
+ }
603
+
604
+ .podcast-chat-input-form {
605
+ display: flex;
606
+ padding: 1rem;
607
+ gap: 0.75rem;
608
+ background: var(--bg-primary);
609
+ border-top: 1px solid var(--border-color);
610
+ }
611
+
612
+ .podcast-chat-input {
613
+ flex: 1;
614
+ padding: 0.75rem 1rem;
615
+ border: 1px solid var(--border-color);
616
+ border-radius: 0.75rem;
617
+ background: var(--bg-primary);
618
+ color: var(--text-primary);
619
+ font-size: 0.95rem;
620
+ transition: all 0.2s;
621
+ }
622
+
623
+ .podcast-chat-input:focus {
624
+ outline: none;
625
+ border-color: var(--accent-primary);
626
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
627
+ }
628
+
629
+ .podcast-chat-send-button {
630
+ padding: 0 1.5rem;
631
+ background: var(--accent-primary);
632
+ color: white;
633
+ border: none;
634
+ border-radius: 0.75rem;
635
+ font-weight: 500;
636
+ cursor: pointer;
637
+ transition: all 0.2s;
638
+ display: flex;
639
+ align-items: center;
640
+ gap: 0.5rem;
641
+ }
642
+
643
+ .podcast-chat-send-button:hover {
644
+ background: var(--accent-secondary);
645
+ transform: translateY(-2px);
646
+ }
647
+
648
+ .podcast-chat-send-button::after {
649
+ content: '▶';
650
+ font-size: 0.8rem;
651
+ }
652
+
653
+ /* Responsive Design for Form Page */
654
+ @media (max-width: 1024px) {
655
+ .podcast-form-container {
656
+ flex-direction: column;
657
+ height: auto;
658
+ }
659
+
660
+ .form-column,
661
+ .chat-column {
662
+ width: 100%;
663
+ }
664
+
665
+ .podcast-chat-container {
666
+ height: 500px;
667
+ }
668
+ }
669
+
670
+ /* Responsive Design */
671
+ @media (max-width: 768px) {
672
+ .leftnav {
673
+ width: 200px;
674
+ }
675
+
676
+ .main-content {
677
+ margin-left: 200px;
678
+ }
679
+
680
+ .nav-brand {
681
+ font-size: 1.5rem;
682
+ padding: 0 1.5rem 1.5rem 1.5rem;
683
+ }
684
+
685
+ .nav-link {
686
+ padding: 0.75rem 1.5rem;
687
+ }
688
+
689
+ .feature-card,
690
+ .podcast-card {
691
+ min-width: 250px;
692
+ }
693
+ }
694
+
695
+ .chat-header {
696
+ padding: 1rem;
697
+ background: var(--bg-primary);
698
+ border-bottom: 1px solid var(--border-color);
699
+ display: flex;
700
+ justify-content: center;
701
+ }
702
+
703
+ .agent-selector {
704
+ padding: 0.75rem 2.5rem 0.75rem 1rem;
705
+ border: 1px solid var(--border-color);
706
+ border-radius: 0.75rem;
707
+ background: var(--bg-primary);
708
+ color: var(--text-primary);
709
+ font-size: 0.95rem;
710
+ cursor: pointer;
711
+ transition: all 0.2s;
712
+ appearance: none;
713
+ -webkit-appearance: none;
714
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
715
+ background-repeat: no-repeat;
716
+ background-position: right 1rem center;
717
+ background-size: 1em;
718
+ }
719
+
720
+ .agent-selector:focus {
721
+ outline: none;
722
+ border-color: var(--accent-primary);
723
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
724
+ }
725
+
726
+ .agent-selector:hover {
727
+ border-color: var(--accent-primary);
728
+ }
729
+
730
+ .agent-selector option {
731
+ padding: 0.5rem;
732
+ background: var(--bg-primary);
733
+ color: var(--text-primary);
734
+ }
735
+
736
+ .loading-dots {
737
+ display: inline-block;
738
+ position: relative;
739
+ color: var(--text-secondary);
740
+ }
741
+
742
+ .loading-dots::after {
743
+ content: '...';
744
+ position: absolute;
745
+ animation: loading-dots 1.5s infinite;
746
+ width: 1.5em;
747
+ text-align: left;
748
+ }
749
+
750
+ @keyframes loading-dots {
751
+ 0% { content: '.'; }
752
+ 33% { content: '..'; }
753
+ 66% { content: '...'; }
754
+ 100% { content: '.'; }
755
+ }
756
+
757
+ .audio-player {
758
+ margin-top: 1rem;
759
+ width: 100%;
760
+ padding: 0.5rem;
761
+ background: var(--bg-primary);
762
+ border-radius: 0.5rem;
763
+ border: 1px solid var(--border-color);
764
+ }
765
+
766
+ .audio-player audio {
767
+ width: 100%;
768
+ height: 40px;
769
+ outline: none;
770
+ }
771
+
772
+ /* Customize audio player controls */
773
+ .audio-player audio::-webkit-media-controls-panel {
774
+ background-color: var(--bg-primary);
775
+ }
776
+
777
+ .audio-player audio::-webkit-media-controls-current-time-display,
778
+ .audio-player audio::-webkit-media-controls-time-remaining-display {
779
+ color: var(--text-primary);
780
+ }
781
+
782
+ .audio-player audio::-webkit-media-controls-play-button,
783
+ .audio-player audio::-webkit-media-controls-timeline,
784
+ .audio-player audio::-webkit-media-controls-volume-slider {
785
+ filter: invert(1);
786
+ }
787
+
788
+ /* Chat Message Podcast Card Styles */
789
+ .chat-messages .podcast-card {
790
+ width: 100%;
791
+ max-width: 600px;
792
+ margin: 1rem auto;
793
+ background-color: var(--card-bg);
794
+ border-radius: 1rem;
795
+ overflow: hidden;
796
+ transition: all 0.3s ease;
797
+ border: 2px solid transparent;
798
+ background-clip: padding-box;
799
+ }
800
+
801
+ .chat-messages .podcast-card::before {
802
+ content: '';
803
+ position: absolute;
804
+ inset: -2px;
805
+ border-radius: 1rem;
806
+ padding: 2px;
807
+ background: linear-gradient(
808
+ 135deg,
809
+ rgb(147, 51, 234) 0%, /* Start with Purple */
810
+ rgb(147, 51, 234) 20%, /* Hold Purple */
811
+ rgb(103, 90, 240) 40%, /* Mix Purple-Blue */
812
+ rgb(59, 130, 246) 60%, /* Mix Purple-Blue */
813
+ rgb(59, 130, 246) 80%, /* Hold Blue */
814
+ rgb(59, 130, 246) 100% /* End with Blue */
815
+ );
816
+ -webkit-mask: linear-gradient(#fff 0 0) content-box,
817
+ linear-gradient(#fff 0 0);
818
+ -webkit-mask-composite: xor;
819
+ mask-composite: exclude;
820
+ opacity: 1;
821
+ transition: opacity 0.3s ease;
822
+ }
823
+
824
+ .chat-messages .podcast-card:hover::before {
825
+ opacity: 1;
826
+ }
827
+
828
+ .chat-messages .podcast-content {
829
+ padding: 1.5rem;
830
+ display: flex;
831
+ flex-direction: column;
832
+ gap: 1rem;
833
+ }
834
+
835
+ .chat-messages .podcast-title {
836
+ font-size: 1.25rem;
837
+ color: var(--text-primary);
838
+ margin: 0;
839
+ }
840
+
841
+ .chat-messages .description {
842
+ font-size: 0.95rem;
843
+ color: var(--text-secondary);
844
+ margin: 0;
845
+ }
846
+
847
+ .chat-messages .listen-button {
848
+ align-self: flex-start;
849
+ padding: 0.75rem 1.5rem;
850
+ background-color: var(--accent-primary);
851
+ color: white;
852
+ border: none;
853
+ border-radius: 0.5rem;
854
+ font-size: 0.9rem;
855
+ cursor: pointer;
856
+ transition: all 0.2s;
857
+ display: flex;
858
+ align-items: center;
859
+ gap: 0.5rem;
860
+ min-width: 120px;
861
+ justify-content: center;
862
+ }
863
+
864
+ .chat-messages .listen-button:hover {
865
+ background-color: var(--accent-secondary);
866
+ transform: translateY(-2px);
867
+ }
868
+
869
+ .chat-messages .listen-button:active {
870
+ transform: translateY(0);
871
+ }
872
+
873
+ .chat-messages .listen-button:focus {
874
+ outline: 2px solid var(--accent-secondary);
875
+ outline-offset: 2px;
876
+ }
877
+
878
+ /* Podcasts Page Messages */
879
+ .loading-message,
880
+ .error-message,
881
+ .no-podcasts-message {
882
+ text-align: center;
883
+ padding: 2rem;
884
+ font-size: 1.2rem;
885
+ color: var(--text-primary);
886
+ background: var(--card-bg);
887
+ border-radius: 1rem;
888
+ border: 1px solid var(--border-color);
889
+ margin: 2rem;
890
+ width: 100%;
891
+ max-width: 600px;
892
+ }
893
+
894
+ .error-message {
895
+ color: #ef4444;
896
+ border-color: #ef4444;
897
+ }
898
+
899
+ .loading-message {
900
+ display: flex;
901
+ align-items: center;
902
+ justify-content: center;
903
+ gap: 1rem;
904
+ }
905
+
906
+ .loading-message::after {
907
+ content: '...';
908
+ animation: loading-dots 1.5s infinite;
909
+ }
910
+
911
+ /* Update podcast card styles for audio player */
912
+ .podcast-card .audio-player {
913
+ width: 100%;
914
+ margin: 1rem 0;
915
+ padding: 0.5rem;
916
+ background: var(--bg-primary);
917
+ border-radius: 0.5rem;
918
+ border: 1px solid var(--border-color);
919
+ }
920
+
921
+ .podcast-card .audio-player audio {
922
+ width: 100%;
923
+ height: 40px;
924
+ outline: none;
925
+ }
926
+
927
+ .podcast-card .audio-player audio::-webkit-media-controls-panel {
928
+ background-color: var(--bg-primary);
929
+ }
930
+
931
+ .podcast-card .audio-player audio::-webkit-media-controls-current-time-display,
932
+ .podcast-card .audio-player audio::-webkit-media-controls-time-remaining-display {
933
+ color: var(--text-primary);
934
+ }
935
+
936
+ .podcast-card .audio-player audio::-webkit-media-controls-play-button,
937
+ .podcast-card .audio-player audio::-webkit-media-controls-timeline,
938
+ .podcast-card .audio-player audio::-webkit-media-controls-volume-slider {
939
+ filter: invert(1);
940
+ }
941
+
942
+ .podcast-header {
943
+ display: flex;
944
+ justify-content: space-between;
945
+ align-items: flex-start;
946
+ position: relative;
947
+ width: 100%;
948
+ }
949
+
950
+ .delete-button {
951
+ opacity: 0;
952
+ position: absolute;
953
+ top: -0.5rem;
954
+ right: -0.5rem;
955
+ width: 2rem;
956
+ height: 2rem;
957
+ border-radius: 0.25rem;
958
+ background-color: var(--accent-secondary);
959
+ color: white;
960
+ border: none;
961
+ cursor: pointer;
962
+ font-size: 1.5rem;
963
+ line-height: 1;
964
+ display: flex;
965
+ align-items: center;
966
+ justify-content: center;
967
+ transition: all 0.2s ease;
968
+ z-index: 1;
969
+ }
970
+
971
+ .podcast-card:hover .delete-button {
972
+ opacity: 1;
973
+ }
974
+
975
+ .delete-button:hover {
976
+ background-color: #ef4444;
977
+ transform: scale(1.1);
978
+ }
979
+
980
+ .delete-button:active {
981
+ transform: scale(0.95);
982
+ }
983
+
984
+ .podcast-player-header {
985
+ padding: 1.5rem;
986
+ background: var(--bg-primary);
987
+ border-bottom: 1px solid var(--border-color);
988
+ }
989
+
990
+ .podcast-player-header .podcast-title {
991
+ font-size: 1.25rem;
992
+ color: var(--text-primary);
993
+ margin-bottom: 1rem;
994
+ }
995
+
996
+ .podcast-player-header .audio-player {
997
+ width: 100%;
998
+ padding: 0.5rem;
999
+ background: var(--card-bg);
1000
+ border-radius: 0.5rem;
1001
+ border: 1px solid var(--border-color);
1002
+ }
1003
+
1004
+ .podcast-player-header .audio-player audio {
1005
+ width: 100%;
1006
+ height: 40px;
1007
+ outline: none;
1008
+ }
1009
+
1010
+ .podcast-player-header .audio-player audio::-webkit-media-controls-panel {
1011
+ background-color: var(--bg-primary);
1012
+ }
1013
+
1014
+ .podcast-player-header .audio-player audio::-webkit-media-controls-current-time-display,
1015
+ .podcast-player-header .audio-player audio::-webkit-media-controls-time-remaining-display {
1016
+ color: var(--text-primary);
1017
+ }
1018
+
1019
+ .podcast-player-header .audio-player audio::-webkit-media-controls-play-button,
1020
+ .podcast-player-header .audio-player audio::-webkit-media-controls-timeline,
1021
+ .podcast-player-header .audio-player audio::-webkit-media-controls-volume-slider {
1022
+ filter: invert(1);
1023
+ }
1024
+
1025
+ .podcast-card .category-pill {
1026
+ display: inline-block;
1027
+ padding: 0.25rem 0.75rem;
1028
+ background: var(--accent-primary);
1029
+ color: white;
1030
+ border-radius: 999px;
1031
+ font-size: 0.8rem;
1032
+ font-weight: 500;
1033
+ text-transform: capitalize;
1034
+ opacity: 0.9;
1035
+ margin-bottom: 0.5rem;
1036
+ transition: all 0.2s ease;
1037
+ }
1038
+
1039
+ .podcast-card .category-pill:hover {
1040
+ opacity: 1;
1041
+ transform: translateY(-1px);
1042
+ }
1043
+
1044
+ .relevant-chunks {
1045
+ margin-top: 1rem;
1046
+ padding: 1rem;
1047
+ background: rgba(var(--card-bg-rgb), 0.5);
1048
+ border-radius: 0.5rem;
1049
+ font-size: 0.9rem;
1050
+ }
1051
+
1052
+ .chunk-section {
1053
+ margin-bottom: 1rem;
1054
+ }
1055
+
1056
+ .chunk-section:last-child {
1057
+ margin-bottom: 0;
1058
+ }
1059
+
1060
+ .chunk-section h4 {
1061
+ color: var(--accent-primary);
1062
+ margin-bottom: 0.5rem;
1063
+ font-size: 0.95rem;
1064
+ }
1065
+
1066
+ .chunk-section ul {
1067
+ list-style: none;
1068
+ padding: 0;
1069
+ margin: 0;
1070
+ }
1071
+
1072
+ .chunk-section li {
1073
+ padding: 0.5rem;
1074
+ margin-bottom: 0.5rem;
1075
+ background: rgba(var(--card-bg-rgb), 0.3);
1076
+ border-radius: 0.25rem;
1077
+ color: var(--text-secondary);
1078
+ font-size: 0.85rem;
1079
+ line-height: 1.4;
1080
+ }
1081
+
1082
+ .chunk-section li:last-child {
1083
+ margin-bottom: 0;
1084
+ }
podcraft/src/App.jsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { BrowserRouter as Router, Routes, Route, NavLink } from 'react-router-dom';
3
+ import './App.css';
4
+ import Home from './pages/Home';
5
+ import Podcasts from './pages/Podcasts';
6
+ import PodcastForm from './pages/PodcastForm';
7
+ import WaveCanvas from './components/WaveCanvas';
8
+
9
+ function App() {
10
+ const [isDarkTheme, setIsDarkTheme] = useState(true);
11
+
12
+ useEffect(() => {
13
+ document.body.setAttribute('data-theme', isDarkTheme ? 'dark' : 'light');
14
+ }, [isDarkTheme]);
15
+
16
+ return (
17
+ <Router>
18
+ <div className="app">
19
+ <WaveCanvas />
20
+ <nav className="leftnav">
21
+ <div className="nav-brand">PodCraft</div>
22
+ <div className="nav-links">
23
+ <NavLink to="/" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
24
+ <span className="nav-icon">🏠</span>
25
+ Home
26
+ </NavLink>
27
+ <NavLink to="/podcasts" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
28
+ <span className="nav-icon">🎧</span>
29
+ Podcasts
30
+ </NavLink>
31
+ </div>
32
+ <div className="theme-toggle">
33
+ <button
34
+ className="theme-button"
35
+ onClick={() => setIsDarkTheme(!isDarkTheme)}
36
+ >
37
+ <span className="nav-icon">{isDarkTheme ? '☀️' : '🌙'}</span>
38
+ {isDarkTheme ? 'Light Mode' : 'Dark Mode'}
39
+ </button>
40
+ </div>
41
+ </nav>
42
+ <main className="main-content">
43
+ <Routes>
44
+ <Route path="/" element={<Home />} />
45
+ <Route path="/podcasts" element={<Podcasts />} />
46
+ <Route path="/podcast/:id" element={<PodcastForm />} />
47
+ </Routes>
48
+ </main>
49
+ </div>
50
+ </Router>
51
+ );
52
+ }
53
+
54
+ export default App;
podcraft/src/assets/react.svg ADDED
podcraft/src/components/WaveCanvas.jsx ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef } from 'react';
2
+
3
+ const WaveCanvas = () => {
4
+ const canvasRef = useRef(null);
5
+
6
+ useEffect(() => {
7
+ const canvas = canvasRef.current;
8
+ if (!canvas) return;
9
+
10
+ const ctx = canvas.getContext('2d');
11
+ if (!ctx) return;
12
+
13
+ let animationFrameId;
14
+
15
+ // Set canvas size
16
+ const setCanvasSize = () => {
17
+ canvas.width = window.innerWidth;
18
+ canvas.height = window.innerHeight;
19
+ };
20
+
21
+ // Initial setup
22
+ setCanvasSize();
23
+
24
+ // Wave parameters
25
+ const waves = [
26
+ { amplitude: 120, frequency: 0.001, speed: 0.0005, phase: 0 },
27
+ { amplitude: 80, frequency: 0.002, speed: 0.0004, phase: 2 }
28
+ ];
29
+
30
+ // Get theme colors
31
+ const getColors = () => {
32
+ const isDarkTheme = document.body.getAttribute('data-theme') === 'dark';
33
+ return {
34
+ firstWave: isDarkTheme
35
+ ? { r: 147, g: 51, b: 234 } // Purple for dark theme
36
+ : { r: 18, g: 163, b: 176 }, // #12A3B0 Bright teal
37
+ secondWave: isDarkTheme
38
+ ? { r: 59, g: 130, b: 246 } // Blue for dark theme
39
+ : { r: 1, g: 73, b: 81 } // #014951 Medium teal
40
+ };
41
+ };
42
+
43
+ // Draw function
44
+ const draw = () => {
45
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
46
+
47
+ // Center the waves vertically
48
+ const centerY = canvas.height * 0.5;
49
+ const colors = getColors();
50
+
51
+ waves.forEach((wave, index) => {
52
+ // Update wave phase
53
+ wave.phase += wave.speed;
54
+
55
+ // Create gradient with theme-aware colors
56
+ const gradient = ctx.createLinearGradient(0, centerY - wave.amplitude, 0, centerY + wave.amplitude);
57
+ const color = index === 0 ? colors.firstWave : colors.secondWave;
58
+
59
+ if (document.body.getAttribute('data-theme') === 'dark') {
60
+ gradient.addColorStop(0, `rgba(${color.r}, ${color.g}, ${color.b}, 0)`);
61
+ gradient.addColorStop(0.5, `rgba(${color.r}, ${color.g}, ${color.b}, 0.8)`);
62
+ gradient.addColorStop(1, `rgba(${color.r}, ${color.g}, ${color.b}, 0)`);
63
+ } else {
64
+ // Light theme gradient with additional color stops
65
+ gradient.addColorStop(0, `rgba(${color.r}, ${color.g}, ${color.b}, 0)`);
66
+ gradient.addColorStop(0.2, `rgba(1, 49, 53, 0.4)`); // #013135
67
+ gradient.addColorStop(0.5, `rgba(${color.r}, ${color.g}, ${color.b}, 0.8)`);
68
+ gradient.addColorStop(0.8, `rgba(175, 221, 229, 0.4)`); // #AFDDE5
69
+ gradient.addColorStop(1, `rgba(${color.r}, ${color.g}, ${color.b}, 0)`);
70
+ }
71
+
72
+ // Begin drawing wave
73
+ ctx.beginPath();
74
+
75
+ // Start from bottom left
76
+ ctx.moveTo(0, canvas.height);
77
+ ctx.lineTo(0, centerY);
78
+
79
+ // Draw wave path
80
+ for (let x = 0; x <= canvas.width; x++) {
81
+ const y = centerY +
82
+ Math.sin(x * wave.frequency + wave.phase) * wave.amplitude;
83
+ ctx.lineTo(x, y);
84
+ }
85
+
86
+ // Complete the path to bottom right
87
+ ctx.lineTo(canvas.width, centerY);
88
+ ctx.lineTo(canvas.width, canvas.height);
89
+ ctx.closePath();
90
+
91
+ // Fill with gradient
92
+ ctx.fillStyle = gradient;
93
+ ctx.fill();
94
+ });
95
+
96
+ animationFrameId = requestAnimationFrame(draw);
97
+ };
98
+
99
+ // Start animation
100
+ draw();
101
+
102
+ // Handle resize and theme changes
103
+ const handleResize = () => {
104
+ setCanvasSize();
105
+ };
106
+
107
+ window.addEventListener('resize', handleResize);
108
+
109
+ // Watch for theme changes
110
+ const observer = new MutationObserver(() => {
111
+ draw(); // Redraw when theme changes
112
+ });
113
+
114
+ observer.observe(document.body, {
115
+ attributes: true,
116
+ attributeFilter: ['data-theme']
117
+ });
118
+
119
+ // Cleanup
120
+ return () => {
121
+ window.removeEventListener('resize', handleResize);
122
+ cancelAnimationFrame(animationFrameId);
123
+ observer.disconnect();
124
+ };
125
+ }, []);
126
+
127
+ return <canvas ref={canvasRef} className="wave-canvas" />;
128
+ };
129
+
130
+ export default WaveCanvas;
podcraft/src/index.css ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ @media (prefers-color-scheme: light) {
58
+ :root {
59
+ color: #213547;
60
+ background-color: #ffffff;
61
+ }
62
+ a:hover {
63
+ color: #747bff;
64
+ }
65
+ button {
66
+ background-color: #f9f9f9;
67
+ }
68
+ }
podcraft/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
podcraft/src/pages/Home.tsx ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import '../App.css';
3
+
4
+ interface Message {
5
+ id: number;
6
+ text: string;
7
+ agent: string;
8
+ audio_file?: string;
9
+ title?: string;
10
+ description?: string;
11
+ category?: string;
12
+ }
13
+
14
+ interface DebateEntry {
15
+ speaker: string;
16
+ content: string;
17
+ }
18
+
19
+ interface ChatResponse {
20
+ debate_history: DebateEntry[];
21
+ supervisor_notes: string[];
22
+ final_podcast?: {
23
+ content: string;
24
+ audio_file: string;
25
+ title: string;
26
+ description: string;
27
+ };
28
+ }
29
+
30
+ const API_URL = 'http://localhost:8000';
31
+
32
+ const Home: React.FC = () => {
33
+ const [messages, setMessages] = useState<Message[]>([
34
+ { id: 1, text: "Welcome! I'll help you create your own podcast content by exploring topics through an AI debate. Enter any topic of choice.", agent: "system" },
35
+ ]);
36
+ const [inputMessage, setInputMessage] = useState('');
37
+ const [isLoading, setIsLoading] = useState(false);
38
+ const messagesEndRef = useRef<HTMLDivElement>(null);
39
+ const audioRef = useRef<HTMLAudioElement>(null);
40
+
41
+ useEffect(() => {
42
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
43
+ }, [messages]);
44
+
45
+ const sendMessageToServer = async (message: string): Promise<ChatResponse> => {
46
+ try {
47
+ setMessages(prev => [...prev, {
48
+ id: prev.length + 1,
49
+ text: `Processing your request...`,
50
+ agent: "system"
51
+ }]);
52
+
53
+ const response = await fetch(`${API_URL}/chat`, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ },
58
+ body: JSON.stringify({
59
+ content: message,
60
+ agent_type: "believer",
61
+ context: {
62
+ podcast_id: null,
63
+ agent_chunks: [],
64
+ current_agent: "believer"
65
+ }
66
+ })
67
+ });
68
+
69
+ if (!response.ok) {
70
+ const errorData = await response.text();
71
+ throw new Error(`Server error: ${response.status} - ${errorData}`);
72
+ }
73
+
74
+ const data: ChatResponse = await response.json();
75
+ return data;
76
+ } catch (error) {
77
+ console.error('Error sending message:', error);
78
+ throw error;
79
+ }
80
+ };
81
+
82
+ const handleSubmit = async (e: React.FormEvent) => {
83
+ e.preventDefault();
84
+ if (!inputMessage.trim()) return;
85
+
86
+ const userMessage: Message = {
87
+ id: messages.length + 1,
88
+ text: inputMessage,
89
+ agent: "user"
90
+ };
91
+
92
+ setMessages(prev => [...prev, userMessage]);
93
+ setInputMessage('');
94
+ setIsLoading(true);
95
+
96
+ try {
97
+ const response = await sendMessageToServer(inputMessage);
98
+
99
+ if (response.debate_history) {
100
+ response.debate_history.forEach((entry) => {
101
+ if (entry.speaker && entry.content) {
102
+ const message: Message = {
103
+ id: messages.length + 1,
104
+ text: entry.content,
105
+ agent: entry.speaker
106
+ };
107
+ setMessages(prev => [...prev, message]);
108
+ }
109
+ });
110
+ }
111
+
112
+ if (response.supervisor_notes) {
113
+ response.supervisor_notes.forEach((note) => {
114
+ const supervisorMessage: Message = {
115
+ id: messages.length + 1,
116
+ text: note,
117
+ agent: "supervisor"
118
+ };
119
+ setMessages(prev => [...prev, supervisorMessage]);
120
+ });
121
+ }
122
+
123
+ if (response.final_podcast && response.final_podcast.audio_file) {
124
+ const filename = response.final_podcast.audio_file;
125
+ const [queryPart, descriptionPart, categoryWithExt] = filename.split('-');
126
+ const category = categoryWithExt.replace('.mp3', '');
127
+
128
+ const podcastMessage: Message = {
129
+ id: messages.length + 1,
130
+ text: response.final_podcast.content || "Podcast generated successfully!",
131
+ agent: "system",
132
+ audio_file: `/audio-files/${filename}`,
133
+ title: descriptionPart.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase()),
134
+ description: `A debate exploring ${queryPart.replace(/_/g, ' ')}`,
135
+ category: category.replace(/_/g, ' ')
136
+ };
137
+ setMessages(prev => [...prev, podcastMessage]);
138
+ }
139
+ } catch (error) {
140
+ setMessages(prev => [...prev, {
141
+ id: prev.length + 1,
142
+ text: `Error: ${error.message}`,
143
+ agent: "system"
144
+ }]);
145
+ } finally {
146
+ setIsLoading(false);
147
+ }
148
+ };
149
+
150
+ return (
151
+ <div className="chat-container">
152
+ <div className="chat-messages">
153
+ {messages.map((message) => (
154
+ <div key={message.id} className={`message ${message.agent}-message`}>
155
+ <div className="message-content">
156
+ <div className="agent-icon">
157
+ {message.agent === "user" ? "👤" :
158
+ message.agent === "system" ? "🤖" :
159
+ message.agent === "believer" ? "💡" :
160
+ message.agent === "skeptic" ? "🤔" :
161
+ message.agent === "supervisor" ? "👀" :
162
+ message.agent === "extractor" ? "🔍" : "💬"}
163
+ </div>
164
+ <div className="message-text-content">
165
+ <div className="agent-name">{message.agent}</div>
166
+ <div className="message-text">{message.text}</div>
167
+ {message.audio_file && (
168
+ <div className="podcast-card">
169
+ <div className="podcast-content">
170
+ <h2 className="podcast-title">{message.title || "Generated Podcast"}</h2>
171
+ {message.category && (
172
+ <div className="category-pill">{message.category}</div>
173
+ )}
174
+ <p className="description">{message.description || "An AI-generated debate podcast exploring different perspectives"}</p>
175
+ <div className="audio-player">
176
+ <audio controls src={`${API_URL}${message.audio_file}`} ref={audioRef}>
177
+ Your browser does not support the audio element.
178
+ </audio>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ )}
183
+ </div>
184
+ </div>
185
+ </div>
186
+ ))}
187
+ {isLoading && (
188
+ <div className="message system-message">
189
+ <div className="message-content">
190
+ <div className="loading-dots">Debating</div>
191
+ <div className="loading-dots">This might take a few moments since 2 agents are fighting over getting the best insights for you</div>
192
+ </div>
193
+ </div>
194
+ )}
195
+ <div ref={messagesEndRef} />
196
+ </div>
197
+ <form onSubmit={handleSubmit} className="chat-input-form">
198
+ <input
199
+ type="text"
200
+ value={inputMessage}
201
+ onChange={(e) => setInputMessage(e.target.value)}
202
+ placeholder="Type your message..."
203
+ className="chat-input"
204
+ disabled={isLoading}
205
+ />
206
+ <button type="submit" className="chat-send-button" disabled={isLoading}>
207
+ Send
208
+ </button>
209
+ </form>
210
+ </div>
211
+ );
212
+ };
213
+
214
+ export default Home;
podcraft/src/pages/PodcastForm.tsx ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { useParams } from 'react-router-dom';
3
+ import '../App.css';
4
+
5
+ const API_URL = 'http://localhost:8000';
6
+
7
+ interface Message {
8
+ id: number;
9
+ text: string;
10
+ agent: string;
11
+ }
12
+
13
+ interface Podcast {
14
+ id: number;
15
+ title: string;
16
+ description: string;
17
+ audio_file: string;
18
+ }
19
+
20
+ interface PodcastContext {
21
+ topic: string;
22
+ believer_chunks: string[];
23
+ skeptic_chunks: string[];
24
+ }
25
+
26
+ const PodcastForm: React.FC = () => {
27
+ const { id } = useParams<{ id: string }>();
28
+ const [podcast, setPodcast] = useState<Podcast | null>(null);
29
+ const [podcastContext, setPodcastContext] = useState<PodcastContext | null>(null);
30
+ const [messages, setMessages] = useState<Message[]>([]);
31
+ const [inputMessage, setInputMessage] = useState('');
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const messagesEndRef = useRef<HTMLDivElement>(null);
34
+
35
+ useEffect(() => {
36
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
37
+ }, [messages]);
38
+
39
+ useEffect(() => {
40
+ const fetchPodcastAndContext = async () => {
41
+ try {
42
+ if (!id) return;
43
+
44
+ // Fetch podcast details
45
+ const response = await fetch(`${API_URL}/audio-list`);
46
+ if (!response.ok) {
47
+ throw new Error('Failed to fetch podcasts');
48
+ }
49
+ const files = await response.json();
50
+
51
+ const podcastList = files.map((file: any, index: number) => ({
52
+ id: index + 1,
53
+ title: file.filename.split('-')[0].replace(/_/g, ' '),
54
+ description: "An AI-generated debate podcast exploring different perspectives",
55
+ audio_file: `${API_URL}/audio/${file.filename}`
56
+ }));
57
+
58
+ const selectedPodcast = podcastList.find(p => p.id === parseInt(id));
59
+ if (selectedPodcast) {
60
+ setPodcast(selectedPodcast);
61
+
62
+ // Fetch podcast context
63
+ const contextResponse = await fetch(`${API_URL}/podcast/${id}/context`);
64
+ if (contextResponse.ok) {
65
+ const contextData: PodcastContext = await contextResponse.json();
66
+ setPodcastContext(contextData);
67
+ }
68
+ }
69
+ } catch (err) {
70
+ console.error('Error fetching podcast:', err);
71
+ }
72
+ };
73
+
74
+ fetchPodcastAndContext();
75
+ }, [id]);
76
+
77
+ const handleSubmit = async (e: React.FormEvent) => {
78
+ e.preventDefault();
79
+ if (!inputMessage.trim() || !id) return;
80
+
81
+ const userMessage: Message = {
82
+ id: messages.length + 1,
83
+ text: inputMessage,
84
+ agent: "user"
85
+ };
86
+
87
+ setMessages(prev => [...prev, userMessage]);
88
+ setInputMessage('');
89
+ setIsLoading(true);
90
+
91
+ try {
92
+ const response = await fetch(`${API_URL}/podcast-chat/${id}`, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ body: JSON.stringify({
98
+ message: inputMessage
99
+ })
100
+ });
101
+
102
+ if (!response.ok) {
103
+ throw new Error(`Server error: ${response.status}`);
104
+ }
105
+
106
+ const data = await response.json();
107
+
108
+ const botMessage: Message = {
109
+ id: messages.length + 2,
110
+ text: data.response,
111
+ agent: "assistant"
112
+ };
113
+
114
+ setMessages(prev => [...prev, botMessage]);
115
+ } catch (error) {
116
+ console.error('Error sending message:', error);
117
+ setMessages(prev => [...prev, {
118
+ id: prev.length + 1,
119
+ text: `Error: ${error instanceof Error ? error.message : 'Failed to send message'}`,
120
+ agent: "system"
121
+ }]);
122
+ } finally {
123
+ setIsLoading(false);
124
+ }
125
+ };
126
+
127
+ return (
128
+ <div className="podcast-form-container">
129
+ <div className="chat-column">
130
+ <div className="podcast-chat-container">
131
+ {podcast && (
132
+ <div className="podcast-player-header">
133
+ <h2 className="podcast-title">{podcast.title}</h2>
134
+ {podcastContext && (
135
+ <p className="podcast-topic">Topic: {podcastContext.topic}</p>
136
+ )}
137
+ <div className="audio-player">
138
+ <audio
139
+ controls
140
+ src={podcast.audio_file}
141
+ >
142
+ Your browser does not support the audio element.
143
+ </audio>
144
+ </div>
145
+ </div>
146
+ )}
147
+
148
+ <div className="podcast-chat-messages">
149
+ {messages.map((message) => (
150
+ <div key={message.id} className={`message ${message.agent}-message`}>
151
+ <div className="message-content">
152
+ <div className="agent-icon">
153
+ {message.agent === "user" ? "👤" :
154
+ message.agent === "system" ? "🤖" :
155
+ message.agent === "assistant" ? "🤖" : "💬"}
156
+ </div>
157
+ <div className="message-text-content">
158
+ <div className="agent-name">{message.agent}</div>
159
+ <div className="message-text">{message.text}</div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ ))}
164
+ {isLoading && (
165
+ <div className="message system-message">
166
+ <div className="message-content">
167
+ <div className="loading-dots">Processing</div>
168
+ </div>
169
+ </div>
170
+ )}
171
+ <div ref={messagesEndRef} />
172
+ </div>
173
+
174
+ <form onSubmit={handleSubmit} className="podcast-chat-input-form">
175
+ <input
176
+ type="text"
177
+ value={inputMessage}
178
+ onChange={(e) => setInputMessage(e.target.value)}
179
+ placeholder="Ask a question about this podcast..."
180
+ className="podcast-chat-input"
181
+ disabled={isLoading}
182
+ />
183
+ <button type="submit" className="podcast-chat-send-button" disabled={isLoading}>
184
+ Send
185
+ </button>
186
+ </form>
187
+
188
+ {podcastContext && (
189
+ <div className="relevant-chunks">
190
+ <div className="chunk-section">
191
+ <h4>Key Points</h4>
192
+ <ul>
193
+ {podcastContext.believer_chunks.map((chunk, i) => (
194
+ <li key={`believer-${i}`}>{chunk}</li>
195
+ ))}
196
+ {podcastContext.skeptic_chunks.map((chunk, i) => (
197
+ <li key={`skeptic-${i}`}>{chunk}</li>
198
+ ))}
199
+ </ul>
200
+ </div>
201
+ </div>
202
+ )}
203
+ </div>
204
+ </div>
205
+ </div>
206
+ );
207
+ };
208
+
209
+ export default PodcastForm;
podcraft/src/pages/Podcasts.tsx ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import '../App.css';
4
+
5
+ const API_URL = 'http://localhost:8000';
6
+
7
+ interface Podcast {
8
+ id: number;
9
+ title: string;
10
+ description: string;
11
+ audio_file: string;
12
+ filename: string;
13
+ category: string;
14
+ }
15
+
16
+ const Podcasts: React.FC = () => {
17
+ const navigate = useNavigate();
18
+ const [podcasts, setPodcasts] = useState<Podcast[]>([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [error, setError] = useState("");
21
+
22
+ useEffect(() => {
23
+ fetchPodcasts();
24
+ }, []);
25
+
26
+ const handleDelete = async (podcast: Podcast) => {
27
+ try {
28
+ const response = await fetch(`${API_URL}/audio/${podcast.filename}`, {
29
+ method: 'DELETE',
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ }
33
+ });
34
+
35
+ if (!response.ok) {
36
+ const errorData = await response.text();
37
+ throw new Error(
38
+ `Failed to delete podcast (${response.status}): ${errorData}`
39
+ );
40
+ }
41
+
42
+ setPodcasts(prev => prev.filter(p => p.filename !== podcast.filename));
43
+
44
+ } catch (err) {
45
+ console.error('Delete error:', err);
46
+ setError(err instanceof Error ? err.message : 'Failed to delete podcast');
47
+
48
+ setTimeout(() => setError(""), 5000);
49
+ }
50
+ };
51
+
52
+ const fetchPodcasts = async () => {
53
+ try {
54
+ const response = await fetch(`${API_URL}/audio-list`);
55
+ if (!response.ok) {
56
+ throw new Error('Failed to fetch podcasts');
57
+ }
58
+
59
+ const files = await response.json();
60
+
61
+ const podcastList: Podcast[] = files.map((file: any, index: number) => {
62
+ const filename = file.filename;
63
+ const [queryPart, descriptionPart, categoryWithExt] = filename.split('-');
64
+ const category = categoryWithExt.replace('.mp3', '');
65
+
66
+ return {
67
+ id: index + 1,
68
+ title: `${descriptionPart.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase())}`,
69
+ description: `A debate exploring ${queryPart.replace(/_/g, ' ')}`,
70
+ audio_file: `${API_URL}${file.path}`,
71
+ filename: filename,
72
+ category: category.replace(/_/g, ' ')
73
+ };
74
+ });
75
+
76
+ setPodcasts(podcastList);
77
+ } catch (err) {
78
+ setError(err instanceof Error ? err.message : 'An error occurred');
79
+ } finally {
80
+ setLoading(false);
81
+ }
82
+ };
83
+
84
+ if (loading) {
85
+ return (
86
+ <div className="podcasts-container">
87
+ <div className="loading-message">Loading podcasts...</div>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ if (error !== "") {
93
+ return (
94
+ <div className="podcasts-container">
95
+ <div className="error-message">Error: {error}</div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ return (
101
+ <div className="podcasts-container">
102
+ <header className="podcasts-header">
103
+ <h1>Your Generated Podcasts</h1>
104
+ <p>Listen to AI-generated debate podcasts on various topics</p>
105
+ </header>
106
+
107
+ <div className="podcasts-grid">
108
+ {podcasts.map(podcast => (
109
+ <div
110
+ key={podcast.id}
111
+ className="podcast-card"
112
+ onClick={() => navigate(`/podcast/${podcast.id}`)}
113
+ style={{ cursor: 'pointer' }}
114
+ >
115
+ <div className="podcast-content">
116
+ <div className="podcast-header">
117
+ <h2 className="podcast-title">{podcast.title}</h2>
118
+ <button
119
+ className="delete-button"
120
+ onClick={(e) => {
121
+ e.stopPropagation();
122
+ handleDelete(podcast);
123
+ }}
124
+ aria-label="Delete podcast"
125
+ >
126
+ ×
127
+ </button>
128
+ </div>
129
+ <div className="category-pill">{podcast.category}</div>
130
+ <p className="description">{podcast.description}</p>
131
+ <div className="audio-player" onClick={e => e.stopPropagation()}>
132
+ <audio
133
+ controls
134
+ src={podcast.audio_file}
135
+ >
136
+ Your browser does not support the audio element.
137
+ </audio>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ ))}
142
+ {podcasts.length === 0 && (
143
+ <div className="no-podcasts-message">
144
+ No podcasts found. Generate your first podcast from the home page!
145
+ </div>
146
+ )}
147
+ </div>
148
+ </div>
149
+ );
150
+ };
151
+
152
+ export default Podcasts;
podcraft/src/types/index.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Message {
2
+ id: number;
3
+ text: string;
4
+ agent: string;
5
+ audio_file?: string;
6
+ title?: string;
7
+ description?: string;
8
+ }
9
+
10
+ export interface DebateEntry {
11
+ speaker: string;
12
+ content: string;
13
+ }
14
+
15
+ export interface PodcastData {
16
+ content: string;
17
+ audio_file: string;
18
+ title: string;
19
+ description: string;
20
+ category: string;
21
+ }
22
+
23
+ export interface ChatResponse {
24
+ debate_history: DebateEntry[];
25
+ supervisor_notes: string[];
26
+ supervisor_chunks: {
27
+ [key: string]: string[];
28
+ }[];
29
+ extractor_data: {
30
+ content: string;
31
+ raw_results: any;
32
+ };
33
+ final_podcast: PodcastData;
34
+ }
podcraft/vite.config.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ server: {
8
+ port: 80,
9
+ proxy: {
10
+ '/api': {
11
+ target: 'http://backend:8000',
12
+ changeOrigin: true,
13
+ rewrite: (path) => path.replace(/^\/api/, '')
14
+ },
15
+ '/audio-files': {
16
+ target: 'http://backend:8000',
17
+ changeOrigin: true
18
+ }
19
+ }
20
+ },
21
+ build: {
22
+ outDir: 'dist',
23
+ assetsDir: 'assets',
24
+ emptyOutDir: true
25
+ }
26
+ })
server/Dockerfile CHANGED
@@ -1,24 +1,26 @@
1
- FROM python:3.10-slim
2
 
3
- WORKDIR /code
4
 
5
  # Install system dependencies
6
  RUN apt-get update && apt-get install -y \
7
  build-essential \
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
- # Copy requirements first for better caching
11
  COPY requirements.txt .
 
 
12
  RUN pip install --no-cache-dir -r requirements.txt
13
 
14
- # Copy the rest of the application
15
  COPY . .
16
 
17
  # Create necessary directories
18
- RUN mkdir -p audio_storage context_storage transcripts
19
 
20
- # Expose the port
21
- EXPOSE 7860
22
 
23
  # Command to run the application
24
- CMD ["python", "app.py"]
 
1
+ FROM python:3.11-slim
2
 
3
+ WORKDIR /app
4
 
5
  # Install system dependencies
6
  RUN apt-get update && apt-get install -y \
7
  build-essential \
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
+ # Copy requirements file
11
  COPY requirements.txt .
12
+
13
+ # Install Python dependencies
14
  RUN pip install --no-cache-dir -r requirements.txt
15
 
16
+ # Copy server code
17
  COPY . .
18
 
19
  # Create necessary directories
20
+ RUN mkdir -p audio_storage transcripts
21
 
22
+ # Expose port
23
+ EXPOSE 8000
24
 
25
  # Command to run the application
26
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
server/README.md DELETED
@@ -1,59 +0,0 @@
1
- # Podcast Discussion Server
2
-
3
- This is a FastAPI-based server that provides podcast discussion and analysis capabilities.
4
-
5
- ## Environment Variables
6
-
7
- The following environment variables need to be set in the Hugging Face Space:
8
-
9
- - `OPENAI_API_KEY`: Your OpenAI API key
10
- - `ALLOWED_ORIGINS`: Comma-separated list of allowed origins (optional, defaults to Vercel domains and localhost)
11
-
12
- ## API Endpoints
13
-
14
- - `/chat`: Main chat endpoint for podcast discussions
15
- - `/podcast-chat/{podcast_id}`: Chat endpoint for specific podcast discussions
16
- - `/audio-list`: List available audio files
17
- - `/audio/{filename}`: Get specific audio file
18
- - `/podcast/{podcast_id}/context`: Get podcast context
19
-
20
- ## Stack
21
-
22
- - FastAPI
23
- - OpenAI
24
- - Langchain
25
- - Qdrant
26
- - GTTS
27
-
28
- ## Deployment
29
-
30
- ### Backend (Hugging Face Spaces)
31
-
32
- This server is deployed on Hugging Face Spaces using their Docker deployment feature.
33
-
34
- ### Frontend (Vercel)
35
-
36
- When deploying the frontend to Vercel:
37
-
38
- 1. Set the API base URL in your frontend environment:
39
-
40
- ```
41
- VITE_API_BASE_URL=https://your-username-your-space-name.hf.space
42
- ```
43
-
44
- 2. The server is already configured to accept requests from:
45
-
46
- - All Vercel domains (\*.vercel.app)
47
- - Local development servers (localhost:3000, localhost:5173)
48
-
49
- 3. If you're using a custom domain, add it to the `ALLOWED_ORIGINS` environment variable in your Hugging Face Space:
50
- ```
51
- ALLOWED_ORIGINS=https://your-custom-domain.com,https://www.your-custom-domain.com
52
- ```
53
-
54
- ## Security Features
55
-
56
- - CORS protection with specific origin allowlist
57
- - Security headers (HSTS, XSS Protection, etc.)
58
- - Rate limiting
59
- - SSL/TLS encryption (provided by Hugging Face Spaces)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/agents.log ADDED
The diff for this file is too large to render. See raw diff
 
server/app.py DELETED
@@ -1,5 +0,0 @@
1
- import uvicorn
2
- from main import app
3
-
4
- if __name__ == "__main__":
5
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
server/generate_test_dataset.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from uuid import uuid4
4
+ from datetime import datetime
5
+
6
+ # Define the path for the golden dataset
7
+ GOLDEN_DATASET_DIR = os.path.join(os.path.dirname(__file__), "test_data")
8
+ os.makedirs(GOLDEN_DATASET_DIR, exist_ok=True)
9
+
10
+ def create_test_case(query, agent_type="believer", context=None):
11
+ """Helper function to create a test case with standard structure."""
12
+ return {
13
+ "id": str(uuid4()),
14
+ "input": {
15
+ "query": query,
16
+ "agent_type": agent_type,
17
+ "context": context
18
+ },
19
+ "expected_output": {
20
+ "debate_structure": {
21
+ "min_turns": 2,
22
+ "max_turns": 4,
23
+ "required_agents": ["extractor", "believer", "skeptic", "supervisor"]
24
+ },
25
+ "transcript_requirements": {
26
+ "required_fields": ["id", "podcastScript", "topic"],
27
+ "topic_match": True,
28
+ "min_script_length": 500
29
+ },
30
+ "podcast_requirements": {
31
+ "required_fields": ["content", "audio_file", "title", "description", "category"],
32
+ "audio_format": "mp3",
33
+ "min_file_size": 1000
34
+ }
35
+ }
36
+ }
37
+
38
+ def generate_test_cases():
39
+ """Generate 50 diverse test cases."""
40
+ test_cases = []
41
+
42
+ # Technology Topics (10 cases)
43
+ tech_topics = [
44
+ ("Are electric vehicles better for the environment?", "believer"),
45
+ ("Should artificial intelligence be regulated?", "skeptic"),
46
+ ("Is blockchain technology revolutionizing finance?", "believer"),
47
+ ("Are smart homes making us too dependent on technology?", "skeptic"),
48
+ ("Should social media platforms be responsible for content moderation?", "believer"),
49
+ ("Is 5G technology safe for public health?", "skeptic"),
50
+ ("Will quantum computing make current encryption obsolete?", "believer"),
51
+ ("Should facial recognition be used in public spaces?", "skeptic"),
52
+ ("Are autonomous vehicles ready for widespread adoption?", "believer"),
53
+ ("Does virtual reality have practical applications beyond gaming?", "skeptic")
54
+ ]
55
+
56
+ # Society and Culture (10 cases)
57
+ society_topics = [
58
+ ("Is remote work the future of employment?", "believer"),
59
+ ("Should universal basic income be implemented globally?", "skeptic"),
60
+ ("Are social media platforms harming mental health?", "believer"),
61
+ ("Should voting be mandatory?", "skeptic"),
62
+ ("Is cancel culture beneficial for society?", "believer"),
63
+ ("Should there be limits on free speech online?", "skeptic"),
64
+ ("Are gender quotas effective in achieving equality?", "believer"),
65
+ ("Should religious education be part of public schools?", "skeptic"),
66
+ ("Is multiculturalism strengthening or weakening societies?", "believer"),
67
+ ("Should citizenship be available for purchase?", "skeptic")
68
+ ]
69
+
70
+ # Environment and Sustainability (10 cases)
71
+ environment_topics = [
72
+ ("Can renewable energy completely replace fossil fuels?", "believer"),
73
+ ("Should nuclear power be part of climate change solution?", "skeptic"),
74
+ ("Is carbon pricing effective in reducing emissions?", "believer"),
75
+ ("Should single-use plastics be completely banned?", "skeptic"),
76
+ ("Are vertical farms the future of agriculture?", "believer"),
77
+ ("Should meat consumption be regulated for environmental reasons?", "skeptic"),
78
+ ("Is geoengineering a viable solution to climate change?", "believer"),
79
+ ("Should private companies be allowed to exploit space resources?", "skeptic"),
80
+ ("Are carbon offsets an effective environmental solution?", "believer"),
81
+ ("Should environmental protection override economic growth?", "skeptic")
82
+ ]
83
+
84
+ # Health and Wellness (10 cases)
85
+ health_topics = [
86
+ ("Should healthcare be completely free?", "believer"),
87
+ ("Is telemedicine as effective as traditional healthcare?", "skeptic"),
88
+ ("Should vaccines be mandatory?", "believer"),
89
+ ("Is genetic engineering of humans ethical?", "skeptic"),
90
+ ("Should alternative medicine be covered by insurance?", "believer"),
91
+ ("Is human enhancement technology ethical?", "skeptic"),
92
+ ("Should organ donation be opt-out rather than opt-in?", "believer"),
93
+ ("Are fitness trackers improving public health?", "skeptic"),
94
+ ("Should sugar be regulated like tobacco?", "believer"),
95
+ ("Is meditation effective as mental health treatment?", "skeptic")
96
+ ]
97
+
98
+ # Education and Career (10 cases)
99
+ education_topics = [
100
+ ("Should college education be free?", "believer"),
101
+ ("Is standardized testing effective?", "skeptic"),
102
+ ("Should coding be mandatory in schools?", "believer"),
103
+ ("Are traditional degrees becoming obsolete?", "skeptic"),
104
+ ("Should student debt be forgiven?", "believer"),
105
+ ("Is homeschooling as effective as traditional schooling?", "skeptic"),
106
+ ("Should arts education be mandatory?", "believer"),
107
+ ("Are gap years beneficial for students?", "skeptic"),
108
+ ("Should schools teach financial literacy?", "believer"),
109
+ ("Is year-round schooling better for learning?", "skeptic")
110
+ ]
111
+
112
+ # Add all topics to test cases
113
+ for topics in [tech_topics, society_topics, environment_topics, health_topics, education_topics]:
114
+ for query, agent_type in topics:
115
+ test_cases.append(create_test_case(query, agent_type))
116
+
117
+ return test_cases
118
+
119
+ def generate_golden_dataset():
120
+ """Generate a golden dataset for testing the podcast debate system."""
121
+
122
+ # Get test cases
123
+ test_cases = generate_test_cases()
124
+
125
+ # Create sample transcripts
126
+ sample_transcripts = [
127
+ {
128
+ "id": str(uuid4()),
129
+ "podcastScript": """**Podcast Script: Electric Vehicles and the Environment**
130
+
131
+ Host: Welcome to our debate on the environmental impact of electric vehicles...
132
+
133
+ Skeptic: While EVs reduce direct emissions, we must consider the environmental cost of battery production...
134
+
135
+ Believer: The long-term benefits of EVs in reducing carbon emissions far outweigh the initial production impact...""",
136
+ "topic": "Are electric vehicles better for the environment?"
137
+ },
138
+ {
139
+ "id": str(uuid4()),
140
+ "podcastScript": """**Podcast Script: AI Regulation Debate**
141
+
142
+ Host: Today we're exploring the complex topic of AI regulation...
143
+
144
+ Skeptic: Without proper oversight, AI development could lead to serious societal risks...
145
+
146
+ Believer: Smart regulation can help us harness AI's benefits while minimizing potential harm...""",
147
+ "topic": "Should artificial intelligence be regulated?"
148
+ }
149
+ ]
150
+
151
+ # Create the golden dataset structure
152
+ golden_dataset = {
153
+ "metadata": {
154
+ "created_at": datetime.now().isoformat(),
155
+ "version": "1.0",
156
+ "description": "Golden dataset for testing the podcast debate system",
157
+ "total_test_cases": len(test_cases),
158
+ "categories": [
159
+ "Technology",
160
+ "Society and Culture",
161
+ "Environment and Sustainability",
162
+ "Health and Wellness",
163
+ "Education and Career"
164
+ ]
165
+ },
166
+ "test_cases": test_cases,
167
+ "sample_transcripts": sample_transcripts,
168
+ "validation_rules": {
169
+ "debate": {
170
+ "required_agents": ["extractor", "believer", "skeptic", "supervisor"],
171
+ "min_debate_turns": 2,
172
+ "max_debate_turns": 4
173
+ },
174
+ "transcript": {
175
+ "required_fields": ["id", "podcastScript", "topic"],
176
+ "min_script_length": 500
177
+ },
178
+ "podcast": {
179
+ "required_fields": ["content", "audio_file", "title", "description", "category"],
180
+ "supported_audio_formats": ["mp3"],
181
+ "min_file_size": 1000
182
+ }
183
+ }
184
+ }
185
+
186
+ # Save the golden dataset
187
+ output_file = os.path.join(GOLDEN_DATASET_DIR, "golden_dataset.json")
188
+ with open(output_file, "w") as f:
189
+ json.dump(golden_dataset, f, indent=2)
190
+
191
+ print(f"Golden dataset generated successfully at: {output_file}")
192
+ return golden_dataset
193
+
194
+ def validate_test_case(test_case, actual_output):
195
+ """Validate a test case against actual output."""
196
+ validation_results = {
197
+ "test_case_id": test_case["id"],
198
+ "query": test_case["input"]["query"],
199
+ "validations": []
200
+ }
201
+
202
+ # Validate debate structure
203
+ expected_structure = test_case["expected_output"]["debate_structure"]
204
+ debate_history = actual_output.get("debate_history", [])
205
+
206
+ validation_results["validations"].append({
207
+ "check": "debate_turns",
208
+ "passed": expected_structure["min_turns"] <= len(debate_history) <= expected_structure["max_turns"],
209
+ "details": f"Expected {expected_structure['min_turns']}-{expected_structure['max_turns']} turns, got {len(debate_history)}"
210
+ })
211
+
212
+ # Validate transcript
213
+ transcript_reqs = test_case["expected_output"]["transcript_requirements"]
214
+ if "transcript" in actual_output:
215
+ transcript = actual_output["transcript"]
216
+ validation_results["validations"].append({
217
+ "check": "transcript_fields",
218
+ "passed": all(field in transcript for field in transcript_reqs["required_fields"]),
219
+ "details": "Transcript field validation"
220
+ })
221
+
222
+ # Validate podcast output
223
+ podcast_reqs = test_case["expected_output"]["podcast_requirements"]
224
+ if "final_podcast" in actual_output:
225
+ podcast = actual_output["final_podcast"]
226
+ validation_results["validations"].append({
227
+ "check": "podcast_fields",
228
+ "passed": all(field in podcast for field in podcast_reqs["required_fields"]),
229
+ "details": "Podcast field validation"
230
+ })
231
+
232
+ return validation_results
233
+
234
+ if __name__ == "__main__":
235
+ # Generate the golden dataset
236
+ dataset = generate_golden_dataset()
237
+ print("\nGolden Dataset Summary:")
238
+ print(f"Number of test cases: {len(dataset['test_cases'])}")
239
+ print(f"Number of sample transcripts: {len(dataset['sample_transcripts'])}")
240
+ print(f"Categories covered: {dataset['metadata']['categories']}")
241
+ print(f"Validation rules defined: {list(dataset['validation_rules'].keys())}")
server/main.py CHANGED
@@ -42,36 +42,19 @@ chat_model = ChatOpenAI(
42
  # Initialize FastAPI app
43
  app = FastAPI()
44
 
45
- # Get allowed origins from environment variable or use default list
46
- ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "").split(",")
47
- if not ALLOWED_ORIGINS:
48
- ALLOWED_ORIGINS = [
49
- "http://localhost:5173",
50
- "http://localhost:3000",
51
- "https://*.vercel.app", # Allow all Vercel preview and production domains
52
- ]
53
-
54
- # Configure CORS for frontend servers
55
  app.add_middleware(
56
  CORSMiddleware,
57
- allow_origins=ALLOWED_ORIGINS,
58
- allow_origin_regex=r"https://.*\.vercel\.app$", # Allow all Vercel domains
59
  allow_credentials=True,
60
  allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"],
61
- allow_headers=["*"],
62
- expose_headers=["*"],
63
- max_age=3600,
64
  )
65
 
66
- # Add security headers middleware
67
- @app.middleware("http")
68
- async def add_security_headers(request, call_next):
69
- response = await call_next(request)
70
- response.headers["X-Content-Type-Options"] = "nosniff"
71
- response.headers["X-Frame-Options"] = "DENY"
72
- response.headers["X-XSS-Protection"] = "1; mode=block"
73
- response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
74
- return response
75
 
76
  # Configure audio storage
77
  audio_dir = os.path.join(os.path.dirname(__file__), "audio_storage")
 
42
  # Initialize FastAPI app
43
  app = FastAPI()
44
 
45
+ # Configure CORS for frontend development server
 
 
 
 
 
 
 
 
 
46
  app.add_middleware(
47
  CORSMiddleware,
48
+ allow_origins=["http://localhost:5173", "http://localhost:3000"],
 
49
  allow_credentials=True,
50
  allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"],
51
+ allow_headers=["Content-Type", "Authorization", "Accept"],
52
+ expose_headers=["Content-Type", "Content-Length"],
53
+ max_age=600,
54
  )
55
 
56
+ # Mount static files for React frontend
57
+ app.mount("/", StaticFiles(directory="static", html=True), name="frontend")
 
 
 
 
 
 
 
58
 
59
  # Configure audio storage
60
  audio_dir = os.path.join(os.path.dirname(__file__), "audio_storage")
server/run_golden_tests.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import os
4
+ from datetime import datetime
5
+ from test_workflow import run_workflow
6
+ from workflow import create_workflow
7
+ from generate_test_dataset import GOLDEN_DATASET_DIR, validate_test_case
8
+ from dotenv import load_dotenv
9
+
10
+ # Load environment variables
11
+ load_dotenv()
12
+
13
+ async def run_golden_tests():
14
+ """Run tests using the golden dataset."""
15
+
16
+ # Load the golden dataset
17
+ dataset_path = os.path.join(GOLDEN_DATASET_DIR, "golden_dataset.json")
18
+ if not os.path.exists(dataset_path):
19
+ print("Golden dataset not found. Generating new dataset...")
20
+ from generate_test_dataset import generate_golden_dataset
21
+ generate_golden_dataset()
22
+
23
+ with open(dataset_path, 'r') as f:
24
+ golden_dataset = json.load(f)
25
+
26
+ # Initialize workflow
27
+ workflow = create_workflow(os.getenv("TAVILY_API_KEY"))
28
+
29
+ # Store test results
30
+ test_results = {
31
+ "metadata": {
32
+ "timestamp": datetime.now().isoformat(),
33
+ "dataset_version": golden_dataset["metadata"]["version"]
34
+ },
35
+ "results": []
36
+ }
37
+
38
+ # Run tests for each test case
39
+ for test_case in golden_dataset["test_cases"]:
40
+ print(f"\nRunning test case: {test_case['input']['query']}")
41
+ try:
42
+ # Run the workflow
43
+ result = await run_workflow(
44
+ workflow,
45
+ test_case["input"]["query"],
46
+ agent_type=test_case["input"]["agent_type"],
47
+ context=test_case["input"]["context"]
48
+ )
49
+
50
+ # Validate the results
51
+ validation_result = validate_test_case(test_case, result)
52
+
53
+ # Add results
54
+ test_results["results"].append({
55
+ "test_case_id": test_case["id"],
56
+ "query": test_case["input"]["query"],
57
+ "success": all(v["passed"] for v in validation_result["validations"]),
58
+ "validation_results": validation_result,
59
+ "workflow_output": result
60
+ })
61
+
62
+ # Print progress
63
+ success = all(v["passed"] for v in validation_result["validations"])
64
+ status = "✅ Passed" if success else "❌ Failed"
65
+ print(f"{status} - {test_case['input']['query']}")
66
+
67
+ except Exception as e:
68
+ print(f"❌ Error running test case: {str(e)}")
69
+ test_results["results"].append({
70
+ "test_case_id": test_case["id"],
71
+ "query": test_case["input"]["query"],
72
+ "success": False,
73
+ "error": str(e)
74
+ })
75
+
76
+ # Save test results
77
+ results_dir = os.path.join(GOLDEN_DATASET_DIR, "results")
78
+ os.makedirs(results_dir, exist_ok=True)
79
+
80
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
81
+ output_file = os.path.join(results_dir, f"test_results_{timestamp}.json")
82
+
83
+ with open(output_file, "w") as f:
84
+ json.dump(test_results, f, indent=2)
85
+
86
+ # Print summary
87
+ total_tests = len(test_results["results"])
88
+ passed_tests = sum(1 for r in test_results["results"] if r.get("success", False))
89
+
90
+ print("\n" + "="*50)
91
+ print("Test Summary:")
92
+ print(f"Total Tests: {total_tests}")
93
+ print(f"Passed: {passed_tests}")
94
+ print(f"Failed: {total_tests - passed_tests}")
95
+ print(f"Success Rate: {(passed_tests/total_tests)*100:.2f}%")
96
+ print("="*50)
97
+ print(f"\nDetailed results saved to: {output_file}")
98
+
99
+ if __name__ == "__main__":
100
+ print("\n" + "="*50)
101
+ print("🧪 Running Golden Dataset Tests")
102
+ print("="*50)
103
+
104
+ try:
105
+ asyncio.run(run_golden_tests())
106
+ except Exception as e:
107
+ print(f"\n❌ Critical error: {str(e)}")
108
+ raise
server/test_data/golden_dataset.json ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "metadata": {
3
+ "created_at": "2025-02-23T19:40:05.819936",
4
+ "version": "1.0",
5
+ "description": "Golden dataset for testing the podcast debate system"
6
+ },
7
+ "test_cases": [
8
+ {
9
+ "id": "6e557a71-7ccf-497c-8b41-b47087dae3ce",
10
+ "input": {
11
+ "query": "Are electric vehicles better for the environment?",
12
+ "agent_type": "believer",
13
+ "context": null
14
+ },
15
+ "expected_output": {
16
+ "debate_structure": {
17
+ "min_turns": 2,
18
+ "max_turns": 4,
19
+ "required_agents": [
20
+ "extractor",
21
+ "believer",
22
+ "skeptic",
23
+ "supervisor"
24
+ ]
25
+ },
26
+ "transcript_requirements": {
27
+ "required_fields": [
28
+ "id",
29
+ "podcastScript",
30
+ "topic"
31
+ ],
32
+ "topic_match": true,
33
+ "min_script_length": 500
34
+ },
35
+ "podcast_requirements": {
36
+ "required_fields": [
37
+ "content",
38
+ "audio_file",
39
+ "title",
40
+ "description",
41
+ "category"
42
+ ],
43
+ "audio_format": "mp3",
44
+ "min_file_size": 1000
45
+ }
46
+ }
47
+ },
48
+ {
49
+ "id": "9c95f4df-fb84-4bc1-b80c-094998e58811",
50
+ "input": {
51
+ "query": "Should artificial intelligence be regulated?",
52
+ "agent_type": "skeptic",
53
+ "context": {
54
+ "believer_chunks": [
55
+ "AI can enhance productivity and innovation",
56
+ "AI systems can help solve complex problems"
57
+ ],
58
+ "skeptic_chunks": [
59
+ "AI poses risks to privacy and security",
60
+ "Unregulated AI could lead to job displacement"
61
+ ]
62
+ }
63
+ },
64
+ "expected_output": {
65
+ "debate_structure": {
66
+ "min_turns": 2,
67
+ "max_turns": 4,
68
+ "required_agents": [
69
+ "extractor",
70
+ "believer",
71
+ "skeptic",
72
+ "supervisor"
73
+ ]
74
+ },
75
+ "transcript_requirements": {
76
+ "required_fields": [
77
+ "id",
78
+ "podcastScript",
79
+ "topic"
80
+ ],
81
+ "topic_match": true,
82
+ "min_script_length": 500
83
+ },
84
+ "podcast_requirements": {
85
+ "required_fields": [
86
+ "content",
87
+ "audio_file",
88
+ "title",
89
+ "description",
90
+ "category"
91
+ ],
92
+ "audio_format": "mp3",
93
+ "min_file_size": 1000
94
+ }
95
+ }
96
+ },
97
+ {
98
+ "id": "a0233d38-9f22-4b6e-9c77-621dc7a2b637",
99
+ "input": {
100
+ "query": "What are the benefits and drawbacks of remote work?",
101
+ "agent_type": "believer",
102
+ "context": null
103
+ },
104
+ "expected_output": {
105
+ "debate_structure": {
106
+ "min_turns": 2,
107
+ "max_turns": 4,
108
+ "required_agents": [
109
+ "extractor",
110
+ "believer",
111
+ "skeptic",
112
+ "supervisor"
113
+ ]
114
+ },
115
+ "transcript_requirements": {
116
+ "required_fields": [
117
+ "id",
118
+ "podcastScript",
119
+ "topic"
120
+ ],
121
+ "topic_match": true,
122
+ "min_script_length": 500
123
+ },
124
+ "podcast_requirements": {
125
+ "required_fields": [
126
+ "content",
127
+ "audio_file",
128
+ "title",
129
+ "description",
130
+ "category"
131
+ ],
132
+ "audio_format": "mp3",
133
+ "min_file_size": 1000
134
+ }
135
+ }
136
+ }
137
+ ],
138
+ "sample_transcripts": [
139
+ {
140
+ "id": "4fb8bc46-dc2a-4e21-abfe-9cb3e1ca77ff",
141
+ "podcastScript": "**Podcast Script: Electric Vehicles and the Environment**\n\nHost: Welcome to our debate on the environmental impact of electric vehicles...\n\nSkeptic: While EVs reduce direct emissions, we must consider the environmental cost of battery production...\n\nBeliever: The long-term benefits of EVs in reducing carbon emissions far outweigh the initial production impact...",
142
+ "topic": "Are electric vehicles better for the environment?"
143
+ },
144
+ {
145
+ "id": "557a2b5c-8f59-46cc-930c-7d69818260d1",
146
+ "podcastScript": "**Podcast Script: AI Regulation Debate**\n\nHost: Today we're exploring the complex topic of AI regulation...\n\nSkeptic: Without proper oversight, AI development could lead to serious societal risks...\n\nBeliever: Smart regulation can help us harness AI's benefits while minimizing potential harm...",
147
+ "topic": "Should artificial intelligence be regulated?"
148
+ }
149
+ ],
150
+ "validation_rules": {
151
+ "debate": {
152
+ "required_agents": [
153
+ "extractor",
154
+ "believer",
155
+ "skeptic",
156
+ "supervisor"
157
+ ],
158
+ "min_debate_turns": 2,
159
+ "max_debate_turns": 4
160
+ },
161
+ "transcript": {
162
+ "required_fields": [
163
+ "id",
164
+ "podcastScript",
165
+ "topic"
166
+ ],
167
+ "min_script_length": 500
168
+ },
169
+ "podcast": {
170
+ "required_fields": [
171
+ "content",
172
+ "audio_file",
173
+ "title",
174
+ "description",
175
+ "category"
176
+ ],
177
+ "supported_audio_formats": [
178
+ "mp3"
179
+ ],
180
+ "min_file_size": 1000
181
+ }
182
+ }
183
+ }
start.sh ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Start nginx in background
4
+ nginx
5
+
6
+ # Start FastAPI application
7
+ uvicorn main:app --host 0.0.0.0 --port 7860