Spaces:
Running
Running
import React, { useState, useEffect } from 'react'; | |
import ReactDOM from 'react-dom/client'; | |
import { GoogleGenAI, GenerateContentResponse } from "@google/genai"; | |
const App = () => { | |
const [platform, setPlatform] = useState<string>('facebook'); | |
const [contentType, setContentType] = useState<string>('facebook-post'); | |
const [contentLength, setContentLength] = useState<string>('standard'); | |
const [contentCategory, setContentCategory] = useState<string>('promotion'); // Kept for general theme | |
const [style, setStyle] = useState<string>('friendly'); // Renamed from tone | |
const [productName, setProductName] = useState<string>(''); | |
const [keyMessage, setKeyMessage] = useState<string>(''); | |
const [facebookPageLink, setFacebookPageLink] = useState<string>(''); | |
const [objective, setObjective] = useState<string>('brand-awareness'); | |
const [targetAudience, setTargetAudience] = useState<string>(''); | |
const [keywords, setKeywords] = useState<string>(''); | |
const [includeCTA, setIncludeCTA] = useState<boolean>(true); | |
const [includeEmojis, setIncludeEmojis] = useState<boolean>(true); | |
const [includeHashtags, setIncludeHashtags] = useState<boolean>(false); | |
const [numVariations, setNumVariations] = useState<number>(1); | |
const [generatedContents, setGeneratedContents] = useState<string[]>([]); | |
const [isLoading, setIsLoading] = useState<boolean>(false); | |
const [error, setError] = useState<string | null>(null); | |
// const [copyButtonText, setCopyButtonText] = useState<string>('π Copy Content'); // Will be per variation | |
const [qaMetrics, setQaMetrics] = useState<{ | |
grammar: string; | |
narrativeFlow: string; | |
culturalContext: string; | |
optimizationSuggestion: string; | |
engagementScore: string; | |
} | null>(null); | |
const API_KEY = process.env.API_KEY; | |
let ai: GoogleGenAI | null = null; | |
if (API_KEY) { | |
ai = new GoogleGenAI({ apiKey: API_KEY }); | |
} else { | |
console.error("API_KEY environment variable not set."); | |
} | |
const platformContentTypeOptions: Record<string, Array<{ value: string; label: string }>> = { | |
facebook: [ | |
{ value: 'facebook-post', label: 'Post (Text, Multimedia)' }, | |
{ value: 'facebook-story-text', label: 'Story (Text-based)' }, | |
{ value: 'facebook-ad-copy', label: 'Ad Copy' }, | |
{ value: 'facebook-reel-script', label: 'Reel Script' }, | |
], | |
instagram: [ | |
{ value: 'instagram-caption', label: 'Caption (for Post/Reel)' }, | |
{ value: 'instagram-story-text', label: 'Story (Text-based)' }, | |
{ value: 'instagram-reel-script', label: 'Reel Script' }, | |
{ value: 'instagram-ad-copy', label: 'Ad Copy' }, | |
], | |
tiktok: [ | |
{ value: 'tiktok-video-script', label: 'Video Script' }, | |
{ value: 'tiktok-ad-script', label: 'Ad Script' }, | |
], | |
youtube: [ | |
{ value: 'youtube-video-script', label: 'Video Script (Main)' }, | |
{ value: 'youtube-short-script', label: 'Short Script' }, | |
{ value: 'youtube-community-post', label: 'Community Post' }, | |
{ value: 'youtube-video-description', label: 'Video Description' }, | |
], | |
viber: [ | |
{ value: 'viber-broadcast', label: 'Broadcast Message' }, | |
{ value: 'viber-channel-update', label: 'Channel Update' }, | |
], | |
telegram: [ | |
{ value: 'telegram-channel-post', label: 'Channel Post' }, | |
{ value: 'telegram-group-message', label: 'Group Message' }, | |
] | |
}; | |
const simplifiedLengthOptions = [ | |
{ value: 'concise', label: 'Concise / Short' }, | |
{ value: 'standard', label: 'Standard / Medium' }, | |
{ value: 'detailed', label: 'Detailed / Long' }, | |
]; | |
const styleOptions = [ | |
{ value: 'polite', label: 'Polite (Formal Burmese)' }, | |
{ value: 'friendly', label: 'Friendly (Casual Burmese)' }, | |
{ value: 'professional', label: 'Professional (Business Burmese)'}, | |
{ value: 'humorous', label: 'Humorous' }, | |
{ value: 'persuasive', label: 'Persuasive' }, | |
{ value: 'urgent', label: 'Urgent' }, | |
{ value: 'empathetic', label: 'Empathetic' }, | |
]; | |
const objectiveOptions = [ | |
{ value: 'brand-awareness', label: 'Brand Awareness' }, | |
{ value: 'lead-generation', label: 'Lead Generation' }, | |
{ value: 'sales', label: 'Sales / Conversions' }, | |
{ value: 'engagement', label: 'Engagement (Likes, Comments, Shares)' }, | |
{ value: 'educate', label: 'Educate Audience' }, | |
{ value: 'event-promotion', label: 'Event Promotion'}, | |
]; | |
useEffect(() => { | |
const currentPlatformOptions = platformContentTypeOptions[platform] || []; | |
if (currentPlatformOptions.length > 0 && !currentPlatformOptions.find(opt => opt.value === contentType)) { | |
setContentType(currentPlatformOptions[0].value); | |
} | |
// Ensure contentLength is valid for the simplified options | |
if (!simplifiedLengthOptions.find(opt => opt.value === contentLength)) { | |
setContentLength(simplifiedLengthOptions[0].value); // Default to first option if current is invalid | |
} | |
}, [platform, contentType, contentLength]); // Added contentLength to dependencies | |
const handleGenerateContent = async () => { | |
if (!ai) { | |
setError("Gemini AI client is not initialized. Check API Key configuration."); | |
return; | |
} | |
if (!productName.trim() && !keyMessage.trim() && contentCategory !== 'seasonal' && objective !== 'brand-awareness') { | |
setError("Please enter Product/Service Name or Key Message/Details, or select a broader category/objective."); | |
return; | |
} | |
setIsLoading(true); | |
setError(null); | |
setGeneratedContents([]); | |
setQaMetrics(null); | |
let lengthInstruction = ""; | |
switch (contentLength) { | |
case 'concise': | |
lengthInstruction = "a concise and short piece of content."; | |
break; | |
case 'standard': | |
lengthInstruction = "a standard length piece of content, providing adequate detail."; | |
break; | |
case 'detailed': | |
lengthInstruction = "a detailed and comprehensive piece of content."; | |
break; | |
default: | |
lengthInstruction = "a standard length piece of content."; | |
} | |
const selectedContentTypeLabel = platformContentTypeOptions[platform]?.find(opt => opt.value === contentType)?.label || contentType; | |
const selectedStyleLabel = styleOptions.find(opt => opt.value === style)?.label || style; | |
const selectedObjectiveLabel = objectiveOptions.find(opt => opt.value === objective)?.label || objective; | |
const capitalizedCategory = contentCategory.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); | |
const prompt = ` | |
You are an expert Burmese social media copywriter with deep understanding of native Burmese speaker patterns and cultural nuances relevant to Myanmar. | |
Your style should emulate successful content found on Myanmar social media business pages and promotional posts. | |
Generate ${numVariations} distinct variation(s) of content based on the following specifications: | |
- Language: Burmese | |
- Platform: ${platform.charAt(0).toUpperCase() + platform.slice(1)} | |
- Content Type: ${selectedContentTypeLabel} | |
- Desired Content Scope/Length: Generate ${lengthInstruction} | |
- Primary Objective/Goal: ${selectedObjectiveLabel} | |
- Content Theme/Category: ${capitalizedCategory} | |
- Style: ${selectedStyleLabel} (translate this style appropriately into Burmese) | |
${productName.trim() ? `- Product/Service Name: "${productName}"` : ''} | |
${keyMessage.trim() ? `- Key Message/Details: "${keyMessage}"` : ''} | |
${targetAudience.trim() ? `- Target Audience: "${targetAudience}"` : ''} | |
${keywords.trim() ? `- Keywords to incorporate (if natural): "${keywords}"` : ''} | |
${facebookPageLink.trim() ? `- Business Context/Reference Facebook Page: ${facebookPageLink.trim()}. (Adapt to their style if possible)` : ''} | |
Output Instructions: | |
- Ensure each variation is clearly separated by "---VARIATION SEPARATOR---". If only 1 variation is requested, this separator is not needed. | |
- For each variation: | |
${includeEmojis ? "- Seamlessly integrate relevant Burmese-style emojis." : "- Do not use emojis unless absolutely essential for the content type."} | |
${includeCTA ? `- Include a clear and natural-sounding Call To Action (CTA) suitable for the platform and objective.` : "- A direct Call To Action is not a priority unless inherent to the content type."} | |
${includeHashtags ? `- Include a few relevant and effective hashtags (Burmese or English as appropriate).` : "- Do not include hashtags unless specifically requested by the content type itself."} | |
- Ensure the output is ONLY the generated Burmese content, ready to be used. | |
- Focus on creating engaging, grammatically correct, and culturally appropriate content for the Myanmar market. | |
- If the content type is a script (e.g., Reel Script, Video Script), format it appropriately with scene descriptions or speaker cues if applicable. | |
Please provide exactly ${numVariations} variation(s). | |
`; | |
try { | |
const response: GenerateContentResponse = await ai.models.generateContent({ | |
model: 'gemini-2.5-flash-preview-04-17', | |
contents: prompt, | |
}); | |
const text = response.text; | |
if (numVariations > 1) { | |
setGeneratedContents(text.split("---VARIATION SEPARATOR---").map(v => v.trim()).filter(v => v)); | |
} else { | |
setGeneratedContents([text.trim()]); | |
} | |
// Simulate QA metrics (applied to the first variation for simplicity) | |
setQaMetrics({ | |
grammar: Math.random() > 0.15 ? 'β Excellent Burmese Grammar' : 'β οΈ Review Grammar Advised', | |
narrativeFlow: Math.random() > 0.2 ? 'π Coherent Narrative Flow' : 'π€ Flow Could Be Smoother', | |
culturalContext: Math.random() > 0.1 ? 'β Culturally Appropriate for Myanmar' : 'β Verify Cultural Context Locally', | |
optimizationSuggestion: ['Consider A/B testing variations.', 'Analyze competitor content for this objective.', 'Ensure the visual pairing matches the text.'][Math.floor(Math.random() * 3)], | |
engagementScore: `Predicted: ${Math.floor(Math.random() * 31) + 65}% Engagement` | |
}); | |
} catch (e: any) { | |
console.error("Error generating content:", e); | |
setError(`Failed to generate content. ${e.message || 'Please try again.'}`); | |
setGeneratedContents([]); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
const handleCopyVariation = (textToCopy: string, variationIndex: number) => { | |
navigator.clipboard.writeText(textToCopy) | |
.then(() => { | |
// Visually indicate copy success, perhaps on the button itself | |
const button = document.getElementById(`copy-button-${variationIndex}`); | |
if (button) { | |
const originalText = button.innerHTML; | |
button.innerHTML = 'β Copied!'; | |
setTimeout(() => { button.innerHTML = originalText; }, 2000); | |
} | |
}) | |
.catch(err => { | |
console.error('Failed to copy variation: ', err); | |
setError('Failed to copy content to clipboard.'); | |
}); | |
}; | |
const handleExportContent = () => { | |
if (generatedContents.length > 0) { | |
const allContent = generatedContents.map((content, index) => { | |
return `--- Variation ${index + 1} ---\n${content}\n\n`; | |
}).join(''); | |
const blob = new Blob([allContent], { type: 'text/plain;charset=utf-8' }); | |
const link = document.createElement('a'); | |
link.href = URL.createObjectURL(blob); | |
link.download = 'burmese_social_media_content.txt'; | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
URL.revokeObjectURL(link.href); | |
} | |
}; | |
const currentPlatformContentTypeOptions = platformContentTypeOptions[platform] || []; | |
return ( | |
<div className="container"> | |
<h1>βοΈ Burmese Social Media Content Generator V2</h1> | |
{!API_KEY && ( | |
<div className="error-message" role="alert"> | |
API Key is not configured. This application requires the API_KEY environment variable to be set to function. | |
</div> | |
)} | |
<div className="form-grid"> | |
{/* Platform */} | |
<div className="form-group"> | |
<label htmlFor="platform">Platform</label> | |
<select id="platform" value={platform} onChange={(e) => setPlatform(e.target.value)} aria-label="Platform"> | |
{Object.keys(platformContentTypeOptions).map(plat => ( | |
<option key={plat} value={plat}>{plat.charAt(0).toUpperCase() + plat.slice(1)}</option> | |
))} | |
</select> | |
</div> | |
{/* Content Type (Dynamic) */} | |
<div className="form-group"> | |
<label htmlFor="contentType">Content Type</label> | |
<select | |
id="contentType" | |
value={contentType} | |
onChange={(e) => setContentType(e.target.value)} | |
aria-label="Content Type" | |
disabled={currentPlatformContentTypeOptions.length === 0} | |
> | |
{currentPlatformContentTypeOptions.map(option => ( | |
<option key={option.value} value={option.value}>{option.label}</option> | |
))} | |
</select> | |
</div> | |
{/* Content Length/Scope */} | |
<div className="form-group"> | |
<label htmlFor="contentLength">Content Length/Scope</label> | |
<select id="contentLength" value={contentLength} onChange={(e) => setContentLength(e.target.value)} aria-label="Content Length or Scope"> | |
{simplifiedLengthOptions.map(option => ( | |
<option key={option.value} value={option.value}>{option.label}</option> | |
))} | |
</select> | |
</div> | |
{/* Objective/Goal */} | |
<div className="form-group"> | |
<label htmlFor="objective">Objective/Goal</label> | |
<select id="objective" value={objective} onChange={(e) => setObjective(e.target.value)} aria-label="Objective or Goal"> | |
{objectiveOptions.map(option => ( | |
<option key={option.value} value={option.value}>{option.label}</option> | |
))} | |
</select> | |
</div> | |
{/* Style (was Tone) */} | |
<div className="form-group"> | |
<label htmlFor="style">Style</label> | |
<select id="style" value={style} onChange={(e) => setStyle(e.target.value)} aria-label="Content Style"> | |
{styleOptions.map(option => ( | |
<option key={option.value} value={option.value}>{option.label}</option> | |
))} | |
</select> | |
</div> | |
{/* Content Category (General Theme) */} | |
<div className="form-group"> | |
<label htmlFor="contentCategory">Content Theme/Category</label> | |
<select id="contentCategory" value={contentCategory} onChange={(e) => setContentCategory(e.target.value)} aria-label="Content Category or Theme"> | |
<option value="promotion">Product/Service Promotions</option> | |
<option value="info">Information/Specifications</option> | |
<option value="seasonal">Seasonal Greetings/Wishes</option> | |
<option value="explainer">Explainer Content</option> | |
<option value="sales">Sales Announcements</option> | |
<option value="creative-ad">Creative Story/Advertisement</option> | |
<option value="tutorial">Tutorial/How-to</option> | |
<option value="behind-the-scenes">Behind The Scenes</option> | |
<option value="user-generated-content">User-Generated Content Spotlight</option> | |
<option value="question-answer">Question & Answer</option> | |
</select> | |
</div> | |
{/* Product/Service Name */} | |
<div className="form-group full-width"> | |
<label htmlFor="productName">Product/Service Name (Optional if covered by Key Message)</label> | |
<input | |
type="text" | |
id="productName" | |
value={productName} | |
onChange={(e) => setProductName(e.target.value)} | |
placeholder="e.g., Shwe Coffee Mix, Yangon Tech Services" | |
aria-label="Product or Service Name" | |
/> | |
</div> | |
{/* Key Message/Details */} | |
<div className="form-group full-width"> | |
<label htmlFor="keyMessage">Key Message/Details/Topic</label> | |
<textarea | |
id="keyMessage" | |
value={keyMessage} | |
onChange={(e) => setKeyMessage(e.target.value)} | |
placeholder="e.g., Special discount, new features, benefits, topic of the content..." | |
aria-label="Key Message, Details or Topic" | |
aria-required="true" | |
></textarea> | |
</div> | |
{/* Target Audience */} | |
<div className="form-group full-width"> | |
<label htmlFor="targetAudience">Target Audience (Optional)</label> | |
<textarea | |
id="targetAudience" | |
value={targetAudience} | |
onChange={(e) => setTargetAudience(e.target.value)} | |
placeholder="e.g., Young adults in Yangon, small business owners, families with children" | |
aria-label="Target Audience" | |
></textarea> | |
</div> | |
{/* Keywords */} | |
<div className="form-group full-width"> | |
<label htmlFor="keywords">Keywords (Comma separated, Optional)</label> | |
<input | |
type="text" | |
id="keywords" | |
value={keywords} | |
onChange={(e) => setKeywords(e.target.value)} | |
placeholder="e.g., organic, sustainable, best coffee, tech solutions" | |
aria-label="Keywords, comma separated" | |
/> | |
</div> | |
<div className="form-group full-width"> | |
<label htmlFor="facebookPageLink">Business Facebook Page Link (Optional for style reference)</label> | |
<input | |
type="url" | |
id="facebookPageLink" | |
value={facebookPageLink} | |
onChange={(e) => setFacebookPageLink(e.target.value)} | |
placeholder="e.g., https://www.facebook.com/yourbusinesspage" | |
aria-label="Facebook Page Link (Optional for style reference)" | |
/> | |
</div> | |
{/* Output Controls */} | |
<div className="form-group full-width output-controls"> | |
<label>Output Controls:</label> | |
<div className="checkbox-group"> | |
<label htmlFor="includeCTA"> | |
<input type="checkbox" id="includeCTA" checked={includeCTA} onChange={(e) => setIncludeCTA(e.target.checked)} /> | |
Include Call-to-Action (CTA) | |
</label> | |
<label htmlFor="includeEmojis"> | |
<input type="checkbox" id="includeEmojis" checked={includeEmojis} onChange={(e) => setIncludeEmojis(e.target.checked)} /> | |
Include Emojis | |
</label> | |
<label htmlFor="includeHashtags"> | |
<input type="checkbox" id="includeHashtags" checked={includeHashtags} onChange={(e) => setIncludeHashtags(e.target.checked)} /> | |
Include Hashtags | |
</label> | |
</div> | |
</div> | |
{/* Number of Variations */} | |
<div className="form-group"> | |
<label htmlFor="numVariations">Number of Variations (1-3)</label> | |
<input | |
type="number" | |
id="numVariations" | |
value={numVariations} | |
onChange={(e) => setNumVariations(Math.max(1, Math.min(3, parseInt(e.target.value, 10) || 1)))} | |
min="1" | |
max="3" | |
aria-label="Number of content variations" | |
/> | |
</div> | |
</div> | |
<button | |
className="generate-button" | |
onClick={handleGenerateContent} | |
disabled={isLoading || !API_KEY} | |
aria-label="Generate Social Media Content" | |
> | |
{isLoading ? 'Generating...' : `β¨ Generate ${numVariations} Variation(s)`} | |
</button> | |
{error && <div className="error-message" role="alert">{error}</div>} | |
{isLoading && <div className="loading-message" aria-busy="true" aria-live="assertive">Generating Burmese content, please wait...</div>} | |
{generatedContents.length > 0 && ( | |
<div className="output-section"> | |
<h2>Generated Content (Burmese)</h2> | |
{generatedContents.map((content, index) => ( | |
<div key={index} className="variation-block"> | |
<h3>Variation {index + 1}</h3> | |
<div className="output-content" aria-live="polite">{content}</div> | |
<button | |
id={`copy-button-${index}`} | |
onClick={() => handleCopyVariation(content, index)} | |
className="action-button copy-button" | |
aria-label={`Copy variation ${index + 1} to clipboard`} | |
> | |
π Copy Variation {index + 1} | |
</button> | |
</div> | |
))} | |
{generatedContents.length > 0 && ( | |
<button | |
onClick={handleExportContent} | |
className="action-button export-button export-all-button" | |
aria-label="Export all generated variations as a text file" | |
> | |
πΎ Export All as .txt | |
</button> | |
)} | |
</div> | |
)} | |
{qaMetrics && generatedContents.length > 0 && ( | |
<div className="qa-section"> | |
<h2>Content Quality Assurance (Simulated for Variation 1)</h2> | |
<ul className="qa-metrics"> | |
<li><strong>Grammar:</strong> {qaMetrics.grammar}</li> | |
<li><strong>Narrative Flow:</strong> {qaMetrics.narrativeFlow}</li> | |
<li><strong>Cultural Context:</strong> {qaMetrics.culturalContext}</li> | |
<li><strong>Optimization Suggestion:</strong> {qaMetrics.optimizationSuggestion}</li> | |
<li><strong>Engagement Predictor:</strong> {qaMetrics.engagementScore}</li> | |
</ul> | |
</div> | |
)} | |
</div> | |
); | |
}; | |
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); | |
root.render(<React.StrictMode><App /></React.StrictMode>); | |