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

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

295
backend/api/brainstorm.py Normal file
View File

@@ -0,0 +1,295 @@
"""
Brainstorming endpoints for generating Google search prompts and running a
single grounded search to surface topic ideas. Built for reusability across
editors. Uses the existing Gemini provider modules.
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from loguru import logger
from services.llm_providers.gemini_provider import gemini_structured_json_response
try:
from services.llm_providers.gemini_grounded_provider import GeminiGroundedProvider
GROUNDED_AVAILABLE = True
except Exception:
GROUNDED_AVAILABLE = False
router = APIRouter(prefix="/api/brainstorm", tags=["Brainstorming"])
class PersonaPayload(BaseModel):
persona_name: Optional[str] = None
archetype: Optional[str] = None
core_belief: Optional[str] = None
tonal_range: Optional[Dict[str, Any]] = None
linguistic_fingerprint: Optional[Dict[str, Any]] = None
class PlatformPersonaPayload(BaseModel):
content_format_rules: Optional[Dict[str, Any]] = None
engagement_patterns: Optional[Dict[str, Any]] = None
content_types: Optional[Dict[str, Any]] = None
tonal_range: Optional[Dict[str, Any]] = None
class PromptRequest(BaseModel):
seed: str = Field(..., description="Idea seed provided by end user")
persona: Optional[PersonaPayload] = None
platformPersona: Optional[PlatformPersonaPayload] = None
count: int = Field(5, ge=3, le=10, description="Number of prompts to generate (default 5)")
class PromptResponse(BaseModel):
prompts: List[str]
@router.post("/prompts", response_model=PromptResponse)
async def generate_prompts(req: PromptRequest) -> PromptResponse:
"""Generate N high-signal Google search prompts using Gemini structured output."""
try:
persona_line = ""
if req.persona:
parts = []
if req.persona.persona_name:
parts.append(req.persona.persona_name)
if req.persona.archetype:
parts.append(f"({req.persona.archetype})")
persona_line = " ".join(parts)
platform_hints = []
if req.platformPersona and req.platformPersona.content_format_rules:
limit = req.platformPersona.content_format_rules.get("character_limit")
if limit:
platform_hints.append(f"respect LinkedIn character limit {limit}")
sys_prompt = (
"You are an expert LinkedIn strategist who crafts precise Google search prompts "
"to ideate content topics. Follow Google grounding best-practices: be specific, "
"time-bound (2024-2025), include entities, and prefer intent-rich phrasing."
)
prompt = f"""
Seed: {req.seed}
Persona: {persona_line or 'N/A'}
Guidelines:
- Generate {req.count} distinct, high-signal Google search prompts.
- Each prompt should include concrete entities (companies, tools, frameworks) when possible.
- Prefer phrasing that yields recent, authoritative sources.
- Avoid generic phrasing ("latest trends") unless combined with concrete qualifiers.
- Optimize for LinkedIn thought leadership and practicality.
{('Platform hints: ' + ', '.join(platform_hints)) if platform_hints else ''}
Return only the list of prompts.
""".strip()
schema = {
"type": "object",
"properties": {
"prompts": {
"type": "array",
"items": {"type": "string"}
}
}
}
result = gemini_structured_json_response(
prompt=prompt,
schema=schema,
temperature=0.2,
top_p=0.9,
top_k=40,
max_tokens=2048,
system_prompt=sys_prompt,
)
prompts = []
if isinstance(result, dict) and isinstance(result.get("prompts"), list):
prompts = [str(p).strip() for p in result["prompts"] if str(p).strip()]
if not prompts:
# Minimal fallback: derive simple variations
base = req.seed.strip()
prompts = [
f"Recent data-backed insights about {base}",
f"Case studies and benchmarks on {base}",
f"Implementation playbooks for {base}",
f"Common pitfalls and solutions in {base}",
f"Industry leader perspectives on {base}",
]
return PromptResponse(prompts=prompts[: req.count])
except Exception as e:
logger.error(f"Error generating brainstorm prompts: {e}")
raise HTTPException(status_code=500, detail=str(e))
class SearchRequest(BaseModel):
prompt: str = Field(..., description="Selected search prompt to run with grounding")
max_tokens: int = Field(1024, ge=256, le=4096)
class SearchResult(BaseModel):
title: Optional[str] = None
url: Optional[str] = None
snippet: Optional[str] = None
class SearchResponse(BaseModel):
results: List[SearchResult] = []
@router.post("/search", response_model=SearchResponse)
async def run_grounded_search(req: SearchRequest) -> SearchResponse:
"""Run a single grounded Google search via GeminiGroundedProvider and return normalized results."""
if not GROUNDED_AVAILABLE:
raise HTTPException(status_code=503, detail="Grounded provider not available")
try:
provider = GeminiGroundedProvider()
resp = await provider.generate_grounded_content(
prompt=req.prompt,
content_type="linkedin_post",
temperature=0.3,
max_tokens=req.max_tokens,
)
items: List[SearchResult] = []
# Normalize 'sources' if present
for s in (resp.get("sources") or []):
items.append(SearchResult(
title=s.get("title") or "Source",
url=s.get("url") or s.get("link"),
snippet=s.get("content") or s.get("snippet")
))
# Provide minimal fallback if no structured sources are returned
if not items and resp.get("content"):
items.append(SearchResult(title="Generated overview", url=None, snippet=resp.get("content")[:400]))
return SearchResponse(results=items[:10])
except Exception as e:
logger.error(f"Error in grounded search: {e}")
raise HTTPException(status_code=500, detail=str(e))
class IdeasRequest(BaseModel):
seed: str
persona: Optional[PersonaPayload] = None
platformPersona: Optional[PlatformPersonaPayload] = None
results: List[SearchResult] = []
count: int = 5
class IdeaItem(BaseModel):
prompt: str
rationale: Optional[str] = None
class IdeasResponse(BaseModel):
ideas: List[IdeaItem]
@router.post("/ideas", response_model=IdeasResponse)
async def generate_brainstorm_ideas(req: IdeasRequest) -> IdeasResponse:
"""
Create brainstorm ideas by combining persona, seed, and Google search results.
Uses gemini_structured_json_response for consistent output.
"""
try:
# Build compact search context
top_results = req.results[:5]
sources_block = "\n".join(
[
f"- {r.title or 'Source'} | {r.url or ''} | {r.snippet or ''}"
for r in top_results
]
) or "(no sources)"
persona_block = ""
if req.persona:
persona_block = (
f"Persona: {req.persona.persona_name or ''} {('(' + req.persona.archetype + ')') if req.persona.archetype else ''}\n"
)
platform_block = ""
if req.platformPersona and req.platformPersona.content_format_rules:
limit = req.platformPersona.content_format_rules.get("character_limit")
platform_block = f"LinkedIn character limit: {limit}" if limit else ""
sys_prompt = (
"You are an enterprise-grade LinkedIn strategist. Generate specific, non-generic "
"brainstorm prompts suitable for LinkedIn posts or carousels. Use the provided web "
"sources to ground ideas and the persona to align tone and style."
)
prompt = f"""
SEED IDEA: {req.seed}
{persona_block}
{platform_block}
RECENT WEB SOURCES (top {len(top_results)}):
{sources_block}
TASK:
- Propose {req.count} LinkedIn-ready brainstorm prompts tailored to the persona and grounded in the sources.
- Each prompt should be specific and actionable for 20242025.
- Prefer thought-leadership angles, contrarian takes with evidence, or practical playbooks.
- Avoid generic phrases like "latest trends" unless qualified by entities.
Return JSON with an array named ideas where each item has:
- prompt: the exact text the user can use to generate a post
- rationale: 12 sentence why this works for the audience/persona
""".strip()
schema = {
"type": "object",
"properties": {
"ideas": {
"type": "array",
"items": {
"type": "object",
"properties": {
"prompt": {"type": "string"},
"rationale": {"type": "string"},
},
},
}
},
}
result = gemini_structured_json_response(
prompt=prompt,
schema=schema,
temperature=0.2,
top_p=0.9,
top_k=40,
max_tokens=2048,
system_prompt=sys_prompt,
)
ideas: List[IdeaItem] = []
if isinstance(result, dict) and isinstance(result.get("ideas"), list):
for item in result["ideas"]:
if isinstance(item, dict) and item.get("prompt"):
ideas.append(IdeaItem(prompt=item["prompt"], rationale=item.get("rationale")))
if not ideas:
# Fallback basic ideas from seed if model returns nothing
ideas = [
IdeaItem(prompt=f"Explain why {req.seed} matters now with 2 recent stats", rationale="Timely and data-backed."),
IdeaItem(prompt=f"Common pitfalls in {req.seed} and how to avoid them", rationale="Actionable and experience-based."),
IdeaItem(prompt=f"A step-by-step playbook to implement {req.seed}", rationale="Practical value."),
IdeaItem(prompt=f"Case study: measurable impact of {req.seed}", rationale="Story + ROI."),
IdeaItem(prompt=f"Contrarian take: what most get wrong about {req.seed}", rationale="Thought leadership.")
]
return IdeasResponse(ideas=ideas[: req.count])
except Exception as e:
logger.error(f"Error generating brainstorm ideas: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -262,6 +262,75 @@ async def delete_persona(user_id: int, persona_id: int):
logger.error(f"Error deleting persona: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to delete persona: {str(e)}")
async def update_platform_persona(user_id: int, platform: str, update_data: Dict[str, Any]):
"""Update platform-specific persona fields for a user's persona.
This updates the underlying PlatformPersona row for the given platform.
"""
try:
from services.database import get_db_session
from models.persona_models import WritingPersona, PlatformPersona
session = get_db_session()
# Find the user's active core persona id
core_persona = session.query(WritingPersona).filter(
WritingPersona.user_id == user_id,
WritingPersona.is_active == True
).order_by(WritingPersona.created_at.desc()).first()
if not core_persona:
raise HTTPException(status_code=404, detail="No active persona found for user")
# Find the platform persona for the requested platform
platform_persona = session.query(PlatformPersona).filter(
PlatformPersona.writing_persona_id == core_persona.id,
PlatformPersona.platform_type.ilike(platform),
PlatformPersona.is_active == True
).first()
if not platform_persona:
raise HTTPException(status_code=404, detail=f"No platform persona found for platform {platform}")
# Update allowed platform fields
updatable_fields = [
'sentence_metrics', 'lexical_features', 'rhetorical_devices', 'tonal_range',
'stylistic_constraints', 'content_format_rules', 'engagement_patterns',
'posting_frequency', 'content_types', 'platform_best_practices', 'algorithm_considerations'
]
updated_any = False
for field in updatable_fields:
if field in update_data:
setattr(platform_persona, field, update_data[field])
updated_any = True
if not updated_any:
# Nothing to update
session.close()
return {
"message": "No updatable fields provided",
"platform": platform_persona.platform_type,
"persona_id": core_persona.id
}
platform_persona.updated_at = datetime.utcnow()
session.commit()
session.close()
return {
"message": "Platform persona updated successfully",
"platform": platform,
"persona_id": core_persona.id,
"updated_at": platform_persona.updated_at.isoformat()
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating platform persona: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to update platform persona: {str(e)}")
async def validate_persona_generation_readiness(user_id: int):
"""Check if user has sufficient onboarding data for persona generation."""
try:

View File

@@ -32,6 +32,7 @@ from api.persona import (
)
from services.persona_replication_engine import PersonaReplicationEngine
from api.persona import update_platform_persona
# Create router
router = APIRouter(prefix="/api/personas", tags=["personas"])
@@ -204,4 +205,16 @@ async def validate_content_endpoint(
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Validation failed: {str(e)}")
raise HTTPException(status_code=500, detail=f"Validation failed: {str(e)}")
@router.put("/platform/{platform}")
async def update_platform_persona_endpoint(
platform: str,
update_data: Dict[str, Any],
user_id: int = Query(1, description="User ID")
):
"""Update platform-specific persona fields for a user.
Allows editing persona fields in the UI and saving them to the database.
"""
return await update_platform_persona(user_id, platform, update_data)

View File

@@ -57,6 +57,7 @@ from api.facebook_writer.routers import facebook_router
from routers.linkedin import router as linkedin_router
# Import LinkedIn image generation router
from api.linkedin_image_generation import router as linkedin_image_router
from api.brainstorm import router as brainstorm_router
# Import hallucination detector router
from api.hallucination_detector import router as hallucination_detector_router
@@ -440,6 +441,7 @@ app.include_router(facebook_router)
app.include_router(linkedin_router)
# Include LinkedIn image generation router
app.include_router(linkedin_image_router)
app.include_router(brainstorm_router)
# Include hallucination detector router
app.include_router(hallucination_detector_router)

View File

@@ -61,6 +61,8 @@ class LinkedInPostRequest(BaseModel):
max_length: int = Field(default=3000, description="Maximum character count", ge=100, le=3000)
grounding_level: GroundingLevel = Field(default=GroundingLevel.ENHANCED, description="Level of content grounding")
include_citations: bool = Field(default=True, description="Whether to include inline citations")
user_id: Optional[int] = Field(default=1, description="User id for persona lookup")
persona_override: Optional[Dict[str, Any]] = Field(default=None, description="Session-only persona overrides to apply without saving")
class Config:
schema_extra = {
@@ -96,6 +98,8 @@ class LinkedInArticleRequest(BaseModel):
word_count: int = Field(default=1500, description="Target word count", ge=500, le=5000)
grounding_level: GroundingLevel = Field(default=GroundingLevel.ENHANCED, description="Level of content grounding")
include_citations: bool = Field(default=True, description="Whether to include inline citations")
user_id: Optional[int] = Field(default=1, description="User id for persona lookup")
persona_override: Optional[Dict[str, Any]] = Field(default=None, description="Session-only persona overrides to apply without saving")
class Config:
schema_extra = {

View File

@@ -21,6 +21,7 @@ from services.linkedin.content_generator_prompts import (
CarouselGenerator,
VideoScriptGenerator
)
from services.persona_analysis_service import PersonaAnalysisService
class ContentGenerator:
@@ -340,8 +341,27 @@ class ContentGenerator:
logger.error("Gemini Grounded Provider not available - cannot generate content without AI provider")
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
# Build the prompt for grounded generation using the new prompt builder
prompt = PostPromptBuilder.build_post_prompt(request)
# Build the prompt for grounded generation using persona if available (DB vs session override)
persona_service = PersonaAnalysisService()
persona_data = persona_service.get_persona_for_platform(user_id=getattr(request, 'user_id', 1), platform='linkedin') if hasattr(request, 'user_id') else None
if getattr(request, 'persona_override', None):
try:
# Merge shallowly: override core and platform adaptation parts
override = request.persona_override
if persona_data:
core = persona_data.get('core_persona', {})
platform_adapt = persona_data.get('platform_adaptation', {})
if 'core_persona' in override:
core.update(override['core_persona'])
if 'platform_adaptation' in override:
platform_adapt.update(override['platform_adaptation'])
persona_data['core_persona'] = core
persona_data['platform_adaptation'] = platform_adapt
else:
persona_data = override
except Exception:
pass
prompt = PostPromptBuilder.build_post_prompt(request, persona=persona_data)
# Generate grounded content using native Google Search grounding
result = await self.gemini_grounded.generate_grounded_content(
@@ -395,8 +415,26 @@ class ContentGenerator:
logger.error("Gemini Grounded Provider not available - cannot generate content without AI provider")
raise Exception("Gemini Grounded Provider not available - cannot generate content without AI provider")
# Build the prompt for grounded generation using the new prompt builder
prompt = ArticlePromptBuilder.build_article_prompt(request)
# Build the prompt for grounded generation using persona if available (DB vs session override)
persona_service = PersonaAnalysisService()
persona_data = persona_service.get_persona_for_platform(user_id=getattr(request, 'user_id', 1), platform='linkedin') if hasattr(request, 'user_id') else None
if getattr(request, 'persona_override', None):
try:
override = request.persona_override
if persona_data:
core = persona_data.get('core_persona', {})
platform_adapt = persona_data.get('platform_adaptation', {})
if 'core_persona' in override:
core.update(override['core_persona'])
if 'platform_adaptation' in override:
platform_adapt.update(override['platform_adaptation'])
persona_data['core_persona'] = core
persona_data['platform_adaptation'] = platform_adapt
else:
persona_data = override
except Exception:
pass
prompt = ArticlePromptBuilder.build_article_prompt(request, persona=persona_data)
# Generate grounded content using native Google Search grounding
result = await self.gemini_grounded.generate_grounded_content(

View File

@@ -4,14 +4,14 @@ LinkedIn Article Generation Prompts
This module contains prompt templates and builders for generating LinkedIn articles.
"""
from typing import Any
from typing import Any, Optional, Dict
class ArticlePromptBuilder:
"""Builder class for LinkedIn article generation prompts."""
@staticmethod
def build_article_prompt(request: Any) -> str:
def build_article_prompt(request: Any, persona: Optional[Dict[str, Any]] = None) -> str:
"""
Build prompt for article generation.
@@ -21,6 +21,27 @@ class ArticlePromptBuilder:
Returns:
Formatted prompt string for article generation
"""
persona_block = ""
if persona:
try:
core = persona.get('core_persona', persona)
platform_adaptation = persona.get('platform_adaptation', persona.get('platform_persona', {}))
linguistic = core.get('linguistic_fingerprint', {})
sentence_metrics = linguistic.get('sentence_metrics', {})
lexical_features = linguistic.get('lexical_features', {})
tonal_range = core.get('tonal_range', {})
persona_block = f"""
PERSONA CONTEXT:
- Persona Name: {core.get('persona_name', 'N/A')}
- Archetype: {core.get('archetype', 'N/A')}
- Core Belief: {core.get('core_belief', 'N/A')}
- Default Tone: {tonal_range.get('default_tone', request.tone)}
- Avg Sentence Length: {sentence_metrics.get('average_sentence_length_words', 18)} words
- Go-to Words: {', '.join(lexical_features.get('go_to_words', [])[:5])}
""".rstrip()
except Exception:
persona_block = ""
prompt = f"""
You are a senior content strategist and industry expert specializing in {request.industry}. Create a comprehensive, thought-provoking LinkedIn article that establishes authority, drives engagement, and provides genuine value to professionals in this field.
@@ -30,6 +51,8 @@ class ArticlePromptBuilder:
TARGET AUDIENCE: {request.target_audience or 'Industry professionals, executives, and thought leaders'}
WORD COUNT: {request.word_count} words
{persona_block}
CONTENT STRUCTURE:
- Compelling headline that promises specific value
- Engaging introduction with a hook and clear value proposition

View File

@@ -4,14 +4,14 @@ LinkedIn Post Generation Prompts
This module contains prompt templates and builders for generating LinkedIn posts.
"""
from typing import Any
from typing import Any, Optional, Dict
class PostPromptBuilder:
"""Builder class for LinkedIn post generation prompts."""
@staticmethod
def build_post_prompt(request: Any) -> str:
def build_post_prompt(request: Any, persona: Optional[Dict[str, Any]] = None) -> str:
"""
Build prompt for post generation.
@@ -21,6 +21,33 @@ class PostPromptBuilder:
Returns:
Formatted prompt string for post generation
"""
persona_block = ""
if persona:
try:
# Expecting structure similar to persona_service.get_persona_for_platform output
core = persona.get('core_persona', persona)
platform_adaptation = persona.get('platform_adaptation', persona.get('platform_persona', {}))
linguistic = core.get('linguistic_fingerprint', {})
sentence_metrics = linguistic.get('sentence_metrics', {})
lexical_features = linguistic.get('lexical_features', {})
rhetorical_devices = linguistic.get('rhetorical_devices', {})
tonal_range = core.get('tonal_range', {})
persona_block = f"""
PERSONA CONTEXT:
- Persona Name: {core.get('persona_name', 'N/A')}
- Archetype: {core.get('archetype', 'N/A')}
- Core Belief: {core.get('core_belief', 'N/A')}
- Tone: {tonal_range.get('default_tone', request.tone)}
- Sentence Length (avg): {sentence_metrics.get('average_sentence_length_words', 15)} words
- Preferred Sentence Type: {sentence_metrics.get('preferred_sentence_type', 'simple and compound')}
- Go-to Words: {', '.join(lexical_features.get('go_to_words', [])[:5])}
- Avoid Words: {', '.join(lexical_features.get('avoid_words', [])[:5])}
- Rhetorical Style: {rhetorical_devices.get('summary','balanced rhetorical questions and examples')}
""".rstrip()
except Exception:
persona_block = ""
prompt = f"""
You are an expert LinkedIn content strategist with 10+ years of experience in the {request.industry} industry. Create a highly engaging, professional LinkedIn post that drives meaningful engagement and establishes thought leadership.
@@ -30,6 +57,8 @@ class PostPromptBuilder:
TARGET AUDIENCE: {request.target_audience or 'Industry professionals, decision-makers, and thought leaders'}
MAX LENGTH: {request.max_length} characters
{persona_block}
CONTENT REQUIREMENTS:
- Start with a compelling hook that addresses a pain point or opportunity
- Include 2-3 specific, actionable insights or data points

View File

@@ -254,12 +254,8 @@ class GeminiGroundedProvider:
result['sources'] = sources
logger.info(f"Extracted {len(sources)} sources")
else:
logger.error("❌ CRITICAL: No grounding chunks found in response")
logger.error(f"Grounding metadata structure: {dir(grounding_metadata)}")
if hasattr(grounding_metadata, 'grounding_chunks'):
logger.error(f"Grounding chunks type: {type(grounding_metadata.grounding_chunks)}")
logger.error(f"Grounding chunks value: {grounding_metadata.grounding_chunks}")
raise ValueError("No grounding chunks found - grounding is not working properly")
logger.warning("⚠️ No grounding chunks found in response. Proceeding with available data (search queries/content).")
# Keep sources empty but continue. This avoids hard failure when Google Search tool returns queries only.
# Extract citations from grounding supports
if hasattr(grounding_metadata, 'grounding_supports') and grounding_metadata.grounding_supports:
@@ -278,12 +274,7 @@ class GeminiGroundedProvider:
result['citations'] = citations
logger.info(f"Extracted {len(citations)} citations")
else:
logger.error("❌ CRITICAL: No grounding supports found in response")
logger.error(f"Grounding metadata structure: {dir(grounding_metadata)}")
if hasattr(grounding_metadata, 'grounding_supports'):
logger.error(f"Grounding supports type: {type(grounding_metadata.grounding_supports)}")
logger.error(f"Grounding supports value: {grounding_metadata.grounding_supports}")
raise ValueError("No grounding supports found - grounding is not working properly")
logger.warning("⚠️ No grounding supports found in response. Continuing without inline citations.")
logger.info(f"✅ Successfully extracted {len(result['sources'])} sources and {len(result['citations'])} citations from grounding metadata")
logger.info(f"Sources: {result['sources']}")
@@ -294,9 +285,7 @@ class GeminiGroundedProvider:
logger.error(f"First candidate structure: {dir(candidates[0]) if candidates else 'No candidates'}")
raise ValueError("No grounding metadata found - grounding is not working properly")
else:
logger.error("❌ CRITICAL: No candidates found in response")
logger.error(f"Response structure: {dir(response)}")
raise ValueError("No candidates found in response - grounding is not working properly")
logger.warning("⚠️ No candidates found in response. Returning content without sources.")
# Add content-specific processing
if content_type == "linkedin_post":

View File

@@ -94,7 +94,7 @@ When using Facebook writer, you'll have access to:
Your persona integrates with CopilotKit to provide intelligent, contextual assistance:
#### **Contextual Conversations**
- **Persona-Aware Responses**: The AI understands your writing style and preferences
- **Persona-Aware Responses**: The AI understands your writing style and preferences
- **Platform-Specific Suggestions**: Recommendations tailored to the platform you're using
- **Real-Time Optimization**: Live suggestions for improving your content
- **Interactive Guidance**: Step-by-step assistance for content creation

View File

@@ -6,7 +6,7 @@ import './styles/alwrity-copilot.css';
import RegisterLinkedInActions from './RegisterLinkedInActions';
import RegisterLinkedInEditActions from './RegisterLinkedInEditActions';
import RegisterLinkedInActionsEnhanced from './RegisterLinkedInActionsEnhanced';
import { Header, ContentEditor, LoadingIndicator, WelcomeMessage, ProgressTracker } from './components';
import { Header, ContentEditor, LoadingIndicator, WelcomeMessage, ProgressTracker, CopilotActions } from './components';
import { useLinkedInWriter } from './hooks/useLinkedInWriter';
import { useCopilotPersistence } from './utils/enhancedPersistence';
import { PlatformPersonaProvider, usePlatformPersonaContext } from '../shared/PersonaContext/PlatformPersonaProvider';
@@ -83,11 +83,9 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
// Get persona context for enhanced AI assistance
const { corePersona, platformPersona, loading: personaLoading } = usePlatformPersonaContext();
// Get enhanced persistence functionality
const {
persistenceManager,
copilotContext,
saveChatHistory,
loadChatHistory,
addChatMessage,
@@ -227,171 +225,17 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
}
});
// Allow Copilot to edit the draft with specific operations
useCopilotActionTyped({
name: 'editLinkedInDraft',
description: 'Apply a quick style or structural edit to the current LinkedIn draft',
parameters: [
{ name: 'operation', type: 'string', description: 'The edit operation to perform', required: true, enum: ['Casual', 'Professional', 'TightenHook', 'AddCTA', 'Shorten', 'Lengthen'] }
],
handler: async ({ operation }: { operation: string }) => {
const currentDraft = draft || '';
if (!currentDraft) {
return { success: false, message: 'No draft content to edit' };
}
let editedContent = currentDraft;
switch (operation) {
case 'Casual':
editedContent = currentDraft.replace(/\b(utilize|implement|facilitate|leverage)\b/gi, (match) => {
const casual = { utilize: 'use', implement: 'put in place', facilitate: 'help', leverage: 'use' };
return casual[match.toLowerCase() as keyof typeof casual] || match;
});
editedContent = editedContent.replace(/\./g, '! 😊');
break;
case 'Professional':
editedContent = currentDraft.replace(/\b(use|put in place|help)\b/gi, (match) => {
const professional = { use: 'utilize', 'put in place': 'implement', help: 'facilitate' };
return professional[match.toLowerCase() as keyof typeof professional] || match;
});
editedContent = editedContent.replace(/! 😊/g, '.');
break;
case 'TightenHook':
const lines = currentDraft.split('\n');
if (lines.length > 0) {
const firstLine = lines[0];
const tightened = firstLine.length > 100 ? firstLine.substring(0, 100) + '...' : firstLine;
lines[0] = tightened;
editedContent = lines.join('\n');
}
break;
case 'AddCTA':
if (!/\b(call now|sign up|join|try|learn more|cta|comment|share|connect|message|dm|reach out)\b/i.test(currentDraft)) {
editedContent = currentDraft + '\n\nWhat are your thoughts on this? Share your experience in the comments below!';
}
break;
case 'Shorten':
if (currentDraft.length > 200) {
editedContent = currentDraft.substring(0, 200) + '...';
}
break;
case 'Lengthen':
if (currentDraft.length < 500) {
editedContent = currentDraft + '\n\nThis approach has shown remarkable results in our industry. The key is to maintain consistency while adapting to changing market conditions.';
}
break;
default:
return { success: false, message: 'Unknown operation' };
}
// Use the edit action to show the diff preview
window.dispatchEvent(new CustomEvent('linkedinwriter:applyEdit', {
detail: { target: editedContent }
}));
return { success: true, message: `Draft ${operation.toLowerCase()} applied`, content: editedContent };
}
// Initialize CopilotActions component to handle all copilot-related functionality
const getIntelligentSuggestions = CopilotActions({
draft,
context,
userPreferences,
justGeneratedContent,
handleContextChange,
setDraft
});
// Intelligent, stage-aware suggestions (memoized to prevent infinite re-rendering)
const getIntelligentSuggestions = useMemo(() => {
const hasContent = draft && draft.trim().length > 0;
const hasCTA = /\b(call now|sign up|join|try|learn more|cta|comment|share|connect|message|dm|reach out)\b/i.test(draft || '');
const hasHashtags = /#[A-Za-z0-9_]+/.test(draft || '');
const isLong = (draft || '').length > 500;
// Debug logging for suggestions
if (DEBUG_LINKEDIN) console.log('[LinkedIn Writer] Generating suggestions:', {
hasContent,
justGeneratedContent,
draftLength: draft?.length || 0
});
if (!hasContent) {
// Initial suggestions for content creation
const initialSuggestions = [
{ title: '📝 LinkedIn Post', message: 'Use tool generateLinkedInPost to create a professional LinkedIn post for your industry.' },
{ title: '📄 Article', message: 'Use tool generateLinkedInArticle to write a thought leadership article.' },
{ title: '🎠 Carousel', message: 'Use tool generateLinkedInCarousel to create a multi-slide carousel presentation.' },
{ title: '🎬 Video Script', message: 'Use tool generateLinkedInVideoScript to draft a video script for LinkedIn.' },
{ title: '💬 Comment Response', message: 'Use tool generateLinkedInCommentResponse to craft a professional comment reply.' },
{ title: '🖼️ Generate Post Image', message: 'Use tool generateLinkedInImagePrompts to create professional images for your LinkedIn content.' },
{ title: '🎨 Visual Content', message: 'Create engaging visual content with AI-generated images optimized for LinkedIn.' }
];
console.log('[LinkedIn Writer] Initial suggestions:', initialSuggestions);
return initialSuggestions;
} else {
// Refinement suggestions for existing content - use direct edit actions
const refinementSuggestions = [
{ title: '🙂 Make it casual', message: 'Use tool editLinkedInDraft with operation Casual' },
{ title: '💼 Make it professional', message: 'Use tool editLinkedInDraft with operation Professional' },
{ title: '✨ Tighten hook', message: 'Use tool editLinkedInDraft with operation TightenHook' },
{ title: '📣 Add a CTA', message: 'Use tool editLinkedInDraft with operation AddCTA' },
{ title: '✂️ Shorten', message: 'Use tool editLinkedInDraft with operation Shorten' },
{ title: ' Lengthen', message: 'Use tool editLinkedInDraft with operation Lengthen' }
];
// Add special suggestions when content was just generated
if (justGeneratedContent) {
console.log('[LinkedIn Writer] Adding post-generation suggestions');
refinementSuggestions.unshift(
{
title: '🎉 Content Generated! Next Steps:',
message: 'Great! Your content is ready. Now let\'s enhance it with images and make it perfect for LinkedIn.'
},
{
title: '🖼️ Generate Post Image',
message: 'Use tool generateLinkedInImagePrompts to create professional images for this LinkedIn post'
}
);
}
// Add contextual suggestions based on content analysis
if (!hasCTA) {
refinementSuggestions.push({ title: '📣 Add CTA', message: 'Use tool editLinkedInDraft with operation AddCTA' });
}
if (!hasHashtags) {
refinementSuggestions.push({ title: '🏷️ Add hashtags', message: 'Use tool addLinkedInHashtags' });
}
if (isLong) {
refinementSuggestions.push({ title: '📝 Summarize intro', message: 'Use tool editLinkedInDraft with operation Shorten' });
}
// Add image generation suggestion when there's content
if (draft && draft.trim().length > 0) {
if (DEBUG_LINKEDIN) console.log('[LinkedIn Writer] Adding image generation suggestion');
// Make image generation suggestion more prominent
refinementSuggestions.push({
title: '🖼️ Generate Post Image',
message: 'Use tool generateLinkedInImagePrompts to create professional images for this LinkedIn post'
});
// Add contextual image suggestions based on content type
if (draft.includes('digital transformation') || draft.includes('technology') || draft.includes('innovation')) {
refinementSuggestions.push({
title: '🚀 Tech-Focused Image',
message: 'Use tool generateLinkedInImagePrompts to create technology-themed professional images for this post'
});
} else if (draft.includes('business') || draft.includes('strategy') || draft.includes('growth')) {
refinementSuggestions.push({
title: '💼 Business Image',
message: 'Use tool generateLinkedInImagePrompts to create business-focused professional images for this post'
});
}
}
if (DEBUG_LINKEDIN) console.log('[LinkedIn Writer] Final suggestions:', refinementSuggestions);
return refinementSuggestions;
}
}, [draft, justGeneratedContent]);
return (
<div className={`linkedin-writer ${className}`} style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
@@ -418,6 +262,7 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
</div>
{/* Debug: Enhanced Persistence Test Buttons (remove in production) */}
@@ -470,6 +315,7 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
{/* Enhanced Persona-Aware Actions */}
<RegisterLinkedInActionsEnhanced />
{/* CopilotKit Sidebar */}
<CopilotSidebar
className="alwrity-copilot-sidebar linkedin-writer"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,5 @@
import React, { useMemo, useState, useEffect } from 'react';
// Extend HTMLDivElement interface for custom tooltip properties
interface ExtendedDivElement extends HTMLDivElement {
_researchTooltip?: HTMLDivElement | null;
_citationsTooltip?: HTMLDivElement | null;
_searchQueriesTooltip?: HTMLDivElement | null;
_qualityTooltip?: HTMLDivElement | null;
_researchTooltipTimeout?: NodeJS.Timeout | null;
_qualityTooltipTimeout?: NodeJS.Timeout | null;
}
import React from 'react';
import { MainContentPreviewHeader, ContentPreviewHeaderWithModals } from './ContentPreviewHeaderComponents/index';
interface ContentPreviewHeaderProps {
researchSources?: any[];
@@ -23,828 +14,11 @@ interface ContentPreviewHeaderProps {
topic?: string;
}
const ContentPreviewHeader: React.FC<ContentPreviewHeaderProps> = ({
researchSources,
citations,
searchQueries,
qualityMetrics,
draft,
showPreview,
onPreviewToggle,
assistantOn,
onAssistantToggle,
topic
}) => {
const formatPercent = (v?: number) => typeof v === 'number' ? `${Math.round(v * 100)}%` : '—';
const getChipColor = (v?: number) => {
if (typeof v !== 'number') return '#6b7280';
if (v >= 0.8) return '#10b981';
if (v >= 0.6) return '#f59e0b';
return '#ef4444';
};
// Memoize chips array to prevent infinite re-rendering
const chips = useMemo(() => {
const chipArray = qualityMetrics ? [
{ label: 'Overall', value: qualityMetrics.overall_score },
{ label: 'Accuracy', value: qualityMetrics.factual_accuracy },
{ label: 'Verification', value: qualityMetrics.source_verification },
{ label: 'Coverage', value: qualityMetrics.citation_coverage }
] : [];
console.log('🔍 [ContentPreviewHeader] Chips array created:', {
qualityMetrics: qualityMetrics,
chips: chipArray,
chipsLength: chipArray.length
});
return chipArray;
}, [qualityMetrics]);
// Helper to build descriptive chip tooltip text
const chipDescriptions: Record<string, string> = {
Overall: 'Overall blends accuracy, verification and coverage into a single reliability score for this draft.',
Accuracy: 'Factual Accuracy estimates how likely statements are to be factually correct based on grounding signals.',
Verification: 'Source Verification reflects how well claims are linked to credible sources and whether citations match claims.',
Coverage: 'Citation Coverage indicates how much of the content is supported with citations. Higher is better.'
};
return (
<div style={{
padding: '12px 16px',
background: '#e1f5fe',
borderBottom: '1px solid #b3e5fc',
fontSize: '12px',
fontWeight: '600',
color: '#0277bd',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<span>{topic ? `${topic} - LinkedIn Content Preview` : 'LinkedIn Content Preview'}</span>
{/* Research Chip with Hover Sub-chips */}
{((researchSources && researchSources.length > 0) || (citations && citations.length > 0) || (searchQueries && searchQueries.length > 0)) && (
<div style={{ position: 'relative' }}>
{/* Main Research Chip */}
<div
style={{
background: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)',
border: '1px solid #0284c7',
borderRadius: '999px',
padding: '6px 14px',
fontSize: '11px',
fontWeight: '700',
color: 'white',
cursor: 'pointer',
transition: 'all 0.3s ease',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '6px',
boxShadow: '0 2px 8px rgba(14, 165, 233, 0.3)',
transform: 'translateZ(0)',
userSelect: 'none'
}}
title="Research data available. Hover to see sources, citations, and queries."
onMouseEnter={(e) => {
// Clear any existing timeout
const target = e.currentTarget as ExtendedDivElement;
if (target._researchTooltipTimeout) {
clearTimeout(target._researchTooltipTimeout);
target._researchTooltipTimeout = null;
}
// Create and show research sub-chips tooltip
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
z-index: 100000;
background: white;
border: 1px solid #cfe9f7;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
padding: 16px;
max-width: 400px;
font-size: 12px;
opacity: 0;
transform: translateY(-8px);
transition: all 0.2s ease;
pointer-events: auto;
`;
let subChipsHtml = '<div style="margin-bottom: 12px; font-weight: 600; color: #0a66c2; font-size: 14px;">Research Data</div>';
// Add Sources sub-chip
if (researchSources && researchSources.length > 0) {
subChipsHtml += `
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
onmouseenter="this.style.background='#e0f2fe'; this.style.transform='scale(1.05)'"
onmouseleave="this.style.background='#f0f9ff'; this.style.transform='scale(1)'"
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showResearchSourcesModal', { detail: 'sources' }))">
<span style="display: inline-block; width: 6px; height: 6px; background: #10b981; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);"></span>
Sources: ${researchSources.length}
</div>
`;
}
// Add Citations sub-chip
if (citations && citations.length > 0) {
subChipsHtml += `
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
onmouseenter="this.style.background='#fde68a'; this.style.transform='scale(1.05)'"
onmouseleave="this.style.background='#fef3c7'; this.style.transform='scale(1)'"
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showCitationsModal', { detail: 'citations' }))">
<span style="display: inline-block; width: 6px; height: 6px; background: #f59e0b; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(245, 158, 11, 0.5);"></span>
Citations: ${citations.length}
</div>
`;
}
// Add Queries sub-chip
if (searchQueries && searchQueries.length > 0) {
subChipsHtml += `
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #f3e8ff; border: 1px solid #8b5cf6; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
onmouseenter="this.style.background='#e9d5ff'; this.style.transform='scale(1.05)'"
onmouseleave="this.style.background='#f3e8ff'; this.style.transform='scale(1)'"
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showSearchQueriesModal', { detail: 'queries' }))">
<span style="display: inline-block; width: 6px; height: 6px; background: #8b5cf6; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(139, 92, 246, 0.5);"></span>
Queries: ${searchQueries.length}
</div>
`;
}
tooltip.innerHTML = subChipsHtml;
// Add mouse events to tooltip to keep it visible
tooltip.addEventListener('mouseenter', () => {
if (target._researchTooltipTimeout) {
clearTimeout(target._researchTooltipTimeout);
target._researchTooltipTimeout = null;
}
});
tooltip.addEventListener('mouseleave', () => {
target._researchTooltipTimeout = setTimeout(() => {
if (tooltip.parentNode) {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-8px)';
setTimeout(() => {
if (tooltip.parentNode) {
tooltip.remove();
}
}, 200);
}
target._researchTooltip = null;
}, 100);
});
document.body.appendChild(tooltip);
const rect = e.currentTarget.getBoundingClientRect();
tooltip.style.left = Math.min(rect.left, window.innerWidth - 420) + 'px';
tooltip.style.top = (rect.bottom + 8) + 'px';
// Animate in
setTimeout(() => {
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(0)';
}, 10);
target._researchTooltip = tooltip;
}}
onMouseLeave={(e) => {
const target = e.currentTarget as ExtendedDivElement;
if (target._researchTooltip) {
// Add delay before hiding to allow moving to tooltip
target._researchTooltipTimeout = setTimeout(() => {
const tooltip = target._researchTooltip;
if (tooltip && tooltip.parentNode) {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-8px)';
setTimeout(() => {
if (tooltip.parentNode) {
tooltip.remove();
}
}, 200);
}
target._researchTooltip = null;
}, 100);
}
}}
onMouseMove={(e) => {
// Keep tooltip visible when moving to sub-chips
const target = e.currentTarget as ExtendedDivElement;
if (target._researchTooltip) {
const tooltip = target._researchTooltip;
const rect = e.currentTarget.getBoundingClientRect();
tooltip.style.left = Math.min(rect.left, window.innerWidth - 420) + 'px';
tooltip.style.top = (rect.bottom + 8) + 'px';
}
}}
onMouseOver={(e) => {
// Add hover effect to the chip itself
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(14, 165, 233, 0.4)';
}}
onMouseOut={(e) => {
// Remove hover effect
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.3)';
}}
>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: 'rgba(255, 255, 255, 0.9)',
flexShrink: 0,
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
}} />
Research
</div>
</div>
)}
</div>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
{/* Quality Metrics Chip */}
{chips.length > 0 && (
<div style={{ position: 'relative' }}>
{/* Main Quality Metrics Chip */}
<div
style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
border: '1px solid #047857',
borderRadius: '999px',
padding: '6px 14px',
fontSize: '11px',
fontWeight: '700',
color: 'white',
cursor: 'pointer',
transition: 'all 0.3s ease',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '6px',
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
transform: 'translateZ(0)',
userSelect: 'none'
}}
title="Quality metrics available. Hover to see detailed progress bars and explanations."
onMouseEnter={(e) => {
// Clear any existing timeout
const target = e.currentTarget as ExtendedDivElement;
if (target._qualityTooltipTimeout) {
clearTimeout(target._qualityTooltipTimeout);
target._qualityTooltipTimeout = null;
}
// Create and show quality metrics tooltip with circular progress bars
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
z-index: 100000;
background: white;
border: 1px solid #d1fae5;
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
padding: 24px;
max-width: 500px;
font-size: 12px;
opacity: 0;
transform: translateY(-8px);
transition: all 0.2s ease;
pointer-events: auto;
`;
// Create circular progress bars for each metric
const createCircularProgress = (label: string, value: number, description: string) => {
const percentage = Math.round(value * 100);
const color = getChipColor(value);
const circumference = 2 * Math.PI * 45; // radius = 45
const strokeDasharray = circumference;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
return `
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding: 12px; background: #f8fafc; border-radius: 12px; border-left: 4px solid ${color};">
<div style="position: relative; width: 60px; height: 60px;">
<svg width="60" height="60" style="transform: rotate(-90deg);">
<circle cx="30" cy="30" r="45" stroke="#e5e7eb" stroke-width="6" fill="none"/>
<circle cx="30" cy="30" r="45" stroke="${color}" stroke-width="6" fill="none"
stroke-dasharray="${strokeDasharray}" stroke-dashoffset="${strokeDashoffset}"
style="transition: stroke-dashoffset 0.5s ease;"/>
</svg>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-weight: 700; font-size: 14px; color: ${color};">
${percentage}%
</div>
</div>
<div style="flex: 1;">
<div style="font-weight: 700; color: #1f2937; margin-bottom: 4px; font-size: 14px;">${label}</div>
<div style="color: #6b7280; line-height: 1.4; font-size: 11px;">${description}</div>
</div>
</div>
`;
};
let progressBarsHtml = '<div style="margin-bottom: 16px; font-weight: 700; color: #059669; font-size: 16px; text-align: center;">Quality Metrics</div>';
chips.forEach(chip => {
progressBarsHtml += createCircularProgress(
chip.label,
chip.value || 0,
chipDescriptions[chip.label] || ''
);
});
tooltip.innerHTML = progressBarsHtml;
// Add mouse events to tooltip to keep it visible
tooltip.addEventListener('mouseenter', () => {
if (target._qualityTooltipTimeout) {
clearTimeout(target._qualityTooltipTimeout);
target._qualityTooltipTimeout = null;
}
});
tooltip.addEventListener('mouseleave', () => {
target._qualityTooltipTimeout = setTimeout(() => {
if (tooltip.parentNode) {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-8px)';
setTimeout(() => {
if (tooltip.parentNode) {
tooltip.remove();
}
}, 200);
}
target._qualityTooltip = null;
}, 100);
});
document.body.appendChild(tooltip);
const rect = e.currentTarget.getBoundingClientRect();
tooltip.style.left = Math.min(rect.left, window.innerWidth - 520) + 'px';
tooltip.style.top = (rect.bottom + 8) + 'px';
// Animate in
setTimeout(() => {
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(0)';
}, 10);
target._qualityTooltip = tooltip;
}}
onMouseLeave={(e) => {
const target = e.currentTarget as ExtendedDivElement;
if (target._qualityTooltip) {
// Add delay before hiding to allow moving to tooltip
target._qualityTooltipTimeout = setTimeout(() => {
const tooltip = target._qualityTooltip;
if (tooltip && tooltip.parentNode) {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-8px)';
setTimeout(() => {
if (tooltip.parentNode) {
tooltip.remove();
}
}, 200);
}
target._qualityTooltip = null;
}, 100);
}
}}
onMouseMove={(e) => {
// Keep tooltip visible when moving to progress bars
const target = e.currentTarget as ExtendedDivElement;
if (target._qualityTooltip) {
const tooltip = target._qualityTooltip;
const rect = e.currentTarget.getBoundingClientRect();
tooltip.style.left = Math.min(rect.left, window.innerWidth - 520) + 'px';
tooltip.style.top = (rect.bottom + 8) + 'px';
}
}}
onMouseOver={(e) => {
// Add hover effect to the chip itself
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(16, 185, 129, 0.4)';
}}
onMouseOut={(e) => {
// Remove hover effect
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.3)';
}}
>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: 'rgba(255, 255, 255, 0.9)',
flexShrink: 0,
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
}} />
Quality Metrics
</div>
</div>
)}
<span style={{ fontSize: '10px', opacity: 0.8 }}>
{draft.split(/\s+/).length} words {Math.ceil(draft.split(/\s+/).length / 200)} min read
</span>
{/* Assistive Writing toggle */}
{onAssistantToggle && (
<label
style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#666', cursor: 'pointer' }}
title="Assistive Writing: Get real-time AI-powered writing suggestions as you type. Uses Exa.ai for web research and Gemini for intelligent content generation. Automatically enables editing mode to allow typing and content modification."
>
<input
type="checkbox"
checked={assistantOn || false}
onChange={(e) => onAssistantToggle(e.target.checked)}
/>
Assistive Writing
</label>
)}
<label
style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#666', cursor: 'pointer' }}
title="Toggle preview visibility"
>
<input
type="checkbox"
checked={!showPreview}
onChange={() => onPreviewToggle()}
style={{ margin: 0 }}
/>
Hide Preview
</label>
</div>
</div>
);
};
// Research Sources Modal Component
const ResearchSourcesModal: React.FC<{ sources: any[]; isOpen: boolean; onClose: () => void }> = ({ sources, isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000000
}} onClick={onClose}>
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '24px',
maxWidth: '600px',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
}} onClick={(e) => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
Research Sources ({sources.length})
</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#666',
padding: '0',
width: '30px',
height: '30px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
×
</button>
</div>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
{sources && Array.isArray(sources) ? sources.map((source, idx) => (
<div key={idx} style={{
marginBottom: '16px',
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
borderLeft: '4px solid #0a66c2'
}}>
<div style={{ fontWeight: '600', marginBottom: '8px', color: '#0a66c2' }}>
{source.title || 'Untitled Source'}
</div>
<div style={{ color: '#666', marginBottom: '12px', lineHeight: '1.5' }}>
{source.content || 'No description available'}
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{source.relevance_score && (
<span style={{
backgroundColor: '#eef6ff',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '11px',
fontWeight: '600',
color: '#0a66c2'
}}>
Relevance: {Math.round(source.relevance_score * 100)}%
</span>
)}
{source.credibility_score && (
<span style={{
backgroundColor: '#eef6ff',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '11px',
fontWeight: '600',
color: '#0a66c2'
}}>
Credibility: {Math.round(source.credibility_score * 100)}%
</span>
)}
{source.domain_authority && (
<span style={{
backgroundColor: '#eef6ff',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '11px',
fontWeight: '600',
color: '#0a66c2'
}}>
Authority: {Math.round(source.domain_authority * 100)}%
</span>
)}
</div>
</div>
)) : (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
No research sources available
</div>
)}
</div>
</div>
</div>
);
};
// Citations Modal Component
const CitationsModal: React.FC<{ citations: any[]; isOpen: boolean; onClose: () => void }> = ({ citations, isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000000
}} onClick={onClose}>
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '24px',
maxWidth: '500px',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
}} onClick={(e) => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
Citations ({citations.length})
</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#666',
padding: '0',
width: '30px',
height: '30px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
×
</button>
</div>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
{citations && Array.isArray(citations) ? citations.map((citation, idx) => (
<div key={idx} style={{
marginBottom: '12px',
padding: '12px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
borderLeft: '3px solid #f59e0b'
}}>
<div style={{ fontWeight: '600', color: '#0a66c2', marginBottom: '4px' }}>
Citation {idx + 1}
</div>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>
Type: {citation.type || 'inline'}
</div>
{citation.reference && (
<div style={{ color: '#666', fontSize: '12px' }}>
Reference: {citation.reference}
</div>
)}
</div>
)) : (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
No citations available
</div>
)}
</div>
</div>
</div>
);
};
// Search Queries Modal Component
const SearchQueriesModal: React.FC<{ queries: string[]; isOpen: boolean; onClose: () => void }> = ({ queries, isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000000
}} onClick={onClose}>
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '24px',
maxWidth: '500px',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
}} onClick={(e) => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
Search Queries Used ({queries.length})
</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#666',
padding: '0',
width: '30px',
height: '30px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
×
</button>
</div>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
{queries && Array.isArray(queries) ? queries.map((query, idx) => (
<div key={idx} style={{
marginBottom: '12px',
padding: '12px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
borderLeft: '3px solid #8b5cf6'
}}>
<div style={{ fontWeight: '600', color: '#7c3aed', marginBottom: '6px' }}>
Query {idx + 1}
</div>
<div style={{ color: '#374151', fontSize: '13px', lineHeight: '1.4' }}>
{query}
</div>
</div>
)) : (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
No search queries available
</div>
)}
</div>
</div>
</div>
);
};
// Enhanced ContentPreviewHeader with Modal State
const ContentPreviewHeaderWithModals: React.FC<ContentPreviewHeaderProps> = (props) => {
const [showResearchSourcesModal, setShowResearchSourcesModal] = useState(false);
const [showCitationsModal, setShowCitationsModal] = useState(false);
const [showSearchQueriesModal, setShowSearchQueriesModal] = useState(false);
const [modalData, setModalData] = useState<any>(null);
useEffect(() => {
const handleShowResearchSourcesModal = (event: CustomEvent) => {
try {
const dataType = event.detail;
let data: any[] = [];
if (dataType === 'sources') {
data = props.researchSources || [];
}
setModalData(Array.isArray(data) ? data : []);
setShowResearchSourcesModal(true);
} catch (error) {
console.error('Error handling research sources modal:', error);
setModalData([]);
setShowResearchSourcesModal(true);
}
};
const handleShowCitationsModal = (event: CustomEvent) => {
try {
const dataType = event.detail;
let data: any[] = [];
if (dataType === 'citations') {
data = props.citations || [];
}
setModalData(Array.isArray(data) ? data : []);
setShowCitationsModal(true);
} catch (error) {
console.error('Error handling citations modal:', error);
setModalData([]);
setShowCitationsModal(true);
}
};
const handleShowSearchQueriesModal = (event: CustomEvent) => {
try {
const dataType = event.detail;
let data: any[] = [];
if (dataType === 'queries') {
data = props.searchQueries || [];
}
setModalData(Array.isArray(data) ? data : []);
setShowSearchQueriesModal(true);
} catch (error) {
console.error('Error handling search queries modal:', error);
setModalData([]);
setShowSearchQueriesModal(true);
}
};
window.addEventListener('showResearchSourcesModal', handleShowResearchSourcesModal as EventListener);
window.addEventListener('showCitationsModal', handleShowCitationsModal as EventListener);
window.addEventListener('showSearchQueriesModal', handleShowSearchQueriesModal as EventListener);
return () => {
window.removeEventListener('showResearchSourcesModal', handleShowResearchSourcesModal as EventListener);
window.removeEventListener('showCitationsModal', handleShowCitationsModal as EventListener);
window.removeEventListener('showSearchQueriesModal', handleShowSearchQueriesModal as EventListener);
};
}, []);
return (
<>
<ContentPreviewHeader {...props} />
<ResearchSourcesModal
sources={modalData || []}
isOpen={showResearchSourcesModal}
onClose={() => setShowResearchSourcesModal(false)}
/>
<CitationsModal
citations={modalData || []}
isOpen={showCitationsModal}
onClose={() => setShowCitationsModal(false)}
/>
<SearchQueriesModal
queries={modalData || []}
isOpen={showSearchQueriesModal}
onClose={() => setShowSearchQueriesModal(false)}
/>
</>
);
// Main ContentPreviewHeader component - now just a wrapper that uses the extracted component
const ContentPreviewHeader: React.FC<ContentPreviewHeaderProps> = (props) => {
return <MainContentPreviewHeader {...props} />;
};
// Export both the main component and the enhanced version with modals
export default ContentPreviewHeader;
export { ContentPreviewHeader, ContentPreviewHeaderWithModals };

View File

@@ -0,0 +1,383 @@
import React, { useState, useEffect } from 'react';
import MainContentPreviewHeader from './MainContentPreviewHeader';
interface ContentPreviewHeaderProps {
researchSources?: any[];
citations?: any[];
searchQueries?: string[];
qualityMetrics?: any;
draft: string;
showPreview: boolean;
onPreviewToggle: () => void;
assistantOn?: boolean;
onAssistantToggle?: (enabled: boolean) => void;
topic?: string;
}
// Research Sources Modal Component
const ResearchSourcesModal: React.FC<{ sources: any[]; isOpen: boolean; onClose: () => void }> = ({ sources, isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000000
}} onClick={onClose}>
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '24px',
maxWidth: '600px',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
}} onClick={(e) => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
Research Sources ({sources.length})
</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#666',
padding: '0',
width: '30px',
height: '30px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
×
</button>
</div>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
{sources && Array.isArray(sources) ? sources.map((source, idx) => (
<div key={idx} style={{
marginBottom: '16px',
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
borderLeft: '4px solid #0a66c2'
}}>
<div style={{ fontWeight: '600', marginBottom: '8px', color: '#0a66c2' }}>
{source.title || 'Untitled Source'}
</div>
<div style={{ color: '#666', marginBottom: '12px', lineHeight: '1.5' }}>
{source.content || 'No description available'}
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{source.relevance_score && (
<span style={{
backgroundColor: '#eef6ff',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '11px',
fontWeight: '600',
color: '#0a66c2'
}}>
Relevance: {Math.round(source.relevance_score * 100)}%
</span>
)}
{source.credibility_score && (
<span style={{
backgroundColor: '#eef6ff',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '11px',
fontWeight: '600',
color: '#0a66c2'
}}>
Credibility: {Math.round(source.credibility_score * 100)}%
</span>
)}
{source.domain_authority && (
<span style={{
backgroundColor: '#eef6ff',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '11px',
fontWeight: '600',
color: '#0a66c2'
}}>
Authority: {Math.round(source.domain_authority * 100)}%
</span>
)}
</div>
</div>
)) : (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
No research sources available
</div>
)}
</div>
</div>
</div>
);
};
// Citations Modal Component
const CitationsModal: React.FC<{ citations: any[]; isOpen: boolean; onClose: () => void }> = ({ citations, isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000000
}} onClick={onClose}>
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '24px',
maxWidth: '500px',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
}} onClick={(e) => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
Citations ({citations.length})
</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#666',
padding: '0',
width: '30px',
height: '30px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
×
</button>
</div>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
{citations && Array.isArray(citations) ? citations.map((citation, idx) => (
<div key={idx} style={{
marginBottom: '12px',
padding: '12px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
borderLeft: '3px solid #f59e0b'
}}>
<div style={{ fontWeight: '600', color: '#0a66c2', marginBottom: '4px' }}>
Citation {idx + 1}
</div>
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>
Type: {citation.type || 'inline'}
</div>
{citation.reference && (
<div style={{ color: '#666', fontSize: '12px' }}>
Reference: {citation.reference}
</div>
)}
</div>
)) : (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
No citations available
</div>
)}
</div>
</div>
</div>
);
};
// Search Queries Modal Component
const SearchQueriesModal: React.FC<{ queries: string[]; isOpen: boolean; onClose: () => void }> = ({ queries, isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000000
}} onClick={onClose}>
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '24px',
maxWidth: '500px',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
}} onClick={(e) => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
Search Queries Used ({queries.length})
</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#666',
padding: '0',
width: '30px',
height: '30px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
×
</button>
</div>
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
{queries && Array.isArray(queries) ? queries.map((query, idx) => (
<div key={idx} style={{
marginBottom: '12px',
padding: '12px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
borderLeft: '3px solid #8b5cf6'
}}>
<div style={{ fontWeight: '600', color: '#7c3aed', marginBottom: '6px' }}>
Query {idx + 1}
</div>
<div style={{ color: '#374151', fontSize: '13px', lineHeight: '1.4' }}>
{query}
</div>
</div>
)) : (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
No search queries available
</div>
)}
</div>
</div>
</div>
);
};
// Enhanced ContentPreviewHeader with Modal State
const ContentPreviewHeaderWithModals: React.FC<ContentPreviewHeaderProps> = (props) => {
const [showResearchSourcesModal, setShowResearchSourcesModal] = useState(false);
const [showCitationsModal, setShowCitationsModal] = useState(false);
const [showSearchQueriesModal, setShowSearchQueriesModal] = useState(false);
const [modalData, setModalData] = useState<any>(null);
useEffect(() => {
const handleShowResearchSourcesModal = (event: CustomEvent) => {
try {
const dataType = event.detail;
let data: any[] = [];
if (dataType === 'sources') {
data = props.researchSources || [];
}
setModalData(Array.isArray(data) ? data : []);
setShowResearchSourcesModal(true);
} catch (error) {
console.error('Error handling research sources modal:', error);
setModalData([]);
setShowResearchSourcesModal(true);
}
};
const handleShowCitationsModal = (event: CustomEvent) => {
try {
const dataType = event.detail;
let data: any[] = [];
if (dataType === 'citations') {
data = props.citations || [];
}
setModalData(Array.isArray(data) ? data : []);
setShowCitationsModal(true);
} catch (error) {
console.error('Error handling citations modal:', error);
setModalData([]);
setShowCitationsModal(true);
}
};
const handleShowSearchQueriesModal = (event: CustomEvent) => {
try {
const dataType = event.detail;
let data: any[] = [];
if (dataType === 'queries') {
data = props.searchQueries || [];
}
setModalData(Array.isArray(data) ? data : []);
setShowSearchQueriesModal(true);
} catch (error) {
console.error('Error handling search queries modal:', error);
setModalData([]);
setShowSearchQueriesModal(true);
}
};
window.addEventListener('showResearchSourcesModal', handleShowResearchSourcesModal as EventListener);
window.addEventListener('showCitationsModal', handleShowCitationsModal as EventListener);
window.addEventListener('showSearchQueriesModal', handleShowSearchQueriesModal as EventListener);
return () => {
window.removeEventListener('showResearchSourcesModal', handleShowResearchSourcesModal as EventListener);
window.removeEventListener('showCitationsModal', handleShowCitationsModal as EventListener);
window.removeEventListener('showSearchQueriesModal', handleShowSearchQueriesModal as EventListener);
};
}, []);
return (
<>
<MainContentPreviewHeader {...props} />
<ResearchSourcesModal
sources={modalData || []}
isOpen={showResearchSourcesModal}
onClose={() => setShowResearchSourcesModal(false)}
/>
<CitationsModal
citations={modalData || []}
isOpen={showCitationsModal}
onClose={() => setShowCitationsModal(false)}
/>
<SearchQueriesModal
queries={modalData || []}
isOpen={showSearchQueriesModal}
onClose={() => setShowSearchQueriesModal(false)}
/>
</>
);
};
export default ContentPreviewHeaderWithModals;

View File

@@ -0,0 +1,494 @@
import React, { useMemo } from 'react';
import PersonaChip from './PersonaChip';
// Extend HTMLDivElement interface for custom tooltip properties
interface ExtendedDivElement extends HTMLDivElement {
_researchTooltip?: HTMLDivElement | null;
_citationsTooltip?: HTMLDivElement | null;
_searchQueriesTooltip?: HTMLDivElement | null;
_qualityTooltip?: HTMLDivElement | null;
_researchTooltipTimeout?: NodeJS.Timeout | null;
_qualityTooltipTimeout?: NodeJS.Timeout | null;
}
interface MainContentPreviewHeaderProps {
researchSources?: any[];
citations?: any[];
searchQueries?: string[];
qualityMetrics?: any;
draft: string;
showPreview: boolean;
onPreviewToggle: () => void;
assistantOn?: boolean;
onAssistantToggle?: (enabled: boolean) => void;
topic?: string;
}
const MainContentPreviewHeader: React.FC<MainContentPreviewHeaderProps> = ({
researchSources,
citations,
searchQueries,
qualityMetrics,
draft,
showPreview,
onPreviewToggle,
assistantOn,
onAssistantToggle,
topic
}) => {
const formatPercent = (v?: number) => typeof v === 'number' ? `${Math.round(v * 100)}%` : '—';
const getChipColor = (v?: number) => {
if (typeof v !== 'number') return '#6b7280';
if (v >= 0.8) return '#10b981';
if (v >= 0.6) return '#f59e0b';
return '#ef4444';
};
// Memoize chips array to prevent infinite re-rendering
const chips = useMemo(() => {
const chipArray = qualityMetrics ? [
{ label: 'Overall', value: qualityMetrics.overall_score },
{ label: 'Accuracy', value: qualityMetrics.factual_accuracy },
{ label: 'Verification', value: qualityMetrics.source_verification },
{ label: 'Coverage', value: qualityMetrics.citation_coverage }
] : [];
console.log('🔍 [ContentPreviewHeader] Chips array created:', {
qualityMetrics: qualityMetrics,
chips: chipArray,
chipsLength: chipArray.length
});
return chipArray;
}, [qualityMetrics]);
// Helper to build descriptive chip tooltip text
const chipDescriptions: Record<string, string> = {
Overall: 'Overall blends accuracy, verification and coverage into a single reliability score for this draft.',
Accuracy: 'Factual Accuracy estimates how likely statements are to be factually correct based on grounding signals.',
Verification: 'Source Verification reflects how well claims are linked to credible sources and whether citations match claims.',
Coverage: 'Citation Coverage indicates how much of the content is supported with citations. Higher is better.'
};
return (
<div style={{
padding: '12px 16px',
background: '#e1f5fe',
borderBottom: '1px solid #b3e5fc',
fontSize: '12px',
fontWeight: '600',
color: '#0277bd',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<span>{topic ? `${topic} - LinkedIn Content Preview` : 'LinkedIn Content Preview'}</span>
{/* Persona Chip */}
<PersonaChip
platform="linkedin"
userId={1}
onPersonaUpdate={(personaData) => {
console.log('Persona updated:', personaData);
// You can add additional logic here to handle persona updates
}}
/>
{/* Research Chip with Hover Sub-chips */}
{((researchSources && researchSources.length > 0) || (citations && citations.length > 0) || (searchQueries && searchQueries.length > 0)) && (
<div style={{ position: 'relative' }}>
{/* Main Research Chip */}
<div
style={{
background: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)',
border: '1px solid #0284c7',
borderRadius: '999px',
padding: '6px 14px',
fontSize: '11px',
fontWeight: '700',
color: 'white',
cursor: 'pointer',
transition: 'all 0.3s ease',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '6px',
boxShadow: '0 2px 8px rgba(14, 165, 233, 0.3)',
transform: 'translateZ(0)',
userSelect: 'none'
}}
title="Research data available. Hover to see sources, citations, and queries."
onMouseEnter={(e) => {
// Clear any existing timeout
const target = e.currentTarget as ExtendedDivElement;
if (target._researchTooltipTimeout) {
clearTimeout(target._researchTooltipTimeout);
target._researchTooltipTimeout = null;
}
// Create and show research sub-chips tooltip
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
z-index: 100000;
background: white;
border: 1px solid #cfe9f7;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
padding: 16px;
max-width: 400px;
font-size: 12px;
opacity: 0;
transform: translateY(-8px);
transition: all 0.2s ease;
pointer-events: auto;
`;
let subChipsHtml = '<div style="margin-bottom: 12px; font-weight: 600; color: #0a66c2; font-size: 14px;">Research Data</div>';
// Add Sources sub-chip
if (researchSources && researchSources.length > 0) {
subChipsHtml += `
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
onmouseenter="this.style.background='#e0f2fe'; this.style.transform='scale(1.05)'"
onmouseleave="this.style.background='#f0f9ff'; this.style.transform='scale(1)'"
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showResearchSourcesModal', { detail: 'sources' }))">
<span style="display: inline-block; width: 6px; height: 6px; background: #10b981; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);"></span>
Sources: ${researchSources.length}
</div>
`;
}
// Add Citations sub-chip
if (citations && citations.length > 0) {
subChipsHtml += `
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
onmouseenter="this.style.background='#fde68a'; this.style.transform='scale(1.05)'"
onmouseleave="this.style.background='#fef3c7'; this.style.transform='scale(1)'"
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showCitationsModal', { detail: 'citations' }))">
<span style="display: inline-block; width: 6px; height: 6px; background: #f59e0b; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(245, 158, 11, 0.5);"></span>
Citations: ${citations.length}
</div>
`;
}
// Add Queries sub-chip
if (searchQueries && searchQueries.length > 0) {
subChipsHtml += `
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #f3e8ff; border: 1px solid #8b5cf6; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
onmouseenter="this.style.background='#e9d5ff'; this.style.transform='scale(1.05)'"
onmouseleave="this.style.background='#f3e8ff'; this.style.transform='scale(1)'"
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showSearchQueriesModal', { detail: 'queries' }))">
<span style="display: inline-block; width: 6px; height: 6px; background: #8b5cf6; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(139, 92, 246, 0.5);"></span>
Queries: ${searchQueries.length}
</div>
`;
}
tooltip.innerHTML = subChipsHtml;
// Add mouse events to tooltip to keep it visible
tooltip.addEventListener('mouseenter', () => {
if (target._researchTooltipTimeout) {
clearTimeout(target._researchTooltipTimeout);
target._researchTooltipTimeout = null;
}
});
tooltip.addEventListener('mouseleave', () => {
target._researchTooltipTimeout = setTimeout(() => {
if (tooltip.parentNode) {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-8px)';
setTimeout(() => {
if (tooltip.parentNode) {
tooltip.remove();
}
}, 200);
}
target._researchTooltip = null;
}, 100);
});
document.body.appendChild(tooltip);
const rect = e.currentTarget.getBoundingClientRect();
tooltip.style.left = Math.min(rect.left, window.innerWidth - 420) + 'px';
tooltip.style.top = (rect.bottom + 8) + 'px';
// Animate in
setTimeout(() => {
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(0)';
}, 10);
target._researchTooltip = tooltip;
}}
onMouseLeave={(e) => {
const target = e.currentTarget as ExtendedDivElement;
if (target._researchTooltip) {
// Add delay before hiding to allow moving to tooltip
target._researchTooltipTimeout = setTimeout(() => {
const tooltip = target._researchTooltip;
if (tooltip && tooltip.parentNode) {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-8px)';
setTimeout(() => {
if (tooltip.parentNode) {
tooltip.remove();
}
}, 200);
}
target._researchTooltip = null;
}, 100);
}
}}
onMouseMove={(e) => {
// Keep tooltip visible when moving to sub-chips
const target = e.currentTarget as ExtendedDivElement;
if (target._researchTooltip) {
const tooltip = target._researchTooltip;
const rect = e.currentTarget.getBoundingClientRect();
tooltip.style.left = Math.min(rect.left, window.innerWidth - 420) + 'px';
tooltip.style.top = (rect.bottom + 8) + 'px';
}
}}
onMouseOver={(e) => {
// Add hover effect to the chip itself
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(14, 165, 233, 0.4)';
}}
onMouseOut={(e) => {
// Remove hover effect
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.3)';
}}
>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: 'rgba(255, 255, 255, 0.9)',
flexShrink: 0,
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
}} />
Research
</div>
</div>
)}
</div>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
{/* Quality Metrics Chip */}
{chips.length > 0 && (
<div style={{ position: 'relative' }}>
{/* Main Quality Metrics Chip */}
<div
style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
border: '1px solid #047857',
borderRadius: '999px',
padding: '6px 14px',
fontSize: '11px',
fontWeight: '700',
color: 'white',
cursor: 'pointer',
transition: 'all 0.3s ease',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '6px',
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
transform: 'translateZ(0)',
userSelect: 'none'
}}
title="Quality metrics available. Hover to see detailed progress bars and explanations."
onMouseEnter={(e) => {
// Clear any existing timeout
const target = e.currentTarget as ExtendedDivElement;
if (target._qualityTooltipTimeout) {
clearTimeout(target._qualityTooltipTimeout);
target._qualityTooltipTimeout = null;
}
// Create and show quality metrics tooltip with circular progress bars
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
z-index: 100000;
background: white;
border: 1px solid #d1fae5;
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
padding: 24px;
max-width: 500px;
font-size: 12px;
opacity: 0;
transform: translateY(-8px);
transition: all 0.2s ease;
pointer-events: auto;
`;
// Create circular progress bars for each metric
const createCircularProgress = (label: string, value: number, description: string) => {
const percentage = Math.round(value * 100);
const color = getChipColor(value);
const circumference = 2 * Math.PI * 45; // radius = 45
const strokeDasharray = circumference;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
return `
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding: 12px; background: #f8fafc; border-radius: 12px; border-left: 4px solid ${color};">
<div style="position: relative; width: 60px; height: 60px;">
<svg width="60" height="60" style="transform: rotate(-90deg);">
<circle cx="30" cy="30" r="45" stroke="#e5e7eb" stroke-width="6" fill="none"/>
<circle cx="30" cy="30" r="45" stroke="${color}" stroke-width="6" fill="none"
stroke-dasharray="${strokeDasharray}" stroke-dashoffset="${strokeDashoffset}"
style="transition: stroke-dashoffset 0.5s ease;"/>
</svg>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-weight: 700; font-size: 14px; color: ${color};">
${percentage}%
</div>
</div>
<div style="flex: 1;">
<div style="font-weight: 700; color: #1f2937; margin-bottom: 4px; font-size: 14px;">${label}</div>
<div style="color: #6b7280; line-height: 1.4; font-size: 11px;">${description}</div>
</div>
</div>
`;
};
let progressBarsHtml = '<div style="margin-bottom: 16px; font-weight: 700; color: #059669; font-size: 16px; text-align: center;">Quality Metrics</div>';
chips.forEach(chip => {
progressBarsHtml += createCircularProgress(
chip.label,
chip.value || 0,
chipDescriptions[chip.label] || ''
);
});
tooltip.innerHTML = progressBarsHtml;
// Add mouse events to tooltip to keep it visible
tooltip.addEventListener('mouseenter', () => {
if (target._qualityTooltipTimeout) {
clearTimeout(target._qualityTooltipTimeout);
target._qualityTooltipTimeout = null;
}
});
tooltip.addEventListener('mouseleave', () => {
target._qualityTooltipTimeout = setTimeout(() => {
if (tooltip.parentNode) {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-8px)';
setTimeout(() => {
if (tooltip.parentNode) {
tooltip.remove();
}
}, 200);
}
target._qualityTooltip = null;
}, 100);
});
document.body.appendChild(tooltip);
const rect = e.currentTarget.getBoundingClientRect();
tooltip.style.left = Math.min(rect.left, window.innerWidth - 520) + 'px';
tooltip.style.top = (rect.bottom + 8) + 'px';
// Animate in
setTimeout(() => {
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(0)';
}, 10);
target._qualityTooltip = tooltip;
}}
onMouseLeave={(e) => {
const target = e.currentTarget as ExtendedDivElement;
if (target._qualityTooltip) {
// Add delay before hiding to allow moving to tooltip
target._qualityTooltipTimeout = setTimeout(() => {
const tooltip = target._qualityTooltip;
if (tooltip && tooltip.parentNode) {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-8px)';
setTimeout(() => {
if (tooltip.parentNode) {
tooltip.remove();
}
}, 200);
}
target._qualityTooltip = null;
}, 100);
}
}}
onMouseMove={(e) => {
// Keep tooltip visible when moving to progress bars
const target = e.currentTarget as ExtendedDivElement;
if (target._qualityTooltip) {
const tooltip = target._qualityTooltip;
const rect = e.currentTarget.getBoundingClientRect();
tooltip.style.left = Math.min(rect.left, window.innerWidth - 520) + 'px';
tooltip.style.top = (rect.bottom + 8) + 'px';
}
}}
onMouseOver={(e) => {
// Add hover effect to the chip itself
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(16, 185, 129, 0.4)';
}}
onMouseOut={(e) => {
// Remove hover effect
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.3)';
}}
>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: 'rgba(255, 255, 255, 0.9)',
flexShrink: 0,
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
}} />
Quality Metrics
</div>
</div>
)}
<span style={{ fontSize: '10px', opacity: 0.8 }}>
{draft.split(/\s+/).length} words {Math.ceil(draft.split(/\s+/).length / 200)} min read
</span>
{/* Assistive Writing toggle */}
{onAssistantToggle && (
<label
style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#666', cursor: 'pointer' }}
title="Assistive Writing: Get real-time AI-powered writing suggestions as you type. Uses Exa.ai for web research and Gemini for intelligent content generation. Automatically enables editing mode to allow typing and content modification."
>
<input
type="checkbox"
checked={assistantOn || false}
onChange={(e) => onAssistantToggle(e.target.checked)}
/>
Assistive Writing
</label>
)}
<label
style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#666', cursor: 'pointer' }}
title="Toggle preview visibility"
>
<input
type="checkbox"
checked={!showPreview}
onChange={() => onPreviewToggle()}
style={{ margin: 0 }}
/>
Hide Preview
</label>
</div>
</div>
);
};
export default MainContentPreviewHeader;

View File

@@ -0,0 +1,324 @@
import React, { useState, useEffect } from 'react';
import PersonaEditorModal from './PersonaEditorModal';
interface PersonaData {
id?: number;
user_id?: number;
persona_name: string;
archetype: string;
core_belief: string;
brand_voice_description: string;
linguistic_fingerprint: any;
platform_adaptations: any;
confidence_score: number;
ai_analysis_version: string;
platform_type: string;
sentence_metrics: any;
lexical_features: any;
rhetorical_devices: any;
tonal_range: any;
stylistic_constraints: any;
content_format_rules: any;
engagement_patterns: any;
posting_frequency: any;
content_types: any;
platform_best_practices: any;
algorithm_considerations: any;
}
interface PersonaChipProps {
platform: string;
userId?: number;
onPersonaUpdate?: (personaData: PersonaData) => void;
}
const PersonaChip: React.FC<PersonaChipProps> = ({
platform,
userId = 1,
onPersonaUpdate
}) => {
const [personaData, setPersonaData] = useState<PersonaData | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [showEditor, setShowEditor] = useState(false);
const [error, setError] = useState<string | null>(null);
// Fetch persona data
const fetchPersonaData = async () => {
setIsLoading(true);
setError(null);
try {
// Fetch core persona list (take most recent active) and platform-specific details
const [coreRes, platformRes] = await Promise.all([
fetch(`/api/personas/user/${userId}`),
fetch(`/api/personas/platform/${platform}?user_id=${userId}`)
]);
if (coreRes.ok && platformRes.ok) {
const coreList = await coreRes.json();
const platformData = await platformRes.json();
const core = (coreList?.personas && coreList.personas.length > 0) ? coreList.personas[0] : {};
// Merge core + platform fields for editor convenience
setPersonaData({
id: core.id,
user_id: core.user_id,
persona_name: core.persona_name,
archetype: core.archetype,
core_belief: core.core_belief,
brand_voice_description: core.brand_voice_description,
linguistic_fingerprint: core.linguistic_fingerprint,
platform_adaptations: core.platform_adaptations,
confidence_score: core.confidence_score,
ai_analysis_version: core.ai_analysis_version,
platform_type: platform,
sentence_metrics: platformData?.sentence_metrics,
lexical_features: platformData?.lexical_features,
rhetorical_devices: platformData?.rhetorical_devices,
tonal_range: platformData?.tonal_range,
stylistic_constraints: platformData?.stylistic_constraints,
content_format_rules: platformData?.content_format_rules,
engagement_patterns: platformData?.engagement_patterns,
posting_frequency: platformData?.posting_frequency,
content_types: platformData?.content_types,
platform_best_practices: platformData?.platform_best_practices,
algorithm_considerations: platformData?.algorithm_considerations,
} as any);
} else {
setError('No persona found for this platform');
}
} catch (err) {
setError('Failed to load persona data');
console.error('Error fetching persona:', err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchPersonaData();
}, [platform, userId]);
const handleSavePersona = async (data: PersonaData, saveToDatabase: boolean) => {
try {
if (saveToDatabase) {
// Save core persona simple fields
if (data.id) {
const corePayload: any = {
persona_name: data.persona_name,
archetype: data.archetype,
core_belief: data.core_belief,
brand_voice_description: data.brand_voice_description,
linguistic_fingerprint: data.linguistic_fingerprint,
platform_adaptations: data.platform_adaptations,
};
const coreRes = await fetch(`/api/personas/${data.id}?user_id=${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(corePayload)
});
if (!coreRes.ok) throw new Error('Failed to update core persona');
}
// Save platform persona fields
const platformPayload: any = {
sentence_metrics: data.sentence_metrics,
lexical_features: data.lexical_features,
rhetorical_devices: data.rhetorical_devices,
tonal_range: data.tonal_range,
stylistic_constraints: data.stylistic_constraints,
content_format_rules: data.content_format_rules,
engagement_patterns: data.engagement_patterns,
posting_frequency: data.posting_frequency,
content_types: data.content_types,
platform_best_practices: data.platform_best_practices,
algorithm_considerations: data.algorithm_considerations,
};
const platRes = await fetch(`/api/personas/platform/${platform}?user_id=${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(platformPayload)
});
if (!platRes.ok) throw new Error('Failed to update platform persona');
}
// Update local state
setPersonaData(data);
// Notify parent component
if (onPersonaUpdate) {
onPersonaUpdate(data);
}
console.log('Persona updated:', saveToDatabase ? 'saved to database' : 'session only');
} catch (err) {
console.error('Error saving persona:', err);
setError('Failed to save persona changes');
}
};
const getPersonaColor = (confidence?: number) => {
if (!confidence) return '#6b7280';
if (confidence >= 0.8) return '#10b981';
if (confidence >= 0.6) return '#f59e0b';
return '#ef4444';
};
const getPersonaIcon = (archetype?: string) => {
if (!archetype) return '👤';
const archetypeIcons: Record<string, string> = {
'pragmatic futurist': '🔮',
'thoughtful educator': '📚',
'innovative leader': '🚀',
'analytical expert': '🔍',
'creative storyteller': '✨',
'strategic advisor': '🎯',
'authentic connector': '🤝',
'data-driven optimist': '📊'
};
const lowerArchetype = archetype.toLowerCase();
for (const [key, icon] of Object.entries(archetypeIcons)) {
if (lowerArchetype.includes(key)) {
return icon;
}
}
return '👤';
};
if (isLoading) {
return (
<div style={{
background: 'linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%)',
border: '1px solid #d1d5db',
borderRadius: '999px',
padding: '6px 14px',
fontSize: '11px',
fontWeight: '700',
color: '#6b7280',
display: 'flex',
alignItems: 'center',
gap: '6px'
}}>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: '#9ca3af',
animation: 'pulse 2s infinite'
}} />
Loading Persona...
</div>
);
}
if (error || !personaData) {
return (
<div style={{
background: 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)',
border: '1px solid #fca5a5',
borderRadius: '999px',
padding: '6px 14px',
fontSize: '11px',
fontWeight: '700',
color: '#dc2626',
display: 'flex',
alignItems: 'center',
gap: '6px',
cursor: 'pointer'
}}
onClick={() => fetchPersonaData()}
title="Click to retry loading persona data"
>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: '#ef4444'
}} />
No Persona
</div>
);
}
const confidence = personaData.confidence_score || 0;
const confidenceColor = getPersonaColor(confidence);
// Debug: Log the confidence score to see what's being stored
console.log('PersonaChip confidence_score:', personaData.confidence_score, 'processed:', confidence);
const personaIcon = getPersonaIcon(personaData.archetype);
return (
<>
<div
style={{
background: `linear-gradient(135deg, ${confidenceColor} 0%, ${confidenceColor}dd 100%)`,
border: `1px solid ${confidenceColor}`,
borderRadius: '999px',
padding: '6px 14px',
fontSize: '11px',
fontWeight: '700',
color: 'white',
cursor: 'pointer',
transition: 'all 0.3s ease',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '6px',
boxShadow: `0 2px 8px ${confidenceColor}40`,
transform: 'translateZ(0)',
userSelect: 'none'
}}
title={`${personaData.persona_name} - ${personaData.archetype || 'No archetype'} (${Math.round(confidence * 100)}% confidence). Click to edit.`}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
e.currentTarget.style.boxShadow = `0 4px 16px ${confidenceColor}60`;
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.boxShadow = `0 2px 8px ${confidenceColor}40`;
}}
onClick={() => setShowEditor(true)}
>
<div style={{
fontSize: '12px',
flexShrink: 0
}}>
{personaIcon}
</div>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: 'rgba(255, 255, 255, 0.9)',
flexShrink: 0,
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
}} />
<span style={{ whiteSpace: 'nowrap' }}>
{personaData.persona_name || 'Untitled Persona'}
</span>
<div style={{
fontSize: '10px',
opacity: 0.8,
marginLeft: '4px'
}}>
{Math.round(confidence * 100)}%
</div>
</div>
<PersonaEditorModal
isOpen={showEditor}
onClose={() => setShowEditor(false)}
personaData={personaData}
onSave={(data, saveToDatabase) => handleSavePersona(data, saveToDatabase)}
platform={platform}
/>
</>
);
};
export default PersonaChip;

View File

@@ -0,0 +1,857 @@
import React, { useState, useEffect } from 'react';
interface PersonaData {
// Core WritingPersona fields
id?: number;
user_id?: number;
persona_name: string;
archetype: string;
core_belief: string;
brand_voice_description: string;
linguistic_fingerprint: any;
platform_adaptations: any;
confidence_score: number;
ai_analysis_version: string;
// PlatformPersona fields
platform_type: string;
sentence_metrics: any;
lexical_features: any;
rhetorical_devices: any;
tonal_range: any;
stylistic_constraints: any;
content_format_rules: any;
engagement_patterns: any;
posting_frequency: any;
content_types: any;
platform_best_practices: any;
algorithm_considerations: any;
}
interface PersonaEditorModalProps {
isOpen: boolean;
onClose: () => void;
personaData: PersonaData | null;
onSave: (data: PersonaData, saveToDatabase: boolean) => void;
platform: string;
}
const PersonaEditorModal: React.FC<PersonaEditorModalProps> = ({
isOpen,
onClose,
personaData,
onSave,
platform
}) => {
const [editedData, setEditedData] = useState<PersonaData | null>(null);
const [activeTab, setActiveTab] = useState<'core' | 'linguistic' | 'platform' | 'optimization'>('core');
const [saveToDatabase, setSaveToDatabase] = useState(true);
useEffect(() => {
if (personaData) {
setEditedData({ ...personaData });
}
}, [personaData]);
if (!isOpen || !editedData) return null;
const handleSave = () => {
onSave(editedData, saveToDatabase);
};
const updateField = (path: string, value: any) => {
setEditedData(prev => {
if (!prev) return prev;
const newData = { ...prev };
const keys = path.split('.');
let current: any = newData;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
return newData;
});
};
const getFieldValue = (path: string, defaultValue: any = '') => {
const keys = path.split('.');
let current: any = editedData;
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
} else {
return defaultValue;
}
}
return current || defaultValue;
};
const tabs = [
{ id: 'core', label: 'Core Identity', icon: '🎭' },
{ id: 'linguistic', label: 'Linguistic', icon: '📝' },
{ id: 'platform', label: 'Platform', icon: '🔗' },
{ id: 'optimization', label: 'Optimization', icon: '⚡' }
] as const;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10000,
padding: '20px'
}}>
<div style={{
backgroundColor: 'white',
borderRadius: '16px',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
width: '100%',
maxWidth: '800px',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}>
{/* Header */}
<div style={{
padding: '24px 24px 0 24px',
borderBottom: '1px solid #e5e7eb',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div>
<h2 style={{ margin: 0, fontSize: '24px', fontWeight: '700' }}>
Edit Persona: {getFieldValue('persona_name', 'Untitled Persona')}
</h2>
<p style={{ margin: '4px 0 0 0', fontSize: '14px', opacity: 0.9 }}>
Platform: {platform} Confidence: {(() => {
const score = getFieldValue('confidence_score', 0) || 0;
console.log('PersonaEditorModal confidence_score:', score);
return score;
})()}%
</p>
</div>
<button
onClick={onClose}
style={{
background: 'rgba(255, 255, 255, 0.2)',
border: 'none',
borderRadius: '8px',
width: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
fontSize: '18px',
color: 'white'
}}
>
</button>
</div>
{/* Tabs */}
<div style={{ display: 'flex', gap: '4px' }}>
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
style={{
background: activeTab === tab.id ? 'rgba(255, 255, 255, 0.2)' : 'transparent',
border: 'none',
borderRadius: '8px 8px 0 0',
padding: '12px 16px',
color: 'white',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.2s ease'
}}
>
<span>{tab.icon}</span>
{tab.label}
</button>
))}
</div>
</div>
{/* Content */}
<div style={{
flex: 1,
overflow: 'auto',
padding: '24px'
}}>
{activeTab === 'core' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Persona Name
</label>
<input
type="text"
value={getFieldValue('persona_name', '')}
onChange={(e) => updateField('persona_name', e.target.value)}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
onFocus={(e) => e.target.style.borderColor = '#667eea'}
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Archetype / Guide
</label>
<textarea
value={getFieldValue('archetype', '')}
onChange={(e) => updateField('archetype', e.target.value)}
rows={2}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '60px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
onFocus={(e) => e.target.style.borderColor = '#667eea'}
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Core Belief / Mission
</label>
<textarea
value={getFieldValue('core_belief', '')}
onChange={(e) => updateField('core_belief', e.target.value)}
rows={3}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '80px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
onFocus={(e) => e.target.style.borderColor = '#667eea'}
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Brand Voice / Speaking Style
</label>
<textarea
value={getFieldValue('brand_voice_description', '')}
onChange={(e) => updateField('brand_voice_description', e.target.value)}
rows={3}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '80px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
onFocus={(e) => e.target.style.borderColor = '#667eea'}
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Confidence Score
</label>
<input
type="number"
min="0"
max="100"
value={getFieldValue('confidence_score', '')}
onChange={(e) => updateField('confidence_score', parseInt(e.target.value) || 0)}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
onFocus={(e) => e.target.style.borderColor = '#667eea'}
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
AI Analysis Version
</label>
<input
type="text"
value={getFieldValue('ai_analysis_version', '')}
onChange={(e) => updateField('ai_analysis_version', e.target.value)}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
onFocus={(e) => e.target.style.borderColor = '#667eea'}
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
/>
</div>
</div>
</div>
)}
{activeTab === 'linguistic' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div>
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
Sentence Metrics
</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Average Sentence Length (words)
</label>
<input
type="number"
value={getFieldValue('linguistic_fingerprint.sentence_metrics.average_sentence_length_words', '')}
onChange={(e) => updateField('linguistic_fingerprint.sentence_metrics.average_sentence_length_words', parseInt(e.target.value) || '')}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Preferred Sentence Type
</label>
<select
value={getFieldValue('linguistic_fingerprint.sentence_metrics.preferred_sentence_type', '')}
onChange={(e) => updateField('linguistic_fingerprint.sentence_metrics.preferred_sentence_type', e.target.value)}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
>
<option value="simple_and_compound">Simple and Compound</option>
<option value="complex">Complex</option>
<option value="mixed">Mixed</option>
</select>
</div>
</div>
</div>
<div>
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
Lexical Features
</h3>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Go-to Words (comma-separated)
</label>
<textarea
value={getFieldValue('linguistic_fingerprint.lexical_features.go_to_words', []).join(', ')}
onChange={(e) => updateField('linguistic_fingerprint.lexical_features.go_to_words', e.target.value.split(',').map(w => w.trim()).filter(w => w))}
rows={2}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '60px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
placeholder="e.g., innovative, strategic, transformative, impactful, leverage"
/>
</div>
<div style={{ marginTop: '16px' }}>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Avoid Words (comma-separated)
</label>
<textarea
value={getFieldValue('linguistic_fingerprint.lexical_features.avoid_words', []).join(', ')}
onChange={(e) => updateField('linguistic_fingerprint.lexical_features.avoid_words', e.target.value.split(',').map(w => w.trim()).filter(w => w))}
rows={2}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '60px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
placeholder="e.g., buzzwords, jargon, clichés, overly technical terms"
/>
</div>
</div>
<div>
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
Tonal Range
</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Default Tone
</label>
<select
value={getFieldValue('tonal_range.default_tone', '')}
onChange={(e) => updateField('tonal_range.default_tone', e.target.value)}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
>
<option value="professional">Professional</option>
<option value="conversational">Conversational</option>
<option value="authoritative">Authoritative</option>
<option value="inspirational">Inspirational</option>
<option value="educational">Educational</option>
<option value="friendly">Friendly</option>
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Permissible Tones (comma-separated)
</label>
<input
type="text"
value={getFieldValue('tonal_range.permissible_tones', []).join(', ')}
onChange={(e) => updateField('tonal_range.permissible_tones', e.target.value.split(',').map(w => w.trim()).filter(w => w))}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
placeholder="e.g., professional, conversational, authoritative"
/>
</div>
</div>
</div>
</div>
)}
{activeTab === 'platform' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div>
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
Content Format Rules
</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Character Limit
</label>
<input
type="number"
value={getFieldValue('content_format_rules.character_limit', '')}
onChange={(e) => updateField('content_format_rules.character_limit', parseInt(e.target.value) || 3000)}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Optimal Length
</label>
<input
type="text"
value={getFieldValue('content_format_rules.optimal_length', '')}
onChange={(e) => updateField('content_format_rules.optimal_length', e.target.value)}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
placeholder="e.g., 150-200 words"
/>
</div>
</div>
<div style={{ marginTop: '16px' }}>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Paragraph Structure
</label>
<textarea
value={getFieldValue('content_format_rules.paragraph_structure', '')}
onChange={(e) => updateField('content_format_rules.paragraph_structure', e.target.value)}
rows={3}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '80px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
/>
</div>
</div>
<div>
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
Engagement Patterns
</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Posting Frequency
</label>
<select
value={getFieldValue('engagement_patterns.posting_frequency', '')}
onChange={(e) => updateField('engagement_patterns.posting_frequency', e.target.value)}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
>
<option value="daily">Daily</option>
<option value="2-3 times per week">2-3 times per week</option>
<option value="weekly">Weekly</option>
<option value="bi-weekly">Bi-weekly</option>
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Best Posting Times
</label>
<input
type="text"
value={getFieldValue('engagement_patterns.best_posting_times', '')}
onChange={(e) => updateField('engagement_patterns.best_posting_times', e.target.value)}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
placeholder="e.g., Tuesday-Thursday, 8-10 AM or 1-3 PM"
/>
</div>
</div>
</div>
<div>
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
Content Types
</h3>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Preferred Content Types (comma-separated)
</label>
<input
type="text"
value={getFieldValue('content_types.preferred_types', []).join(', ')}
onChange={(e) => updateField('content_types.preferred_types', e.target.value.split(',').map(w => w.trim()).filter(w => w))}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
placeholder="e.g., thought leadership, industry insights, case studies, tips"
/>
</div>
</div>
</div>
)}
{activeTab === 'optimization' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div>
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
Platform Best Practices
</h3>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Hashtag Strategy
</label>
<textarea
value={getFieldValue('platform_best_practices.hashtag_strategy', '')}
onChange={(e) => updateField('platform_best_practices.hashtag_strategy', e.target.value)}
rows={3}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '80px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
/>
</div>
<div style={{ marginTop: '16px' }}>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Call-to-Action Style
</label>
<textarea
value={getFieldValue('platform_best_practices.cta_style', '')}
onChange={(e) => updateField('platform_best_practices.cta_style', e.target.value)}
rows={3}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '80px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
/>
</div>
</div>
<div>
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
Algorithm Considerations
</h3>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Engagement Optimization
</label>
<textarea
value={getFieldValue('algorithm_considerations.engagement_optimization', '')}
onChange={(e) => updateField('algorithm_considerations.engagement_optimization', e.target.value)}
rows={3}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '80px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
/>
</div>
<div style={{ marginTop: '16px' }}>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Content Timing
</label>
<textarea
value={getFieldValue('algorithm_considerations.content_timing', '')}
onChange={(e) => updateField('algorithm_considerations.content_timing', e.target.value)}
rows={3}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '80px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
/>
</div>
</div>
<div>
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
Stylistic Constraints
</h3>
<div>
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
Forbidden Elements
</label>
<textarea
value={getFieldValue('stylistic_constraints.forbidden_elements', '')}
onChange={(e) => updateField('stylistic_constraints.forbidden_elements', e.target.value)}
rows={3}
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
fontSize: '16px',
resize: 'vertical',
minHeight: '80px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
/>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div style={{
padding: '20px 24px',
borderTop: '1px solid #e5e7eb',
backgroundColor: '#f9fafb',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '14px', color: '#374151' }}>
<input
type="checkbox"
checked={saveToDatabase}
onChange={(e) => setSaveToDatabase(e.target.checked)}
style={{ margin: 0 }}
/>
Save changes to database
<span style={{ fontSize: '12px', color: '#6b7280' }}>
{saveToDatabase ? 'Changes will be permanent' : 'Changes will be session-only'}
</span>
</label>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={onClose}
style={{
padding: '10px 20px',
border: '2px solid #e5e7eb',
borderRadius: '8px',
backgroundColor: 'white',
color: '#374151',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#d1d5db';
e.currentTarget.style.backgroundColor = '#f9fafb';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb';
e.currentTarget.style.backgroundColor = 'white';
}}
>
Cancel
</button>
<button
onClick={handleSave}
style={{
padding: '10px 20px',
border: 'none',
borderRadius: '8px',
backgroundColor: '#667eea',
color: 'white',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#5a67d8';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#667eea';
}}
>
Save Changes
</button>
</div>
</div>
</div>
</div>
);
};
export default PersonaEditorModal;

View File

@@ -0,0 +1,4 @@
export { default as MainContentPreviewHeader } from './MainContentPreviewHeader';
export { default as ContentPreviewHeaderWithModals } from './ContentPreviewHeaderWithModals';
export { default as PersonaEditorModal } from './PersonaEditorModal';
export { default as PersonaChip } from './PersonaChip';

View File

@@ -2,7 +2,6 @@ export { default as CitationHoverHandler } from './CitationHoverHandler';
export { default as useTextSelectionHandler } from './TextSelectionHandler';
export { default as QuickEditToolbar } from './QuickEditToolbar';
export { default as DiffPreviewModal } from './DiffPreviewModal';
export { default as ContentPreviewHeader } from './ContentPreviewHeader';
export { ContentPreviewHeaderWithModals } from './ContentPreviewHeader';
export { MainContentPreviewHeader as ContentPreviewHeader, ContentPreviewHeaderWithModals } from './ContentPreviewHeaderComponents';
export { default as WritingAssistantCard } from './WritingAssistantCard';
export { default as ContentDisplayArea } from './ContentDisplayArea';