ALwrity HALLUCINATION DETECTOR AND ASSISTIVE WRITING

This commit is contained in:
ajaysi
2025-09-08 21:14:27 +05:30
parent 5ba19c097a
commit 6fd9a4e354
51 changed files with 8224 additions and 1086 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CopilotSidebar } from '@copilotkit/react-ui';
import { useCopilotReadable, useCopilotAction, useCopilotContext } from '@copilotkit/react-core';
import '@copilotkit/react-ui/styles.css';
@@ -13,7 +13,8 @@ import { PlatformPersonaProvider, usePlatformPersonaContext } from '../shared/Pe
const useCopilotActionTyped = useCopilotAction as any;
// Optional debug flag: set to true to enable verbose logs locally
const DEBUG_LINKEDIN = false;
interface LinkedInWriterProps {
className?: string;
@@ -299,15 +300,15 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
}
});
// Intelligent, stage-aware suggestions
const getIntelligentSuggestions = () => {
// 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
console.log('[LinkedIn Writer] Generating suggestions:', {
if (DEBUG_LINKEDIN) console.log('[LinkedIn Writer] Generating suggestions:', {
hasContent,
justGeneratedContent,
draftLength: draft?.length || 0
@@ -365,7 +366,7 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
// Add image generation suggestion when there's content
if (draft && draft.trim().length > 0) {
console.log('[LinkedIn Writer] Adding image generation suggestion');
if (DEBUG_LINKEDIN) console.log('[LinkedIn Writer] Adding image generation suggestion');
// Make image generation suggestion more prominent
refinementSuggestions.push({
title: '🖼️ Generate Post Image',
@@ -386,10 +387,10 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
}
}
console.log('[LinkedIn Writer] Final suggestions:', refinementSuggestions);
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' }}>
@@ -398,94 +399,11 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
userPreferences={userPreferences}
chatHistory={chatHistory}
showPreferencesModal={showPreferencesModal}
showContextModal={showContextModal}
context={context}
onPreferencesModalChange={setShowPreferencesModal}
onContextModalChange={setShowContextModal}
onContextChange={handleContextChange}
onPreferencesChange={handlePreferencesChange}
onCopy={handleCopy}
onClear={handleClear}
onClearHistory={handleClearHistory}
draft={draft}
getHistoryLength={getHistoryLength}
/>
{/* Persona Integration Indicator */}
{corePersona && !personaLoading && (
<div
style={{
padding: '8px 16px',
backgroundColor: '#f0f8ff',
borderBottom: '1px solid #e1e8ed',
fontSize: '12px',
color: '#666',
display: 'flex',
alignItems: 'center',
gap: '8px',
cursor: 'help',
position: 'relative'
}}
title={`🎭 YOUR PERSONALIZED WRITING ASSISTANT
🤔 WHAT IS A PERSONA?
A persona is your unique writing style profile that AI uses to create content that sounds exactly like you. It's like having a digital twin of your writing voice!
🎯 HOW DOES IT HELP YOU?
✅ Generates content that matches your natural writing style
✅ Maintains consistent voice across all your LinkedIn posts
✅ Saves time by understanding your preferences automatically
✅ Optimizes content for LinkedIn's algorithm and your audience
✅ Provides personalized suggestions based on your industry
🧠 HOW WAS IT CREATED?
Your persona was built by analyzing:
• Your website content and writing patterns
• Your research preferences and content goals
• Your target audience and industry focus
• Your communication style and tone preferences
• LinkedIn-specific optimization requirements
🤖 HOW DOES COPILOTKIT USE IT?
The AI assistant now knows:
• Your preferred sentence length and structure
• Your go-to words and phrases to use/avoid
• Your professional tone and communication style
• LinkedIn-specific optimization strategies
• Your engagement patterns and posting preferences
🚀 HYPER-PERSONALIZATION ACHIEVED!
Instead of generic content, you get:
• Content that sounds authentically like you
• Industry-specific insights and terminology
• LinkedIn algorithm-optimized posts
• Professional networking strategies
• Personalized engagement tactics
📊 YOUR PERSONA DETAILS:
🎭 Name: ${corePersona.persona_name}
📋 Style: ${corePersona.archetype}
💭 Philosophy: ${corePersona.core_belief}
📈 Confidence: ${corePersona.confidence_score}% accuracy
🎯 LINKEDIN OPTIMIZATION:
• Optimal length: ${platformPersona?.content_format_rules?.optimal_length || '150-300 words'}
• Posting frequency: ${platformPersona?.engagement_patterns?.posting_frequency || '2-3 times per week'}
• Hashtag strategy: ${platformPersona?.lexical_features?.hashtag_strategy || '3-5 relevant hashtags'}
• Engagement style: ${platformPersona?.engagement_patterns?.interaction_style || 'conversational'}
💡 TRY THIS: Ask the AI to "generate a LinkedIn post about [your topic]" and watch how it automatically applies your persona to create content that sounds like you!`}
>
<span style={{ color: '#0073b1' }}>🎭</span>
<span><strong>🎭 Your Writing Assistant:</strong> {corePersona.persona_name} ({corePersona.archetype})</span>
<span style={{ marginLeft: 'auto', fontSize: '11px' }}>
{corePersona.confidence_score}% accuracy |
Platform: LinkedIn Optimized
</span>
<span style={{ fontSize: '10px', color: '#999', marginLeft: '8px' }}>
(Hover for details)
</span>
</div>
)}
{/* Lightweight progress tracker under header */}
<div style={{
@@ -533,6 +451,7 @@ Instead of generic content, you get:
onDiscardChanges={handleDiscardChanges}
onDraftChange={handleDraftChange}
onPreviewToggle={handlePreviewToggle}
topic={context ? context.split('\n')[0].substring(0, 50) : undefined}
/>
@@ -560,7 +479,7 @@ Instead of generic content, you get:
'Great! I can see you have content to work with. Use the quick edit suggestions below to refine your post in real-time, or ask me to make specific changes.' :
`Hi! I'm your ALwrity Co-Pilot, your LinkedIn writing assistant${corePersona ? ` with ${corePersona.persona_name} persona optimization` : ''}. I can help you create professional posts, articles, carousels, video scripts, and comment responses. Try the new persona-aware actions for enhanced content generation!`
}}
suggestions={getIntelligentSuggestions()}
suggestions={getIntelligentSuggestions}
makeSystemMessage={(context: string, additional?: string) => {
const prefs = userPreferences;
const prefsLine = Object.keys(prefs).length ? `User preferences (remember and respect unless changed): ${JSON.stringify(prefs)}` : '';

View File

@@ -117,6 +117,15 @@ const RegisterLinkedInActions: React.FC = () => {
],
handler: async (args: any) => {
const prefs = readPrefs();
// Start loading state
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
detail: {
action: 'generateLinkedInPost',
message: 'Generating LinkedIn post with persona optimization...'
}
}));
// Emit progress init
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
steps: [
@@ -251,6 +260,10 @@ const RegisterLinkedInActions: React.FC = () => {
}
}));
// Debug: Log the content being sent
console.log('[LinkedIn Writer] Sending draft update:', fullContent?.substring(0, 100) + '...');
console.log('[LinkedIn Writer] Full content length:', fullContent?.length);
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: fullContent }));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
@@ -263,6 +276,10 @@ const RegisterLinkedInActions: React.FC = () => {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
// End loading state
console.log('[LinkedIn Writer] Ending loading state...');
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
// Return recommendations message that CopilotKit can render
const recommendations = res.data?.quality_metrics?.recommendations || [];
if (recommendations.length > 0) {
@@ -284,6 +301,8 @@ const RegisterLinkedInActions: React.FC = () => {
};
}
}
// End loading state on error
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
return { success: false, message: res.error || 'Failed to generate LinkedIn post' };
}
@@ -301,6 +320,15 @@ const RegisterLinkedInActions: React.FC = () => {
],
handler: async (args: any) => {
const prefs = readPrefs();
// Start loading state
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
detail: {
action: 'generateLinkedInArticle',
message: 'Generating LinkedIn article with persona optimization...'
}
}));
// Emit progress init for article
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
steps: [
@@ -429,6 +457,9 @@ const RegisterLinkedInActions: React.FC = () => {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
// End loading state
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
// Return recommendations message that CopilotKit can render
const recommendations = res.data?.quality_metrics?.recommendations || [];
if (recommendations.length > 0) {
@@ -450,6 +481,8 @@ const RegisterLinkedInActions: React.FC = () => {
};
}
}
// End loading state on error
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
return { success: false, message: res.error || 'Failed to generate LinkedIn article' };
}

View File

@@ -58,6 +58,14 @@ const RegisterLinkedInActionsEnhanced: React.FC = () => {
// Persona-aware progress tracking
const personaInfo = corePersona ? `using ${corePersona.persona_name} persona` : 'with standard settings';
// Start loading state for chat-triggered flow as well
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
detail: {
action: 'generateLinkedInPostWithPersona',
message: 'Generating LinkedIn post with persona optimization...'
}
}));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
steps: [
{ id: 'persona_analysis', label: `Analyzing ${personaInfo}` },
@@ -143,6 +151,13 @@ const RegisterLinkedInActionsEnhanced: React.FC = () => {
});
}
// Append hashtags and CTA if present
const hashtags = res.data.hashtags?.map((h: any) => h.hashtag).join(' ') || '';
const cta = res.data.call_to_action || '';
let fullContent = enhancedContent;
if (hashtags) fullContent += `\n\n${hashtags}`;
if (cta) fullContent += `\n\n${cta}`;
// Update progress with persona validation
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
detail: {
@@ -217,10 +232,28 @@ const RegisterLinkedInActionsEnhanced: React.FC = () => {
}
}));
// Update grounding data so citations and quality chips render
window.dispatchEvent(new CustomEvent('linkedinwriter:updateGroundingData', {
detail: {
researchSources: res.research_sources || [],
citations: res.data?.citations || [],
qualityMetrics: res.data?.quality_metrics || null,
groundingEnabled: res.data?.grounding_enabled || false,
searchQueries: res.data?.search_queries || []
}
}));
// Send draft content to editor
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: fullContent }));
// Complete progress and end loading
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
// Return enhanced content with persona information
return {
success: true,
content: enhancedContent,
content: fullContent,
persona_applied: corePersona ? {
name: corePersona.persona_name,
archetype: corePersona.archetype,
@@ -238,6 +271,7 @@ const RegisterLinkedInActionsEnhanced: React.FC = () => {
};
} else {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd', { detail: { error: res.error } }));
return { success: false, message: res.error || 'Failed to generate LinkedIn post' };
}
}

View File

@@ -0,0 +1,397 @@
import React from 'react';
import { Box, Typography, Chip, Button, Collapse, Link } from '@mui/material';
import { ExpandMore, ExpandLess, CheckCircle, Cancel, Help } from '@mui/icons-material';
interface SourceDocument {
title: string;
url: string;
text: string;
published_date?: string;
author?: string;
score: number;
}
interface Claim {
text: string;
confidence: number;
assessment: 'supported' | 'refuted' | 'insufficient_information';
supporting_sources: SourceDocument[];
refuting_sources: SourceDocument[];
reasoning?: string;
}
interface FactCheckResultsProps {
results: {
success: boolean;
claims: Claim[];
overall_confidence: number;
total_claims: number;
supported_claims: number;
refuted_claims: number;
insufficient_claims: number;
timestamp: string;
processing_time_ms?: number;
error?: string;
};
onClose: () => void;
}
const FactCheckResults: React.FC<FactCheckResultsProps> = ({ results, onClose }) => {
const [expandedClaims, setExpandedClaims] = React.useState<Set<number>>(new Set());
const toggleClaimExpansion = (index: number) => {
const newExpanded = new Set(expandedClaims);
if (newExpanded.has(index)) {
newExpanded.delete(index);
} else {
newExpanded.add(index);
}
setExpandedClaims(newExpanded);
};
const getAssessmentIcon = (assessment: string) => {
switch (assessment) {
case 'supported':
return <CheckCircle sx={{ color: '#4caf50', fontSize: 20 }} />;
case 'refuted':
return <Cancel sx={{ color: '#f44336', fontSize: 20 }} />;
default:
return <Help sx={{ color: '#ff9800', fontSize: 20 }} />;
}
};
const getAssessmentColor = (assessment: string) => {
switch (assessment) {
case 'supported':
return '#4caf50';
case 'refuted':
return '#f44336';
default:
return '#ff9800';
}
};
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.8) return '#4caf50';
if (confidence >= 0.6) return '#ff9800';
return '#f44336';
};
if (!results.success) {
return (
<Box
sx={{
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
}}
>
<Box
sx={{
backgroundColor: 'white',
borderRadius: 2,
padding: 3,
maxWidth: 500,
width: '90%',
maxHeight: '80vh',
overflow: 'auto'
}}
>
<Typography variant="h6" color="error" gutterBottom>
Fact-Checking Failed
</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
{results.error || 'An error occurred while checking facts. Please try again.'}
</Typography>
<Button variant="contained" onClick={onClose} fullWidth>
Close
</Button>
</Box>
</Box>
);
}
return (
<Box
sx={{
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
}}
>
<Box
sx={{
backgroundColor: 'white',
borderRadius: 2,
padding: 3,
maxWidth: 800,
width: '90%',
maxHeight: '80vh',
overflow: 'auto'
}}
>
{/* Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5" component="h2">
Fact-Check Results
</Typography>
<Button onClick={onClose} variant="outlined">
Close
</Button>
</Box>
{/* Summary */}
<Box sx={{ mb: 3, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
<Typography variant="h6" gutterBottom>
Fact-Check Summary
</Typography>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }}>
<Chip
label={`Overall Confidence: ${Math.round(results.overall_confidence * 100)}%`}
color={results.overall_confidence >= 0.8 ? 'success' : results.overall_confidence >= 0.6 ? 'warning' : 'error'}
variant="outlined"
/>
<Chip
label={`Total Claims: ${results.total_claims}`}
color="primary"
variant="outlined"
/>
<Chip
label={`Supported: ${results.supported_claims}`}
color="success"
variant="outlined"
/>
<Chip
label={`Refuted: ${results.refuted_claims}`}
color="error"
variant="outlined"
/>
<Chip
label={`Insufficient: ${results.insufficient_claims}`}
color="warning"
variant="outlined"
/>
</Box>
{/* Key Insights */}
<Box sx={{ mt: 2, p: 2, backgroundColor: 'white', borderRadius: 1, border: '1px solid #e0e0e0' }}>
<Typography variant="subtitle2" gutterBottom sx={{ fontWeight: 'bold', color: '#1976d2' }}>
Key Insights:
</Typography>
<Typography variant="body2" color="text.secondary">
{results.supported_claims > 0 && `${results.supported_claims} claim${results.supported_claims > 1 ? 's' : ''} verified with supporting evidence`}
{results.supported_claims > 0 && results.refuted_claims > 0 && ' • '}
{results.refuted_claims > 0 && `${results.refuted_claims} claim${results.refuted_claims > 1 ? 's' : ''} contradicted by sources`}
{results.insufficient_claims > 0 && (results.supported_claims > 0 || results.refuted_claims > 0) && ' • '}
{results.insufficient_claims > 0 && `⚠️ ${results.insufficient_claims} claim${results.insufficient_claims > 1 ? 's' : ''} need more evidence`}
</Typography>
</Box>
{results.processing_time_ms && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
Analysis completed in {results.processing_time_ms}ms using AI-powered fact-checking
</Typography>
)}
</Box>
{/* Claims */}
<Box>
<Typography variant="h6" gutterBottom>
Claims Analysis
</Typography>
{results.claims.map((claim, index) => (
<Box
key={index}
sx={{
border: '1px solid #e0e0e0',
borderRadius: 1,
mb: 2,
overflow: 'hidden'
}}
>
{/* Claim Header */}
<Box
sx={{
p: 2,
backgroundColor: '#fafafa',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
cursor: 'pointer'
}}
onClick={() => toggleClaimExpansion(index)}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1 }}>
{getAssessmentIcon(claim.assessment)}
<Typography variant="body1" sx={{ flex: 1 }}>
{claim.text}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
label={`${Math.round(claim.confidence * 100)}%`}
size="small"
sx={{
backgroundColor: getConfidenceColor(claim.confidence),
color: 'white'
}}
/>
<Chip
label={claim.assessment.replace('_', ' ')}
size="small"
sx={{
backgroundColor: getAssessmentColor(claim.assessment),
color: 'white'
}}
/>
{expandedClaims.has(index) ? <ExpandLess /> : <ExpandMore />}
</Box>
</Box>
{/* Claim Details */}
<Collapse in={expandedClaims.has(index)}>
<Box sx={{ p: 2 }}>
{/* Reasoning Section */}
<Box sx={{ mb: 2, p: 2, backgroundColor: '#f8f9fa', borderRadius: 1, border: '1px solid #e9ecef' }}>
<Typography variant="subtitle2" gutterBottom sx={{ fontWeight: 'bold', color: '#495057' }}>
Analysis Reasoning:
</Typography>
{claim.reasoning ? (
<Typography variant="body2" color="text.secondary">
{claim.reasoning}
</Typography>
) : (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
No detailed reasoning available for this assessment.
</Typography>
)}
</Box>
{/* Supporting Sources */}
{claim.supporting_sources.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" color="success.main" gutterBottom>
Supporting Sources ({claim.supporting_sources.length})
</Typography>
{claim.supporting_sources.map((source, sourceIndex) => (
<Box
key={sourceIndex}
sx={{
p: 1,
mb: 1,
backgroundColor: '#e8f5e8',
borderRadius: 1,
border: '1px solid #c8e6c9'
}}
>
<Link
href={source.url}
target="_blank"
rel="noopener noreferrer"
sx={{ fontWeight: 'bold', textDecoration: 'none' }}
>
{source.title}
</Link>
<Typography variant="caption" display="block" color="text.secondary">
<strong>Relevance Score:</strong> {Math.round(source.score * 100)}%
{source.author && ` • Author: ${source.author}`}
{source.published_date && ` • Published: ${source.published_date}`}
</Typography>
{source.text && (
<Box sx={{ mt: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 'bold' }}>
Relevant Excerpt:
</Typography>
<Typography variant="body2" sx={{ mt: 0.5, fontStyle: 'italic', backgroundColor: 'rgba(0,0,0,0.05)', p: 1, borderRadius: 0.5 }}>
"{source.text.substring(0, 300)}{source.text.length > 300 ? '...' : ''}"
</Typography>
</Box>
)}
</Box>
))}
</Box>
)}
{/* Refuting Sources */}
{claim.refuting_sources.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" color="error.main" gutterBottom>
Refuting Sources ({claim.refuting_sources.length})
</Typography>
{claim.refuting_sources.map((source, sourceIndex) => (
<Box
key={sourceIndex}
sx={{
p: 1,
mb: 1,
backgroundColor: '#ffebee',
borderRadius: 1,
border: '1px solid #ffcdd2'
}}
>
<Link
href={source.url}
target="_blank"
rel="noopener noreferrer"
sx={{ fontWeight: 'bold', textDecoration: 'none' }}
>
{source.title}
</Link>
<Typography variant="caption" display="block" color="text.secondary">
<strong>Relevance Score:</strong> {Math.round(source.score * 100)}%
{source.author && ` • Author: ${source.author}`}
{source.published_date && ` • Published: ${source.published_date}`}
</Typography>
{source.text && (
<Box sx={{ mt: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 'bold' }}>
Relevant Excerpt:
</Typography>
<Typography variant="body2" sx={{ mt: 0.5, fontStyle: 'italic', backgroundColor: 'rgba(0,0,0,0.05)', p: 1, borderRadius: 0.5 }}>
"{source.text.substring(0, 300)}{source.text.length > 300 ? '...' : ''}"
</Typography>
</Box>
)}
</Box>
))}
</Box>
)}
{/* No Sources */}
{claim.supporting_sources.length === 0 && claim.refuting_sources.length === 0 && (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
No sources found for this claim.
</Typography>
)}
</Box>
</Collapse>
</Box>
))}
</Box>
{/* Footer */}
<Box sx={{ mt: 3, pt: 2, borderTop: '1px solid #e0e0e0' }}>
<Typography variant="caption" color="text.secondary">
Analysis completed at {new Date(results.timestamp).toLocaleString()}
</Typography>
</Box>
</Box>
</Box>
);
};
export default FactCheckResults;

View File

@@ -7,16 +7,9 @@ interface HeaderProps {
userPreferences: LinkedInPreferences;
chatHistory: any[];
showPreferencesModal: boolean;
showContextModal: boolean;
context: string;
onPreferencesModalChange: (show: boolean) => void;
onContextModalChange: (show: boolean) => void;
onContextChange: (value: string) => void;
onPreferencesChange: (prefs: Partial<LinkedInPreferences>) => void;
onCopy: () => void;
onClear: () => void;
onClearHistory: () => void;
draft: string;
getHistoryLength: () => number;
}
@@ -24,16 +17,9 @@ export const Header: React.FC<HeaderProps> = ({
userPreferences,
chatHistory,
showPreferencesModal,
showContextModal,
context,
onPreferencesModalChange,
onContextModalChange,
onContextChange,
onPreferencesChange,
onCopy,
onClear,
onClearHistory,
draft,
getHistoryLength
}) => {
const handlePreferenceChange = (key: keyof LinkedInPreferences, value: any) => {
@@ -68,16 +54,8 @@ export const Header: React.FC<HeaderProps> = ({
fontWeight: 700,
letterSpacing: '-0.5px'
}}>
LinkedIn Writer
ALwrity LinkedIn Assistant
</h1>
<p style={{
margin: '6px 0 0 0',
fontSize: '14px',
opacity: 0.9,
fontWeight: 400
}}>
Professional content creation for LinkedIn
</p>
</div>
</div>
@@ -126,13 +104,79 @@ export const Header: React.FC<HeaderProps> = ({
}}>
<div style={{ marginBottom: '16px' }}>
<h4 style={{ margin: '0 0 12px 0', color: '#333', fontSize: '16px', fontWeight: 600 }}>
Content Preferences & Context
Content Preferences & Persona
</h4>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '16px' }}>
<strong>Current Settings:</strong> {userPreferences.tone} tone {userPreferences.industry || 'Not set'} industry {chatHistory.length} messages
</div>
</div>
{/* Persona Section */}
<div style={{
border: '1px solid #e2e8f0',
borderRadius: '8px',
padding: '16px',
marginBottom: '16px',
background: '#f8f9fa'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
<h5 style={{ margin: 0, color: '#2d3748', fontSize: '14px', fontWeight: '600' }}>
Writing Persona
</h5>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px', color: '#4a5568' }}>
<input
type="radio"
name="personaEnabled"
defaultChecked={true}
style={{ margin: 0 }}
/>
On
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px', color: '#4a5568' }}>
<input
type="radio"
name="personaEnabled"
style={{ margin: 0 }}
/>
Off
</label>
</div>
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '12px',
background: 'white',
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>
</div>
<div style={{
marginTop: '8px',
fontSize: '11px',
color: '#666',
fontStyle: 'italic'
}}>
Hover over persona for detailed information
</div>
</div>
{/* Preferences Grid */}
<div style={{
display: 'grid',
@@ -300,111 +344,10 @@ export const Header: React.FC<HeaderProps> = ({
)}
</div>
{/* Context & Notes Button */}
<div
style={{
position: 'relative',
cursor: 'pointer'
}}
onMouseEnter={() => onContextModalChange(true)}
onMouseLeave={() => onContextModalChange(false)}
>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '10px 16px',
background: 'rgba(255, 255, 255, 0.15)',
borderRadius: '24px',
border: '1px solid rgba(255, 255, 255, 0.2)',
transition: 'all 0.2s ease',
backdropFilter: 'blur(10px)'
}}>
<span style={{ fontSize: '14px', opacity: 0.9 }}>📝</span>
<span style={{ fontSize: '13px', fontWeight: 600 }}>Context & Notes</span>
<span style={{ fontSize: '10px', opacity: 0.7 }}></span>
</div>
{/* Context & Notes Modal */}
{showContextModal && (
<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={{ marginBottom: '16px' }}>
<h4 style={{ margin: '0 0 12px 0', color: '#333', fontSize: '16px', fontWeight: 600 }}>
Context & Notes
</h4>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '16px' }}>
Add context, notes, or specific requirements for your LinkedIn content
</div>
</div>
<textarea
value={context}
onChange={(e) => onContextChange(e.target.value)}
placeholder="Add context, notes, or specific requirements for your LinkedIn content..."
style={{
width: '100%',
minHeight: '120px',
padding: '12px',
border: '1px solid #ddd',
borderRadius: '8px',
fontSize: '14px',
fontFamily: 'inherit',
resize: 'vertical',
background: '#f8f9fa'
}}
/>
</div>
)}
</div>
</div>
</div>
<div style={{ display: 'flex', gap: 12 }}>
<button
onClick={onCopy}
disabled={!draft.trim()}
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: draft.trim() ? 'pointer' : 'not-allowed',
fontSize: 14,
fontWeight: 500
}}
>
Copy
</button>
<button
onClick={onClear}
disabled={!draft.trim()}
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: draft.trim() ? 'pointer' : 'not-allowed',
fontSize: 14,
fontWeight: 500
}}
>
Clear
</button>
<button
onClick={onClearHistory}
style={{

View File

@@ -156,10 +156,12 @@ export function useLinkedInWriter() {
};
const handleProgressComplete = () => {
console.log('[LinkedIn Writer] Progress completed - hiding progress tracker');
setProgressSteps(prev => prev.map(s => s.status === 'completed' ? s : { ...s, status: 'completed', timestamp: new Date().toISOString() }));
setProgressActive(false);
// Keep progress visible for a moment to show completion, then hide
setTimeout(() => {
console.log('[LinkedIn Writer] Hiding progress steps after delay');
setProgressSteps([]);
}, 1500);
};
@@ -234,6 +236,9 @@ export function useLinkedInWriter() {
// Handle draft updates from CopilotKit actions
useEffect(() => {
const handleUpdateDraft = (event: CustomEvent) => {
console.log('[LinkedIn Writer] Draft updated:', event.detail?.substring(0, 100) + '...');
console.log('[LinkedIn Writer] Draft length:', event.detail?.length);
console.log('[LinkedIn Writer] Setting draft and clearing loading state...');
setDraft(event.detail);
setIsGenerating(false);
setLoadingMessage('');
@@ -243,6 +248,7 @@ export function useLinkedInWriter() {
// Hide progress tracker when content is generated
setProgressActive(false);
setProgressSteps([]);
console.log('[LinkedIn Writer] Draft update complete');
};
const handleAppendDraft = (event: CustomEvent) => {
@@ -255,15 +261,18 @@ export function useLinkedInWriter() {
const handleLoadingStart = (event: CustomEvent) => {
const { action, message } = event.detail;
console.log('[LinkedIn Writer] Loading started:', { action, message });
setCurrentAction(action);
setLoadingMessage(message);
setIsGenerating(true);
};
const handleLoadingEnd = (event: CustomEvent) => {
console.log('[LinkedIn Writer] Loading ended - clearing all loading states');
setIsGenerating(false);
setLoadingMessage('');
setCurrentAction(null);
console.log('[LinkedIn Writer] Loading state cleared');
};
const handleApplyEdit = (event: CustomEvent) => {

View File

@@ -13,12 +13,6 @@ export function formatDraftContent(content: string, citations?: any[], researchS
// Insert inline citations if available
if (citations && citations.length > 0 && researchSources && researchSources.length > 0) {
console.log('🔍 [formatDraftContent] Processing citations:', {
citationsCount: citations.length,
researchSourcesCount: researchSources.length,
citations: citations,
contentLength: content.length
});
// Create a map of citation references to source numbers
const citationMap = new Map();
@@ -28,8 +22,6 @@ export function formatDraftContent(content: string, citations?: any[], researchS
citationMap.set(citation.reference, sourceNum);
}
});
console.log('🔍 [formatDraftContent] Citation map created:', citationMap);
// Since citation references don't exist in the content text,
// we need to insert citations strategically throughout the content
@@ -51,26 +43,13 @@ export function formatDraftContent(content: string, citations?: any[], researchS
const sentenceWithCitation = targetSentence.trim() + citeHtml;
sentencesWithCitations[targetSentenceIndex] = sentenceWithCitation;
console.log(`✅ [formatDraftContent] Added citation [${sourceNum}] to sentence ${targetSentenceIndex + 1}`);
});
// Reconstruct content with citations
formatted = sentences.map((sentence, index) => {
return sentencesWithCitations[index] || sentence;
}).join('. ') + '.';
console.log(`✅ [formatDraftContent] Inserted ${totalCitations} citations strategically throughout content`);
// Debug: Show sample of content with citations
const sampleContent = formatted.substring(0, 500) + (formatted.length > 500 ? '...' : '');
console.log('🔍 [formatDraftContent] Sample content with citations:', sampleContent);
// Debug: Count citation markers in final content
const citationMarkers = (formatted.match(/\[\d+\]/g) || []).length;
console.log(`🔍 [formatDraftContent] Found ${citationMarkers} citation markers in final content`);
}
console.log('🔍 [formatDraftContent] Final formatted content length:', formatted.length);
}
// Format hashtags

View File

@@ -5,6 +5,9 @@
import { useCopilotContext } from '@copilotkit/react-core';
// Optional debug flag: set to true to enable verbose logs locally
const DEBUG_PERSISTENCE = false;
// Storage keys for different types of data
export const STORAGE_KEYS = {
CHAT_HISTORY: 'alwrity-copilot-chat-history',
@@ -198,7 +201,7 @@ export class CopilotPersistenceManager {
public saveDraftContent(draft: string): void {
try {
localStorage.setItem(STORAGE_KEYS.DRAFT_CONTENT, draft);
console.log('💾 Saved draft content');
if (DEBUG_PERSISTENCE) console.log('💾 Saved draft content');
} catch (error) {
console.error('❌ Failed to save draft content:', error);
}