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 { 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 CollapsibleSection from './CollapsibleSection';
|
||||
import StepHeader from './StepHeader';
|
||||
@ -29,6 +29,8 @@ export default function StepGenerate({
|
||||
}) {
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [useWebSearch, setUseWebSearch] = useState(false);
|
||||
const [sources, setSources] = useState<Array<{ title: string; url: string }>>([]);
|
||||
return (
|
||||
<Box sx={{ display: 'grid', gap: 2 }}>
|
||||
<StepHeader
|
||||
@ -61,6 +63,7 @@ export default function StepGenerate({
|
||||
|
||||
{/* Prompt */}
|
||||
<CollapsibleSection title="AI Prompt">
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label="Instructions + context for AI generation"
|
||||
value={promptText}
|
||||
@ -70,6 +73,23 @@ export default function StepGenerate({
|
||||
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>
|
||||
|
||||
{/* Generate Button */}
|
||||
@ -96,10 +116,12 @@ export default function StepGenerate({
|
||||
prompt: promptText,
|
||||
audioTranscriptions: transcriptions.length > 0 ? transcriptions : undefined,
|
||||
selectedImageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
||||
useWebSearch,
|
||||
});
|
||||
|
||||
onGeneratedDraft(result.content);
|
||||
onImagePlaceholders(result.imagePlaceholders);
|
||||
setSources(result.sources || []);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Generation failed');
|
||||
} finally {
|
||||
@ -127,6 +149,18 @@ export default function StepGenerate({
|
||||
{generatedDraft && (
|
||||
<CollapsibleSection title="Generated Draft">
|
||||
<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 && (
|
||||
<Alert severity="info">
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', mb: 0.5 }}>Image Placeholders Detected:</Typography>
|
||||
|
||||
@ -2,6 +2,7 @@ export async function generateDraft(payload: {
|
||||
prompt: string;
|
||||
audioTranscriptions?: string[];
|
||||
selectedImageUrls?: string[];
|
||||
useWebSearch?: boolean;
|
||||
}) {
|
||||
const res = await fetch('/api/ai/generate', {
|
||||
method: 'POST',
|
||||
@ -14,6 +15,7 @@ export async function generateDraft(payload: {
|
||||
imagePlaceholders: string[];
|
||||
tokensUsed: number;
|
||||
model: string;
|
||||
sources?: Array<{ title: string; url: string }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
@ -33,10 +33,12 @@ router.post('/generate', async (req, res) => {
|
||||
prompt,
|
||||
audioTranscriptions,
|
||||
selectedImageUrls,
|
||||
useWebSearch = false,
|
||||
} = req.body as {
|
||||
prompt: string;
|
||||
audioTranscriptions?: string[];
|
||||
selectedImageUrls?: string[];
|
||||
useWebSearch?: boolean;
|
||||
};
|
||||
|
||||
if (!prompt) {
|
||||
@ -87,9 +89,13 @@ router.post('/generate', async (req, res) => {
|
||||
|
||||
console.log('[AI Generate] Starting generation with OpenAI...');
|
||||
console.log('[AI Generate] User prompt length:', userPrompt.length);
|
||||
console.log('[AI Generate] Web search enabled:', useWebSearch);
|
||||
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: 'gpt-4o', // Using GPT-4 with internet access capability
|
||||
// Choose model based on web search requirement
|
||||
const model = useWebSearch ? 'gpt-4o-mini-search-preview-2025-03-11' : 'gpt-4o';
|
||||
|
||||
const completionParams: any = {
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@ -100,9 +106,18 @@ router.post('/generate', async (req, res) => {
|
||||
content: userPrompt,
|
||||
},
|
||||
],
|
||||
temperature: 0.7,
|
||||
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 || '';
|
||||
|
||||
@ -120,11 +135,27 @@ router.post('/generate', async (req, res) => {
|
||||
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({
|
||||
content: generatedContent,
|
||||
imagePlaceholders,
|
||||
tokensUsed: completion.usage?.total_tokens || 0,
|
||||
model: completion.model,
|
||||
sources: sources.length > 0 ? sources : undefined,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('[AI Generate] Error:', err);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user