import React, { useState, useRef } from 'react';
import {
Wand2,
MousePointerClick,
Heading1,
AlignLeft,
Copy,
CheckCircle2,
Sparkles,
AlertCircle,
Lightbulb,
Smile,
Code,
LayoutTemplate,
RotateCcw
} from 'lucide-react';
const apiKey = ""; // API key is provided by the execution environment
// --- Utility Functions ---
const copyToClipboard = (text) => {
return new Promise((resolve, reject) => {
// Create an invisible textarea to execute the copy command on
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const handleCopy = (e) => {
// Encode emojis as HTML entities for rich text editors
const htmlText = Array.from(text)
.map(char => char.codePointAt(0) > 127 ? `${char.codePointAt(0)};` : char)
.join('');
e.clipboardData.setData('text/plain', text);
e.clipboardData.setData('text/html', `${htmlText}`);
e.preventDefault();
};
// Attach listener specifically for this operation
document.addEventListener('copy', handleCopy);
try {
const successful = document.execCommand('copy');
if (successful) {
resolve();
} else {
reject(new Error('Copy command failed'));
}
} catch (err) {
reject(err);
} finally {
document.removeEventListener('copy', handleCopy);
document.body.removeChild(textArea);
}
});
};
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const fetchWithRetry = async (url, options, maxRetries = 5) => {
let retries = 0;
while (retries < maxRetries) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
retries++;
if (retries >= maxRetries) throw error;
const backoffTime = Math.pow(2, retries - 1) * 1000;
await delay(backoffTime);
}
}
};
// --- Main Application ---
export default function App() {
const [intention, setIntention] = useState('');
const [textType, setTextType] = useState('button');
const [includeEmoji, setIncludeEmoji] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isPolishing, setIsPolishing] = useState(false);
const [results, setResults] = useState([]);
const [error, setError] = useState('');
const handlePolishDraft = async () => {
if (!intention.trim()) return;
setIsPolishing(true);
setError('');
const prompt = `Rewrite the following rough thought into a clear, concise, professional goal statement for a UX copywriter. Keep it to exactly one sentence.\n\nRough thought: "${intention}"`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
systemInstruction: { parts: [{ text: "You are a UX product manager. Return ONLY the rewritten sentence in plain text. No quotes, no markdown." }] }
};
try {
const data = await fetchWithRetry(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}
);
const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (responseText) {
setIntention(responseText.trim());
}
} catch (err) {
console.error("Failed to polish draft", err);
} finally {
setIsPolishing(false);
}
};
const handleGenerate = async () => {
if (!intention.trim()) {
setError("Please tell me what you want to say first!");
return;
}
setIsLoading(true);
setError('');
setResults([]);
let systemInstruction = "";
if (textType === 'button') {
systemInstruction = "You are an expert UX copywriter. Generate Call-to-Action (CTA) button text. It must be short, punchy, and action-oriented (up to 5 words). Correct any errors in the user's intent.";
} else if (textType === 'header') {
systemInstruction = "You are an expert UX copywriter. Generate a header or title for a popup/balloon. It must be catchy, clear, and concise (3-6 words max). Correct any errors in the user's intent.";
} else if (textType === 'smartTip') {
systemInstruction = "You are an expert UX copywriter. Generate smart tip or tooltip text. It must be helpful, contextual, and very brief. Length should be 1-2 short sentences. Correct any errors in the user's intent.";
} else if (textType === 'fullPopup') {
systemInstruction = "You are an expert UX copywriter. Generate a complete popup/balloon containing a header, body text, and a CTA button. The header must be 3-6 words. The body must be 1-3 short sentences. The button must be 1-5 words. Ensure they flow logically. Correct any errors in the user's intent.";
} else {
systemInstruction = "You are an expert UX copywriter. Generate body text for a popup/balloon. It should be engaging, informative, easy to read, and grammatically perfect. Length should be 1-3 short, scannable sentences.";
}
if (includeEmoji && textType === 'smartTip') {
systemInstruction += " You MUST include exactly one highly relevant emoji AT THE VERY BEGINNING of the generated text (e.g., '🚀 Your text here').";
} else {
systemInstruction += " Do NOT include any emojis in the text.";
}
const prompt = `User's Intention/Draft: "${intention}"\n\nProvide 3 distinct options for the ${textType} text. Focus on clarity, tone, and conversion.`;
let itemSchema = {};
if (textType === 'fullPopup') {
itemSchema = {
type: "OBJECT",
properties: {
header: { type: "STRING", description: "The popup header/title" },
body: { type: "STRING", description: "The popup body text" },
button: { type: "STRING", description: "The popup CTA button text" },
reasoning: { type: "STRING", description: "A brief explanation of why this combination works well" }
},
required: ["header", "body", "button", "reasoning"]
};
} else {
itemSchema = {
type: "OBJECT",
properties: {
text: { type: "STRING", description: "The refined text suggestion" },
reasoning: { type: "STRING", description: "A very brief explanation of why this option works well" }
},
required: ["text", "reasoning"]
};
}
const payload = {
contents: [{ parts: [{ text: prompt }] }],
systemInstruction: { parts: [{ text: systemInstruction }] },
generationConfig: {
responseMimeType: "application/json",
responseSchema: {
type: "OBJECT",
properties: {
options: {
type: "ARRAY",
items: itemSchema
}
},
required: ["options"]
}
}
};
try {
const data = await fetchWithRetry(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}
);
const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (responseText) {
const parsed = JSON.parse(responseText);
setResults(parsed.options || []);
} else {
throw new Error("Invalid response format from AI");
}
} catch (err) {
console.error(err);
setError("Oops! Something went wrong while generating the text. Please try again.");
} finally {
setIsLoading(false);
}
};
const types = [
{ id: 'button', label: 'CTA Button', icon: MousePointerClick, desc: 'Short & Punchy (up to 5 words)' },
{ id: 'header', label: 'Title / Header', icon: Heading1, desc: 'Catchy & Clear (3-6 words)' },
{ id: 'body', label: 'Body Text', icon: AlignLeft, desc: 'Informative (1-3 sentences)' },
{ id: 'smartTip', label: 'Smart Tip', icon: Lightbulb, desc: 'Helpful hints (1-2 sentences)' },
{ id: 'fullPopup', label: 'Full Popup Set', icon: LayoutTemplate, desc: 'Header, Body & CTA Button' },
];
return (
{/* Header Title (Mobile only, moves to sidebar on desktop) */}
Copy Refiner
Turn messy thoughts into perfect UX copy.
{/* Left Column: Inputs */}
{/* Decorative background blob */}
Copy Refiner
Turn messy thoughts into perfect UX copy.
{/* Input 1: The Intention */}
{/* Input 2: Text Type Selection */}
{types.map((type) => {
const isSelected = textType === type.id;
const Icon = type.icon;
const isFullPopup = type.id === 'fullPopup';
return (
);
})}
{/* Input 3: Emoji Toggle (Only for Smart Tip) */}
{textType === 'smartTip' && (
setIncludeEmoji(!includeEmoji)}
>
Add an emoji?
Sprinkle some personality
)}
{/* Action Button */}
{error && (
)}
{/* Right Column: Results */}
{isLoading ? (
{[1, 2, 3].map((i) => (
))}
) : results.length > 0 ? (
Here are some options:
{results.map((result, index) => (
))}
) : (
Awaiting your prompt
Write your raw thoughts on the left, pick the type of text you need, and I'll polish it up for you.
)}
);
}
// --- Subcomponents ---
function ResultCard({ initialResult, type, intention }) {
const [result, setResult] = useState(initialResult);
const [copied, setCopied] = useState(false);
const [copiedCode, setCopiedCode] = useState(false);
const [tweakLoading, setTweakLoading] = useState(null);
const [tweakTarget, setTweakTarget] = useState('all');
const [isTweaked, setIsTweaked] = useState(false);
// Update local result if parent generates new results
React.useEffect(() => {
setResult(initialResult);
setIsTweaked(false);
}, [initialResult]);
const handleTweak = async (tweakType) => {
setTweakLoading(tweakType);
let constraint = "";
let promptText = "";
let tweakSchema = {};
if (type === 'fullPopup') {
let targetInstruction = "";
const lenConstraint = tweakType === 'longer'
? "Header: 4-8 words, Body: 3-5 sentences, Button: 3-6 words."
: "Header: 3-6 words, Body: 1-3 sentences, Button: 1-5 words.";
if (tweakTarget === 'all') {
targetInstruction = `Provide a completely rewritten header, body, and button following the new tone/style. Length constraints: ${lenConstraint}`;
} else if (tweakTarget === 'header') {
targetInstruction = `Rewrite ONLY the Header to make it ${tweakType}. Keep the Body and Button EXACTLY the same as the original. New Header constraint: ${tweakType === 'longer' ? '4-8 words' : '3-6 words'}.`;
} else if (tweakTarget === 'body') {
targetInstruction = `Rewrite ONLY the Body to make it ${tweakType}. Keep the Header and Button EXACTLY the same as the original. New Body constraint: ${tweakType === 'longer' ? '3-5 sentences' : '1-3 sentences'}.`;
} else if (tweakTarget === 'button') {
targetInstruction = `Rewrite ONLY the Button to make it ${tweakType}. Keep the Header and Body EXACTLY the same as the original. New Button constraint: ${tweakType === 'longer' ? '3-6 words' : '1-5 words'}.`;
}
constraint = targetInstruction;
promptText = `Original Header: "${result.header}"\nOriginal Body: "${result.body}"\nOriginal Button: "${result.button}"\nOriginal Intention: "${intention}"\n\nTask: Rewrite the original text (or targeted part) to make it ${tweakType}. \nConstraint: ${constraint}\n\nProvide the new popup text and reasoning.`;
tweakSchema = {
type: "OBJECT",
properties: {
header: { type: "STRING" },
body: { type: "STRING" },
button: { type: "STRING" },
reasoning: { type: "STRING" }
},
required: ["header", "body", "button", "reasoning"]
};
} else {
if (type === 'button') constraint = tweakType === 'longer' ? "Make it slightly longer and more descriptive (up to 7 words)." : "It must be short, punchy, and action-oriented (up to 5 words).";
if (type === 'header') constraint = tweakType === 'longer' ? "Make it a longer, more descriptive header (up to 10 words)." : "It must be catchy, clear, and concise (3-6 words max).";
if (type === 'smartTip') constraint = tweakType === 'longer' ? "Make it more detailed and explanatory (2-3 sentences)." : "It must be helpful, contextual, and very brief. Length should be 1-2 short sentences.";
if (type === 'body') constraint = tweakType === 'longer' ? "Expand on the details, make it 3-5 sentences long." : "It should be engaging, informative, easy to read. Length should be 1-3 short sentences.";
promptText = `Original text: "${result.text}"\nOriginal Intention: "${intention}"\n\nTask: Rewrite the original text to make it ${tweakType}. \nConstraint: ${constraint}\n\nProvide the new text and a brief reasoning for why this new version works well.`;
tweakSchema = {
type: "OBJECT",
properties: {
text: { type: "STRING" },
reasoning: { type: "STRING" }
},
required: ["text", "reasoning"]
};
}
const payload = {
contents: [{ parts: [{ text: promptText }] }],
systemInstruction: { parts: [{ text: "You are an expert UX copywriter. Respond ONLY with the requested JSON schema." }] },
generationConfig: {
responseMimeType: "application/json",
responseSchema: tweakSchema
}
};
try {
const data = await fetchWithRetry(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}
);
const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (responseText) {
setResult(JSON.parse(responseText));
setIsTweaked(true);
}
} catch (err) {
console.error("Failed to tweak text", err);
} finally {
setTweakLoading(null);
}
};
const handleRestore = () => {
setResult(initialResult);
setIsTweaked(false);
};
const handleCopy = async () => {
try {
const textToCopy = type === 'fullPopup'
? `${result.header}\n\n${result.body}\n\n${result.button}`
: result.text;
await copyToClipboard(textToCopy);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy", err);
}
};
const handleCopyCode = async () => {
try {
const rawText = type === 'fullPopup' ? `${result.header}\n\n${result.body}\n\n${result.button}` : result.text;
// Create the literal HTML code string
const codeText = Array.from(rawText)
.map(char => char.codePointAt(0) > 127 ? `${char.codePointAt(0)};` : char)
.join('');
// Use a simple plain text copy for the code string
const textArea = document.createElement("textarea");
textArea.value = codeText;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopiedCode(true);
setTimeout(() => setCopiedCode(false), 2000);
} catch (err) {
console.error("Failed to copy code", err);
}
};
const handleManualCopy = (e) => {
// Intercept manual Ctrl+C / Cmd+C to inject HTML entities
const rawText = type === 'fullPopup' ? `${result.header}\n\n${result.body}\n\n${result.button}` : result.text;
const htmlText = Array.from(rawText)
.map(char => char.codePointAt(0) > 127 ? `${char.codePointAt(0)};` : char)
.join('');
e.clipboardData.setData('text/plain', rawText);
e.clipboardData.setData('text/html', `${htmlText.replace(/\n/g, '
')}`);
e.preventDefault();
};
// Adjust display style based on type to give a visual hint
let textStyle = "text-lg text-slate-800 font-medium";
if (type === 'button') {
textStyle = "text-lg font-bold text-indigo-700 bg-indigo-50 inline-block px-3 py-1.5 rounded-lg border border-indigo-100";
} else if (type === 'header') {
textStyle = "text-xl font-extrabold text-slate-900";
} else if (type === 'body') {
textStyle = "text-sm text-slate-600 leading-relaxed";
} else if (type === 'smartTip') {
textStyle = "text-sm text-slate-700 bg-slate-50 inline-block px-3 py-2 rounded-lg border border-slate-200 shadow-sm font-medium";
}
// Detect if the result text contains an emoji/special character
const rawTextForEmojiCheck = type === 'fullPopup' ? `${result.header}${result.body}${result.button}` : result.text;
const hasEmoji = Array.from(rawTextForEmojiCheck || '').some(char => char.codePointAt(0) > 127);
return (
{type === 'fullPopup' ? (
{result.header}
{result.body}
{result.button}
) : (
{type === 'smartTip' && }
{result.text}
)}
{result.reasoning}
{/* Quick AI Tweak Actions */}
{type === 'fullPopup' && (
<>
>
)}
{['shorter', 'longer', 'friendlier', 'more urgent'].map(tweak => (
))}
{isTweaked && (
<>
>
)}
{hasEmoji && (
)}
);
}