feat: add web search capability with source citations for AI content generation
This commit is contained in:
parent
d6b4109b22
commit
4d1f8298de
@ -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,15 +63,33 @@ export default function StepGenerate({
|
|||||||
|
|
||||||
{/* Prompt */}
|
{/* Prompt */}
|
||||||
<CollapsibleSection title="AI Prompt">
|
<CollapsibleSection title="AI Prompt">
|
||||||
<TextField
|
<Stack spacing={2}>
|
||||||
label="Instructions + context for AI generation"
|
<TextField
|
||||||
value={promptText}
|
label="Instructions + context for AI generation"
|
||||||
onChange={(e) => onChangePrompt(e.target.value)}
|
value={promptText}
|
||||||
fullWidth
|
onChange={(e) => onChangePrompt(e.target.value)}
|
||||||
multiline
|
fullWidth
|
||||||
minRows={4}
|
multiline
|
||||||
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."
|
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."
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
|||||||
@ -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 }>;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user