feat: implement AI conversation generation with Mistral

- Install Mistral AI SDK and required dependencies
- Create API endpoint for generating podcast conversations using structured data
- Generate conversations between two hosts with distinct personalities:
  * Host 1: Bubbly, excited, and enthusiastic
  * Host 2: Skeptical, sarcastic, and thoughtful
- Include emotional expressions in brackets like [giggles], [sarcastically]
- Use same language as scraped content (English, Italian, etc.)
- Update frontend to automatically generate conversation after scraping
- Remove mock data and unused variables
- Add proper error handling and loading states

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Rosario Moscato
2025-09-22 20:45:23 +02:00
parent d380d68555
commit 1b4bfd44e2
4 changed files with 243 additions and 76 deletions

114
package-lock.json generated
View File

@@ -8,16 +8,19 @@
"name": "ai-podcast",
"version": "0.1.0",
"dependencies": {
"@ai-sdk/mistral": "^2.0.15",
"@mendable/firecrawl-js": "^4.3.5",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"ai": "^5.0.48",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
"next": "15.5.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -32,6 +35,67 @@
"typescript": "^5"
}
},
"node_modules/@ai-sdk/gateway": {
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.25.tgz",
"integrity": "sha512-eI/6LLmn1tWFzuhjxgcPEqUFXwLjyRuGFrwkCoqLaTKe/qMYBEAV3iddnGUM0AV+Hp4NEykzP4ly5tibOLDMXw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.9"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4"
}
},
"node_modules/@ai-sdk/mistral": {
"version": "2.0.15",
"resolved": "https://registry.npmjs.org/@ai-sdk/mistral/-/mistral-2.0.15.tgz",
"integrity": "sha512-vaNNd+Ekjosizsm4r23UUZnMNY++3HtGadTZ/BcY6eCrIq9sk+KMNfu4HO7ojlcm0P4/kuPMyiq38Me46C7O+Q==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.9"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4"
}
},
"node_modules/@ai-sdk/provider": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz",
"integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/provider-utils": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.9.tgz",
"integrity": "sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@standard-schema/spec": "^1.0.0",
"eventsource-parser": "^3.0.5"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -982,6 +1046,15 @@
"node": ">=12.4.0"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -1091,6 +1164,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -2018,6 +2097,24 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/ai": {
"version": "5.0.48",
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.48.tgz",
"integrity": "sha512-+oYhbN3NGRXayGfTFI8k1Fu4rhiJcQ0mbgiAOJGFkzvCxunRRQu5cyDl7y6cHNTj1QvHmIBROK5u655Ss2oI0g==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/gateway": "1.0.25",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.9",
"@opentelemetry/api": "1.9.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3324,6 +3421,15 @@
"node": ">=0.10.0"
}
},
"node_modules/eventsource-parser": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4291,6 +4397,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",

View File

@@ -9,16 +9,19 @@
"lint": "eslint"
},
"dependencies": {
"@ai-sdk/mistral": "^2.0.15",
"@mendable/firecrawl-js": "^4.3.5",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"ai": "^5.0.48",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
"next": "15.5.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/eslintrc": "^3",

View File

@@ -0,0 +1,81 @@
import { NextRequest, NextResponse } from 'next/server';
import { generateObject } from 'ai';
import { mistral } from '@ai-sdk/mistral';
import { z } from 'zod';
const messageSchema = z.object({
id: z.string(),
speaker: z.enum(['host1', 'host2']),
text: z.string(),
timestamp: z.string(),
});
const conversationSchema = z.object({
messages: z.array(messageSchema),
detectedLanguage: z.string(),
});
export async function POST(request: NextRequest) {
try {
const { content, title, url } = await request.json();
if (!content) {
return NextResponse.json(
{ error: 'Content is required' },
{ status: 400 }
);
}
console.log('Generating conversation for:', { title, url, contentLength: content.length });
// Generate podcast conversation using Mistral
const { object } = await generateObject({
model: mistral('mistral-medium-latest'),
schema: conversationSchema,
schemaName: 'PodcastConversation',
schemaDescription: 'A podcast-style conversation between two hosts discussing scraped content',
prompt: `You are generating a podcast conversation between two hosts discussing the following scraped content from "${title}" at ${url}.
CONTENT:
${content}
Generate a natural, engaging podcast conversation with exactly 10 turns (5 per host). The conversation should:
1. HOST 1 PERSONALITY: Bubbly, excited, enthusiastic, and optimistic. Uses expressions like "Wow!", "Amazing!", "That's so cool!". Often laughs [giggles] and shows genuine excitement.
2. HOST 2 PERSONALITY: Skeptical, sarcastic, and thoughtful. Uses dry humor and irony. Often makes sarcastic comments [sarcastically] and plays devil's advocate.
3. Use the same language as the content (if content is in English, respond in English; if in Italian, respond in Italian, etc.)
4. Include emotional expressions in brackets like [giggles], [laughs], [sarcastically], [whispers], [excitedly], [thoughtfully], etc.
5. Make it sound natural and conversational, like a real podcast
6. Include timestamps in MM:SS format (starting from 0:15 and incrementing by 20-30 seconds each)
7. The conversation should flow naturally and cover the main points of the content
Format the response as a structured object with messages array and detected language.`,
temperature: 0.7,
maxTokens: 2000,
});
console.log('Conversation generated successfully');
return NextResponse.json({
success: true,
data: {
messages: object.messages,
detectedLanguage: object.detectedLanguage,
generatedAt: new Date().toISOString()
}
});
} catch (error) {
console.error('Conversation generation error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to generate conversation' },
{ status: 500 }
);
}
}

View File

@@ -28,7 +28,7 @@ export default function Home() {
excerpt: string;
scrapedAt: string;
}>>([]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
@@ -54,14 +54,9 @@ export default function Home() {
scrapedAt: result.data.scrapedAt
}, ...prev]);
// Show success message
setMessages([{
id: '1',
speaker: 'host1',
text: `Great! I've successfully scraped the content from "${result.data.title || 'the website'}". The content has been processed and is ready for podcast generation.`,
timestamp: '0:15'
}]);
setDuration(320);
// Generate conversation
await generateConversation(result.data.content, result.data.title || 'Untitled', result.data.url);
} else {
throw new Error(result.error || 'Failed to scrape website');
}
@@ -78,6 +73,39 @@ export default function Home() {
}
};
const generateConversation = async (content: string, title: string, url: string) => {
try {
const response = await fetch('/api/generate-conversation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content, title, url }),
});
const result = await response.json();
if (result.success) {
setMessages(result.data.messages);
// Calculate duration based on conversation length
const avgTimePerMessage = 25; // seconds
const totalDuration = result.data.messages.length * avgTimePerMessage;
setDuration(totalDuration);
} else {
throw new Error(result.error || 'Failed to generate conversation');
}
} catch (error) {
console.error('Conversation generation error:', error);
setMessages([{
id: '1',
speaker: 'host1',
text: 'Sorry, I encountered an error while generating the podcast conversation. Please try again.',
timestamp: '0:15'
}]);
}
};
const togglePlay = () => {
if (isPlaying) {
setIsPlaying(false);
@@ -149,7 +177,7 @@ export default function Home() {
className="w-full"
disabled={isLoading || !url}
>
{isLoading ? 'Generating...' : 'Generate Podcast'}
{isLoading ? 'Scraping & Generating...' : 'Generate Podcast'}
</Button>
</form>
@@ -185,7 +213,7 @@ export default function Home() {
{messages.length > 0 && (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-md">
<p className="text-sm text-green-800 dark:text-green-200">
Website scraped successfully! Content is ready for podcast generation.
Podcast conversation generated successfully!
</p>
</div>
)}
@@ -245,6 +273,12 @@ export default function Home() {
</div>
) : (
<div className="space-y-6">
<div className="text-center p-4 bg-blue-50 dark:bg-blue-900/20 rounded-md">
<p className="text-sm text-blue-800 dark:text-blue-200">
AI-generated conversation ready! Click play to listen.
</p>
</div>
{/* Audio Controls */}
<div className="flex justify-center items-center space-x-4">
<Button
@@ -324,66 +358,3 @@ function formatTime(seconds: number): string {
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'
}
];