diff --git a/backend/models/__pycache__/__init__.cpython-313.pyc b/backend/models/__pycache__/__init__.cpython-313.pyc index ba47e176..50d767c3 100644 Binary files a/backend/models/__pycache__/__init__.cpython-313.pyc and b/backend/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/frontend/src/components/FacebookWriter/components/AdCopyHITL.tsx b/frontend/src/components/FacebookWriter/components/AdCopyHITL.tsx new file mode 100644 index 00000000..e6f31c41 --- /dev/null +++ b/frontend/src/components/FacebookWriter/components/AdCopyHITL.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { facebookWriterApi } from '../../../services/facebookWriterApi'; +import { readPrefs, logAssistant } from '../utils/facebookWriterUtils'; + +interface AdCopyHITLProps { + args: any; + respond?: (data: any) => void; +} + +const AdCopyHITL: React.FC = ({ args, respond }) => { + const prefs = React.useMemo(() => readPrefs(), []); + const [form, setForm] = React.useState({ + business_type: args?.business_type || prefs.business_type || 'SaaS', + product_service: args?.product_service || 'Product X', + ad_objective: args?.ad_objective || 'Conversions', + ad_format: args?.ad_format || 'Single image', + target_audience: args?.target_audience || prefs.target_audience || 'Marketing managers at SMEs', + targeting_options: { + age_group: (args?.targeting_options?.age_group) || '18-24', + gender: args?.targeting_options?.gender || 'All', + location: args?.targeting_options?.location || 'Global', + interests: args?.targeting_options?.interests || '', + behaviors: args?.targeting_options?.behaviors || '', + lookalike_audience: args?.targeting_options?.lookalike_audience || '' + }, + unique_selling_proposition: args?.unique_selling_proposition || 'Fast, reliable, loved by users', + offer_details: args?.offer_details || '', + budget_range: args?.budget_range || '$50-200/day', + custom_budget: args?.custom_budget || '', + campaign_duration: args?.campaign_duration || '2 weeks', + competitor_analysis: args?.competitor_analysis || '', + brand_voice: args?.brand_voice || (prefs.post_tone || 'Professional'), + compliance_requirements: args?.compliance_requirements || '' + }); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + const safeRespond = React.useCallback((data: any) => { + try { + if (typeof respond === 'function') respond(data); + else console.log('[FB Writer][HITL] respond unavailable; payload:', data); + } catch (e) { console.warn('[FB Writer][HITL] respond error', e); } + }, [respond]); + + const run = async () => { + try { + setLoading(true); + setError(null); + const res = await facebookWriterApi.adCopyGenerate(form as any); + const variations = { + headline_variations: res?.ad_variations?.headline_variations || res?.data?.ad_variations?.headline_variations || [], + primary_text_variations: res?.ad_variations?.primary_text_variations || res?.data?.ad_variations?.primary_text_variations || [], + description_variations: res?.ad_variations?.description_variations || res?.data?.ad_variations?.description_variations || [], + cta_variations: res?.ad_variations?.cta_variations || res?.data?.ad_variations?.cta_variations || [] + }; + window.dispatchEvent(new CustomEvent('fbwriter:adVariations', { detail: variations })); + const primaryObj = res?.primary_ad_copy || res?.data?.primary_ad_copy; + const message = primaryObj?.primary_text || primaryObj?.text || res?.content || res?.data?.content || 'Ad copy generated.'; + window.dispatchEvent(new CustomEvent('fbwriter:appendDraft', { detail: `\n\n${message}` })); + logAssistant(message); + safeRespond({ success: true, content: message }); + } catch (e: any) { + const msg = e?.response?.data?.detail || e?.message || 'Failed to generate ad copy'; + setError(`${msg}`); + safeRespond({ success: false, message: `${msg}` }); + } finally { + setLoading(false); + } + }; + + const set = (k: string, v: any) => setForm((prev: any) => ({ ...prev, [k]: v })); + const setNested = (k: keyof typeof form.targeting_options, v: any) => setForm((prev: any) => ({ ...prev, targeting_options: { ...prev.targeting_options, [k]: v } })); + + return ( +
+
Generate Ad Copy
+
+ set('business_type', e.target.value)} /> + set('product_service', e.target.value)} /> + set('ad_objective', e.target.value)} /> + set('ad_format', e.target.value)} /> + set('target_audience', e.target.value)} /> +
+
Targeting
+ setNested('age_group', e.target.value)} /> + setNested('gender', e.target.value)} /> + setNested('location', e.target.value)} /> + setNested('interests', e.target.value)} /> +
+ set('unique_selling_proposition', e.target.value)} /> + set('offer_details', e.target.value)} /> + set('budget_range', e.target.value)} /> + set('campaign_duration', e.target.value)} /> + set('brand_voice', e.target.value)} /> +
+ + {error &&
{error}
} +
+ ); +}; + +export default AdCopyHITL; diff --git a/frontend/src/components/FacebookWriter/components/CarouselHITL.tsx b/frontend/src/components/FacebookWriter/components/CarouselHITL.tsx new file mode 100644 index 00000000..68075e45 --- /dev/null +++ b/frontend/src/components/FacebookWriter/components/CarouselHITL.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { facebookWriterApi } from '../../../services/facebookWriterApi'; +import { readPrefs, logAssistant } from '../utils/facebookWriterUtils'; + +interface CarouselHITLProps { + args: any; + respond: (data: any) => void; +} + +const CarouselHITL: React.FC = ({ args, respond }) => { + const VALID_TYPES = ['Product showcase','Step-by-step guide','Before/After','Customer testimonials','Features & Benefits','Portfolio showcase','Educational content','Custom']; + + const mapType = (t?: string) => { + const s = (t || '').trim().toLowerCase(); + const exact = VALID_TYPES.find(v => v.toLowerCase() === s); + if (exact) return exact; + if (s.includes('step')) return 'Step-by-step guide'; + if (s.includes('before') || s.includes('after')) return 'Before/After'; + if (s.includes('testi')) return 'Customer testimonials'; + if (s.includes('feature') || s.includes('benefit')) return 'Features & Benefits'; + if (s.includes('portfolio')) return 'Portfolio showcase'; + if (s.includes('educat')) return 'Educational content'; + return 'Product showcase'; + }; + + const prefs = React.useMemo(() => readPrefs(), []); + const [form, setForm] = React.useState({ + business_type: args?.business_type || prefs.business_type || 'SaaS', + target_audience: args?.target_audience || prefs.target_audience || 'Marketing managers at SMEs', + carousel_type: args?.carousel_type || 'Product showcase', + topic: args?.topic || 'Feature breakdown', + num_slides: 5, + include_cta: true, + cta_text: '', + brand_colors: '', + include: '', + avoid: '' + }); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const run = async () => { + try { + setLoading(true); + setError(null); + const payload = { ...form, carousel_type: mapType(form.carousel_type) } as any; + const res = await facebookWriterApi.carouselGenerate(payload); + const main = res?.main_caption || res?.data?.main_caption; + const slides = res?.slides || res?.data?.slides; + let out = ''; + if (main) out += `\n\n${main}`; + if (Array.isArray(slides)) { + out += '\n\nCarousel Slides:'; + slides.forEach((s: any, i: number) => { + out += `\n${i + 1}. ${s.title}: ${s.content}`; + }); + } + if (out) { + window.dispatchEvent(new CustomEvent('fbwriter:appendDraft', { detail: out })); + logAssistant(out); + respond({ success: true, content: out }); + } else { + respond({ success: true, message: 'Carousel generated.' }); + } + } catch (e: any) { + const msg = e?.response?.data?.detail || e?.message || 'Failed to generate carousel'; + setError(`${msg}`); + respond({ success: false, message: `${msg}` }); + } finally { + setLoading(false); + } + }; + + const set = (k: string, v: any) => setForm((p: any) => ({ ...p, [k]: v })); + + return ( +
+
Generate Carousel
+
+ set('business_type', e.target.value)} /> + set('target_audience', e.target.value)} /> + set('carousel_type', e.target.value)} /> + set('topic', e.target.value)} /> + set('num_slides', Number(e.target.value) || 5)} /> + + set('cta_text', e.target.value)} /> + set('brand_colors', e.target.value)} /> + set('include', e.target.value)} /> + set('avoid', e.target.value)} /> +
+ + {error &&
{error}
} +
+ ); +}; + +export default CarouselHITL; diff --git a/frontend/src/components/FacebookWriter/components/EventHITL.tsx b/frontend/src/components/FacebookWriter/components/EventHITL.tsx new file mode 100644 index 00000000..a19064dd --- /dev/null +++ b/frontend/src/components/FacebookWriter/components/EventHITL.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { facebookWriterApi } from '../../../services/facebookWriterApi'; + +interface EventHITLProps { + args: any; + respond: (data: any) => void; +} + +const EventHITL: React.FC = ({ args, respond }) => { + const TYPES = ['Workshop','Webinar','Conference','Networking event','Product launch','Sale/Promotion','Community event','Educational event','Custom']; + const FORMATS = ['In-person','Virtual','Hybrid']; + + const mapType = (t?: string) => { + const s = (t || '').trim().toLowerCase(); + const exact = TYPES.find(v => v.toLowerCase() === s); + if (exact) return exact; + if (s.includes('web')) return 'Webinar'; + if (s.includes('work')) return 'Workshop'; + if (s.includes('network')) return 'Networking event'; + if (s.includes('launch')) return 'Product launch'; + if (s.includes('sale') || s.includes('promo')) return 'Sale/Promotion'; + if (s.includes('communi')) return 'Community event'; + if (s.includes('educat')) return 'Educational event'; + if (s.includes('conf')) return 'Conference'; + return 'Webinar'; + }; + + const mapFormat = (f?: string) => { + const s = (f || '').trim().toLowerCase(); + const exact = FORMATS.find(v => v.toLowerCase() === s); + if (exact) return exact; + if (s.includes('in') || s.includes('person')) return 'In-person'; + if (s.includes('hybr')) return 'Hybrid'; + return 'Virtual'; + }; + + const [form, setForm] = React.useState({ + event_name: args?.event_name || 'Monthly Growth Webinar', + event_type: mapType(args?.event_type) || 'Webinar', + event_format: mapFormat(args?.event_format) || 'Virtual', + business_type: args?.business_type || 'SaaS', + target_audience: args?.target_audience || 'Marketing managers at SMEs', + event_date: args?.event_date || '', + event_time: args?.event_time || '', + location: args?.location || '', + duration: args?.duration || '60 minutes', + key_benefits: args?.key_benefits || '', + speakers: args?.speakers || '', + agenda: args?.agenda || '', + ticket_info: args?.ticket_info || '', + special_offers: args?.special_offers || '', + include: args?.include || '', + avoid: args?.avoid || '' + }); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const run = async () => { + try { + setLoading(true); + setError(null); + const payload = { ...form, event_type: mapType(form.event_type), event_format: mapFormat(form.event_format) } as any; + const res = await facebookWriterApi.eventGenerate(payload); + const title = res?.event_title || res?.data?.event_title; + const desc = res?.event_description || res?.data?.event_description; + let out = ''; + if (title) out += `\n\n${title}`; + if (desc) out += `\n\n${desc}`; + if (out) { + window.dispatchEvent(new CustomEvent('fbwriter:appendDraft', { detail: out })); + respond({ success: true, content: out }); + } else { + respond({ success: true, message: 'Event generated.' }); + } + } catch (e: any) { + const msg = e?.response?.data?.detail || e?.message || 'Failed to generate event'; + setError(`${msg}`); + respond({ success: false, message: `${msg}` }); + } finally { + setLoading(false); + } + }; + + const set = (k: string, v: any) => setForm((p: any) => ({ ...p, [k]: v })); + + return ( +
+
Generate Event
+
+ set('event_name', e.target.value)} /> + set('event_type', e.target.value)} /> + set('event_format', e.target.value)} /> + set('business_type', e.target.value)} /> + set('target_audience', e.target.value)} /> + set('event_date', e.target.value)} /> + set('event_time', e.target.value)} /> + set('location', e.target.value)} /> + set('duration', e.target.value)} /> + set('key_benefits', e.target.value)} /> + set('speakers', e.target.value)} /> + set('agenda', e.target.value)} /> + set('ticket_info', e.target.value)} /> + set('special_offers', e.target.value)} /> + set('include', e.target.value)} /> + set('avoid', e.target.value)} /> +
+ + {error &&
{error}
} +
+ ); +}; + +export default EventHITL; diff --git a/frontend/src/components/FacebookWriter/components/GroupHITL.tsx b/frontend/src/components/FacebookWriter/components/GroupHITL.tsx new file mode 100644 index 00000000..b29ed773 --- /dev/null +++ b/frontend/src/components/FacebookWriter/components/GroupHITL.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { facebookWriterApi } from '../../../services/facebookWriterApi'; + +interface GroupHITLProps { + args: any; + respond: (data: any) => void; +} + +const GroupHITL: React.FC = ({ args, respond }) => { + const TYPES = ['Industry/Professional','Hobby/Interest','Local community','Support group','Educational','Business networking','Lifestyle','Custom']; + const PURPOSES = ['Share knowledge','Ask question','Promote business','Build relationships','Provide value','Seek advice','Announce news','Custom']; + + const mapType = (t?: string) => { + const s = (t || '').trim().toLowerCase(); + const exact = TYPES.find(v => v.toLowerCase() === s); if (exact) return exact; + if (s.includes('industry')) return 'Industry/Professional'; + if (s.includes('hobby') || s.includes('interest')) return 'Hobby/Interest'; + if (s.includes('local')) return 'Local community'; + if (s.includes('support')) return 'Support group'; + if (s.includes('educat')) return 'Educational'; + if (s.includes('business')) return 'Business networking'; + if (s.includes('life')) return 'Lifestyle'; + return 'Industry/Professional'; + }; + + const mapPurpose = (p?: string) => { + const s = (p || '').trim().toLowerCase(); + const exact = PURPOSES.find(v => v.toLowerCase() === s); if (exact) return exact; + if (s.includes('ask')) return 'Ask question'; + if (s.includes('promot')) return 'Promote business'; + if (s.includes('build')) return 'Build relationships'; + if (s.includes('value')) return 'Provide value'; + if (s.includes('advice')) return 'Seek advice'; + if (s.includes('news')) return 'Announce news'; + return 'Share knowledge'; + }; + + const [form, setForm] = React.useState({ + group_name: args?.group_name || 'Marketing Managers Community', + group_type: mapType(args?.group_type) || 'Industry/Professional', + post_purpose: mapPurpose(args?.post_purpose) || 'Share knowledge', + business_type: args?.business_type || 'SaaS', + topic: args?.topic || 'Content strategy tips', + target_audience: args?.target_audience || 'Marketing managers at SMEs', + value_proposition: args?.value_proposition || '3 actionable tips with examples', + group_rules: { no_promotion: true, value_first: true, no_links: true, community_focused: true, relevant_only: true }, + include: '', + avoid: '', + call_to_action: 'Share your approach' + }); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const run = async () => { + try { + setLoading(true); + setError(null); + const res = await facebookWriterApi.groupPostGenerate(form as any); + const content = res?.content || res?.data?.content; + const starters = res?.engagement_starters || res?.data?.engagement_starters; + let out = ''; + if (content) out += `\n\n${content}`; + if (Array.isArray(starters) && starters.length) { + out += '\n\nEngagement starters:'; + starters.forEach((s: string) => out += `\n- ${s}`); + } + if (out) { + window.dispatchEvent(new CustomEvent('fbwriter:appendDraft', { detail: out })); + respond({ success: true, content: out }); + } else { + respond({ success: true, message: 'Group post generated.' }); + } + } catch (e: any) { + const msg = e?.response?.data?.detail || e?.message || 'Failed to generate group post'; + setError(`${msg}`); + respond({ success: false, message: `${msg}` }); + } finally { + setLoading(false); + } + }; + + const set = (k: string, v: any) => setForm((p: any) => ({ ...p, [k]: v })); + + return ( +
+
Generate Group Post
+
+ set('group_name', e.target.value)} /> + set('group_type', e.target.value)} /> + set('post_purpose', e.target.value)} /> + set('business_type', e.target.value)} /> + set('topic', e.target.value)} /> + set('target_audience', e.target.value)} /> + set('value_proposition', e.target.value)} /> + set('include', e.target.value)} /> + set('avoid', e.target.value)} /> + set('call_to_action', e.target.value)} /> +
+ + {error &&
{error}
} +
+ ); +}; + +export default GroupHITL; diff --git a/frontend/src/components/FacebookWriter/components/HashtagsHITL.tsx b/frontend/src/components/FacebookWriter/components/HashtagsHITL.tsx new file mode 100644 index 00000000..b137ffef --- /dev/null +++ b/frontend/src/components/FacebookWriter/components/HashtagsHITL.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { facebookWriterApi } from '../../../services/facebookWriterApi'; +import { logAssistant } from '../utils/facebookWriterUtils'; + +interface HashtagsHITLProps { + args: any; + respond: (data: any) => void; +} + +const HashtagsHITL: React.FC = ({ args, respond }) => { + const [topic, setTopic] = React.useState(args?.content_topic || 'product launch'); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const run = async () => { + try { + setLoading(true); + setError(null); + const res = await facebookWriterApi.hashtagsGenerate({ content_topic: topic }); + const hashtags = res?.hashtags || res?.data?.hashtags; + if (Array.isArray(hashtags) && hashtags.length) { + const line = hashtags.join(' '); + window.dispatchEvent(new CustomEvent('fbwriter:appendDraft', { detail: `\n\n${line}` })); + logAssistant(line); + respond({ success: true, hashtags }); + } else { + respond({ success: true, message: 'Hashtags generated.' }); + } + } catch (e: any) { + const msg = e?.response?.data?.detail || e?.message || 'Failed to generate hashtags'; + setError(`${msg}`); + respond({ success: false, message: `${msg}` }); + console.error('[FB Writer] hashtags.generate error', e); + } finally { + setLoading(false); + } + }; + + return ( +
+
Generate Hashtags
+ setTopic(e.target.value)} /> + + {error &&
{error}
} +
+ ); +}; + +export default HashtagsHITL; diff --git a/frontend/src/components/FacebookWriter/components/PageAboutHITL.tsx b/frontend/src/components/FacebookWriter/components/PageAboutHITL.tsx new file mode 100644 index 00000000..32192081 --- /dev/null +++ b/frontend/src/components/FacebookWriter/components/PageAboutHITL.tsx @@ -0,0 +1,245 @@ +import React from 'react'; +import { facebookWriterApi } from '../../../services/facebookWriterApi'; +import { mapBusinessCategory, mapPageTone, VALID_BUSINESS_CATEGORIES, VALID_PAGE_TONES } from '../utils/facebookWriterUtils'; + +interface PageAboutHITLProps { + args: any; + respond: (data: any) => void; +} + +const PageAboutHITL: React.FC = ({ args, respond }) => { + const [form, setForm] = React.useState({ + business_name: args?.business_name || 'TechStart Solutions', + business_category: mapBusinessCategory(args?.business_category) || 'Technology', + custom_category: args?.custom_category || '', + business_description: args?.business_description || 'We provide innovative software solutions for modern businesses', + target_audience: args?.target_audience || 'Small to medium-sized businesses looking to digitize their operations', + unique_value_proposition: args?.unique_value_proposition || 'Affordable, scalable solutions with 24/7 support', + services_products: args?.services_products || 'Cloud-based CRM, project management tools, and custom software development', + company_history: args?.company_history || '', + mission_vision: args?.mission_vision || '', + achievements: args?.achievements || '', + page_tone: mapPageTone(args?.page_tone) || 'Professional', + custom_tone: args?.custom_tone || '', + contact_info: { + website: args?.contact_info?.website || '', + phone: args?.contact_info?.phone || '', + email: args?.contact_info?.email || '', + address: args?.contact_info?.address || '', + hours: args?.contact_info?.hours || '' + }, + keywords: args?.keywords || '', + call_to_action: args?.call_to_action || '' + }); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const set = (key: string, value: any) => setForm(prev => ({ ...prev, [key]: value })); + const setContact = (key: string, value: any) => setForm(prev => ({ + ...prev, + contact_info: { ...prev.contact_info, [key]: value } + })); + + const run = async () => { + try { + setLoading(true); + setError(null); + + const payload = { + ...form, + business_category: mapBusinessCategory(form.business_category), + page_tone: mapPageTone(form.page_tone) + }; + + const res = await facebookWriterApi.pageAboutGenerate(payload); + const shortDesc = res?.short_description || res?.data?.short_description; + const longDesc = res?.long_description || res?.data?.long_description; + const companyOverview = res?.company_overview || res?.data?.company_overview; + const missionStatement = res?.mission_statement || res?.data?.mission_statement; + const storySection = res?.story_section || res?.data?.story_section; + const servicesSection = res?.services_section || res?.data?.services_section; + const ctaSuggestions = res?.cta_suggestions || res?.data?.cta_suggestions; + const keywordOptimization = res?.keyword_optimization || res?.data?.keyword_optimization; + const completionTips = res?.completion_tips || res?.data?.completion_tips; + + let output = ''; + if (shortDesc) output += `\n\n**Short Description:**\n${shortDesc}`; + if (longDesc) output += `\n\n**Long Description:**\n${longDesc}`; + if (companyOverview) output += `\n\n**Company Overview:**\n${companyOverview}`; + if (missionStatement) output += `\n\n**Mission Statement:**\n${missionStatement}`; + if (storySection) output += `\n\n**Company Story:**\n${storySection}`; + if (servicesSection) output += `\n\n**Services/Products:**\n${servicesSection}`; + + if (Array.isArray(ctaSuggestions) && ctaSuggestions.length) { + output += '\n\n**CTA Suggestions:**'; + ctaSuggestions.forEach((cta: string) => output += `\n- ${cta}`); + } + + if (Array.isArray(keywordOptimization) && keywordOptimization.length) { + output += '\n\n**Keyword Optimization:**'; + keywordOptimization.forEach((keyword: string) => output += `\n- ${keyword}`); + } + + if (Array.isArray(completionTips) && completionTips.length) { + output += '\n\n**Completion Tips:**'; + completionTips.forEach((tip: string) => output += `\n- ${tip}`); + } + + if (output) { + window.dispatchEvent(new CustomEvent('fbwriter:appendDraft', { detail: output })); + respond({ success: true, content: output }); + } else { + respond({ success: true, message: 'Page About content generated.' }); + } + } catch (err: any) { + setError(err?.message || 'Failed to generate page about content'); + respond({ success: false, error: err?.message || 'Generation failed' }); + } finally { + setLoading(false); + } + }; + + return ( +
+

Generate Facebook Page About

+ +
+ set('business_name', e.target.value)} + /> + + + + {form.business_category === 'Custom' && ( + set('custom_category', e.target.value)} + /> + )} + +