Added citation and quality metrics to the content editor.

This commit is contained in:
ajaysi
2025-09-03 09:40:05 +05:30
parent 10b50f9732
commit 5efee4235d
35 changed files with 6987 additions and 1123 deletions

View File

@@ -1,12 +1,13 @@
import React from 'react';
import React, { useEffect } from 'react';
import { CopilotSidebar } from '@copilotkit/react-ui';
import { useCopilotReadable, useCopilotAction } from '@copilotkit/react-core';
import { useCopilotReadable, useCopilotAction, useCopilotContext } from '@copilotkit/react-core';
import '@copilotkit/react-ui/styles.css';
import './styles/alwrity-copilot.css';
import RegisterLinkedInActions from './RegisterLinkedInActions';
import RegisterLinkedInEditActions from './RegisterLinkedInEditActions';
import { Header, ContentEditor, LoadingIndicator, WelcomeMessage } from './components';
import { useLinkedInWriter } from './hooks/useLinkedInWriter';
import { useCopilotPersistence } from './utils/enhancedPersistence';
const useCopilotActionTyped = useCopilotAction as any;
@@ -34,6 +35,13 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
showContextModal,
showPreview,
// Grounding data
researchSources,
citations,
qualityMetrics,
groundingEnabled,
searchQueries,
// Setters
setDraft,
setIsPreviewing,
@@ -57,6 +65,74 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
summarizeHistory
} = useLinkedInWriter();
// Get enhanced persistence functionality
const {
persistenceManager,
copilotContext,
saveChatHistory,
loadChatHistory,
addChatMessage,
saveUserPreferences: savePersistedPreferences,
loadUserPreferences: loadPersistedPreferences,
saveConversationContext,
loadConversationContext,
saveDraftContent,
loadDraftContent,
saveLastSession,
loadLastSession,
getStorageStats
} = useCopilotPersistence();
// Sync component state with enhanced persistence
useEffect(() => {
console.log('[LinkedIn Writer] Component mounted, enhanced persistence enabled');
// Load persisted data on component mount
const loadPersistedData = () => {
try {
// Load chat history
const chatHistory = loadChatHistory();
console.log(`📖 Loaded ${chatHistory.length} persisted chat messages`);
// Load user preferences
const persistedPrefs = loadPersistedPreferences();
console.log('📖 Loaded persisted user preferences:', persistedPrefs);
// Load conversation context
const conversationContext = loadConversationContext();
console.log('📖 Loaded persisted conversation context:', conversationContext);
// Load draft content
const persistedDraft = loadDraftContent();
if (persistedDraft && !draft) {
console.log('📖 Restoring persisted draft content');
// Note: We'll need to integrate this with the useLinkedInWriter hook
}
// Load last session
const lastSession = loadLastSession();
if (lastSession) {
console.log('📖 Last session:', lastSession);
}
// Get storage statistics
const stats = getStorageStats();
console.log('📊 Persistence stats:', stats);
} catch (error) {
console.error('❌ Error loading persisted data:', error);
}
};
// Load data after a short delay to allow CopilotKit to initialize
setTimeout(loadPersistedData, 1000);
// Save session data when component unmounts
return () => {
saveLastSession();
};
}, []);
// Handle preview changes
const handleConfirmChanges = () => {
if (pendingEdit) {
@@ -81,6 +157,9 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
const updated = { ...userPreferences, ...prefs };
setUserPreferences(updated);
savePreferences(prefs);
// Also save to enhanced persistence
savePersistedPreferences(prefs);
};
// Share current draft and context with CopilotKit for better context awareness
@@ -89,6 +168,13 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
value: draft,
categories: ['social', 'linkedin', 'draft']
});
// Auto-save draft content when it changes
useEffect(() => {
if (draft && draft.trim().length > 0) {
saveDraftContent(draft);
}
}, [draft, saveDraftContent]);
useCopilotReadable({
description: 'User context and notes for LinkedIn content',
@@ -256,6 +342,9 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
draft={draft}
getHistoryLength={getHistoryLength}
/>
{/* Debug: Enhanced Persistence Test Buttons (remove in production) */}
{/* Main Content */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
@@ -266,9 +355,9 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
currentAction={currentAction}
/>
{/* Content Area */}
{draft || isGenerating ? (
/* Editor Panel - Show when there's content or generating */
{/* Content Area */}
{draft || isGenerating ? (<>
{/* Editor Panel - Show when there's content or generating */}
<ContentEditor
isPreviewing={isPreviewing}
pendingEdit={pendingEdit}
@@ -277,12 +366,20 @@ const LinkedInWriter: React.FC<LinkedInWriterProps> = ({ className = '' }) => {
showPreview={showPreview}
isGenerating={isGenerating}
loadingMessage={loadingMessage}
// Grounding data
researchSources={researchSources}
citations={citations}
qualityMetrics={qualityMetrics}
groundingEnabled={groundingEnabled}
searchQueries={searchQueries}
onConfirmChanges={handleConfirmChanges}
onDiscardChanges={handleDiscardChanges}
onDraftChange={handleDraftChange}
onPreviewToggle={handlePreviewToggle}
/>
) : (
</>) : (
/* Welcome Message - Show when no content */
<WelcomeMessage
draft={draft}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { linkedInWriterApi, LinkedInPostRequest } from '../../services/linkedInWriterApi';
import { linkedInWriterApi, LinkedInPostRequest, GroundingLevel } from '../../services/linkedInWriterApi';
import {
mapPostType,
mapTone,
@@ -49,7 +49,9 @@ const RegisterLinkedInActions: React.FC = () => {
include_call_to_action: args?.include_call_to_action ?? (prefs.include_call_to_action ?? true),
research_enabled: args?.research_enabled ?? (prefs.research_enabled ?? true),
search_engine: mapSearchEngine(args?.search_engine || prefs.search_engine),
max_length: args?.max_length || prefs.max_length || 2000
max_length: args?.max_length || prefs.max_length || 2000,
grounding_level: 'enhanced' as GroundingLevel,
include_citations: true
});
if (res.success && res.data) {
@@ -61,6 +63,24 @@ const RegisterLinkedInActions: React.FC = () => {
if (hashtags) fullContent += `\n\n${hashtags}`;
if (cta) fullContent += `\n\n${cta}`;
// Debug: Log the full response structure
console.log('[LinkedIn Writer] Full API response:', res);
console.log('[LinkedIn Writer] Research sources:', res.research_sources);
console.log('[LinkedIn Writer] Citations:', res.data?.citations);
console.log('[LinkedIn Writer] Quality metrics:', res.data?.quality_metrics);
console.log('[LinkedIn Writer] Grounding enabled:', res.data?.grounding_enabled);
// Update grounding data
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 || []
}
}));
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: fullContent }));
return { success: true, content: fullContent };
}
@@ -90,11 +110,32 @@ const RegisterLinkedInActions: React.FC = () => {
seo_optimization: args?.seo_optimization ?? (prefs.seo_optimization ?? true),
research_enabled: args?.research_enabled ?? (prefs.research_enabled ?? true),
search_engine: mapSearchEngine(args?.search_engine || prefs.search_engine),
word_count: args?.word_count || prefs.word_count || 1500
word_count: args?.word_count || prefs.word_count || 1500,
grounding_level: 'enhanced' as GroundingLevel,
include_citations: true
});
if (res.success && res.data) {
const content = `# ${res.data.title}\n\n${res.data.content}`;
// Debug: Log the full response structure
console.log('[LinkedIn Writer] Full API response:', res);
console.log('[LinkedIn Writer] Research sources:', res.research_sources);
console.log('[LinkedIn Writer] Citations:', res.data?.citations);
console.log('[LinkedIn Writer] Quality metrics:', res.data?.quality_metrics);
console.log('[LinkedIn Writer] Grounding enabled:', res.data?.grounding_enabled);
// Update grounding data
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 || []
}
}));
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
return { success: true, content };
}

View File

@@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import { formatDraftContent, diffMarkup } from '../utils/contentFormatters';
interface ContentEditorProps {
isPreviewing: boolean;
pendingEdit: { src: string; target: string } | null;
@@ -9,13 +10,28 @@ interface ContentEditorProps {
showPreview: boolean;
isGenerating: boolean;
loadingMessage: string;
// Grounding data props
researchSources?: any[];
citations?: any[];
qualityMetrics?: any;
groundingEnabled?: boolean;
searchQueries?: string[];
onConfirmChanges: () => void;
onDiscardChanges: () => void;
onDraftChange: (value: string) => void;
onPreviewToggle: () => void;
}
export const ContentEditor: React.FC<ContentEditorProps> = ({
// Extend HTMLDivElement interface for custom tooltip properties
interface ExtendedDivElement extends HTMLDivElement {
_researchTooltip?: HTMLDivElement | null;
_citationsTooltip?: HTMLDivElement | null;
_searchQueriesTooltip?: HTMLDivElement | null;
}
export { ContentEditor };
const ContentEditor: React.FC<ContentEditorProps> = ({
isPreviewing,
pendingEdit,
livePreviewHtml,
@@ -23,6 +39,12 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
showPreview,
isGenerating,
loadingMessage,
// Grounding data props
researchSources,
citations,
qualityMetrics,
groundingEnabled,
searchQueries,
onConfirmChanges,
onDiscardChanges,
onDraftChange,
@@ -35,6 +57,316 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
}
}, [draft, showPreview, onPreviewToggle]);
// Debug logging for quality metrics and research sources
useEffect(() => {
console.log('🔍 [ContentEditor] Props received:', {
researchSources: researchSources,
citations: citations,
qualityMetrics: qualityMetrics,
groundingEnabled: groundingEnabled,
draftLength: draft?.length || 0
});
if (qualityMetrics) {
console.log('🔍 [ContentEditor] Quality metrics details:', {
overall_score: qualityMetrics.overall_score,
factual_accuracy: qualityMetrics.factual_accuracy,
source_verification: qualityMetrics.source_verification,
professional_tone: qualityMetrics.professional_tone,
industry_relevance: qualityMetrics.industry_relevance,
citation_coverage: qualityMetrics.citation_coverage
});
}
if (researchSources && researchSources.length > 0) {
console.log('🔍 [ContentEditor] Research sources details:', {
count: researchSources.length,
sample: researchSources.slice(0, 3).map(s => ({
title: s.title,
url: s.url,
source_type: s.source_type,
credibility_score: s.credibility_score,
relevance_score: s.relevance_score,
domain_authority: s.domain_authority
}))
});
}
}, [researchSources, citations, qualityMetrics, groundingEnabled, draft]);
// Citation hover functionality
useEffect(() => {
if (!researchSources || researchSources.length === 0) return;
console.log('🔍 [Citation Hover] useEffect triggered with', researchSources.length, 'sources');
// Keep track of currently open tooltip
let currentOpenTooltip: HTMLDivElement | null = null;
// Extend Element interface for our custom property
interface ExtendedElement extends Element {
_liwTip?: HTMLDivElement | null;
}
const initCitationHover = () => {
try {
console.log('🔍 [Citation Hover] Script starting...');
console.log('🔍 [Citation Hover] Research sources count:', researchSources.length);
// Test if script is running
document.body.style.setProperty('--citation-hover-active', 'true');
console.log('🔍 [Citation Hover] Script is running, CSS variable set');
// Wait for content to be rendered
const waitForCitations = () => {
const citations = document.querySelectorAll('.liw-cite');
console.log('🔍 [Citation Hover] Looking for citations, found:', citations.length);
if (citations.length === 0) {
// If no citations found, wait a bit and try again
console.log('🔍 [Citation Hover] No citations found, waiting...');
setTimeout(waitForCitations, 200);
return;
}
console.log('🔍 [Citation Hover] Found', citations.length, 'citation elements');
citations.forEach((cite, idx) => {
console.log(`🔍 [Citation Hover] Citation ${idx}: ${cite.outerHTML}`);
console.log(`🔍 [Citation Hover] Citation classes: ${cite.className}`);
console.log(`🔍 [Citation Hover] Citation data-source-index: ${cite.getAttribute('data-source-index')}`);
});
setupCitationHover();
};
const setupCitationHover = () => {
console.log('🔍 [Citation Hover] Initializing hover functionality...');
const data = researchSources;
console.log('🔍 [Citation Hover] Research data loaded:', data.length, 'sources');
const openOverlay = (idx: string, src: any) => {
console.log('🔍 [Citation Hover] Opening overlay for source', idx, src);
const existing = document.getElementById('liw-cite-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.id = 'liw-cite-overlay';
overlay.style.position = 'fixed';
overlay.style.inset = '0';
overlay.style.background = 'rgba(0,0,0,0.35)';
overlay.style.backdropFilter = 'blur(2px)';
overlay.style.zIndex = '100000';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
const modal = document.createElement('div');
modal.style.width = 'min(720px, 92vw)';
modal.style.maxHeight = '80vh';
modal.style.overflow = 'auto';
modal.style.borderRadius = '14px';
modal.style.background = 'linear-gradient(180deg, #ffffff, #f8fdff)';
modal.style.border = '1px solid #cfe9f7';
modal.style.boxShadow = '0 24px 80px rgba(10,102,194,0.25)';
modal.style.padding = '18px 20px';
const title = (src.title || 'Untitled').replace(/</g, '&lt;');
const url = (src.url || '').replace(/</g, '&lt;');
const sourceType = src.source_type ? String(src.source_type).replace('_', ' ') : '';
modal.innerHTML =
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">' +
'<div style="font-size:16px;font-weight:800;color:#0a66c2">Source ' + idx + '</div>' +
'<button id="liw-cite-close" style="border:none;background:#eff6ff;color:#0a66c2;border-radius:8px;padding:8px 12px;cursor:pointer;font-weight:700">✕ Close</button>' +
'</div>' +
'<div style="font-size:18px;font-weight:700;color:#1f2937;margin-bottom:8px">' + title + '</div>' +
'<a href="' + (src.url || '#') + '" target="_blank" style="display:inline-block;color:#0a66c2;text-decoration:none;margin-bottom:12px;font-size:14px;font-weight:600;">View Source →</a>' +
(src.content ? '<div style="margin-bottom:16px;color:#374151;font-size:14px;line-height:1.6;background:#f9fafb;padding:16px;border-radius:8px;border-left:4px solid #0a66c2;">' + src.content + '</div>' : '') +
'<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px">' +
(typeof src.relevance_score === 'number' ? '<span style="background:#eef6ff;border:1px solid #d9ecff;border-radius:999px;padding:8px 12px;font-size:13px;color:#055a8c;font-weight:600">Relevance: ' + Math.round(src.relevance_score * 100) + '%</span>' : '') +
(typeof src.credibility_score === 'number' ? '<span style="background:#eef6ff;border:1px solid #d9ecff;border-radius:999px;padding:8px 12px;font-size:13px;color:#055a8c;font-weight:600">Credibility: ' + Math.round(src.credibility_score * 100) + '%</span>' : '') +
(typeof src.domain_authority === 'number' ? '<span style="background:#eef6ff;border:1px solid #d9ecff;border-radius:999px;padding:8px 12px;font-size:13px;color:#055a8c;font-weight:600">Authority: ' + Math.round(src.domain_authority * 100) + '%</span>' : '') +
'</div>' +
'<div style="display:flex;gap:16px;color:#6b7280;font-size:13px;padding-top:12px;border-top:1px solid #e5e7eb">' +
(src.source_type ? '<div>Type: <span style="color:#374151;font-weight:600">' + src.source_type.replace('_', ' ') + '</span></div>' : '') +
(src.publication_date ? '<div>Published: <span style="color:#374151;font-weight:600">' + src.publication_date + '</span></div>' : '') +
'</div>' +
(src.raw_result ? '<div style="color:#6b7280;font-size:12px;margin-top:12px;padding:8px;background:#f3f4f6;border-radius:6px;border-top:1px solid #e5e7eb;">Raw Data: ' + JSON.stringify(src.raw_result).substring(0, 150) + (JSON.stringify(src.raw_result).length > 150 ? '...' : '') + '</div>' : '');
overlay.appendChild(modal);
document.body.appendChild(overlay);
const close = () => {
try { overlay.remove(); } catch(_){}
};
overlay.addEventListener('click', (e) => {
if(e.target === overlay) close();
});
document.getElementById('liw-cite-close')?.addEventListener('click', close);
document.addEventListener('keydown', function esc(ev: KeyboardEvent) {
if(ev.key === 'Escape') {
close();
document.removeEventListener('keydown', esc);
}
});
};
// Add event listeners directly to each citation element
const citations = document.querySelectorAll('.liw-cite');
citations.forEach((cite) => {
console.log('🔍 [Citation Hover] Adding event listeners to citation:', cite.outerHTML);
cite.addEventListener('mouseenter', () => {
console.log('🔍 [Citation Hover] Mouse enter on citation:', cite.outerHTML);
// Close any existing tooltip first
if (currentOpenTooltip) {
try { currentOpenTooltip.remove(); } catch(_) {}
currentOpenTooltip = null;
}
const idx = cite.getAttribute('data-source-index');
console.log('🔍 [Citation Hover] Citation index:', idx);
if (!idx) return;
const i = parseInt(idx, 10) - 1;
const src = data[i];
if (!src) {
console.log('🔍 [Citation Hover] No source found for index:', idx);
return;
}
console.log('🔍 [Citation Hover] Creating tooltip for source:', src);
let tip = document.createElement('div');
tip.className = 'liw-cite-tip';
tip.style.position = 'fixed';
tip.style.zIndex = '99999';
tip.style.maxWidth = '420px';
tip.style.background = 'linear-gradient(180deg, #ffffff, #f8fdff)';
tip.style.border = '1px solid #cfe9f7';
tip.style.borderRadius = '10px';
tip.style.boxShadow = '0 12px 40px rgba(10,102,194,0.18)';
tip.style.padding = '12px 14px';
tip.style.fontSize = '12px';
tip.style.color = '#1f2937';
tip.style.backdropFilter = 'blur(5px)';
const title = (src.title || 'Untitled').replace(/</g, '&lt;');
const url = (src.url || '').replace(/</g, '&lt;');
const sourceType = src.source_type ? String(src.source_type).replace('_', ' ') : '';
tip.innerHTML =
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">' +
'<div style="font-weight:700;color:#0a66c2">Source ' + idx + '</div>' +
'<button class="liw-pin" title="Pin" style="border:none;background:#eef6ff;border-radius:8px;padding:4px 8px;cursor:pointer;color:#0a66c2;font-weight:800">📌</button>' +
'</div>' +
'<div style="font-weight:600;margin-bottom:6px;color:#1f2937">' + title + '</div>' +
'<a href="' + (src.url || '#') + '" target="_blank" style="color:#0a66c2;text-decoration:none;margin-bottom:8px;display:block;font-weight:600;">View Source →</a>' +
(src.content ? '<div style="margin-bottom:8px;color:#374151;font-size:11px;line-height:1.4;background:#f9fafb;padding:8px;border-radius:6px;border-left:3px solid #0a66c2;">' + src.content + '</div>' : '') +
'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px">' +
(typeof src.relevance_score === 'number' ? '<span style="background:#eef6ff;border:1px solid #d9ecff;border-radius:999px;padding:4px 8px;font-size:11px;color:#055a8c;font-weight:600">Relevance: ' + Math.round(src.relevance_score * 100) + '%</span>' : '') +
(typeof src.credibility_score === 'number' ? '<span style="background:#eef6ff;border:1px solid #d9ecff;border-radius:999px;padding:4px 8px;font-size:11px;color:#055a8c;font-weight:600">Credibility: ' + Math.round(src.credibility_score * 100) + '%</span>' : '') +
(typeof src.domain_authority === 'number' ? '<span style="background:#eef6ff;border:1px solid #d9ecff;border-radius:999px;padding:4px 8px;font-size:11px;color:#055a8c;font-weight:600">Authority: ' + Math.round(src.domain_authority * 100) + '%</span>' : '') +
'</div>' +
(src.source_type ? '<div style="color:#6b7280;font-size:11px;margin-bottom:4px">Type: <span style="color:#374151;font-weight:600">' + src.source_type.replace('_', ' ') + '</span></div>' : '') +
(src.publication_date ? '<div style="color:#6b7280;font-size:11px">Published: <span style="color:#374151;font-weight:600">' + src.publication_date + '</span></div>' : '') +
(src.raw_result ? '<div style="color:#6b7280;font-size:11px;margin-top:4px;padding:4px;background:#f3f4f6;border-radius:4px;">Raw Data: ' + JSON.stringify(src.raw_result).substring(0, 100) + (JSON.stringify(src.raw_result).length > 100 ? '...' : '') + '</div>' : '');
document.body.appendChild(tip);
const rect = cite.getBoundingClientRect();
tip.style.left = Math.min(rect.left, window.innerWidth - 460) + 'px';
tip.style.top = (rect.bottom + 8) + 'px';
tip.querySelector('.liw-pin')?.addEventListener('click', (ev) => {
ev.stopPropagation();
openOverlay(idx, src);
try { tip.remove(); } catch(_) {
// Remove the custom property reference
const extendedTip = tip as any;
extendedTip._liwTip = undefined;
}
currentOpenTooltip = null;
});
(cite as ExtendedElement)._liwTip = tip;
currentOpenTooltip = tip;
console.log('🔍 [Citation Hover] Tooltip created and positioned');
});
cite.addEventListener('mouseleave', () => {
console.log('🔍 [Citation Hover] Mouse leave on citation:', cite.outerHTML);
const extendedCite = cite as ExtendedElement;
if (extendedCite._liwTip) {
try { extendedCite._liwTip.remove(); } catch(_) {}
extendedCite._liwTip = null;
currentOpenTooltip = null;
}
});
});
console.log('✅ [Citation Hover] Hover functionality initialized for', citations.length, 'citations');
};
// Start waiting for citations with a longer delay to ensure content is rendered
setTimeout(waitForCitations, 500);
} catch(e: any) {
console.warn('liw cite tooltip init failed', e);
console.error('Error details:', e);
// Show error in UI
const errorDiv = document.createElement('div');
errorDiv.style.cssText = 'position:fixed;top:10px;right:10px;background:#ffebee;border:1px solid #f44336;border-radius:4px;padding:10px;z-index:100000;color:#c62828;';
errorDiv.innerHTML = 'Citation hover failed: ' + e.message;
document.body.appendChild(errorDiv);
setTimeout(() => errorDiv.remove(), 5000);
}
};
// Initialize citation hover after a short delay to ensure content is rendered
const timer = setTimeout(initCitationHover, 100);
// Cleanup function
return () => {
clearTimeout(timer);
// Remove any existing tooltips
const tooltips = document.querySelectorAll('.liw-cite-tip');
tooltips.forEach(tip => tip.remove());
// Remove overlay if exists
const overlay = document.getElementById('liw-cite-overlay');
if (overlay) overlay.remove();
// Reset current tooltip reference
currentOpenTooltip = null;
};
}, [researchSources]); // Dependency on researchSources
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';
};
const chips = 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('🔍 [ContentEditor] Chips array created:', {
qualityMetrics: qualityMetrics,
chips: chips,
chipsLength: chips.length
});
// 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={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Predictive Diff Preview - Show when there are pending changes */}
@@ -110,7 +442,7 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
borderRadius: '8px',
background: '#f8fdff',
overflow: 'hidden',
height: '100%'
height: 'auto'
}}>
<div style={{
padding: '12px 16px',
@@ -123,8 +455,283 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
alignItems: 'center',
justifyContent: 'space-between'
}}>
<span>LinkedIn Content Preview</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<span>LinkedIn Content Preview</span>
{/* Research Sources & Citations Count Chips */}
{researchSources && researchSources.length > 0 && (
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{/* Research Sources Count Chip */}
<div
style={{
background: 'rgba(255, 255, 255, 0.9)',
border: '1px solid rgba(2, 119, 189, 0.3)',
borderRadius: '999px',
padding: '4px 10px',
fontSize: '11px',
fontWeight: '600',
color: '#0277bd',
cursor: 'pointer',
transition: 'all 0.2s ease',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
title={`${researchSources.length} research sources available. Hover to see details.`}
onMouseEnter={(e) => {
// Create and show research sources tooltip
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
z-index: 100000;
background: white;
border: 1px solid #cfe9f7;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 16px;
max-width: 500px;
max-height: 400px;
overflow-y: auto;
font-size: 12px;
`;
tooltip.innerHTML = `
<div style="margin-bottom: 12px; font-weight: 600; color: #0a66c2; font-size: 14px;">
Research Sources (${researchSources.length})
</div>
${researchSources.map((source, idx) => `
<div style="margin-bottom: 12px; padding: 8px; background: #f8f9fa; border-radius: 6px; border-left: 3px solid #0a66c2;">
<div style="font-weight: 600; margin-bottom: 4px;">${source.title || 'Untitled'}</div>
<div style="color: #666; margin-bottom: 4px;">${source.content || 'No description'}</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
${source.relevance_score ? `<span style="background: #eef6ff; padding: 2px 6px; border-radius: 4px; font-size: 10px;">Relevance: ${Math.round(source.relevance_score * 100)}%</span>` : ''}
${source.credibility_score ? `<span style="background: #eef6ff; padding: 2px 6px; border-radius: 4px; font-size: 10px;">Credibility: ${Math.round(source.credibility_score * 100)}%</span>` : ''}
${source.domain_authority ? `<span style="background: #eef6ff; padding: 2px 6px; border-radius: 4px; font-size: 10px;">Authority: ${Math.round(source.domain_authority * 100)}%</span>` : ''}
</div>
</div>
`).join('')}
`;
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';
(e.currentTarget as ExtendedDivElement)._researchTooltip = tooltip;
}}
onMouseLeave={(e) => {
const target = e.currentTarget as ExtendedDivElement;
if (target._researchTooltip) {
target._researchTooltip.remove();
target._researchTooltip = null;
}
}}
>
<div style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: '#10b981',
flexShrink: 0
}} />
Sources: {researchSources.length}
</div>
{/* Citations Count Chip */}
{citations && citations.length > 0 && (
<div
style={{
background: 'rgba(255, 255, 255, 0.9)',
border: '1px solid rgba(2, 119, 189, 0.3)',
borderRadius: '999px',
padding: '4px 10px',
fontSize: '11px',
fontWeight: '600',
color: '#0277bd',
cursor: 'pointer',
transition: 'all 0.2s ease',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
title={`${citations.length} citations in content. Hover to see details.`}
onMouseEnter={(e) => {
// Create and show citations tooltip
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
z-index: 100000;
background: white;
border: 1px solid #cfe9f7;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 16px;
max-width: 500px;
max-height: 400px;
overflow-y: auto;
font-size: 12px;
`;
tooltip.innerHTML = `
<div style="margin-bottom: 12px; font-weight: 600; color: #0a66c2; font-size: 14px;">
Citations (${citations.length})
</div>
${citations.map((citation, idx) => `
<div style="margin-bottom: 8px; padding: 6px; background: #f8f9fa; border-radius: 4px;">
<div style="font-weight: 600; color: #0a66c2;">Citation ${idx + 1}</div>
<div style="color: #666; font-size: 11px;">Type: ${citation.type || 'inline'}</div>
${citation.reference ? `<div style="color: #666; font-size: 11px;">Reference: ${citation.reference}</div>` : ''}
</div>
`).join('')}
`;
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';
(e.currentTarget as ExtendedDivElement)._citationsTooltip = tooltip;
}}
onMouseLeave={(e) => {
const target = e.currentTarget as ExtendedDivElement;
if (target._citationsTooltip) {
target._citationsTooltip.remove();
target._citationsTooltip = null;
}
}}
>
<div style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: '#f59e0b',
flexShrink: 0
}} />
Citations: {citations.length}
</div>
)}
{/* Search Queries Count Chip */}
{searchQueries && searchQueries.length > 0 && (
<div
style={{
background: 'rgba(255, 255, 255, 0.9)',
border: '1px solid rgba(2, 119, 189, 0.3)',
borderRadius: '999px',
padding: '4px 10px',
fontSize: '11px',
fontWeight: '600',
color: '#0277bd',
cursor: 'pointer',
transition: 'all 0.2s ease',
position: 'relative',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
title={`${searchQueries.length} search queries used for research. Hover to see details.`}
onMouseEnter={(e) => {
// Create and show search queries tooltip
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
z-index: 100000;
background: white;
border: 1px solid #cfe9f7;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 16px;
max-width: 500px;
max-height: 400px;
overflow-y: auto;
font-size: 12px;
`;
tooltip.innerHTML = `
<div style="margin-bottom: 12px; font-weight: 600; color: #0a66c2; font-size: 14px;">
Search Queries Used (${searchQueries.length})
</div>
${searchQueries.map((query, idx) => `
<div style="margin-bottom: 8px; padding: 8px; background: #f8f9fa; border-radius: 6px; border-left: 3px solid #8b5cf6;">
<div style="font-weight: 600; color: #7c3aed; margin-bottom: 4px;">Query ${idx + 1}</div>
<div style="color: #374151; font-size: 12px; line-height: 1.4;">${query}</div>
</div>
`).join('')}
`;
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';
(e.currentTarget as ExtendedDivElement)._searchQueriesTooltip = tooltip;
}}
onMouseLeave={(e) => {
const target = e.currentTarget as ExtendedDivElement;
if (target._searchQueriesTooltip) {
target._searchQueriesTooltip.remove();
target._searchQueriesTooltip = null;
}
}}
>
<div style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: '#8b5cf6',
flexShrink: 0
}} />
Queries: {searchQueries.length}
</div>
)}
</div>
)}
</div>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
{/* Quality Chips */}
{chips.length > 0 && (
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
{chips.map((c, idx) => (
<div key={idx}
title={`${c.label}: ${formatPercent(c.value)}. ${chipDescriptions[c.label] || ''}`}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
padding: '6px 10px',
borderRadius: 999,
background: 'linear-gradient(135deg, rgba(255,255,255,0.9), rgba(225,245,254,0.9))',
boxShadow: '0 6px 14px rgba(2,119,189,0.12), inset 0 0 8px rgba(2,119,189,0.08)',
border: '1px solid rgba(2,119,189,0.25)',
transform: 'translateZ(0)',
willChange: 'transform, box-shadow',
position: 'relative',
overflow: 'hidden'
}}
>
<span style={{
width: 8, height: 8, borderRadius: 999,
background: getChipColor(c.value),
boxShadow: `0 0 10px ${getChipColor(c.value)}`
}} />
<span style={{ color: '#055a8c', fontWeight: 700 }}>{formatPercent(c.value)}</span>
<span style={{ color: '#0a66c2', fontWeight: 600, opacity: 0.9 }}>{c.label}</span>
<span style={{
position: 'absolute',
inset: 0,
background: 'linear-gradient(120deg, transparent, rgba(255,255,255,0.6), transparent)',
transform: 'translateX(-100%)',
animation: 'liw-shimmer 2.2s infinite'
}} />
</div>
))}
<style>{`
@keyframes liw-shimmer { 0% { transform: translateX(-100%); } 60% { transform: translateX(100%); } 100% { transform: translateX(100%); } }
`}</style>
</div>
)}
<span style={{ fontSize: '10px', opacity: 0.8 }}>
{draft.split(/\s+/).length} words {Math.ceil(draft.split(/\s+/).length / 200)} min read
</span>
@@ -149,7 +756,7 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
<div
style={{
padding: '20px',
height: 'calc(100% - 60px)',
maxHeight: '68vh',
overflowY: 'auto',
lineHeight: '1.6',
position: 'relative'
@@ -198,14 +805,14 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
`}</style>
</div>
)}
{/* Content Display */}
<div style={{
opacity: isGenerating ? 0.3 : 1,
transition: 'opacity 0.3s ease'
}}>
{draft ? (
<div dangerouslySetInnerHTML={{ __html: formatDraftContent(draft) }} />
<div dangerouslySetInnerHTML={{ __html: formatDraftContent(draft, citations, researchSources) }} />
) : (
<p style={{
color: '#666',
@@ -216,11 +823,42 @@ export const ContentEditor: React.FC<ContentEditorProps> = ({
Content will appear here when generated. Use the AI assistant to create your LinkedIn content.
</p>
)}
{/* Citation Styling */}
<style>{`
.liw-cite {
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
border: 1px solid #64b5f6;
border-radius: 4px;
padding: 2px 6px;
margin: 0 2px;
font-size: 0.8em;
font-weight: 600;
color: #1976d2;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(25, 118, 210, 0.1);
}
.liw-cite:hover {
background: linear-gradient(135deg, #bbdefb, #90caf9);
border-color: #42a5f5;
box-shadow: 0 4px 8px rgba(25, 118, 210, 0.2);
transform: translateY(-1px);
}
.liw-cite:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(25, 118, 210, 0.1);
}
`}</style>
</div>
</div>
</div>
)}
</div>
{/* Citation Hover Handler - Now working automatically via useEffect */}
</div>
);
};

View File

@@ -0,0 +1,229 @@
import React from 'react';
import { ResearchSource, Citation, ContentQualityMetrics } from '../../../services/linkedInWriterApi';
interface GroundingDataDisplayProps {
researchSources: ResearchSource[];
citations: Citation[];
qualityMetrics?: ContentQualityMetrics;
groundingEnabled: boolean;
}
export const GroundingDataDisplay: React.FC<GroundingDataDisplayProps> = ({
researchSources,
citations,
qualityMetrics,
groundingEnabled
}) => {
if (!groundingEnabled || researchSources.length === 0) {
return null;
}
const formatScore = (score: number) => `${(score * 100).toFixed(0)}%`;
const getQualityColor = (score: number) => {
if (score >= 0.8) return '#10b981'; // Green
if (score >= 0.6) return '#f59e0b'; // Yellow
return '#ef4444'; // Red
};
return (
<div style={{
margin: '24px 0',
padding: '20px',
border: '1px solid #e5e7eb',
borderRadius: '12px',
backgroundColor: '#fff',
boxShadow: '0 4px 16px rgba(0,0,0,0.06)',
position: 'relative',
zIndex: 1,
minHeight: '120px',
fontSize: '16px'
}}>
{/* Header */}
<div style={{
display: 'flex',
alignItems: 'center',
marginBottom: '20px',
paddingBottom: '12px',
borderBottom: '2px solid #e5e7eb'
}}>
<div style={{
width: '24px',
height: '24px',
borderRadius: '50%',
backgroundColor: '#0a66c2',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '12px'
}}>
<span style={{ color: 'white', fontSize: '14px', fontWeight: 'bold' }}></span>
</div>
<h3 style={{
margin: 0,
color: '#0a66c2',
fontSize: '18px',
fontWeight: '600'
}}>
AI-Generated Content with Factual Grounding
</h3>
</div>
{/* Note: Quality chips moved to header bar; keep detail cards minimal here if needed */}
{/* Research Sources */}
<div style={{ marginBottom: '24px' }}>
<h4 style={{
margin: '0 0 16px 0',
fontSize: '16px',
fontWeight: '600',
color: '#374151'
}}>
Research Sources ({researchSources.length})
</h4>
<div style={{
display: 'grid',
gap: '12px'
}}>
{researchSources.map((source, index) => (
<div key={index} style={{
padding: '16px',
backgroundColor: 'white',
borderRadius: '8px',
border: '1px solid #e5e7eb',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
}}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '8px'
}}>
<h5 style={{
margin: '0 0 8px 0',
fontSize: '14px',
fontWeight: '600',
color: '#1f2937'
}}>
{source.title}
</h5>
<div style={{
fontSize: '12px',
color: '#6b7280',
backgroundColor: '#f3f4f6',
padding: '4px 8px',
borderRadius: '12px'
}}>
Source {index + 1}
</div>
</div>
<div style={{
fontSize: '13px',
color: '#6b7280',
marginBottom: '8px',
wordBreak: 'break-all'
}}>
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
style={{
color: '#0a66c2',
textDecoration: 'none'
}}
>
{source.url}
</a>
</div>
{/* Source Metrics */}
<div style={{
display: 'flex',
gap: '16px',
fontSize: '12px',
color: '#6b7280'
}}>
{source.relevance_score && (
<span>Relevance: {formatScore(source.relevance_score)}</span>
)}
{source.credibility_score && (
<span>Credibility: {formatScore(source.credibility_score)}</span>
)}
{source.domain_authority && (
<span>Authority: {formatScore(source.domain_authority)}</span>
)}
{source.source_type && (
<span>Type: {source.source_type.replace('_', ' ')}</span>
)}
</div>
</div>
))}
</div>
</div>
{/* Citations */}
{citations.length > 0 && (
<div>
<h4 style={{
margin: '0 0 16px 0',
fontSize: '16px',
fontWeight: '600',
color: '#374151'
}}>
Inline Citations ({citations.length})
</h4>
<div style={{
backgroundColor: 'white',
borderRadius: '8px',
border: '1px solid #e5e7eb',
padding: '16px'
}}>
<div style={{
fontSize: '13px',
color: '#6b7280',
marginBottom: '12px'
}}>
The content includes {citations.length} inline citations linking to research sources.
</div>
<div style={{
display: 'grid',
gap: '8px'
}}>
{citations.map((citation, index) => (
<div key={index} style={{
padding: '8px 12px',
backgroundColor: '#f9fafb',
borderRadius: '6px',
fontSize: '13px',
color: '#374151'
}}>
<strong>{citation.reference}</strong>
{citation.text && (
<span style={{ marginLeft: '8px', color: '#6b7280' }}>
"{citation.text.substring(0, 100)}..."
</span>
)}
</div>
))}
</div>
</div>
</div>
)}
{/* Footer */}
<div style={{
marginTop: '20px',
paddingTop: '16px',
borderTop: '1px solid #e5e7eb',
fontSize: '12px',
color: '#6b7280',
textAlign: 'center'
}}>
This content was generated using AI with real-time web research and factual grounding.
All claims are supported by current, verifiable sources.
</div>
</div>
);
};

View File

@@ -32,7 +32,7 @@ const PostHITL: React.FC<PostHITLProps> = ({ args, respond }) => {
include_hashtags: args?.include_hashtags ?? (prefs.include_hashtags ?? true),
include_call_to_action: args?.include_call_to_action ?? (prefs.include_call_to_action ?? true),
research_enabled: args?.research_enabled ?? (prefs.research_enabled ?? true),
search_engine: args?.search_engine || prefs.search_engine || 'metaphor',
search_engine: args?.search_engine || prefs.search_engine || 'google',
max_length: args?.max_length || prefs.max_length || 2000
});
const [loading, setLoading] = React.useState(false);

View File

@@ -24,6 +24,13 @@ export function useLinkedInWriter() {
const [pendingEdit, setPendingEdit] = useState<{ src: string; target: string } | null>(null);
const [loadingMessage, setLoadingMessage] = useState('');
const [currentAction, setCurrentAction] = useState<string | null>(null);
// Grounding data state
const [researchSources, setResearchSources] = useState<any[]>([]);
const [citations, setCitations] = useState<any[]>([]);
const [qualityMetrics, setQualityMetrics] = useState<any>(null);
const [groundingEnabled, setGroundingEnabled] = useState(false);
const [searchQueries, setSearchQueries] = useState<string[]>([]);
// Chat history state
const [historyVersion, setHistoryVersion] = useState<number>(0);
@@ -86,6 +93,42 @@ export function useLinkedInWriter() {
loadInitialData();
}, []);
// Listen for grounding data updates from CopilotKit actions
useEffect(() => {
const handleGroundingDataUpdate = (event: CustomEvent) => {
console.log('[LinkedIn Writer] Received grounding data event:', event.detail);
const { researchSources, citations, qualityMetrics, groundingEnabled, searchQueries } = event.detail;
console.log('[LinkedIn Writer] Extracted data:', {
researchSources: researchSources?.length || 0,
citations: citations?.length || 0,
qualityMetrics: !!qualityMetrics,
groundingEnabled,
searchQueries: searchQueries?.length || 0
});
setResearchSources(researchSources || []);
setCitations(citations || []);
setQualityMetrics(qualityMetrics || null);
setGroundingEnabled(groundingEnabled || false);
setSearchQueries(searchQueries || []);
console.log('[LinkedIn Writer] Grounding data updated:', {
sourcesCount: researchSources?.length || 0,
citationsCount: citations?.length || 0,
hasQualityMetrics: !!qualityMetrics,
groundingEnabled
});
};
window.addEventListener('linkedinwriter:updateGroundingData', handleGroundingDataUpdate as EventListener);
return () => {
window.removeEventListener('linkedinwriter:updateGroundingData', handleGroundingDataUpdate as EventListener);
};
}, []);
// Save context changes to localStorage
useEffect(() => {
if (context) {
@@ -105,6 +148,8 @@ export function useLinkedInWriter() {
setIsGenerating(false);
setLoadingMessage('');
setCurrentAction(null);
// Auto-show preview when new content is generated
setShowPreview(true);
};
const handleAppendDraft = (event: CustomEvent) => {
@@ -256,6 +301,18 @@ export function useLinkedInWriter() {
updateSuggestions,
getHistoryLength,
savePreferences,
summarizeHistory
summarizeHistory,
// Grounding data
researchSources,
citations,
qualityMetrics,
groundingEnabled,
searchQueries,
setResearchSources,
setCitations,
setQualityMetrics,
setGroundingEnabled,
setSearchQueries
};
}

View File

@@ -5,12 +5,74 @@ export function escapeHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Format draft content with proper LinkedIn styling
export function formatDraftContent(content: string): string {
// Format draft content with proper LinkedIn styling and inline citations
export function formatDraftContent(content: string, citations?: any[], researchSources?: any[]): string {
if (!content) return '';
let formatted = escapeHtml(content);
// 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();
citations.forEach((citation, index) => {
if (citation.reference && citation.reference.startsWith('Source ')) {
const sourceNum = citation.reference.replace('Source ', '');
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
const citationEntries = Array.from(citationMap.entries());
const totalCitations = citationEntries.length;
if (totalCitations > 0) {
// Split content into sentences for strategic citation placement
const sentences = formatted.split(/[.!?]+/).filter(s => s.trim().length > 0);
const sentencesWithCitations: string[] = [];
citationEntries.forEach(([reference, sourceNum], index) => {
// Distribute citations across sentences
const targetSentenceIndex = Math.floor((index / totalCitations) * sentences.length);
const targetSentence = sentences[targetSentenceIndex] || sentences[sentences.length - 1];
// Add citation to the end of the target sentence using a superscript marker
const citeHtml = ` <sup class="liw-cite" data-source-index="${sourceNum}">[${sourceNum}]</sup>`;
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
formatted = formatted.replace(/#(\w+)/g, '<span style="color: #0a66c2; font-weight: 600;">#$1</span>');

View File

@@ -0,0 +1,307 @@
/**
* Enhanced persistence utility for CopilotKit integration
* Uses localStorage and CopilotKit hooks for better state management
*/
import { useCopilotContext } from '@copilotkit/react-core';
// Storage keys for different types of data
export const STORAGE_KEYS = {
CHAT_HISTORY: 'alwrity-copilot-chat-history',
USER_PREFERENCES: 'alwrity-copilot-user-preferences',
CONVERSATION_CONTEXT: 'alwrity-copilot-conversation-context',
DRAFT_CONTENT: 'alwrity-copilot-draft-content',
LAST_SESSION: 'alwrity-copilot-last-session'
};
// Chat message interface
export interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: number;
metadata?: {
action?: string;
result?: any;
context?: string;
};
}
// User preferences interface
export interface UserPreferences {
tone: string;
industry: string;
target_audience: string;
content_goals: string[];
writing_style: string;
hashtag_preferences: boolean;
cta_preferences: boolean;
last_used_actions: string[];
favorite_topics: string[];
last_updated: number;
}
// Conversation context interface
export interface ConversationContext {
currentTopic: string;
industry: string;
tone: string;
targetAudience: string;
keyPoints: string[];
lastUpdated: number;
}
// Main persistence manager class
export class CopilotPersistenceManager {
private static instance: CopilotPersistenceManager;
private constructor() {}
public static getInstance(): CopilotPersistenceManager {
if (!CopilotPersistenceManager.instance) {
CopilotPersistenceManager.instance = new CopilotPersistenceManager();
}
return CopilotPersistenceManager.instance;
}
// Chat history persistence
public saveChatHistory(messages: ChatMessage[]): void {
try {
// Keep only last 100 messages to prevent excessive storage
const trimmedMessages = messages.slice(-100);
localStorage.setItem(STORAGE_KEYS.CHAT_HISTORY, JSON.stringify(trimmedMessages));
console.log(`💾 Saved ${trimmedMessages.length} chat messages`);
} catch (error) {
console.error('❌ Failed to save chat history:', error);
}
}
public loadChatHistory(): ChatMessage[] {
try {
const stored = localStorage.getItem(STORAGE_KEYS.CHAT_HISTORY);
if (!stored) return [];
const messages = JSON.parse(stored);
console.log(`📖 Loaded ${messages.length} chat messages`);
return messages;
} catch (error) {
console.error('❌ Failed to load chat history:', error);
return [];
}
}
public addChatMessage(message: ChatMessage): void {
try {
const existing = this.loadChatHistory();
existing.push(message);
this.saveChatHistory(existing);
} catch (error) {
console.error('❌ Failed to add chat message:', error);
}
}
// User preferences persistence
public saveUserPreferences(preferences: Partial<UserPreferences>): void {
try {
const existing = this.loadUserPreferences();
const updated = { ...existing, ...preferences, last_updated: Date.now() };
localStorage.setItem(STORAGE_KEYS.USER_PREFERENCES, JSON.stringify(updated));
console.log('💾 Saved user preferences');
} catch (error) {
console.error('❌ Failed to save user preferences:', error);
}
}
public loadUserPreferences(): UserPreferences {
try {
const stored = localStorage.getItem(STORAGE_KEYS.USER_PREFERENCES);
if (!stored) {
return {
tone: 'Professional',
industry: 'Technology',
target_audience: 'Professionals',
content_goals: ['Engagement', 'Thought Leadership'],
writing_style: 'Clear and Concise',
hashtag_preferences: true,
cta_preferences: true,
last_used_actions: [],
favorite_topics: [],
last_updated: Date.now()
};
}
const preferences = JSON.parse(stored);
console.log('📖 Loaded user preferences');
return preferences;
} catch (error) {
console.error('❌ Failed to load user preferences:', error);
// Return default preferences instead of recursive call
return {
tone: 'Professional',
industry: 'Technology',
target_audience: 'Professionals',
content_goals: ['Engagement', 'Thought Leadership'],
writing_style: 'Clear and Concise',
hashtag_preferences: true,
cta_preferences: true,
last_used_actions: [],
favorite_topics: [],
last_updated: Date.now()
};
}
}
// Conversation context persistence
public saveConversationContext(context: Partial<ConversationContext>): void {
try {
const existing = this.loadConversationContext();
const updated = { ...existing, ...context, lastUpdated: Date.now() };
localStorage.setItem(STORAGE_KEYS.CONVERSATION_CONTEXT, JSON.stringify(updated));
console.log('💾 Saved conversation context');
} catch (error) {
console.error('❌ Failed to save conversation context:', error);
}
}
public loadConversationContext(): ConversationContext {
try {
const stored = localStorage.getItem(STORAGE_KEYS.CONVERSATION_CONTEXT);
if (!stored) {
return {
currentTopic: '',
industry: 'Technology',
tone: 'Professional',
targetAudience: 'Professionals',
keyPoints: [],
lastUpdated: Date.now()
};
}
const context = JSON.parse(stored);
console.log('📖 Loaded conversation context');
return context;
} catch (error) {
console.error('❌ Failed to load conversation context:', error);
// Return default context instead of recursive call
return {
currentTopic: '',
industry: 'Technology',
tone: 'Professional',
targetAudience: 'Professionals',
keyPoints: [],
lastUpdated: Date.now()
};
}
}
// Draft content persistence
public saveDraftContent(draft: string): void {
try {
localStorage.setItem(STORAGE_KEYS.DRAFT_CONTENT, draft);
console.log('💾 Saved draft content');
} catch (error) {
console.error('❌ Failed to save draft content:', error);
}
}
public loadDraftContent(): string {
try {
const stored = localStorage.getItem(STORAGE_KEYS.DRAFT_CONTENT);
if (stored) {
console.log('📖 Loaded draft content');
return stored;
}
return '';
} catch (error) {
console.error('❌ Failed to load draft content:', error);
return '';
}
}
// Session management
public saveLastSession(): void {
try {
const sessionData = {
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
};
localStorage.setItem(STORAGE_KEYS.LAST_SESSION, JSON.stringify(sessionData));
console.log('💾 Saved session data');
} catch (error) {
console.error('❌ Failed to save session data:', error);
}
}
public loadLastSession(): any {
try {
const stored = localStorage.getItem(STORAGE_KEYS.LAST_SESSION);
if (stored) {
const session = JSON.parse(stored);
console.log('📖 Loaded session data');
return session;
}
return null;
} catch (error) {
console.error('❌ Failed to load session data:', error);
return null;
}
}
// Clear all persistence data
public clearAllData(): void {
try {
Object.values(STORAGE_KEYS).forEach(key => {
localStorage.removeItem(key);
});
console.log('🗑️ Cleared all persistence data');
} catch (error) {
console.error('❌ Failed to clear persistence data:', error);
}
}
// Get storage statistics
public getStorageStats(): any {
try {
const stats = {
chatHistory: this.loadChatHistory().length,
hasUserPreferences: !!localStorage.getItem(STORAGE_KEYS.USER_PREFERENCES),
hasConversationContext: !!localStorage.getItem(STORAGE_KEYS.CONVERSATION_CONTEXT),
hasDraftContent: !!localStorage.getItem(STORAGE_KEYS.DRAFT_CONTENT),
hasLastSession: !!localStorage.getItem(STORAGE_KEYS.LAST_SESSION),
totalKeys: Object.keys(localStorage).filter(key => key.includes('alwrity-copilot')).length
};
console.log('📊 Storage statistics:', stats);
return stats;
} catch (error) {
console.error('❌ Failed to get storage stats:', error);
return {};
}
}
}
// Hook for using persistence in React components
export const useCopilotPersistence = () => {
const copilotContext = useCopilotContext();
const persistenceManager = CopilotPersistenceManager.getInstance();
return {
persistenceManager,
copilotContext,
// Convenience methods
saveChatHistory: persistenceManager.saveChatHistory.bind(persistenceManager),
loadChatHistory: persistenceManager.loadChatHistory.bind(persistenceManager),
addChatMessage: persistenceManager.addChatMessage.bind(persistenceManager),
saveUserPreferences: persistenceManager.saveUserPreferences.bind(persistenceManager),
loadUserPreferences: persistenceManager.loadUserPreferences.bind(persistenceManager),
saveConversationContext: persistenceManager.saveConversationContext.bind(persistenceManager),
loadConversationContext: persistenceManager.loadConversationContext.bind(persistenceManager),
saveDraftContent: persistenceManager.saveDraftContent.bind(persistenceManager),
loadDraftContent: persistenceManager.loadDraftContent.bind(persistenceManager),
saveLastSession: persistenceManager.saveLastSession.bind(persistenceManager),
loadLastSession: persistenceManager.loadLastSession.bind(persistenceManager),
clearAllData: persistenceManager.clearAllData.bind(persistenceManager),
getStorageStats: persistenceManager.getStorageStats.bind(persistenceManager)
};
};

View File

@@ -23,7 +23,6 @@ export const VALID_TONES = [
] as const;
export const VALID_SEARCH_ENGINES = [
'metaphor',
'google',
'tavily'
] as const;
@@ -158,8 +157,12 @@ export function mapIndustry(industry: string | undefined): string {
}
export function mapSearchEngine(engine: string | undefined): SearchEngine {
// Force Google for now until METAPHOR issue is resolved
return SearchEngine.GOOGLE;
/* Original logic - commented out temporarily
const eng = normalizeEnum(engine);
if (!eng) return SearchEngine.METAPHOR;
if (!eng) return SearchEngine.GOOGLE;
const exact = VALID_SEARCH_ENGINES.find(v => v.toLowerCase() === eng);
if (exact) return exact as SearchEngine;
@@ -167,7 +170,8 @@ export function mapSearchEngine(engine: string | undefined): SearchEngine {
if (eng.includes('google')) return SearchEngine.GOOGLE;
if (eng.includes('tavily')) return SearchEngine.TAVILY;
return SearchEngine.METAPHOR;
return SearchEngine.GOOGLE;
*/
}
export function mapResponseType(responseType: string | undefined): string {

View File

@@ -0,0 +1,88 @@
/**
* Utility to test and debug CopilotKit persistence
*/
export const testPersistence = () => {
console.log('🧪 Testing CopilotKit persistence...');
// Check localStorage for persisted data
const chatData = localStorage.getItem('alwrity-copilot-chat');
const prefsData = localStorage.getItem('alwrity-copilot-preferences');
const contextData = localStorage.getItem('alwrity-copilot-context');
console.log('📊 Persistence Test Results:', {
chat: {
exists: !!chatData,
length: chatData ? JSON.parse(chatData).length : 0,
sample: chatData ? JSON.parse(chatData).slice(0, 2) : null
},
preferences: {
exists: !!prefsData,
data: prefsData ? JSON.parse(prefsData) : null
},
context: {
exists: !!contextData,
data: contextData ? JSON.parse(contextData) : null
}
});
// Check for any other CopilotKit related data
const allKeys = Object.keys(localStorage);
const copilotKeys = allKeys.filter(key => key.includes('copilot') || key.includes('alwrity'));
console.log('🔍 All CopilotKit related localStorage keys:', copilotKeys);
return {
chat: !!chatData,
preferences: !!prefsData,
context: !!contextData,
allCopilotKeys: copilotKeys
};
};
export const clearPersistence = () => {
console.log('🗑️ Clearing CopilotKit persistence...');
localStorage.removeItem('alwrity-copilot-chat');
localStorage.removeItem('alwrity-copilot-preferences');
localStorage.removeItem('alwrity-copilot-context');
// Clear any other CopilotKit related data
const allKeys = Object.keys(localStorage);
const copilotKeys = allKeys.filter(key => key.includes('copilot') || key.includes('alwrity'));
copilotKeys.forEach(key => {
localStorage.removeItem(key);
console.log(`🗑️ Removed: ${key}`);
});
console.log('✅ Persistence cleared');
};
export const simulateChatMessage = () => {
console.log('💬 Simulating chat message for persistence test...');
const testMessage = {
role: 'user',
content: 'This is a test message to verify persistence',
timestamp: Date.now(),
id: `test-${Date.now()}`
};
// Try to store in the expected format
try {
const existingChat = localStorage.getItem('alwrity-copilot-chat');
const chatArray = existingChat ? JSON.parse(existingChat) : [];
chatArray.push(testMessage);
// Keep only last 10 messages for testing
const trimmedChat = chatArray.slice(-10);
localStorage.setItem('alwrity-copilot-chat', JSON.stringify(trimmedChat));
console.log('✅ Test message stored:', testMessage);
return true;
} catch (error) {
console.error('❌ Failed to store test message:', error);
return false;
}
};