ALwrity HALLUCINATION DETECTOR AND ASSISTIVE WRITING
This commit is contained in:
@@ -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)}` : '';
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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={{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user