ALwrity Prompts - AI Integration Plan
This commit is contained in:
@@ -5,7 +5,7 @@ import '@copilotkit/react-ui/styles.css';
|
||||
import './styles/alwrity-copilot.css';
|
||||
import RegisterLinkedInActions from './RegisterLinkedInActions';
|
||||
import RegisterLinkedInEditActions from './RegisterLinkedInEditActions';
|
||||
import { Header, ContentEditor, LoadingIndicator, WelcomeMessage } from './components';
|
||||
import { Header, ContentEditor, LoadingIndicator, WelcomeMessage, ProgressTracker } from './components';
|
||||
import { useLinkedInWriter } from './hooks/useLinkedInWriter';
|
||||
import { useCopilotPersistence } from './utils/enhancedPersistence';
|
||||
|
||||
@@ -34,6 +34,7 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
|
||||
showPreferencesModal,
|
||||
showContextModal,
|
||||
showPreview,
|
||||
justGeneratedContent,
|
||||
|
||||
// Grounding data
|
||||
researchSources,
|
||||
@@ -41,6 +42,8 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
|
||||
qualityMetrics,
|
||||
groundingEnabled,
|
||||
searchQueries,
|
||||
progressSteps,
|
||||
progressActive,
|
||||
|
||||
// Setters
|
||||
setDraft,
|
||||
@@ -65,7 +68,9 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
|
||||
summarizeHistory
|
||||
} = useLinkedInWriter();
|
||||
|
||||
// Get enhanced persistence functionality
|
||||
|
||||
|
||||
// Get enhanced persistence functionality
|
||||
const {
|
||||
persistenceManager,
|
||||
copilotContext,
|
||||
@@ -287,18 +292,29 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
|
||||
const hasCTA = /\b(call now|sign up|join|try|learn more|cta|comment|share|connect|message|dm|reach out)\b/i.test(draft || '');
|
||||
const hasHashtags = /#[A-Za-z0-9_]+/.test(draft || '');
|
||||
const isLong = (draft || '').length > 500;
|
||||
|
||||
// Debug logging for suggestions
|
||||
console.log('[LinkedIn Writer] Generating suggestions:', {
|
||||
hasContent,
|
||||
justGeneratedContent,
|
||||
draftLength: draft?.length || 0
|
||||
});
|
||||
|
||||
if (!hasContent) {
|
||||
// Initial suggestions for content creation
|
||||
return [
|
||||
const initialSuggestions = [
|
||||
{ title: '📝 LinkedIn Post', message: 'Use tool generateLinkedInPost to create a professional LinkedIn post for your industry.' },
|
||||
{ title: '📄 Article', message: 'Use tool generateLinkedInArticle to write a thought leadership article.' },
|
||||
{ title: '🎠 Carousel', message: 'Use tool generateLinkedInCarousel to create a multi-slide carousel presentation.' },
|
||||
{ title: '🎬 Video Script', message: 'Use tool generateLinkedInVideoScript to draft a video script for LinkedIn.' },
|
||||
{ title: '💬 Comment Response', message: 'Use tool generateLinkedInCommentResponse to craft a professional comment reply.' }
|
||||
{ title: '💬 Comment Response', message: 'Use tool generateLinkedInCommentResponse to craft a professional comment reply.' },
|
||||
{ title: '🖼️ Generate Post Image', message: 'Use tool generateLinkedInImagePrompts to create professional images for your LinkedIn content.' },
|
||||
{ title: '🎨 Visual Content', message: 'Create engaging visual content with AI-generated images optimized for LinkedIn.' }
|
||||
];
|
||||
console.log('[LinkedIn Writer] Initial suggestions:', initialSuggestions);
|
||||
return initialSuggestions;
|
||||
} else {
|
||||
// Refinement suggestions for existing content - use direct edit actions
|
||||
// Refinement suggestions for existing content - use direct edit actions
|
||||
const refinementSuggestions = [
|
||||
{ title: '🙂 Make it casual', message: 'Use tool editLinkedInDraft with operation Casual' },
|
||||
{ title: '💼 Make it professional', message: 'Use tool editLinkedInDraft with operation Professional' },
|
||||
@@ -307,6 +323,21 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
|
||||
{ title: '✂️ Shorten', message: 'Use tool editLinkedInDraft with operation Shorten' },
|
||||
{ title: '➕ Lengthen', message: 'Use tool editLinkedInDraft with operation Lengthen' }
|
||||
];
|
||||
|
||||
// Add special suggestions when content was just generated
|
||||
if (justGeneratedContent) {
|
||||
console.log('[LinkedIn Writer] Adding post-generation suggestions');
|
||||
refinementSuggestions.unshift(
|
||||
{
|
||||
title: '🎉 Content Generated! Next Steps:',
|
||||
message: 'Great! Your content is ready. Now let\'s enhance it with images and make it perfect for LinkedIn.'
|
||||
},
|
||||
{
|
||||
title: '🖼️ Generate Post Image',
|
||||
message: 'Use tool generateLinkedInImagePrompts to create professional images for this LinkedIn post'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add contextual suggestions based on content analysis
|
||||
if (!hasCTA) {
|
||||
@@ -318,7 +349,31 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
|
||||
if (isLong) {
|
||||
refinementSuggestions.push({ title: '📝 Summarize intro', message: 'Use tool editLinkedInDraft with operation Shorten' });
|
||||
}
|
||||
|
||||
// Add image generation suggestion when there's content
|
||||
if (draft && draft.trim().length > 0) {
|
||||
console.log('[LinkedIn Writer] Adding image generation suggestion');
|
||||
// Make image generation suggestion more prominent
|
||||
refinementSuggestions.push({
|
||||
title: '🖼️ Generate Post Image',
|
||||
message: 'Use tool generateLinkedInImagePrompts to create professional images for this LinkedIn post'
|
||||
});
|
||||
|
||||
// Add contextual image suggestions based on content type
|
||||
if (draft.includes('digital transformation') || draft.includes('technology') || draft.includes('innovation')) {
|
||||
refinementSuggestions.push({
|
||||
title: '🚀 Tech-Focused Image',
|
||||
message: 'Use tool generateLinkedInImagePrompts to create technology-themed professional images for this post'
|
||||
});
|
||||
} else if (draft.includes('business') || draft.includes('strategy') || draft.includes('growth')) {
|
||||
refinementSuggestions.push({
|
||||
title: '💼 Business Image',
|
||||
message: 'Use tool generateLinkedInImagePrompts to create business-focused professional images for this post'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[LinkedIn Writer] Final suggestions:', refinementSuggestions);
|
||||
return refinementSuggestions;
|
||||
}
|
||||
};
|
||||
@@ -342,7 +397,19 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
|
||||
draft={draft}
|
||||
getHistoryLength={getHistoryLength}
|
||||
/>
|
||||
|
||||
{/* Lightweight progress tracker under header */}
|
||||
<div style={{
|
||||
padding: '6px 16px',
|
||||
transition: 'all 300ms ease',
|
||||
opacity: progressActive || progressSteps.length > 0 ? 1 : 0,
|
||||
transform: progressActive || progressSteps.length > 0 ? 'translateY(0)' : 'translateY(-10px)',
|
||||
height: progressActive || progressSteps.length > 0 ? 'auto' : 0,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<ProgressTracker steps={progressSteps as any} active={progressActive} />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Debug: Enhanced Persistence Test Buttons (remove in production) */}
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,97 @@ import { PostHITL, ArticleHITL, CarouselHITL, VideoScriptHITL, CommentResponseHI
|
||||
const useCopilotActionTyped = useCopilotAction as any;
|
||||
|
||||
const RegisterLinkedInActions: React.FC = () => {
|
||||
// LinkedIn Image Generation Actions
|
||||
useCopilotActionTyped({
|
||||
name: 'generateLinkedInImagePrompts',
|
||||
description: 'Generate three AI-optimized image prompts for LinkedIn content',
|
||||
parameters: [
|
||||
{ name: 'content_type', type: 'string', required: true, description: 'Type of LinkedIn content (post, article, carousel, video_script)' },
|
||||
{ name: 'topic', type: 'string', required: true, description: 'Main topic of the content' },
|
||||
{ name: 'industry', type: 'string', required: true, description: 'Industry context' },
|
||||
{ name: 'content', type: 'string', required: true, description: 'The actual content text' }
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const response = await fetch('/api/linkedin/generate-image-prompts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content_type: args.content_type,
|
||||
topic: args.topic,
|
||||
industry: args.industry,
|
||||
content: args.content
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to generate image prompts: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
prompts: result,
|
||||
message: `Generated ${result.length} professional image prompts for your LinkedIn content. Choose one to generate the actual image.`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error generating image prompts:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to generate image prompts. Please try again.'
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'generateLinkedInImage',
|
||||
description: 'Generate LinkedIn-optimized image from selected prompt',
|
||||
parameters: [
|
||||
{ name: 'prompt', type: 'string', required: true, description: 'The image generation prompt' },
|
||||
{ name: 'content_context', type: 'object', required: true, description: 'Content context including topic, industry, content_type, and style' },
|
||||
{ name: 'aspect_ratio', type: 'string', required: false, description: 'Image aspect ratio (default: 1:1)' }
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const response = await fetch('/api/linkedin/generate-image', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: args.prompt,
|
||||
content_context: args.content_context,
|
||||
aspect_ratio: args.aspect_ratio || '1:1'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to generate image: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
image_url: result.image_url,
|
||||
image_id: result.image_id,
|
||||
message: `✅ LinkedIn image generated successfully! Your professional image is ready to use.`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || 'Image generation failed'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating image:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to generate image. Please try again.'
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// LinkedIn Post Generation
|
||||
useCopilotActionTyped({
|
||||
name: 'generateLinkedInPost',
|
||||
@@ -26,6 +117,19 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
const prefs = readPrefs();
|
||||
// Emit progress init
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
|
||||
steps: [
|
||||
{ id: 'personalize', label: 'Personalizing topic & context' },
|
||||
{ id: 'prepare_queries', label: 'Preparing research queries' },
|
||||
{ id: 'research', label: 'Conducting research & analysis' },
|
||||
{ id: 'grounding', label: 'Applying AI grounding' },
|
||||
{ id: 'content_generation', label: 'Generating content' },
|
||||
{ id: 'citations', label: 'Extracting citations' },
|
||||
{ id: 'quality_analysis', label: 'Quality assessment' },
|
||||
{ id: 'finalize', label: 'Finalizing & optimizing' }
|
||||
]
|
||||
}}));
|
||||
|
||||
// If refining existing content, use the current draft as context
|
||||
let existingContent = '';
|
||||
@@ -38,6 +142,15 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Start detailed progress tracking
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'personalize',
|
||||
status: 'active',
|
||||
message: 'Analyzing topic, industry context, and target audience...'
|
||||
}
|
||||
}));
|
||||
|
||||
const res = await linkedInWriterApi.generatePost({
|
||||
topic: args?.topic || prefs.topic || 'AI transformation in business',
|
||||
industry: mapIndustry(args?.industry || prefs.industry),
|
||||
@@ -55,6 +168,63 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
});
|
||||
|
||||
if (res.success && res.data) {
|
||||
// Update progress with detailed information
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'personalize',
|
||||
status: 'completed',
|
||||
message: 'Topic personalized successfully'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'prepare_queries',
|
||||
status: 'completed',
|
||||
message: `Prepared ${(res.data?.search_queries || []).length} research queries`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'research',
|
||||
status: 'completed',
|
||||
message: `Research completed with ${(res.research_sources || []).length} sources`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'grounding',
|
||||
status: 'completed',
|
||||
message: 'AI grounding applied successfully'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'content_generation',
|
||||
status: 'completed',
|
||||
message: 'Content generated with industry insights'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'citations',
|
||||
status: 'completed',
|
||||
message: `Extracted ${(res.data?.citations || []).length} citations`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'quality_analysis',
|
||||
status: 'completed',
|
||||
message: 'Quality assessment completed'
|
||||
}
|
||||
}));
|
||||
|
||||
const content = res.data.content;
|
||||
const hashtags = res.data.hashtags?.map(h => h.hashtag).join(' ') || '';
|
||||
const cta = res.data.call_to_action || '';
|
||||
@@ -82,8 +252,39 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: fullContent }));
|
||||
return { success: true, content: fullContent };
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'finalize',
|
||||
status: 'completed',
|
||||
message: 'Content finalized and optimized'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
|
||||
|
||||
// Return recommendations message that CopilotKit can render
|
||||
const recommendations = res.data?.quality_metrics?.recommendations || [];
|
||||
if (recommendations.length > 0) {
|
||||
// Create a markdown-formatted message with recommendations
|
||||
const recommendationsMarkdown = recommendations.map((rec, index) =>
|
||||
`${index + 1}. **${rec}**`
|
||||
).join('\n\n');
|
||||
|
||||
// Return a message that CopilotKit can render with image generation suggestion
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ LinkedIn post generated successfully! Your content is now displayed in the preview.\n\n**🎯 AI Content Improvement Recommendations:**\n\n${recommendationsMarkdown}\n\n**🖼️ Enhance Your Post with AI-Generated Images:**\n\nNow that your content is ready, you can make it even more engaging with professional LinkedIn-optimized images! Here are your options:\n\n• **Professional Style**: Clean, corporate aesthetics perfect for business audiences\n• **Creative Style**: Eye-catching visuals that boost social media engagement\n• **Industry-Specific**: Tailored imagery for your ${mapIndustry(args?.industry || prefs.industry)} industry\n\n*To generate images, simply ask: "Generate images for my LinkedIn post" or "Create professional images for this content"*\n\n*To get specific improvement guidance for any recommendation, type: "Help me improve [specific recommendation]"*`
|
||||
};
|
||||
} else {
|
||||
// Return a message with image generation suggestion even without recommendations
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ LinkedIn post generated successfully! Your content is now displayed in the preview.\n\n**🖼️ Enhance Your Post with AI-Generated Images:**\n\nNow that your content is ready, you can make it even more engaging with professional LinkedIn-optimized images! Here are your options:\n\n• **Professional Style**: Clean, corporate aesthetics perfect for business audiences\n• **Creative Style**: Eye-catching visuals that boost social media engagement\n• **Industry-Specific**: Tailored imagery for your ${mapIndustry(args?.industry || prefs.industry)} industry\n\n*To generate images, simply ask: "Generate images for my LinkedIn post" or "Create professional images for this content"*`
|
||||
};
|
||||
}
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
||||
return { success: false, message: res.error || 'Failed to generate LinkedIn post' };
|
||||
}
|
||||
});
|
||||
@@ -100,6 +301,29 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
const prefs = readPrefs();
|
||||
// Emit progress init for article
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
|
||||
steps: [
|
||||
{ id: 'personalize', label: 'Personalizing topic & context' },
|
||||
{ id: 'prepare_queries', label: 'Preparing research queries' },
|
||||
{ id: 'research', label: 'Conducting research & analysis' },
|
||||
{ id: 'grounding', label: 'Applying AI grounding' },
|
||||
{ id: 'content_generation', label: 'Generating article content' },
|
||||
{ id: 'citations', label: 'Extracting citations' },
|
||||
{ id: 'quality_analysis', label: 'Quality assessment' },
|
||||
{ id: 'finalize', label: 'Finalizing & optimizing' }
|
||||
]
|
||||
}}));
|
||||
|
||||
// Start detailed progress tracking
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'personalize',
|
||||
status: 'active',
|
||||
message: 'Analyzing topic, industry context, and target audience...'
|
||||
}
|
||||
}));
|
||||
|
||||
const res = await linkedInWriterApi.generateArticle({
|
||||
topic: args?.topic || prefs.topic || 'Digital transformation strategies',
|
||||
industry: mapIndustry(args?.industry || prefs.industry),
|
||||
@@ -116,6 +340,63 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
});
|
||||
|
||||
if (res.success && res.data) {
|
||||
// Update progress with detailed information
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'personalize',
|
||||
status: 'completed',
|
||||
message: 'Topic personalized successfully'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'prepare_queries',
|
||||
status: 'completed',
|
||||
message: `Prepared ${(res.data?.search_queries || []).length} research queries`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'research',
|
||||
status: 'completed',
|
||||
message: `Research completed with ${(res.research_sources || []).length} sources`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'grounding',
|
||||
status: 'completed',
|
||||
message: 'AI grounding applied successfully'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'content_generation',
|
||||
status: 'completed',
|
||||
message: 'Article content generated with industry insights'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'citations',
|
||||
status: 'completed',
|
||||
message: `Extracted ${(res.data?.citations || []).length} citations`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'quality_analysis',
|
||||
status: 'completed',
|
||||
message: 'Quality assessment completed'
|
||||
}
|
||||
}));
|
||||
|
||||
const content = `# ${res.data.title}\n\n${res.data.content}`;
|
||||
|
||||
// Debug: Log the full response structure
|
||||
@@ -137,8 +418,39 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
|
||||
return { success: true, content };
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'finalize',
|
||||
status: 'completed',
|
||||
message: 'Article finalized and optimized'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
|
||||
|
||||
// Return recommendations message that CopilotKit can render
|
||||
const recommendations = res.data?.quality_metrics?.recommendations || [];
|
||||
if (recommendations.length > 0) {
|
||||
// Create a markdown-formatted message with recommendations
|
||||
const recommendationsMarkdown = recommendations.map((rec, index) =>
|
||||
`${index + 1}. **${rec}**`
|
||||
).join('\n\n');
|
||||
|
||||
// Return a message that CopilotKit can render with image generation suggestion
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ LinkedIn article generated successfully! Your content is now displayed in the preview.\n\n**🎯 AI Content Improvement Recommendations:**\n\n${recommendationsMarkdown}\n\n**🖼️ Enhance Your Article with AI-Generated Images:**\n\nNow that your article is ready, you can make it even more engaging with professional LinkedIn-optimized images! Here are your options:\n\n• **Professional Style**: Clean, corporate aesthetics perfect for business audiences\n• **Creative Style**: Eye-catching visuals that boost social media engagement\n• **Industry-Specific**: Tailored imagery for your ${mapIndustry(args?.industry || prefs.industry)} industry\n\n*To generate images, simply ask: "Generate images for my LinkedIn article" or "Create professional images for this content"*\n\n*To get specific improvement guidance for any recommendation, type: "Help me improve [specific recommendation]"*`
|
||||
};
|
||||
} else {
|
||||
// Return a message with image generation suggestion even without recommendations
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ LinkedIn article generated successfully! Your content is now displayed in the preview.\n\n**🖼️ Enhance Your Article with AI-Generated Images:**\n\nNow that your article is ready, you can make it even more engaging with professional LinkedIn-optimized images! Here are your options:\n\n• **Professional Style**: Clean, corporate aesthetics perfect for business audiences\n• **Creative Style**: Eye-catching visuals that boost social media engagement\n• **Industry-Specific**: Tailored imagery for your ${mapIndustry(args?.industry || prefs.industry)} industry\n\n*To generate images, simply ask: "Generate images for my LinkedIn article" or "Create professional images for this content"*`
|
||||
};
|
||||
}
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
||||
return { success: false, message: res.error || 'Failed to generate LinkedIn article' };
|
||||
}
|
||||
});
|
||||
@@ -154,6 +466,30 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
const prefs = readPrefs();
|
||||
|
||||
// Emit progress init for carousel
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
|
||||
steps: [
|
||||
{ id: 'personalize', label: 'Personalizing topic & context' },
|
||||
{ id: 'prepare_queries', label: 'Preparing research queries' },
|
||||
{ id: 'research', label: 'Conducting research & analysis' },
|
||||
{ id: 'grounding', label: 'Applying AI grounding' },
|
||||
{ id: 'content_generation', label: 'Generating carousel slides' },
|
||||
{ id: 'citations', label: 'Extracting citations' },
|
||||
{ id: 'quality_analysis', label: 'Quality assessment' },
|
||||
{ id: 'finalize', label: 'Finalizing & optimizing' }
|
||||
]
|
||||
}}));
|
||||
|
||||
// Start detailed progress tracking
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'personalize',
|
||||
status: 'active',
|
||||
message: 'Analyzing topic, industry context, and target audience...'
|
||||
}
|
||||
}));
|
||||
|
||||
const res = await linkedInWriterApi.generateCarousel({
|
||||
topic: args?.topic || prefs.topic || 'Professional development tips',
|
||||
industry: mapIndustry(args?.industry || prefs.industry),
|
||||
@@ -167,14 +503,84 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
});
|
||||
|
||||
if (res.success && res.data) {
|
||||
// Update progress with detailed information
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'personalize',
|
||||
status: 'completed',
|
||||
message: 'Topic personalized successfully'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'prepare_queries',
|
||||
status: 'completed',
|
||||
message: `Prepared research queries for carousel`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'research',
|
||||
status: 'completed',
|
||||
message: `Research completed for carousel content`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'grounding',
|
||||
status: 'completed',
|
||||
message: 'AI grounding applied successfully'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'content_generation',
|
||||
status: 'completed',
|
||||
message: `Generated ${res.data.slides?.length || 0} carousel slides`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'citations',
|
||||
status: 'completed',
|
||||
message: 'Citations extracted for carousel'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'quality_analysis',
|
||||
status: 'completed',
|
||||
message: 'Quality assessment completed'
|
||||
}
|
||||
}));
|
||||
|
||||
let content = `# ${res.data.title}\n\n`;
|
||||
res.data.slides.forEach((slide, index) => {
|
||||
content += `## Slide ${index + 1}: ${slide.title}\n\n${slide.content}\n\n`;
|
||||
});
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'finalize',
|
||||
status: 'completed',
|
||||
message: 'Carousel finalized and optimized'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
|
||||
|
||||
return { success: true, content };
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
||||
return { success: false, message: res.error || 'Failed to generate LinkedIn carousel' };
|
||||
}
|
||||
});
|
||||
@@ -190,6 +596,30 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
const prefs = readPrefs();
|
||||
|
||||
// Emit progress init for video script
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
|
||||
steps: [
|
||||
{ id: 'personalize', label: 'Personalizing topic & context' },
|
||||
{ id: 'prepare_queries', label: 'Preparing research queries' },
|
||||
{ id: 'research', label: 'Conducting research & analysis' },
|
||||
{ id: 'grounding', label: 'Applying AI grounding' },
|
||||
{ id: 'content_generation', label: 'Generating video script' },
|
||||
{ id: 'citations', label: 'Extracting citations' },
|
||||
{ id: 'quality_analysis', label: 'Quality assessment' },
|
||||
{ id: 'finalize', label: 'Finalizing & optimizing' }
|
||||
]
|
||||
}}));
|
||||
|
||||
// Start detailed progress tracking
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'personalize',
|
||||
status: 'active',
|
||||
message: 'Analyzing topic, industry context, and target audience...'
|
||||
}
|
||||
}));
|
||||
|
||||
const res = await linkedInWriterApi.generateVideoScript({
|
||||
topic: args?.topic || prefs.topic || 'Professional networking tips',
|
||||
industry: mapIndustry(args?.industry || prefs.industry),
|
||||
@@ -202,6 +632,63 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
});
|
||||
|
||||
if (res.success && res.data) {
|
||||
// Update progress with detailed information
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'personalize',
|
||||
status: 'completed',
|
||||
message: 'Topic personalized successfully'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'prepare_queries',
|
||||
status: 'completed',
|
||||
message: `Prepared research queries for video script`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'research',
|
||||
status: 'completed',
|
||||
message: `Research completed for video content`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'grounding',
|
||||
status: 'completed',
|
||||
message: 'AI grounding applied successfully'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'content_generation',
|
||||
status: 'completed',
|
||||
message: `Generated video script with ${res.data.main_content?.length || 0} scenes`
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'citations',
|
||||
status: 'completed',
|
||||
message: 'Citations extracted for video script'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'quality_analysis',
|
||||
status: 'completed',
|
||||
message: 'Quality assessment completed'
|
||||
}
|
||||
}));
|
||||
|
||||
let content = `# Video Script: ${args?.topic || 'Professional Content'}\n\n`;
|
||||
content += `## Hook\n${res.data.hook}\n\n`;
|
||||
content += `## Main Content\n`;
|
||||
@@ -216,12 +703,167 @@ const RegisterLinkedInActions: React.FC = () => {
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||
detail: {
|
||||
id: 'finalize',
|
||||
status: 'completed',
|
||||
message: 'Video script finalized and optimized'
|
||||
}
|
||||
}));
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
|
||||
|
||||
return { success: true, content };
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
||||
return { success: false, message: res.error || 'Failed to generate LinkedIn video script' };
|
||||
}
|
||||
});
|
||||
|
||||
// Content Improvement Action
|
||||
useCopilotActionTyped({
|
||||
name: 'improveContent',
|
||||
description: 'Improve specific aspects of LinkedIn content based on AI recommendations',
|
||||
parameters: [
|
||||
{ name: 'recommendation', type: 'string', required: true },
|
||||
{ name: 'current_content', type: 'string', required: false },
|
||||
{ name: 'improvement_type', type: 'string', required: false }
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
const { recommendation, current_content, improvement_type } = args;
|
||||
|
||||
// Analyze the recommendation and provide specific improvement guidance
|
||||
let improvementGuidance = '';
|
||||
let actionItems = [];
|
||||
let examples = [];
|
||||
|
||||
if (recommendation.toLowerCase().includes('factual accuracy') || recommendation.toLowerCase().includes('accuracy')) {
|
||||
improvementGuidance = 'To improve factual accuracy, consider:';
|
||||
actionItems = [
|
||||
'Add specific data points and statistics',
|
||||
'Include recent research findings',
|
||||
'Cite authoritative sources',
|
||||
'Verify all claims against reliable sources'
|
||||
];
|
||||
examples = [
|
||||
'Instead of "AI is growing rapidly", use "AI market grew 37% in 2023 according to Gartner"',
|
||||
'Replace "many companies" with "73% of Fortune 500 companies"',
|
||||
'Add source: "According to a 2024 McKinsey report..."'
|
||||
];
|
||||
} else if (recommendation.toLowerCase().includes('professional tone') || recommendation.toLowerCase().includes('tone')) {
|
||||
improvementGuidance = 'To enhance professional tone, consider:';
|
||||
actionItems = [
|
||||
'Use industry-specific terminology',
|
||||
'Maintain consistent formality level',
|
||||
'Avoid casual language and slang',
|
||||
'Structure content with clear headings'
|
||||
];
|
||||
examples = [
|
||||
'Instead of "cool new features", use "innovative capabilities"',
|
||||
'Replace "huge impact" with "significant impact"',
|
||||
'Use "Furthermore" instead of "Also"'
|
||||
];
|
||||
} else if (recommendation.toLowerCase().includes('citation') || recommendation.toLowerCase().includes('sources')) {
|
||||
improvementGuidance = 'To improve citation coverage, consider:';
|
||||
actionItems = [
|
||||
'Add inline citations for factual claims',
|
||||
'Include source references for statistics',
|
||||
'Link to relevant research or reports',
|
||||
'Provide source list at the end'
|
||||
];
|
||||
examples = [
|
||||
'Add [1] after statistics: "The market grew 25% [1]"',
|
||||
'Include source links: "According to [Harvard Business Review](link)..."',
|
||||
'Create a numbered source list at the bottom'
|
||||
];
|
||||
} else if (recommendation.toLowerCase().includes('industry relevance') || recommendation.toLowerCase().includes('relevance')) {
|
||||
improvementGuidance = 'To increase industry relevance, consider:';
|
||||
actionItems = [
|
||||
'Use industry-specific examples',
|
||||
'Reference current industry trends',
|
||||
'Include relevant case studies',
|
||||
'Address industry-specific challenges'
|
||||
];
|
||||
examples = [
|
||||
'Add industry-specific metrics: "In healthcare, this translates to..."',
|
||||
'Reference current trends: "With the rise of telemedicine..."',
|
||||
'Use industry jargon appropriately: "EMR integration" vs "electronic records"'
|
||||
];
|
||||
} else {
|
||||
improvementGuidance = 'To address this recommendation, consider:';
|
||||
actionItems = [
|
||||
'Review the content for clarity',
|
||||
'Ensure consistency in messaging',
|
||||
'Check for grammatical accuracy',
|
||||
'Verify alignment with target audience'
|
||||
];
|
||||
examples = [
|
||||
'Break long sentences into shorter ones',
|
||||
'Use consistent terminology throughout',
|
||||
'Check subject-verb agreement',
|
||||
'Ensure content matches audience expertise level'
|
||||
];
|
||||
}
|
||||
|
||||
const actionItemsMarkdown = actionItems.map((item, index) =>
|
||||
`${index + 1}. ${item}`
|
||||
).join('\n');
|
||||
|
||||
const examplesMarkdown = examples.map((example, index) =>
|
||||
`**Example ${index + 1}:** ${example}`
|
||||
).join('\n\n');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `**🔧 Content Improvement Guide for: "${recommendation}"**\n\n${improvementGuidance}\n\n${actionItemsMarkdown}\n\n**💡 Practical Examples:**\n\n${examplesMarkdown}\n\n*Would you like me to help you implement any of these improvements to your content?*`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Natural Language Content Improvement Action
|
||||
useCopilotActionTyped({
|
||||
name: 'helpWithContentImprovement',
|
||||
description: 'Help users improve their LinkedIn content based on natural language requests',
|
||||
parameters: [
|
||||
{ name: 'user_request', type: 'string', required: true }
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
const { user_request } = args;
|
||||
const request = user_request.toLowerCase();
|
||||
|
||||
// Handle various ways users might ask for help
|
||||
if (request.includes('help me improve') || request.includes('how to improve') || request.includes('improve')) {
|
||||
// Extract the specific aspect they want to improve
|
||||
let aspect = 'content quality';
|
||||
if (request.includes('tone')) aspect = 'professional tone';
|
||||
else if (request.includes('accuracy') || request.includes('factual')) aspect = 'factual accuracy';
|
||||
else if (request.includes('citation') || request.includes('source')) aspect = 'citation coverage';
|
||||
else if (request.includes('relevance') || request.includes('industry')) aspect = 'industry relevance';
|
||||
else if (request.includes('grammar') || request.includes('language')) aspect = 'language quality';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `I'd be happy to help you improve your ${aspect}! Let me provide specific guidance and examples.\n\n*Please use the "improveContent" action with the specific recommendation you'd like to address, or let me know what aspect you'd like to focus on.*`
|
||||
};
|
||||
}
|
||||
|
||||
if (request.includes('recommendation') || request.includes('suggestion')) {
|
||||
return {
|
||||
success: true,
|
||||
message: `I can see you have several AI-generated recommendations for improving your content! Here's how to get specific help:\n\n**To get detailed improvement guidance:**\n• Type: "Help me improve [specific recommendation]"\n• Example: "Help me improve the professional tone"\n• Or: "How can I improve factual accuracy?"\n\n*Which specific recommendation would you like me to help you with?*`
|
||||
};
|
||||
}
|
||||
|
||||
// Default response
|
||||
return {
|
||||
success: true,
|
||||
message: `I'm here to help you improve your LinkedIn content! You can:\n\n**1. Get specific improvement guidance:**\n• "Help me improve [specific recommendation]"\n• "How to improve professional tone?"\n• "Improve factual accuracy"\n\n**2. Ask general questions:**\n• "What are the best practices for LinkedIn posts?"\n• "How can I make my content more engaging?"\n\n*What would you like to improve today?*`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// LinkedIn Comment Response Generation
|
||||
useCopilotActionTyped({
|
||||
name: 'generateLinkedInCommentResponse',
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ContentRecommendationsProps {
|
||||
recommendations: string[];
|
||||
onSelectRecommendation: (recommendation: string) => void;
|
||||
}
|
||||
|
||||
export const ContentRecommendations: React.FC<ContentRecommendationsProps> = ({
|
||||
recommendations,
|
||||
onSelectRecommendation
|
||||
}) => {
|
||||
if (!recommendations || recommendations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)',
|
||||
border: '1px solid #cbd5e1',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
margin: '12px 0',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: '#1e293b',
|
||||
marginBottom: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<span style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #3b82f6, #1d4ed8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontSize: '12px',
|
||||
fontWeight: '700'
|
||||
}}>
|
||||
💡
|
||||
</span>
|
||||
AI Content Improvement Recommendations
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#64748b',
|
||||
marginBottom: '16px',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
Select any recommendation below to get specific guidance on improving your content:
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px'
|
||||
}}>
|
||||
{recommendations.map((recommendation, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onSelectRecommendation(recommendation)}
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '8px',
|
||||
padding: '12px 16px',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 200ms ease',
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.4',
|
||||
color: '#334155',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#f1f5f9';
|
||||
e.currentTarget.style.borderColor = '#3b82f6';
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.15)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'white';
|
||||
e.currentTarget.style.borderColor = '#e2e8f0';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<span style={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
borderRadius: '50%',
|
||||
background: '#3b82f6',
|
||||
color: 'white',
|
||||
fontSize: '10px',
|
||||
fontWeight: '700',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span style={{ flex: 1 }}>
|
||||
{recommendation}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '11px',
|
||||
color: '#64748b',
|
||||
fontWeight: '500',
|
||||
marginLeft: '8px'
|
||||
}}>
|
||||
Click to improve →
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#94a3b8',
|
||||
marginTop: '12px',
|
||||
textAlign: 'center',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
These recommendations are based on AI analysis of your content quality and can help improve engagement and credibility.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CopilotRecommendationsMessageProps {
|
||||
recommendations: string[];
|
||||
onSelectRecommendation: (recommendation: string) => void;
|
||||
}
|
||||
|
||||
export const CopilotRecommendationsMessage: React.FC<CopilotRecommendationsMessageProps> = ({
|
||||
recommendations,
|
||||
onSelectRecommendation
|
||||
}) => {
|
||||
if (!recommendations || recommendations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
|
||||
border: '1px solid #0ea5e9',
|
||||
borderRadius: '16px',
|
||||
padding: '20px',
|
||||
margin: '16px 0',
|
||||
boxShadow: '0 8px 25px -5px rgba(14, 165, 233, 0.15)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Decorative background elements */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '-20px',
|
||||
right: '-20px',
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(14, 165, 233, 0.1) 0%, transparent 70%)',
|
||||
zIndex: 0
|
||||
}} />
|
||||
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: '-30px',
|
||||
left: '-30px',
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(14, 165, 233, 0.08) 0%, transparent 70%)',
|
||||
zIndex: 0
|
||||
}} />
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #0ea5e9, #0284c7)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
fontWeight: '700',
|
||||
boxShadow: '0 4px 12px rgba(14, 165, 233, 0.3)'
|
||||
}}>
|
||||
🎯
|
||||
</div>
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: '700',
|
||||
color: '#0c4a6e',
|
||||
marginBottom: '2px'
|
||||
}}>
|
||||
AI Content Improvement Suggestions
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
color: '#0369a1',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
Select any recommendation to get specific guidance
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}>
|
||||
{recommendations.map((recommendation, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onSelectRecommendation(recommendation)}
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e0f2fe',
|
||||
borderRadius: '12px',
|
||||
padding: '14px 18px',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 250ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
color: '#0f172a',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 2px 8px rgba(14, 165, 233, 0.08)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#f0f9ff';
|
||||
e.currentTarget.style.borderColor = '#0ea5e9';
|
||||
e.currentTarget.style.transform = 'translateY(-2px) scale(1.01)';
|
||||
e.currentTarget.style.boxShadow = '0 8px 25px rgba(14, 165, 233, 0.2)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'white';
|
||||
e.currentTarget.style.borderColor = '#e0f2fe';
|
||||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.08)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '14px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #0ea5e9, #0284c7)',
|
||||
color: 'white',
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
marginTop: '2px',
|
||||
boxShadow: '0 2px 6px rgba(14, 165, 233, 0.3)'
|
||||
}}>
|
||||
{index + 1}
|
||||
</div>
|
||||
<span style={{ flex: 1, fontWeight: '500' }}>
|
||||
{recommendation}
|
||||
</span>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '12px',
|
||||
color: '#0ea5e9',
|
||||
fontWeight: '600',
|
||||
marginLeft: '8px'
|
||||
}}>
|
||||
<span>Improve</span>
|
||||
<span style={{ fontSize: '14px' }}>→</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#0c4a6e',
|
||||
marginTop: '16px',
|
||||
textAlign: 'center',
|
||||
fontStyle: 'italic',
|
||||
opacity: 0.8
|
||||
}}>
|
||||
💡 These recommendations are based on AI analysis of your content quality and can help improve engagement and credibility.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CopilotRecommendationsRendererProps {
|
||||
message: any;
|
||||
onRecommendationClick: (recommendation: string) => void;
|
||||
}
|
||||
|
||||
export const CopilotRecommendationsRenderer: React.FC<CopilotRecommendationsRendererProps> = ({
|
||||
message,
|
||||
onRecommendationClick
|
||||
}) => {
|
||||
// Check if this message contains recommendations
|
||||
if (!message?.recommendations || !Array.isArray(message.recommendations)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recommendations = message.recommendations;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
|
||||
border: '1px solid #0ea5e9',
|
||||
borderRadius: '16px',
|
||||
padding: '20px',
|
||||
margin: '16px 0',
|
||||
boxShadow: '0 8px 25px -5px rgba(14, 165, 233, 0.15)'
|
||||
}}>
|
||||
{/* Success message */}
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#166534',
|
||||
marginBottom: '16px',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
{message.message}
|
||||
</div>
|
||||
|
||||
{/* Recommendations as interactive buttons */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}>
|
||||
{recommendations.map((recommendation: string, index: number) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onRecommendationClick(recommendation)}
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #0ea5e9',
|
||||
borderRadius: '12px',
|
||||
padding: '14px 18px',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 250ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
color: '#0f172a',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 2px 8px rgba(14, 165, 233, 0.08)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#f0f9ff';
|
||||
e.currentTarget.style.borderColor = '#0284c7';
|
||||
e.currentTarget.style.transform = 'translateY(-2px) scale(1.01)';
|
||||
e.currentTarget.style.boxShadow = '0 8px 25px rgba(14, 165, 233, 0.2)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'white';
|
||||
e.currentTarget.style.borderColor = '#0ea5e9';
|
||||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.08)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '14px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #0ea5e9, #0284c7)',
|
||||
color: 'white',
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
{index + 1}
|
||||
</div>
|
||||
<span style={{ flex: 1, fontWeight: '500' }}>
|
||||
{recommendation}
|
||||
</span>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '12px',
|
||||
color: '#0ea5e9',
|
||||
fontWeight: '600',
|
||||
marginLeft: '8px'
|
||||
}}>
|
||||
<span>Apply</span>
|
||||
<span style={{ fontSize: '14px' }}>→</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#0c4a6e',
|
||||
marginTop: '16px',
|
||||
textAlign: 'center',
|
||||
fontStyle: 'italic',
|
||||
opacity: 0.8
|
||||
}}>
|
||||
💡 Click any recommendation above to get specific improvement guidance
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { CopilotRecommendationsMessage } from './CopilotRecommendationsMessage';
|
||||
|
||||
interface CustomMessageRendererProps {
|
||||
message: any;
|
||||
onSelectRecommendation: (recommendation: string) => void;
|
||||
}
|
||||
|
||||
export const CustomMessageRenderer: React.FC<CustomMessageRendererProps> = ({
|
||||
message,
|
||||
onSelectRecommendation
|
||||
}) => {
|
||||
// Check if this is a message with recommendations
|
||||
if (message?.content?.recommendations && message?.content?.showRecommendations) {
|
||||
return (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
{/* Success message */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)',
|
||||
border: '1px solid #22c55e',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '12px',
|
||||
color: '#166534',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
{message.content.message}
|
||||
</div>
|
||||
|
||||
{/* Recommendations */}
|
||||
<CopilotRecommendationsMessage
|
||||
recommendations={message.content.recommendations}
|
||||
onSelectRecommendation={onSelectRecommendation}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if this is a regular success message
|
||||
if (message?.content?.message && !message?.content?.content) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%)',
|
||||
border: '1px solid #22c55e',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px',
|
||||
color: '#166534',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
{message.content.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default message rendering (fallback)
|
||||
if (message?.content?.content) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.6',
|
||||
color: '#334155',
|
||||
whiteSpace: 'pre-wrap'
|
||||
}}>
|
||||
{message.content.content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback for other message types
|
||||
return (
|
||||
<div style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.6',
|
||||
color: '#334155'
|
||||
}}>
|
||||
{JSON.stringify(message, null, 2)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { ImageGenerationSuggestions } from './index';
|
||||
|
||||
const ImageGenerationDemo: React.FC = () => {
|
||||
// Sample LinkedIn content for demonstration
|
||||
const sampleContent = {
|
||||
contentType: 'post' as const,
|
||||
topic: 'AI in Marketing',
|
||||
industry: 'Technology',
|
||||
content: `🚀 Exciting news! Artificial Intelligence is revolutionizing how we approach marketing strategies.
|
||||
|
||||
Here are 3 game-changing ways AI is transforming the industry:
|
||||
|
||||
1️⃣ **Predictive Analytics**: AI algorithms can now predict customer behavior with 95% accuracy, allowing marketers to create hyper-personalized campaigns.
|
||||
|
||||
2️⃣ **Content Optimization**: Machine learning models analyze engagement patterns to optimize content timing, format, and messaging for maximum impact.
|
||||
|
||||
3️⃣ **Automated Personalization**: AI-powered tools automatically adjust marketing messages based on individual user preferences and behavior.
|
||||
|
||||
The future of marketing is here, and it's powered by AI! 🎯
|
||||
|
||||
What's your experience with AI in marketing? Share your thoughts below! 👇
|
||||
|
||||
#AIMarketing #DigitalTransformation #MarketingInnovation #TechTrends #FutureOfMarketing`
|
||||
};
|
||||
|
||||
const handleImageGenerated = (imageData: any) => {
|
||||
console.log('Image generated successfully:', imageData);
|
||||
// Here you would typically:
|
||||
// 1. Update the LinkedIn preview editor
|
||||
// 2. Store the image in your content
|
||||
// 3. Trigger any follow-up actions
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-generation-demo">
|
||||
<div className="demo-header">
|
||||
<h1 className="demo-title">LinkedIn Image Generation Demo</h1>
|
||||
<p className="demo-description">
|
||||
This demo showcases the ImageGenerationSuggestions component integrated with CopilotKit.
|
||||
Try generating image prompts and creating images for the sample LinkedIn content below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="demo-content">
|
||||
<div className="content-preview">
|
||||
<h2 className="content-title">Sample LinkedIn Content</h2>
|
||||
<div className="content-display">
|
||||
<div className="content-header">
|
||||
<span className="content-type-badge">{sampleContent.contentType}</span>
|
||||
<span className="content-topic">{sampleContent.topic}</span>
|
||||
<span className="content-industry">{sampleContent.industry}</span>
|
||||
</div>
|
||||
<div className="content-text">
|
||||
{sampleContent.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="image-generation-section">
|
||||
<h2 className="section-title">Image Generation</h2>
|
||||
<ImageGenerationSuggestions
|
||||
contentType={sampleContent.contentType}
|
||||
topic={sampleContent.topic}
|
||||
industry={sampleContent.industry}
|
||||
content={sampleContent.content}
|
||||
onImageGenerated={handleImageGenerated}
|
||||
className="demo-image-suggestions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="demo-footer">
|
||||
<h3 className="footer-title">How It Works</h3>
|
||||
<div className="workflow-steps">
|
||||
<div className="step">
|
||||
<div className="step-number">1</div>
|
||||
<div className="step-content">
|
||||
<h4>Content Analysis</h4>
|
||||
<p>The system analyzes your LinkedIn content to understand context, tone, and target audience.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="step">
|
||||
<div className="step-number">2</div>
|
||||
<div className="step-content">
|
||||
<h4>Prompt Generation</h4>
|
||||
<p>AI generates three distinct image prompts: Professional, Creative, and Industry-Specific.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="step">
|
||||
<div className="step-number">3</div>
|
||||
<div className="step-content">
|
||||
<h4>Image Creation</h4>
|
||||
<p>Using Gemini API, creates LinkedIn-optimized images from your selected prompt.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="step">
|
||||
<div className="step-number">4</div>
|
||||
<div className="step-content">
|
||||
<h4>Integration</h4>
|
||||
<p>Generated images are ready to use in your LinkedIn content editor.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGenerationDemo;
|
||||
@@ -0,0 +1,469 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useCopilotAction } from '@copilotkit/react-core';
|
||||
import {
|
||||
AutoAwesome as SparklesIcon,
|
||||
PhotoCamera as PhotoIcon,
|
||||
ArrowForward as ArrowRightIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as ExclamationTriangleIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface ImageGenerationSuggestionsProps {
|
||||
contentType: 'post' | 'article' | 'carousel' | 'video_script';
|
||||
topic: string;
|
||||
industry: string;
|
||||
content: string;
|
||||
onImageGenerated?: (imageData: any) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface ImagePrompt {
|
||||
style: string;
|
||||
prompt: string;
|
||||
description: string;
|
||||
prompt_index: number;
|
||||
}
|
||||
|
||||
interface ImageGenerationState {
|
||||
isGenerating: boolean;
|
||||
selectedPrompt: ImagePrompt | null;
|
||||
generatedImage: any | null;
|
||||
error: string | null;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
const ImageGenerationSuggestions: React.FC<ImageGenerationSuggestionsProps> = ({
|
||||
contentType,
|
||||
topic,
|
||||
industry,
|
||||
content,
|
||||
onImageGenerated,
|
||||
className = ''
|
||||
}) => {
|
||||
const [state, setState] = useState<ImageGenerationState>({
|
||||
isGenerating: false,
|
||||
selectedPrompt: null,
|
||||
generatedImage: null,
|
||||
error: null,
|
||||
progress: 0
|
||||
});
|
||||
|
||||
const [prompts, setPrompts] = useState<ImagePrompt[]>([]);
|
||||
const [showPrompts, setShowPrompts] = useState(false);
|
||||
|
||||
// Use the same pattern as other components in the project
|
||||
const useCopilotActionTyped = useCopilotAction as any;
|
||||
|
||||
// Register Copilot action for generating image prompts
|
||||
useCopilotActionTyped({
|
||||
name: 'generate_image_prompts',
|
||||
description: 'Generate three AI-optimized image prompts for LinkedIn content',
|
||||
parameters: [
|
||||
{ name: 'content_type', type: 'string', required: true },
|
||||
{ name: 'topic', type: 'string', required: true },
|
||||
{ name: 'industry', type: 'string', required: true },
|
||||
{ name: 'content', type: 'string', required: true }
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
// Call the actual backend API
|
||||
const response = await fetch('/api/linkedin/generate-image-prompts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content_type: args.content_type,
|
||||
topic: args.topic,
|
||||
industry: args.industry,
|
||||
content: args.content
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API call failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const prompts = await response.json();
|
||||
return { prompts };
|
||||
} catch (error) {
|
||||
console.error('Error generating image prompts:', error);
|
||||
// Fallback to predefined prompts if API fails
|
||||
const fallbackPrompts = [
|
||||
{
|
||||
style: 'Professional',
|
||||
prompt: `Create a professional LinkedIn ${args.content_type} image for ${args.topic} in the ${args.industry} industry with corporate aesthetics, clean lines, and professional color palette.`,
|
||||
description: 'Clean, business-appropriate visual for LinkedIn',
|
||||
prompt_index: 0
|
||||
},
|
||||
{
|
||||
style: 'Creative',
|
||||
prompt: `Generate a creative LinkedIn ${args.content_type} image for ${args.topic} with eye-catching design, vibrant colors while maintaining professional appeal, and social media engagement optimization.`,
|
||||
description: 'Eye-catching, shareable design for LinkedIn',
|
||||
prompt_index: 1
|
||||
},
|
||||
{
|
||||
style: 'Industry-Specific',
|
||||
prompt: `Design a ${args.industry} industry-specific LinkedIn ${args.content_type} image for ${args.topic} with industry-relevant imagery, colors, and visual elements that appeal to business professionals.`,
|
||||
description: `Industry-tailored professional design for ${args.industry}`,
|
||||
prompt_index: 2
|
||||
}
|
||||
];
|
||||
|
||||
return { prompts: fallbackPrompts };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register Copilot action for generating images
|
||||
useCopilotActionTyped({
|
||||
name: 'generate_linkedin_image',
|
||||
description: 'Generate LinkedIn-optimized image from selected prompt',
|
||||
parameters: [
|
||||
{ name: 'prompt', type: 'string', required: true },
|
||||
{ name: 'content_context', type: 'object', required: true },
|
||||
{ name: 'aspect_ratio', type: 'string', required: false }
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
// Call the actual backend API
|
||||
const response = await fetch('/api/linkedin/generate-image', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: args.prompt,
|
||||
content_context: args.content_context,
|
||||
aspect_ratio: args.aspect_ratio || '1:1'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API call failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error generating image:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle prompt generation
|
||||
const handleGeneratePrompts = async () => {
|
||||
try {
|
||||
setShowPrompts(true);
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
|
||||
// Call the backend API directly for immediate response
|
||||
const response = await fetch('/api/linkedin/generate-image-prompts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content_type: contentType,
|
||||
topic,
|
||||
industry,
|
||||
content
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const apiPrompts = await response.json();
|
||||
if (apiPrompts && apiPrompts.length >= 3) {
|
||||
setPrompts(apiPrompts);
|
||||
} else {
|
||||
throw new Error('API returned insufficient prompts');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`API call failed: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating prompts:', error);
|
||||
|
||||
// Fallback to predefined prompts if API fails
|
||||
setPrompts([
|
||||
{
|
||||
style: 'Professional',
|
||||
prompt: `Create a professional LinkedIn ${contentType} image for ${topic} in the ${industry} industry with corporate aesthetics, clean lines, and professional color palette.`,
|
||||
description: 'Clean, business-appropriate visual for LinkedIn',
|
||||
prompt_index: 0
|
||||
},
|
||||
{
|
||||
style: 'Creative',
|
||||
prompt: `Generate a creative LinkedIn ${contentType} image for ${topic} with eye-catching design, vibrant colors while maintaining professional appeal, and social media engagement optimization.`,
|
||||
description: 'Eye-catching, shareable design for LinkedIn',
|
||||
prompt_index: 1
|
||||
},
|
||||
{
|
||||
style: 'Industry-Specific',
|
||||
prompt: `Design a ${industry} industry-specific LinkedIn ${contentType} image for ${topic} with industry-relevant imagery, colors, and visual elements that appeal to business professionals.`,
|
||||
description: `Industry-tailored professional design for ${industry}`,
|
||||
prompt_index: 2
|
||||
}
|
||||
]);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: 'Using fallback prompts due to API error. Please try again later.'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle prompt selection and image generation
|
||||
const handlePromptSelect = async (prompt: ImagePrompt) => {
|
||||
setState(prev => ({ ...prev, selectedPrompt: prompt }));
|
||||
|
||||
try {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isGenerating: true,
|
||||
error: null,
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
// Call the actual backend API for image generation
|
||||
const response = await fetch('/api/linkedin/generate-image', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: prompt.prompt,
|
||||
content_context: {
|
||||
topic,
|
||||
industry,
|
||||
content_type: contentType,
|
||||
content,
|
||||
style: prompt.style
|
||||
},
|
||||
aspect_ratio: '1:1'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Image generation failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isGenerating: false,
|
||||
generatedImage: result,
|
||||
progress: 100
|
||||
}));
|
||||
|
||||
if (onImageGenerated) {
|
||||
onImageGenerated(result);
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error || 'Image generation failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating image:', error);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isGenerating: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to generate image'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Progress simulation for better UX
|
||||
useEffect(() => {
|
||||
if (state.isGenerating) {
|
||||
const interval = setInterval(() => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
progress: Math.min(prev.progress + Math.random() * 15, 90)
|
||||
}));
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [state.isGenerating]);
|
||||
|
||||
return (
|
||||
<div className={`image-generation-suggestions ${className}`}>
|
||||
{/* Main Suggestion Card */}
|
||||
{!showPrompts && !state.generatedImage && (
|
||||
<div className="suggestion-card">
|
||||
<div className="suggestion-header">
|
||||
<div className="suggestion-icon">
|
||||
<PhotoIcon className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<div className="suggestion-content">
|
||||
<h3 className="suggestion-title">
|
||||
Enhance Your {contentType.charAt(0).toUpperCase() + contentType.slice(1)} with AI-Generated Images
|
||||
</h3>
|
||||
<p className="suggestion-description">
|
||||
Create professional, LinkedIn-optimized images that perfectly complement your content and boost engagement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="suggestion-features">
|
||||
<div className="feature-item">
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
<span>3 distinct visual styles</span>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
<span>Content-aware prompts</span>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
<span>LinkedIn-optimized</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGeneratePrompts}
|
||||
className="generate-prompts-btn"
|
||||
disabled={state.isGenerating}
|
||||
>
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
Generate Image Prompts
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prompt Selection */}
|
||||
{showPrompts && !state.isGenerating && !state.generatedImage && (
|
||||
<div className="prompts-selection">
|
||||
<div className="prompts-header">
|
||||
<h3 className="prompts-title">Choose Your Visual Style</h3>
|
||||
<p className="prompts-subtitle">
|
||||
Select from three AI-optimized image styles that match your content
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="prompts-grid">
|
||||
{prompts.map((prompt) => (
|
||||
<div
|
||||
key={prompt.prompt_index}
|
||||
className={`prompt-card ${state.selectedPrompt?.prompt_index === prompt.prompt_index ? 'selected' : ''}`}
|
||||
onClick={() => handlePromptSelect(prompt)}
|
||||
>
|
||||
<div className="prompt-header">
|
||||
<div className="prompt-style-badge">
|
||||
{prompt.style}
|
||||
</div>
|
||||
</div>
|
||||
<div className="prompt-content">
|
||||
<p className="prompt-description">{prompt.description}</p>
|
||||
<div className="prompt-preview">
|
||||
{prompt.prompt.substring(0, 120)}...
|
||||
</div>
|
||||
</div>
|
||||
<div className="prompt-actions">
|
||||
<button className="select-prompt-btn">
|
||||
Select & Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowPrompts(false)}
|
||||
className="back-btn"
|
||||
>
|
||||
← Back to Suggestions
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image Generation Progress */}
|
||||
{state.isGenerating && (
|
||||
<div className="generation-progress">
|
||||
<div className="progress-header">
|
||||
<PhotoIcon className="h-6 w-6 text-blue-600 animate-pulse" />
|
||||
<h3 className="progress-title">Generating Your Image</h3>
|
||||
</div>
|
||||
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: `${state.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="progress-status">
|
||||
<span className="progress-text">
|
||||
{state.selectedPrompt?.style} style • {Math.round(state.progress)}% complete
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="progress-message">
|
||||
Creating a professional, LinkedIn-optimized image...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generated Image Display */}
|
||||
{state.generatedImage && (
|
||||
<div className="generated-image">
|
||||
<div className="image-header">
|
||||
<CheckCircleIcon className="h-6 w-6 text-green-500" />
|
||||
<h3 className="image-title">Image Generated Successfully!</h3>
|
||||
</div>
|
||||
|
||||
<div className="image-preview">
|
||||
<img
|
||||
src={state.generatedImage.image_url || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDMwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIiBmaWxsPSIjRjNGNEY2Ii8+Cjx0ZXh0IHg9IjE1MCIgeT0iMTUwIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiM2QjcyODAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj5JbWFnZSBHZW5lcmF0ZWQ8L3RleHQ+Cjwvc3ZnPgo='}
|
||||
alt="Generated LinkedIn image"
|
||||
className="preview-image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="image-actions">
|
||||
<button className="action-btn primary">
|
||||
<PhotoIcon className="h-4 w-4" />
|
||||
Use This Image
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
Generate Another
|
||||
</button>
|
||||
<button className="action-btn secondary">
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
Edit Image
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="image-metadata">
|
||||
<div className="metadata-item">
|
||||
<span className="metadata-label">Style:</span>
|
||||
<span className="metadata-value">{state.selectedPrompt?.style}</span>
|
||||
</div>
|
||||
<div className="metadata-item">
|
||||
<span className="metadata-label">Aspect Ratio:</span>
|
||||
<span className="metadata-value">1:1 (Square)</span>
|
||||
</div>
|
||||
<div className="metadata-item">
|
||||
<span className="metadata-label">Optimized for:</span>
|
||||
<span className="metadata-value">LinkedIn {contentType}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{state.error && (
|
||||
<div className="error-message">
|
||||
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
||||
<span className="error-text">{state.error}</span>
|
||||
<button
|
||||
onClick={() => setState(prev => ({ ...prev, error: null }))}
|
||||
className="error-dismiss"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGenerationSuggestions;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { ImageGenerationSuggestions } from './index';
|
||||
|
||||
const ImageGenerationTest: React.FC = () => {
|
||||
const handleImageGenerated = (imageData: any) => {
|
||||
console.log('Image generated successfully:', imageData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
|
||||
<h1>Image Generation Test</h1>
|
||||
<p>Testing the ImageGenerationSuggestions component...</p>
|
||||
|
||||
<div style={{
|
||||
background: '#f8f9fa',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '20px',
|
||||
border: '1px solid #e9ecef'
|
||||
}}>
|
||||
<h3>🎯 How It Works Now:</h3>
|
||||
<ol>
|
||||
<li><strong>Generate LinkedIn Content:</strong> Use Copilot to generate a post, article, or carousel</li>
|
||||
<li><strong>See Image Suggestions:</strong> After content generation, Copilot will automatically suggest image generation</li>
|
||||
<li><strong>Ask for Images:</strong> Type "Generate images for my LinkedIn post" or similar</li>
|
||||
<li><strong>Choose Style:</strong> Select from Professional, Creative, or Industry-Specific styles</li>
|
||||
</ol>
|
||||
|
||||
<div style={{
|
||||
background: '#e3f2fd',
|
||||
padding: '15px',
|
||||
borderRadius: '6px',
|
||||
marginTop: '15px',
|
||||
border: '1px solid #2196f3'
|
||||
}}>
|
||||
<strong>💡 Pro Tip:</strong> The image generation suggestions now appear automatically after every successful content generation in the Copilot chat!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ImageGenerationSuggestions
|
||||
contentType="post"
|
||||
topic="AI in Marketing"
|
||||
industry="Technology"
|
||||
content="This is a test LinkedIn post about AI in marketing. It demonstrates the image generation capabilities."
|
||||
onImageGenerated={handleImageGenerated}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGenerationTest;
|
||||
@@ -0,0 +1,247 @@
|
||||
import React from 'react';
|
||||
|
||||
type ProgressStatus = 'pending' | 'active' | 'completed' | 'error';
|
||||
|
||||
interface ProgressStep {
|
||||
id: string;
|
||||
label: string;
|
||||
status: ProgressStatus;
|
||||
message?: string;
|
||||
details?: Record<string, any>;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
interface ProgressTrackerProps {
|
||||
steps: ProgressStep[];
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export const ProgressTracker: React.FC<ProgressTrackerProps> = ({ steps, active }) => {
|
||||
if (!steps || steps.length === 0) return null;
|
||||
|
||||
const completedSteps = steps.filter(step => step.status === 'completed').length;
|
||||
const progressPercentage = Math.round((completedSteps / steps.length) * 100);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
marginBottom: '24px',
|
||||
padding: '20px',
|
||||
borderRadius: '16px',
|
||||
border: '1px solid rgba(10,102,194,0.15)',
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.98), rgba(250,253,255,0.98))',
|
||||
boxShadow: '0 8px 32px rgba(10,102,194,0.12)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Header with progress percentage */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '20px',
|
||||
paddingBottom: '16px',
|
||||
borderBottom: '1px solid rgba(10,102,194,0.1)'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '#0f172a'
|
||||
}}>
|
||||
LinkedIn Content Generation
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '#0a66c2',
|
||||
padding: '6px 12px',
|
||||
background: 'rgba(10,102,194,0.1)',
|
||||
borderRadius: '20px'
|
||||
}}>
|
||||
{progressPercentage}% Complete
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '6px',
|
||||
background: '#e2e8f0',
|
||||
borderRadius: '3px',
|
||||
marginBottom: '20px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${progressPercentage}%`,
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, #0a66c2, #3b82f6)',
|
||||
borderRadius: '3px',
|
||||
transition: 'width 0.5s ease',
|
||||
boxShadow: '0 0 8px rgba(10,102,194,0.3)'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px'
|
||||
}}>
|
||||
{steps.map((step, idx) => (
|
||||
<div key={step.id} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '16px',
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
background: step.status === 'active' ? 'rgba(10,102,194,0.05)' : 'transparent',
|
||||
border: step.status === 'active' ? '1px solid rgba(10,102,194,0.2)' : '1px solid transparent',
|
||||
transition: 'all 300ms ease',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Step indicator */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
background: step.status === 'completed' ? '#10b981' :
|
||||
step.status === 'active' ? '#0a66c2' :
|
||||
step.status === 'error' ? '#ef4444' : '#cbd5e1',
|
||||
color: 'white',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
flexShrink: 0,
|
||||
position: 'relative'
|
||||
}}>
|
||||
{step.status === 'completed' ? '✓' :
|
||||
step.status === 'active' ? '●' :
|
||||
step.status === 'error' ? '✕' : (idx + 1)}
|
||||
|
||||
{/* Active step glow effect */}
|
||||
{step.status === 'active' && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '-4px',
|
||||
left: '-4px',
|
||||
right: '-4px',
|
||||
bottom: '-4px',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(10,102,194,0.3) 0%, transparent 70%)',
|
||||
animation: 'pulse 2s ease-in-out infinite alternate',
|
||||
zIndex: -1
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: step.status === 'active' ? '#0a66c2' :
|
||||
step.status === 'completed' ? '#10b981' :
|
||||
step.status === 'error' ? '#ef4444' : '#64748b',
|
||||
marginBottom: '4px',
|
||||
transition: 'color 200ms ease'
|
||||
}}>
|
||||
{step.label}
|
||||
</div>
|
||||
|
||||
{/* Step message */}
|
||||
{step.message && (
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
color: step.status === 'active' ? '#475569' : '#94a3b8',
|
||||
lineHeight: '1.4',
|
||||
fontStyle: step.status === 'active' ? 'normal' : 'italic'
|
||||
}}>
|
||||
{step.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step details */}
|
||||
{step.details && step.status === 'completed' && (
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(16,185,129,0.1)',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px',
|
||||
color: '#065f46'
|
||||
}}>
|
||||
{Object.entries(step.details).map(([key, value]) => (
|
||||
<div key={key} style={{ marginBottom: '4px' }}>
|
||||
<strong>{key}:</strong> {String(value)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status indicator */}
|
||||
<div style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '500',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
background: step.status === 'completed' ? 'rgba(16,185,129,0.1)' :
|
||||
step.status === 'active' ? 'rgba(10,102,194,0.1)' :
|
||||
step.status === 'error' ? 'rgba(239,68,68,0.1)' : 'rgba(203,213,225,0.1)',
|
||||
color: step.status === 'completed' ? '#065f46' :
|
||||
step.status === 'active' ? '#0a66c2' :
|
||||
step.status === 'error' ? '#991b1b' : '#64748b',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{step.status}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Active status indicator */}
|
||||
{active && (
|
||||
<div style={{
|
||||
marginTop: '20px',
|
||||
padding: '12px 16px',
|
||||
background: 'rgba(10,102,194,0.05)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(10,102,194,0.1)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
background: '#0a66c2',
|
||||
animation: 'pulse 1.5s ease-in-out infinite'
|
||||
}} />
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#0a66c2',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
Content generation in progress...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CSS Animations */}
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@keyframes pulse {
|
||||
0% { opacity: 0.6; transform: scale(1); }
|
||||
100% { opacity: 1; transform: scale(1.1); }
|
||||
}
|
||||
`
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
# LinkedIn Image Generation Components
|
||||
|
||||
This document provides comprehensive documentation for the LinkedIn Image Generation components that integrate with CopilotKit to provide AI-powered image generation capabilities.
|
||||
|
||||
## 🚀 Overview
|
||||
|
||||
The Image Generation components provide a seamless way to generate professional, LinkedIn-optimized images for content using Google's Gemini API. The system analyzes generated LinkedIn content and creates contextually relevant image prompts.
|
||||
|
||||
## 📁 Components
|
||||
|
||||
### 1. ImageGenerationSuggestions
|
||||
|
||||
The main component that handles the complete image generation workflow.
|
||||
|
||||
**Location**: `ImageGenerationSuggestions.tsx`
|
||||
|
||||
**Features**:
|
||||
- Content-aware image prompt generation
|
||||
- Three distinct visual styles (Professional, Creative, Industry-Specific)
|
||||
- Real-time progress tracking
|
||||
- Error handling with fallback prompts
|
||||
- Mobile-optimized responsive design
|
||||
- CopilotKit integration
|
||||
|
||||
### 2. ImageGenerationDemo
|
||||
|
||||
A demonstration component showcasing the ImageGenerationSuggestions functionality.
|
||||
|
||||
**Location**: `ImageGenerationDemo.tsx`
|
||||
|
||||
**Features**:
|
||||
- Sample LinkedIn content display
|
||||
- Interactive workflow demonstration
|
||||
- Step-by-step explanation
|
||||
- Responsive layout
|
||||
|
||||
## 🔧 Installation & Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **CopilotKit**: Ensure CopilotKit is properly configured in your project
|
||||
2. **Heroicons**: Install Heroicons for the icon components
|
||||
3. **Backend API**: The backend image generation services must be running
|
||||
|
||||
### Dependencies
|
||||
|
||||
```bash
|
||||
npm install @heroicons/react
|
||||
# or
|
||||
yarn add @heroicons/react
|
||||
```
|
||||
|
||||
### Import
|
||||
|
||||
```typescript
|
||||
import { ImageGenerationSuggestions, ImageGenerationDemo } from './components';
|
||||
```
|
||||
|
||||
## 📖 Usage
|
||||
|
||||
### Basic Implementation
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { ImageGenerationSuggestions } from './components';
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
const handleImageGenerated = (imageData: any) => {
|
||||
console.log('Image generated:', imageData);
|
||||
// Handle the generated image
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageGenerationSuggestions
|
||||
contentType="post"
|
||||
topic="AI in Marketing"
|
||||
industry="Technology"
|
||||
content="Your LinkedIn content here..."
|
||||
onImageGenerated={handleImageGenerated}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Integration with LinkedIn Content
|
||||
|
||||
```typescript
|
||||
import React, { useState } from 'react';
|
||||
import { ImageGenerationSuggestions } from './components';
|
||||
|
||||
interface LinkedInContent {
|
||||
contentType: 'post' | 'article' | 'carousel' | 'video_script';
|
||||
topic: string;
|
||||
industry: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const LinkedInContentEditor: React.FC = () => {
|
||||
const [content, setContent] = useState<LinkedInContent>({
|
||||
contentType: 'post',
|
||||
topic: '',
|
||||
industry: '',
|
||||
content: ''
|
||||
});
|
||||
|
||||
const [generatedImage, setGeneratedImage] = useState<any>(null);
|
||||
|
||||
const handleImageGenerated = (imageData: any) => {
|
||||
setGeneratedImage(imageData);
|
||||
// Update your content editor with the generated image
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="linkedin-editor">
|
||||
{/* Your existing content editor */}
|
||||
|
||||
{/* Image generation suggestions */}
|
||||
{content.content && (
|
||||
<ImageGenerationSuggestions
|
||||
contentType={content.contentType}
|
||||
topic={content.topic}
|
||||
industry={content.industry}
|
||||
content={content.content}
|
||||
onImageGenerated={handleImageGenerated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Display generated image */}
|
||||
{generatedImage && (
|
||||
<div className="generated-image-display">
|
||||
<img src={generatedImage.image_url} alt="Generated LinkedIn image" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Demo Component
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { ImageGenerationDemo } from './components';
|
||||
|
||||
const DemoPage: React.FC = () => {
|
||||
return (
|
||||
<div className="demo-page">
|
||||
<ImageGenerationDemo />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 🎨 Props Interface
|
||||
|
||||
### ImageGenerationSuggestions Props
|
||||
|
||||
```typescript
|
||||
interface ImageGenerationSuggestionsProps {
|
||||
contentType: 'post' | 'article' | 'carousel' | 'video_script';
|
||||
topic: string;
|
||||
industry: string;
|
||||
content: string;
|
||||
onImageGenerated?: (imageData: any) => void;
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Props Description**:
|
||||
|
||||
- **contentType**: The type of LinkedIn content (post, article, carousel, or video_script)
|
||||
- **topic**: The main topic or subject of the content
|
||||
- **industry**: The industry context for the content
|
||||
- **content**: The actual LinkedIn content text
|
||||
- **onImageGenerated**: Callback function when an image is successfully generated
|
||||
- **className**: Optional CSS class for custom styling
|
||||
|
||||
## 🔄 Component States
|
||||
|
||||
The component manages several states to provide a smooth user experience:
|
||||
|
||||
1. **Initial State**: Shows the main suggestion card
|
||||
2. **Prompt Generation**: Displays three AI-optimized image prompts
|
||||
3. **Image Generation**: Shows progress bar and status
|
||||
4. **Success State**: Displays the generated image with action buttons
|
||||
5. **Error State**: Shows error messages with retry options
|
||||
|
||||
## 🎯 User Flow
|
||||
|
||||
1. **Content Generation Complete**: User finishes creating LinkedIn content
|
||||
2. **Image Suggestion**: Component automatically suggests image generation
|
||||
3. **Prompt Selection**: User chooses from three visual styles
|
||||
4. **Image Creation**: AI generates LinkedIn-optimized image
|
||||
5. **Result Display**: Generated image shown with management options
|
||||
6. **Integration**: Image ready for use in LinkedIn content
|
||||
|
||||
## 🎨 Visual Styles
|
||||
|
||||
The component generates three distinct image styles:
|
||||
|
||||
### 1. Professional Style
|
||||
- Corporate aesthetics and clean lines
|
||||
- Professional color scheme (blues, grays, whites)
|
||||
- Business-appropriate imagery
|
||||
- Clean typography and layout
|
||||
|
||||
### 2. Creative Style
|
||||
- Engaging and eye-catching visuals
|
||||
- Vibrant colors while maintaining professionalism
|
||||
- Social media engagement optimization
|
||||
- Modern design elements
|
||||
|
||||
### 3. Industry-Specific Style
|
||||
- Tailored to specific business sectors
|
||||
- Industry-relevant imagery and colors
|
||||
- Professional appeal for target audience
|
||||
- Contextual visual elements
|
||||
|
||||
## 🔌 CopilotKit Integration
|
||||
|
||||
The component integrates seamlessly with CopilotKit through two main actions:
|
||||
|
||||
### 1. generate_image_prompts
|
||||
Generates three AI-optimized image prompts based on content analysis.
|
||||
|
||||
**Parameters**:
|
||||
- `content_type`: Type of LinkedIn content
|
||||
- `topic`: Content topic
|
||||
- `industry`: Industry context
|
||||
- `content`: Actual content text
|
||||
|
||||
### 2. generate_linkedin_image
|
||||
Creates LinkedIn-optimized images from selected prompts.
|
||||
|
||||
**Parameters**:
|
||||
- `prompt`: Selected image prompt
|
||||
- `content_context`: Full content context object
|
||||
- `aspect_ratio`: Image aspect ratio (default: "1:1")
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
The component is fully responsive and mobile-optimized:
|
||||
|
||||
- **Desktop**: Full-width layout with side-by-side content
|
||||
- **Tablet**: Adaptive grid layouts
|
||||
- **Mobile**: Stacked layout with touch-friendly buttons
|
||||
- **Accessibility**: Proper focus states and keyboard navigation
|
||||
|
||||
## 🎨 Customization
|
||||
|
||||
### CSS Customization
|
||||
|
||||
The component uses CSS custom properties and can be styled through:
|
||||
|
||||
```css
|
||||
.image-generation-suggestions {
|
||||
/* Custom styles */
|
||||
}
|
||||
|
||||
.suggestion-card {
|
||||
/* Customize suggestion card */
|
||||
}
|
||||
|
||||
.prompt-card {
|
||||
/* Customize prompt selection cards */
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Support
|
||||
|
||||
The component includes built-in dark mode support and can be extended with custom themes.
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Component Testing
|
||||
|
||||
```typescript
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ImageGenerationSuggestions } from './components';
|
||||
|
||||
describe('ImageGenerationSuggestions', () => {
|
||||
it('renders suggestion card initially', () => {
|
||||
render(
|
||||
<ImageGenerationSuggestions
|
||||
contentType="post"
|
||||
topic="Test Topic"
|
||||
industry="Technology"
|
||||
content="Test content"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Enhance Your Post with AI-Generated Images/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('generates prompts when button is clicked', async () => {
|
||||
// Test implementation
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
Test the component with your existing LinkedIn content workflow:
|
||||
|
||||
1. Generate LinkedIn content
|
||||
2. Trigger image generation
|
||||
3. Select image prompt
|
||||
4. Verify image generation
|
||||
5. Test error handling
|
||||
|
||||
## 🚀 Performance Considerations
|
||||
|
||||
- **Lazy Loading**: Images are loaded only when needed
|
||||
- **Progress Simulation**: Smooth progress animation for better UX
|
||||
- **Error Boundaries**: Graceful error handling with fallbacks
|
||||
- **Memory Management**: Proper cleanup of intervals and event listeners
|
||||
|
||||
## 🔒 Security & Validation
|
||||
|
||||
- **Input Validation**: All props are validated before processing
|
||||
- **API Security**: Secure API calls to backend services
|
||||
- **Error Handling**: No sensitive information exposed in error messages
|
||||
- **Content Filtering**: Generated content follows LinkedIn guidelines
|
||||
|
||||
## 📚 API Endpoints
|
||||
|
||||
The component expects these backend endpoints:
|
||||
|
||||
- `POST /api/linkedin/generate-image-prompts` - Generate image prompts
|
||||
- `POST /api/linkedin/generate-image` - Create image from prompt
|
||||
- `POST /api/linkedin/edit-image` - Edit existing image
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Prompts Not Generating**: Check backend API connectivity
|
||||
2. **Images Not Loading**: Verify image generation service status
|
||||
3. **Styling Issues**: Ensure CSS is properly imported
|
||||
4. **CopilotKit Errors**: Verify CopilotKit configuration
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging by checking browser console for detailed error information.
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
Planned features for upcoming versions:
|
||||
|
||||
- **Batch Image Generation**: Multiple images from single prompt
|
||||
- **Style Transfer**: Apply consistent visual themes
|
||||
- **Brand Templates**: Company-specific image styles
|
||||
- **Advanced Editing**: More sophisticated image modification options
|
||||
- **Analytics**: Track image performance and user engagement
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For issues or questions:
|
||||
|
||||
1. Check the troubleshooting section above
|
||||
2. Review the CopilotKit documentation
|
||||
3. Check backend service logs
|
||||
4. Review component error messages in browser console
|
||||
|
||||
## 📄 License
|
||||
|
||||
This component is part of the Alwrity LinkedIn Writer project and follows the same licensing terms.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: Current Session
|
||||
**Version**: 1.0.0
|
||||
**Status**: Ready for Production Use
|
||||
@@ -9,3 +9,13 @@ export { Header } from './Header';
|
||||
export { ContentEditor } from './ContentEditor';
|
||||
export { LoadingIndicator } from './LoadingIndicator';
|
||||
export { WelcomeMessage } from './WelcomeMessage';
|
||||
export { ProgressTracker } from './ProgressTracker';
|
||||
export { ContentRecommendations } from './ContentRecommendations';
|
||||
export { CopilotRecommendationsMessage } from './CopilotRecommendationsMessage';
|
||||
export { CustomMessageRenderer } from './CustomMessageRenderer';
|
||||
export { CopilotRecommendationsRenderer } from './CopilotRecommendationsRenderer';
|
||||
|
||||
// Image Generation Components
|
||||
export { default as ImageGenerationSuggestions } from './ImageGenerationSuggestions';
|
||||
export { default as ImageGenerationDemo } from './ImageGenerationDemo';
|
||||
export { default as ImageGenerationTest } from './ImageGenerationTest';
|
||||
|
||||
@@ -32,6 +32,19 @@ export function useLinkedInWriter() {
|
||||
const [groundingEnabled, setGroundingEnabled] = useState(false);
|
||||
const [searchQueries, setSearchQueries] = useState<string[]>([]);
|
||||
|
||||
// Progress state (lightweight custom system)
|
||||
type ProgressStatus = 'pending' | 'active' | 'completed' | 'error';
|
||||
type ProgressStep = {
|
||||
id: string;
|
||||
label: string;
|
||||
status: ProgressStatus;
|
||||
message?: string;
|
||||
details?: any;
|
||||
timestamp?: string;
|
||||
};
|
||||
const [progressSteps, setProgressSteps] = useState<ProgressStep[]>([]);
|
||||
const [progressActive, setProgressActive] = useState<boolean>(false);
|
||||
|
||||
// Chat history state
|
||||
const [historyVersion, setHistoryVersion] = useState<number>(0);
|
||||
const [chatHistory, setChatHistory] = useState<ChatMsg[]>([]);
|
||||
@@ -43,6 +56,7 @@ export function useLinkedInWriter() {
|
||||
const [showPreferencesModal, setShowPreferencesModal] = useState(false);
|
||||
const [showContextModal, setShowContextModal] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [justGeneratedContent, setJustGeneratedContent] = useState(false);
|
||||
|
||||
// Update suggestions when context changes
|
||||
const updateSuggestions = useCallback(() => {
|
||||
@@ -62,6 +76,13 @@ export function useLinkedInWriter() {
|
||||
savePreferences({ last_used_actions: updatedActions });
|
||||
setUserPreferences(prev => ({ ...prev, last_used_actions: updatedActions }));
|
||||
|
||||
// Mark content as just generated for content creation actions
|
||||
if (['generateLinkedInPost', 'generateLinkedInArticle', 'generateLinkedInCarousel', 'generateLinkedInVideoScript'].includes(actionName)) {
|
||||
setJustGeneratedContent(true);
|
||||
// Reset the flag after 30 seconds
|
||||
setTimeout(() => setJustGeneratedContent(false), 30000);
|
||||
}
|
||||
|
||||
// Update suggestions after action usage
|
||||
setTimeout(() => updateSuggestions(), 100);
|
||||
}, [updateSuggestions]);
|
||||
@@ -93,6 +114,75 @@ export function useLinkedInWriter() {
|
||||
loadInitialData();
|
||||
}, []);
|
||||
|
||||
// Listen for lightweight progress events
|
||||
useEffect(() => {
|
||||
const handleProgressInit = (event: CustomEvent) => {
|
||||
const steps: Array<{ id: string; label: string; message?: string }> = event.detail?.steps || [];
|
||||
const initialized: ProgressStep[] = steps.map((s, index) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
message: s.message,
|
||||
status: index === 0 ? 'active' : 'pending',
|
||||
timestamp: new Date().toISOString()
|
||||
}));
|
||||
setProgressSteps(initialized);
|
||||
setProgressActive(true);
|
||||
};
|
||||
|
||||
const handleProgressStep = (event: CustomEvent) => {
|
||||
const { id, status, details, message } = event.detail || {};
|
||||
if (!id) return;
|
||||
setProgressSteps(prev => {
|
||||
const updated = prev.map(step => step.id === id ? {
|
||||
...step,
|
||||
status: (status || 'completed') as ProgressStatus,
|
||||
details,
|
||||
message,
|
||||
timestamp: new Date().toISOString()
|
||||
} : step);
|
||||
// Mark next pending as active if current completed
|
||||
if ((status || 'completed') === 'completed') {
|
||||
const nextIdx = updated.findIndex(s => s.status === 'pending');
|
||||
if (nextIdx !== -1) {
|
||||
updated[nextIdx] = {
|
||||
...updated[nextIdx],
|
||||
status: 'active',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleProgressComplete = () => {
|
||||
setProgressSteps(prev => prev.map(s => s.status === 'completed' ? s : { ...s, status: 'completed', timestamp: new Date().toISOString() }));
|
||||
setProgressActive(false);
|
||||
// Keep progress visible for a moment to show completion, then hide
|
||||
setTimeout(() => {
|
||||
setProgressSteps([]);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleProgressError = (event: CustomEvent) => {
|
||||
const { id, details } = event.detail || {};
|
||||
setProgressSteps(prev => prev.map(s => (id ? (s.id === id) : (s.status === 'active')) ? { ...s, status: 'error', details, timestamp: new Date().toISOString() } : s));
|
||||
setProgressActive(false);
|
||||
};
|
||||
|
||||
window.addEventListener('linkedinwriter:progressInit', handleProgressInit as EventListener);
|
||||
window.addEventListener('linkedinwriter:progressStep', handleProgressStep as EventListener);
|
||||
window.addEventListener('linkedinwriter:progressComplete', handleProgressComplete as EventListener);
|
||||
window.addEventListener('linkedinwriter:progressError', handleProgressError as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('linkedinwriter:progressInit', handleProgressInit as EventListener);
|
||||
window.removeEventListener('linkedinwriter:progressStep', handleProgressStep as EventListener);
|
||||
window.removeEventListener('linkedinwriter:progressComplete', handleProgressComplete as EventListener);
|
||||
window.removeEventListener('linkedinwriter:progressError', handleProgressError as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Listen for grounding data updates from CopilotKit actions
|
||||
useEffect(() => {
|
||||
const handleGroundingDataUpdate = (event: CustomEvent) => {
|
||||
@@ -150,6 +240,9 @@ export function useLinkedInWriter() {
|
||||
setCurrentAction(null);
|
||||
// Auto-show preview when new content is generated
|
||||
setShowPreview(true);
|
||||
// Hide progress tracker when content is generated
|
||||
setProgressActive(false);
|
||||
setProgressSteps([]);
|
||||
};
|
||||
|
||||
const handleAppendDraft = (event: CustomEvent) => {
|
||||
@@ -271,6 +364,7 @@ export function useLinkedInWriter() {
|
||||
showPreferencesModal,
|
||||
showContextModal,
|
||||
showPreview,
|
||||
justGeneratedContent,
|
||||
|
||||
// Setters
|
||||
setDraft,
|
||||
@@ -288,6 +382,7 @@ export function useLinkedInWriter() {
|
||||
setShowPreferencesModal,
|
||||
setShowContextModal,
|
||||
setShowPreview,
|
||||
setJustGeneratedContent: setJustGeneratedContent,
|
||||
|
||||
// Handlers
|
||||
handleDraftChange,
|
||||
@@ -313,6 +408,10 @@ export function useLinkedInWriter() {
|
||||
setCitations,
|
||||
setQualityMetrics,
|
||||
setGroundingEnabled,
|
||||
setSearchQueries
|
||||
setSearchQueries,
|
||||
|
||||
// Progress (exposed to UI)
|
||||
progressSteps,
|
||||
progressActive
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface Recommendation {
|
||||
id: string;
|
||||
text: string;
|
||||
category: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
export const useRecommendations = () => {
|
||||
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
|
||||
const [showRecommendations, setShowRecommendations] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRecommendationsUpdate = (event: CustomEvent) => {
|
||||
const { recommendations: newRecommendations } = event.detail || {};
|
||||
if (newRecommendations && Array.isArray(newRecommendations)) {
|
||||
// Convert string recommendations to structured format
|
||||
const structuredRecommendations: Recommendation[] = newRecommendations.map((rec, index) => ({
|
||||
id: `rec-${index}`,
|
||||
text: rec,
|
||||
category: 'content-improvement',
|
||||
priority: 'medium' as const
|
||||
}));
|
||||
|
||||
setRecommendations(structuredRecommendations);
|
||||
setShowRecommendations(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('linkedinwriter:recommendationsUpdate', handleRecommendationsUpdate as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('linkedinwriter:recommendationsUpdate', handleRecommendationsUpdate as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleRecommendationSelect = (recommendation: Recommendation) => {
|
||||
console.log('Selected recommendation:', recommendation);
|
||||
|
||||
// Here you can implement specific actions for each recommendation
|
||||
// For now, we'll just log it and could trigger specific improvement actions
|
||||
|
||||
// Example: Trigger specific improvement actions based on recommendation
|
||||
if (recommendation.text.toLowerCase().includes('factual accuracy')) {
|
||||
// Could trigger factual accuracy improvement workflow
|
||||
console.log('Triggering factual accuracy improvement workflow');
|
||||
} else if (recommendation.text.toLowerCase().includes('professional tone')) {
|
||||
// Could trigger tone improvement workflow
|
||||
console.log('Triggering professional tone improvement workflow');
|
||||
} else if (recommendation.text.toLowerCase().includes('citation')) {
|
||||
// Could trigger citation improvement workflow
|
||||
console.log('Triggering citation improvement workflow');
|
||||
}
|
||||
|
||||
// You could also dispatch events to trigger specific CopilotKit actions
|
||||
window.dispatchEvent(new CustomEvent('linkedinwriter:improvementRequested', {
|
||||
detail: { recommendation, action: 'improve' }
|
||||
}));
|
||||
};
|
||||
|
||||
const hideRecommendations = () => {
|
||||
setShowRecommendations(false);
|
||||
setRecommendations([]);
|
||||
};
|
||||
|
||||
return {
|
||||
recommendations,
|
||||
showRecommendations,
|
||||
handleRecommendationSelect,
|
||||
hideRecommendations
|
||||
};
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ProgressTracker } from './components/ProgressTracker';
|
||||
|
||||
type ProgressStatus = 'pending' | 'active' | 'completed' | 'error';
|
||||
|
||||
interface TestProgressStep {
|
||||
id: string;
|
||||
label: string;
|
||||
status: ProgressStatus;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Test component to verify enhanced progress tracking
|
||||
export const TestEnhancedProgress: React.FC = () => {
|
||||
const [testSteps, setTestSteps] = useState<TestProgressStep[]>([
|
||||
{ id: 'personalize', label: 'Personalizing topic & context', status: 'pending' },
|
||||
{ id: 'prepare_queries', label: 'Preparing research queries', status: 'pending' },
|
||||
{ id: 'research', label: 'Conducting research & analysis', status: 'pending' },
|
||||
{ id: 'grounding', label: 'Applying AI grounding', status: 'pending' },
|
||||
{ id: 'content_generation', label: 'Generating content', status: 'pending' },
|
||||
{ id: 'citations', label: 'Extracting citations', status: 'pending' },
|
||||
{ id: 'quality_analysis', label: 'Quality assessment', status: 'pending' },
|
||||
{ id: 'finalize', label: 'Finalizing & optimizing', status: 'pending' }
|
||||
]);
|
||||
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
|
||||
const startTest = () => {
|
||||
setIsActive(true);
|
||||
setTestSteps(prev => prev.map((step, index) =>
|
||||
index === 0 ? { ...step, status: 'active', message: 'Analyzing topic, industry context, and target audience...' } : step
|
||||
));
|
||||
|
||||
// Simulate progress updates
|
||||
let currentStep = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (currentStep < testSteps.length) {
|
||||
setTestSteps(prev => {
|
||||
const updated = [...prev];
|
||||
// Mark current step as completed
|
||||
if (currentStep > 0) {
|
||||
updated[currentStep - 1] = {
|
||||
...updated[currentStep - 1],
|
||||
status: 'completed',
|
||||
message: getCompletionMessage(currentStep - 1)
|
||||
};
|
||||
}
|
||||
// Mark next step as active
|
||||
if (currentStep < updated.length) {
|
||||
updated[currentStep] = {
|
||||
...updated[currentStep],
|
||||
status: 'active',
|
||||
message: getActiveMessage(currentStep)
|
||||
};
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
currentStep++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
setIsActive(false);
|
||||
// Mark all as completed
|
||||
setTestSteps(prev => prev.map(step => ({
|
||||
...step,
|
||||
status: 'completed',
|
||||
message: getCompletionMessage(testSteps.findIndex(s => s.id === step.id))
|
||||
})));
|
||||
}
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const getActiveMessage = (stepIndex: number): string => {
|
||||
const messages = [
|
||||
'Analyzing topic, industry context, and target audience...',
|
||||
'Preparing research queries for content generation...',
|
||||
'Conducting research and analyzing industry trends...',
|
||||
'Applying AI grounding for enhanced accuracy...',
|
||||
'Generating content with industry insights...',
|
||||
'Extracting citations and references...',
|
||||
'Assessing content quality and relevance...',
|
||||
'Finalizing and optimizing content...'
|
||||
];
|
||||
return messages[stepIndex] || 'Processing...';
|
||||
};
|
||||
|
||||
const getCompletionMessage = (stepIndex: number): string => {
|
||||
const messages = [
|
||||
'Topic personalized successfully',
|
||||
'Research queries prepared',
|
||||
'Research completed with industry insights',
|
||||
'AI grounding applied successfully',
|
||||
'Content generated with professional quality',
|
||||
'Citations extracted and formatted',
|
||||
'Quality assessment completed',
|
||||
'Content finalized and optimized'
|
||||
];
|
||||
return messages[stepIndex] || 'Step completed';
|
||||
};
|
||||
|
||||
const resetTest = () => {
|
||||
setTestSteps(prev => prev.map(step => ({
|
||||
...step,
|
||||
status: 'pending' as const,
|
||||
message: undefined
|
||||
})));
|
||||
setIsActive(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
|
||||
<h2 style={{ color: '#0f172a', marginBottom: '20px' }}>
|
||||
Enhanced LinkedIn Progress Tracker Test
|
||||
</h2>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<button
|
||||
onClick={startTest}
|
||||
disabled={isActive}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: isActive ? '#cbd5e1' : '#0a66c2',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
cursor: isActive ? 'not-allowed' : 'pointer',
|
||||
marginRight: '10px'
|
||||
}}
|
||||
>
|
||||
{isActive ? 'Running...' : 'Start Progress Test'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={resetTest}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: '#64748b',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Reset Test
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px', padding: '16px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0', color: '#374151' }}>Test Description:</h3>
|
||||
<p style={{ margin: 0, color: '#6b7280', lineHeight: '1.5' }}>
|
||||
This test demonstrates the enhanced LinkedIn progress tracker with detailed messages,
|
||||
progress percentages, and improved visual design. The tracker now shows informative
|
||||
messages for each step, making it easier for users to understand what's happening
|
||||
during content generation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ProgressTracker steps={testSteps} active={isActive} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestEnhancedProgress;
|
||||
@@ -222,60 +222,89 @@ Focus on actionable recommendations and use the registered tools.
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ALwrity glassomorphic styling for Copilot sidebar */
|
||||
/* ALwrity Compact Copilot Styling - 60% Smaller & More Efficient */
|
||||
.alwrity-copilot-sidebar {
|
||||
--alwrity-bg: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0.08));
|
||||
--alwrity-border: rgba(255,255,255,0.22);
|
||||
--alwrity-shadow: 0 18px 50px rgba(0,0,0,0.35);
|
||||
--alwrity-shadow: 0 8px 24px rgba(0,0,0,0.25); /* Reduced from 18px 50px */
|
||||
--alwrity-accent: #667eea;
|
||||
--alwrity-accent2: #764ba2;
|
||||
--alwrity-text: rgba(255,255,255,0.92);
|
||||
--alwrity-subtext: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar * {
|
||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, 'Helvetica Neue', Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
}
|
||||
|
||||
/* Compact sidebar container */
|
||||
.alwrity-copilot-sidebar .copilot-sidebar-container,
|
||||
.alwrity-copilot-sidebar .copilotkit-sidebar,
|
||||
.alwrity-copilot-sidebar .copilotkit-chat-container {
|
||||
background: var(--alwrity-bg) !important;
|
||||
backdrop-filter: blur(22px) !important;
|
||||
-webkit-backdrop-filter: blur(22px) !important;
|
||||
backdrop-filter: blur(16px) !important; /* Reduced from 22px */
|
||||
-webkit-backdrop-filter: blur(16px) !important;
|
||||
border: 1px solid var(--alwrity-border) !important;
|
||||
box-shadow: var(--alwrity-shadow) !important;
|
||||
color: var(--alwrity-text) !important;
|
||||
|
||||
/* Compact dimensions */
|
||||
width: 40% !important; /* Reduced from 100% */
|
||||
max-width: 320px !important;
|
||||
min-width: 280px !important;
|
||||
height: 85vh !important;
|
||||
max-height: 600px !important;
|
||||
|
||||
/* Compact spacing */
|
||||
padding: 8px !important;
|
||||
margin: 8px !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilotkit-title,
|
||||
.alwrity-copilot-sidebar .copilot-title {
|
||||
color: var(--alwrity-text) !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 0.2px !important;
|
||||
font-weight: 600 !important; /* Reduced from 700 */
|
||||
letter-spacing: 0.1px !important; /* Reduced from 0.2px */
|
||||
font-size: 14px !important; /* Reduced from 18px+ */
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilotkit-sidebar,
|
||||
.alwrity-copilot-sidebar .copilot-sidebar-container {
|
||||
z-index: 1200 !important;
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilotkit-subtitle,
|
||||
.alwrity-copilot-sidebar .copilot-subtitle,
|
||||
.alwrity-copilot-sidebar .copilotkit-initial-message {
|
||||
color: var(--alwrity-subtext) !important;
|
||||
font-size: 12px !important; /* Reduced from 14px+ */
|
||||
line-height: 1.3 !important; /* Reduced from 1.5 */
|
||||
margin: 4px 0 8px 0 !important;
|
||||
}
|
||||
/* Suggestions: border, glow, depth, enterprise look */
|
||||
|
||||
/* Compact Suggestions - 60% smaller */
|
||||
.alwrity-copilot-sidebar .copilot-suggestion,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestion,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions button,
|
||||
.alwrity-copilot-sidebar .copilot-suggestions button {
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0.08)) !important;
|
||||
border: 1.5px solid rgba(255,255,255,0.32) !important;
|
||||
border: 1px solid rgba(255,255,255,0.32) !important; /* Reduced from 1.5px */
|
||||
color: var(--alwrity-text) !important;
|
||||
box-shadow: 0 10px 28px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.2) !important;
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.2) !important; /* Reduced from 10px 28px */
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease; /* Reduced from 0.25s */
|
||||
border-radius: 8px !important; /* Reduced from 12px */
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
backdrop-filter: blur(12px); /* Reduced from 16px */
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
|
||||
/* Compact padding and margins */
|
||||
padding: 6px 10px !important; /* Reduced from 12px+ */
|
||||
margin: 3px !important; /* Reduced from 6px+ */
|
||||
font-size: 12px !important; /* Reduced from 14px+ */
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilot-suggestion::before,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestion::before,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions button::before,
|
||||
@@ -286,57 +315,69 @@ Focus on actionable recommendations and use the registered tools.
|
||||
left: -120%;
|
||||
width: 120%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.18), transparent);
|
||||
transition: left 0.6s ease;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent); /* Reduced from 0.18 */
|
||||
transition: left 0.4s ease; /* Reduced from 0.6s */
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilot-suggestion:hover::before,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestion:hover::before,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions button:hover::before,
|
||||
.alwrity-copilot-sidebar .copilot-suggestions button:hover::before {
|
||||
left: 120%;
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilot-suggestion:hover,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestion:hover,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions button:hover,
|
||||
.alwrity-copilot-sidebar .copilot-suggestions button:hover {
|
||||
transform: translateY(-4px) scale(1.015);
|
||||
box-shadow: 0 18px 44px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.18) inset !important;
|
||||
border-color: rgba(255,255,255,0.45) !important;
|
||||
transform: translateY(-2px) scale(1.01); /* Reduced from -4px 1.015 */
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.25), 0 0 0 1px rgba(255,255,255,0.15) inset !important; /* Reduced from 18px 44px */
|
||||
border-color: rgba(255,255,255,0.4) !important; /* Reduced from 0.45 */
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilot-suggestion:active,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestion:active,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions button:active,
|
||||
.alwrity-copilot-sidebar .copilot-suggestions button:active {
|
||||
transform: translateY(-1px) scale(0.995);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.3) !important;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.25) !important; /* Reduced from 8px 20px */
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilot-suggestion:focus-visible,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestion:focus-visible,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions button:focus-visible,
|
||||
.alwrity-copilot-sidebar .copilot-suggestions button:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(255,255,255,0.45), 0 0 0 4px rgba(102,126,234,0.35) !important;
|
||||
box-shadow: 0 0 0 2px rgba(255,255,255,0.35), 0 0 0 3px rgba(102,126,234,0.3) !important; /* Reduced from 4px */
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilot-suggestion .icon,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestion .icon,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions button .icon {
|
||||
filter: drop-shadow(0 2px 6px rgba(0,0,0,0.25));
|
||||
filter: drop-shadow(0 1px 3px rgba(0,0,0,0.2)); /* Reduced from 2px 6px */
|
||||
width: 14px !important; /* Reduced from 18px+ */
|
||||
height: 14px !important; /* Reduced from 18px+ */
|
||||
margin-right: 6px !important; /* Reduced from 8px+ */
|
||||
}
|
||||
|
||||
/* Compact suggestions grid */
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions,
|
||||
.alwrity-copilot-sidebar .copilot-suggestions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
gap: 6px; /* Reduced from 10px */
|
||||
margin: 8px 0; /* Reduced from 16px+ */
|
||||
}
|
||||
|
||||
@media (min-width: 420px) {
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions,
|
||||
.alwrity-copilot-sidebar .copilot-suggestions {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
gap: 8px; /* Reduced from 12px */
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduce motion for users who prefer it */
|
||||
/* Compact motion for users who prefer it */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.alwrity-copilot-sidebar .copilot-suggestion,
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestion,
|
||||
@@ -352,32 +393,92 @@ Focus on actionable recommendations and use the registered tools.
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar styling (webkit) */
|
||||
/* Compact scrollbar styling */
|
||||
.alwrity-copilot-sidebar ::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 6px; /* Reduced from 10px */
|
||||
height: 6px; /* Reduced from 10px */
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255,255,255,0.25);
|
||||
border: 2px solid rgba(0,0,0,0.1);
|
||||
border-radius: 10px;
|
||||
background: rgba(255,255,255,0.2); /* Reduced from 0.25 */
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 6px; /* Reduced from 10px */
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar ::-webkit-scrollbar-track {
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-radius: 10px;
|
||||
background: rgba(255,255,255,0.05); /* Reduced from 0.08 */
|
||||
border-radius: 6px; /* Reduced from 10px */
|
||||
}
|
||||
|
||||
/* Compact primary buttons */
|
||||
.alwrity-copilot-sidebar .copilot-primary,
|
||||
.alwrity-copilot-sidebar .copilotkit-primary-button,
|
||||
.alwrity-copilot-sidebar button[type="submit"] {
|
||||
background: linear-gradient(90deg, var(--alwrity-accent), var(--alwrity-accent2)) !important;
|
||||
border: 1px solid rgba(255,255,255,0.35) !important;
|
||||
border: 1px solid rgba(255,255,255,0.3) !important; /* Reduced from 0.35 */
|
||||
color: white !important;
|
||||
padding: 6px 12px !important; /* Reduced from 10px 20px */
|
||||
font-size: 12px !important; /* Reduced from 14px+ */
|
||||
border-radius: 6px !important; /* Reduced from 8px+ */
|
||||
}
|
||||
|
||||
/* Compact input styling */
|
||||
.alwrity-copilot-sidebar .copilot-input,
|
||||
.alwrity-copilot-sidebar .copilotkit-input {
|
||||
background: rgba(255,255,255,0.14) !important;
|
||||
background: rgba(255,255,255,0.12) !important; /* Reduced from 0.14 */
|
||||
color: var(--alwrity-text) !important;
|
||||
border: 1px solid rgba(255,255,255,0.22) !important;
|
||||
border: 1px solid rgba(255,255,255,0.2) !important; /* Reduced from 0.22 */
|
||||
padding: 8px 12px !important; /* Reduced from 14px 18px */
|
||||
font-size: 13px !important; /* Reduced from 14px+ */
|
||||
border-radius: 6px !important; /* Reduced from 8px+ */
|
||||
}
|
||||
|
||||
/* Compact chat messages container */
|
||||
.alwrity-copilot-sidebar .copilotkit-messages,
|
||||
.alwrity-copilot-sidebar .copilot-messages,
|
||||
.alwrity-copilot-sidebar .chat-messages {
|
||||
padding: 8px !important; /* Reduced from 16px+ */
|
||||
margin: 0 !important;
|
||||
max-height: 70vh !important; /* Ensure chat takes most space */
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* Compact chat input area */
|
||||
.alwrity-copilot-sidebar .copilotkit-input-container,
|
||||
.alwrity-copilot-sidebar .copilot-input-container {
|
||||
padding: 8px !important; /* Reduced from 16px+ */
|
||||
margin: 8px 0 !important; /* Reduced from 16px+ */
|
||||
border-top: 1px solid rgba(255,255,255,0.1) !important;
|
||||
}
|
||||
|
||||
/* Compact close button */
|
||||
.alwrity-copilot-sidebar .copilotkit-close,
|
||||
.alwrity-copilot-sidebar .copilot-close,
|
||||
.alwrity-copilot-sidebar button[aria-label*="close"],
|
||||
.alwrity-copilot-sidebar button[aria-label*="Close"] {
|
||||
width: 24px !important; /* Reduced from 32px+ */
|
||||
height: 24px !important; /* Reduced from 32px+ */
|
||||
border-radius: 50% !important;
|
||||
padding: 0 !important;
|
||||
font-size: 12px !important; /* Reduced from 16px+ */
|
||||
}
|
||||
|
||||
/* Compact responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.alwrity-copilot-sidebar .copilot-sidebar-container,
|
||||
.alwrity-copilot-sidebar .copilotkit-sidebar,
|
||||
.alwrity-copilot-sidebar .copilotkit-chat-container {
|
||||
width: 90% !important; /* Mobile: take more width */
|
||||
max-width: none !important;
|
||||
min-width: 280px !important;
|
||||
height: 80vh !important;
|
||||
}
|
||||
|
||||
.alwrity-copilot-sidebar .copilotkit-suggestions,
|
||||
.alwrity-copilot-sidebar .copilot-suggestions {
|
||||
grid-template-columns: 1fr !important; /* Single column on mobile */
|
||||
gap: 4px !important; /* Even more compact on mobile */
|
||||
}
|
||||
}
|
||||
|
||||
.seo-copilotkit-loading {
|
||||
|
||||
@@ -155,6 +155,7 @@ export interface ContentQualityMetrics {
|
||||
content_length: number;
|
||||
word_count: number;
|
||||
analysis_timestamp: string;
|
||||
recommendations?: string[];
|
||||
}
|
||||
|
||||
export interface ArticleContent {
|
||||
|
||||
Reference in New Issue
Block a user