feat: implement AI Podcast Generator UI with 3-column layout

- Add responsive 3-column design similar to NotebookLM
- Implement URL input form with loading states and validation
- Create conversation display between two hosts (Alex & Sarah)
- Build functional audio player with play/pause/restart controls
- Add progress bar, volume control, and episode information
- Include mock conversation data with 10 rounds of dialogue
- Implement hover effects and smooth transitions
- Add dark mode support and custom scrollbar styling
- Update metadata and application title

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Rosario Moscato
2025-09-21 19:25:20 +02:00
parent 198fdb01ab
commit 8a50a03a2f
3 changed files with 325 additions and 97 deletions

View File

@@ -3,11 +3,15 @@
:root {
--background: #ffffff;
--foreground: #171717;
--card: #ffffff;
--card-foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@@ -16,6 +20,8 @@
:root {
--background: #0a0a0a;
--foreground: #ededed;
--card: #0a0a0a;
--card-foreground: #ededed;
}
}
@@ -24,3 +30,21 @@ body {
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
/* Custom scrollbar for conversation area */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #666;
}

View File

@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "AI Podcast Generator",
description: "Convert any URL into a natural sounding podcast audio file",
};
export default function RootLayout({

View File

@@ -1,103 +1,307 @@
import Image from "next/image";
'use client';
import { useState } from 'react';
interface Message {
id: string;
speaker: 'host1' | 'host2';
text: string;
timestamp: string;
}
export default function Home() {
return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [messages, setMessages] = useState<Message[]>([]);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [progressInterval, setProgressInterval] = useState<NodeJS.Timeout | null>(null);
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
// Simulate API call
setTimeout(() => {
setMessages(mockMessages);
setIsLoading(false);
setDuration(320); // 5 minutes 20 seconds
}, 2000);
};
const togglePlay = () => {
if (isPlaying) {
setIsPlaying(false);
if (progressInterval) {
clearInterval(progressInterval);
setProgressInterval(null);
}
} else {
setIsPlaying(true);
const interval = setInterval(() => {
setCurrentTime(prev => {
if (prev >= duration) {
setIsPlaying(false);
clearInterval(interval);
setProgressInterval(null);
return duration;
}
return prev + 1;
});
}, 1000);
setProgressInterval(interval);
}
};
const restartAudio = () => {
setCurrentTime(0);
setIsPlaying(false);
if (progressInterval) {
clearInterval(progressInterval);
setProgressInterval(null);
}
};
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b border-gray-200 dark:border-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<h1 className="text-2xl font-bold text-foreground">AI Podcast Generator</h1>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4 lg:gap-6 h-[calc(100vh-10rem)] sm:h-[calc(100vh-12rem)]">
{/* Left Column - URL Input */}
<div className="xl:col-span-1">
<div className="bg-card border border-gray-200 dark:border-gray-800 rounded-lg p-6 h-full">
<h2 className="text-lg font-semibold mb-4">Generate Podcast</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="url" className="block text-sm font-medium mb-2">
Enter Website URL
</label>
<input
type="url"
id="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<button
type="submit"
disabled={isLoading || !url}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? 'Generating...' : 'Generate Podcast'}
</button>
</form>
{messages.length > 0 && (
<div className="mt-6 p-4 bg-green-50 dark:bg-green-900/20 rounded-md">
<p className="text-sm text-green-800 dark:text-green-200">
Podcast generated successfully! Listen to the conversation below.
</p>
</div>
)}
</div>
</div>
{/* Middle Column - Conversation */}
<div className="xl:col-span-1">
<div className="bg-card border border-gray-200 dark:border-gray-800 rounded-lg p-6 h-full flex flex-col">
<h2 className="text-lg font-semibold mb-4">Podcast Conversation</h2>
<div className="flex-1 overflow-y-auto space-y-4">
{messages.length === 0 ? (
<div className="text-center text-gray-500 mt-8">
<p>Enter a URL and generate a podcast to see the conversation here.</p>
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={`flex ${message.speaker === 'host1' ? 'justify-start' : 'justify-end'}`}
>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 transition-all duration-200 hover:scale-[1.02] ${
message.speaker === 'host1'
? 'bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-900/50'
: 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
<div className="font-medium text-sm mb-1">
{message.speaker === 'host1' ? 'Alex' : 'Sarah'}
</div>
<p className="text-sm">{message.text}</p>
<div className="text-xs text-gray-500 mt-1">{message.timestamp}</div>
</div>
</div>
))
)}
</div>
</div>
</div>
{/* Right Column - Audio Player */}
<div className="xl:col-span-1">
<div className="bg-card border border-gray-200 dark:border-gray-800 rounded-lg p-6 h-full">
<h2 className="text-lg font-semibold mb-4">Audio Player</h2>
{messages.length === 0 ? (
<div className="text-center text-gray-500 mt-8">
<p>Generate a podcast to enable audio playback.</p>
</div>
) : (
<div className="space-y-6">
{/* Audio Controls */}
<div className="flex justify-center items-center space-x-4">
<button
onClick={restartAudio}
className="p-2 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all duration-200 hover:scale-110"
title="Restart"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<button
onClick={togglePlay}
className="p-3 rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-all duration-200 hover:scale-110 shadow-lg hover:shadow-xl"
title={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
) : (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
</button>
</div>
{/* Progress Bar */}
<div className="space-y-2">
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${(currentTime / duration) * 100}%` }}
/>
</div>
<div className="flex justify-between text-sm text-gray-500">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
{/* Volume Control */}
<div className="flex items-center space-x-2">
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072M12 6a9 9 0 010 12m-5.5-7.5h.01M4 12h.01" />
</svg>
<input
type="range"
min="0"
max="100"
defaultValue="70"
className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer"
/>
</div>
{/* Episode Info */}
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="font-medium mb-2">Episode Details</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Duration: {formatTime(duration)}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Speakers: Alex & Sarah
</p>
</div>
</div>
)}
</div>
</div>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}
// Helper function to format time
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Mock conversation data
const mockMessages: Message[] = [
{
id: '1',
speaker: 'host1',
text: "Hey Sarah! Today we're discussing this fascinating article about artificial intelligence and its impact on creative industries.",
timestamp: '0:15'
},
{
id: '2',
speaker: 'host2',
text: "That's really interesting, Alex! I've been following this topic closely. The article mentions some really compelling points about AI-generated art and music.",
timestamp: '0:45'
},
{
id: '3',
speaker: 'host1',
text: "Exactly! What I found most surprising was the section about how AI is actually helping human creators become more productive rather than replacing them entirely.",
timestamp: '1:20'
},
{
id: '4',
speaker: 'host2',
text: "That's a crucial distinction. The article highlights several case studies where artists are using AI as a collaborative tool to enhance their creative process.",
timestamp: '1:55'
},
{
id: '5',
speaker: 'host1',
text: "I particularly loved the example about the musician who used AI to generate backing tracks and then added their own creative twist on top.",
timestamp: '2:30'
},
{
id: '6',
speaker: 'host2',
text: "And what about the ethical considerations? The article raises some important questions about copyright and attribution when AI is involved in the creative process.",
timestamp: '3:05'
},
{
id: '7',
speaker: 'host1',
text: "That's definitely something we need to think about. The author suggests that we might need new frameworks for understanding creativity in the age of AI.",
timestamp: '3:40'
},
{
id: '8',
speaker: 'host2',
text: "I agree. It's not about rejecting AI, but finding ways to integrate it responsibly into our creative workflows while maintaining human oversight and artistic vision.",
timestamp: '4:15'
},
{
id: '9',
speaker: 'host1',
text: "The future looks really exciting! Imagine what we'll be able to create when we embrace these technologies as tools rather than threats.",
timestamp: '4:50'
},
{
id: '10',
speaker: 'host2',
text: "Absolutely! Thanks for sharing this article with our listeners. It really gives us a lot to think about regarding the future of creativity and AI.",
timestamp: '5:20'
}
];