Video_AdGenesis_App / frontend /src /components /SavedPromptsLibrary.tsx
sushilideaclan01's picture
Enhance prompt validation and safety features
82a1419
import { useEffect, useState } from 'react';
const API_BASE = import.meta.env.VITE_API_BASE_URL || '';
interface CachedPrompt {
prompt_id: string;
created_at: string;
updated_at: string;
metadata: {
script: string;
style: string;
model: string;
segments_count: number;
};
segments_count: number;
}
export function SavedPromptsLibrary({ onClose, onReuse }: {
onClose: () => void;
onReuse: (payload: any) => void;
}) {
const [prompts, setPrompts] = useState<CachedPrompt[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [editingPromptId, setEditingPromptId] = useState<string | null>(null);
const [editingPayloadJson, setEditingPayloadJson] = useState('');
const [editError, setEditError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState<{
valid: boolean;
schema_errors?: string[];
ai_checked?: boolean;
ai_valid?: boolean;
ai_warnings?: string[];
ai_suggestions?: string[];
} | null>(null);
useEffect(() => {
loadSavedPrompts();
}, []);
const loadSavedPrompts = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`${API_BASE}/api/cached-prompts?limit=50`);
if (!response.ok) throw new Error('Failed to load prompts');
const data = await response.json();
const list: CachedPrompt[] = data.prompts || [];
// Deduplicate: same script + same segment count = same prompt; keep most recent
const seen = new Map<string, CachedPrompt>();
const key = (p: CachedPrompt) => {
const script = (p.metadata?.script || '').trim().slice(0, 300);
return `${script}|${p.segments_count ?? p.metadata?.segments_count ?? 0}`;
};
for (const p of list) {
const k = key(p);
const existing = seen.get(k);
if (!existing || new Date(p.updated_at) > new Date(existing.updated_at)) {
seen.set(k, p);
}
}
setPrompts(Array.from(seen.values()));
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load prompts');
} finally {
setLoading(false);
}
};
const handleReuse = async (promptId: string) => {
try {
const response = await fetch(`${API_BASE}/api/use-cached-prompt/${promptId}`, {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to load prompt');
const { payload } = await response.json();
onReuse(payload);
onClose();
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to reuse prompt');
}
};
const handleDelete = async (promptId: string) => {
if (!confirm('Delete this saved prompt?')) return;
try {
await fetch(`${API_BASE}/api/cached-prompts/${promptId}`, { method: 'DELETE' });
loadSavedPrompts(); // Refresh list
} catch (err) {
alert('Failed to delete prompt');
}
};
const handleEdit = async (promptId: string) => {
setEditError(null);
try {
const response = await fetch(`${API_BASE}/api/cached-prompts/${promptId}`);
if (!response.ok) throw new Error('Failed to load prompt');
const entry = await response.json();
const payload = entry.payload ?? { segments: entry.segments ?? [] };
setEditingPayloadJson(JSON.stringify(payload, null, 2));
setEditingPromptId(promptId);
} catch (err) {
setEditError(err instanceof Error ? err.message : 'Failed to load prompt');
}
};
const validatePayload = (): { valid: boolean; payload?: any; error?: string } => {
try {
const parsed = JSON.parse(editingPayloadJson);
if (!parsed || typeof parsed !== 'object') return { valid: false, error: 'Payload must be an object' };
if (!Array.isArray(parsed.segments)) return { valid: false, error: 'Payload must have a "segments" array' };
return { valid: true, payload: parsed };
} catch {
return { valid: false, error: 'Invalid JSON' };
}
};
const handleValidateWithAi = async () => {
const { valid, payload, error } = validatePayload();
if (!valid || !payload) {
setEditError(error ?? 'Invalid payload');
setValidationResult(null);
return;
}
setEditError(null);
setValidationResult(null);
setValidating(true);
try {
const response = await fetch(`${API_BASE}/api/validate-payload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payload, use_ai: true }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.detail || response.statusText || 'Validation failed');
setValidationResult({
valid: data.valid,
schema_errors: data.schema_errors,
ai_checked: data.ai_checked,
ai_valid: data.ai_valid,
ai_warnings: data.ai_warnings,
ai_suggestions: data.ai_suggestions,
});
} catch (err) {
setEditError(err instanceof Error ? err.message : 'Validation request failed');
} finally {
setValidating(false);
}
};
const handleSaveEdit = async () => {
const { valid, payload, error } = validatePayload();
if (!valid || !payload || !editingPromptId) {
setEditError(error ?? 'Invalid payload');
return;
}
setEditError(null);
setSaving(true);
try {
const response = await fetch(`${API_BASE}/api/cached-prompts/${editingPromptId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw new Error(errData.detail || response.statusText || 'Failed to update');
}
setEditingPromptId(null);
setEditingPayloadJson('');
loadSavedPrompts();
} catch (err) {
setEditError(err instanceof Error ? err.message : 'Failed to save');
} finally {
setSaving(false);
}
};
const closeEditModal = () => {
setEditingPromptId(null);
setEditingPayloadJson('');
setEditError(null);
setValidationResult(null);
};
return (
<div className="fixed inset-0 bg-void-950/80 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-void-900/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-void-700/50 max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="p-6 border-b border-void-700/50 flex justify-between items-center">
<h2 className="text-2xl font-bold text-void-100">💾 My Saved Prompts</h2>
<button
onClick={onClose}
className="text-void-400 hover:text-void-100 text-2xl font-light leading-none transition-colors"
>
×
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-2 border-void-600 border-t-coral-500" />
</div>
) : error ? (
<div className="text-center py-12">
<p className="text-coral-400 mb-4">{error}</p>
<button
onClick={loadSavedPrompts}
className="px-4 py-2 bg-coral-500/20 text-coral-400 rounded-xl border border-coral-500/50 hover:bg-coral-500/30 transition-colors"
>
Retry
</button>
</div>
) : prompts.length === 0 ? (
<div className="text-center py-12">
<div className="text-6xl mb-4 opacity-60">📭</div>
<p className="text-void-300 mb-2">No saved prompts yet</p>
<p className="text-sm text-void-500">Generate some prompts to see them here</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{prompts.map((prompt) => (
<div
key={prompt.prompt_id}
className="card hover:border-void-600/50 hover:bg-void-800/40 transition-all duration-200"
>
{/* Header */}
<div className="flex justify-between items-start mb-3">
<span className="text-xs text-void-500">
{new Date(prompt.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})}
</span>
<span className="px-2 py-1 bg-electric-500/20 text-electric-400 text-xs rounded-full font-medium border border-electric-500/30">
{prompt.segments_count} segments
</span>
</div>
{/* Content Preview */}
<div className="mb-4">
<p className="text-sm text-void-200 line-clamp-3 mb-2">
{(prompt.metadata?.script || '').slice(0, 120)}
{(prompt.metadata?.script || '').length > 120 ? '…' : ''}
</p>
<span className="inline-block px-2 py-1 bg-void-800 text-void-300 text-xs rounded-lg border border-void-600/50">
{prompt.metadata?.style || '—'}
</span>
</div>
{/* Actions */}
<div className="flex gap-2">
<button
onClick={() => handleReuse(prompt.prompt_id)}
className="flex-1 px-3 py-2 bg-coral-500/20 text-coral-400 rounded-xl border border-coral-500/50 hover:bg-coral-500/30 text-sm font-medium transition-colors"
>
♻️ Reuse
</button>
<button
onClick={() => handleEdit(prompt.prompt_id)}
className="px-3 py-2 bg-void-800 text-void-300 rounded-xl border border-void-600 hover:bg-void-700 hover:text-void-100 text-sm transition-colors"
title="Edit prompt"
>
✏️
</button>
<button
onClick={() => handleDelete(prompt.prompt_id)}
className="px-3 py-2 bg-void-800/80 text-red-400 rounded-xl border border-void-600 hover:bg-red-500/10 hover:border-red-500/40 text-sm transition-colors"
>
🗑️
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-void-700/50">
<button
onClick={onClose}
className="w-full py-2.5 bg-void-800 text-void-200 rounded-xl border border-void-600 hover:bg-void-700 transition-colors"
>
Close
</button>
</div>
</div>
{/* Edit modal - dark chrome to match app; light body so JSON is readable */}
{editingPromptId && (
<div className="fixed inset-0 bg-void-950/90 backdrop-blur-sm flex items-center justify-center p-4 z-[60]">
<div className="bg-void-900 rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col border border-void-600">
<div className="p-4 border-b border-void-600 flex justify-between items-center">
<h3 className="text-lg font-bold text-void-100">✏️ Edit saved prompt</h3>
<button onClick={closeEditModal} className="text-void-400 hover:text-void-100 text-2xl font-bold leading-none transition-colors">×</button>
</div>
<div className="flex-1 overflow-hidden flex flex-col p-4 bg-void-800/30">
{editError && (
<p className="text-coral-400 text-sm mb-2 font-medium">{editError}</p>
)}
{validationResult && (
<div className="mb-3 p-3 rounded-xl bg-void-900/80 border border-void-600 text-sm">
{!validationResult.valid && validationResult.schema_errors && validationResult.schema_errors.length > 0 && (
<div className="mb-2">
<p className="font-medium text-coral-400 mb-1">Schema issues:</p>
<ul className="list-disc list-inside text-coral-300/90 space-y-0.5">
{validationResult.schema_errors.map((e, i) => (
<li key={i}>{e}</li>
))}
</ul>
</div>
)}
{validationResult.ai_checked && (
<>
{validationResult.ai_warnings && validationResult.ai_warnings.length > 0 && (
<div className="mb-2">
<p className="font-medium text-amber-400 mb-1">AI review warnings:</p>
<ul className="list-disc list-inside text-amber-300/90 space-y-0.5">
{validationResult.ai_warnings.map((w, i) => (
<li key={i}>{w}</li>
))}
</ul>
</div>
)}
{validationResult.ai_suggestions && validationResult.ai_suggestions.length > 0 && (
<div className="mb-2">
<p className="font-medium text-electric-400 mb-1">Suggestions:</p>
<ul className="list-disc list-inside text-electric-300/90 space-y-0.5">
{validationResult.ai_suggestions.map((s, i) => (
<li key={i}>{s}</li>
))}
</ul>
</div>
)}
{validationResult.valid && (!validationResult.ai_warnings?.length) && (!validationResult.ai_suggestions?.length) && validationResult.ai_valid !== false && (
<p className="text-electric-400 font-medium">✓ Schema and AI review passed.</p>
)}
</>
)}
</div>
)}
<p className="text-sm text-void-300 mb-2">
Edit the JSON below. Keep the <code className="bg-void-700 text-void-100 px-1.5 py-0.5 rounded font-mono text-xs">segments</code> array structure. Use &quot;Validate with AI&quot; to check schema and content.
</p>
<textarea
value={editingPayloadJson}
onChange={(e) => { setEditingPayloadJson(e.target.value); setValidationResult(null); }}
className="flex-1 w-full p-4 font-mono text-sm text-void-100 bg-void-950 border border-void-600 rounded-xl resize-none min-h-[320px] placeholder-void-500 focus:border-coral-500/50 focus:ring-2 focus:ring-coral-500/20 focus:outline-none"
spellCheck={false}
placeholder='{"segments": [...]}'
/>
<div className="flex gap-2 mt-3">
<button
onClick={handleValidateWithAi}
disabled={validating}
className="px-4 py-2 bg-amber-500/20 text-amber-400 rounded-xl border border-amber-500/40 hover:bg-amber-500/30 disabled:opacity-50 text-sm font-medium transition-colors"
>
{validating ? 'Validating…' : 'Validate with AI'}
</button>
<button
onClick={handleSaveEdit}
disabled={saving}
className="px-4 py-2 bg-coral-500/20 text-coral-400 rounded-xl border border-coral-500/50 hover:bg-coral-500/30 disabled:opacity-50 text-sm font-medium transition-colors"
>
{saving ? 'Saving…' : 'Save changes'}
</button>
<button
onClick={closeEditModal}
className="px-4 py-2 bg-void-800 text-void-300 rounded-xl border border-void-600 hover:bg-void-700 hover:text-void-100 text-sm font-medium transition-colors"
>
Cancel
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}