ALwrity LinkedIn Writer: Brainstorm Flow, Copilot Actions, Feature Carousel, Info Modals, Welcome Message

This commit is contained in:
ajaysi
2025-09-10 13:58:56 +05:30
parent 489a60e4a2
commit da091f7c47
26 changed files with 5029 additions and 1893 deletions

View File

@@ -6,7 +6,7 @@ import './styles/alwrity-copilot.css';
import RegisterLinkedInActions from './RegisterLinkedInActions';
import RegisterLinkedInEditActions from './RegisterLinkedInEditActions';
import RegisterLinkedInActionsEnhanced from './RegisterLinkedInActionsEnhanced';
import { Header, ContentEditor, LoadingIndicator, WelcomeMessage, ProgressTracker } from './components';
import { Header, ContentEditor, LoadingIndicator, WelcomeMessage, ProgressTracker, CopilotActions } from './components';
import { useLinkedInWriter } from './hooks/useLinkedInWriter';
import { useCopilotPersistence } from './utils/enhancedPersistence';
import { PlatformPersonaProvider, usePlatformPersonaContext } from '../shared/PersonaContext/PlatformPersonaProvider';
@@ -83,11 +83,9 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
// Get persona context for enhanced AI assistance
const { corePersona, platformPersona, loading: personaLoading } = usePlatformPersonaContext();
// Get enhanced persistence functionality
const {
persistenceManager,
copilotContext,
saveChatHistory,
loadChatHistory,
addChatMessage,
@@ -227,171 +225,17 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
}
});
// Allow Copilot to edit the draft with specific operations
useCopilotActionTyped({
name: 'editLinkedInDraft',
description: 'Apply a quick style or structural edit to the current LinkedIn draft',
parameters: [
{ name: 'operation', type: 'string', description: 'The edit operation to perform', required: true, enum: ['Casual', 'Professional', 'TightenHook', 'AddCTA', 'Shorten', 'Lengthen'] }
],
handler: async ({ operation }: { operation: string }) => {
const currentDraft = draft || '';
if (!currentDraft) {
return { success: false, message: 'No draft content to edit' };
}
let editedContent = currentDraft;
switch (operation) {
case 'Casual':
editedContent = currentDraft.replace(/\b(utilize|implement|facilitate|leverage)\b/gi, (match) => {
const casual = { utilize: 'use', implement: 'put in place', facilitate: 'help', leverage: 'use' };
return casual[match.toLowerCase() as keyof typeof casual] || match;
});
editedContent = editedContent.replace(/\./g, '! 😊');
break;
case 'Professional':
editedContent = currentDraft.replace(/\b(use|put in place|help)\b/gi, (match) => {
const professional = { use: 'utilize', 'put in place': 'implement', help: 'facilitate' };
return professional[match.toLowerCase() as keyof typeof professional] || match;
});
editedContent = editedContent.replace(/! 😊/g, '.');
break;
case 'TightenHook':
const lines = currentDraft.split('\n');
if (lines.length > 0) {
const firstLine = lines[0];
const tightened = firstLine.length > 100 ? firstLine.substring(0, 100) + '...' : firstLine;
lines[0] = tightened;
editedContent = lines.join('\n');
}
break;
case 'AddCTA':
if (!/\b(call now|sign up|join|try|learn more|cta|comment|share|connect|message|dm|reach out)\b/i.test(currentDraft)) {
editedContent = currentDraft + '\n\nWhat are your thoughts on this? Share your experience in the comments below!';
}
break;
case 'Shorten':
if (currentDraft.length > 200) {
editedContent = currentDraft.substring(0, 200) + '...';
}
break;
case 'Lengthen':
if (currentDraft.length < 500) {
editedContent = currentDraft + '\n\nThis approach has shown remarkable results in our industry. The key is to maintain consistency while adapting to changing market conditions.';
}
break;
default:
return { success: false, message: 'Unknown operation' };
}
// Use the edit action to show the diff preview
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', {
detail: { target: editedContent }
}));
return { success: true, message: `Draft ${operation.toLowerCase()} applied`, content: editedContent };
}
// Initialize CopilotActions component to handle all copilot-related functionality
const getIntelligentSuggestions = CopilotActions({
draft,
context,
userPreferences,
justGeneratedContent,
handleContextChange,
setDraft
});
// Intelligent, stage-aware suggestions (memoized to prevent infinite re-rendering)
const getIntelligentSuggestions = useMemo(() => {
const hasContent = draft && draft.trim().length > 0;
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
if (DEBUG_LINKEDIN) console.log('[LinkedIn Writer] Generating suggestions:', {
hasContent,
justGeneratedContent,
draftLength: draft?.length || 0
});
if (!hasContent) {
// Initial suggestions for content creation
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: '🖼️ 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
const refinementSuggestions = [
{ title: '🙂 Make it casual', message: 'Use tool editLinkedInDraft with operation Casual' },
{ title: '💼 Make it professional', message: 'Use tool editLinkedInDraft with operation Professional' },
{ title: '✨ Tighten hook', message: 'Use tool editLinkedInDraft with operation TightenHook' },
{ title: '📣 Add a CTA', message: 'Use tool editLinkedInDraft with operation AddCTA' },
{ 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) {
refinementSuggestions.push({ title: '📣 Add CTA', message: 'Use tool editLinkedInDraft with operation AddCTA' });
}
if (!hasHashtags) {
refinementSuggestions.push({ title: '🏷️ Add hashtags', message: 'Use tool addLinkedInHashtags' });
}
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) {
if (DEBUG_LINKEDIN) 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'
});
}
}
if (DEBUG_LINKEDIN) console.log('[LinkedIn Writer] Final suggestions:', refinementSuggestions);
return refinementSuggestions;
}
}, [draft, justGeneratedContent]);
return (
<div className={`linkedin-writer ${className}`} style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
@@ -418,6 +262,7 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
</div>
{/* Debug: Enhanced Persistence Test Buttons (remove in production) */}
@@ -470,6 +315,7 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
{/* Enhanced Persona-Aware Actions */}
<RegisterLinkedInActionsEnhanced />
{/* CopilotKit Sidebar */}
<CopilotSidebar
className="alwrity-copilot-sidebar linkedin-writer"

View File

@@ -0,0 +1,573 @@
import React, { useCallback, useMemo, useState } from 'react';
import { usePlatformPersonaContext } from '../../shared/PersonaContext/PlatformPersonaProvider';
// Define the cache data type
interface BrainstormCacheData {
ideas: { prompt: string; rationale?: string }[];
searchResults: any[];
timestamp: number;
}
// Type guard function
const isBrainstormCacheData = (data: any): data is BrainstormCacheData => {
return data &&
Array.isArray(data.ideas) &&
Array.isArray(data.searchResults) &&
typeof data.timestamp === 'number';
};
interface BrainstormFlowProps {
brainstormVisible: boolean;
setBrainstormVisible: React.Dispatch<React.SetStateAction<boolean>>;
brainstormStage: 'loading' | 'select' | 'results';
setBrainstormStage: React.Dispatch<React.SetStateAction<'loading' | 'select' | 'results'>>;
loaderMessageIndex: number;
setLoaderMessageIndex: React.Dispatch<React.SetStateAction<number>>;
aiSearchPrompts: string[];
setAiSearchPrompts: React.Dispatch<React.SetStateAction<string[]>>;
selectedPrompt: string;
setSelectedPrompt: React.Dispatch<React.SetStateAction<string>>;
searchResults: any[];
setSearchResults: React.Dispatch<React.SetStateAction<any[]>>;
ideas: { prompt: string; rationale?: string }[];
setIdeas: React.Dispatch<React.SetStateAction<{ prompt: string; rationale?: string }[]>>;
isUsingCache: boolean;
setIsUsingCache: React.Dispatch<React.SetStateAction<boolean>>;
}
const BrainstormFlow: React.FC<BrainstormFlowProps> = ({
brainstormVisible,
setBrainstormVisible,
brainstormStage,
setBrainstormStage,
loaderMessageIndex,
setLoaderMessageIndex,
aiSearchPrompts,
setAiSearchPrompts,
selectedPrompt,
setSelectedPrompt,
searchResults,
setSearchResults,
ideas,
setIdeas,
isUsingCache,
setIsUsingCache
}) => {
const { corePersona, platformPersona } = usePlatformPersonaContext();
const loaderMessages = useMemo(() => ([
'Searching the web for the most recent and relevant coverage...',
'Extracting entities and context from top sources...',
'Aligning findings with your persona and audience...',
'Formulating high-signal brainstorm prompts you can use right away...'
]), []);
// Cache management utilities
const getCacheKey = useCallback((seed: string, personaId?: string, platformPersonaId?: string) => {
return `brainstorm_ideas_${seed}_${personaId || 'default'}_${platformPersonaId || 'default'}`;
}, []);
const getCachedIdeas = useCallback((cacheKey: string): BrainstormCacheData | null => {
try {
const cached = sessionStorage.getItem(cacheKey);
if (cached) {
const data = JSON.parse(cached);
if (isBrainstormCacheData(data)) {
// Check if cache is less than 1 hour old
if (Date.now() - data.timestamp < 3600000) {
return data;
} else {
sessionStorage.removeItem(cacheKey);
}
}
}
} catch (e) {
console.warn('Failed to read brainstorm cache:', e);
}
return null;
}, []);
const setCachedIdeas = useCallback((cacheKey: string, ideas: any[], searchResults: any[]) => {
try {
const cacheData = {
ideas,
searchResults,
timestamp: Date.now()
};
sessionStorage.setItem(cacheKey, JSON.stringify(cacheData));
} catch (e) {
console.warn('Failed to cache brainstorm ideas:', e);
}
}, []);
const clearCache = useCallback(() => {
try {
const keys = Object.keys(sessionStorage);
keys.forEach(key => {
if (key.startsWith('brainstorm_ideas_')) {
sessionStorage.removeItem(key);
}
});
} catch (e) {
console.warn('Failed to clear brainstorm cache:', e);
}
}, []);
React.useEffect(() => {
const handler = async (ev: any) => {
try {
// Store the event for refresh functionality
(window as any).lastBrainstormEvent = ev;
const { prompt, seed: ideaSeed, forceRefresh = false } = ev.detail || {};
const finalSeed = ideaSeed || prompt;
setBrainstormVisible(true);
setBrainstormStage('loading');
setLoaderMessageIndex(0);
// Special case: show most recent cached ideas when seed is 'cached'
if (finalSeed === 'cached') {
try {
const keys = Object.keys(sessionStorage);
let mostRecentCache: BrainstormCacheData | null = null;
let mostRecentKey = '';
let mostRecentTimestamp = 0;
for (const key of keys) {
if (key.startsWith('brainstorm_ideas_')) {
const cached = sessionStorage.getItem(key);
if (cached) {
const data = JSON.parse(cached);
if (isBrainstormCacheData(data) && data.timestamp > mostRecentTimestamp && data.ideas.length > 0) {
mostRecentTimestamp = data.timestamp;
mostRecentCache = data;
mostRecentKey = key;
}
}
}
}
if (mostRecentCache !== null) {
console.log('Showing most recent cached brainstorm ideas from:', mostRecentKey);
setIdeas(mostRecentCache.ideas);
setAiSearchPrompts(mostRecentCache.ideas.map((x) => x.prompt));
setSelectedPrompt(mostRecentCache.ideas[0]?.prompt || '');
setSearchResults(mostRecentCache.searchResults || []);
setIsUsingCache(true);
setBrainstormStage('select');
return;
} else {
// No cached ideas found, close modal
setBrainstormVisible(false);
return;
}
} catch (e) {
console.warn('Failed to load cached ideas:', e);
setBrainstormVisible(false);
return;
}
}
// Check cache first (unless force refresh)
const personaId = corePersona?.id?.toString();
const platformPersonaId = platformPersona?.id?.toString();
const cacheKey = getCacheKey(finalSeed, personaId, platformPersonaId);
if (!forceRefresh) {
const cached = getCachedIdeas(cacheKey);
if (cached) {
console.log('Using cached brainstorm ideas for:', finalSeed);
setIdeas(cached.ideas);
setAiSearchPrompts(cached.ideas.map((x) => x.prompt));
setSelectedPrompt(cached.ideas[0]?.prompt || '');
setSearchResults(cached.searchResults || []);
setIsUsingCache(true);
setBrainstormStage('select');
return;
}
}
setIsUsingCache(false);
// Gentle loader progression
let step = 0;
const interval = setInterval(() => {
step += 1;
setLoaderMessageIndex((idx: number) => Math.min(idx + 1, loaderMessages.length - 1));
if (step >= loaderMessages.length - 1) clearInterval(interval);
}, 700);
// First: run grounded search for the seed prompt
let results: any[] = [];
try {
const sr = await fetch('/api/brainstorm/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: finalSeed })
});
if (sr.ok) {
const data = await sr.json();
results = data?.results || [];
}
} catch {}
setSearchResults(results);
// Then: request persona-aware brainstorm ideas using the search results
try {
const ir = await fetch('/api/brainstorm/ideas', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
seed: finalSeed,
persona: corePersona || null,
platformPersona: platformPersona || null,
results,
count: 5
})
});
if (ir.ok) {
const data = await ir.json();
const list = Array.isArray(data?.ideas) ? data.ideas : [];
setIdeas(list);
setAiSearchPrompts(list.map((x: any) => x.prompt));
setSelectedPrompt(list[0]?.prompt || '');
// Cache the results
setCachedIdeas(cacheKey, list, results);
console.log('Cached brainstorm ideas for:', finalSeed);
} else {
setIdeas([]);
}
} catch {
setIdeas([]);
}
setBrainstormStage('select');
} catch (e) {
console.error('Brainstorm flow error:', e);
setBrainstormVisible(false);
}
};
window.addEventListener('linkedinwriter:runGoogleSearchForIdeas' as any, handler);
return () => window.removeEventListener('linkedinwriter:runGoogleSearchForIdeas' as any, handler);
}, [corePersona, platformPersona, loaderMessages, getCacheKey, getCachedIdeas, setCachedIdeas, setBrainstormVisible, setBrainstormStage, setLoaderMessageIndex, setIdeas, setAiSearchPrompts, setSelectedPrompt, setSearchResults, setIsUsingCache]);
return (
<>
{/* Brainstorm Flow UI */}
{brainstormVisible && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10010, padding: 20 }}>
<div style={{
background: 'white',
width: 800,
maxWidth: '100%',
height: '90vh',
borderRadius: 16,
boxShadow: '0 20px 60px rgba(0,0,0,0.25)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}}>
{/* Fixed Header */}
<div style={{
padding: 16,
background: '#0a66c2',
color: 'white',
fontWeight: 800,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexShrink: 0
}}>
<div>Brainstorm: Google Search Prompts</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<button
onClick={() => {
// Force refresh by clearing cache and re-running
const { prompt, seed: ideaSeed } = (window as any).lastBrainstormEvent?.detail || {};
if (prompt || ideaSeed) {
window.dispatchEvent(new CustomEvent('linkedinwriter:runGoogleSearchForIdeas', {
detail: { prompt, seed: ideaSeed, forceRefresh: true }
}));
}
}}
style={{
background: 'rgba(255,255,255,0.2)',
border: 'none',
color: 'white',
borderRadius: 6,
padding: '4px 8px',
cursor: 'pointer',
fontSize: 12,
fontWeight: 600
}}
title="Refresh ideas (bypass cache)"
>
🔄
</button>
<button
onClick={() => {
clearCache();
console.log('Brainstorm cache cleared');
}}
style={{
background: 'rgba(255,255,255,0.2)',
border: 'none',
color: 'white',
borderRadius: 6,
padding: '4px 8px',
cursor: 'pointer',
fontSize: 12,
fontWeight: 600
}}
title="Clear all cached brainstorm ideas"
>
🗑
</button>
<button
onClick={() => {
setBrainstormVisible(false);
setBrainstormStage('loading');
setLoaderMessageIndex(0);
setAiSearchPrompts([]);
setSelectedPrompt('');
setSearchResults([]);
setIdeas([]);
}}
style={{ background: 'rgba(255,255,255,0.2)', border: 'none', color: 'white', borderRadius: 8, padding: '6px 10px', cursor: 'pointer' }}
>
</button>
</div>
</div>
{/* Scrollable Content */}
<div style={{ flex: 1, overflow: 'auto' }}>
{brainstormStage === 'loading' && (
<div style={{ padding: 24 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: 12, alignItems: 'center' }}>
<div style={{ width: 16, height: 16, borderRadius: '50%', border: '2px solid #0a66c2', borderTopColor: 'transparent', animation: 'spin 0.8s linear infinite' }} />
<div>
<div style={{ fontWeight: 800, color: '#111827' }}>Preparing Google search prompts</div>
<div style={{ marginTop: 6, color: '#374151', fontSize: 14 }}>{loaderMessages[loaderMessageIndex]}</div>
</div>
</div>
<ul style={{ margin: '12px 0 0 28px', color: '#6b7280', fontSize: 12, lineHeight: 1.6 }}>
<li>1/4 Persona-aware analysis</li>
<li>2/4 Seed expansion and entities</li>
<li>3/4 Grounding and timeliness checks</li>
<li>4/4 Output assembly</li>
</ul>
<style>{'@keyframes spin{to{transform:rotate(360deg)}}'}</style>
</div>
)}
{brainstormStage === 'select' && (
<div style={{ padding: 20 }}>
<div style={{ marginBottom: 16, fontWeight: 700, color: '#1f2937', display: 'flex', alignItems: 'center', gap: 8 }}>
Select one prompt to run with Google Search
{isUsingCache && (
<span style={{
fontSize: 12,
color: '#059669',
background: '#d1fae5',
padding: '2px 8px',
borderRadius: 12,
fontWeight: 500
}}>
📦 Cached
</span>
)}
</div>
<div style={{ display: 'grid', gap: 12, marginBottom: 20 }}>
{aiSearchPrompts.map((p, i) => {
const rationale = ideas[i]?.rationale;
return (
<label key={i} style={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
gap: 12,
alignItems: 'flex-start',
border: '1px solid #e5e7eb',
borderRadius: 10,
padding: '12px 16px',
cursor: 'pointer',
transition: 'border-color 0.2s'
}}>
<input type="radio" name="aiPrompt" checked={selectedPrompt === p} onChange={() => setSelectedPrompt(p)} style={{ marginTop: 3 }} />
<div>
<div style={{ fontSize: 14, color: '#111827', fontWeight: 600, lineHeight: 1.4 }}>{p}</div>
{rationale && <div style={{ marginTop: 6, color: '#6b7280', fontSize: 12, lineHeight: 1.3 }}>{rationale}</div>}
</div>
</label>
);
})}
</div>
</div>
)}
{brainstormStage === 'results' && (
<div style={{ padding: 20 }}>
<div style={{ marginBottom: 16, fontWeight: 700, color: '#1f2937' }}>Search Results</div>
{searchResults.length === 0 ? (
<div style={{ color: '#6b7280' }}>No results or search unavailable. Try another prompt.</div>
) : (
<div style={{ display: 'grid', gap: 12, marginBottom: 20 }}>
{searchResults.map((r: any, idx: number) => (
<div key={idx} style={{ border: '1px solid #e5e7eb', borderRadius: 10, padding: '12px 16px' }}>
<div style={{ fontWeight: 700, color: '#111827', marginBottom: 4 }}>{r.title || r.name || 'Result'}</div>
<div style={{ color: '#374151', fontSize: 13, lineHeight: 1.4 }}>{r.snippet || r.description || r.content || ''}</div>
{r.url && (<div style={{ marginTop: 6, fontSize: 12, color: '#2563eb' }}><a href={r.url} target="_blank" rel="noreferrer">{r.url}</a></div>)}
</div>
))}
</div>
)}
</div>
)}
</div>
{/* Fixed Footer */}
{brainstormStage !== 'loading' && (
<div style={{
padding: '16px 20px',
borderTop: '1px solid #e5e7eb',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
flexShrink: 0,
backgroundColor: '#f9fafb'
}}>
{brainstormStage === 'select' && (
<>
<button
onClick={() => {
// Send prompt to copilot chat input to generate a post from this prompt
window.dispatchEvent(new CustomEvent('linkedinwriter:copilotSeedFromPrompt', { detail: { prompt: selectedPrompt } }));
setBrainstormVisible(false);
}}
disabled={!selectedPrompt}
style={{
padding: '10px 20px',
borderRadius: 8,
background: selectedPrompt ? '#111827' : '#9ca3af',
color: 'white',
border: 'none',
cursor: selectedPrompt ? 'pointer' : 'not-allowed',
fontWeight: 600
}}
>
Generate post from this prompt
</button>
<button
onClick={() => setBrainstormVisible(false)}
style={{
padding: '10px 20px',
borderRadius: 8,
background: 'white',
border: '1px solid #e5e7eb',
cursor: 'pointer',
fontWeight: 600
}}
>
Cancel
</button>
<button
onClick={async () => {
// Use existing Google grounding flow via backend LinkedInService
try {
const resp = await fetch('/api/brainstorm/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: selectedPrompt })
});
if (resp.ok) {
const data = await resp.json();
setSearchResults(data?.results || []);
setBrainstormStage('results');
} else {
setSearchResults([]);
setBrainstormStage('results');
}
} catch {
setSearchResults([]);
setBrainstormStage('results');
}
}}
disabled={!selectedPrompt}
style={{
padding: '10px 20px',
borderRadius: 8,
background: selectedPrompt ? '#0a66c2' : '#c7d2fe',
color: 'white',
border: 'none',
cursor: selectedPrompt ? 'pointer' : 'not-allowed',
fontWeight: 600
}}
>
Run Google Search
</button>
</>
)}
{brainstormStage === 'results' && (
<>
<button
onClick={() => setBrainstormStage('select')}
style={{
padding: '10px 20px',
borderRadius: 8,
background: '#6b7280',
color: 'white',
border: 'none',
cursor: 'pointer',
fontWeight: 600
}}
>
Back to Prompts
</button>
<button
onClick={() => {
// Seed Copilot chat to generate a post
window.dispatchEvent(new CustomEvent('linkedinwriter:copilotSeedFromPrompt', { detail: { prompt: selectedPrompt } }));
setBrainstormVisible(false);
}}
style={{
padding: '10px 20px',
borderRadius: 8,
background: '#111827',
color: 'white',
border: 'none',
cursor: 'pointer',
fontWeight: 600
}}
>
Generate post from this prompt
</button>
<button
onClick={() => setBrainstormVisible(false)}
style={{
padding: '10px 20px',
borderRadius: 8,
background: '#0a66c2',
color: 'white',
border: 'none',
cursor: 'pointer',
fontWeight: 600
}}
>
Done
</button>
</>
)}
</div>
)}
</div>
</div>
)}
</>
);
};
export default BrainstormFlow;

View File

@@ -0,0 +1,432 @@
import React, { useMemo } from 'react';
import { useCopilotAction, useCopilotContext } from '@copilotkit/react-core';
import { usePlatformPersonaContext } from '../../shared/PersonaContext/PlatformPersonaProvider';
const useCopilotActionTyped = useCopilotAction as any;
// Optional debug flag: set to true to enable verbose logs locally
const DEBUG_LINKEDIN = false;
interface CopilotActionsProps {
draft: string;
context: string;
userPreferences: any;
justGeneratedContent: boolean;
handleContextChange: (value: string) => void;
setDraft: (draft: string) => void;
}
// Note: This is implemented as a hook-like utility, not a rendered component.
// It returns the getIntelligentSuggestions function for use by the caller.
const CopilotActions = ({
draft,
context,
userPreferences,
justGeneratedContent,
handleContextChange,
setDraft
}: CopilotActionsProps) => {
const { corePersona, platformPersona } = usePlatformPersonaContext();
const copilotContext = useCopilotContext();
// Listen for copilot seed events to open sidebar with prompt
React.useEffect(() => {
const handler = (ev: any) => {
try {
const { prompt } = ev.detail || {};
if (!prompt) return;
// First, open the copilot sidebar
const copilotButton = document.querySelector('.copilotkit-open-button') ||
document.querySelector('[data-copilot-open]') ||
document.querySelector('button[aria-label*="Open"]') ||
document.querySelector('.alwrity-copilot-sidebar button') ||
document.querySelector('[data-testid="copilot-open-button"]');
if (copilotButton) {
(copilotButton as HTMLElement).click();
// Try context-based approach first (if available)
if (copilotContext && typeof copilotContext === 'object') {
try {
// Check if context has any message sending capabilities
if ('sendMessage' in copilotContext && typeof copilotContext.sendMessage === 'function') {
setTimeout(() => {
(copilotContext as any).sendMessage(prompt);
console.log('Message sent via context');
return;
}, 500);
}
} catch (e) {
console.log('Context-based approach failed, falling back to DOM manipulation');
}
}
// Alternative: Try to trigger the generateFromPrompt action directly
setTimeout(() => {
// Try to find and trigger the generateFromPrompt action
const actionButton = document.querySelector('[data-action="generateFromPrompt"]') ||
document.querySelector('button[title*="generateFromPrompt"]');
if (actionButton) {
// Set the prompt in a temporary storage for the action to pick up
(window as any).tempPromptForGeneration = prompt;
(actionButton as HTMLElement).click();
console.log('Triggered generateFromPrompt action with:', prompt);
return;
}
}, 200);
// Fallback: Wait a bit for the sidebar to open, then set the input value
setTimeout(() => {
// Try multiple selectors for the chat input
const chatInput = document.querySelector('.copilotkit-chat-input') ||
document.querySelector('textarea[placeholder*="message"]') ||
document.querySelector('input[placeholder*="message"]') ||
document.querySelector('.copilot-chat-input') ||
document.querySelector('[data-testid="chat-input"]') ||
document.querySelector('textarea[data-testid="chat-input"]') ||
document.querySelector('.copilotkit-chat-input textarea') ||
document.querySelector('.copilotkit-chat-input input') ||
document.querySelector('textarea[data-copilot-input]') ||
document.querySelector('input[data-copilot-input]');
if (chatInput) {
const inputElement = chatInput as HTMLInputElement | HTMLTextAreaElement;
// Check if input is disabled or read-only
if (inputElement.disabled || inputElement.readOnly) {
console.warn('Input is disabled or read-only, attempting to enable it');
inputElement.disabled = false;
inputElement.readOnly = false;
inputElement.removeAttribute('disabled');
inputElement.removeAttribute('readonly');
}
// Clear any existing value first
inputElement.value = '';
// Set the new value
inputElement.value = prompt;
// Focus the input
inputElement.focus();
// Trigger multiple events to ensure React state updates
const inputEvent = new Event('input', { bubbles: true, cancelable: true });
const changeEvent = new Event('change', { bubbles: true, cancelable: true });
const keyupEvent = new Event('keyup', { bubbles: true, cancelable: true });
// Set the target property for React synthetic events
Object.defineProperty(inputEvent, 'target', { value: inputElement, enumerable: true });
Object.defineProperty(changeEvent, 'target', { value: inputElement, enumerable: true });
Object.defineProperty(keyupEvent, 'target', { value: inputElement, enumerable: true });
// Dispatch events in sequence
inputElement.dispatchEvent(inputEvent);
inputElement.dispatchEvent(changeEvent);
inputElement.dispatchEvent(keyupEvent);
// Try to trigger a React synthetic event with more properties
const syntheticEvent = new Event('input', { bubbles: true, cancelable: true });
Object.defineProperty(syntheticEvent, 'target', { value: inputElement, enumerable: true });
Object.defineProperty(syntheticEvent, 'currentTarget', { value: inputElement, enumerable: true });
Object.defineProperty(syntheticEvent, 'nativeEvent', { value: syntheticEvent, enumerable: true });
inputElement.dispatchEvent(syntheticEvent);
// Also try to trigger a focus event to ensure the input is active
const focusEvent = new Event('focus', { bubbles: true, cancelable: true });
inputElement.dispatchEvent(focusEvent);
// Try to find and enable the send button if it exists
setTimeout(() => {
const sendButton = document.querySelector('button[type="submit"]') ||
document.querySelector('button[data-copilot-send]') ||
document.querySelector('.copilotkit-send-button') ||
document.querySelector('button[aria-label*="Send"]') ||
document.querySelector('button[title*="Send"]');
if (sendButton) {
// Remove disabled attribute if it exists
(sendButton as HTMLButtonElement).disabled = false;
(sendButton as HTMLButtonElement).removeAttribute('disabled');
console.log('Send button enabled');
// Try to automatically send the message after a short delay
setTimeout(() => {
if (!(sendButton as HTMLButtonElement).disabled) {
(sendButton as HTMLButtonElement).click();
console.log('Message sent automatically');
}
}, 500);
// Alternative: Try to simulate Enter key press
setTimeout(() => {
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
});
inputElement.dispatchEvent(enterEvent);
const enterUpEvent = new KeyboardEvent('keyup', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
});
inputElement.dispatchEvent(enterUpEvent);
console.log('Enter key simulated');
}, 600);
}
}, 100);
console.log('Copilot sidebar opened with prompt:', prompt);
console.log('Input element details:', {
value: inputElement.value,
disabled: inputElement.disabled,
readOnly: inputElement.readOnly,
className: inputElement.className,
id: inputElement.id
});
} else {
console.warn('Could not find copilot chat input to prefill. Available elements:',
Array.from(document.querySelectorAll('textarea, input')).map(el => ({
tag: el.tagName,
className: el.className,
placeholder: el.getAttribute('placeholder'),
id: el.id,
'data-copilot-input': el.getAttribute('data-copilot-input')
}))
);
}
}, 1000); // Increased timeout to ensure sidebar is fully loaded
} else {
console.warn('Could not find copilot sidebar button to open. Available buttons:',
Array.from(document.querySelectorAll('button')).map(btn => ({
className: btn.className,
text: btn.textContent?.trim(),
'aria-label': btn.getAttribute('aria-label')
}))
);
}
} catch (e) {
console.error('Error handling copilot seed event:', e);
}
};
window.addEventListener('linkedinwriter:copilotSeedFromPrompt' as any, handler);
return () => window.removeEventListener('linkedinwriter:copilotSeedFromPrompt' as any, handler);
}, []);
// Allow external prompts to trigger content generation
useCopilotActionTyped({
name: 'generateFromPrompt',
description: 'Generate LinkedIn content from a specific prompt or idea',
parameters: [
{ name: 'prompt', type: 'string', description: 'The prompt or idea to generate content from', required: true }
],
handler: async ({ prompt }: { prompt: string }) => {
// Check for temporary prompt from brainstorm flow
const finalPrompt = prompt || (window as any).tempPromptForGeneration;
if (!finalPrompt) {
return { success: false, message: 'No prompt provided' };
}
// Clear the temporary prompt
if ((window as any).tempPromptForGeneration) {
delete (window as any).tempPromptForGeneration;
}
// Set the prompt as context and trigger generation
handleContextChange(finalPrompt);
// Use the existing LinkedIn post generation action
try {
// This will trigger the existing generateLinkedInPost action
return {
success: true,
message: `Generating LinkedIn content from prompt: "${finalPrompt}"`,
prompt: finalPrompt
};
} catch (error) {
return { success: false, message: 'Failed to generate content from prompt' };
}
}
});
// Allow Copilot to edit the draft with specific operations
useCopilotActionTyped({
name: 'editLinkedInDraft',
description: 'Apply a quick style or structural edit to the current LinkedIn draft',
parameters: [
{ name: 'operation', type: 'string', description: 'The edit operation to perform', required: true, enum: ['Casual', 'Professional', 'TightenHook', 'AddCTA', 'Shorten', 'Lengthen'] }
],
handler: async ({ operation }: { operation: string }) => {
const currentDraft = draft || '';
if (!currentDraft) {
return { success: false, message: 'No draft content to edit' };
}
let editedContent = currentDraft;
switch (operation) {
case 'Casual':
editedContent = currentDraft.replace(/\b(utilize|implement|facilitate|leverage)\b/gi, (match) => {
const casual = { utilize: 'use', implement: 'put in place', facilitate: 'help', leverage: 'use' };
return casual[match.toLowerCase() as keyof typeof casual] || match;
});
editedContent = editedContent.replace(/\./g, '! 😊');
break;
case 'Professional':
editedContent = currentDraft.replace(/\b(use|put in place|help)\b/gi, (match) => {
const professional = { use: 'utilize', 'put in place': 'implement', help: 'facilitate' };
return professional[match.toLowerCase() as keyof typeof professional] || match;
});
editedContent = editedContent.replace(/! 😊/g, '.');
break;
case 'TightenHook':
const lines = currentDraft.split('\n');
if (lines.length > 0) {
const firstLine = lines[0];
const tightened = firstLine.length > 100 ? firstLine.substring(0, 100) + '...' : firstLine;
lines[0] = tightened;
editedContent = lines.join('\n');
}
break;
case 'AddCTA':
if (!/\b(call now|sign up|join|try|learn more|cta|comment|share|connect|message|dm|reach out)\b/i.test(currentDraft)) {
editedContent = currentDraft + '\n\nWhat are your thoughts on this? Share your experience in the comments below!';
}
break;
case 'Shorten':
if (currentDraft.length > 200) {
editedContent = currentDraft.substring(0, 200) + '...';
}
break;
case 'Lengthen':
if (currentDraft.length < 500) {
editedContent = currentDraft + '\n\nThis approach has shown remarkable results in our industry. The key is to maintain consistency while adapting to changing market conditions.';
}
break;
default:
return { success: false, message: 'Unknown operation' };
}
// Use the edit action to show the diff preview
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', {
detail: { target: editedContent }
}));
return { success: true, message: `Draft ${operation.toLowerCase()} applied`, content: editedContent };
}
});
// Intelligent, stage-aware suggestions (memoized to prevent infinite re-rendering)
const getIntelligentSuggestions = useMemo(() => {
const hasContent = draft && draft.trim().length > 0;
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
if (DEBUG_LINKEDIN) console.log('[LinkedIn Writer] Generating suggestions:', {
hasContent,
justGeneratedContent,
draftLength: draft?.length || 0
});
if (!hasContent) {
// Initial suggestions for content creation
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: '🖼️ 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
const refinementSuggestions = [
{ title: '🙂 Make it casual', message: 'Use tool editLinkedInDraft with operation Casual' },
{ title: '💼 Make it professional', message: 'Use tool editLinkedInDraft with operation Professional' },
{ title: '✨ Tighten hook', message: 'Use tool editLinkedInDraft with operation TightenHook' },
{ title: '📣 Add a CTA', message: 'Use tool editLinkedInDraft with operation AddCTA' },
{ 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) {
refinementSuggestions.push({ title: '📣 Add CTA', message: 'Use tool editLinkedInDraft with operation AddCTA' });
}
if (!hasHashtags) {
refinementSuggestions.push({ title: '🏷️ Add hashtags', message: 'Use tool addLinkedInHashtags' });
}
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) {
if (DEBUG_LINKEDIN) 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'
});
}
}
if (DEBUG_LINKEDIN) console.log('[LinkedIn Writer] Final suggestions:', refinementSuggestions);
return refinementSuggestions;
}
}, [draft, justGeneratedContent]);
// Return the suggestions function directly
return getIntelligentSuggestions;
};
export default CopilotActions;

View File

@@ -0,0 +1,337 @@
import React, { useState } from 'react';
interface FeatureCard {
title: string;
desc: string;
icon: string;
image?: string;
onClick?: () => void;
}
interface FeatureCarouselProps {
onFactCheckClick: () => void;
onCopilotClick: () => void;
}
export const FeatureCarousel: React.FC<FeatureCarouselProps> = ({
onFactCheckClick,
onCopilotClick
}) => {
const [currentCardIndex, setCurrentCardIndex] = useState(0);
const featureCards: FeatureCard[] = [
{
title: 'Check Facts',
desc: 'Select text and verify claims with web-backed evidence.',
icon: '🔍',
image: '/Alwrity-fact-check.png',
onClick: onFactCheckClick
},
{
title: 'Google-Grounded Search',
desc: 'Use native Google grounding to inform content with current sources.',
icon: '🌐'
},
{
title: 'Persona-Aware Writing',
desc: 'Generate content tailored to your writing persona and audience.',
icon: '👤'
},
{
title: 'Assistive Writing',
desc: 'Inline, contextual suggestions as you type with citations.',
icon: '✍️',
image: '/ALwrity-assistive-writing.png'
},
{
title: 'ALwrity Copilot',
desc: 'Advanced AI assistant for comprehensive content creation and editing.',
icon: '🤖',
image: '/Alwrity-copilot1.png',
onClick: onCopilotClick
},
{
title: 'Multimodal Generation',
desc: 'Create content with images, videos, and interactive elements.',
icon: '🎨'
}
];
const nextCard = () => {
setCurrentCardIndex((prev) => {
const maxIndex = Math.max(0, featureCards.length - 3);
return prev >= maxIndex ? 0 : prev + 3;
});
};
const prevCard = () => {
setCurrentCardIndex((prev) => {
const maxIndex = Math.max(0, featureCards.length - 3);
return prev <= 0 ? maxIndex : prev - 3;
});
};
return (
<div style={{
marginBottom: 20,
width: '100%',
maxWidth: 1200,
position: 'relative',
padding: '10px 0'
}}>
{/* Carousel Container with Enhanced Styling */}
<div style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: '20px',
padding: '12px',
boxShadow: `
0 20px 60px rgba(0,0,0,0.15),
0 8px 32px rgba(102, 126, 234, 0.1),
inset 0 1px 0 rgba(255,255,255,0.2)
`,
position: 'relative',
overflow: 'hidden'
}}>
{/* Background Glow Effect */}
<div style={{
position: 'absolute',
top: '-50%',
left: '-50%',
width: '200%',
height: '200%',
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
animation: 'rotate 20s linear infinite',
zIndex: 0
}} />
{/* Compact Navigation - Positioned on the sides */}
<button
onClick={prevCard}
style={{
position: 'absolute',
left: '8px',
top: '50%',
transform: 'translateY(-50%)',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '50%',
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4), 0 2px 8px rgba(0,0,0,0.1)',
transition: 'all 0.3s ease',
color: 'white',
fontSize: '14px',
fontWeight: 'bold',
zIndex: 3
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-50%) scale(1.1)';
e.currentTarget.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6), 0 3px 12px rgba(0,0,0,0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(-50%) scale(1)';
e.currentTarget.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4), 0 2px 8px rgba(0,0,0,0.1)';
}}
>
</button>
<button
onClick={nextCard}
style={{
position: 'absolute',
right: '8px',
top: '50%',
transform: 'translateY(-50%)',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '50%',
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4), 0 2px 8px rgba(0,0,0,0.1)',
transition: 'all 0.3s ease',
color: 'white',
fontSize: '14px',
fontWeight: 'bold',
zIndex: 3
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-50%) scale(1.1)';
e.currentTarget.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6), 0 3px 12px rgba(0,0,0,0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(-50%) scale(1)';
e.currentTarget.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4), 0 2px 8px rgba(0,0,0,0.1)';
}}
>
</button>
{/* Features Grid - 3 at a time */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '12px',
zIndex: 2,
position: 'relative'
}}>
{featureCards.slice(currentCardIndex, currentCardIndex + 3).map((card, index) => (
<div
key={currentCardIndex + index}
onClick={card.onClick}
title={card.desc}
style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(15px)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: '16px',
padding: '16px',
boxShadow: `
0 12px 40px rgba(0,0,0,0.1),
0 4px 20px rgba(102, 126, 234, 0.1),
inset 0 1px 0 rgba(255,255,255,0.3)
`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
position: 'relative',
overflow: 'hidden',
transition: 'all 0.3s ease',
minHeight: '140px',
cursor: card.onClick ? 'pointer' : 'default'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-8px) scale(1.02)';
e.currentTarget.style.boxShadow = `
0 20px 60px rgba(0,0,0,0.15),
0 8px 30px rgba(102, 126, 234, 0.2),
inset 0 1px 0 rgba(255,255,255,0.4)
`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.boxShadow = `
0 12px 40px rgba(0,0,0,0.1),
0 4px 20px rgba(102, 126, 234, 0.1),
inset 0 1px 0 rgba(255,255,255,0.3)
`;
}}
>
{/* Card Background Pattern */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: `linear-gradient(45deg,
rgba(102, 126, 234, ${0.1 + index * 0.05}) 0%,
rgba(118, 75, 162, ${0.1 + index * 0.05}) 100%)`,
opacity: 0.4
}} />
{/* Icon/Image - Much Larger */}
<div style={{
fontSize: '48px',
marginBottom: '8px',
zIndex: 1,
filter: 'drop-shadow(0 4px 8px rgba(0,0,0,0.1))',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100px',
flex: '1'
}}>
{card.image ? (
<img
src={card.image}
alt={card.title}
style={{
width: '95%',
height: '100%',
objectFit: 'contain',
borderRadius: '8px'
}}
/>
) : (
<div style={{ fontSize: '64px' }}>
{card.icon}
</div>
)}
</div>
{/* Title Only - Description moved to tooltip */}
<h4 style={{
margin: '0',
color: '#1a202c',
fontSize: '14px',
fontWeight: '700',
zIndex: 1,
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
textAlign: 'center',
lineHeight: '1.2',
padding: '0 4px'
}}>
{card.title}
</h4>
</div>
))}
</div>
{/* Enhanced Dots Indicator */}
<div style={{
display: 'flex',
justifyContent: 'center',
gap: '10px',
marginTop: '12px',
zIndex: 2,
position: 'relative'
}}>
{Array.from({ length: Math.ceil(featureCards.length / 3) }).map((_, index) => (
<button
key={index}
onClick={() => setCurrentCardIndex(index * 3)}
style={{
width: '10px',
height: '10px',
borderRadius: '50%',
border: 'none',
background: Math.floor(currentCardIndex / 3) === index
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: 'rgba(255,255,255,0.3)',
cursor: 'pointer',
transition: 'all 0.3s ease',
boxShadow: Math.floor(currentCardIndex / 3) === index
? '0 3px 12px rgba(102, 126, 234, 0.4)'
: '0 2px 6px rgba(0,0,0,0.1)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.2)';
e.currentTarget.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.5)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)';
e.currentTarget.style.boxShadow = Math.floor(currentCardIndex / 3) === index
? '0 3px 12px rgba(102, 126, 234, 0.4)'
: '0 2px 6px rgba(0,0,0,0.1)';
}}
/>
))}
</div>
</div>
</div>
);
};

View File

@@ -1,5 +1,8 @@
import React from 'react';
import React, { useState, useMemo } from 'react';
import { LinkedInPreferences } from '../utils/storageUtils';
import { PersonaChip } from '../../TextEditor/ContentPreviewHeaderComponents';
import { usePlatformPersonaContext } from '../../shared/PersonaContext/PlatformPersonaProvider';
import BrainstormFlow from './BrainstormFlow';
// Temporary fix: use require for image import
const alwrityLogo = require('../../../assets/images/alwrity_logo.png');
@@ -22,10 +25,59 @@ export const Header: React.FC<HeaderProps> = ({
onClearHistory,
getHistoryLength
}) => {
const [personaOverride, setPersonaOverride] = useState<any>(null);
const { corePersona, platformPersona } = usePlatformPersonaContext();
// Brainstorm modal state
const [showBrainstormModal, setShowBrainstormModal] = useState(false);
const [seed, setSeed] = useState('');
const [usePersona, setUsePersona] = useState(true);
const [useGoogleSearch, setUseGoogleSearch] = useState(true);
const [includeTrending, setIncludeTrending] = useState(false);
const [remarketContent, setRemarketContent] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [aiSearchPrompt, setAiSearchPrompt] = useState('');
// BrainstormFlow state management
const [brainstormVisible, setBrainstormVisible] = useState(false);
const [brainstormStage, setBrainstormStage] = useState<'loading' | 'select' | 'results'>('loading');
const [loaderMessageIndex, setLoaderMessageIndex] = useState(0);
const [aiSearchPrompts, setAiSearchPrompts] = useState<string[]>([]);
const [selectedPrompt, setSelectedPrompt] = useState<string>('');
const [searchResults, setSearchResults] = useState<any[]>([]);
const [ideas, setIdeas] = useState<{ prompt: string; rationale?: string }[]>([]);
const [isUsingCache, setIsUsingCache] = useState(false);
// Check if there are cached brainstorm ideas
const hasCachedIdeas = useMemo(() => {
try {
const keys = Object.keys(sessionStorage);
return keys.some(key => {
if (key.startsWith('brainstorm_ideas_')) {
const cached = sessionStorage.getItem(key);
if (cached) {
const data = JSON.parse(cached);
// Check if cache is less than 1 hour old and has ideas
return Date.now() - data.timestamp < 3600000 && data.ideas && data.ideas.length > 0;
}
}
return false;
});
} catch (e) {
return false;
}
}, [showBrainstormModal]); // Re-check when modal opens
const handlePreferenceChange = (key: keyof LinkedInPreferences, value: any) => {
onPreferencesChange({ [key]: value });
};
const handlePersonaUpdate = (personaData: any) => {
console.log('Persona updated in LinkedIn writer:', personaData);
setPersonaOverride(personaData);
// You can also save this to user preferences or pass it up to the parent component
};
return (
<div style={{
background: 'linear-gradient(135deg, #0a66c2 0%, #0056b3 100%)',
@@ -68,7 +120,6 @@ export const Header: React.FC<HeaderProps> = ({
cursor: 'pointer'
}}
onMouseEnter={() => onPreferencesModalChange(true)}
onMouseLeave={() => onPreferencesModalChange(false)}
>
<div style={{
display: 'flex',
@@ -88,20 +139,24 @@ export const Header: React.FC<HeaderProps> = ({
{/* Preferences Modal */}
{showPreferencesModal && (
<div style={{
position: 'absolute',
top: '100%',
left: '0',
width: '400px',
background: 'white',
borderRadius: '12px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.15)',
border: '1px solid #e9ecef',
padding: '20px',
zIndex: 1000,
marginTop: '8px',
animation: 'slideIn 0.2s ease-out'
}}>
<div
style={{
position: 'absolute',
top: '100%',
left: '0',
width: '400px',
background: 'white',
borderRadius: '12px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.15)',
border: '1px solid #e9ecef',
padding: '20px',
zIndex: 1000,
marginTop: '8px',
animation: 'slideIn 0.2s ease-out'
}}
onMouseEnter={() => onPreferencesModalChange(true)}
onMouseLeave={() => onPreferencesModalChange(false)}
>
<div style={{ marginBottom: '16px' }}>
<h4 style={{ margin: '0 0 12px 0', color: '#333', fontSize: '16px', fontWeight: 600 }}>
Content Preferences & Persona
@@ -144,6 +199,7 @@ export const Header: React.FC<HeaderProps> = ({
</div>
</div>
{/* Interactive Persona Chip */}
<div style={{
display: 'flex',
alignItems: 'center',
@@ -153,18 +209,11 @@ export const Header: React.FC<HeaderProps> = ({
borderRadius: '6px',
border: '1px solid #e2e8f0'
}}>
<div style={{ display: 'flex', gap: '4px' }}>
<span style={{ fontSize: '16px' }}>🎭</span>
<span style={{ fontSize: '16px' }}>🎯</span>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '13px', fontWeight: '600', color: '#2d3748', marginBottom: '2px' }}>
The Digital Strategist (The Insightful Guide)
</div>
<div style={{ fontSize: '11px', color: '#666' }}>
88% accuracy | Platform: LinkedIn Optimized
</div>
</div>
<PersonaChip
platform="linkedin"
userId={1}
onPersonaUpdate={handlePersonaUpdate}
/>
</div>
<div style={{
@@ -173,7 +222,7 @@ export const Header: React.FC<HeaderProps> = ({
color: '#666',
fontStyle: 'italic'
}}>
Hover over persona for detailed information
Click persona to edit writing style, tone, and preferences
</div>
</div>
@@ -347,7 +396,50 @@ export const Header: React.FC<HeaderProps> = ({
</div>
</div>
<div style={{ display: 'flex', gap: 12 }}>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
{/* Today's Tasks Button */}
<button
onClick={() => window.dispatchEvent(new CustomEvent('linkedinwriter:showTodaysTasks'))}
style={{
padding: '8px 16px',
background: 'rgba(255, 255, 255, 0.1)',
color: 'white',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
fontWeight: 500,
display: 'flex',
alignItems: 'center',
gap: 6
}}
title="View today's tasks"
>
📅 Today's Tasks
</button>
{/* Brainstorm Ideas Button */}
<button
onClick={() => setShowBrainstormModal(true)}
style={{
padding: '8px 16px',
background: 'rgba(255, 255, 255, 0.1)',
color: 'white',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
fontWeight: 500,
display: 'flex',
alignItems: 'center',
gap: 6
}}
title="Brainstorm content ideas"
>
💡 Brainstorm Ideas
</button>
{/* Clear Memory Button */}
<button
onClick={onClearHistory}
style={{
@@ -366,6 +458,390 @@ export const Header: React.FC<HeaderProps> = ({
</button>
</div>
</div>
{/* Initial Brainstorm Modal */}
{showBrainstormModal && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000 }}>
<div style={{ background: 'white', width: 720, maxWidth: '92vw', borderRadius: 16, boxShadow: '0 20px 60px rgba(0,0,0,0.25)', overflow: 'hidden' }}>
{/* Header */}
<div style={{ padding: '18px 20px', background: 'linear-gradient(135deg, #0a66c2 0%, #125ea2 100%)', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ fontWeight: 800, fontSize: 16 }}>Brainstorm LinkedIn Content Ideas</div>
<button onClick={() => setShowBrainstormModal(false)} style={{ background: 'rgba(255,255,255,0.2)', border: 'none', color: 'white', borderRadius: 8, padding: '6px 10px', cursor: 'pointer' }}>✕</button>
</div>
{/* Body */}
<div style={{ padding: 20, display: 'grid', gridTemplateColumns: '1.1fr 0.9fr', gap: 16 }}>
<div>
<div style={{ marginBottom: 10, fontWeight: 700, color: '#1f2937' }}>Options</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
border: '1px solid #e5e7eb',
borderRadius: 10,
padding: '10px 12px',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
title="Use your personalized writing persona to generate content that matches your unique voice, tone, and style preferences."
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#0a66c2';
e.currentTarget.style.backgroundColor = '#f8f9ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb';
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<input
type="checkbox"
checked={usePersona}
onChange={(e) => setUsePersona(e.target.checked)}
style={{
accentColor: '#0a66c2',
transform: 'scale(1.1)'
}}
/>
<div style={{ fontWeight: 600, color: '#1f2937' }}>Use Persona</div>
</label>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
border: '1px solid #e5e7eb',
borderRadius: 10,
padding: '10px 12px',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
title="Enable Google Search to find current, relevant information and trending topics for your content ideas."
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#0a66c2';
e.currentTarget.style.backgroundColor = '#f8f9ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb';
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<input
type="checkbox"
checked={useGoogleSearch}
onChange={(e) => setUseGoogleSearch(e.target.checked)}
style={{
accentColor: '#0a66c2',
transform: 'scale(1.1)'
}}
/>
<div style={{ fontWeight: 600, color: '#1f2937' }}>Google Search</div>
</label>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
border: '1px solid #e5e7eb',
borderRadius: 10,
padding: '10px 12px',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
title="Include trending topics and current events to make your content more timely and engaging."
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#0a66c2';
e.currentTarget.style.backgroundColor = '#f8f9ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb';
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<input
type="checkbox"
checked={includeTrending}
onChange={(e) => setIncludeTrending(e.target.checked)}
style={{
accentColor: '#0a66c2',
transform: 'scale(1.1)'
}}
/>
<div style={{ fontWeight: 600, color: '#1f2937' }}>Trending Topics</div>
</label>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
border: '1px solid #e5e7eb',
borderRadius: 10,
padding: '10px 12px',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
title="Repurpose and remarket your existing high-performing content into new formats and angles."
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#0a66c2';
e.currentTarget.style.backgroundColor = '#f8f9ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb';
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<input
type="checkbox"
checked={remarketContent}
onChange={(e) => setRemarketContent(e.target.checked)}
style={{
accentColor: '#0a66c2',
transform: 'scale(1.1)'
}}
/>
<div style={{ fontWeight: 600, color: '#1f2937' }}>Remarket Content</div>
</label>
</div>
<div style={{ marginTop: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
<div style={{ fontWeight: 700, color: '#1f2937' }}>Idea Seed (optional)</div>
</div>
<textarea
value={seed}
onChange={(e) => setSeed(e.target.value)}
placeholder={corePersona?.core_belief ? `Ex: Show how "${corePersona.core_belief}" applies to SMB founders this quarter` : 'Add a theme, problem, or audience'}
rows={3}
style={{ width: '100%', border: '1px solid #e5e7eb', borderRadius: 10, padding: '10px 12px', fontSize: 14, resize: 'vertical' }}
/>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 8 }}>
<div style={{ fontSize: 12, color: '#6b7280' }}>
Alwrity It requires Google Search enabled and an idea seed with at least 4 words.
</div>
<button
onClick={() => {
const words = (seed || '').trim().split(/\s+/).filter(Boolean);
if (!useGoogleSearch || words.length < 4) return;
const personaLine = corePersona ? `${corePersona.persona_name} (${corePersona.archetype})` : 'the user\'s writing persona';
const tone = (corePersona as any)?.tonal_range?.default_tone || (platformPersona as any)?.tonal_range?.default_tone || 'professional';
const goTo = corePersona?.linguistic_fingerprint?.lexical_features?.go_to_words?.slice(0,5)?.join(', ');
const platformHints = platformPersona ? `Respect LinkedIn constraints like character limits and engagement patterns.` : '';
const trending = includeTrending ? 'Blend industry trending topics.' : '';
const repurpose = remarketContent ? 'Consider repurposing top-performing content into new angles.' : '';
const prompt = `You are an expert LinkedIn content strategist writing in a ${tone} tone for ${personaLine}. Generate a list of highly-relevant, specific topic ideas based on this seed: "${seed}". Prioritize originality, practical value, and thought leadership. ${platformHints} ${trending} ${repurpose} Use current (20242025) language and avoid generic suggestions.`.trim();
setAiSearchPrompt(prompt);
setShowConfirm(true);
}}
disabled={!(useGoogleSearch && (seed || '').trim().split(/\s+/).filter(Boolean).length >= 4)}
style={{ padding: '8px 12px', borderRadius: 8, border: '1px solid #0a66c2', background: useGoogleSearch && (seed || '').trim().split(/\s+/).filter(Boolean).length >= 4 ? '#0a66c2' : '#c7d2fe', color: 'white', fontWeight: 800, cursor: useGoogleSearch && (seed || '').trim().split(/\s+/).filter(Boolean).length >= 4 ? 'pointer' : 'not-allowed' }}
>
Alwrity It
</button>
</div>
</div>
</div>
<div>
<div style={{ fontWeight: 700, color: '#1f2937', marginBottom: 6 }}>Quick Actions</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<button
onClick={() => {
if (hasCachedIdeas) {
window.dispatchEvent(new CustomEvent('linkedinwriter:runGoogleSearchForIdeas', {
detail: { prompt: 'View cached ideas', seed: 'cached', forceRefresh: false }
}));
} else {
window.dispatchEvent(new CustomEvent('linkedinwriter:runGoogleSearchForIdeas', {
detail: { usePersona, useGoogleSearch, includeTrending, remarketContent, seed }
}));
}
setShowBrainstormModal(false);
setBrainstormVisible(true);
}}
style={{
padding: '12px 16px',
borderRadius: 8,
background: hasCachedIdeas ? '#0a66c2' : '#6b7280',
color: 'white',
border: 'none',
cursor: 'pointer',
fontWeight: 800,
fontSize: 14
}}
>
{hasCachedIdeas ? 'View Previous Ideas' : 'Generate Ideas'}
</button>
</div>
{/* Suggestions Section */}
<div style={{ marginTop: 20 }}>
<div style={{ fontWeight: 700, color: '#1f2937', marginBottom: 8 }}>💡 Suggestions</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<button
onClick={() => setSeed('AI and automation trends in 2024')}
style={{
padding: '8px 12px',
background: '#f8f9ff',
border: '1px solid #e5e7eb',
borderRadius: 6,
cursor: 'pointer',
fontSize: 12,
color: '#374151',
textAlign: 'left',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#e0e7ff';
e.currentTarget.style.borderColor = '#0a66c2';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#f8f9ff';
e.currentTarget.style.borderColor = '#e5e7eb';
}}
>
🤖 AI and automation trends
</button>
<button
onClick={() => setSeed('Remote work productivity tips')}
style={{
padding: '8px 12px',
background: '#f8f9ff',
border: '1px solid #e5e7eb',
borderRadius: 6,
cursor: 'pointer',
fontSize: 12,
color: '#374151',
textAlign: 'left',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#e0e7ff';
e.currentTarget.style.borderColor = '#0a66c2';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#f8f9ff';
e.currentTarget.style.borderColor = '#e5e7eb';
}}
>
🏠 Remote work productivity
</button>
<button
onClick={() => setSeed('Leadership lessons from failures')}
style={{
padding: '8px 12px',
background: '#f8f9ff',
border: '1px solid #e5e7eb',
borderRadius: 6,
cursor: 'pointer',
fontSize: 12,
color: '#374151',
textAlign: 'left',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#e0e7ff';
e.currentTarget.style.borderColor = '#0a66c2';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#f8f9ff';
e.currentTarget.style.borderColor = '#e5e7eb';
}}
>
🎯 Leadership lessons
</button>
<button
onClick={() => setSeed('Industry insights and predictions')}
style={{
padding: '8px 12px',
background: '#f8f9ff',
border: '1px solid #e5e7eb',
borderRadius: 6,
cursor: 'pointer',
fontSize: 12,
color: '#374151',
textAlign: 'left',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#e0e7ff';
e.currentTarget.style.borderColor = '#0a66c2';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#f8f9ff';
e.currentTarget.style.borderColor = '#e5e7eb';
}}
>
📈 Industry insights
</button>
</div>
</div>
</div>
</div>
{/* Footer */}
<div style={{ padding: 16, borderTop: '1px solid #e5e7eb', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f9fafb' }}>
<div style={{ color: '#6b7280', fontSize: 12 }}>
{hasCachedIdeas ? 'You have previously generated ideas. Click "View Previous Ideas" to see them.' : 'These settings guide idea generation. You can fine-tune results in the editor.'}
</div>
<div style={{ display: 'flex', gap: 10 }}>
<button onClick={() => setShowBrainstormModal(false)} style={{ padding: '10px 16px', borderRadius: 8, background: 'white', border: '1px solid #e5e7eb', cursor: 'pointer', fontWeight: 700 }}>Cancel</button>
</div>
</div>
</div>
</div>
)}
{/* Confirmation Modal for AI Search Prompt */}
{showConfirm && (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10001 }}>
<div style={{ background: 'white', width: 680, maxWidth: '92vw', borderRadius: 16, boxShadow: '0 20px 60px rgba(0,0,0,0.25)', overflow: 'hidden' }}>
<div style={{ padding: '16px 18px', background: '#0a66c2', color: 'white', fontWeight: 800 }}>Confirm Google Search Prompt</div>
<div style={{ padding: 16 }}>
<div style={{ fontSize: 13, color: '#374151', marginBottom: 8 }}>We crafted this AI prompt using your persona and seed. Review and confirm to run Google Search for topic ideas.</div>
<textarea value={aiSearchPrompt} onChange={(e) => setAiSearchPrompt(e.target.value)} rows={6} style={{ width: '100%', border: '1px solid #e5e7eb', borderRadius: 10, padding: '10px 12px', fontSize: 13 }} />
</div>
<div style={{ padding: 12, borderTop: '1px solid #e5e7eb', display: 'flex', justifyContent: 'flex-end', gap: 10, background: '#f9fafb' }}>
<button onClick={() => setShowConfirm(false)} style={{ padding: '8px 12px', borderRadius: 8, background: 'white', border: '1px solid #e5e7eb', cursor: 'pointer', fontWeight: 700 }}>Back</button>
<button
onClick={() => {
window.dispatchEvent(new CustomEvent('linkedinwriter:runGoogleSearchForIdeas', { detail: { prompt: aiSearchPrompt, seed, usePersona, includeTrending, remarketContent } }));
setShowConfirm(false);
setShowBrainstormModal(false);
setBrainstormVisible(true);
}}
style={{ padding: '8px 12px', borderRadius: 8, background: '#0a66c2', color: 'white', border: 'none', cursor: 'pointer', fontWeight: 800 }}
>
Run Google Search
</button>
</div>
</div>
</div>
)}
{/* BrainstormFlow Component */}
<BrainstormFlow
brainstormVisible={brainstormVisible}
setBrainstormVisible={setBrainstormVisible}
brainstormStage={brainstormStage}
setBrainstormStage={setBrainstormStage}
loaderMessageIndex={loaderMessageIndex}
setLoaderMessageIndex={setLoaderMessageIndex}
aiSearchPrompts={aiSearchPrompts}
setAiSearchPrompts={setAiSearchPrompts}
selectedPrompt={selectedPrompt}
setSelectedPrompt={setSelectedPrompt}
searchResults={searchResults}
setSearchResults={setSearchResults}
ideas={ideas}
setIdeas={setIdeas}
isUsingCache={isUsingCache}
setIsUsingCache={setIsUsingCache}
/>
</div>
);
};

View File

@@ -0,0 +1,493 @@
import React from 'react';
interface InfoModalsProps {
showCopilotModal: boolean;
showAssistiveModal: boolean;
showFactCheckModal: boolean;
onCloseCopilotModal: () => void;
onCloseAssistiveModal: () => void;
onCloseFactCheckModal: () => void;
onOpenCopilot: () => void;
}
export const InfoModals: React.FC<InfoModalsProps> = ({
showCopilotModal,
showAssistiveModal,
showFactCheckModal,
onCloseCopilotModal,
onCloseAssistiveModal,
onCloseFactCheckModal,
onOpenCopilot
}) => {
return (
<>
{/* Copilot Modal */}
{showCopilotModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
backdropFilter: 'blur(5px)'
}}>
<div style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.9) 100%)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.3)',
borderRadius: '24px',
padding: '32px',
maxWidth: '800px',
width: '90%',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
position: 'relative'
}}>
<button
onClick={onCloseCopilotModal}
style={{
position: 'absolute',
top: '16px',
right: '16px',
background: 'rgba(0,0,0,0.1)',
border: 'none',
borderRadius: '50%',
width: '32px',
height: '32px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
color: '#666'
}}
>
×
</button>
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
<h2 style={{ margin: '0 0 16px 0', color: '#1a202c', fontSize: '24px', fontWeight: '700' }}>
ALwrity Copilot
</h2>
<p style={{ margin: '0 0 20px 0', color: '#4a5568', fontSize: '16px' }}>
Your comprehensive AI writing assistant
</p>
{/* Screenshot Images */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '16px',
marginBottom: '20px'
}}>
<div style={{ textAlign: 'center' }}>
<img
src="/Alwrity-copilot1.png"
alt="ALwrity Copilot Interface"
style={{
width: '100%',
maxWidth: '250px',
height: 'auto',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
border: '1px solid #e2e8f0'
}}
/>
<p style={{
margin: '8px 0 0 0',
fontSize: '12px',
color: '#666',
fontWeight: '500'
}}>
Main Interface
</p>
</div>
<div style={{ textAlign: 'center' }}>
<img
src="/Alwrity-copilot2.png"
alt="ALwrity Copilot Features"
style={{
width: '100%',
maxWidth: '250px',
height: 'auto',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
border: '1px solid #e2e8f0'
}}
/>
<p style={{
margin: '8px 0 0 0',
fontSize: '12px',
color: '#666',
fontWeight: '500'
}}>
Advanced Features
</p>
</div>
</div>
</div>
<div style={{ textAlign: 'left' }}>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
What is ALwrity Copilot?
</h3>
<p style={{ color: '#4a5568', lineHeight: '1.6', marginBottom: '20px' }}>
ALwrity Copilot is an advanced AI assistant that provides comprehensive support for all your content creation needs.
It combines multiple AI capabilities to help you create, edit, and optimize content across various formats.
</p>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
Key Features:
</h3>
<ul style={{ color: '#4a5568', lineHeight: '1.6', paddingLeft: '20px', marginBottom: '20px' }}>
<li>Generate LinkedIn posts, articles, carousels, and video scripts</li>
<li>Real-time content editing and optimization suggestions</li>
<li>Research-backed content with source citations</li>
<li>Persona-aware writing tailored to your audience</li>
<li>Fact-checking and verification capabilities</li>
<li>Multi-format content creation (text, images, videos)</li>
</ul>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
How to Use:
</h3>
<p style={{ color: '#4a5568', lineHeight: '1.6', marginBottom: '20px' }}>
Click the ALwrity Copilot icon in the bottom-right corner of your screen to open the chat interface.
You can then ask for help with any content creation task, and the AI will guide you through the process.
</p>
<button
onClick={() => {
onCloseCopilotModal();
onOpenCopilot();
}}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
color: 'white',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
width: '100%',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 8px 25px rgba(102, 126, 234, 0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
Open ALwrity Copilot
</button>
</div>
</div>
</div>
)}
{/* Assistive Research Modal */}
{showAssistiveModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
backdropFilter: 'blur(5px)'
}}>
<div style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.9) 100%)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.3)',
borderRadius: '24px',
padding: '32px',
maxWidth: '600px',
width: '90%',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
position: 'relative'
}}>
<button
onClick={onCloseAssistiveModal}
style={{
position: 'absolute',
top: '16px',
right: '16px',
background: 'rgba(0,0,0,0.1)',
border: 'none',
borderRadius: '50%',
width: '32px',
height: '32px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
color: '#666'
}}
>
×
</button>
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🔬</div>
<h2 style={{ margin: '0 0 8px 0', color: '#1a202c', fontSize: '24px', fontWeight: '700' }}>
Assistive Research Writing
</h2>
<p style={{ margin: 0, color: '#4a5568', fontSize: '16px' }}>
Real-time AI writing assistance with research-backed suggestions
</p>
</div>
<div style={{ textAlign: 'left' }}>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
What is Assistive Research Writing?
</h3>
<p style={{ color: '#4a5568', lineHeight: '1.6', marginBottom: '20px' }}>
Assistive Research Writing provides real-time, contextual writing suggestions as you type.
It combines AI-powered content generation with web research to provide accurate, up-to-date information
and suggestions that enhance your writing quality and credibility.
</p>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
Key Features:
</h3>
<ul style={{ color: '#4a5568', lineHeight: '1.6', paddingLeft: '20px', marginBottom: '20px' }}>
<li>Real-time writing suggestions as you type</li>
<li>Research-backed content with source citations</li>
<li>Contextual continuation of your thoughts</li>
<li>Fact-checking and verification of claims</li>
<li>Smart gating to prevent excessive API usage</li>
<li>Seamless integration with your writing flow</li>
</ul>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
How to Use:
</h3>
<p style={{ color: '#4a5568', lineHeight: '1.6', marginBottom: '20px' }}>
Enable Assistive Writing in the editor settings. Once enabled, start typing your content.
After typing 5+ words and pausing for 5 seconds, you'll receive contextual writing suggestions.
You can accept, dismiss, or request more suggestions as needed.
</p>
<button
onClick={onCloseAssistiveModal}
style={{
background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
color: 'white',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
width: '100%',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 8px 25px rgba(240, 147, 251, 0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
Got it, let's start writing!
</button>
</div>
</div>
</div>
)}
{/* Fact Check Modal */}
{showFactCheckModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
backdropFilter: 'blur(5px)'
}}>
<div style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.9) 100%)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.3)',
borderRadius: '24px',
padding: '32px',
maxWidth: '800px',
width: '90%',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
position: 'relative'
}}>
<button
onClick={onCloseFactCheckModal}
style={{
position: 'absolute',
top: '16px',
right: '16px',
background: 'rgba(0,0,0,0.1)',
border: 'none',
borderRadius: '50%',
width: '32px',
height: '32px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
color: '#666'
}}
>
×
</button>
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🔍</div>
<h2 style={{ margin: '0 0 8px 0', color: '#1a202c', fontSize: '24px', fontWeight: '700' }}>
Check Facts Feature
</h2>
<p style={{ margin: 0, color: '#4a5568', fontSize: '16px' }}>
Verify claims with web-backed evidence and AI-powered analysis
</p>
</div>
{/* Images Section */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '20px',
marginBottom: '24px'
}}>
<div style={{
background: 'rgba(255,255,255,0.5)',
borderRadius: '12px',
padding: '16px',
textAlign: 'center'
}}>
<img
src="/Alwrity-fact-check.png"
alt="ALwrity Fact Check Interface"
style={{
width: '100%',
maxWidth: '300px',
height: 'auto',
borderRadius: '8px',
boxShadow: '0 4px 15px rgba(0,0,0,0.1)',
marginBottom: '12px'
}}
/>
<h4 style={{ margin: '0 0 8px 0', color: '#2d3748', fontSize: '16px', fontWeight: '600' }}>
ALwrity Fact Check Interface
</h4>
<p style={{ margin: 0, color: '#4a5568', fontSize: '14px' }}>
Select any text in your content to verify claims
</p>
</div>
<div style={{
background: 'rgba(255,255,255,0.5)',
borderRadius: '12px',
padding: '16px',
textAlign: 'center'
}}>
<img
src="/Fact-check1.png"
alt="Fact Check Results"
style={{
width: '100%',
maxWidth: '300px',
height: 'auto',
borderRadius: '8px',
boxShadow: '0 4px 15px rgba(0,0,0,0.1)',
marginBottom: '12px'
}}
/>
<h4 style={{ margin: '0 0 8px 0', color: '#2d3748', fontSize: '16px', fontWeight: '600' }}>
Detailed Fact Check Results
</h4>
<p style={{ margin: 0, color: '#4a5568', fontSize: '14px' }}>
Get comprehensive analysis with source citations
</p>
</div>
</div>
<div style={{ textAlign: 'left' }}>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
How Fact Checking Works:
</h3>
<ol style={{ color: '#4a5568', lineHeight: '1.6', paddingLeft: '20px', marginBottom: '20px' }}>
<li><strong>Select Text:</strong> Highlight any claim or statement in your content</li>
<li><strong>AI Analysis:</strong> Our AI extracts key claims and identifies fact-checkable statements</li>
<li><strong>Web Search:</strong> Search for evidence using Exa.ai and Google Search</li>
<li><strong>Verification:</strong> Compare claims against reliable sources and evidence</li>
<li><strong>Results:</strong> Get detailed analysis with confidence scores and source citations</li>
</ol>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
Key Benefits:
</h3>
<ul style={{ color: '#4a5568', lineHeight: '1.6', paddingLeft: '20px', marginBottom: '20px' }}>
<li>Verify claims before publishing to maintain credibility</li>
<li>Get source citations for better content transparency</li>
<li>Identify potentially misleading or false information</li>
<li>Enhance content quality with evidence-based writing</li>
<li>Build trust with your audience through verified content</li>
</ul>
<button
onClick={onCloseFactCheckModal}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
color: 'white',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
width: '100%',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 8px 25px rgba(102, 126, 234, 0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
Got it, let's start fact-checking!
</button>
</div>
</div>
</div>
)}
</>
);
};

View File

@@ -9,6 +9,8 @@ export { Header } from './Header';
export { ContentEditor } from './ContentEditor';
export { LoadingIndicator } from './LoadingIndicator';
export { WelcomeMessage } from './WelcomeMessage';
export { FeatureCarousel } from './FeatureCarousel';
export { InfoModals } from './InfoModals';
export { ProgressTracker } from './ProgressTracker';
export { ContentRecommendations } from './ContentRecommendations';
export { CopilotRecommendationsMessage } from './CopilotRecommendationsMessage';
@@ -21,3 +23,7 @@ export { default as ImageGenerationDemo } from './ImageGenerationDemo';
export { default as ImageGenerationTest } from './ImageGenerationTest';
// Persona Integration Components - Now integrated into main LinkedInWriter
// Refactored Components
export { default as BrainstormFlow } from './BrainstormFlow';
export { default as CopilotActions } from './CopilotActions';