ALwrity LinkedIn Writer: Brainstorm Flow, Copilot Actions, Feature Carousel, Info Modals, Welcome Message
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 (2024–2025) 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>
|
||||
);
|
||||
};
|
||||
|
||||
493
frontend/src/components/LinkedInWriter/components/InfoModals.tsx
Normal file
493
frontend/src/components/LinkedInWriter/components/InfoModals.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user