feat: add web search capability with source citations for AI content generation

This commit is contained in:
Ender 2025-10-25 13:28:36 +02:00
parent d6b4109b22
commit 4d1f8298de
3 changed files with 81 additions and 14 deletions

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Box, Stack, TextField, Typography, Button, Alert, CircularProgress } from '@mui/material'; import { Box, Stack, TextField, Typography, Button, Alert, CircularProgress, FormControlLabel, Checkbox, Link } from '@mui/material';
import SelectedImages from './SelectedImages'; import SelectedImages from './SelectedImages';
import CollapsibleSection from './CollapsibleSection'; import CollapsibleSection from './CollapsibleSection';
import StepHeader from './StepHeader'; import StepHeader from './StepHeader';
@ -29,6 +29,8 @@ export default function StepGenerate({
}) { }) {
const [generating, setGenerating] = useState(false); const [generating, setGenerating] = useState(false);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [useWebSearch, setUseWebSearch] = useState(false);
const [sources, setSources] = useState<Array<{ title: string; url: string }>>([]);
return ( return (
<Box sx={{ display: 'grid', gap: 2 }}> <Box sx={{ display: 'grid', gap: 2 }}>
<StepHeader <StepHeader
@ -61,6 +63,7 @@ export default function StepGenerate({
{/* Prompt */} {/* Prompt */}
<CollapsibleSection title="AI Prompt"> <CollapsibleSection title="AI Prompt">
<Stack spacing={2}>
<TextField <TextField
label="Instructions + context for AI generation" label="Instructions + context for AI generation"
value={promptText} value={promptText}
@ -70,6 +73,23 @@ export default function StepGenerate({
minRows={4} minRows={4}
placeholder="Example: Write a comprehensive technical article about building a modern blog platform. Include sections on architecture, key features, and deployment. Target audience: developers with React experience." placeholder="Example: Write a comprehensive technical article about building a modern blog platform. Include sections on architecture, key features, and deployment. Target audience: developers with React experience."
/> />
<FormControlLabel
control={
<Checkbox
checked={useWebSearch}
onChange={(e) => setUseWebSearch(e.target.checked)}
/>
}
label={
<Box>
<Typography variant="body2">Research with web search (gpt-4o-mini-search)</Typography>
<Typography variant="caption" color="text.secondary">
AI will search the internet for current information, facts, and statistics
</Typography>
</Box>
}
/>
</Stack>
</CollapsibleSection> </CollapsibleSection>
{/* Generate Button */} {/* Generate Button */}
@ -96,10 +116,12 @@ export default function StepGenerate({
prompt: promptText, prompt: promptText,
audioTranscriptions: transcriptions.length > 0 ? transcriptions : undefined, audioTranscriptions: transcriptions.length > 0 ? transcriptions : undefined,
selectedImageUrls: imageUrls.length > 0 ? imageUrls : undefined, selectedImageUrls: imageUrls.length > 0 ? imageUrls : undefined,
useWebSearch,
}); });
onGeneratedDraft(result.content); onGeneratedDraft(result.content);
onImagePlaceholders(result.imagePlaceholders); onImagePlaceholders(result.imagePlaceholders);
setSources(result.sources || []);
} catch (err: any) { } catch (err: any) {
setError(err?.message || 'Generation failed'); setError(err?.message || 'Generation failed');
} finally { } finally {
@ -127,6 +149,18 @@ export default function StepGenerate({
{generatedDraft && ( {generatedDraft && (
<CollapsibleSection title="Generated Draft"> <CollapsibleSection title="Generated Draft">
<Stack spacing={2}> <Stack spacing={2}>
{sources.length > 0 && (
<Alert severity="success">
<Typography variant="body2" sx={{ fontWeight: 'bold', mb: 0.5 }}>Sources ({sources.length}):</Typography>
<Stack spacing={0.5}>
{sources.map((source, idx) => (
<Typography key={idx} variant="caption" component="div">
{idx + 1}. <Link href={source.url} target="_blank" rel="noopener noreferrer">{source.title}</Link>
</Typography>
))}
</Stack>
</Alert>
)}
{imagePlaceholders.length > 0 && ( {imagePlaceholders.length > 0 && (
<Alert severity="info"> <Alert severity="info">
<Typography variant="body2" sx={{ fontWeight: 'bold', mb: 0.5 }}>Image Placeholders Detected:</Typography> <Typography variant="body2" sx={{ fontWeight: 'bold', mb: 0.5 }}>Image Placeholders Detected:</Typography>

View File

@ -2,6 +2,7 @@ export async function generateDraft(payload: {
prompt: string; prompt: string;
audioTranscriptions?: string[]; audioTranscriptions?: string[];
selectedImageUrls?: string[]; selectedImageUrls?: string[];
useWebSearch?: boolean;
}) { }) {
const res = await fetch('/api/ai/generate', { const res = await fetch('/api/ai/generate', {
method: 'POST', method: 'POST',
@ -14,6 +15,7 @@ export async function generateDraft(payload: {
imagePlaceholders: string[]; imagePlaceholders: string[];
tokensUsed: number; tokensUsed: number;
model: string; model: string;
sources?: Array<{ title: string; url: string }>;
}>; }>;
} }

View File

@ -33,10 +33,12 @@ router.post('/generate', async (req, res) => {
prompt, prompt,
audioTranscriptions, audioTranscriptions,
selectedImageUrls, selectedImageUrls,
useWebSearch = false,
} = req.body as { } = req.body as {
prompt: string; prompt: string;
audioTranscriptions?: string[]; audioTranscriptions?: string[];
selectedImageUrls?: string[]; selectedImageUrls?: string[];
useWebSearch?: boolean;
}; };
if (!prompt) { if (!prompt) {
@ -87,9 +89,13 @@ router.post('/generate', async (req, res) => {
console.log('[AI Generate] Starting generation with OpenAI...'); console.log('[AI Generate] Starting generation with OpenAI...');
console.log('[AI Generate] User prompt length:', userPrompt.length); console.log('[AI Generate] User prompt length:', userPrompt.length);
console.log('[AI Generate] Web search enabled:', useWebSearch);
const completion = await openai.chat.completions.create({ // Choose model based on web search requirement
model: 'gpt-4o', // Using GPT-4 with internet access capability const model = useWebSearch ? 'gpt-4o-mini-search-preview-2025-03-11' : 'gpt-4o';
const completionParams: any = {
model,
messages: [ messages: [
{ {
role: 'system', role: 'system',
@ -100,9 +106,18 @@ router.post('/generate', async (req, res) => {
content: userPrompt, content: userPrompt,
}, },
], ],
temperature: 0.7,
max_tokens: 4000, max_tokens: 4000,
}); };
// Add web search options if enabled
if (useWebSearch) {
completionParams.web_search_options = {};
} else {
// Only add temperature for non-search models
completionParams.temperature = 0.7;
}
const completion = await openai.chat.completions.create(completionParams);
const generatedContent = completion.choices[0]?.message?.content || ''; const generatedContent = completion.choices[0]?.message?.content || '';
@ -120,11 +135,27 @@ router.post('/generate', async (req, res) => {
imagePlaceholders.push(match[1]); imagePlaceholders.push(match[1]);
} }
// Extract source citations if web search was used
const sources: Array<{ title: string; url: string }> = [];
if (useWebSearch && completion.choices[0]?.message?.annotations) {
const annotations = completion.choices[0].message.annotations as any[];
for (const annotation of annotations) {
if (annotation.type === 'url_citation' && annotation.url_citation) {
sources.push({
title: annotation.url_citation.title || 'Source',
url: annotation.url_citation.url,
});
}
}
console.log('[AI Generate] Found', sources.length, 'source citations');
}
return res.json({ return res.json({
content: generatedContent, content: generatedContent,
imagePlaceholders, imagePlaceholders,
tokensUsed: completion.usage?.total_tokens || 0, tokensUsed: completion.usage?.total_tokens || 0,
model: completion.model, model: completion.model,
sources: sources.length > 0 ? sources : undefined,
}); });
} catch (err: any) { } catch (err: any) {
console.error('[AI Generate] Error:', err); console.error('[AI Generate] Error:', err);