ALwrity Prompts - AI Integration Plan

This commit is contained in:
ajaysi
2025-09-03 23:16:39 +05:30
parent 5efee4235d
commit c19fc3f225
104 changed files with 9392 additions and 17462 deletions

View File

@@ -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) */}

View File

@@ -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',

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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

View File

@@ -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';

View File

@@ -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
};
}

View File

@@ -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
};
};

View File

@@ -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;

View File

@@ -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 {

View File

@@ -155,6 +155,7 @@ export interface ContentQualityMetrics {
content_length: number;
word_count: number;
analysis_timestamp: string;
recommendations?: string[];
}
export interface ArticleContent {