| "use client"; | |
| import React, { useState, useEffect } from "react"; | |
| import { X, Wand2, Edit3, Sparkles, Loader2, CheckCircle2, AlertCircle } from "lucide-react"; | |
| import { editAdCopy } from "@/lib/api/endpoints"; | |
| import type { AdCreativeDB } from "@/types/api"; | |
| import { Button } from "@/components/ui/Button"; | |
| import { Input } from "@/components/ui/Input"; | |
| import { Card, CardContent } from "@/components/ui/Card"; | |
| interface EditCopyModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| adId: string; | |
| ad?: AdCreativeDB | null; | |
| field: "title" | "headline" | "primary_text" | "description" | "body_story" | "cta"; | |
| fieldLabel: string; | |
| currentValue: string; | |
| onSuccess?: () => void; | |
| } | |
| type EditMode = "manual" | "ai"; | |
| export const EditCopyModal: React.FC<EditCopyModalProps> = ({ | |
| isOpen, | |
| onClose, | |
| adId, | |
| ad, | |
| field, | |
| fieldLabel, | |
| currentValue, | |
| onSuccess, | |
| }) => { | |
| const [mode, setMode] = useState<EditMode>("manual"); | |
| const [editedValue, setEditedValue] = useState(currentValue); | |
| const [aiSuggestion, setAiSuggestion] = useState(""); | |
| const [userSuggestion, setUserSuggestion] = useState(""); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [success, setSuccess] = useState(false); | |
| useEffect(() => { | |
| if (isOpen) { | |
| setEditedValue(currentValue); | |
| setAiSuggestion(""); | |
| setUserSuggestion(""); | |
| setError(null); | |
| setSuccess(false); | |
| setMode("manual"); | |
| } | |
| }, [isOpen, currentValue]); | |
| const handleManualSave = async () => { | |
| if (!editedValue.trim()) { | |
| setError("This field cannot be empty"); | |
| return; | |
| } | |
| setIsLoading(true); | |
| setError(null); | |
| try { | |
| await editAdCopy({ | |
| ad_id: adId, | |
| field, | |
| value: editedValue, | |
| mode: "manual", | |
| }); | |
| setSuccess(true); | |
| setTimeout(() => { | |
| onSuccess?.(); | |
| onClose(); | |
| }, 1000); | |
| } catch (err: any) { | |
| setError(err.message || "Failed to update. Please try again."); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleAIGenerate = async () => { | |
| setIsLoading(true); | |
| setError(null); | |
| try { | |
| const result = await editAdCopy({ | |
| ad_id: adId, | |
| field, | |
| value: currentValue, | |
| mode: "ai", | |
| user_suggestion: userSuggestion || undefined, | |
| }); | |
| setAiSuggestion(result.edited_value || ""); | |
| } catch (err: any) { | |
| setError(err.message || "Failed to generate AI suggestion. Please try again."); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleAIApply = async () => { | |
| if (!aiSuggestion.trim()) { | |
| setError("No AI suggestion available"); | |
| return; | |
| } | |
| setIsLoading(true); | |
| setError(null); | |
| try { | |
| await editAdCopy({ | |
| ad_id: adId, | |
| field, | |
| value: aiSuggestion, | |
| mode: "manual", | |
| }); | |
| setSuccess(true); | |
| setTimeout(() => { | |
| onSuccess?.(); | |
| onClose(); | |
| }, 1000); | |
| } catch (err: any) { | |
| setError(err.message || "Failed to apply AI suggestion. Please try again."); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| if (!isOpen) return null; | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in"> | |
| <Card className="w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl border-2 border-blue-200 animate-scale-in"> | |
| <CardContent className="p-6"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between mb-6"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-lg"> | |
| <Edit3 className="h-5 w-5 text-white" /> | |
| </div> | |
| <div> | |
| <h2 className="text-xl font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent"> | |
| Edit {fieldLabel} | |
| </h2> | |
| <p className="text-sm text-gray-500">Update your ad copy</p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="p-2 hover:bg-gray-100 rounded-lg transition-colors" | |
| > | |
| <X className="h-5 w-5 text-gray-500" /> | |
| </button> | |
| </div> | |
| {/* Mode Toggle */} | |
| <div className="flex gap-2 mb-6 p-1 bg-gray-100 rounded-lg"> | |
| <button | |
| onClick={() => setMode("manual")} | |
| className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-md font-medium transition-all ${ | |
| mode === "manual" | |
| ? "bg-white text-blue-600 shadow-sm" | |
| : "text-gray-600 hover:text-gray-900" | |
| }`} | |
| > | |
| <Edit3 className="h-4 w-4" /> | |
| Manual Edit | |
| </button> | |
| <button | |
| onClick={() => setMode("ai")} | |
| className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-md font-medium transition-all ${ | |
| mode === "ai" | |
| ? "bg-white text-blue-600 shadow-sm" | |
| : "text-gray-600 hover:text-gray-900" | |
| }`} | |
| > | |
| <Wand2 className="h-4 w-4" /> | |
| AI Edit | |
| </button> | |
| </div> | |
| {/* Manual Edit Mode */} | |
| {mode === "manual" && ( | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2"> | |
| {fieldLabel} | |
| </label> | |
| <textarea | |
| value={editedValue} | |
| onChange={(e) => setEditedValue(e.target.value)} | |
| className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all resize-none" | |
| rows={field === "body_story" ? 8 : field === "description" ? 4 : 3} | |
| placeholder={`Enter your ${fieldLabel.toLowerCase()}...`} | |
| /> | |
| </div> | |
| {error && ( | |
| <div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600"> | |
| <AlertCircle className="h-4 w-4" /> | |
| <span className="text-sm">{error}</span> | |
| </div> | |
| )} | |
| {success && ( | |
| <div className="flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-lg text-green-600"> | |
| <CheckCircle2 className="h-4 w-4" /> | |
| <span className="text-sm">Successfully updated!</span> | |
| </div> | |
| )} | |
| <div className="flex gap-3"> | |
| <Button | |
| onClick={handleManualSave} | |
| isLoading={isLoading} | |
| variant="primary" | |
| className="flex-1" | |
| > | |
| Save Changes | |
| </Button> | |
| <Button | |
| onClick={onClose} | |
| variant="ghost" | |
| disabled={isLoading} | |
| > | |
| Cancel | |
| </Button> | |
| </div> | |
| </div> | |
| )} | |
| {/* AI Edit Mode */} | |
| {mode === "ai" && ( | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2"> | |
| Your Suggestion (Optional) | |
| </label> | |
| <textarea | |
| value={userSuggestion} | |
| onChange={(e) => setUserSuggestion(e.target.value)} | |
| className="w-full px-4 py-3 rounded-xl border-2 border-gray-300 bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all resize-none" | |
| rows={3} | |
| placeholder="e.g., Make it more emotional, Add urgency, Make it shorter, Focus on benefits..." | |
| /> | |
| <p className="text-xs text-gray-500 mt-1"> | |
| Tell AI how you'd like the {fieldLabel.toLowerCase()} improved | |
| </p> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2"> | |
| Current {fieldLabel} | |
| </label> | |
| <div className="px-4 py-3 rounded-xl border-2 border-gray-200 bg-gray-50 text-gray-700"> | |
| {currentValue || <span className="text-gray-400">No content</span>} | |
| </div> | |
| </div> | |
| <Button | |
| onClick={handleAIGenerate} | |
| isLoading={isLoading} | |
| variant="primary" | |
| className="w-full" | |
| disabled={isLoading} | |
| > | |
| <Wand2 className="h-4 w-4 mr-2" /> | |
| {isLoading ? "Generating..." : "Generate AI Suggestion"} | |
| </Button> | |
| {aiSuggestion && ( | |
| <div className="space-y-3"> | |
| <div> | |
| <label className="block text-sm font-semibold text-gray-700 mb-2"> | |
| AI Suggestion | |
| </label> | |
| <div className="px-4 py-3 rounded-xl border-2 border-blue-200 bg-blue-50 text-gray-700 whitespace-pre-line"> | |
| {aiSuggestion} | |
| </div> | |
| </div> | |
| {error && ( | |
| <div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600"> | |
| <AlertCircle className="h-4 w-4" /> | |
| <span className="text-sm">{error}</span> | |
| </div> | |
| )} | |
| {success && ( | |
| <div className="flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-lg text-green-600"> | |
| <CheckCircle2 className="h-4 w-4" /> | |
| <span className="text-sm">Successfully updated!</span> | |
| </div> | |
| )} | |
| <div className="flex gap-3"> | |
| <Button | |
| onClick={handleAIApply} | |
| isLoading={isLoading} | |
| variant="primary" | |
| className="flex-1" | |
| > | |
| Apply AI Suggestion | |
| </Button> | |
| <Button | |
| onClick={() => { | |
| setAiSuggestion(""); | |
| setUserSuggestion(""); | |
| }} | |
| variant="ghost" | |
| disabled={isLoading} | |
| > | |
| Regenerate | |
| </Button> | |
| <Button | |
| onClick={onClose} | |
| variant="ghost" | |
| disabled={isLoading} | |
| > | |
| Cancel | |
| </Button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| }; | |