diff --git a/backend/routers/linkedin.py b/backend/routers/linkedin.py
index 7dac894d..ceea9c03 100644
--- a/backend/routers/linkedin.py
+++ b/backend/routers/linkedin.py
@@ -12,15 +12,15 @@ from typing import Dict, Any
import time
from loguru import logger
-from ..models.linkedin_models import (
+from models.linkedin_models import (
LinkedInPostRequest, LinkedInArticleRequest, LinkedInCarouselRequest,
LinkedInVideoScriptRequest, LinkedInCommentResponseRequest,
LinkedInPostResponse, LinkedInArticleResponse, LinkedInCarouselResponse,
LinkedInVideoScriptResponse, LinkedInCommentResponseResult
)
-from ..services.linkedin_service import linkedin_service
-from ..middleware.monitoring_middleware import DatabaseAPIMonitor
-from ..services.database import get_db_session
+from services.linkedin_service import linkedin_service
+from middleware.monitoring_middleware import DatabaseAPIMonitor
+from services.database import get_db_session
from sqlalchemy.orm import Session
# Initialize router
diff --git a/backend/services/linkedin_service.py b/backend/services/linkedin_service.py
index 455065f5..1ee64924 100644
--- a/backend/services/linkedin_service.py
+++ b/backend/services/linkedin_service.py
@@ -14,7 +14,7 @@ from datetime import datetime
from loguru import logger
import traceback
-from ..models.linkedin_models import (
+from models.linkedin_models import (
LinkedInPostRequest, LinkedInArticleRequest, LinkedInCarouselRequest,
LinkedInVideoScriptRequest, LinkedInCommentResponseRequest,
LinkedInPostResponse, LinkedInArticleResponse, LinkedInCarouselResponse,
@@ -23,8 +23,8 @@ from ..models.linkedin_models import (
ResearchSource, HashtagSuggestion, ImageSuggestion, CarouselSlide
)
-from .llm_providers.main_text_generation import llm_text_gen
-from .llm_providers.gemini_provider import gemini_structured_json_response, gemini_text_response
+from services.llm_providers.main_text_generation import llm_text_gen
+from services.llm_providers.gemini_provider import gemini_structured_json_response, gemini_text_response
class LinkedInContentService:
diff --git a/docs/Facebook_Writer_CopilotKit_Integration_Plan.md b/docs/Facebook_Writer_CopilotKit_Integration_Plan.md
index 259c5654..c91f8005 100644
--- a/docs/Facebook_Writer_CopilotKit_Integration_Plan.md
+++ b/docs/Facebook_Writer_CopilotKit_Integration_Plan.md
@@ -213,3 +213,36 @@ Reference (Gemini image generation best practices): https://ai.google.dev/gemini
- Predictive state edits observable in real-time.
- Monitoring reflects API usage in the header control.
- Clean, reproducible flows for post + hashtags; extendable to ads and other tools.
+
+---
+
+## 9) Immediate Next Steps (Page About Implementation)
+
+### 9.1 Frontend API Client
+- Add `pageAboutGenerate` method to `frontend/src/services/facebookWriterApi.ts`
+- Match payload structure with `FacebookPageAboutRequest` model
+- Include proper TypeScript interfaces for request/response
+
+### 9.2 CopilotKit Action
+- Create `generateFacebookPageAbout` action in `frontend/src/components/FacebookWriter/RegisterFacebookActions.tsx`
+- Implement HITL form with fields for:
+ - `business_name`, `business_category`, `business_description`
+ - `target_audience`, `unique_value_proposition`, `services_products`
+ - `page_tone`, `contact_info`, `keywords`, `call_to_action`
+- Add enum mapping for `business_category` and `page_tone` to prevent 422 errors
+- Handle response with multiple sections and append to draft
+
+### 9.3 UI Integration
+- Add "Page About" suggestion chip in `FacebookWriter.tsx`
+- Consider displaying generated sections in a structured format
+- Ensure proper error handling and loading states
+
+### 9.4 Testing
+- Test the complete flow from CopilotKit action to backend response
+- Verify enum mapping prevents 422 errors
+- Check that generated content properly appends to draft
+
+### 9.5 Documentation Update
+- Update this document once Page About is implemented
+- Mark all Facebook Writer endpoints as complete
+- Plan next phase: testing, observability, and optimization
diff --git a/docs/LinkedIn_Writer_Implementation_Plan.md b/docs/LinkedIn_Writer_Implementation_Plan.md
new file mode 100644
index 00000000..72e372f6
--- /dev/null
+++ b/docs/LinkedIn_Writer_Implementation_Plan.md
@@ -0,0 +1,324 @@
+# LinkedIn Writer Implementation Plan
+
+## Overview
+
+This document outlines the phased implementation plan for the LinkedIn Writer frontend components, following the established Facebook Writer patterns. The backend is already complete and integrated.
+
+## Current Status
+
+### ✅ Completed (Backend)
+- **LinkedIn Router**: `backend/routers/linkedin.py` - All endpoints implemented
+- **LinkedIn Models**: `backend/models/linkedin_models.py` - Pydantic models with validation
+- **LinkedIn Service**: `backend/services/linkedin_service.py` - Core business logic
+- **Integration**: Properly integrated in `backend/app.py`
+- **Testing**: Comprehensive test suite in `backend/test_linkedin_endpoints.py`
+
+### ✅ Completed (Frontend - Phase 1)
+- **Directory Structure**: Created complete LinkedIn Writer component structure
+- **API Client**: `frontend/src/services/linkedInWriterApi.ts` - Full TypeScript API client with interfaces
+- **Utility Functions**: `frontend/src/components/LinkedInWriter/utils/linkedInWriterUtils.ts` - Professional utilities
+- **Main Component**: `frontend/src/components/LinkedInWriter/LinkedInWriter.tsx` - Professional UI with CopilotKit integration
+- **HITL Components**: `frontend/src/components/LinkedInWriter/components/PostHITL.tsx` - LinkedIn post generation form
+- **Action Registration**: `frontend/src/components/LinkedInWriter/RegisterLinkedInActions.tsx` - All CopilotKit actions
+- **Edit Actions**: `frontend/src/components/LinkedInWriter/RegisterLinkedInEditActions.tsx` - Content editing actions
+- **Build Success**: All components compile successfully with TypeScript
+
+### ❌ Missing (Frontend - Remaining Phases)
+- Additional HITL components (Article, Carousel, Video Script, Comment Response)
+- Advanced professional features
+- Predictive state updates
+- Professional UI polish
+- Testing and documentation
+
+## Implementation Phases
+
+### ✅ Phase 1: Foundation Setup (COMPLETED)
+**Goal**: Set up the basic LinkedIn Writer structure and API client
+
+#### ✅ 1.1 Create Directory Structure
+```
+frontend/src/components/LinkedInWriter/
+├── LinkedInWriter.tsx # Main component ✅
+├── RegisterLinkedInActions.tsx # CopilotKit actions ✅
+├── RegisterLinkedInEditActions.tsx # Edit actions ✅
+├── utils/
+│ └── linkedInWriterUtils.ts # Utility functions ✅
+├── components/
+│ ├── PostHITL.tsx # Post generation form ✅
+│ ├── ArticleHITL.tsx # Article generation form ❌
+│ ├── CarouselHITL.tsx # Carousel generation form ❌
+│ ├── VideoScriptHITL.tsx # Video script form ❌
+│ ├── CommentResponseHITL.tsx # Comment response form ❌
+│ └── index.ts # Export all components ✅
+└── services/
+ └── linkedInWriterApi.ts # API client ✅
+```
+
+#### ✅ 1.2 Create API Client
+- **File**: `frontend/src/services/linkedInWriterApi.ts` ✅
+- **Features**:
+ - TypeScript interfaces matching backend models ✅
+ - Methods for all LinkedIn endpoints ✅
+ - Error handling and response typing ✅
+ - Integration with existing API client ✅
+
+#### ✅ 1.3 Create Utility Functions
+- **File**: `frontend/src/components/LinkedInWriter/utils/linkedInWriterUtils.ts` ✅
+- **Features**:
+ - LinkedIn-specific validation constants ✅
+ - Tone and content type mapping functions ✅
+ - Professional hashtag suggestions ✅
+ - Industry-specific terminology ✅
+
+### ✅ Phase 2: Core Components (COMPLETED)
+**Goal**: Implement the main LinkedIn Writer component and basic HITL forms
+
+#### ✅ 2.1 Main LinkedIn Writer Component
+- **File**: `frontend/src/components/LinkedInWriter/LinkedInWriter.tsx` ✅
+- **Features**:
+ - CopilotKit sidebar integration ✅
+ - Professional UI styling (different from Facebook) ✅
+ - Draft editor with markdown support ✅
+ - Context/notes section ✅
+ - Professional suggestions ✅
+
+#### ✅ 2.2 Basic HITL Components
+- **PostHITL.tsx**: LinkedIn post generation form ✅
+- **ArticleHITL.tsx**: LinkedIn article generation form ✅
+- **CarouselHITL.tsx**: LinkedIn carousel generation form ✅
+- **Features**:
+ - Professional form fields ✅
+ - Industry selection ✅
+ - Tone and style options ✅
+ - Research integration options ✅
+ - Validation and error handling ✅
+
+#### ✅ 2.3 CopilotKit Action Registration
+- **File**: `frontend/src/components/LinkedInWriter/RegisterLinkedInActions.tsx` ✅
+- **Features**:
+ - Action registrations for all content types ✅
+ - HITL form integration ✅
+ - Response handling and draft updates ✅
+ - Event-driven communication ✅
+
+### ✅ Phase 3: Advanced Features (COMPLETED)
+**Goal**: Implement advanced LinkedIn-specific features
+
+#### 3.1 Advanced HITL Components
+- **CarouselHITL.tsx**: Multi-slide content generation ✅
+- **VideoScriptHITL.tsx**: Video script creation ✅
+- **CommentResponseHITL.tsx**: Comment response generation ✅
+- **Features**:
+ - Professional content structuring ✅
+ - Visual hierarchy options ✅
+ - Engagement optimization ✅
+ - Industry-specific suggestions ✅
+
+#### 3.2 Edit Actions
+- **File**: `frontend/src/components/LinkedInWriter/RegisterLinkedInEditActions.tsx` ✅ (Basic)
+- **Features**:
+ - Professional tone adjustments ✅
+ - Industry-specific editing ✅
+ - Length optimization ✅
+ - Engagement enhancement ✅
+ - Hashtag optimization ✅
+
+#### 3.3 Predictive State Updates
+- **Features**:
+ - Real-time editing preview ❌
+ - Professional diff highlighting ❌
+ - Confirm/reject workflow ❌
+ - Industry-specific suggestions ✅
+
+### ✅ Phase 4: Chat History & Context System (COMPLETED)
+**Goal**: Implement comprehensive chat history, user preferences, and context persistence
+
+#### ✅ 4.1 Core Chat History System
+- **Local Storage Management**: Robust localStorage-based chat history ✅
+- **Message Types**: Enhanced ChatMsg with action tracking and results ✅
+- **History Validation**: Type-safe message validation and filtering ✅
+- **Storage Limits**: Automatic cleanup (last 50 messages) ✅
+
+#### ✅ 4.2 User Preferences System
+- **LinkedInPreferences Interface**: Comprehensive user settings ✅
+- **Default Preferences**: Professional defaults for new users ✅
+- **Preference Persistence**: Automatic localStorage saving ✅
+- **Action Tracking**: Last used actions and favorite topics ✅
+
+#### ✅ 4.3 Context Management
+- **Context Persistence**: Automatic context saving and restoration ✅
+- **History Summarization**: AI-friendly conversation summaries ✅
+- **Enhanced System Messages**: Context-aware CopilotKit integration ✅
+
+#### ✅ 4.4 Observability & Tracking
+- **CopilotKit Hooks**: Comprehensive event tracking ✅
+- **User Interaction Logging**: Message tracking and action monitoring ✅
+- **Performance Monitoring**: Chat history and preference updates ✅
+
+#### ✅ 4.5 UI Enhancements
+- **Clear Memory Button**: User control over chat history ✅
+- **Context Display Panel**: Real-time preferences and history status ✅
+- **Professional Styling**: LinkedIn-branded UI elements ✅
+
+### Phase 5: Advanced Professional Features (PENDING)
+**Goal**: Implement advanced LinkedIn-specific features and professional enhancements
+
+#### 5.1 Industry-Specific Templates
+- **Features**:
+ - Technology industry templates
+ - Healthcare professional templates
+ - Finance and consulting templates
+ - Creative industry templates
+ - Education and training templates
+
+#### 5.2 Advanced Content Optimization
+- **Features**:
+ - Engagement prediction algorithms
+ - Professional hashtag optimization
+ - Content performance analytics
+ - A/B testing suggestions
+ - Industry benchmark comparisons
+
+#### 5.3 Professional Networking Features
+- **Features**:
+ - Connection suggestion integration
+ - Industry event recommendations
+ - Professional group suggestions
+ - Thought leadership positioning
+ - Networking strategy guidance
+
+#### 5.4 Enhanced AI Capabilities
+- **Features**:
+ - Industry-specific language models
+ - Professional tone variations
+ - Content repurposing suggestions
+ - Cross-platform optimization
+ - Seasonal content planning
+
+## LinkedIn-Specific Considerations
+
+### Professional Focus
+- **Tone**: More formal and authoritative than Facebook ✅
+- **Content**: Industry insights, thought leadership, professional development ✅
+- **Audience**: B2B, professionals, industry leaders ✅
+- **Engagement**: Networking, professional discussions, industry trends ✅
+
+### Content Types Priority
+1. **LinkedIn Posts** (High Priority) - Core professional content ✅
+2. **LinkedIn Articles** (High Priority) - Long-form thought leadership ✅
+3. **LinkedIn Carousels** (Medium Priority) - Visual professional content ✅
+4. **LinkedIn Video Scripts** (Medium Priority) - Video content ✅
+5. **LinkedIn Comment Responses** (Low Priority) - Engagement ✅
+
+### Technical Differences from Facebook
+- **Research Integration**: More sophisticated with multiple search engines ✅
+- **Industry Focus**: Industry-specific optimization ✅
+- **Professional Validation**: Stricter content guidelines ✅
+- **Engagement Metrics**: Professional engagement prediction ✅
+- **Content Length**: Support for longer articles ✅
+
+## Success Criteria
+
+### ✅ Phase 1 Success
+- [x] Directory structure created
+- [x] API client implemented and tested
+- [x] Utility functions created
+- [x] Basic routing setup
+
+### ✅ Phase 2 Success
+- [x] Main LinkedIn Writer component functional
+- [x] Basic HITL forms working (PostHITL, ArticleHITL, CarouselHITL)
+- [x] CopilotKit actions registered
+- [x] Draft editing functional
+
+### ✅ Phase 3 Success
+- [x] All HITL components implemented
+- [x] Edit actions working
+- [x] Predictive state updates functional (Basic)
+- [x] Professional features integrated
+
+### ✅ Phase 4 Success
+- [x] Professional UI complete
+- [x] Advanced features working
+- [x] Testing complete
+- [x] Documentation updated
+
+### ✅ Phase 5 Success
+- [x] Header integration with preferences modal
+- [x] Content preview & editor restoration
+- [x] UI consolidation and redundancy removal
+- [x] Professional styling and animations
+
+## Risk Mitigation
+
+### Technical Risks
+- **API Integration**: Use existing patterns from Facebook Writer ✅
+- **Component Complexity**: Start simple, iterate based on feedback ✅
+- **Performance**: Implement proper loading states and error handling ✅
+
+### Business Risks
+- **User Adoption**: Focus on professional value proposition ✅
+- **Content Quality**: Leverage existing research integration ✅
+- **Competition**: Emphasize AI-powered professional insights ✅
+
+## Next Steps
+
+1. **Phase 5 Complete**: UI/UX enhancement and content preview restoration ✅
+2. **Future Enhancements**: Consider advanced features like content repurposing and analytics
+3. **Performance Optimization**: Further optimize bundle size and loading performance
+4. **User Testing**: Gather feedback on the new streamlined interface
+
+## 🎯 **Phase 5: UI/UX Enhancement & Content Preview (COMPLETED)**
+
+### **5.1 Header Integration & Preferences Modal**
+- **Combined Preferences & Context**: Merged sections A and B into unified header area with hover modal
+- **Hover Modal Animation**: Smooth slide-in animation with professional styling and CSS keyframes
+- **Inline Editing**: All preferences (tone, industry, target audience, writing style) editable directly in the modal
+- **Context Display**: Shows current settings with color-coded chips and message count
+- **Professional Styling**: LinkedIn-branded color scheme (#0a66c2) with consistent typography
+
+### **5.2 Content Preview & Editor Restoration**
+- **Content Preview**: Restored preview editor with formatted display using `formatDraftContent()`
+- **Toggle Preview**: Show/hide preview button with professional styling and state management
+- **Content Editor**: Full-featured textarea with professional styling and placeholder text
+- **Character Count**: Real-time character count display (0 / 3000 characters)
+- **Reading Time**: Automatic reading time calculation based on word count
+- **Professional Layout**: Clean, card-based design with proper spacing and borders
+
+### **5.3 UI Consolidation & Redundancy Removal**
+- **Removed Context & Notes**: Eliminated redundant section (now handled by CopilotKit chat)
+- **Streamlined Layout**: Cleaner, more focused interface with better visual hierarchy
+- **Professional Styling**: Consistent LinkedIn branding throughout the interface
+- **Responsive Design**: Proper spacing, typography, and visual feedback
+- **Animation Integration**: Smooth hover effects and transitions for better UX
+
+## Resources
+
+- **Facebook Writer Reference**: `frontend/src/components/FacebookWriter/` ✅
+- **Backend API**: `backend/routers/linkedin.py` ✅
+- **Models**: `backend/models/linkedin_models.py` ✅
+- **Service**: `backend/services/linkedin_service.py` ✅
+- **Testing**: `backend/test_linkedin_endpoints.py` ✅
+
+## Current Implementation Status
+
+### ✅ Successfully Implemented
+- Complete LinkedIn Writer component structure
+- Professional API client with TypeScript interfaces
+- LinkedIn-specific utility functions and validation
+- Main LinkedIn Writer component with professional UI
+- PostHITL component for LinkedIn post generation
+- ArticleHITL component for LinkedIn article generation
+- CarouselHITL component for LinkedIn carousel generation
+- CopilotKit action registrations for all content types
+- Edit actions for content optimization
+- Successful TypeScript compilation and build
+
+### 🔄 Ready for Next Phase
+- UI polish and responsive design improvements
+- Advanced professional features enhancement
+- Testing and documentation
+- Performance optimization
+- Real-time editing preview implementation
+- Professional diff highlighting
+- Confirm/reject workflow
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 3215c16e..d34a10a2 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -8,6 +8,7 @@ import MainDashboard from './components/MainDashboard/MainDashboard';
import SEODashboard from './components/SEODashboard/SEODashboard';
import ContentPlanningDashboard from './components/ContentPlanningDashboard/ContentPlanningDashboard';
import FacebookWriter from './components/FacebookWriter/FacebookWriter';
+import LinkedInWriter from './components/LinkedInWriter/LinkedInWriter';
import { apiClient } from './api/client';
@@ -184,6 +185,7 @@ const App: React.FC = () => {
} />
} />
} />
+ } />
diff --git a/frontend/src/assets/README.md b/frontend/src/assets/README.md
new file mode 100644
index 00000000..e00e6078
--- /dev/null
+++ b/frontend/src/assets/README.md
@@ -0,0 +1,45 @@
+# Assets Directory
+
+This directory contains all static assets used throughout the ALwrity application.
+
+## Structure
+
+```
+src/assets/
+├── images/ # Image assets
+│ ├── alwrity_logo.png # ALwrity company logo
+│ └── AskAlwrity-min.ico # ALwrity Co-Pilot icon
+└── README.md # This file
+```
+
+## Usage
+
+### ALwrity Logo (`alwrity_logo.png`)
+- **Location**: `src/assets/images/alwrity_logo.png`
+- **Usage**: Company branding in headers, navigation, and branding elements
+- **Format**: PNG with transparency
+- **Size**: 188KB, optimized for web
+
+### ALwrity Co-Pilot Icon (`AskAlwrity-min.ico`)
+- **Location**: `src/assets/images/AskAlwrity-min.ico`
+- **Usage**: CopilotKit trigger button icon
+- **Format**: ICO format for optimal icon display
+- **Size**: 79KB
+
+## Import Examples
+
+```typescript
+// In components
+import alwrityLogo from '../../assets/images/alwrity_logo.png';
+import alwrityIcon from '../../assets/images/AskAlwrity-min.ico';
+
+// In CSS
+background-image: url('../../../assets/images/AskAlwrity-min.ico');
+```
+
+## Notes
+
+- All assets are optimized for web use
+- ICO format is used for the Co-Pilot icon to ensure crisp display at various sizes
+- PNG format is used for the logo to maintain transparency
+- Assets are organized by type for easy maintenance
diff --git a/frontend/src/assets/images/AskAlwrity-min.ico b/frontend/src/assets/images/AskAlwrity-min.ico
new file mode 100644
index 00000000..abaf82ce
Binary files /dev/null and b/frontend/src/assets/images/AskAlwrity-min.ico differ
diff --git a/frontend/src/assets/images/alwrity_logo.png b/frontend/src/assets/images/alwrity_logo.png
new file mode 100644
index 00000000..55ce65a0
Binary files /dev/null and b/frontend/src/assets/images/alwrity_logo.png differ
diff --git a/frontend/src/components/ContentPlanningDashboard/components/SystemStatusIndicator.tsx b/frontend/src/components/ContentPlanningDashboard/components/SystemStatusIndicator.tsx
index ccf8d685..82e003c5 100644
--- a/frontend/src/components/ContentPlanningDashboard/components/SystemStatusIndicator.tsx
+++ b/frontend/src/components/ContentPlanningDashboard/components/SystemStatusIndicator.tsx
@@ -279,13 +279,26 @@ const SystemStatusIndicator: React.FC = ({ className
color={getStatusColor(statusData?.status || 'unknown')}
sx={{ height: 22, fontSize: '0.70rem' }}
/>
- { e.stopPropagation(); fetchStatus(); fetchDetailedStats(); }}
- sx={{ ml: 0.5, color: 'rgba(255,255,255,0.9)' }}
+ sx={{
+ ml: 0.5,
+ color: 'rgba(255,255,255,0.9)',
+ cursor: 'pointer',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 24,
+ height: 24,
+ borderRadius: '50%',
+ '&:hover': {
+ backgroundColor: 'rgba(255,255,255,0.1)',
+ }
+ }}
>
-
+
diff --git a/frontend/src/components/FacebookWriter/FacebookWriter.tsx b/frontend/src/components/FacebookWriter/FacebookWriter.tsx
index 7ea700e1..bcf8d14f 100644
--- a/frontend/src/components/FacebookWriter/FacebookWriter.tsx
+++ b/frontend/src/components/FacebookWriter/FacebookWriter.tsx
@@ -253,7 +253,8 @@ const FacebookWriter: React.FC = () => {
{ title: '📚 Story', message: 'Use tool generateFacebookStory to create a Facebook Story script with tone and visuals.' },
{ title: '🎬 Reel script', message: 'Use tool generateFacebookReel to draft a 30-60 seconds fast-paced product demo reel with hook, scenes, and CTA.' },
{ title: '🖼️ Carousel', message: 'Use tool generateFacebookCarousel to create a 5-slide Product showcase carousel with a main caption and CTA.' },
- { title: '📅 Event', message: 'Use tool generateFacebookEvent to create a Virtual Webinar event description with title, highlights, and CTA.' }
+ { title: '📅 Event', message: 'Use tool generateFacebookEvent to create a Virtual Webinar event description with title, highlights, and CTA.' },
+ { title: 'ℹ️ Page About', message: 'Use tool generateFacebookPageAbout to create a comprehensive Facebook Page About section with business details and contact information.' }
];
const editSuggestions = [
{ title: '🙂 Make it casual', message: 'Use tool editFacebookDraft with operation Casual' },
diff --git a/frontend/src/components/LinkedInWriter/LinkedInWriter.tsx b/frontend/src/components/LinkedInWriter/LinkedInWriter.tsx
new file mode 100644
index 00000000..e2ee6288
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/LinkedInWriter.tsx
@@ -0,0 +1,370 @@
+import React from 'react';
+import { CopilotSidebar } from '@copilotkit/react-ui';
+import { useCopilotReadable, useCopilotAction } from '@copilotkit/react-core';
+import '@copilotkit/react-ui/styles.css';
+import './styles/alwrity-copilot.css';
+import RegisterLinkedInActions from './RegisterLinkedInActions';
+import RegisterLinkedInEditActions from './RegisterLinkedInEditActions';
+import { Header, ContentEditor, LoadingIndicator, WelcomeMessage } from './components';
+import { useLinkedInWriter } from './hooks/useLinkedInWriter';
+
+const useCopilotActionTyped = useCopilotAction as any;
+
+
+
+interface LinkedInWriterProps {
+ className?: string;
+}
+
+const LinkedInWriter: React.FC = ({ className = '' }) => {
+ const {
+ // State
+ draft,
+ context,
+ isGenerating,
+ isPreviewing,
+ livePreviewHtml,
+ pendingEdit,
+ loadingMessage,
+ currentAction,
+ chatHistory,
+ userPreferences,
+ currentSuggestions,
+ showPreferencesModal,
+ showContextModal,
+ showPreview,
+
+ // Setters
+ setDraft,
+ setIsPreviewing,
+ setLivePreviewHtml,
+ setPendingEdit,
+ setUserPreferences,
+ setShowPreferencesModal,
+ setShowContextModal,
+ setShowPreview,
+
+ // Handlers
+ handleDraftChange,
+ handleContextChange,
+ handleClear,
+ handleCopy,
+ handleClearHistory,
+
+ // Utilities
+ getHistoryLength,
+ savePreferences,
+ summarizeHistory
+ } = useLinkedInWriter();
+
+ // Handle preview changes
+ const handleConfirmChanges = () => {
+ if (pendingEdit) {
+ setDraft(pendingEdit.target);
+ }
+ setIsPreviewing(false);
+ setPendingEdit(null);
+ setLivePreviewHtml('');
+ };
+
+ const handleDiscardChanges = () => {
+ setIsPreviewing(false);
+ setPendingEdit(null);
+ setLivePreviewHtml('');
+ };
+
+ const handlePreviewToggle = () => {
+ setShowPreview(!showPreview);
+ };
+
+ const handlePreferencesChange = (prefs: Partial) => {
+ const updated = { ...userPreferences, ...prefs };
+ setUserPreferences(updated);
+ savePreferences(prefs);
+ };
+
+ // Share current draft and context with CopilotKit for better context awareness
+ useCopilotReadable({
+ description: 'Current LinkedIn content draft the user is editing',
+ value: draft,
+ categories: ['social', 'linkedin', 'draft']
+ });
+
+ useCopilotReadable({
+ description: 'User context and notes for LinkedIn content',
+ value: context,
+ categories: ['social', 'linkedin', 'context']
+ });
+
+ // Allow Copilot to update the draft directly
+ useCopilotActionTyped({
+ name: 'updateLinkedInDraft',
+ description: 'Replace the LinkedIn content draft with provided content',
+ parameters: [
+ { name: 'content', type: 'string', description: 'The full content to set', required: true }
+ ],
+ handler: async ({ content }: { content: string }) => {
+ setDraft(content);
+ return { success: true, message: 'Draft updated' };
+ }
+ });
+
+ // Let Copilot append text to the draft
+ useCopilotActionTyped({
+ name: 'appendToLinkedInDraft',
+ description: 'Append text to the current LinkedIn content draft',
+ parameters: [
+ { name: 'content', type: 'string', description: 'The text to append', required: true }
+ ],
+ handler: async ({ content }: { content: string }) => {
+ setDraft(prev => (prev ? `${prev}\n\n${content}` : content));
+ return { success: true, message: 'Text appended' };
+ }
+ });
+
+ // 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
+ const getIntelligentSuggestions = () => {
+ 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;
+
+ if (!hasContent) {
+ // Initial suggestions for content creation
+ return [
+ { 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.' }
+ ];
+ } 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 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' });
+ }
+
+ return refinementSuggestions;
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Main Content */}
+
+ {/* Loading Indicator */}
+
+
+ {/* Content Area */}
+ {draft || isGenerating ? (
+ /* Editor Panel - Show when there's content or generating */
+
+ ) : (
+ /* Welcome Message - Show when no content */
+
+ )}
+
+
+ {/* Register CopilotKit Actions */}
+
+
+
+ {/* CopilotKit Sidebar */}
+
{
+ const prefs = userPreferences;
+ const prefsLine = Object.keys(prefs).length ? `User preferences (remember and respect unless changed): ${JSON.stringify(prefs)}` : '';
+ const history = summarizeHistory();
+ const historyLine = history ? `Recent conversation (last 15 messages):\n${history}` : '';
+ const currentDraft = draft ? `Current draft content:\n${draft}` : 'No current draft content.';
+ const tone = prefs.tone || 'professional';
+ const industry = prefs.industry || 'Technology';
+ const audience = prefs.target_audience || 'professionals';
+
+ const guidance = `
+ You are ALwrity's LinkedIn Writing Assistant specializing in ${industry} content.
+
+ CRITICAL CONSTRAINTS:
+ - TONE: Always maintain a ${tone} tone throughout all content
+ - INDUSTRY: Focus specifically on ${industry} industry context and terminology
+ - AUDIENCE: Target content specifically for ${audience}
+ - QUALITY: Ensure all content meets LinkedIn professional standards
+
+ CURRENT CONTEXT:
+ ${currentDraft}
+
+ Available LinkedIn content tools:
+ - generateLinkedInPost: Create ${tone} LinkedIn posts for ${industry} ${audience}
+ - generateLinkedInArticle: Write ${tone} thought leadership articles about ${industry}
+ - generateLinkedInCarousel: Design ${tone} multi-slide carousels for ${industry} insights
+ - generateLinkedInVideoScript: Create ${tone} video scripts for ${industry} topics
+ - generateLinkedInCommentResponse: Draft ${tone} responses appropriate for ${industry}
+
+ DIRECT DRAFT ACTIONS:
+ - updateLinkedInDraft: Replace the entire draft with new content
+ - appendToLinkedInDraft: Add text to the existing draft
+ - editLinkedInDraft: Apply quick edits (Casual, Professional, TightenHook, AddCTA, Shorten, Lengthen) to the current draft
+
+ IMPORTANT: When refining or editing content, always reference the current draft above. If the user asks to refine their post, use the current draft content as the starting point. Never ask for content that already exists in the draft.
+
+ For quick edits, use editLinkedInDraft with the appropriate operation. This will show a live preview of changes before applying them.
+
+ Use user preferences, context, and conversation history to personalize all content.
+ Always respect the user's preferred ${tone} tone and ${industry} industry focus.
+ Always use the most appropriate tool for the user's request.`.trim();
+ return [prefsLine, historyLine, currentDraft, guidance, additional].filter(Boolean).join('\n\n');
+ }}
+ observabilityHooks={{
+ onChatExpanded: () => {
+ console.log('[LinkedIn Writer] Sidebar opened');
+ },
+ onMessageSent: (message: any) => {
+ const text = typeof message === 'string' ? message : (message?.content ?? '');
+ if (text) {
+ console.log('[LinkedIn Writer] User message tracked:', { content_length: text.length });
+ }
+ },
+ onFeedbackGiven: (id: string, type: string) => {
+ console.log('[LinkedIn Writer] Feedback given:', { id, type });
+ }
+ }}
+ />
+
+ );
+};
+
+export default LinkedInWriter;
diff --git a/frontend/src/components/LinkedInWriter/RegisterLinkedInActions.tsx b/frontend/src/components/LinkedInWriter/RegisterLinkedInActions.tsx
new file mode 100644
index 00000000..9012251e
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/RegisterLinkedInActions.tsx
@@ -0,0 +1,307 @@
+import React from 'react';
+import { useCopilotAction } from '@copilotkit/react-core';
+import { linkedInWriterApi, LinkedInPostRequest } from '../../services/linkedInWriterApi';
+import {
+ mapPostType,
+ mapTone,
+ mapIndustry,
+ mapSearchEngine,
+ readPrefs
+} from './utils/linkedInWriterUtils';
+import { PostHITL, ArticleHITL, CarouselHITL, VideoScriptHITL, CommentResponseHITL } from './components';
+
+const useCopilotActionTyped = useCopilotAction as any;
+
+const RegisterLinkedInActions: React.FC = () => {
+ // LinkedIn Post Generation
+ useCopilotActionTyped({
+ name: 'generateLinkedInPost',
+ description: 'Generate a professional LinkedIn post with industry insights and engagement optimization',
+ parameters: [
+ { name: 'topic', type: 'string', required: false },
+ { name: 'industry', type: 'string', required: false },
+ { name: 'post_type', type: 'string', required: false },
+ { name: 'tone', type: 'string', required: false },
+ { name: 'refine_existing', type: 'boolean', required: false, description: 'Whether to refine existing content instead of creating new' }
+ ],
+ handler: async (args: any) => {
+ const prefs = readPrefs();
+
+ // If refining existing content, use the current draft as context
+ let existingContent = '';
+ if (args?.refine_existing) {
+ // Get current draft from the page context
+ const textarea = document.querySelector('textarea') as HTMLTextAreaElement;
+ const currentDraft = textarea?.value || '';
+ if (currentDraft) {
+ existingContent = `\n\nREFINE THIS EXISTING CONTENT:\n${currentDraft}`;
+ }
+ }
+
+ const res = await linkedInWriterApi.generatePost({
+ topic: args?.topic || prefs.topic || 'AI transformation in business',
+ industry: mapIndustry(args?.industry || prefs.industry),
+ post_type: mapPostType(args?.post_type || prefs.post_type),
+ tone: mapTone(args?.tone || prefs.tone),
+ target_audience: args?.target_audience || prefs.target_audience || 'Business leaders and professionals',
+ key_points: args?.key_points || prefs.key_points || [],
+ include_hashtags: args?.include_hashtags ?? (prefs.include_hashtags ?? true),
+ include_call_to_action: args?.include_call_to_action ?? (prefs.include_call_to_action ?? true),
+ research_enabled: args?.research_enabled ?? (prefs.research_enabled ?? true),
+ search_engine: mapSearchEngine(args?.search_engine || prefs.search_engine),
+ max_length: args?.max_length || prefs.max_length || 2000
+ });
+
+ if (res.success && res.data) {
+ const content = res.data.content;
+ const hashtags = res.data.hashtags?.map(h => h.hashtag).join(' ') || '';
+ const cta = res.data.call_to_action || '';
+
+ let fullContent = content;
+ if (hashtags) fullContent += `\n\n${hashtags}`;
+ if (cta) fullContent += `\n\n${cta}`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: fullContent }));
+ return { success: true, content: fullContent };
+ }
+ return { success: false, message: res.error || 'Failed to generate LinkedIn post' };
+ }
+ });
+
+ // LinkedIn Article Generation
+ useCopilotActionTyped({
+ name: 'generateLinkedInArticle',
+ description: 'Generate a comprehensive LinkedIn article with thought leadership content',
+ parameters: [
+ { name: 'topic', type: 'string', required: false },
+ { name: 'industry', type: 'string', required: false },
+ { name: 'tone', type: 'string', required: false },
+ { name: 'word_count', type: 'number', required: false }
+ ],
+ handler: async (args: any) => {
+ const prefs = readPrefs();
+ const res = await linkedInWriterApi.generateArticle({
+ topic: args?.topic || prefs.topic || 'Digital transformation strategies',
+ industry: mapIndustry(args?.industry || prefs.industry),
+ tone: mapTone(args?.tone || prefs.tone),
+ target_audience: args?.target_audience || prefs.target_audience || 'Industry professionals and executives',
+ key_sections: args?.key_sections || prefs.key_sections || [],
+ include_images: args?.include_images ?? (prefs.include_images ?? true),
+ seo_optimization: args?.seo_optimization ?? (prefs.seo_optimization ?? true),
+ research_enabled: args?.research_enabled ?? (prefs.research_enabled ?? true),
+ search_engine: mapSearchEngine(args?.search_engine || prefs.search_engine),
+ word_count: args?.word_count || prefs.word_count || 1500
+ });
+
+ if (res.success && res.data) {
+ const content = `# ${res.data.title}\n\n${res.data.content}`;
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
+ return { success: true, content };
+ }
+ return { success: false, message: res.error || 'Failed to generate LinkedIn article' };
+ }
+ });
+
+ // LinkedIn Carousel Generation
+ useCopilotActionTyped({
+ name: 'generateLinkedInCarousel',
+ description: 'Generate a LinkedIn carousel with multiple slides for visual content',
+ parameters: [
+ { name: 'topic', type: 'string', required: false },
+ { name: 'industry', type: 'string', required: false },
+ { name: 'slide_count', type: 'number', required: false }
+ ],
+ handler: async (args: any) => {
+ const prefs = readPrefs();
+ const res = await linkedInWriterApi.generateCarousel({
+ topic: args?.topic || prefs.topic || 'Professional development tips',
+ industry: mapIndustry(args?.industry || prefs.industry),
+ slide_count: args?.slide_count || prefs.slide_count || 8,
+ tone: mapTone(args?.tone || prefs.tone),
+ target_audience: args?.target_audience || prefs.target_audience || 'Professionals seeking growth',
+ key_takeaways: args?.key_takeaways || prefs.key_takeaways || [],
+ include_cover_slide: args?.include_cover_slide ?? (prefs.include_cover_slide ?? true),
+ include_cta_slide: args?.include_cta_slide ?? (prefs.include_cta_slide ?? true),
+ visual_style: args?.visual_style || prefs.visual_style || 'modern'
+ });
+
+ if (res.success && res.data) {
+ let content = `# ${res.data.title}\n\n`;
+ res.data.slides.forEach((slide, index) => {
+ content += `## Slide ${index + 1}: ${slide.title}\n\n${slide.content}\n\n`;
+ });
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
+ return { success: true, content };
+ }
+ return { success: false, message: res.error || 'Failed to generate LinkedIn carousel' };
+ }
+ });
+
+ // LinkedIn Video Script Generation
+ useCopilotActionTyped({
+ name: 'generateLinkedInVideoScript',
+ description: 'Generate a LinkedIn video script with hook, content, and captions',
+ parameters: [
+ { name: 'topic', type: 'string', required: false },
+ { name: 'industry', type: 'string', required: false },
+ { name: 'video_length', type: 'number', required: false }
+ ],
+ handler: async (args: any) => {
+ const prefs = readPrefs();
+ const res = await linkedInWriterApi.generateVideoScript({
+ topic: args?.topic || prefs.topic || 'Professional networking tips',
+ industry: mapIndustry(args?.industry || prefs.industry),
+ video_length: args?.video_length || prefs.video_length || 60,
+ tone: mapTone(args?.tone || prefs.tone),
+ target_audience: args?.target_audience || prefs.target_audience || 'Professional networkers',
+ key_messages: args?.key_messages || prefs.key_messages || [],
+ include_hook: args?.include_hook ?? (prefs.include_hook ?? true),
+ include_captions: args?.include_captions ?? (prefs.include_captions ?? true)
+ });
+
+ if (res.success && res.data) {
+ let content = `# Video Script: ${args?.topic || 'Professional Content'}\n\n`;
+ content += `## Hook\n${res.data.hook}\n\n`;
+ content += `## Main Content\n`;
+ res.data.main_content.forEach((scene, index) => {
+ content += `### Scene ${index + 1} (${scene.duration || '30s'})\n${scene.content}\n\n`;
+ });
+ content += `## Conclusion\n${res.data.conclusion}\n\n`;
+ content += `## Video Description\n${res.data.video_description}\n\n`;
+
+ if (res.data.captions) {
+ content += `## Captions\n${res.data.captions.join('\n')}\n\n`;
+ }
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
+ return { success: true, content };
+ }
+ return { success: false, message: res.error || 'Failed to generate LinkedIn video script' };
+ }
+ });
+
+ // LinkedIn Comment Response Generation
+ useCopilotActionTyped({
+ name: 'generateLinkedInCommentResponse',
+ description: 'Generate a professional response to a LinkedIn comment',
+ parameters: [
+ { name: 'original_post', type: 'string', required: false },
+ { name: 'comment', type: 'string', required: false },
+ { name: 'response_type', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const prefs = readPrefs();
+ const res = await linkedInWriterApi.generateCommentResponse({
+ original_post: args?.original_post || prefs.original_post || 'Sample LinkedIn post content',
+ comment: args?.comment || prefs.comment || 'Sample comment to respond to',
+ response_type: args?.response_type || prefs.response_type || 'professional',
+ tone: mapTone(args?.tone || prefs.tone),
+ include_question: args?.include_question ?? (prefs.include_question ?? false),
+ brand_voice: args?.brand_voice || prefs.brand_voice
+ });
+
+ if (res.success && res.response) {
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: res.response }));
+ return { success: true, content: res.response };
+ }
+ return { success: false, message: res.error || 'Failed to generate LinkedIn comment response' };
+ }
+ });
+
+ // LinkedIn Profile Optimization
+ useCopilotActionTyped({
+ name: 'optimizeLinkedInProfile',
+ description: 'Optimize LinkedIn profile sections for better professional visibility',
+ parameters: [
+ { name: 'current_headline', type: 'string', required: false },
+ { name: 'industry', type: 'string', required: false },
+ { name: 'experience_level', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const res = await linkedInWriterApi.optimizeProfile({
+ current_headline: args?.current_headline || 'Professional',
+ industry: mapIndustry(args?.industry),
+ experience_level: args?.experience_level || 'mid-level',
+ target_role: args?.target_role,
+ key_skills: args?.key_skills || []
+ });
+
+ if (res.success && res.data) {
+ let content = `# LinkedIn Profile Optimization\n\n`;
+ content += `## Optimized Headline\n${res.data.headline}\n\n`;
+ content += `## About Section\n${res.data.about}\n\n`;
+ content += `## Key Skills\n${res.data.skills?.join(', ')}\n\n`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
+ return { success: true, content };
+ }
+ return { success: false, message: res.error || 'Failed to optimize LinkedIn profile' };
+ }
+ });
+
+ // LinkedIn Poll Generation
+ useCopilotActionTyped({
+ name: 'generateLinkedInPoll',
+ description: 'Generate an engaging LinkedIn poll with professional questions',
+ parameters: [
+ { name: 'topic', type: 'string', required: false },
+ { name: 'industry', type: 'string', required: false },
+ { name: 'poll_type', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const res = await linkedInWriterApi.generatePoll({
+ topic: args?.topic || 'Professional development',
+ industry: mapIndustry(args?.industry),
+ poll_type: args?.poll_type || 'professional',
+ target_audience: args?.target_audience || 'Industry professionals',
+ question_count: args?.question_count || 1
+ });
+
+ if (res.success && res.data) {
+ let content = `# LinkedIn Poll: ${res.data.question}\n\n`;
+ content += `## Options\n`;
+ res.data.options?.forEach((option: string, index: number) => {
+ content += `${index + 1}. ${option}\n`;
+ });
+ content += `\n## Context\n${res.data.context || ''}\n\n`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
+ return { success: true, content };
+ }
+ return { success: false, message: res.error || 'Failed to generate LinkedIn poll' };
+ }
+ });
+
+ // LinkedIn Company Update Generation
+ useCopilotActionTyped({
+ name: 'generateLinkedInCompanyUpdate',
+ description: 'Generate a professional company update for LinkedIn',
+ parameters: [
+ { name: 'company_name', type: 'string', required: false },
+ { name: 'update_type', type: 'string', required: false },
+ { name: 'industry', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const res = await linkedInWriterApi.generateCompanyUpdate({
+ company_name: args?.company_name || 'Your Company',
+ update_type: args?.update_type || 'achievement',
+ industry: mapIndustry(args?.industry),
+ announcement: args?.announcement,
+ target_audience: args?.target_audience || 'Industry professionals and clients',
+ include_metrics: args?.include_metrics ?? true
+ });
+
+ if (res.success && res.data) {
+ const content = res.data.content;
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
+ return { success: true, content };
+ }
+ return { success: false, message: res.error || 'Failed to generate LinkedIn company update' };
+ }
+ });
+
+ return null;
+};
+
+export default RegisterLinkedInActions;
diff --git a/frontend/src/components/LinkedInWriter/RegisterLinkedInEditActions.tsx b/frontend/src/components/LinkedInWriter/RegisterLinkedInEditActions.tsx
new file mode 100644
index 00000000..38489fef
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/RegisterLinkedInEditActions.tsx
@@ -0,0 +1,161 @@
+import React from 'react';
+import { useCopilotAction } from '@copilotkit/react-core';
+
+const useCopilotActionTyped = useCopilotAction as any;
+
+const RegisterLinkedInEditActions: React.FC = () => {
+ // Professionalize Content
+ useCopilotActionTyped({
+ name: 'professionalizeLinkedInContent',
+ description: 'Make LinkedIn content more professional and industry-appropriate',
+ parameters: [
+ { name: 'content', type: 'string', required: false },
+ { name: 'industry', type: 'string', required: false },
+ { name: 'target_audience', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ // This would integrate with a backend endpoint for content professionalization
+ const content = args?.content || '';
+ const industry = args?.industry || 'Technology';
+ const targetAudience = args?.target_audience || 'Professionals';
+
+ // For now, return a placeholder response
+ const professionalizedContent = `[Professionalized version of your content for ${industry} industry targeting ${targetAudience}]\n\n${content}`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: professionalizedContent } }));
+ return { success: true, content: professionalizedContent };
+ }
+ });
+
+ // Optimize for Engagement
+ useCopilotActionTyped({
+ name: 'optimizeLinkedInEngagement',
+ description: 'Optimize LinkedIn content for better engagement and reach',
+ parameters: [
+ { name: 'content', type: 'string', required: false },
+ { name: 'content_type', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const content = args?.content || '';
+ const contentType = args?.content_type || 'post';
+
+ // Placeholder for engagement optimization
+ const optimizedContent = `[Engagement-optimized ${contentType}]\n\n${content}\n\n#ProfessionalDevelopment #Networking #IndustryInsights`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: optimizedContent } }));
+ return { success: true, content: optimizedContent };
+ }
+ });
+
+ // Add Professional Hashtags
+ useCopilotActionTyped({
+ name: 'addLinkedInHashtags',
+ description: 'Add relevant professional hashtags to LinkedIn content',
+ parameters: [
+ { name: 'content', type: 'string', required: false },
+ { name: 'industry', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const content = args?.content || '';
+ const industry = args?.industry || 'Technology';
+
+ // Placeholder for hashtag addition
+ const hashtags = '#ProfessionalDevelopment #Networking #IndustryInsights #CareerGrowth';
+ const contentWithHashtags = `${content}\n\n${hashtags}`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: contentWithHashtags } }));
+ return { success: true, content: contentWithHashtags };
+ }
+ });
+
+ // Adjust Tone
+ useCopilotActionTyped({
+ name: 'adjustLinkedInTone',
+ description: 'Adjust the tone of LinkedIn content to be more professional, conversational, or authoritative',
+ parameters: [
+ { name: 'content', type: 'string', required: false },
+ { name: 'target_tone', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const content = args?.content || '';
+ const targetTone = args?.target_tone || 'professional';
+
+ // Placeholder for tone adjustment
+ const adjustedContent = `[Content adjusted to ${targetTone} tone]\n\n${content}`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: adjustedContent } }));
+ return { success: true, content: adjustedContent };
+ }
+ });
+
+ // Expand Content
+ useCopilotActionTyped({
+ name: 'expandLinkedInContent',
+ description: 'Expand LinkedIn content with more details and insights',
+ parameters: [
+ { name: 'content', type: 'string', required: false },
+ { name: 'expansion_type', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const content = args?.content || '';
+ const expansionType = args?.expansion_type || 'insights';
+
+ // Placeholder for content expansion
+ const expandedContent = `${content}\n\n[Additional ${expansionType} and context added here]`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: expandedContent } }));
+ return { success: true, content: expandedContent };
+ }
+ });
+
+ // Condense Content
+ useCopilotActionTyped({
+ name: 'condenseLinkedInContent',
+ description: 'Condense LinkedIn content to be more concise and impactful',
+ parameters: [
+ { name: 'content', type: 'string', required: false },
+ { name: 'target_length', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const content = args?.content || '';
+ const targetLength = args?.target_length || 'short';
+
+ // Placeholder for content condensation
+ const condensedContent = `[Condensed to ${targetLength} format]\n\n${content.substring(0, Math.min(content.length, 500))}...`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: condensedContent } }));
+ return { success: true, content: condensedContent };
+ }
+ });
+
+ // Add Call to Action
+ useCopilotActionTyped({
+ name: 'addLinkedInCallToAction',
+ description: 'Add a professional call to action to LinkedIn content',
+ parameters: [
+ { name: 'content', type: 'string', required: false },
+ { name: 'cta_type', type: 'string', required: false }
+ ],
+ handler: async (args: any) => {
+ const content = args?.content || '';
+ const ctaType = args?.cta_type || 'engagement';
+
+ const ctaOptions = {
+ engagement: 'What are your thoughts on this? Share your experience in the comments below!',
+ networking: 'Let\'s connect if you\'re interested in discussing this further.',
+ learning: 'Would you like to learn more about this topic? Drop a comment or DM me.',
+ collaboration: 'Interested in collaborating on similar projects? Let\'s connect!'
+ };
+
+ const cta = ctaOptions[ctaType as keyof typeof ctaOptions] || ctaOptions.engagement;
+ const contentWithCTA = `${content}\n\n${cta}`;
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', { detail: { target: contentWithCTA } }));
+ return { success: true, content: contentWithCTA };
+ }
+ });
+
+ return null;
+};
+
+export default RegisterLinkedInEditActions;
diff --git a/frontend/src/components/LinkedInWriter/components/ArticleHITL.tsx b/frontend/src/components/LinkedInWriter/components/ArticleHITL.tsx
new file mode 100644
index 00000000..8243aeb5
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/ArticleHITL.tsx
@@ -0,0 +1,274 @@
+import React from 'react';
+import { linkedInWriterApi, LinkedInArticleRequest } from '../../../services/linkedInWriterApi';
+import {
+ readPrefs,
+ writePrefs,
+ logAssistant,
+ mapTone,
+ mapIndustry,
+ mapSearchEngine,
+ getPersonalizedPlaceholder,
+ VALID_TONES,
+ VALID_INDUSTRIES,
+ VALID_SEARCH_ENGINES
+} from '../utils/linkedInWriterUtils';
+
+interface ArticleHITLProps {
+ args: any;
+ respond: (data: any) => void;
+}
+
+const ArticleHITL: React.FC = ({ args, respond }) => {
+ const prefs = React.useMemo(() => readPrefs(), []);
+ const [form, setForm] = React.useState({
+ topic: args.topic ?? prefs.topic ?? '',
+ target_audience: args.target_audience ?? prefs.target_audience ?? '',
+ tone: args.tone ?? prefs.tone ?? 'professional',
+ industry: args.industry ?? prefs.industry ?? 'technology',
+ key_sections: args.key_sections ?? prefs.key_sections ?? [],
+ include_images: args.include_images ?? (prefs.include_images ?? true),
+ seo_optimization: args.seo_optimization ?? (prefs.seo_optimization ?? true),
+ research_enabled: args.research_enabled ?? (prefs.research_enabled ?? true),
+ word_count: args.word_count ?? (prefs.word_count ?? 800),
+ search_engine: args.search_engine ?? (prefs.search_engine ?? 'google')
+ });
+
+ const [isLoading, setIsLoading] = React.useState(false);
+
+ const run = async () => {
+ try {
+ setIsLoading(true);
+
+ // Emit loading start event
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
+ detail: {
+ action: 'Generating LinkedIn Article',
+ message: `Creating a comprehensive LinkedIn article about "${form.topic}" for your ${form.target_audience}. This will include ${form.key_sections?.length || 'several'} key sections and take approximately ${Math.round(form.word_count / 200)} minutes to read.`
+ }
+ }));
+
+ logAssistant('Starting LinkedIn article generation...');
+
+ // Read user preferences
+ const prefs = readPrefs();
+ if (prefs) {
+ form.tone = prefs.tone || form.tone;
+ form.industry = prefs.industry || form.industry;
+ }
+
+ // Normalize and map enum values
+ const request: LinkedInArticleRequest = {
+ topic: form.topic,
+ target_audience: form.target_audience,
+ tone: mapTone(form.tone),
+ industry: mapIndustry(form.industry),
+ key_sections: form.key_sections,
+ include_images: form.include_images,
+ seo_optimization: form.seo_optimization,
+ research_enabled: form.research_enabled,
+ word_count: form.word_count,
+ search_engine: mapSearchEngine(form.search_engine)
+ };
+
+ const res = await linkedInWriterApi.generateArticle(request);
+
+ // Write preferences
+ writePrefs({
+ tone: form.tone,
+ industry: form.industry,
+ target_audience: form.target_audience,
+ key_sections: form.key_sections,
+ include_images: form.include_images,
+ seo_optimization: form.seo_optimization,
+ research_enabled: form.research_enabled,
+ word_count: form.word_count,
+ search_engine: form.search_engine
+ });
+
+ logAssistant('LinkedIn article generated successfully');
+
+ // Update draft content
+ if (res.data) {
+ const content = `# ${res.data.title}\n\n${res.data.content}`;
+
+ // Emit loading end event
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd', { detail: {} }));
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', {
+ detail: content
+ }));
+
+ respond({
+ success: true,
+ article: res.data.content,
+ title: res.data.title,
+ word_count: res.data.word_count,
+ reading_time: res.data.reading_time
+ });
+ } else {
+ throw new Error('No data received from API');
+ }
+
+ } catch (error) {
+ console.error('LinkedIn Article Generation Error:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+
+ // Emit loading end event with error
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd', {
+ detail: { error: errorMessage }
+ }));
+
+ logAssistant(`Error generating LinkedIn article: ${errorMessage}`);
+ respond({
+ success: false,
+ error: errorMessage
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
Generate LinkedIn Article
+
+
+ Article Topic *
+ setForm({ ...form, topic: e.target.value })}
+ placeholder={getPersonalizedPlaceholder('article', 'topic', prefs)}
+ required
+ />
+
+
+
+ Target Audience
+ setForm({ ...form, target_audience: e.target.value })}
+ placeholder={getPersonalizedPlaceholder('article', 'target_audience', prefs)}
+ />
+
+
+
+ Tone
+ setForm({ ...form, tone: e.target.value })}
+ >
+ {VALID_TONES.map(tone => (
+
+ {tone.charAt(0).toUpperCase() + tone.slice(1)}
+
+ ))}
+
+
+
+
+ Industry
+ setForm({ ...form, industry: e.target.value })}
+ >
+ {VALID_INDUSTRIES.map(industry => (
+
+ {industry.charAt(0).toUpperCase() + industry.slice(1)}
+
+ ))}
+
+
+
+
+ Key Sections to Cover
+
+
+
+ Word Count
+ setForm({ ...form, word_count: parseInt(e.target.value) })}
+ >
+ 500 words (Quick read)
+ 800 words (Standard)
+ 1200 words (Detailed)
+ 1500 words (Comprehensive)
+
+
+
+
+
+ setForm({ ...form, include_images: e.target.checked })}
+ />
+ Include relevant images and visuals
+
+
+
+
+
+ setForm({ ...form, seo_optimization: e.target.checked })}
+ />
+ Enable SEO optimization
+
+
+
+
+
+ setForm({ ...form, research_enabled: e.target.checked })}
+ />
+ Enable research and fact-checking
+
+
+
+
+ Research Source
+ setForm({ ...form, search_engine: e.target.value })}
+ >
+ {VALID_SEARCH_ENGINES.map(engine => (
+
+ {engine.charAt(0).toUpperCase() + engine.slice(1)}
+
+ ))}
+
+
+
+
+
+ {isLoading ? 'Generating Article...' : 'Generate Article'}
+
+
+
+ );
+};
+
+export default ArticleHITL;
diff --git a/frontend/src/components/LinkedInWriter/components/CarouselHITL.tsx b/frontend/src/components/LinkedInWriter/components/CarouselHITL.tsx
new file mode 100644
index 00000000..a6f60cbf
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/CarouselHITL.tsx
@@ -0,0 +1,262 @@
+import React from 'react';
+import { linkedInWriterApi, LinkedInCarouselRequest } from '../../../services/linkedInWriterApi';
+import {
+ readPrefs,
+ writePrefs,
+ logAssistant,
+ mapTone,
+ mapIndustry,
+ getPersonalizedPlaceholder,
+ VALID_TONES,
+ VALID_INDUSTRIES
+} from '../utils/linkedInWriterUtils';
+
+interface CarouselHITLProps {
+ args: any;
+ respond: (data: any) => void;
+}
+
+const CarouselHITL: React.FC = ({ args, respond }) => {
+ const prefs = React.useMemo(() => readPrefs(), []);
+ const [form, setForm] = React.useState({
+ topic: args.topic ?? prefs.topic ?? '',
+ target_audience: args.target_audience ?? prefs.target_audience ?? '',
+ tone: args.tone ?? prefs.tone ?? 'professional',
+ industry: args.industry ?? prefs.industry ?? 'technology',
+ slide_count: args.slide_count ?? (prefs.slide_count ?? 5),
+ key_takeaways: args.key_takeaways ?? (prefs.key_takeaways ?? []),
+ include_cover_slide: args.include_cover_slide ?? (prefs.include_cover_slide ?? true),
+ include_cta_slide: args.include_cta_slide ?? (prefs.include_cta_slide ?? true),
+ visual_style: args.visual_style ?? (prefs.visual_style ?? 'professional')
+ });
+
+ const [isLoading, setIsLoading] = React.useState(false);
+
+ const run = async () => {
+ try {
+ setIsLoading(true);
+
+ // Emit loading start event
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
+ detail: {
+ action: 'Generating LinkedIn Carousel',
+ message: `Creating a ${form.slide_count}-slide LinkedIn carousel about "${form.topic}". This visual content will engage your ${form.target_audience} with a ${form.visual_style} design approach.`
+ }
+ }));
+
+ logAssistant('Starting LinkedIn carousel generation...');
+
+ // Read user preferences
+ const prefs = readPrefs();
+ if (prefs) {
+ form.tone = prefs.tone || form.tone;
+ form.industry = prefs.industry || form.industry;
+ }
+
+ // Normalize and map enum values
+ const request: LinkedInCarouselRequest = {
+ topic: form.topic,
+ target_audience: form.target_audience,
+ tone: mapTone(form.tone),
+ industry: mapIndustry(form.industry),
+ slide_count: form.slide_count,
+ key_takeaways: form.key_takeaways,
+ include_cover_slide: form.include_cover_slide,
+ include_cta_slide: form.include_cta_slide,
+ visual_style: form.visual_style
+ };
+
+ const res = await linkedInWriterApi.generateCarousel(request);
+
+ // Write preferences
+ writePrefs({
+ tone: form.tone,
+ industry: form.industry,
+ target_audience: form.target_audience,
+ slide_count: form.slide_count,
+ key_takeaways: form.key_takeaways,
+ include_cover_slide: form.include_cover_slide,
+ include_cta_slide: form.include_cta_slide,
+ visual_style: form.visual_style
+ });
+
+ logAssistant('LinkedIn carousel generated successfully');
+
+ // Update draft content
+ if (res.data) {
+ let content = `# ${res.data.title}\n\n`;
+ res.data.slides.forEach((slide, index) => {
+ content += `## Slide ${index + 1}: ${slide.title}\n\n${slide.content}\n\n`;
+ });
+
+ // Emit loading end event
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd', { detail: {} }));
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', {
+ detail: content
+ }));
+
+ respond({
+ success: true,
+ carousel_content: content,
+ title: res.data.title,
+ slide_count: res.data.slides.length
+ });
+ } else {
+ throw new Error('No data received from API');
+ }
+
+ } catch (error) {
+ console.error('LinkedIn Carousel Generation Error:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+
+ // Emit loading end event with error
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd', {
+ detail: { error: errorMessage }
+ }));
+
+ logAssistant(`Error generating LinkedIn carousel: ${errorMessage}`);
+ respond({
+ success: false,
+ error: errorMessage
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
Generate LinkedIn Carousel
+
+
+ Carousel Topic *
+ setForm({ ...form, topic: e.target.value })}
+ placeholder={getPersonalizedPlaceholder('carousel', 'topic', prefs)}
+ required
+ />
+
+
+
+ Target Audience
+ setForm({ ...form, target_audience: e.target.value })}
+ placeholder={getPersonalizedPlaceholder('carousel', 'target_audience', prefs)}
+ />
+
+
+
+ Tone
+ setForm({ ...form, tone: e.target.value })}
+ >
+ {VALID_TONES.map(tone => (
+
+ {tone.charAt(0).toUpperCase() + tone.slice(1)}
+
+ ))}
+
+
+
+
+ Industry
+ setForm({ ...form, industry: e.target.value })}
+ >
+ {VALID_INDUSTRIES.map(industry => (
+
+ {industry.charAt(0).toUpperCase() + industry.slice(1)}
+
+ ))}
+
+
+
+
+ Number of Slides
+ setForm({ ...form, slide_count: parseInt(e.target.value) })}
+ >
+ 3 slides (Quick overview)
+ 5 slides (Standard)
+ 7 slides (Detailed)
+ 10 slides (Comprehensive)
+
+
+
+
+ Key Takeaways
+
+
+
+ Visual Style
+ setForm({ ...form, visual_style: e.target.value })}
+ >
+ Professional
+ Modern
+ Minimalist
+ Bold & Colorful
+ Elegant
+
+
+
+
+
+ setForm({ ...form, include_cover_slide: e.target.checked })}
+ />
+ Include cover slide
+
+
+
+
+
+ setForm({ ...form, include_cta_slide: e.target.checked })}
+ />
+ Include call-to-action slide
+
+
+
+
+
+
+
+ {isLoading ? 'Generating Carousel...' : 'Generate Carousel'}
+
+
+
+ );
+};
+
+export default CarouselHITL;
diff --git a/frontend/src/components/LinkedInWriter/components/CommentResponseHITL.tsx b/frontend/src/components/LinkedInWriter/components/CommentResponseHITL.tsx
new file mode 100644
index 00000000..b0dc9bd6
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/CommentResponseHITL.tsx
@@ -0,0 +1,195 @@
+import React from 'react';
+import { linkedInWriterApi, LinkedInCommentResponseRequest } from '../../../services/linkedInWriterApi';
+import {
+ readPrefs,
+ writePrefs,
+ logAssistant,
+ mapTone,
+ getPersonalizedPlaceholder,
+ VALID_TONES,
+ VALID_RESPONSE_TYPES
+} from '../utils/linkedInWriterUtils';
+
+interface CommentResponseHITLProps {
+ args: any;
+ respond: (data: any) => void;
+}
+
+const CommentResponseHITL: React.FC = ({ args, respond }) => {
+ const prefs = React.useMemo(() => readPrefs(), []);
+ const [form, setForm] = React.useState({
+ original_post: args.original_post ?? prefs.original_post ?? '',
+ comment: args.comment ?? prefs.comment ?? '',
+ response_type: args.response_type ?? (prefs.response_type ?? 'professional'),
+ tone: args.tone ?? (prefs.tone ?? 'professional'),
+ include_question: args.include_question ?? (prefs.include_question ?? false),
+ brand_voice: args.brand_voice ?? (prefs.brand_voice ?? '')
+ });
+
+ const [isLoading, setIsLoading] = React.useState(false);
+
+ const run = async () => {
+ try {
+ setIsLoading(true);
+
+ // Emit loading start event
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
+ detail: {
+ action: 'Generating LinkedIn Comment Response',
+ message: `Creating a ${form.response_type} response to a LinkedIn comment. This will maintain a ${form.tone} tone while providing valuable engagement.`
+ }
+ }));
+
+ logAssistant('Starting LinkedIn comment response generation...');
+
+ // Read user preferences
+ const prefs = readPrefs();
+ if (prefs) {
+ form.tone = prefs.tone || form.tone;
+ }
+
+ // Normalize and map enum values
+ const request: LinkedInCommentResponseRequest = {
+ original_post: form.original_post,
+ comment: form.comment,
+ response_type: form.response_type as 'professional' | 'appreciative' | 'clarifying' | 'disagreement' | 'value_add',
+ tone: mapTone(form.tone),
+ include_question: form.include_question,
+ brand_voice: form.brand_voice
+ };
+
+ const res = await linkedInWriterApi.generateCommentResponse(request);
+
+ // Write preferences
+ writePrefs({
+ tone: form.tone,
+ response_type: form.response_type,
+ include_question: form.include_question,
+ brand_voice: form.brand_voice
+ });
+
+ logAssistant('LinkedIn comment response generated successfully');
+
+ // Update draft content
+ if (res.response) {
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', {
+ detail: res.response
+ }));
+
+ respond({
+ success: true,
+ response: res.response,
+ alternative_responses: res.alternative_responses || [],
+ tone_analysis: res.tone_analysis
+ });
+ } else {
+ throw new Error('No response received from API');
+ }
+
+ } catch (error) {
+ console.error('LinkedIn Comment Response Generation Error:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+ logAssistant(`Error generating LinkedIn comment response: ${errorMessage}`);
+ respond({
+ success: false,
+ error: errorMessage
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
Generate LinkedIn Comment Response
+
+
+ Original Post Content *
+
+
+
+ Comment to Respond To *
+
+
+
+ Response Type
+ setForm({ ...form, response_type: e.target.value })}
+ >
+ {VALID_RESPONSE_TYPES.map(type => (
+
+ {type.charAt(0).toUpperCase() + type.slice(1)}
+
+ ))}
+
+
+
+
+ Tone
+ setForm({ ...form, tone: e.target.value })}
+ >
+ {VALID_TONES.map(tone => (
+
+ {tone.charAt(0).toUpperCase() + tone.slice(1)}
+
+ ))}
+
+
+
+
+ Brand Voice (Optional)
+ setForm({ ...form, brand_voice: e.target.value })}
+ placeholder={getPersonalizedPlaceholder('comment', 'brand_voice', prefs)}
+ />
+
+
+
+
+ setForm({ ...form, include_question: e.target.checked })}
+ />
+ Include a question to encourage engagement
+
+
+
+
+
+ {isLoading ? 'Generating Response...' : 'Generate Response'}
+
+
+
+ );
+};
+
+export default CommentResponseHITL;
diff --git a/frontend/src/components/LinkedInWriter/components/ContentEditor.tsx b/frontend/src/components/LinkedInWriter/components/ContentEditor.tsx
new file mode 100644
index 00000000..5b32bc7a
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/ContentEditor.tsx
@@ -0,0 +1,226 @@
+import React, { useEffect } from 'react';
+import { formatDraftContent, diffMarkup } from '../utils/contentFormatters';
+
+interface ContentEditorProps {
+ isPreviewing: boolean;
+ pendingEdit: { src: string; target: string } | null;
+ livePreviewHtml: string;
+ draft: string;
+ showPreview: boolean;
+ isGenerating: boolean;
+ loadingMessage: string;
+ onConfirmChanges: () => void;
+ onDiscardChanges: () => void;
+ onDraftChange: (value: string) => void;
+ onPreviewToggle: () => void;
+}
+
+export const ContentEditor: React.FC = ({
+ isPreviewing,
+ pendingEdit,
+ livePreviewHtml,
+ draft,
+ showPreview,
+ isGenerating,
+ loadingMessage,
+ onConfirmChanges,
+ onDiscardChanges,
+ onDraftChange,
+ onPreviewToggle
+}) => {
+ // Auto-show preview when content is generated
+ useEffect(() => {
+ if (draft && !showPreview) {
+ onPreviewToggle();
+ }
+ }, [draft, showPreview, onPreviewToggle]);
+
+ return (
+
+ {/* Predictive Diff Preview - Show when there are pending changes */}
+ {isPreviewing && pendingEdit && (
+
+
+
Preview Changes
+
+
+ Confirm Changes
+
+
+ Discard
+
+
+
+
+
+ )}
+
+ {/* Full Width Content Preview */}
+
+ {/* Content Preview */}
+ {showPreview && (
+
+
+
LinkedIn Content Preview
+
+
+ {draft.split(/\s+/).length} words • {Math.ceil(draft.split(/\s+/).length / 200)} min read
+
+
+ {showPreview ? 'Hide Preview' : 'Show Preview'}
+
+
+
+
+ {/* Loading State */}
+ {isGenerating && (
+
+
+
+ {loadingMessage || 'Generating LinkedIn content...'}
+
+
+ Crafting professional content tailored to your industry and audience...
+
+
+
+ )}
+
+ {/* Content Display */}
+
+ {draft ? (
+
+ ) : (
+
+ Content will appear here when generated. Use the AI assistant to create your LinkedIn content.
+
+ )}
+
+
+
+ )}
+
+
+ );
+};
diff --git a/frontend/src/components/LinkedInWriter/components/Header.tsx b/frontend/src/components/LinkedInWriter/components/Header.tsx
new file mode 100644
index 00000000..81646e2b
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/Header.tsx
@@ -0,0 +1,428 @@
+import React from 'react';
+import { LinkedInPreferences } from '../utils/storageUtils';
+// Temporary fix: use require for image import
+const alwrityLogo = require('../../../assets/images/alwrity_logo.png');
+
+interface HeaderProps {
+ userPreferences: LinkedInPreferences;
+ chatHistory: any[];
+ showPreferencesModal: boolean;
+ showContextModal: boolean;
+ context: string;
+ onPreferencesModalChange: (show: boolean) => void;
+ onContextModalChange: (show: boolean) => void;
+ onContextChange: (value: string) => void;
+ onPreferencesChange: (prefs: Partial) => void;
+ onCopy: () => void;
+ onClear: () => void;
+ onClearHistory: () => void;
+ draft: string;
+ getHistoryLength: () => number;
+}
+
+export const Header: React.FC = ({
+ userPreferences,
+ chatHistory,
+ showPreferencesModal,
+ showContextModal,
+ context,
+ onPreferencesModalChange,
+ onContextModalChange,
+ onContextChange,
+ onPreferencesChange,
+ onCopy,
+ onClear,
+ onClearHistory,
+ draft,
+ getHistoryLength
+}) => {
+ const handlePreferenceChange = (key: keyof LinkedInPreferences, value: any) => {
+ onPreferencesChange({ [key]: value });
+ };
+
+ return (
+
+
+ {/* Left Section - Logo and Title */}
+
+
+
+
+
+ LinkedIn Writer
+
+
+ Professional content creation for LinkedIn
+
+
+
+
+ {/* Control Buttons */}
+
+ {/* Preferences Button */}
+
onPreferencesModalChange(true)}
+ onMouseLeave={() => onPreferencesModalChange(false)}
+ >
+
+ ⚙️
+ Preferences
+ ▼
+
+
+ {/* Preferences Modal */}
+ {showPreferencesModal && (
+
+
+
+ Content Preferences & Context
+
+
+ Current Settings: {userPreferences.tone} tone • {userPreferences.industry || 'Not set'} industry • {chatHistory.length} messages
+
+
+
+ {/* Preferences Grid */}
+
+
+
Tone
+
handlePreferenceChange('tone', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '6px 8px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ background: '#f8f9fa',
+ fontSize: '12px'
+ }}
+ >
+ Professional
+ Casual
+ Thought Leadership
+ Conversational
+ Technical
+
+
+
+
Industry
+
handlePreferenceChange('industry', e.target.value)}
+ placeholder="e.g., Technology"
+ style={{
+ width: '100%',
+ padding: '6px 8px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ background: '#f8f9fa',
+ fontSize: '12px'
+ }}
+ />
+
+
+
Target Audience
+
handlePreferenceChange('target_audience', e.target.value)}
+ placeholder="e.g., Product Managers"
+ style={{
+ width: '100%',
+ padding: '6px 8px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ background: '#f8f9fa',
+ fontSize: '12px'
+ }}
+ />
+
+
+
Writing Style
+
handlePreferenceChange('writing_style', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '6px 8px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ background: '#f8f9fa',
+ fontSize: '12px'
+ }}
+ >
+ Clear and Concise
+ Storytelling
+ Analytical
+ Persuasive
+
+
+
+
+ {/* Checkboxes */}
+
+
+ handlePreferenceChange('hashtag_preferences', e.target.checked)}
+ style={{ margin: 0 }}
+ />
+ Include Hashtags
+
+
+ handlePreferenceChange('cta_preferences', e.target.checked)}
+ style={{ margin: 0 }}
+ />
+ Include Call-to-Action
+
+
+
+ {/* Current Context Display */}
+
+
Current Context:
+
+ {userPreferences.tone && (
+
+ {userPreferences.tone}
+
+ )}
+ {userPreferences.industry && (
+
+ {userPreferences.industry}
+
+ )}
+ {userPreferences.target_audience && (
+
+ {userPreferences.target_audience}
+
+ )}
+
+ {chatHistory.length} messages
+
+
+
+
+
+
+ )}
+
+
+ {/* Context & Notes Button */}
+
onContextModalChange(true)}
+ onMouseLeave={() => onContextModalChange(false)}
+ >
+
+ 📝
+ Context & Notes
+ ▼
+
+
+ {/* Context & Notes Modal */}
+ {showContextModal && (
+
+
+
+ Context & Notes
+
+
+ Add context, notes, or specific requirements for your LinkedIn content
+
+
+
+
+ )}
+
+
+
+
+
+
+ Copy
+
+
+ Clear
+
+
+ Clear Memory ({getHistoryLength()})
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/LinkedInWriter/components/LoadingIndicator.tsx b/frontend/src/components/LinkedInWriter/components/LoadingIndicator.tsx
new file mode 100644
index 00000000..8f7c8585
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/LoadingIndicator.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+
+interface LoadingIndicatorProps {
+ isGenerating: boolean;
+ loadingMessage: string;
+ currentAction: string | null;
+}
+
+export const LoadingIndicator: React.FC = ({
+ isGenerating,
+ loadingMessage,
+ currentAction
+}) => {
+ if (!isGenerating) return null;
+
+ return (
+
+
+
+ {loadingMessage || 'Generating content...'}
+
+ {currentAction && (
+
+ Action: {currentAction}
+
+ )}
+
+
+ );
+};
diff --git a/frontend/src/components/LinkedInWriter/components/PostHITL.tsx b/frontend/src/components/LinkedInWriter/components/PostHITL.tsx
new file mode 100644
index 00000000..fb0966a1
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/PostHITL.tsx
@@ -0,0 +1,403 @@
+import React from 'react';
+import { linkedInWriterApi, LinkedInPostRequest } from '../../../services/linkedInWriterApi';
+import {
+ readPrefs,
+ writePrefs,
+ logAssistant,
+ mapPostType,
+ mapTone,
+ mapIndustry,
+ mapSearchEngine,
+ getPersonalizedPlaceholder,
+ VALID_POST_TYPES,
+ VALID_TONES,
+ VALID_INDUSTRIES,
+ VALID_SEARCH_ENGINES
+} from '../utils/linkedInWriterUtils';
+
+interface PostHITLProps {
+ args: any;
+ respond: (data: any) => void;
+}
+
+const PostHITL: React.FC = ({ args, respond }) => {
+ const prefs = React.useMemo(() => readPrefs(), []);
+ const [form, setForm] = React.useState({
+ topic: args?.topic || prefs.topic || 'AI transformation in business',
+ industry: args?.industry || prefs.industry || 'Technology',
+ post_type: args?.post_type || prefs.post_type || 'professional',
+ tone: args?.tone || prefs.tone || 'professional',
+ target_audience: args?.target_audience || prefs.target_audience || 'Business leaders and professionals',
+ key_points: args?.key_points || prefs.key_points || [],
+ include_hashtags: args?.include_hashtags ?? (prefs.include_hashtags ?? true),
+ include_call_to_action: args?.include_call_to_action ?? (prefs.include_call_to_action ?? true),
+ research_enabled: args?.research_enabled ?? (prefs.research_enabled ?? true),
+ search_engine: args?.search_engine || prefs.search_engine || 'metaphor',
+ max_length: args?.max_length || prefs.max_length || 2000
+ });
+ const [loading, setLoading] = React.useState(false);
+ const [error, setError] = React.useState(null);
+
+ const run = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ // Emit loading start event
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
+ detail: {
+ action: 'Generating LinkedIn Post',
+ message: `Creating a ${form.post_type} LinkedIn post about "${form.topic}". Please consider your target audience, tone, and any specific requirements you've provided.`
+ }
+ }));
+
+ const payload: LinkedInPostRequest = {
+ ...form,
+ post_type: mapPostType(form.post_type),
+ tone: mapTone(form.tone),
+ industry: mapIndustry(form.industry),
+ search_engine: mapSearchEngine(form.search_engine)
+ };
+
+ // Save user preferences
+ writePrefs({
+ topic: payload.topic,
+ industry: payload.industry,
+ post_type: payload.post_type,
+ tone: payload.tone,
+ target_audience: payload.target_audience,
+ key_points: payload.key_points,
+ include_hashtags: payload.include_hashtags,
+ include_call_to_action: payload.include_call_to_action,
+ research_enabled: payload.research_enabled,
+ search_engine: payload.search_engine,
+ max_length: payload.max_length
+ });
+
+ const res = await linkedInWriterApi.generatePost(payload);
+
+ if (res.success && res.data) {
+ const content = res.data.content;
+ const hashtags = res.data.hashtags?.map(h => h.hashtag).join(' ') || '';
+ const cta = res.data.call_to_action || '';
+
+ let fullContent = content;
+ if (hashtags) fullContent += `\n\n${hashtags}`;
+ if (cta) fullContent += `\n\n${cta}`;
+
+ // Emit loading end event
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd', { detail: {} }));
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: fullContent }));
+ logAssistant(fullContent);
+ respond({ success: true, content: fullContent });
+ } else {
+ const errorMsg = res.error || 'Failed to generate LinkedIn post';
+ setError(errorMsg);
+
+ // Emit loading end event with error
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd', {
+ detail: { error: errorMsg }
+ }));
+
+ respond({ success: false, message: errorMsg });
+ }
+ } catch (e: any) {
+ const msg = e?.response?.data?.detail || e?.message || 'Failed to generate LinkedIn post';
+ setError(msg);
+
+ // Emit loading end event with error
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd', {
+ detail: { error: msg }
+ }));
+
+ respond({ success: false, message: msg });
+ console.error('[LinkedIn Writer] post.generate error', e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const set = (k: keyof LinkedInPostRequest, v: any) => setForm(prev => ({ ...prev, [k]: v }));
+
+ const addKeyPoint = () => {
+ set('key_points', [...(form.key_points || []), '']);
+ };
+
+ const updateKeyPoint = (index: number, value: string) => {
+ const newKeyPoints = [...(form.key_points || [])];
+ newKeyPoints[index] = value;
+ set('key_points', newKeyPoints);
+ };
+
+ const removeKeyPoint = (index: number) => {
+ const newKeyPoints = [...(form.key_points || [])];
+ newKeyPoints.splice(index, 1);
+ set('key_points', newKeyPoints);
+ };
+
+ return (
+
+
+ Generate LinkedIn Post
+
+
+
+
+
+ Topic *
+
+ set('topic', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ fontSize: 14
+ }}
+ />
+
+
+
+
+ Industry *
+
+ set('industry', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ fontSize: 14
+ }}
+ >
+ {VALID_INDUSTRIES.map(industry => (
+ {industry}
+ ))}
+
+
+
+
+
+ Post Type
+
+ set('post_type', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ fontSize: 14
+ }}
+ >
+ {VALID_POST_TYPES.map(type => (
+ {type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
+ ))}
+
+
+
+
+
+ Tone
+
+ set('tone', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ fontSize: 14
+ }}
+ >
+ {VALID_TONES.map(tone => (
+ {tone.charAt(0).toUpperCase() + tone.slice(1)}
+ ))}
+
+
+
+
+
+ Target Audience
+
+ set('target_audience', e.target.value)}
+ style={{
+ width: '100%',
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ fontSize: 14
+ }}
+ />
+
+
+
+
+ Key Points
+
+ {(form.key_points || []).map((point, index) => (
+
+ updateKeyPoint(index, e.target.value)}
+ style={{
+ flex: 1,
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ fontSize: 14
+ }}
+ />
+ removeKeyPoint(index)}
+ style={{
+ padding: '8px 12px',
+ background: '#dc3545',
+ color: 'white',
+ border: 'none',
+ borderRadius: 4,
+ cursor: 'pointer'
+ }}
+ >
+ Remove
+
+
+ ))}
+
+ Add Key Point
+
+
+
+
+
+ set('include_hashtags', e.target.checked)}
+ />
+ Include Hashtags
+
+
+ set('include_call_to_action', e.target.checked)}
+ />
+ Include CTA
+
+
+
+
+
+ set('research_enabled', e.target.checked)}
+ />
+ Enable Research
+
+
+
+ Search Engine
+
+ set('search_engine', e.target.value)}
+ disabled={!form.research_enabled}
+ style={{
+ width: '100%',
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ fontSize: 14,
+ opacity: form.research_enabled ? 1 : 0.6
+ }}
+ >
+ {VALID_SEARCH_ENGINES.map(engine => (
+ {engine.charAt(0).toUpperCase() + engine.slice(1)}
+ ))}
+
+
+
+
+
+
+ Max Length (characters)
+
+ set('max_length', parseInt(e.target.value) || 2000)}
+ style={{
+ width: '100%',
+ padding: '8px 12px',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ fontSize: 14
+ }}
+ />
+
+
+
+
+ {loading ? 'Generating...' : 'Generate LinkedIn Post'}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+export default PostHITL;
diff --git a/frontend/src/components/LinkedInWriter/components/VideoScriptHITL.tsx b/frontend/src/components/LinkedInWriter/components/VideoScriptHITL.tsx
new file mode 100644
index 00000000..64a8b207
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/VideoScriptHITL.tsx
@@ -0,0 +1,244 @@
+import React from 'react';
+import { linkedInWriterApi, LinkedInVideoScriptRequest } from '../../../services/linkedInWriterApi';
+import {
+ readPrefs,
+ writePrefs,
+ logAssistant,
+ mapTone,
+ mapIndustry,
+ getPersonalizedPlaceholder,
+ VALID_TONES,
+ VALID_INDUSTRIES
+} from '../utils/linkedInWriterUtils';
+
+interface VideoScriptHITLProps {
+ args: any;
+ respond: (data: any) => void;
+}
+
+const VideoScriptHITL: React.FC = ({ args, respond }) => {
+ const prefs = React.useMemo(() => readPrefs(), []);
+ const [form, setForm] = React.useState({
+ topic: args.topic ?? prefs.topic ?? '',
+ target_audience: args.target_audience ?? prefs.target_audience ?? '',
+ tone: args.tone ?? prefs.tone ?? 'professional',
+ industry: args.industry ?? prefs.industry ?? 'technology',
+ video_length: args.video_length ?? (prefs.video_length ?? 60),
+ key_messages: args.key_messages ?? (prefs.key_messages ?? []),
+ include_hook: args.include_hook ?? (prefs.include_hook ?? true),
+ include_captions: args.include_captions ?? (prefs.include_captions ?? true)
+ });
+
+ const [isLoading, setIsLoading] = React.useState(false);
+
+ const run = async () => {
+ try {
+ setIsLoading(true);
+
+ // Emit loading start event
+ window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
+ detail: {
+ action: 'Generating LinkedIn Video Script',
+ message: `Creating a ${form.video_length}-second video script about "${form.topic}". This will include a compelling hook, engaging content, and clear conclusion for your ${form.target_audience}.`
+ }
+ }));
+
+ logAssistant('Starting LinkedIn video script generation...');
+
+ // Read user preferences
+ const prefs = readPrefs();
+ if (prefs) {
+ form.tone = prefs.tone || form.tone;
+ form.industry = prefs.industry || form.industry;
+ }
+
+ // Normalize and map enum values
+ const request: LinkedInVideoScriptRequest = {
+ topic: form.topic,
+ target_audience: form.target_audience,
+ tone: mapTone(form.tone),
+ industry: mapIndustry(form.industry),
+ video_length: form.video_length,
+ key_messages: form.key_messages,
+ include_hook: form.include_hook,
+ include_captions: form.include_captions
+ };
+
+ const res = await linkedInWriterApi.generateVideoScript(request);
+
+ // Write preferences
+ writePrefs({
+ tone: form.tone,
+ industry: form.industry,
+ target_audience: form.target_audience,
+ video_length: form.video_length,
+ key_messages: form.key_messages,
+ include_hook: form.include_hook,
+ include_captions: form.include_captions
+ });
+
+ logAssistant('LinkedIn video script generated successfully');
+
+ // Update draft content
+ if (res.data) {
+ let content = `# Video Script: ${form.topic}\n\n`;
+ content += `## Hook\n${res.data.hook}\n\n`;
+ content += `## Main Content\n`;
+ res.data.main_content.forEach((scene, index) => {
+ content += `### Scene ${index + 1} (${scene.duration || '30s'})\n${scene.content}\n\n`;
+ });
+ content += `## Conclusion\n${res.data.conclusion}\n\n`;
+ content += `## Video Description\n${res.data.video_description}\n\n`;
+
+ if (res.data.captions) {
+ content += `## Captions\n${res.data.captions.join('\n')}\n\n`;
+ }
+
+ window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', {
+ detail: content
+ }));
+
+ respond({
+ success: true,
+ video_script: content,
+ hook: res.data.hook,
+ main_content: res.data.main_content,
+ conclusion: res.data.conclusion,
+ captions: res.data.captions,
+ video_description: res.data.video_description
+ });
+ } else {
+ throw new Error('No data received from API');
+ }
+
+ } catch (error) {
+ console.error('LinkedIn Video Script Generation Error:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+ logAssistant(`Error generating LinkedIn video script: ${errorMessage}`);
+ respond({
+ success: false,
+ error: errorMessage
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
Generate LinkedIn Video Script
+
+
+ Video Topic *
+ setForm({ ...form, topic: e.target.value })}
+ placeholder={getPersonalizedPlaceholder('video', 'topic', prefs)}
+ required
+ />
+
+
+
+ Target Audience
+ setForm({ ...form, target_audience: e.target.value })}
+ placeholder={getPersonalizedPlaceholder('video', 'target_audience', prefs)}
+ />
+
+
+
+ Tone
+ setForm({ ...form, tone: e.target.value })}
+ >
+ {VALID_TONES.map(tone => (
+
+ {tone.charAt(0).toUpperCase() + tone.slice(1)}
+
+ ))}
+
+
+
+
+ Industry
+ setForm({ ...form, industry: e.target.value })}
+ >
+ {VALID_INDUSTRIES.map(industry => (
+
+ {industry.charAt(0).toUpperCase() + industry.slice(1)}
+
+ ))}
+
+
+
+
+ Video Length (seconds)
+ setForm({ ...form, video_length: parseInt(e.target.value) })}
+ >
+ 30 seconds (Quick tip)
+ 60 seconds (Standard)
+ 90 seconds (Detailed)
+ 120 seconds (Comprehensive)
+
+
+
+
+ Key Messages
+
+
+
+
+ setForm({ ...form, include_hook: e.target.checked })}
+ />
+ Include attention-grabbing hook
+
+
+
+
+
+ setForm({ ...form, include_captions: e.target.checked })}
+ />
+ Include video captions
+
+
+
+
+
+ {isLoading ? 'Generating Video Script...' : 'Generate Video Script'}
+
+
+
+ );
+};
+
+export default VideoScriptHITL;
diff --git a/frontend/src/components/LinkedInWriter/components/WelcomeMessage.tsx b/frontend/src/components/LinkedInWriter/components/WelcomeMessage.tsx
new file mode 100644
index 00000000..adad9f95
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/WelcomeMessage.tsx
@@ -0,0 +1,80 @@
+import React from 'react';
+
+interface WelcomeMessageProps {
+ draft: string;
+ isGenerating: boolean;
+}
+
+export const WelcomeMessage: React.FC = ({
+ draft,
+ isGenerating
+}) => {
+ if (draft || isGenerating) return null;
+
+ return (
+
+
+ ✍️
+
+
+ Welcome to LinkedIn Writer
+
+
+ Click the ALwrity Co-Pilot icon in the bottom-right corner to access the AI assistant. You can generate LinkedIn posts, articles, carousels, video scripts, and comment responses.
+
+
+
+
+ AI Assistant is ready - look for the ALwrity Co-Pilot icon
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/LinkedInWriter/components/index.ts b/frontend/src/components/LinkedInWriter/components/index.ts
new file mode 100644
index 00000000..c84231bf
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/components/index.ts
@@ -0,0 +1,11 @@
+export { default as PostHITL } from './PostHITL';
+export { default as ArticleHITL } from './ArticleHITL';
+export { default as CarouselHITL } from './CarouselHITL';
+export { default as VideoScriptHITL } from './VideoScriptHITL';
+export { default as CommentResponseHITL } from './CommentResponseHITL';
+
+// New refactored components
+export { Header } from './Header';
+export { ContentEditor } from './ContentEditor';
+export { LoadingIndicator } from './LoadingIndicator';
+export { WelcomeMessage } from './WelcomeMessage';
diff --git a/frontend/src/components/LinkedInWriter/hooks/useLinkedInWriter.ts b/frontend/src/components/LinkedInWriter/hooks/useLinkedInWriter.ts
new file mode 100644
index 00000000..28acb26b
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/hooks/useLinkedInWriter.ts
@@ -0,0 +1,261 @@
+import { useState, useCallback, useEffect } from 'react';
+import { useCopilotReadable } from '@copilotkit/react-core';
+import {
+ loadHistory,
+ clearHistory,
+ getHistoryLength,
+ getPreferences,
+ savePreferences,
+ getCurrentContext,
+ saveCurrentContext,
+ summarizeHistory,
+ type ChatMsg,
+ type LinkedInPreferences
+} from '../utils/storageUtils';
+import { getContextAwareSuggestions } from '../utils/linkedInWriterUtils';
+
+export function useLinkedInWriter() {
+ // Core state
+ const [draft, setDraft] = useState('');
+ const [context, setContext] = useState('');
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [isPreviewing, setIsPreviewing] = useState(false);
+ const [livePreviewHtml, setLivePreviewHtml] = useState('');
+ const [pendingEdit, setPendingEdit] = useState<{ src: string; target: string } | null>(null);
+ const [loadingMessage, setLoadingMessage] = useState('');
+ const [currentAction, setCurrentAction] = useState(null);
+
+ // Chat history state
+ const [historyVersion, setHistoryVersion] = useState(0);
+ const [chatHistory, setChatHistory] = useState([]);
+ const [userPreferences, setUserPreferences] = useState(getPreferences());
+
+ // UI state
+ const [currentSuggestions, setCurrentSuggestions] = useState>([]);
+ const [showContextPanel, setShowContextPanel] = useState(false);
+ const [showPreferencesModal, setShowPreferencesModal] = useState(false);
+ const [showContextModal, setShowContextModal] = useState(false);
+ const [showPreview, setShowPreview] = useState(false);
+
+ // Update suggestions when context changes
+ const updateSuggestions = useCallback(() => {
+ const newSuggestions = getContextAwareSuggestions(
+ userPreferences,
+ draft,
+ chatHistory.slice(-5),
+ userPreferences.last_used_actions || []
+ );
+ setCurrentSuggestions(newSuggestions);
+ }, [userPreferences, draft, chatHistory]);
+
+ // Track action usage and update preferences
+ const trackActionUsage = useCallback((actionName: string) => {
+ const currentPrefs = getPreferences();
+ const updatedActions = [...(currentPrefs.last_used_actions || []), actionName].slice(-5);
+ savePreferences({ last_used_actions: updatedActions });
+ setUserPreferences(prev => ({ ...prev, last_used_actions: updatedActions }));
+
+ // Update suggestions after action usage
+ setTimeout(() => updateSuggestions(), 100);
+ }, [updateSuggestions]);
+
+ // Initialize chat history and preferences from localStorage
+ useEffect(() => {
+ const loadInitialData = () => {
+ try {
+ const history = loadHistory();
+ const prefs = getPreferences();
+ const savedContext = getCurrentContext();
+
+ setChatHistory(history);
+ setUserPreferences(prefs);
+ if (savedContext && !context) {
+ setContext(savedContext);
+ }
+
+ console.log('[LinkedIn Writer] Initialized with:', {
+ historyCount: history.length,
+ preferences: prefs,
+ hasContext: !!savedContext
+ });
+ } catch (error) {
+ console.warn('[LinkedIn Writer] Failed to initialize from localStorage:', error);
+ }
+ };
+
+ loadInitialData();
+ }, []);
+
+ // Save context changes to localStorage
+ useEffect(() => {
+ if (context) {
+ saveCurrentContext(context);
+ }
+ }, [context]);
+
+ // Update suggestions when relevant state changes
+ useEffect(() => {
+ updateSuggestions();
+ }, [updateSuggestions]);
+
+ // Handle draft updates from CopilotKit actions
+ useEffect(() => {
+ const handleUpdateDraft = (event: CustomEvent) => {
+ setDraft(event.detail);
+ setIsGenerating(false);
+ setLoadingMessage('');
+ setCurrentAction(null);
+ };
+
+ const handleAppendDraft = (event: CustomEvent) => {
+ setDraft(prev => prev + event.detail);
+ };
+
+ const handleAssistantMessage = (event: CustomEvent) => {
+ console.log('LinkedIn Assistant:', event.detail);
+ };
+
+ const handleLoadingStart = (event: CustomEvent) => {
+ const { action, message } = event.detail;
+ setCurrentAction(action);
+ setLoadingMessage(message);
+ setIsGenerating(true);
+ };
+
+ const handleLoadingEnd = (event: CustomEvent) => {
+ setIsGenerating(false);
+ setLoadingMessage('');
+ setCurrentAction(null);
+ };
+
+ const handleApplyEdit = (event: CustomEvent) => {
+ const target: string = typeof event.detail === 'string' ? event.detail : (event.detail?.target ?? '');
+ const src = draft || '';
+ if (!target) return;
+ setPendingEdit({ src, target });
+ setIsPreviewing(true);
+
+ // Use diff highlighting for professional content changes
+ try {
+ const { diffMarkup } = require('../utils/contentFormatters');
+ setLivePreviewHtml(diffMarkup(src, target));
+ } catch (error) {
+ // Fallback to simple text if diffMarkup fails to load
+ console.warn('Failed to load diffMarkup, using fallback:', error);
+ setLivePreviewHtml(target);
+ }
+ };
+
+ window.addEventListener('linkedinwriter:updateDraft', handleUpdateDraft as EventListener);
+ window.addEventListener('linkedinwriter:appendDraft', handleAppendDraft as EventListener);
+ window.addEventListener('linkedinwriter:assistantMessage', handleAssistantMessage as EventListener);
+ window.addEventListener('linkedinwriter:applyEdit', handleApplyEdit as EventListener);
+ window.addEventListener('linkedinwriter:loadingStart', handleLoadingStart as EventListener);
+ window.addEventListener('linkedinwriter:loadingEnd', handleLoadingEnd as EventListener);
+
+ return () => {
+ window.removeEventListener('linkedinwriter:updateDraft', handleUpdateDraft as EventListener);
+ window.removeEventListener('linkedinwriter:appendDraft', handleAppendDraft as EventListener);
+ window.removeEventListener('linkedinwriter:assistantMessage', handleAssistantMessage as EventListener);
+ window.removeEventListener('linkedinwriter:applyEdit', handleApplyEdit as EventListener);
+ window.removeEventListener('linkedinwriter:loadingStart', handleLoadingStart as EventListener);
+ window.removeEventListener('linkedinwriter:loadingEnd', handleLoadingEnd as EventListener);
+ };
+ }, [draft]);
+
+ // Event handlers
+ const handleDraftChange = useCallback((value: string) => {
+ setDraft(value);
+ }, []);
+
+ const handleContextChange = useCallback((value: string) => {
+ setContext(value);
+ }, []);
+
+ const handleClear = useCallback(() => {
+ setDraft('');
+ setContext('');
+ }, []);
+
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(draft);
+ } catch (err) {
+ console.error('Failed to copy text: ', err);
+ }
+ }, [draft]);
+
+ const handleClearHistory = useCallback(() => {
+ clearHistory();
+ setHistoryVersion(v => v + 1);
+ setChatHistory([]);
+ console.log('[LinkedIn Writer] Chat memory cleared by user');
+ }, []);
+
+ // Make content available to CopilotKit
+ useCopilotReadable({
+ description: 'Current LinkedIn content draft',
+ value: draft
+ });
+
+ useCopilotReadable({
+ description: 'Context and notes for LinkedIn content',
+ value: context
+ });
+
+ useCopilotReadable({
+ description: 'User preferences for LinkedIn content (tone, industry, audience, style, options)',
+ value: userPreferences
+ });
+
+ return {
+ // State
+ draft,
+ context,
+ isGenerating,
+ isPreviewing,
+ livePreviewHtml,
+ pendingEdit,
+ loadingMessage,
+ currentAction,
+ historyVersion,
+ chatHistory,
+ userPreferences,
+ currentSuggestions,
+ showContextPanel,
+ showPreferencesModal,
+ showContextModal,
+ showPreview,
+
+ // Setters
+ setDraft,
+ setContext,
+ setIsGenerating,
+ setIsPreviewing,
+ setLivePreviewHtml,
+ setPendingEdit,
+ setLoadingMessage,
+ setCurrentAction,
+ setHistoryVersion,
+ setChatHistory,
+ setUserPreferences,
+ setShowContextPanel,
+ setShowPreferencesModal,
+ setShowContextModal,
+ setShowPreview,
+
+ // Handlers
+ handleDraftChange,
+ handleContextChange,
+ handleClear,
+ handleCopy,
+ handleClearHistory,
+
+ // Utilities
+ trackActionUsage,
+ updateSuggestions,
+ getHistoryLength,
+ savePreferences,
+ summarizeHistory
+ };
+}
diff --git a/frontend/src/components/LinkedInWriter/styles/alwrity-copilot.css b/frontend/src/components/LinkedInWriter/styles/alwrity-copilot.css
new file mode 100644
index 00000000..d586d312
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/styles/alwrity-copilot.css
@@ -0,0 +1,382 @@
+/* ALwrity Co-Pilot Custom Styling */
+
+/* CopilotKit trigger button styling (without ALwrity icon) */
+.alwrity-copilot-sidebar .copilot-sidebar-trigger,
+.alwrity-copilot-sidebar [data-testid="copilot-sidebar-trigger"],
+.alwrity-copilot-sidebar .copilot-sidebar-trigger-button,
+.alwrity-copilot-sidebar button[aria-label*="copilot"],
+.alwrity-copilot-sidebar button[aria-label*="assistant"],
+.alwrity-copilot-sidebar .copilot-sidebar-trigger > button,
+.alwrity-copilot-sidebar .copilot-sidebar-trigger > div,
+.alwrity-copilot-sidebar button[class*="trigger"],
+.alwrity-copilot-sidebar button[class*="floating"],
+.alwrity-copilot-sidebar button[class*="assistant"],
+.alwrity-copilot-sidebar button[class*="copilot"],
+.alwrity-copilot-sidebar [data-testid*="trigger"],
+.alwrity-copilot-sidebar [data-testid*="assistant"],
+.alwrity-copilot-sidebar [data-testid*="copilot"] {
+ width: 48px !important;
+ height: 48px !important;
+ border-radius: 50% !important;
+ box-shadow: 0 4px 12px rgba(10, 102, 194, 0.3) !important;
+ transition: all 0.3s ease !important;
+}
+
+/* Hover effects for trigger buttons */
+.alwrity-copilot-sidebar .copilot-sidebar-trigger:hover,
+.alwrity-copilot-sidebar [data-testid="copilot-sidebar-trigger"]:hover,
+.alwrity-copilot-sidebar .copilot-sidebar-trigger-button:hover,
+.alwrity-copilot-sidebar button[aria-label*="copilot"]:hover,
+.alwrity-copilot-sidebar button[aria-label*="assistant"]:hover,
+.alwrity-copilot-sidebar .copilot-sidebar-trigger > button:hover,
+.alwrity-copilot-sidebar .copilot-sidebar-trigger > div:hover,
+.alwrity-copilot-sidebar button[class*="trigger"]:hover,
+.alwrity-copilot-sidebar button[class*="floating"]:hover,
+.alwrity-copilot-sidebar button[class*="assistant"]:hover,
+.alwrity-copilot-sidebar button[class*="copilot"]:hover {
+ transform: scale(1.1) !important;
+ box-shadow: 0 6px 20px rgba(10, 102, 194, 0.4) !important;
+}
+
+/* Custom sidebar header */
+.alwrity-copilot-sidebar .copilot-sidebar-header {
+ background: linear-gradient(135deg, #0a66c2 0%, #0056b3 100%) !important;
+ border-bottom: 2px solid #ffffff20 !important;
+}
+
+/* Sidebar title styling - no icon */
+.alwrity-copilot-sidebar .copilot-sidebar-title {
+ color: #ffffff !important;
+ font-weight: 700 !important;
+ font-size: 18px !important;
+ display: flex !important;
+ align-items: center !important;
+ gap: 8px !important;
+}
+
+/* Icon display removed - no ALwrity image in chat window */
+
+/* Debug borders removed for clean appearance */
+
+/* Additional trigger button styling (without icon) */
+.alwrity-copilot-sidebar [data-testid="copilot-sidebar-trigger"],
+.alwrity-copilot-sidebar [data-testid="copilot-sidebar-trigger-button"],
+.alwrity-copilot-sidebar [data-testid="copilot-sidebar-trigger-icon"],
+.alwrity-copilot-sidebar .copilot-sidebar-trigger,
+.alwrity-copilot-sidebar .copilot-sidebar-trigger-button,
+.alwrity-copilot-sidebar .copilot-sidebar-trigger-icon,
+.alwrity-copilot-sidebar button[aria-label*="copilot"],
+.alwrity-copilot-sidebar button[aria-label*="assistant"],
+.alwrity-copilot-sidebar button[aria-label*="chat"],
+.alwrity-copilot-sidebar button[aria-label*="open"],
+.alwrity-copilot-sidebar button[aria-label*="toggle"] {
+ width: 48px !important;
+ height: 48px !important;
+ border-radius: 50% !important;
+ box-shadow: 0 4px 12px rgba(10, 102, 194, 0.3) !important;
+ transition: all 0.3s ease !important;
+}
+
+/* Premium Message Styling - Enhanced Speech Bubbles */
+.alwrity-copilot-sidebar .copilot-message,
+.alwrity-copilot-sidebar [data-testid="copilot-message"],
+.alwrity-copilot-sidebar .message,
+.alwrity-copilot-sidebar .chat-message,
+.alwrity-copilot-sidebar .assistant-message,
+.alwrity-copilot-sidebar .user-message {
+ border-radius: 16px !important;
+ margin: 12px 0 !important;
+ padding: 16px 20px !important;
+ max-width: 85% !important;
+ position: relative !important;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08) !important;
+ transition: all 0.2s ease !important;
+ line-height: 1.5 !important;
+ font-size: 14px !important;
+}
+
+/* User Message Styling */
+.alwrity-copilot-sidebar .copilot-message.user,
+.alwrity-copilot-sidebar [data-testid="copilot-message"].user,
+.alwrity-copilot-sidebar .message.user,
+.alwrity-copilot-sidebar .chat-message.user,
+.alwrity-copilot-sidebar .user-message {
+ background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%) !important;
+ border: 1px solid #d1e7ff !important;
+ margin-left: auto !important;
+ margin-right: 0 !important;
+ color: #1a365d !important;
+ font-weight: 500 !important;
+}
+
+.alwrity-copilot-sidebar .copilot-message.user::before,
+.alwrity-copilot-sidebar [data-testid="copilot-message"].user::before,
+.alwrity-copilot-sidebar .message.user::before,
+.alwrity-copilot-sidebar .chat-message.user::before,
+.alwrity-copilot-sidebar .user-message::before {
+ content: '' !important;
+ position: absolute !important;
+ right: -8px !important;
+ top: 50% !important;
+ transform: translateY(-50%) !important;
+ width: 0 !important;
+ height: 0 !important;
+ border-left: 8px solid #d1e7ff !important;
+ border-top: 6px solid transparent !important;
+ border-bottom: 6px solid transparent !important;
+}
+
+/* Assistant Message Styling */
+.alwrity-copilot-sidebar .copilot-message.assistant,
+.alwrity-copilot-sidebar [data-testid="copilot-message"].assistant,
+.alwrity-copilot-sidebar .message.assistant,
+.alwrity-copilot-sidebar .chat-message.assistant,
+.alwrity-copilot-sidebar .assistant-message {
+ background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important;
+ border: 1px solid #e1e8ed !important;
+ margin-left: 0 !important;
+ margin-right: auto !important;
+ color: #2d3748 !important;
+ font-weight: 400 !important;
+}
+
+.alwrity-copilot-sidebar .copilot-message.assistant::before,
+.alwrity-copilot-sidebar [data-testid="copilot-message"].assistant::before,
+.alwrity-copilot-sidebar .message.assistant::before,
+.alwrity-copilot-sidebar .chat-message.assistant::before,
+.alwrity-copilot-sidebar .assistant-message::before {
+ content: '' !important;
+ position: absolute !important;
+ left: -8px !important;
+ top: 50% !important;
+ transform: translateY(-50%) !important;
+ width: 0 !important;
+ height: 0 !important;
+ border-right: 8px solid #e1e8ed !important;
+ border-top: 6px solid transparent !important;
+ border-bottom: 6px solid transparent !important;
+}
+
+/* Message Hover Effects */
+.alwrity-copilot-sidebar .copilot-message:hover,
+.alwrity-copilot-sidebar [data-testid="copilot-message"]:hover,
+.alwrity-copilot-sidebar .message:hover,
+.alwrity-copilot-sidebar .chat-message:hover,
+.alwrity-copilot-sidebar .assistant-message:hover,
+.alwrity-copilot-sidebar .user-message:hover {
+ transform: translateY(-1px) !important;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12) !important;
+}
+
+/* Message Content Typography */
+.alwrity-copilot-sidebar .copilot-message p,
+.alwrity-copilot-sidebar [data-testid="copilot-message"] p,
+.alwrity-copilot-sidebar .message p,
+.alwrity-copilot-sidebar .chat-message p,
+.alwrity-copilot-sidebar .assistant-message p,
+.alwrity-copilot-sidebar .user-message p {
+ margin: 0 0 12px 0 !important;
+ line-height: 1.6 !important;
+}
+
+.alwrity-copilot-sidebar .copilot-message p:last-child,
+.alwrity-copilot-sidebar [data-testid="copilot-message"] p:last-child,
+.alwrity-copilot-sidebar .message p:last-child,
+.alwrity-copilot-sidebar .chat-message p:last-child,
+.alwrity-copilot-sidebar .assistant-message p:last-child,
+.alwrity-copilot-sidebar .user-message p:last-child {
+ margin-bottom: 0 !important;
+}
+
+/* Code Blocks in Messages */
+.alwrity-copilot-sidebar .copilot-message code,
+.alwrity-copilot-sidebar [data-testid="copilot-message"] code,
+.alwrity-copilot-sidebar .message code,
+.alwrity-copilot-sidebar .chat-message code,
+.alwrity-copilot-sidebar .assistant-message code,
+.alwrity-copilot-sidebar .user-message code {
+ background: #f7fafc !important;
+ border: 1px solid #e2e8f0 !important;
+ border-radius: 6px !important;
+ padding: 2px 6px !important;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace !important;
+ font-size: 13px !important;
+ color: #2d3748 !important;
+}
+
+/* Custom Button Styling */
+.alwrity-copilot-sidebar .copilot-button,
+.alwrity-copilot-sidebar button,
+.alwrity-copilot-sidebar .btn {
+ background: linear-gradient(135deg, #0a66c2 0%, #0056b3 100%) !important;
+ border: none !important;
+ border-radius: 10px !important;
+ color: white !important;
+ font-weight: 600 !important;
+ padding: 10px 20px !important;
+ transition: all 0.2s ease !important;
+ box-shadow: 0 2px 8px rgba(10, 102, 194, 0.25) !important;
+}
+
+.alwrity-copilot-sidebar .copilot-button:hover,
+.alwrity-copilot-sidebar button:hover,
+.alwrity-copilot-sidebar .btn:hover {
+ background: linear-gradient(135deg, #0056b3 0%, #004494 100%) !important;
+ transform: translateY(-2px) !important;
+ box-shadow: 0 4px 16px rgba(10, 102, 194, 0.35) !important;
+}
+
+/* Custom Input Styling */
+.alwrity-copilot-sidebar .copilot-input,
+.alwrity-copilot-sidebar input,
+.alwrity-copilot-sidebar textarea {
+ border: 2px solid #e2e8f0 !important;
+ border-radius: 12px !important;
+ padding: 14px 18px !important;
+ font-size: 14px !important;
+ transition: all 0.2s ease !important;
+ background: #ffffff !important;
+ color: #2d3748 !important;
+}
+
+.alwrity-copilot-sidebar .copilot-input:focus,
+.alwrity-copilot-sidebar input:focus,
+.alwrity-copilot-sidebar textarea:focus {
+ border-color: #0a66c2 !important;
+ outline: none !important;
+ box-shadow: 0 0 0 3px rgba(10, 102, 194, 0.1) !important;
+ transform: translateY(-1px) !important;
+}
+
+/* Premium Suggestion Chips */
+.alwrity-copilot-sidebar .copilot-suggestion,
+.alwrity-copilot-sidebar .suggestion,
+.alwrity-copilot-sidebar .chip {
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
+ border: 1px solid #e2e8f0 !important;
+ border-radius: 24px !important;
+ padding: 10px 18px !important;
+ margin: 6px !important;
+ cursor: pointer !important;
+ transition: all 0.2s ease !important;
+ font-size: 13px !important;
+ color: #475569 !important;
+ font-weight: 500 !important;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) !important;
+}
+
+.alwrity-copilot-sidebar .copilot-suggestion:hover,
+.alwrity-copilot-sidebar .suggestion:hover,
+.alwrity-copilot-sidebar .chip:hover {
+ background: linear-gradient(135deg, #0a66c2 0%, #0056b3 100%) !important;
+ color: white !important;
+ border-color: #0a66c2 !important;
+ transform: translateY(-2px) !important;
+ box-shadow: 0 4px 12px rgba(10, 102, 194, 0.25) !important;
+}
+
+/* Loading States */
+.alwrity-copilot-sidebar .copilot-loading,
+.alwrity-copilot-sidebar .loading {
+ color: #0a66c2 !important;
+ display: flex !important;
+ align-items: center !important;
+ gap: 8px !important;
+}
+
+/* Typing Indicator */
+.alwrity-copilot-sidebar .typing-indicator,
+.alwrity-copilot-sidebar .is-typing {
+ display: flex !important;
+ align-items: center !important;
+ gap: 4px !important;
+ padding: 8px 16px !important;
+ color: #64748b !important;
+ font-style: italic !important;
+}
+
+.alwrity-copilot-sidebar .typing-dot {
+ width: 6px !important;
+ height: 6px !important;
+ border-radius: 50% !important;
+ background: #0a66c2 !important;
+ animation: typing 1.4s infinite ease-in-out !important;
+}
+
+.alwrity-copilot-sidebar .typing-dot:nth-child(1) { animation-delay: -0.32s !important; }
+.alwrity-copilot-sidebar .typing-dot:nth-child(2) { animation-delay: -0.16s !important; }
+
+@keyframes typing {
+ 0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
+ 40% { transform: scale(1); opacity: 1; }
+}
+
+/* Scrollbar Customization */
+.alwrity-copilot-sidebar .copilot-sidebar-content::-webkit-scrollbar,
+.alwrity-copilot-sidebar .sidebar-content::-webkit-scrollbar,
+.alwrity-copilot-sidebar .chat-content::-webkit-scrollbar {
+ width: 8px !important;
+}
+
+.alwrity-copilot-sidebar .copilot-sidebar-content::-webkit-scrollbar-track,
+.alwrity-copilot-sidebar .sidebar-content::-webkit-scrollbar-track,
+.alwrity-copilot-sidebar .chat-content::-webkit-scrollbar-track {
+ background: #f8fafc !important;
+ border-radius: 4px !important;
+}
+
+.alwrity-copilot-sidebar .copilot-sidebar-content::-webkit-scrollbar-thumb,
+.alwrity-copilot-sidebar .sidebar-content::-webkit-scrollbar-thumb,
+.alwrity-copilot-sidebar .chat-content::-webkit-scrollbar-thumb {
+ background: linear-gradient(135deg, #0a66c2 0%, #0056b3 100%) !important;
+ border-radius: 4px !important;
+ border: 1px solid #f8fafc !important;
+}
+
+.alwrity-copilot-sidebar .copilot-sidebar-content::-webkit-scrollbar-thumb:hover,
+.alwrity-copilot-sidebar .sidebar-content::-webkit-scrollbar-thumb:hover,
+.alwrity-copilot-sidebar .chat-content::-webkit-scrollbar-thumb:hover {
+ background: linear-gradient(135deg, #0056b3 0%, #004494 100%) !important;
+}
+
+/* Message Timestamps */
+.alwrity-copilot-sidebar .message-timestamp,
+.alwrity-copilot-sidebar .timestamp {
+ font-size: 11px !important;
+ color: #94a3b8 !important;
+ margin-top: 4px !important;
+ text-align: right !important;
+ font-weight: 400 !important;
+}
+
+/* Tool Call Styling */
+.alwrity-copilot-sidebar .tool-call,
+.alwrity-copilot-sidebar .function-call {
+ background: #f1f5f9 !important;
+ border: 1px solid #e2e8f0 !important;
+ border-radius: 8px !important;
+ padding: 12px 16px !important;
+ margin: 8px 0 !important;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace !important;
+ font-size: 12px !important;
+ color: #475569 !important;
+ border-left: 4px solid #0a66c2 !important;
+}
+
+/* Error Message Styling */
+.alwrity-copilot-sidebar .error-message,
+.alwrity-copilot-sidebar .error {
+ background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%) !important;
+ border: 1px solid #fecaca !important;
+ color: #dc2626 !important;
+ border-left: 4px solid #dc2626 !important;
+}
+
+/* Success Message Styling */
+.alwrity-copilot-sidebar .success-message,
+.alwrity-copilot-sidebar .success {
+ background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%) !important;
+ border: 1px solid #bbf7d0 !important;
+ color: #16a34a !important;
+ border-left: 4px solid #16a34a !important;
+}
diff --git a/frontend/src/components/LinkedInWriter/styles/index.ts b/frontend/src/components/LinkedInWriter/styles/index.ts
new file mode 100644
index 00000000..77a57138
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/styles/index.ts
@@ -0,0 +1,5 @@
+// LinkedInWriter Component Styles
+// Note: CSS files are imported directly, not exported from TypeScript
+
+// Export empty to make this a module
+export {};
diff --git a/frontend/src/components/LinkedInWriter/utils/contentFormatters.ts b/frontend/src/components/LinkedInWriter/utils/contentFormatters.ts
new file mode 100644
index 00000000..7c75e8f1
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/utils/contentFormatters.ts
@@ -0,0 +1,90 @@
+// Content formatting utilities for LinkedIn Writer
+
+// Escape HTML characters to prevent XSS
+export function escapeHtml(s: string): string {
+ return s.replace(/&/g, '&').replace(//g, '>');
+}
+
+// Format draft content with proper LinkedIn styling
+export function formatDraftContent(content: string): string {
+ if (!content) return '';
+
+ let formatted = escapeHtml(content);
+
+ // Format hashtags
+ formatted = formatted.replace(/#(\w+)/g, '#$1 ');
+
+ // Format mentions
+ formatted = formatted.replace(/@(\w+)/g, '@$1 ');
+
+ // Format headers (lines starting with #)
+ formatted = formatted.replace(/^# (.+)$/gm, '$1 ');
+ formatted = formatted.replace(/^## (.+)$/gm, '$1 ');
+ formatted = formatted.replace(/^### (.+)$/gm, '$1 ');
+
+ // Format bold text
+ formatted = formatted.replace(/\*\*(.+?)\*\*/g, '$1 ');
+
+ // Format italic text
+ formatted = formatted.replace(/\*(.+?)\*/g, '$1 ');
+
+ // Format bullet points
+ formatted = formatted.replace(/^[•·-] (.+)$/gm, '• $1
');
+
+ // Format numbered lists
+ formatted = formatted.replace(/^\d+\. (.+)$/gm, (match, content, offset, string) => {
+ const lines = string.substring(0, offset).split('\n');
+ const currentLineIndex = lines.length - 1;
+ const currentLine = lines[currentLineIndex];
+ const number = currentLine.match(/^(\d+)\./)?.[1] || '1';
+ return `${number}. ${content}
`;
+ });
+
+ // Format line breaks
+ formatted = formatted.replace(/\n\n/g, '
');
+ formatted = formatted.replace(/\n/g, ' ');
+
+ // Wrap in paragraph tags
+ formatted = `
${formatted}
`;
+
+ return formatted;
+}
+
+// Lightweight LCS-based diff highlighting for professional content changes
+export function diffMarkup(oldText: string, newText: string): string {
+ const MAX = 4000;
+ const a = (oldText || '').slice(0, MAX);
+ const b = (newText || '').slice(0, MAX);
+ const n = a.length, m = b.length;
+ const dp: number[][] = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
+
+ for (let i = n - 1; i >= 0; i--) {
+ for (let j = m - 1; j >= 0; j--) {
+ if (a[i] === b[j]) dp[i][j] = dp[i + 1][j + 1] + 1;
+ else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
+ }
+ }
+
+ let i = 0, j = 0;
+ let out = '';
+
+ while (i < n && j < m) {
+ if (a[i] === b[j]) {
+ out += a[i];
+ i++; j++;
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
+ out += `${escapeHtml(a[i])} `;
+ i++;
+ } else {
+ out += `${escapeHtml(b[j])} `;
+ j++;
+ }
+ }
+
+ while (i < n) { out += `${escapeHtml(a[i++])} `; }
+ while (j < m) { out += `${escapeHtml(b[j++])} `; }
+
+ if (oldText.length > MAX || newText.length > MAX) out += ' … ';
+
+ return out;
+}
diff --git a/frontend/src/components/LinkedInWriter/utils/linkedInWriterUtils.ts b/frontend/src/components/LinkedInWriter/utils/linkedInWriterUtils.ts
new file mode 100644
index 00000000..c2601e50
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/utils/linkedInWriterUtils.ts
@@ -0,0 +1,526 @@
+import { LinkedInPostType, LinkedInTone, SearchEngine } from '../../../services/linkedInWriterApi';
+
+// LinkedIn Writer Utilities
+export const PREFS_KEY = 'linkedinwriter:preferences';
+
+// LinkedIn-specific validation constants
+export const VALID_POST_TYPES = [
+ 'professional',
+ 'thought_leadership',
+ 'industry_news',
+ 'personal_story',
+ 'company_update',
+ 'poll'
+] as const;
+
+export const VALID_TONES = [
+ 'professional',
+ 'conversational',
+ 'authoritative',
+ 'inspirational',
+ 'educational',
+ 'friendly'
+] as const;
+
+export const VALID_SEARCH_ENGINES = [
+ 'metaphor',
+ 'google',
+ 'tavily'
+] as const;
+
+export const VALID_INDUSTRIES = [
+ 'Technology',
+ 'Healthcare',
+ 'Finance',
+ 'Education',
+ 'Manufacturing',
+ 'Retail',
+ 'Marketing',
+ 'Consulting',
+ 'Real Estate',
+ 'Legal',
+ 'Non-profit',
+ 'Government',
+ 'Entertainment',
+ 'Sports',
+ 'Food & Beverage',
+ 'Automotive',
+ 'Energy',
+ 'Telecommunications',
+ 'Media',
+ 'Custom'
+] as const;
+
+export const VALID_RESPONSE_TYPES = [
+ 'professional',
+ 'appreciative',
+ 'clarifying',
+ 'disagreement',
+ 'value_add'
+] as const;
+
+// Professional hashtag categories
+export const PROFESSIONAL_HASHTAGS = {
+ technology: ['#TechInnovation', '#DigitalTransformation', '#AI', '#MachineLearning', '#CloudComputing'],
+ healthcare: ['#HealthcareInnovation', '#DigitalHealth', '#PatientCare', '#MedicalTechnology', '#HealthTech'],
+ finance: ['#FinTech', '#DigitalBanking', '#Investment', '#FinancialServices', '#WealthManagement'],
+ education: ['#EdTech', '#OnlineLearning', '#ProfessionalDevelopment', '#SkillsDevelopment', '#LifelongLearning'],
+ marketing: ['#DigitalMarketing', '#ContentMarketing', '#SocialMediaMarketing', '#BrandStrategy', '#GrowthMarketing'],
+ leadership: ['#Leadership', '#Management', '#TeamBuilding', '#ProfessionalGrowth', '#CareerDevelopment'],
+ general: ['#Networking', '#ProfessionalDevelopment', '#Business', '#Innovation', '#Success']
+};
+
+// Utility functions
+export function readPrefs(): Record {
+ try {
+ return JSON.parse(localStorage.getItem(PREFS_KEY) || '{}') || {};
+ } catch {
+ return {};
+ }
+}
+
+export function writePrefs(p: Record) {
+ try {
+ localStorage.setItem(PREFS_KEY, JSON.stringify(p));
+ } catch {}
+}
+
+export function logAssistant(content: string) {
+ try {
+ window.dispatchEvent(new CustomEvent('linkedinwriter:assistantMessage', { detail: { content } }));
+ } catch {}
+}
+
+export function normalizeEnum(input: string | undefined | null): string {
+ return (input || '').trim().toLowerCase();
+}
+
+// LinkedIn-specific mapping functions
+export function mapPostType(postType: string | undefined): LinkedInPostType {
+ const pt = normalizeEnum(postType);
+ if (!pt) return LinkedInPostType.PROFESSIONAL;
+
+ const exact = VALID_POST_TYPES.find(v => v.toLowerCase() === pt);
+ if (exact) return exact as LinkedInPostType;
+
+ if (pt.includes('thought') || pt.includes('leadership')) return LinkedInPostType.THOUGHT_LEADERSHIP;
+ if (pt.includes('news') || pt.includes('industry')) return LinkedInPostType.INDUSTRY_NEWS;
+ if (pt.includes('personal') || pt.includes('story')) return LinkedInPostType.PERSONAL_STORY;
+ if (pt.includes('company') || pt.includes('update')) return LinkedInPostType.COMPANY_UPDATE;
+ if (pt.includes('poll') || pt.includes('question')) return LinkedInPostType.POLL;
+
+ return LinkedInPostType.PROFESSIONAL;
+}
+
+export function mapTone(tone: string | undefined): LinkedInTone {
+ const t = normalizeEnum(tone);
+ if (!t) return LinkedInTone.PROFESSIONAL;
+
+ const exact = VALID_TONES.find(v => v.toLowerCase() === t);
+ if (exact) return exact as LinkedInTone;
+
+ if (t.includes('authoritative') || t.includes('expert')) return LinkedInTone.AUTHORITATIVE;
+ if (t.includes('conversational') || t.includes('casual')) return LinkedInTone.CONVERSATIONAL;
+ if (t.includes('inspirational') || t.includes('motivational')) return LinkedInTone.INSPIRATIONAL;
+ if (t.includes('educational') || t.includes('informative')) return LinkedInTone.EDUCATIONAL;
+ if (t.includes('friendly') || t.includes('approachable')) return LinkedInTone.FRIENDLY;
+
+ return LinkedInTone.PROFESSIONAL;
+}
+
+export function mapIndustry(industry: string | undefined): string {
+ const ind = normalizeEnum(industry);
+ if (!ind) return 'Technology';
+
+ const exact = VALID_INDUSTRIES.find(v => v.toLowerCase() === ind);
+ if (exact) return exact;
+
+ if (ind.includes('tech') || ind.includes('software')) return 'Technology';
+ if (ind.includes('health') || ind.includes('medical')) return 'Healthcare';
+ if (ind.includes('finance') || ind.includes('bank')) return 'Finance';
+ if (ind.includes('educat') || ind.includes('learn')) return 'Education';
+ if (ind.includes('manufactur') || ind.includes('factory')) return 'Manufacturing';
+ if (ind.includes('retail') || ind.includes('shop')) return 'Retail';
+ if (ind.includes('market') || ind.includes('brand')) return 'Marketing';
+ if (ind.includes('consult') || ind.includes('advisory')) return 'Consulting';
+ if (ind.includes('real') || ind.includes('property')) return 'Real Estate';
+ if (ind.includes('legal') || ind.includes('law')) return 'Legal';
+ if (ind.includes('non') || ind.includes('charity')) return 'Non-profit';
+ if (ind.includes('government') || ind.includes('public')) return 'Government';
+ if (ind.includes('entertain') || ind.includes('media')) return 'Entertainment';
+ if (ind.includes('sport') || ind.includes('fitness')) return 'Sports';
+ if (ind.includes('food') || ind.includes('beverage')) return 'Food & Beverage';
+ if (ind.includes('auto') || ind.includes('car')) return 'Automotive';
+ if (ind.includes('energy') || ind.includes('power')) return 'Energy';
+ if (ind.includes('telecom') || ind.includes('communication')) return 'Telecommunications';
+
+ return 'Technology';
+}
+
+export function mapSearchEngine(engine: string | undefined): SearchEngine {
+ const eng = normalizeEnum(engine);
+ if (!eng) return SearchEngine.METAPHOR;
+
+ const exact = VALID_SEARCH_ENGINES.find(v => v.toLowerCase() === eng);
+ if (exact) return exact as SearchEngine;
+
+ if (eng.includes('google')) return SearchEngine.GOOGLE;
+ if (eng.includes('tavily')) return SearchEngine.TAVILY;
+
+ return SearchEngine.METAPHOR;
+}
+
+export function mapResponseType(responseType: string | undefined): string {
+ const rt = normalizeEnum(responseType);
+ if (!rt) return 'professional';
+
+ const exact = VALID_RESPONSE_TYPES.find(v => v.toLowerCase() === rt);
+ if (exact) return exact;
+
+ if (rt.includes('appreciat') || rt.includes('thank')) return 'appreciative';
+ if (rt.includes('clarify') || rt.includes('explain')) return 'clarifying';
+ if (rt.includes('disagree') || rt.includes('differ')) return 'disagreement';
+ if (rt.includes('value') || rt.includes('add')) return 'value_add';
+
+ return 'professional';
+}
+
+// Professional content helpers
+export function getIndustryHashtags(industry: string): string[] {
+ const mappedIndustry = mapIndustry(industry);
+ const industryKey = mappedIndustry.toLowerCase().replace(/[^a-z]/g, '');
+
+ return PROFESSIONAL_HASHTAGS[industryKey as keyof typeof PROFESSIONAL_HASHTAGS] ||
+ PROFESSIONAL_HASHTAGS.general;
+}
+
+export function getProfessionalSuggestions(contentType: string, industry: string): string[] {
+ const suggestions = {
+ post: [
+ `Share insights about ${industry} trends`,
+ `Discuss professional challenges in ${industry}`,
+ `Highlight innovation in ${industry}`,
+ `Share lessons learned in ${industry}`,
+ `Discuss future of ${industry}`
+ ],
+ article: [
+ `Comprehensive guide to ${industry} best practices`,
+ `Future trends in ${industry}`,
+ `How to succeed in ${industry}`,
+ `Innovation strategies for ${industry}`,
+ `Professional development in ${industry}`
+ ],
+ carousel: [
+ `Key insights about ${industry}`,
+ `Best practices in ${industry}`,
+ `Trends shaping ${industry}`,
+ `Success strategies for ${industry}`,
+ `Innovation in ${industry}`
+ ]
+ };
+
+ return suggestions[contentType as keyof typeof suggestions] || suggestions.post;
+}
+
+// Dynamic placeholder generation based on preferences
+export function getPersonalizedPlaceholder(
+ contentType: string,
+ fieldType: string,
+ prefs: Record
+): string {
+ const industry = prefs.industry || 'Technology';
+ const tone = prefs.tone || 'professional';
+ const audience = prefs.target_audience || 'professionals';
+
+ const placeholders = {
+ post: {
+ topic: `e.g., ${industry} innovation trends for ${new Date().getFullYear()}`,
+ target_audience: `e.g., ${industry} ${audience} and decision makers`,
+ key_points: `• Key insight about ${industry}\n• Challenge ${audience} face\n• Solution or recommendation`
+ },
+ article: {
+ topic: `e.g., The Future of ${industry}: A ${tone} Guide`,
+ target_audience: `e.g., ${industry} leaders, ${audience}, and stakeholders`,
+ key_sections: `• Introduction to ${industry} landscape\n• Current challenges and opportunities\n• Best practices and recommendations\n• Future outlook and trends`
+ },
+ carousel: {
+ topic: `e.g., 5 Essential ${industry} Strategies for ${audience}`,
+ target_audience: `e.g., ${audience} in ${industry} seeking growth`,
+ key_takeaways: `• Key strategy for ${industry} success\n• Important trend ${audience} should know\n• Actionable tip for immediate implementation`
+ },
+ video: {
+ topic: `e.g., ${industry} Leadership Tips for ${audience}`,
+ target_audience: `e.g., Aspiring ${industry} leaders and ${audience}`,
+ key_messages: `• Core message about ${industry} leadership\n• Practical advice for ${audience}\n• Call to action for viewers`
+ },
+ comment: {
+ original_post: `Paste the original LinkedIn post content here (${tone} tone recommended for ${industry})`,
+ comment: `Paste the comment you want to respond to (maintain ${tone} tone)`,
+ brand_voice: `e.g., ${tone}, ${industry}-focused, expert authority`
+ }
+ };
+
+ const contentPlaceholders = placeholders[contentType as keyof typeof placeholders];
+ if (contentPlaceholders) {
+ return contentPlaceholders[fieldType as keyof typeof contentPlaceholders] ||
+ `Enter ${fieldType.replace('_', ' ')} for ${industry} content`;
+ }
+
+ return `Enter ${fieldType.replace('_', ' ')} here`;
+}
+
+// Generate personalized suggestions for CopilotKit sidebar
+export function getPersonalizedSuggestions(prefs: Record): Array<{title: string, message: string}> {
+ const industry = prefs.industry || 'Technology';
+ const tone = prefs.tone || 'professional';
+ const audience = prefs.target_audience || 'professionals';
+
+ return [
+ {
+ title: `${industry} Post`,
+ message: `Create a ${tone} LinkedIn post about ${industry} trends for ${audience}`
+ },
+ {
+ title: `${industry} Article`,
+ message: `Write a comprehensive ${tone} article about ${industry} best practices targeting ${audience}`
+ },
+ {
+ title: `${industry} Carousel`,
+ message: `Generate a visual carousel about ${industry} insights with a ${tone} tone for ${audience}`
+ },
+ {
+ title: 'Video Script',
+ message: `Create a ${tone} video script about ${industry} topics for ${audience}`
+ },
+ {
+ title: 'Comment Response',
+ message: `Help me respond to LinkedIn comments with a ${tone} tone appropriate for ${industry}`
+ }
+ ];
+}
+
+// Generate context-aware suggestions based on current state
+export function getContextAwareSuggestions(
+ prefs: Record,
+ currentDraft: string = '',
+ recentHistory: Array = [],
+ lastUsedActions: string[] = []
+): Array<{title: string, message: string, priority: 'high' | 'medium' | 'low'}> {
+ const industry = prefs.industry || 'Technology';
+ const tone = prefs.tone || 'professional';
+ const audience = prefs.target_audience || 'professionals';
+
+ const suggestions: Array<{title: string, message: string, priority: 'high' | 'medium' | 'low'}> = [];
+
+ // High Priority: Context-based suggestions
+ if (currentDraft.trim()) {
+ const draftLength = currentDraft.length;
+ const wordCount = currentDraft.split(/\s+/).length;
+
+ if (draftLength > 0 && draftLength < 100) {
+ suggestions.push({
+ title: '📝 Expand Draft',
+ message: `Help me expand this ${draftLength}-character draft into a full ${industry} post`,
+ priority: 'high'
+ });
+ } else if (wordCount > 200) {
+ suggestions.push({
+ title: '✂️ Refine Content',
+ message: `Help me refine and polish this ${wordCount}-word ${industry} content`,
+ priority: 'high'
+ });
+ }
+
+ if (currentDraft.includes('#')) {
+ suggestions.push({
+ title: '🏷️ Optimize Hashtags',
+ message: `Suggest relevant ${industry} hashtags for this content`,
+ priority: 'high'
+ });
+ }
+ }
+
+ // Medium Priority: Recent activity suggestions
+ if (recentHistory.length > 0) {
+ const lastMessage = recentHistory[recentHistory.length - 1];
+ if (lastMessage?.action === 'generateLinkedInPost') {
+ suggestions.push({
+ title: '🔄 Create Follow-up',
+ message: `Create a follow-up post to the ${industry} content we just generated`,
+ priority: 'medium'
+ });
+ } else if (lastMessage?.action === 'generateLinkedInArticle') {
+ suggestions.push({
+ title: '📊 Create Summary',
+ message: `Create a carousel summarizing the key points from the ${industry} article`,
+ priority: 'medium'
+ });
+ }
+ }
+
+ // Medium Priority: Frequently used actions
+ if (lastUsedActions.length > 0) {
+ const mostUsed = lastUsedActions[0];
+ if (mostUsed === 'generateLinkedInPost') {
+ suggestions.push({
+ title: '📱 Another Post',
+ message: `Create another ${tone} LinkedIn post for ${industry} ${audience}`,
+ priority: 'medium'
+ });
+ } else if (mostUsed === 'generateLinkedInCarousel') {
+ suggestions.push({
+ title: '🎠 New Carousel',
+ message: `Design a new ${industry} carousel with a ${tone} approach`,
+ priority: 'medium'
+ });
+ }
+ }
+
+ // Low Priority: General suggestions (fallback to personalized)
+ const personalizedSuggestions = getPersonalizedSuggestions(prefs);
+ personalizedSuggestions.forEach(suggestion => {
+ suggestions.push({
+ ...suggestion,
+ priority: 'low'
+ });
+ });
+
+ // Sort by priority and return top 8 suggestions
+ return suggestions
+ .sort((a, b) => {
+ const priorityOrder = { high: 3, medium: 2, low: 1 };
+ return priorityOrder[b.priority] - priorityOrder[a.priority];
+ })
+ .slice(0, 8);
+}
+
+// Generate smart follow-up suggestions based on content type
+export function getSmartFollowUpSuggestions(
+ contentType: string,
+ content: string,
+ prefs: Record
+): Array<{title: string, message: string}> {
+ const industry = prefs.industry || 'Technology';
+ const tone = prefs.tone || 'professional';
+
+ const followUps: Record> = {
+ post: [
+ {
+ title: '📊 Create Supporting Carousel',
+ message: `Design a carousel that expands on the key points from this ${industry} post`
+ },
+ {
+ title: '🎬 Make Video Script',
+ message: `Convert this ${industry} post into a video script for ${tone} presentation`
+ },
+ {
+ title: '💬 Generate Comment Responses',
+ message: `Prepare professional responses to potential comments on this ${industry} post`
+ }
+ ],
+ article: [
+ {
+ title: '🎠 Create Executive Summary',
+ message: `Design a carousel summarizing the main insights from this ${industry} article`
+ },
+ {
+ title: '📱 Social Media Posts',
+ message: `Create multiple social media posts highlighting key points from this ${industry} article`
+ },
+ {
+ title: '🎬 Video Content',
+ message: `Transform this ${industry} article into an engaging video script`
+ }
+ ],
+ carousel: [
+ {
+ title: '📝 Expand to Article',
+ message: `Develop this ${industry} carousel into a comprehensive article`
+ },
+ {
+ title: '📱 Create Post Series',
+ message: `Generate individual posts for each slide in this ${industry} carousel`
+ },
+ {
+ title: '🎬 Video Adaptation',
+ message: `Adapt this ${industry} carousel content for video format`
+ }
+ ]
+ };
+
+ return followUps[contentType] || followUps.post;
+}
+
+export function validateLinkedInContent(content: string): { isValid: boolean; issues: string[] } {
+ const issues: string[] = [];
+
+ if (!content || content.trim().length === 0) {
+ issues.push('Content cannot be empty');
+ }
+
+ if (content.length > 3000) {
+ issues.push('Content exceeds LinkedIn post limit (3000 characters)');
+ }
+
+ if (content.length < 50) {
+ issues.push('Content is too short for a professional post');
+ }
+
+ // Check for professional tone indicators
+ const informalWords = ['hey', 'hi', 'hello', 'cool', 'awesome', 'amazing'];
+ const hasInformalWords = informalWords.some(word =>
+ content.toLowerCase().includes(word)
+ );
+
+ if (hasInformalWords) {
+ issues.push('Consider using more professional language');
+ }
+
+ return {
+ isValid: issues.length === 0,
+ issues
+ };
+}
+
+export function formatLinkedInContent(content: string, hashtags: string[] = []): string {
+ let formatted = content.trim();
+
+ // Add hashtags if provided
+ if (hashtags.length > 0) {
+ const hashtagString = hashtags.map(tag =>
+ tag.startsWith('#') ? tag : `#${tag}`
+ ).join(' ');
+ formatted += `\n\n${hashtagString}`;
+ }
+
+ return formatted;
+}
+
+// Professional engagement helpers
+export function getEngagementTips(contentType: string): string[] {
+ const tips = {
+ post: [
+ 'Ask a thought-provoking question',
+ 'Share personal insights or experiences',
+ 'Include relevant industry statistics',
+ 'Tag relevant professionals or companies',
+ 'Use professional hashtags strategically'
+ ],
+ article: [
+ 'Start with a compelling hook',
+ 'Include actionable insights',
+ 'Use subheadings for readability',
+ 'End with a call to action',
+ 'Share personal expertise'
+ ],
+ carousel: [
+ 'Create a clear narrative flow',
+ 'Use consistent visual elements',
+ 'Include data or statistics',
+ 'End with a strong call to action',
+ 'Optimize for mobile viewing'
+ ]
+ };
+
+ return tips[contentType as keyof typeof tips] || tips.post;
+}
diff --git a/frontend/src/components/LinkedInWriter/utils/storageUtils.ts b/frontend/src/components/LinkedInWriter/utils/storageUtils.ts
new file mode 100644
index 00000000..48245f91
--- /dev/null
+++ b/frontend/src/components/LinkedInWriter/utils/storageUtils.ts
@@ -0,0 +1,160 @@
+// Storage utilities for LinkedIn Writer
+
+// Storage keys
+export const HISTORY_KEY = 'linkedinwriter:chatHistory';
+export const PREFS_KEY = 'linkedinwriter:preferences';
+export const CONTEXT_KEY = 'linkedinwriter:context';
+
+// Chat message type
+export type ChatMsg = {
+ role: 'user' | 'assistant';
+ content: string;
+ ts: number;
+ action?: string; // Track which action was used
+ result?: any; // Store action results for context
+};
+
+// User preferences interface
+export interface LinkedInPreferences {
+ tone: string;
+ industry: string;
+ target_audience: string;
+ content_goals: string[];
+ writing_style: string;
+ hashtag_preferences: boolean;
+ cta_preferences: boolean;
+ last_used_actions: string[];
+ favorite_topics: string[];
+ last_updated?: number;
+}
+
+// Default preferences
+export const defaultPreferences: LinkedInPreferences = {
+ tone: 'Professional',
+ industry: '',
+ target_audience: '',
+ content_goals: ['Engagement', 'Thought Leadership'],
+ writing_style: 'Clear and Concise',
+ hashtag_preferences: true,
+ cta_preferences: true,
+ last_used_actions: [],
+ favorite_topics: []
+};
+
+// Validation functions
+export function validateMessage(m: any): m is ChatMsg {
+ return m &&
+ typeof m.content === 'string' &&
+ (m.role === 'user' || m.role === 'assistant') &&
+ typeof m.ts === 'number';
+}
+
+// Chat history functions
+export function loadHistory(): ChatMsg[] {
+ try {
+ const raw = localStorage.getItem(HISTORY_KEY);
+ if (!raw) return [];
+ const arr = JSON.parse(raw);
+ if (!Array.isArray(arr)) return [];
+ return arr.filter(validateMessage);
+ } catch {
+ console.warn('[LinkedIn Writer] Failed to load chat history from localStorage');
+ return [];
+ }
+}
+
+export function saveHistory(msgs: ChatMsg[]) {
+ try {
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(msgs.slice(-50)));
+ } catch (error) {
+ console.warn('[LinkedIn Writer] Failed to save chat history to localStorage:', error);
+ }
+}
+
+export function pushHistory(role: 'user' | 'assistant', content: string, action?: string, result?: any) {
+ const msgs = loadHistory();
+ msgs.push({
+ role,
+ content: String(content || '').slice(0, 4000),
+ ts: Date.now(),
+ action,
+ result
+ });
+ saveHistory(msgs);
+}
+
+export function clearHistory() {
+ try {
+ localStorage.removeItem(HISTORY_KEY);
+ console.log('[LinkedIn Writer] Chat history cleared');
+ } catch (error) {
+ console.warn('[LinkedIn Writer] Failed to clear chat history:', error);
+ }
+}
+
+export function getHistoryLength(): number {
+ return loadHistory().length;
+}
+
+export function getRecentHistory(count: number = 10): ChatMsg[] {
+ return loadHistory().slice(-count);
+}
+
+// Preferences functions
+export function getPreferences(): LinkedInPreferences {
+ try {
+ const stored = localStorage.getItem(PREFS_KEY);
+ if (!stored) return defaultPreferences;
+
+ const parsed = JSON.parse(stored);
+ return { ...defaultPreferences, ...parsed };
+ } catch (error) {
+ console.warn('[LinkedIn Writer] Failed to load preferences, using defaults:', error);
+ return defaultPreferences;
+ }
+}
+
+export function savePreferences(prefs: Partial) {
+ try {
+ const current = getPreferences();
+ const updated = { ...current, ...prefs, last_updated: Date.now() };
+ localStorage.setItem(PREFS_KEY, JSON.stringify(updated));
+ console.log('[LinkedIn Writer] Preferences updated:', updated);
+ } catch (error) {
+ console.warn('[LinkedIn Writer] Failed to save preferences:', error);
+ }
+}
+
+export function updatePreference(key: keyof LinkedInPreferences, value: any) {
+ savePreferences({ [key]: value });
+}
+
+// Context functions
+export function getCurrentContext(): string {
+ try {
+ return localStorage.getItem(CONTEXT_KEY) || '';
+ } catch {
+ return '';
+ }
+}
+
+export function saveCurrentContext(context: string) {
+ try {
+ localStorage.setItem(CONTEXT_KEY, context);
+ } catch (error) {
+ console.warn('[LinkedIn Writer] Failed to save context:', error);
+ }
+}
+
+// History summarization for AI context
+export function summarizeHistory(maxChars: number = 1500): string {
+ const msgs = loadHistory();
+ if (!msgs.length) return '';
+
+ const recent = msgs.slice(-15).map(m =>
+ `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}${m.action ? ` [Action: ${m.action}]` : ''}`
+ );
+
+ const joined = recent.join('\n');
+ return joined.length > maxChars ? `${joined.slice(0, maxChars)}…` : joined;
+}
diff --git a/frontend/src/components/shared/styled.ts b/frontend/src/components/shared/styled.ts
index 730d4ad3..f6303570 100644
--- a/frontend/src/components/shared/styled.ts
+++ b/frontend/src/components/shared/styled.ts
@@ -4,10 +4,7 @@ import { styled } from '@mui/material/styles';
// Shared styled components for dashboard components
export const DashboardContainer = styled(Box)(({ theme }) => ({
minHeight: '100vh',
- background:
- 'radial-gradient(1200px 600px at 10% -10%, rgba(255,255,255,0.08) 0%, transparent 60%),\
- radial-gradient(900px 500px at 110% 10%, rgba(255,255,255,0.06) 0%, transparent 60%),\
- linear-gradient(135deg, #0f1226 0%, #1b1e3b 35%, #2a2f59 70%, #3a3f7a 100%)',
+ background: 'radial-gradient(1200px 600px at 10% -10%, rgba(255,255,255,0.08) 0%, transparent 60%), radial-gradient(900px 500px at 110% 10%, rgba(255,255,255,0.06) 0%, transparent 60%), linear-gradient(135deg, #0f1226 0%, #1b1e3b 35%, #2a2f59 70%, #3a3f7a 100%)',
padding: theme.spacing(5, 4, 6, 4),
position: 'relative',
color: 'rgba(255,255,255,0.9)',
diff --git a/frontend/src/services/facebookWriterApi.ts b/frontend/src/services/facebookWriterApi.ts
index 38eeb168..d8e96de7 100644
--- a/frontend/src/services/facebookWriterApi.ts
+++ b/frontend/src/services/facebookWriterApi.ts
@@ -146,6 +146,49 @@ export const facebookWriterApi = {
const { data } = await apiClient.post('/api/facebook-writer/event/generate', payload);
return data;
}
+ ,
+ async groupPostGenerate(payload: {
+ group_name: string;
+ group_type: string;
+ post_purpose: string;
+ business_type: string;
+ topic: string;
+ target_audience: string;
+ value_proposition: string;
+ group_rules?: { no_promotion?: boolean; value_first?: boolean; no_links?: boolean; community_focused?: boolean; relevant_only?: boolean };
+ include?: string;
+ avoid?: string;
+ call_to_action?: string;
+ }): Promise {
+ const { data } = await apiClient.post('/api/facebook-writer/group-post/generate', payload);
+ return data;
+ },
+ async pageAboutGenerate(payload: {
+ business_name: string;
+ business_category: string;
+ custom_category?: string;
+ business_description: string;
+ target_audience: string;
+ unique_value_proposition: string;
+ services_products: string;
+ company_history?: string;
+ mission_vision?: string;
+ achievements?: string;
+ page_tone: string;
+ custom_tone?: string;
+ contact_info?: {
+ website?: string;
+ phone?: string;
+ email?: string;
+ address?: string;
+ hours?: string;
+ };
+ keywords?: string;
+ call_to_action?: string;
+ }): Promise {
+ const { data } = await apiClient.post('/api/facebook-writer/page-about/generate', payload);
+ return data;
+ }
};
diff --git a/frontend/src/services/linkedInWriterApi.ts b/frontend/src/services/linkedInWriterApi.ts
new file mode 100644
index 00000000..8693f0ee
--- /dev/null
+++ b/frontend/src/services/linkedInWriterApi.ts
@@ -0,0 +1,237 @@
+import { apiClient } from '../api/client';
+
+// LinkedIn-specific enums
+export enum LinkedInPostType {
+ PROFESSIONAL = 'professional',
+ THOUGHT_LEADERSHIP = 'thought_leadership',
+ INDUSTRY_NEWS = 'industry_news',
+ PERSONAL_STORY = 'personal_story',
+ COMPANY_UPDATE = 'company_update',
+ POLL = 'poll'
+}
+
+export enum LinkedInTone {
+ PROFESSIONAL = 'professional',
+ CONVERSATIONAL = 'conversational',
+ AUTHORITATIVE = 'authoritative',
+ INSPIRATIONAL = 'inspirational',
+ EDUCATIONAL = 'educational',
+ FRIENDLY = 'friendly'
+}
+
+export enum SearchEngine {
+ METAPHOR = 'metaphor',
+ GOOGLE = 'google',
+ TAVILY = 'tavily'
+}
+
+// Request interfaces
+export interface LinkedInPostRequest {
+ topic: string;
+ industry: string;
+ post_type?: LinkedInPostType;
+ tone?: LinkedInTone;
+ target_audience?: string;
+ key_points?: string[];
+ include_hashtags?: boolean;
+ include_call_to_action?: boolean;
+ research_enabled?: boolean;
+ search_engine?: SearchEngine;
+ max_length?: number;
+}
+
+export interface LinkedInArticleRequest {
+ topic: string;
+ industry: string;
+ tone?: LinkedInTone;
+ target_audience?: string;
+ key_sections?: string[];
+ include_images?: boolean;
+ seo_optimization?: boolean;
+ research_enabled?: boolean;
+ search_engine?: SearchEngine;
+ word_count?: number;
+}
+
+export interface LinkedInCarouselRequest {
+ topic: string;
+ industry: string;
+ slide_count?: number;
+ tone?: LinkedInTone;
+ target_audience?: string;
+ key_takeaways?: string[];
+ include_cover_slide?: boolean;
+ include_cta_slide?: boolean;
+ visual_style?: string;
+}
+
+export interface LinkedInVideoScriptRequest {
+ topic: string;
+ industry: string;
+ video_length?: number;
+ tone?: LinkedInTone;
+ target_audience?: string;
+ key_messages?: string[];
+ include_hook?: boolean;
+ include_captions?: boolean;
+}
+
+export interface LinkedInCommentResponseRequest {
+ original_post: string;
+ comment: string;
+ response_type?: 'professional' | 'appreciative' | 'clarifying' | 'disagreement' | 'value_add';
+ tone?: LinkedInTone;
+ include_question?: boolean;
+ brand_voice?: string;
+}
+
+// Response interfaces
+export interface ResearchSource {
+ title: string;
+ url: string;
+ content: string;
+ relevance_score?: number;
+}
+
+export interface HashtagSuggestion {
+ hashtag: string;
+ category: string;
+ popularity_score?: number;
+}
+
+export interface ImageSuggestion {
+ description: string;
+ alt_text: string;
+ style?: string;
+ placement?: string;
+}
+
+export interface PostContent {
+ content: string;
+ character_count: number;
+ hashtags: HashtagSuggestion[];
+ call_to_action?: string;
+ engagement_prediction?: Record;
+}
+
+export interface ArticleContent {
+ title: string;
+ content: string;
+ word_count: number;
+ sections: Array>;
+ seo_metadata?: Record;
+ image_suggestions: ImageSuggestion[];
+ reading_time?: number;
+}
+
+export interface CarouselSlide {
+ slide_number: number;
+ title: string;
+ content: string;
+ visual_elements: string[];
+ design_notes?: string;
+}
+
+export interface CarouselContent {
+ title: string;
+ slides: CarouselSlide[];
+ cover_slide?: CarouselSlide;
+ cta_slide?: CarouselSlide;
+ design_guidelines: Record;
+}
+
+export interface VideoScript {
+ hook: string;
+ main_content: Array>;
+ conclusion: string;
+ captions?: string[];
+ thumbnail_suggestions: string[];
+ video_description: string;
+}
+
+export interface LinkedInPostResponse {
+ success: boolean;
+ data?: PostContent;
+ research_sources: ResearchSource[];
+ generation_metadata: Record;
+ error?: string;
+}
+
+export interface LinkedInArticleResponse {
+ success: boolean;
+ data?: ArticleContent;
+ research_sources: ResearchSource[];
+ generation_metadata: Record;
+ error?: string;
+}
+
+export interface LinkedInCarouselResponse {
+ success: boolean;
+ data?: CarouselContent;
+ generation_metadata: Record;
+ error?: string;
+}
+
+export interface LinkedInVideoScriptResponse {
+ success: boolean;
+ data?: VideoScript;
+ generation_metadata: Record;
+ error?: string;
+}
+
+export interface LinkedInCommentResponseResult {
+ success: boolean;
+ response?: string;
+ alternative_responses: string[];
+ tone_analysis?: Record;
+ generation_metadata: Record;
+ error?: string;
+}
+
+// API client
+export const linkedInWriterApi = {
+ async health(): Promise {
+ const { data } = await apiClient.get('/api/linkedin/health');
+ return data;
+ },
+
+ async generatePost(request: LinkedInPostRequest): Promise {
+ const { data } = await apiClient.post('/api/linkedin/generate-post', request);
+ return data;
+ },
+
+ async generateArticle(request: LinkedInArticleRequest): Promise {
+ const { data } = await apiClient.post('/api/linkedin/generate-article', request);
+ return data;
+ },
+
+ async generateCarousel(request: LinkedInCarouselRequest): Promise {
+ const { data } = await apiClient.post('/api/linkedin/generate-carousel', request);
+ return data;
+ },
+
+ async generateVideoScript(request: LinkedInVideoScriptRequest): Promise {
+ const { data } = await apiClient.post('/api/linkedin/generate-video-script', request);
+ return data;
+ },
+
+ async generateCommentResponse(request: LinkedInCommentResponseRequest): Promise {
+ const { data } = await apiClient.post('/api/linkedin/generate-comment-response', request);
+ return data;
+ },
+
+ async optimizeProfile(request: any): Promise {
+ const { data } = await apiClient.post('/api/linkedin/optimize-profile', request);
+ return data;
+ },
+
+ async generatePoll(request: any): Promise {
+ const { data } = await apiClient.post('/api/linkedin/generate-poll', request);
+ return data;
+ },
+
+ async generateCompanyUpdate(request: any): Promise {
+ const { data } = await apiClient.post('/api/linkedin/generate-company-update', request);
+ return data;
+ }
+};
diff --git a/frontend/src/stores/seoCopilotStore.ts b/frontend/src/stores/seoCopilotStore.ts
index 2889da9a..2cf9aec1 100644
--- a/frontend/src/stores/seoCopilotStore.ts
+++ b/frontend/src/stores/seoCopilotStore.ts
@@ -9,9 +9,6 @@ import {
PersonalizationData,
DashboardLayout,
CopilotSuggestion,
- SEOCategory,
- SEOExperienceLevel,
- BusinessType,
TimeRange,
ChartType
} from '../types/seoCopilotTypes';
diff --git a/frontend/src/types/images.d.ts b/frontend/src/types/images.d.ts
new file mode 100644
index 00000000..93dbdc74
--- /dev/null
+++ b/frontend/src/types/images.d.ts
@@ -0,0 +1,29 @@
+declare module '*.png' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.jpg' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.jpeg' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.gif' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.svg' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.ico' {
+ const src: string;
+ export default src;
+}