ALwrity Facebook Writer CopilotKit Implementation Plan
This commit is contained in:
Binary file not shown.
101
frontend/src/components/FacebookWriter/components/AdCopyHITL.tsx
Normal file
101
frontend/src/components/FacebookWriter/components/AdCopyHITL.tsx
Normal file
@@ -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<AdCopyHITLProps> = ({ 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<string | null>(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 (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>Generate Ad Copy</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<input placeholder="Business type" value={form.business_type} onChange={e => set('business_type', e.target.value)} />
|
||||
<input placeholder="Product/Service" value={form.product_service} onChange={e => set('product_service', e.target.value)} />
|
||||
<input placeholder="Ad objective (e.g., Conversions)" value={form.ad_objective} onChange={e => set('ad_objective', e.target.value)} />
|
||||
<input placeholder="Ad format (e.g., Single image)" value={form.ad_format} onChange={e => set('ad_format', e.target.value)} />
|
||||
<input placeholder="Target audience" value={form.target_audience} onChange={e => set('target_audience', e.target.value)} />
|
||||
<div style={{ display: 'grid', gap: 6 }}>
|
||||
<div style={{ fontSize: 12, opacity: 0.9 }}>Targeting</div>
|
||||
<input placeholder="Age group (e.g., 18-24)" value={form.targeting_options.age_group} onChange={e => setNested('age_group', e.target.value)} />
|
||||
<input placeholder="Gender" value={form.targeting_options.gender || ''} onChange={e => setNested('gender', e.target.value)} />
|
||||
<input placeholder="Location" value={form.targeting_options.location || ''} onChange={e => setNested('location', e.target.value)} />
|
||||
<input placeholder="Interests" value={form.targeting_options.interests || ''} onChange={e => setNested('interests', e.target.value)} />
|
||||
</div>
|
||||
<input placeholder="USP" value={form.unique_selling_proposition} onChange={e => set('unique_selling_proposition', e.target.value)} />
|
||||
<input placeholder="Offer details" value={form.offer_details || ''} onChange={e => set('offer_details', e.target.value)} />
|
||||
<input placeholder="Budget range (e.g., $50-200/day)" value={form.budget_range} onChange={e => set('budget_range', e.target.value)} />
|
||||
<input placeholder="Campaign duration" value={form.campaign_duration || ''} onChange={e => set('campaign_duration', e.target.value)} />
|
||||
<input placeholder="Brand voice" value={form.brand_voice || ''} onChange={e => set('brand_voice', e.target.value)} />
|
||||
</div>
|
||||
<button onClick={run} disabled={loading} style={{ marginTop: 8 }}>{loading ? 'Generating…' : 'Generate'}</button>
|
||||
{error && <div style={{ marginTop: 8, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdCopyHITL;
|
||||
@@ -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<CarouselHITLProps> = ({ 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<string | null>(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 (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>Generate Carousel</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<input placeholder="Business type" value={form.business_type} onChange={e => set('business_type', e.target.value)} />
|
||||
<input placeholder="Target audience" value={form.target_audience} onChange={e => set('target_audience', e.target.value)} />
|
||||
<input placeholder="Carousel type (e.g., Product showcase)" value={form.carousel_type} onChange={e => set('carousel_type', e.target.value)} />
|
||||
<input placeholder="Topic" value={form.topic} onChange={e => set('topic', e.target.value)} />
|
||||
<input placeholder="Number of slides (3-10)" value={form.num_slides} onChange={e => set('num_slides', Number(e.target.value) || 5)} />
|
||||
<label><input type="checkbox" checked={!!form.include_cta} onChange={e => set('include_cta', e.target.checked)} /> Include CTA</label>
|
||||
<input placeholder="CTA text" value={form.cta_text} onChange={e => set('cta_text', e.target.value)} />
|
||||
<input placeholder="Brand colors" value={form.brand_colors} onChange={e => set('brand_colors', e.target.value)} />
|
||||
<input placeholder="Include" value={form.include} onChange={e => set('include', e.target.value)} />
|
||||
<input placeholder="Avoid" value={form.avoid} onChange={e => set('avoid', e.target.value)} />
|
||||
</div>
|
||||
<button onClick={run} disabled={loading} style={{ marginTop: 8 }}>{loading ? 'Generating…' : 'Generate'}</button>
|
||||
{error && <div style={{ marginTop: 8, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CarouselHITL;
|
||||
113
frontend/src/components/FacebookWriter/components/EventHITL.tsx
Normal file
113
frontend/src/components/FacebookWriter/components/EventHITL.tsx
Normal file
@@ -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<EventHITLProps> = ({ 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<string | null>(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 (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>Generate Event</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<input placeholder="Event name" value={form.event_name} onChange={e => set('event_name', e.target.value)} />
|
||||
<input placeholder="Event type (e.g., Webinar)" value={form.event_type} onChange={e => set('event_type', e.target.value)} />
|
||||
<input placeholder="Format (In-person/Virtual/Hybrid)" value={form.event_format} onChange={e => set('event_format', e.target.value)} />
|
||||
<input placeholder="Business type" value={form.business_type} onChange={e => set('business_type', e.target.value)} />
|
||||
<input placeholder="Target audience" value={form.target_audience} onChange={e => set('target_audience', e.target.value)} />
|
||||
<input placeholder="Date (YYYY-MM-DD)" value={form.event_date} onChange={e => set('event_date', e.target.value)} />
|
||||
<input placeholder="Time" value={form.event_time} onChange={e => set('event_time', e.target.value)} />
|
||||
<input placeholder="Location" value={form.location} onChange={e => set('location', e.target.value)} />
|
||||
<input placeholder="Duration" value={form.duration} onChange={e => set('duration', e.target.value)} />
|
||||
<input placeholder="Key benefits" value={form.key_benefits} onChange={e => set('key_benefits', e.target.value)} />
|
||||
<input placeholder="Speakers" value={form.speakers} onChange={e => set('speakers', e.target.value)} />
|
||||
<input placeholder="Agenda" value={form.agenda} onChange={e => set('agenda', e.target.value)} />
|
||||
<input placeholder="Ticket info" value={form.ticket_info} onChange={e => set('ticket_info', e.target.value)} />
|
||||
<input placeholder="Special offers" value={form.special_offers} onChange={e => set('special_offers', e.target.value)} />
|
||||
<input placeholder="Include" value={form.include} onChange={e => set('include', e.target.value)} />
|
||||
<input placeholder="Avoid" value={form.avoid} onChange={e => set('avoid', e.target.value)} />
|
||||
</div>
|
||||
<button onClick={run} disabled={loading} style={{ marginTop: 8 }}>{loading ? 'Generating…' : 'Generate'}</button>
|
||||
{error && <div style={{ marginTop: 8, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventHITL;
|
||||
105
frontend/src/components/FacebookWriter/components/GroupHITL.tsx
Normal file
105
frontend/src/components/FacebookWriter/components/GroupHITL.tsx
Normal file
@@ -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<GroupHITLProps> = ({ 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<string | null>(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 (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>Generate Group Post</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<input placeholder="Group name" value={form.group_name} onChange={e => set('group_name', e.target.value)} />
|
||||
<input placeholder="Group type (e.g., Industry/Professional)" value={form.group_type} onChange={e => set('group_type', e.target.value)} />
|
||||
<input placeholder="Purpose (e.g., Share knowledge)" value={form.post_purpose} onChange={e => set('post_purpose', e.target.value)} />
|
||||
<input placeholder="Business type" value={form.business_type} onChange={e => set('business_type', e.target.value)} />
|
||||
<input placeholder="Topic" value={form.topic} onChange={e => set('topic', e.target.value)} />
|
||||
<input placeholder="Target audience" value={form.target_audience} onChange={e => set('target_audience', e.target.value)} />
|
||||
<input placeholder="Value proposition" value={form.value_proposition} onChange={e => set('value_proposition', e.target.value)} />
|
||||
<input placeholder="Include" value={form.include} onChange={e => set('include', e.target.value)} />
|
||||
<input placeholder="Avoid" value={form.avoid} onChange={e => set('avoid', e.target.value)} />
|
||||
<input placeholder="Call to action" value={form.call_to_action || ''} onChange={e => set('call_to_action', e.target.value)} />
|
||||
</div>
|
||||
<button onClick={run} disabled={loading} style={{ marginTop: 8 }}>{loading ? 'Generating…' : 'Generate'}</button>
|
||||
{error && <div style={{ marginTop: 8, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupHITL;
|
||||
@@ -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<HashtagsHITLProps> = ({ args, respond }) => {
|
||||
const [topic, setTopic] = React.useState<string>(args?.content_topic || 'product launch');
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(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 (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>Generate Hashtags</div>
|
||||
<input placeholder="Topic" value={topic} onChange={e => setTopic(e.target.value)} />
|
||||
<button onClick={run} disabled={loading} style={{ marginLeft: 8 }}>{loading ? 'Generating…' : 'Generate'}</button>
|
||||
{error && <div style={{ marginTop: 8, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HashtagsHITL;
|
||||
@@ -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<PageAboutHITLProps> = ({ 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<string | null>(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 (
|
||||
<div style={{ padding: 16, background: '#f5f5f5', borderRadius: 8, marginBottom: 16 }}>
|
||||
<h4 style={{ margin: '0 0 12px 0', color: '#333' }}>Generate Facebook Page About</h4>
|
||||
|
||||
<div style={{ display: 'grid', gap: 8, fontSize: 14 }}>
|
||||
<input
|
||||
placeholder="Business name"
|
||||
value={form.business_name}
|
||||
onChange={e => set('business_name', e.target.value)}
|
||||
/>
|
||||
|
||||
<select
|
||||
value={form.business_category}
|
||||
onChange={e => set('business_category', e.target.value)}
|
||||
>
|
||||
{VALID_BUSINESS_CATEGORIES.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{form.business_category === 'Custom' && (
|
||||
<input
|
||||
placeholder="Custom business category"
|
||||
value={form.custom_category}
|
||||
onChange={e => set('custom_category', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<textarea
|
||||
placeholder="Business description"
|
||||
value={form.business_description}
|
||||
onChange={e => set('business_description', e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
placeholder="Target audience"
|
||||
value={form.target_audience}
|
||||
onChange={e => set('target_audience', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
placeholder="Unique value proposition"
|
||||
value={form.unique_value_proposition}
|
||||
onChange={e => set('unique_value_proposition', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
placeholder="Services/products offered"
|
||||
value={form.services_products}
|
||||
onChange={e => set('services_products', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
placeholder="Company history (optional)"
|
||||
value={form.company_history}
|
||||
onChange={e => set('company_history', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
placeholder="Mission/vision (optional)"
|
||||
value={form.mission_vision}
|
||||
onChange={e => set('mission_vision', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
placeholder="Achievements/awards (optional)"
|
||||
value={form.achievements}
|
||||
onChange={e => set('achievements', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<select
|
||||
value={form.page_tone}
|
||||
onChange={e => set('page_tone', e.target.value)}
|
||||
>
|
||||
{VALID_PAGE_TONES.map(tone => (
|
||||
<option key={tone} value={tone}>{tone}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{form.page_tone === 'Custom' && (
|
||||
<input
|
||||
placeholder="Custom page tone"
|
||||
value={form.custom_tone}
|
||||
onChange={e => set('custom_tone', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ fontWeight: 600, marginTop: 8 }}>Contact Information (Optional)</div>
|
||||
<input
|
||||
placeholder="Website URL"
|
||||
value={form.contact_info.website}
|
||||
onChange={e => setContact('website', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
placeholder="Phone number"
|
||||
value={form.contact_info.phone}
|
||||
onChange={e => setContact('phone', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
placeholder="Email address"
|
||||
value={form.contact_info.email}
|
||||
onChange={e => setContact('email', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
placeholder="Physical address"
|
||||
value={form.contact_info.address}
|
||||
onChange={e => setContact('address', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
placeholder="Business hours"
|
||||
value={form.contact_info.hours}
|
||||
onChange={e => setContact('hours', e.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
placeholder="Important keywords to include"
|
||||
value={form.keywords}
|
||||
onChange={e => set('keywords', e.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
placeholder="Primary call-to-action"
|
||||
value={form.call_to_action}
|
||||
onChange={e => set('call_to_action', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button onClick={run} disabled={loading} style={{ marginTop: 12, width: '100%' }}>
|
||||
{loading ? 'Generating...' : 'Generate Page About'}
|
||||
</button>
|
||||
|
||||
{error && <div style={{ marginTop: 8, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageAboutHITL;
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { facebookWriterApi, PostGenerateRequest } from '../../../services/facebookWriterApi';
|
||||
import { readPrefs, writePrefs, logAssistant, mapGoal, mapTone, VALID_GOALS, VALID_TONES } from '../utils/facebookWriterUtils';
|
||||
|
||||
interface PostHITLProps {
|
||||
args: any;
|
||||
respond: (data: any) => void;
|
||||
}
|
||||
|
||||
const PostHITL: React.FC<PostHITLProps> = ({ args, respond }) => {
|
||||
const prefs = React.useMemo(() => readPrefs(), []);
|
||||
const [form, setForm] = React.useState<PostGenerateRequest>({
|
||||
business_type: args?.business_type || prefs.business_type || 'SaaS',
|
||||
target_audience: args?.target_audience || prefs.target_audience || 'Marketing managers at SMEs',
|
||||
post_goal: args?.post_goal || prefs.post_goal || 'Build brand awareness',
|
||||
post_tone: args?.post_tone || prefs.post_tone || 'Professional',
|
||||
include: args?.include || prefs.include || '',
|
||||
avoid: args?.avoid || prefs.avoid || '',
|
||||
media_type: args?.media_type || 'None',
|
||||
advanced_options: { use_hook: true, use_story: true, use_cta: true, use_question: true, use_emoji: true, use_hashtags: true }
|
||||
});
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const payload: PostGenerateRequest = {
|
||||
...form,
|
||||
post_goal: mapGoal(form.post_goal),
|
||||
post_tone: mapTone(form.post_tone),
|
||||
media_type: 'None'
|
||||
};
|
||||
// Save user preference snapshot
|
||||
writePrefs({
|
||||
business_type: payload.business_type,
|
||||
target_audience: payload.target_audience,
|
||||
post_goal: payload.post_goal,
|
||||
post_tone: payload.post_tone,
|
||||
include: payload.include,
|
||||
avoid: payload.avoid
|
||||
});
|
||||
const res = await facebookWriterApi.postGenerate(payload);
|
||||
const content = res?.content || res?.data?.content;
|
||||
if (content) {
|
||||
window.dispatchEvent(new CustomEvent('fbwriter:updateDraft', { detail: content }));
|
||||
logAssistant(content);
|
||||
respond({ success: true, content });
|
||||
} else {
|
||||
respond({ success: true, message: 'Post generated.' });
|
||||
}
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.detail || e?.message || 'Failed to generate post';
|
||||
const tip = `Tip: goals must be one of ${VALID_GOALS.join(', ')}; tones must be one of ${VALID_TONES.join(', ')}.`;
|
||||
setError(`${msg}`);
|
||||
respond({ success: false, message: `${msg}` });
|
||||
console.error('[FB Writer] post.generate error', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const set = (k: keyof PostGenerateRequest, v: any) => setForm(prev => ({ ...prev, [k]: v }));
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>Generate Facebook Post</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<input placeholder={`Business type`} value={form.business_type} onChange={e => set('business_type', e.target.value)} />
|
||||
<input placeholder={`Target audience`} value={form.target_audience} onChange={e => set('target_audience', e.target.value)} />
|
||||
<input placeholder={`Goal (e.g., ${VALID_GOALS[3]})`} value={form.post_goal} onChange={e => set('post_goal', e.target.value)} />
|
||||
<input placeholder={`Tone (e.g., ${VALID_TONES[5]})`} value={form.post_tone} onChange={e => set('post_tone', e.target.value)} />
|
||||
<input placeholder="Include" value={form.include || ''} onChange={e => set('include', e.target.value)} />
|
||||
<input placeholder="Avoid" value={form.avoid || ''} onChange={e => set('avoid', e.target.value)} />
|
||||
</div>
|
||||
<button onClick={run} disabled={loading} style={{ marginTop: 8 }}>{loading ? 'Generating…' : 'Generate'}</button>
|
||||
{error && <div style={{ marginTop: 8, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostHITL;
|
||||
116
frontend/src/components/FacebookWriter/components/ReelHITL.tsx
Normal file
116
frontend/src/components/FacebookWriter/components/ReelHITL.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { facebookWriterApi } from '../../../services/facebookWriterApi';
|
||||
import { readPrefs, logAssistant } from '../utils/facebookWriterUtils';
|
||||
|
||||
interface ReelHITLProps {
|
||||
args: any;
|
||||
respond: (data: any) => void;
|
||||
}
|
||||
|
||||
const ReelHITL: React.FC<ReelHITLProps> = ({ args, respond }) => {
|
||||
const VALID_REEL_TYPES = ['Product demonstration','Tutorial/How-to','Entertainment','Educational','Trend-based','Behind the scenes','User-generated content','Custom'];
|
||||
const VALID_REEL_LENGTHS = ['15-30 seconds','30-60 seconds','60-90 seconds'];
|
||||
const VALID_REEL_STYLES = ['Fast-paced','Relaxed','Dramatic','Minimalist','Vibrant','Custom'];
|
||||
|
||||
const mapReelType = (t?: string) => {
|
||||
const s = (t || '').trim().toLowerCase();
|
||||
const exact = VALID_REEL_TYPES.find(v => v.toLowerCase() === s);
|
||||
if (exact) return exact;
|
||||
if (s.includes('product')) return 'Product demonstration';
|
||||
if (s.includes('tutorial') || s.includes('how')) return 'Tutorial/How-to';
|
||||
if (s.includes('behind')) return 'Behind the scenes';
|
||||
if (s.includes('user')) return 'User-generated content';
|
||||
if (s.includes('trend')) return 'Trend-based';
|
||||
if (s.includes('educat')) return 'Educational';
|
||||
if (s.includes('entertain')) return 'Entertainment';
|
||||
return 'Product demonstration';
|
||||
};
|
||||
|
||||
const mapReelLength = (l?: string) => {
|
||||
const s = (l || '').trim().toLowerCase();
|
||||
const exact = VALID_REEL_LENGTHS.find(v => v.toLowerCase() === s);
|
||||
if (exact) return exact;
|
||||
if (s.includes('15')) return '15-30 seconds';
|
||||
if (s.includes('60') || s.includes('30-60')) return '30-60 seconds';
|
||||
if (s.includes('90') || s.includes('60-90')) return '60-90 seconds';
|
||||
return '30-60 seconds';
|
||||
};
|
||||
|
||||
const mapReelStyle = (st?: string) => {
|
||||
const s = (st || '').trim().toLowerCase();
|
||||
const exact = VALID_REEL_STYLES.find(v => v.toLowerCase() === s);
|
||||
if (exact) return exact;
|
||||
if (s.includes('fast')) return 'Fast-paced';
|
||||
if (s.includes('relax')) return 'Relaxed';
|
||||
if (s.includes('dram')) return 'Dramatic';
|
||||
if (s.includes('mini')) return 'Minimalist';
|
||||
if (s.includes('vibr')) return 'Vibrant';
|
||||
return 'Fast-paced';
|
||||
};
|
||||
|
||||
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',
|
||||
reel_type: args?.reel_type || 'Product demonstration',
|
||||
reel_length: args?.reel_length || '30-60 seconds',
|
||||
reel_style: args?.reel_style || 'Fast-paced',
|
||||
topic: args?.topic || 'Feature walkthrough',
|
||||
include: args?.include || '',
|
||||
avoid: args?.avoid || '',
|
||||
music_preference: args?.music_preference || ''
|
||||
});
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const payload = {
|
||||
...form,
|
||||
reel_type: mapReelType(form.reel_type),
|
||||
reel_length: mapReelLength(form.reel_length),
|
||||
reel_style: mapReelStyle(form.reel_style)
|
||||
} as any;
|
||||
const res = await facebookWriterApi.reelGenerate(payload);
|
||||
const script = res?.script || res?.data?.script;
|
||||
if (script) {
|
||||
window.dispatchEvent(new CustomEvent('fbwriter:appendDraft', { detail: `\n\n${script}` }));
|
||||
logAssistant(script);
|
||||
respond({ success: true, content: script });
|
||||
} else {
|
||||
respond({ success: true, message: 'Reel generated.' });
|
||||
}
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.detail || e?.message || 'Failed to generate reel. Tip: type should be one of ' + VALID_REEL_TYPES.join(', ') + '; length one of ' + VALID_REEL_LENGTHS.join(', ') + '; style one of ' + VALID_REEL_STYLES.join(', ') + '.';
|
||||
setError(`${msg}`);
|
||||
respond({ success: false, message: `${msg}` });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const set = (k: string, v: any) => setForm((p: any) => ({ ...p, [k]: v }));
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>Generate Reel</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<input placeholder="Business type" value={form.business_type} onChange={e => set('business_type', e.target.value)} />
|
||||
<input placeholder="Target audience" value={form.target_audience} onChange={e => set('target_audience', e.target.value)} />
|
||||
<input placeholder="Reel type (e.g., Product demonstration)" value={form.reel_type} onChange={e => set('reel_type', e.target.value)} />
|
||||
<input placeholder="Length (e.g., 30-60 seconds)" value={form.reel_length} onChange={e => set('reel_length', e.target.value)} />
|
||||
<input placeholder="Style (e.g., Fast-paced)" value={form.reel_style} onChange={e => set('reel_style', e.target.value)} />
|
||||
<input placeholder="Topic" value={form.topic} onChange={e => set('topic', e.target.value)} />
|
||||
<input placeholder="Include" value={form.include} onChange={e => set('include', e.target.value)} />
|
||||
<input placeholder="Avoid" value={form.avoid} onChange={e => set('avoid', e.target.value)} />
|
||||
<input placeholder="Music preference" value={form.music_preference} onChange={e => set('music_preference', e.target.value)} />
|
||||
</div>
|
||||
<button onClick={run} disabled={loading} style={{ marginTop: 8 }}>{loading ? 'Generating…' : 'Generate'}</button>
|
||||
{error && <div style={{ marginTop: 8, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReelHITL;
|
||||
119
frontend/src/components/FacebookWriter/components/StoryHITL.tsx
Normal file
119
frontend/src/components/FacebookWriter/components/StoryHITL.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import { facebookWriterApi } from '../../../services/facebookWriterApi';
|
||||
import { readPrefs, logAssistant, mapStoryType, mapStoryTone } from '../utils/facebookWriterUtils';
|
||||
|
||||
interface StoryHITLProps {
|
||||
args: any;
|
||||
respond?: (data: any) => void;
|
||||
}
|
||||
|
||||
const StoryHITL: React.FC<StoryHITLProps> = ({ args, respond }) => {
|
||||
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',
|
||||
story_type: mapStoryType(args?.story_type) || 'Product showcase',
|
||||
story_tone: mapStoryTone(args?.story_tone) || 'Casual',
|
||||
include: args?.include || '',
|
||||
avoid: args?.avoid || '',
|
||||
// Advanced options
|
||||
use_hook: true,
|
||||
use_story: true,
|
||||
use_cta: true,
|
||||
use_question: true,
|
||||
use_emoji: true,
|
||||
use_hashtags: true,
|
||||
// Visual options
|
||||
visual_options: {
|
||||
background_type: args?.visual_options?.background_type || 'Solid color',
|
||||
background_image_prompt: args?.visual_options?.background_image_prompt || '',
|
||||
gradient_style: args?.visual_options?.gradient_style || '',
|
||||
text_overlay: args?.visual_options?.text_overlay ?? true,
|
||||
text_style: args?.visual_options?.text_style || '',
|
||||
text_color: args?.visual_options?.text_color || '',
|
||||
text_position: args?.visual_options?.text_position || '',
|
||||
stickers: args?.visual_options?.stickers ?? true,
|
||||
interactive_elements: args?.visual_options?.interactive_elements ?? true,
|
||||
interactive_types: args?.visual_options?.interactive_types || [],
|
||||
call_to_action: args?.visual_options?.call_to_action || ''
|
||||
}
|
||||
});
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const safeRespond = (d: any) => { try { if (typeof respond === 'function') respond(d); } catch {} };
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const payload = {
|
||||
...form,
|
||||
story_type: mapStoryType(form.story_type),
|
||||
story_tone: mapStoryTone(form.story_tone),
|
||||
visual_options: {
|
||||
...form.visual_options,
|
||||
interactive_types: Array.isArray(form.visual_options?.interactive_types)
|
||||
? form.visual_options?.interactive_types
|
||||
: []
|
||||
}
|
||||
} as any;
|
||||
const res = await facebookWriterApi.storyGenerate(payload);
|
||||
const content = res?.content || res?.data?.content;
|
||||
if (content) {
|
||||
window.dispatchEvent(new CustomEvent('fbwriter:appendDraft', { detail: `\n\n${content}` }));
|
||||
logAssistant(content);
|
||||
safeRespond({ success: true, content });
|
||||
} else {
|
||||
safeRespond({ success: true, message: 'Story generated.' });
|
||||
}
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.detail || e?.message || 'Failed to generate story';
|
||||
setError(`${msg}`);
|
||||
safeRespond({ success: false, message: `${msg}` });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const set = (k: string, v: any) => setForm((prev: any) => ({ ...prev, [k]: v }));
|
||||
const setVisual = (k: string, v: any) => setForm((prev: any) => ({ ...prev, visual_options: { ...prev.visual_options, [k]: v } }));
|
||||
const parseInteractive = (s: string): string[] => s.split(',').map(x => x.trim()).filter(Boolean);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>Generate Story</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<input placeholder="Business type" value={form.business_type} onChange={e => set('business_type', e.target.value)} />
|
||||
<input placeholder="Target audience" value={form.target_audience} onChange={e => set('target_audience', e.target.value)} />
|
||||
<input placeholder="Story type (e.g., Product showcase)" value={form.story_type} onChange={e => set('story_type', e.target.value)} />
|
||||
<input placeholder="Tone (e.g., Casual)" value={form.story_tone} onChange={e => set('story_tone', e.target.value)} />
|
||||
<input placeholder="Include" value={form.include} onChange={e => set('include', e.target.value)} />
|
||||
<input placeholder="Avoid" value={form.avoid} onChange={e => set('avoid', e.target.value)} />
|
||||
<div style={{ marginTop: 8, fontWeight: 600 }}>Advanced options</div>
|
||||
<label><input type="checkbox" checked={form.use_hook} onChange={e => set('use_hook', e.target.checked)} /> Hook</label>
|
||||
<label><input type="checkbox" checked={form.use_story} onChange={e => set('use_story', e.target.checked)} /> Narrative</label>
|
||||
<label><input type="checkbox" checked={form.use_cta} onChange={e => set('use_cta', e.target.checked)} /> Include CTA</label>
|
||||
<label><input type="checkbox" checked={form.use_question} onChange={e => set('use_question', e.target.checked)} /> Ask question</label>
|
||||
<label><input type="checkbox" checked={form.use_emoji} onChange={e => set('use_emoji', e.target.checked)} /> Emojis</label>
|
||||
<label><input type="checkbox" checked={form.use_hashtags} onChange={e => set('use_hashtags', e.target.checked)} /> Hashtags</label>
|
||||
|
||||
<div style={{ marginTop: 8, fontWeight: 600 }}>Visual options</div>
|
||||
<input placeholder="Background type (Solid color, Gradient, Image, Video)" value={form.visual_options.background_type} onChange={e => setVisual('background_type', e.target.value)} />
|
||||
<input placeholder="Background image/video prompt (if applicable)" value={form.visual_options.background_image_prompt || ''} onChange={e => setVisual('background_image_prompt', e.target.value)} />
|
||||
<input placeholder="Gradient style" value={form.visual_options.gradient_style || ''} onChange={e => setVisual('gradient_style', e.target.value)} />
|
||||
<label><input type="checkbox" checked={!!form.visual_options.text_overlay} onChange={e => setVisual('text_overlay', e.target.checked)} /> Text overlay</label>
|
||||
<input placeholder="Text style" value={form.visual_options.text_style || ''} onChange={e => setVisual('text_style', e.target.value)} />
|
||||
<input placeholder="Text color" value={form.visual_options.text_color || ''} onChange={e => setVisual('text_color', e.target.value)} />
|
||||
<input placeholder="Text position (e.g., Top-Left)" value={form.visual_options.text_position || ''} onChange={e => setVisual('text_position', e.target.value)} />
|
||||
<label><input type="checkbox" checked={!!form.visual_options.stickers} onChange={e => setVisual('stickers', e.target.checked)} /> Stickers/Emojis</label>
|
||||
<label><input type="checkbox" checked={!!form.visual_options.interactive_elements} onChange={e => setVisual('interactive_elements', e.target.checked)} /> Interactive elements</label>
|
||||
<input placeholder="Interactive types (comma-separated: poll,quiz,slider,countdown)" value={(form.visual_options.interactive_types || []).join(', ')} onChange={e => setVisual('interactive_types', parseInteractive(e.target.value))} />
|
||||
<input placeholder="CTA overlay text" value={form.visual_options.call_to_action || ''} onChange={e => setVisual('call_to_action', e.target.value)} />
|
||||
</div>
|
||||
<button onClick={run} disabled={loading} style={{ marginTop: 8 }}>{loading ? 'Generating…' : 'Generate'}</button>
|
||||
{error && <div style={{ marginTop: 8, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StoryHITL;
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as PostHITL } from './PostHITL';
|
||||
export { default as HashtagsHITL } from './HashtagsHITL';
|
||||
export { default as GroupHITL } from './GroupHITL';
|
||||
export { default as EventHITL } from './EventHITL';
|
||||
export { default as CarouselHITL } from './CarouselHITL';
|
||||
export { default as ReelHITL } from './ReelHITL';
|
||||
export { default as AdCopyHITL } from './AdCopyHITL';
|
||||
export { default as StoryHITL } from './StoryHITL';
|
||||
export { default as PageAboutHITL } from './PageAboutHITL';
|
||||
@@ -0,0 +1,172 @@
|
||||
// Facebook Writer Utilities
|
||||
export const PREFS_KEY = 'fbwriter:preferences';
|
||||
|
||||
// Validation constants
|
||||
export const VALID_GOALS = [
|
||||
'Promote a product/service',
|
||||
'Share valuable content',
|
||||
'Increase engagement',
|
||||
'Build brand awareness',
|
||||
'Drive website traffic',
|
||||
'Generate leads',
|
||||
'Announce news/updates',
|
||||
'Custom'
|
||||
];
|
||||
|
||||
export const VALID_TONES = [
|
||||
'Informative',
|
||||
'Humorous',
|
||||
'Inspirational',
|
||||
'Upbeat',
|
||||
'Casual',
|
||||
'Professional',
|
||||
'Conversational',
|
||||
'Custom'
|
||||
];
|
||||
|
||||
export const VALID_STORY_TYPES = [
|
||||
'Product showcase',
|
||||
'Behind the scenes',
|
||||
'User testimonial',
|
||||
'Event promotion',
|
||||
'Tutorial/How-to',
|
||||
'Question/Poll',
|
||||
'Announcement',
|
||||
'Custom'
|
||||
];
|
||||
|
||||
export const VALID_STORY_TONES = [
|
||||
'Casual',
|
||||
'Fun',
|
||||
'Professional',
|
||||
'Inspirational',
|
||||
'Educational',
|
||||
'Entertaining',
|
||||
'Custom'
|
||||
];
|
||||
|
||||
export const VALID_BUSINESS_CATEGORIES = [
|
||||
'Retail', 'Restaurant/Food', 'Health & Fitness', 'Education', 'Technology',
|
||||
'Consulting', 'Creative Services', 'Non-profit', 'Entertainment', 'Real Estate',
|
||||
'Automotive', 'Beauty & Personal Care', 'Finance', 'Travel & Tourism', 'Custom'
|
||||
];
|
||||
|
||||
export const VALID_PAGE_TONES = [
|
||||
'Professional', 'Friendly', 'Innovative', 'Trustworthy', 'Creative',
|
||||
'Approachable', 'Authoritative', 'Custom'
|
||||
];
|
||||
|
||||
// Utility functions
|
||||
export function readPrefs(): Record<string, any> {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(PREFS_KEY) || '{}') || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function writePrefs(p: Record<string, any>) {
|
||||
try {
|
||||
localStorage.setItem(PREFS_KEY, JSON.stringify(p));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function logAssistant(content: string) {
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent('fbwriter:assistantMessage', { detail: { content } }));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function normalizeEnum(input: string | undefined | null): string {
|
||||
return (input || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function mapGoal(goal: string | undefined): string {
|
||||
const g = normalizeEnum(goal);
|
||||
if (!g) return 'Build brand awareness';
|
||||
const exact = VALID_GOALS.find(v => v.toLowerCase() === g);
|
||||
if (exact) return exact;
|
||||
if (g.includes('announce')) return 'Announce news/updates';
|
||||
if (g.includes('awareness') || g.includes('brand')) return 'Build brand awareness';
|
||||
if (g.includes('engagement') || g.includes('engage')) return 'Increase engagement';
|
||||
if (g.includes('traffic')) return 'Drive website traffic';
|
||||
if (g.includes('lead')) return 'Generate leads';
|
||||
if (g.includes('share') || g.includes('content')) return 'Share valuable content';
|
||||
if (g.includes('promote') || g.includes('product') || g.includes('service')) return 'Promote a product/service';
|
||||
return 'Build brand awareness';
|
||||
}
|
||||
|
||||
export function mapTone(tone: string | undefined): string {
|
||||
const t = normalizeEnum(tone);
|
||||
if (!t) return 'Professional';
|
||||
const exact = VALID_TONES.find(v => v.toLowerCase() === t);
|
||||
if (exact) return exact;
|
||||
if (t.includes('friendly') || t.includes('casual')) return 'Casual';
|
||||
if (t.includes('professional') || t.includes('pro')) return 'Professional';
|
||||
if (t.includes('exciting') || t.includes('energetic') || t.includes('upbeat')) return 'Upbeat';
|
||||
if (t.includes('inspir')) return 'Inspirational';
|
||||
if (t.includes('humor') || t.includes('funny')) return 'Humorous';
|
||||
if (t.includes('convers')) return 'Conversational';
|
||||
if (t.includes('info')) return 'Informative';
|
||||
return 'Professional';
|
||||
}
|
||||
|
||||
export function mapStoryType(t?: string) {
|
||||
const s = (t || '').trim().toLowerCase();
|
||||
const exact = VALID_STORY_TYPES.find(v => v.toLowerCase() === s);
|
||||
if (exact) return exact;
|
||||
if (s.includes('behind')) return 'Behind the scenes';
|
||||
if (s.includes('testi')) return 'User testimonial';
|
||||
if (s.includes('event')) return 'Event promotion';
|
||||
if (s.includes('tutorial') || s.includes('how')) return 'Tutorial/How-to';
|
||||
if (s.includes('poll') || s.includes('question')) return 'Question/Poll';
|
||||
if (s.includes('announce')) return 'Announcement';
|
||||
return 'Product showcase';
|
||||
}
|
||||
|
||||
export function mapStoryTone(t?: string) {
|
||||
const s = (t || '').trim().toLowerCase();
|
||||
const exact = VALID_STORY_TONES.find(v => v.toLowerCase() === s);
|
||||
if (exact) return exact;
|
||||
if (s.includes('fun')) return 'Fun';
|
||||
if (s.includes('inspir')) return 'Inspirational';
|
||||
if (s.includes('educat')) return 'Educational';
|
||||
if (s.includes('entertain')) return 'Entertaining';
|
||||
if (s.includes('pro')) return 'Professional';
|
||||
return 'Casual';
|
||||
}
|
||||
|
||||
export function mapBusinessCategory(cat?: string) {
|
||||
const s = (cat || '').trim().toLowerCase();
|
||||
const exact = VALID_BUSINESS_CATEGORIES.find(v => v.toLowerCase() === s);
|
||||
if (exact) return exact;
|
||||
if (s.includes('tech') || s.includes('software')) return 'Technology';
|
||||
if (s.includes('health') || s.includes('fitness')) return 'Health & Fitness';
|
||||
if (s.includes('food') || s.includes('restaurant')) return 'Restaurant/Food';
|
||||
if (s.includes('retail') || s.includes('shop')) return 'Retail';
|
||||
if (s.includes('educat')) return 'Education';
|
||||
if (s.includes('consult')) return 'Consulting';
|
||||
if (s.includes('creative') || s.includes('design')) return 'Creative Services';
|
||||
if (s.includes('non') || s.includes('charity')) return 'Non-profit';
|
||||
if (s.includes('entertain')) return 'Entertainment';
|
||||
if (s.includes('real') || s.includes('property')) return 'Real Estate';
|
||||
if (s.includes('auto') || s.includes('car')) return 'Automotive';
|
||||
if (s.includes('beauty') || s.includes('personal')) return 'Beauty & Personal Care';
|
||||
if (s.includes('finance') || s.includes('bank')) return 'Finance';
|
||||
if (s.includes('travel') || s.includes('tourism')) return 'Travel & Tourism';
|
||||
return 'Technology';
|
||||
}
|
||||
|
||||
export function mapPageTone(tone?: string) {
|
||||
const s = (tone || '').trim().toLowerCase();
|
||||
const exact = VALID_PAGE_TONES.find(v => v.toLowerCase() === s);
|
||||
if (exact) return exact;
|
||||
if (s.includes('profession')) return 'Professional';
|
||||
if (s.includes('friend')) return 'Friendly';
|
||||
if (s.includes('innov')) return 'Innovative';
|
||||
if (s.includes('trust')) return 'Trustworthy';
|
||||
if (s.includes('creativ')) return 'Creative';
|
||||
if (s.includes('approach')) return 'Approachable';
|
||||
if (s.includes('authorit')) return 'Authoritative';
|
||||
return 'Professional';
|
||||
}
|
||||
1
how --name-only HEAD
Normal file
1
how --name-only HEAD
Normal file
@@ -0,0 +1 @@
|
||||
[33m58918d3[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mcursor/migrate-linkedin-writer-to-fastapi-backend-225b[m[33m, [m[1;31morigin/cursor/migrate-linkedin-writer-to-fastapi-backend-225b[m[33m)[m Add LinkedIn content generation service to backend
|
||||
Reference in New Issue
Block a user