ALwrity LinkedIn Writer: Brainstorm Flow, Copilot Actions, Feature Carousel, Info Modals, Welcome Message
This commit is contained in:
@@ -1,14 +1,5 @@
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
|
||||
// Extend HTMLDivElement interface for custom tooltip properties
|
||||
interface ExtendedDivElement extends HTMLDivElement {
|
||||
_researchTooltip?: HTMLDivElement | null;
|
||||
_citationsTooltip?: HTMLDivElement | null;
|
||||
_searchQueriesTooltip?: HTMLDivElement | null;
|
||||
_qualityTooltip?: HTMLDivElement | null;
|
||||
_researchTooltipTimeout?: NodeJS.Timeout | null;
|
||||
_qualityTooltipTimeout?: NodeJS.Timeout | null;
|
||||
}
|
||||
import React from 'react';
|
||||
import { MainContentPreviewHeader, ContentPreviewHeaderWithModals } from './ContentPreviewHeaderComponents/index';
|
||||
|
||||
interface ContentPreviewHeaderProps {
|
||||
researchSources?: any[];
|
||||
@@ -23,828 +14,11 @@ interface ContentPreviewHeaderProps {
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
const ContentPreviewHeader: React.FC<ContentPreviewHeaderProps> = ({
|
||||
researchSources,
|
||||
citations,
|
||||
searchQueries,
|
||||
qualityMetrics,
|
||||
draft,
|
||||
showPreview,
|
||||
onPreviewToggle,
|
||||
assistantOn,
|
||||
onAssistantToggle,
|
||||
topic
|
||||
}) => {
|
||||
const formatPercent = (v?: number) => typeof v === 'number' ? `${Math.round(v * 100)}%` : '—';
|
||||
const getChipColor = (v?: number) => {
|
||||
if (typeof v !== 'number') return '#6b7280';
|
||||
if (v >= 0.8) return '#10b981';
|
||||
if (v >= 0.6) return '#f59e0b';
|
||||
return '#ef4444';
|
||||
};
|
||||
|
||||
// Memoize chips array to prevent infinite re-rendering
|
||||
const chips = useMemo(() => {
|
||||
const chipArray = qualityMetrics ? [
|
||||
{ label: 'Overall', value: qualityMetrics.overall_score },
|
||||
{ label: 'Accuracy', value: qualityMetrics.factual_accuracy },
|
||||
{ label: 'Verification', value: qualityMetrics.source_verification },
|
||||
{ label: 'Coverage', value: qualityMetrics.citation_coverage }
|
||||
] : [];
|
||||
|
||||
console.log('🔍 [ContentPreviewHeader] Chips array created:', {
|
||||
qualityMetrics: qualityMetrics,
|
||||
chips: chipArray,
|
||||
chipsLength: chipArray.length
|
||||
});
|
||||
|
||||
return chipArray;
|
||||
}, [qualityMetrics]);
|
||||
|
||||
// Helper to build descriptive chip tooltip text
|
||||
const chipDescriptions: Record<string, string> = {
|
||||
Overall: 'Overall blends accuracy, verification and coverage into a single reliability score for this draft.',
|
||||
Accuracy: 'Factual Accuracy estimates how likely statements are to be factually correct based on grounding signals.',
|
||||
Verification: 'Source Verification reflects how well claims are linked to credible sources and whether citations match claims.',
|
||||
Coverage: 'Citation Coverage indicates how much of the content is supported with citations. Higher is better.'
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
background: '#e1f5fe',
|
||||
borderBottom: '1px solid #b3e5fc',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0277bd',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<span>{topic ? `${topic} - LinkedIn Content Preview` : 'LinkedIn Content Preview'}</span>
|
||||
|
||||
{/* Research Chip with Hover Sub-chips */}
|
||||
{((researchSources && researchSources.length > 0) || (citations && citations.length > 0) || (searchQueries && searchQueries.length > 0)) && (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{/* Main Research Chip */}
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)',
|
||||
border: '1px solid #0284c7',
|
||||
borderRadius: '999px',
|
||||
padding: '6px 14px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
boxShadow: '0 2px 8px rgba(14, 165, 233, 0.3)',
|
||||
transform: 'translateZ(0)',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
title="Research data available. Hover to see sources, citations, and queries."
|
||||
onMouseEnter={(e) => {
|
||||
// Clear any existing timeout
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._researchTooltipTimeout) {
|
||||
clearTimeout(target._researchTooltipTimeout);
|
||||
target._researchTooltipTimeout = null;
|
||||
}
|
||||
|
||||
// Create and show research sub-chips tooltip
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 100000;
|
||||
background: white;
|
||||
border: 1px solid #cfe9f7;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
|
||||
padding: 16px;
|
||||
max-width: 400px;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: auto;
|
||||
`;
|
||||
|
||||
let subChipsHtml = '<div style="margin-bottom: 12px; font-weight: 600; color: #0a66c2; font-size: 14px;">Research Data</div>';
|
||||
|
||||
// Add Sources sub-chip
|
||||
if (researchSources && researchSources.length > 0) {
|
||||
subChipsHtml += `
|
||||
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
|
||||
onmouseenter="this.style.background='#e0f2fe'; this.style.transform='scale(1.05)'"
|
||||
onmouseleave="this.style.background='#f0f9ff'; this.style.transform='scale(1)'"
|
||||
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showResearchSourcesModal', { detail: 'sources' }))">
|
||||
<span style="display: inline-block; width: 6px; height: 6px; background: #10b981; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);"></span>
|
||||
Sources: ${researchSources.length}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Add Citations sub-chip
|
||||
if (citations && citations.length > 0) {
|
||||
subChipsHtml += `
|
||||
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
|
||||
onmouseenter="this.style.background='#fde68a'; this.style.transform='scale(1.05)'"
|
||||
onmouseleave="this.style.background='#fef3c7'; this.style.transform='scale(1)'"
|
||||
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showCitationsModal', { detail: 'citations' }))">
|
||||
<span style="display: inline-block; width: 6px; height: 6px; background: #f59e0b; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(245, 158, 11, 0.5);"></span>
|
||||
Citations: ${citations.length}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Add Queries sub-chip
|
||||
if (searchQueries && searchQueries.length > 0) {
|
||||
subChipsHtml += `
|
||||
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #f3e8ff; border: 1px solid #8b5cf6; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
|
||||
onmouseenter="this.style.background='#e9d5ff'; this.style.transform='scale(1.05)'"
|
||||
onmouseleave="this.style.background='#f3e8ff'; this.style.transform='scale(1)'"
|
||||
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showSearchQueriesModal', { detail: 'queries' }))">
|
||||
<span style="display: inline-block; width: 6px; height: 6px; background: #8b5cf6; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(139, 92, 246, 0.5);"></span>
|
||||
Queries: ${searchQueries.length}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
tooltip.innerHTML = subChipsHtml;
|
||||
|
||||
// Add mouse events to tooltip to keep it visible
|
||||
tooltip.addEventListener('mouseenter', () => {
|
||||
if (target._researchTooltipTimeout) {
|
||||
clearTimeout(target._researchTooltipTimeout);
|
||||
target._researchTooltipTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
tooltip.addEventListener('mouseleave', () => {
|
||||
target._researchTooltipTimeout = setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.style.opacity = '0';
|
||||
tooltip.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.remove();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
target._researchTooltip = null;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
tooltip.style.left = Math.min(rect.left, window.innerWidth - 420) + 'px';
|
||||
tooltip.style.top = (rect.bottom + 8) + 'px';
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
tooltip.style.opacity = '1';
|
||||
tooltip.style.transform = 'translateY(0)';
|
||||
}, 10);
|
||||
|
||||
target._researchTooltip = tooltip;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._researchTooltip) {
|
||||
// Add delay before hiding to allow moving to tooltip
|
||||
target._researchTooltipTimeout = setTimeout(() => {
|
||||
const tooltip = target._researchTooltip;
|
||||
if (tooltip && tooltip.parentNode) {
|
||||
tooltip.style.opacity = '0';
|
||||
tooltip.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.remove();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
target._researchTooltip = null;
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
// Keep tooltip visible when moving to sub-chips
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._researchTooltip) {
|
||||
const tooltip = target._researchTooltip;
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
tooltip.style.left = Math.min(rect.left, window.innerWidth - 420) + 'px';
|
||||
tooltip.style.top = (rect.bottom + 8) + 'px';
|
||||
}
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
// Add hover effect to the chip itself
|
||||
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 16px rgba(14, 165, 233, 0.4)';
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
// Remove hover effect
|
||||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.3)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
flexShrink: 0,
|
||||
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
|
||||
}} />
|
||||
Research
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
{/* Quality Metrics Chip */}
|
||||
{chips.length > 0 && (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{/* Main Quality Metrics Chip */}
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
border: '1px solid #047857',
|
||||
borderRadius: '999px',
|
||||
padding: '6px 14px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
|
||||
transform: 'translateZ(0)',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
title="Quality metrics available. Hover to see detailed progress bars and explanations."
|
||||
onMouseEnter={(e) => {
|
||||
// Clear any existing timeout
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._qualityTooltipTimeout) {
|
||||
clearTimeout(target._qualityTooltipTimeout);
|
||||
target._qualityTooltipTimeout = null;
|
||||
}
|
||||
|
||||
// Create and show quality metrics tooltip with circular progress bars
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 100000;
|
||||
background: white;
|
||||
border: 1px solid #d1fae5;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: auto;
|
||||
`;
|
||||
|
||||
// Create circular progress bars for each metric
|
||||
const createCircularProgress = (label: string, value: number, description: string) => {
|
||||
const percentage = Math.round(value * 100);
|
||||
const color = getChipColor(value);
|
||||
const circumference = 2 * Math.PI * 45; // radius = 45
|
||||
const strokeDasharray = circumference;
|
||||
const strokeDashoffset = circumference - (percentage / 100) * circumference;
|
||||
|
||||
return `
|
||||
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding: 12px; background: #f8fafc; border-radius: 12px; border-left: 4px solid ${color};">
|
||||
<div style="position: relative; width: 60px; height: 60px;">
|
||||
<svg width="60" height="60" style="transform: rotate(-90deg);">
|
||||
<circle cx="30" cy="30" r="45" stroke="#e5e7eb" stroke-width="6" fill="none"/>
|
||||
<circle cx="30" cy="30" r="45" stroke="${color}" stroke-width="6" fill="none"
|
||||
stroke-dasharray="${strokeDasharray}" stroke-dashoffset="${strokeDashoffset}"
|
||||
style="transition: stroke-dashoffset 0.5s ease;"/>
|
||||
</svg>
|
||||
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-weight: 700; font-size: 14px; color: ${color};">
|
||||
${percentage}%
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 700; color: #1f2937; margin-bottom: 4px; font-size: 14px;">${label}</div>
|
||||
<div style="color: #6b7280; line-height: 1.4; font-size: 11px;">${description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
let progressBarsHtml = '<div style="margin-bottom: 16px; font-weight: 700; color: #059669; font-size: 16px; text-align: center;">Quality Metrics</div>';
|
||||
|
||||
chips.forEach(chip => {
|
||||
progressBarsHtml += createCircularProgress(
|
||||
chip.label,
|
||||
chip.value || 0,
|
||||
chipDescriptions[chip.label] || ''
|
||||
);
|
||||
});
|
||||
|
||||
tooltip.innerHTML = progressBarsHtml;
|
||||
|
||||
// Add mouse events to tooltip to keep it visible
|
||||
tooltip.addEventListener('mouseenter', () => {
|
||||
if (target._qualityTooltipTimeout) {
|
||||
clearTimeout(target._qualityTooltipTimeout);
|
||||
target._qualityTooltipTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
tooltip.addEventListener('mouseleave', () => {
|
||||
target._qualityTooltipTimeout = setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.style.opacity = '0';
|
||||
tooltip.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.remove();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
target._qualityTooltip = null;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
tooltip.style.left = Math.min(rect.left, window.innerWidth - 520) + 'px';
|
||||
tooltip.style.top = (rect.bottom + 8) + 'px';
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
tooltip.style.opacity = '1';
|
||||
tooltip.style.transform = 'translateY(0)';
|
||||
}, 10);
|
||||
|
||||
target._qualityTooltip = tooltip;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._qualityTooltip) {
|
||||
// Add delay before hiding to allow moving to tooltip
|
||||
target._qualityTooltipTimeout = setTimeout(() => {
|
||||
const tooltip = target._qualityTooltip;
|
||||
if (tooltip && tooltip.parentNode) {
|
||||
tooltip.style.opacity = '0';
|
||||
tooltip.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.remove();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
target._qualityTooltip = null;
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
// Keep tooltip visible when moving to progress bars
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._qualityTooltip) {
|
||||
const tooltip = target._qualityTooltip;
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
tooltip.style.left = Math.min(rect.left, window.innerWidth - 520) + 'px';
|
||||
tooltip.style.top = (rect.bottom + 8) + 'px';
|
||||
}
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
// Add hover effect to the chip itself
|
||||
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 16px rgba(16, 185, 129, 0.4)';
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
// Remove hover effect
|
||||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.3)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
flexShrink: 0,
|
||||
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
|
||||
}} />
|
||||
Quality Metrics
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span style={{ fontSize: '10px', opacity: 0.8 }}>
|
||||
{draft.split(/\s+/).length} words • {Math.ceil(draft.split(/\s+/).length / 200)} min read
|
||||
</span>
|
||||
{/* Assistive Writing toggle */}
|
||||
{onAssistantToggle && (
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#666', cursor: 'pointer' }}
|
||||
title="Assistive Writing: Get real-time AI-powered writing suggestions as you type. Uses Exa.ai for web research and Gemini for intelligent content generation. Automatically enables editing mode to allow typing and content modification."
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assistantOn || false}
|
||||
onChange={(e) => onAssistantToggle(e.target.checked)}
|
||||
/>
|
||||
Assistive Writing
|
||||
</label>
|
||||
)}
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#666', cursor: 'pointer' }}
|
||||
title="Toggle preview visibility"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!showPreview}
|
||||
onChange={() => onPreviewToggle()}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
Hide Preview
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Research Sources Modal Component
|
||||
const ResearchSourcesModal: React.FC<{ sources: any[]; isOpen: boolean; onClose: () => void }> = ({ sources, isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000000
|
||||
}} onClick={onClose}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
maxWidth: '600px',
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
|
||||
Research Sources ({sources.length})
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: '#666',
|
||||
padding: '0',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
{sources && Array.isArray(sources) ? sources.map((source, idx) => (
|
||||
<div key={idx} style={{
|
||||
marginBottom: '16px',
|
||||
padding: '16px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
borderLeft: '4px solid #0a66c2'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', marginBottom: '8px', color: '#0a66c2' }}>
|
||||
{source.title || 'Untitled Source'}
|
||||
</div>
|
||||
<div style={{ color: '#666', marginBottom: '12px', lineHeight: '1.5' }}>
|
||||
{source.content || 'No description available'}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
{source.relevance_score && (
|
||||
<span style={{
|
||||
backgroundColor: '#eef6ff',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: '#0a66c2'
|
||||
}}>
|
||||
Relevance: {Math.round(source.relevance_score * 100)}%
|
||||
</span>
|
||||
)}
|
||||
{source.credibility_score && (
|
||||
<span style={{
|
||||
backgroundColor: '#eef6ff',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: '#0a66c2'
|
||||
}}>
|
||||
Credibility: {Math.round(source.credibility_score * 100)}%
|
||||
</span>
|
||||
)}
|
||||
{source.domain_authority && (
|
||||
<span style={{
|
||||
backgroundColor: '#eef6ff',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: '#0a66c2'
|
||||
}}>
|
||||
Authority: {Math.round(source.domain_authority * 100)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
No research sources available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Citations Modal Component
|
||||
const CitationsModal: React.FC<{ citations: any[]; isOpen: boolean; onClose: () => void }> = ({ citations, isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000000
|
||||
}} onClick={onClose}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
maxWidth: '500px',
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
|
||||
Citations ({citations.length})
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: '#666',
|
||||
padding: '0',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
{citations && Array.isArray(citations) ? citations.map((citation, idx) => (
|
||||
<div key={idx} style={{
|
||||
marginBottom: '12px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '6px',
|
||||
borderLeft: '3px solid #f59e0b'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#0a66c2', marginBottom: '4px' }}>
|
||||
Citation {idx + 1}
|
||||
</div>
|
||||
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>
|
||||
Type: {citation.type || 'inline'}
|
||||
</div>
|
||||
{citation.reference && (
|
||||
<div style={{ color: '#666', fontSize: '12px' }}>
|
||||
Reference: {citation.reference}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
No citations available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Search Queries Modal Component
|
||||
const SearchQueriesModal: React.FC<{ queries: string[]; isOpen: boolean; onClose: () => void }> = ({ queries, isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000000
|
||||
}} onClick={onClose}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
maxWidth: '500px',
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
|
||||
Search Queries Used ({queries.length})
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: '#666',
|
||||
padding: '0',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
{queries && Array.isArray(queries) ? queries.map((query, idx) => (
|
||||
<div key={idx} style={{
|
||||
marginBottom: '12px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '6px',
|
||||
borderLeft: '3px solid #8b5cf6'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#7c3aed', marginBottom: '6px' }}>
|
||||
Query {idx + 1}
|
||||
</div>
|
||||
<div style={{ color: '#374151', fontSize: '13px', lineHeight: '1.4' }}>
|
||||
{query}
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
No search queries available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Enhanced ContentPreviewHeader with Modal State
|
||||
const ContentPreviewHeaderWithModals: React.FC<ContentPreviewHeaderProps> = (props) => {
|
||||
const [showResearchSourcesModal, setShowResearchSourcesModal] = useState(false);
|
||||
const [showCitationsModal, setShowCitationsModal] = useState(false);
|
||||
const [showSearchQueriesModal, setShowSearchQueriesModal] = useState(false);
|
||||
const [modalData, setModalData] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleShowResearchSourcesModal = (event: CustomEvent) => {
|
||||
try {
|
||||
const dataType = event.detail;
|
||||
let data: any[] = [];
|
||||
|
||||
if (dataType === 'sources') {
|
||||
data = props.researchSources || [];
|
||||
}
|
||||
|
||||
setModalData(Array.isArray(data) ? data : []);
|
||||
setShowResearchSourcesModal(true);
|
||||
} catch (error) {
|
||||
console.error('Error handling research sources modal:', error);
|
||||
setModalData([]);
|
||||
setShowResearchSourcesModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowCitationsModal = (event: CustomEvent) => {
|
||||
try {
|
||||
const dataType = event.detail;
|
||||
let data: any[] = [];
|
||||
|
||||
if (dataType === 'citations') {
|
||||
data = props.citations || [];
|
||||
}
|
||||
|
||||
setModalData(Array.isArray(data) ? data : []);
|
||||
setShowCitationsModal(true);
|
||||
} catch (error) {
|
||||
console.error('Error handling citations modal:', error);
|
||||
setModalData([]);
|
||||
setShowCitationsModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowSearchQueriesModal = (event: CustomEvent) => {
|
||||
try {
|
||||
const dataType = event.detail;
|
||||
let data: any[] = [];
|
||||
|
||||
if (dataType === 'queries') {
|
||||
data = props.searchQueries || [];
|
||||
}
|
||||
|
||||
setModalData(Array.isArray(data) ? data : []);
|
||||
setShowSearchQueriesModal(true);
|
||||
} catch (error) {
|
||||
console.error('Error handling search queries modal:', error);
|
||||
setModalData([]);
|
||||
setShowSearchQueriesModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('showResearchSourcesModal', handleShowResearchSourcesModal as EventListener);
|
||||
window.addEventListener('showCitationsModal', handleShowCitationsModal as EventListener);
|
||||
window.addEventListener('showSearchQueriesModal', handleShowSearchQueriesModal as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('showResearchSourcesModal', handleShowResearchSourcesModal as EventListener);
|
||||
window.removeEventListener('showCitationsModal', handleShowCitationsModal as EventListener);
|
||||
window.removeEventListener('showSearchQueriesModal', handleShowSearchQueriesModal as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentPreviewHeader {...props} />
|
||||
<ResearchSourcesModal
|
||||
sources={modalData || []}
|
||||
isOpen={showResearchSourcesModal}
|
||||
onClose={() => setShowResearchSourcesModal(false)}
|
||||
/>
|
||||
<CitationsModal
|
||||
citations={modalData || []}
|
||||
isOpen={showCitationsModal}
|
||||
onClose={() => setShowCitationsModal(false)}
|
||||
/>
|
||||
<SearchQueriesModal
|
||||
queries={modalData || []}
|
||||
isOpen={showSearchQueriesModal}
|
||||
onClose={() => setShowSearchQueriesModal(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
// Main ContentPreviewHeader component - now just a wrapper that uses the extracted component
|
||||
const ContentPreviewHeader: React.FC<ContentPreviewHeaderProps> = (props) => {
|
||||
return <MainContentPreviewHeader {...props} />;
|
||||
};
|
||||
|
||||
// Export both the main component and the enhanced version with modals
|
||||
export default ContentPreviewHeader;
|
||||
export { ContentPreviewHeader, ContentPreviewHeaderWithModals };
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import MainContentPreviewHeader from './MainContentPreviewHeader';
|
||||
|
||||
interface ContentPreviewHeaderProps {
|
||||
researchSources?: any[];
|
||||
citations?: any[];
|
||||
searchQueries?: string[];
|
||||
qualityMetrics?: any;
|
||||
draft: string;
|
||||
showPreview: boolean;
|
||||
onPreviewToggle: () => void;
|
||||
assistantOn?: boolean;
|
||||
onAssistantToggle?: (enabled: boolean) => void;
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
// Research Sources Modal Component
|
||||
const ResearchSourcesModal: React.FC<{ sources: any[]; isOpen: boolean; onClose: () => void }> = ({ sources, isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000000
|
||||
}} onClick={onClose}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
maxWidth: '600px',
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
|
||||
Research Sources ({sources.length})
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: '#666',
|
||||
padding: '0',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
{sources && Array.isArray(sources) ? sources.map((source, idx) => (
|
||||
<div key={idx} style={{
|
||||
marginBottom: '16px',
|
||||
padding: '16px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
borderLeft: '4px solid #0a66c2'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', marginBottom: '8px', color: '#0a66c2' }}>
|
||||
{source.title || 'Untitled Source'}
|
||||
</div>
|
||||
<div style={{ color: '#666', marginBottom: '12px', lineHeight: '1.5' }}>
|
||||
{source.content || 'No description available'}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
{source.relevance_score && (
|
||||
<span style={{
|
||||
backgroundColor: '#eef6ff',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: '#0a66c2'
|
||||
}}>
|
||||
Relevance: {Math.round(source.relevance_score * 100)}%
|
||||
</span>
|
||||
)}
|
||||
{source.credibility_score && (
|
||||
<span style={{
|
||||
backgroundColor: '#eef6ff',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: '#0a66c2'
|
||||
}}>
|
||||
Credibility: {Math.round(source.credibility_score * 100)}%
|
||||
</span>
|
||||
)}
|
||||
{source.domain_authority && (
|
||||
<span style={{
|
||||
backgroundColor: '#eef6ff',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: '#0a66c2'
|
||||
}}>
|
||||
Authority: {Math.round(source.domain_authority * 100)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
No research sources available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Citations Modal Component
|
||||
const CitationsModal: React.FC<{ citations: any[]; isOpen: boolean; onClose: () => void }> = ({ citations, isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000000
|
||||
}} onClick={onClose}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
maxWidth: '500px',
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
|
||||
Citations ({citations.length})
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: '#666',
|
||||
padding: '0',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
{citations && Array.isArray(citations) ? citations.map((citation, idx) => (
|
||||
<div key={idx} style={{
|
||||
marginBottom: '12px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '6px',
|
||||
borderLeft: '3px solid #f59e0b'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#0a66c2', marginBottom: '4px' }}>
|
||||
Citation {idx + 1}
|
||||
</div>
|
||||
<div style={{ color: '#666', fontSize: '12px', marginBottom: '4px' }}>
|
||||
Type: {citation.type || 'inline'}
|
||||
</div>
|
||||
{citation.reference && (
|
||||
<div style={{ color: '#666', fontSize: '12px' }}>
|
||||
Reference: {citation.reference}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
No citations available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Search Queries Modal Component
|
||||
const SearchQueriesModal: React.FC<{ queries: string[]; isOpen: boolean; onClose: () => void }> = ({ queries, isOpen, onClose }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000000
|
||||
}} onClick={onClose}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
maxWidth: '500px',
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: 0, color: '#0a66c2', fontSize: '18px', fontWeight: '600' }}>
|
||||
Search Queries Used ({queries.length})
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: '#666',
|
||||
padding: '0',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
{queries && Array.isArray(queries) ? queries.map((query, idx) => (
|
||||
<div key={idx} style={{
|
||||
marginBottom: '12px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '6px',
|
||||
borderLeft: '3px solid #8b5cf6'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', color: '#7c3aed', marginBottom: '6px' }}>
|
||||
Query {idx + 1}
|
||||
</div>
|
||||
<div style={{ color: '#374151', fontSize: '13px', lineHeight: '1.4' }}>
|
||||
{query}
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
No search queries available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Enhanced ContentPreviewHeader with Modal State
|
||||
const ContentPreviewHeaderWithModals: React.FC<ContentPreviewHeaderProps> = (props) => {
|
||||
const [showResearchSourcesModal, setShowResearchSourcesModal] = useState(false);
|
||||
const [showCitationsModal, setShowCitationsModal] = useState(false);
|
||||
const [showSearchQueriesModal, setShowSearchQueriesModal] = useState(false);
|
||||
const [modalData, setModalData] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleShowResearchSourcesModal = (event: CustomEvent) => {
|
||||
try {
|
||||
const dataType = event.detail;
|
||||
let data: any[] = [];
|
||||
|
||||
if (dataType === 'sources') {
|
||||
data = props.researchSources || [];
|
||||
}
|
||||
|
||||
setModalData(Array.isArray(data) ? data : []);
|
||||
setShowResearchSourcesModal(true);
|
||||
} catch (error) {
|
||||
console.error('Error handling research sources modal:', error);
|
||||
setModalData([]);
|
||||
setShowResearchSourcesModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowCitationsModal = (event: CustomEvent) => {
|
||||
try {
|
||||
const dataType = event.detail;
|
||||
let data: any[] = [];
|
||||
|
||||
if (dataType === 'citations') {
|
||||
data = props.citations || [];
|
||||
}
|
||||
|
||||
setModalData(Array.isArray(data) ? data : []);
|
||||
setShowCitationsModal(true);
|
||||
} catch (error) {
|
||||
console.error('Error handling citations modal:', error);
|
||||
setModalData([]);
|
||||
setShowCitationsModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowSearchQueriesModal = (event: CustomEvent) => {
|
||||
try {
|
||||
const dataType = event.detail;
|
||||
let data: any[] = [];
|
||||
|
||||
if (dataType === 'queries') {
|
||||
data = props.searchQueries || [];
|
||||
}
|
||||
|
||||
setModalData(Array.isArray(data) ? data : []);
|
||||
setShowSearchQueriesModal(true);
|
||||
} catch (error) {
|
||||
console.error('Error handling search queries modal:', error);
|
||||
setModalData([]);
|
||||
setShowSearchQueriesModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('showResearchSourcesModal', handleShowResearchSourcesModal as EventListener);
|
||||
window.addEventListener('showCitationsModal', handleShowCitationsModal as EventListener);
|
||||
window.addEventListener('showSearchQueriesModal', handleShowSearchQueriesModal as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('showResearchSourcesModal', handleShowResearchSourcesModal as EventListener);
|
||||
window.removeEventListener('showCitationsModal', handleShowCitationsModal as EventListener);
|
||||
window.removeEventListener('showSearchQueriesModal', handleShowSearchQueriesModal as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainContentPreviewHeader {...props} />
|
||||
<ResearchSourcesModal
|
||||
sources={modalData || []}
|
||||
isOpen={showResearchSourcesModal}
|
||||
onClose={() => setShowResearchSourcesModal(false)}
|
||||
/>
|
||||
<CitationsModal
|
||||
citations={modalData || []}
|
||||
isOpen={showCitationsModal}
|
||||
onClose={() => setShowCitationsModal(false)}
|
||||
/>
|
||||
<SearchQueriesModal
|
||||
queries={modalData || []}
|
||||
isOpen={showSearchQueriesModal}
|
||||
onClose={() => setShowSearchQueriesModal(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentPreviewHeaderWithModals;
|
||||
@@ -0,0 +1,494 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PersonaChip from './PersonaChip';
|
||||
|
||||
// Extend HTMLDivElement interface for custom tooltip properties
|
||||
interface ExtendedDivElement extends HTMLDivElement {
|
||||
_researchTooltip?: HTMLDivElement | null;
|
||||
_citationsTooltip?: HTMLDivElement | null;
|
||||
_searchQueriesTooltip?: HTMLDivElement | null;
|
||||
_qualityTooltip?: HTMLDivElement | null;
|
||||
_researchTooltipTimeout?: NodeJS.Timeout | null;
|
||||
_qualityTooltipTimeout?: NodeJS.Timeout | null;
|
||||
}
|
||||
|
||||
interface MainContentPreviewHeaderProps {
|
||||
researchSources?: any[];
|
||||
citations?: any[];
|
||||
searchQueries?: string[];
|
||||
qualityMetrics?: any;
|
||||
draft: string;
|
||||
showPreview: boolean;
|
||||
onPreviewToggle: () => void;
|
||||
assistantOn?: boolean;
|
||||
onAssistantToggle?: (enabled: boolean) => void;
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
const MainContentPreviewHeader: React.FC<MainContentPreviewHeaderProps> = ({
|
||||
researchSources,
|
||||
citations,
|
||||
searchQueries,
|
||||
qualityMetrics,
|
||||
draft,
|
||||
showPreview,
|
||||
onPreviewToggle,
|
||||
assistantOn,
|
||||
onAssistantToggle,
|
||||
topic
|
||||
}) => {
|
||||
const formatPercent = (v?: number) => typeof v === 'number' ? `${Math.round(v * 100)}%` : '—';
|
||||
const getChipColor = (v?: number) => {
|
||||
if (typeof v !== 'number') return '#6b7280';
|
||||
if (v >= 0.8) return '#10b981';
|
||||
if (v >= 0.6) return '#f59e0b';
|
||||
return '#ef4444';
|
||||
};
|
||||
|
||||
// Memoize chips array to prevent infinite re-rendering
|
||||
const chips = useMemo(() => {
|
||||
const chipArray = qualityMetrics ? [
|
||||
{ label: 'Overall', value: qualityMetrics.overall_score },
|
||||
{ label: 'Accuracy', value: qualityMetrics.factual_accuracy },
|
||||
{ label: 'Verification', value: qualityMetrics.source_verification },
|
||||
{ label: 'Coverage', value: qualityMetrics.citation_coverage }
|
||||
] : [];
|
||||
|
||||
console.log('🔍 [ContentPreviewHeader] Chips array created:', {
|
||||
qualityMetrics: qualityMetrics,
|
||||
chips: chipArray,
|
||||
chipsLength: chipArray.length
|
||||
});
|
||||
|
||||
return chipArray;
|
||||
}, [qualityMetrics]);
|
||||
|
||||
// Helper to build descriptive chip tooltip text
|
||||
const chipDescriptions: Record<string, string> = {
|
||||
Overall: 'Overall blends accuracy, verification and coverage into a single reliability score for this draft.',
|
||||
Accuracy: 'Factual Accuracy estimates how likely statements are to be factually correct based on grounding signals.',
|
||||
Verification: 'Source Verification reflects how well claims are linked to credible sources and whether citations match claims.',
|
||||
Coverage: 'Citation Coverage indicates how much of the content is supported with citations. Higher is better.'
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
background: '#e1f5fe',
|
||||
borderBottom: '1px solid #b3e5fc',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
color: '#0277bd',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<span>{topic ? `${topic} - LinkedIn Content Preview` : 'LinkedIn Content Preview'}</span>
|
||||
|
||||
{/* Persona Chip */}
|
||||
<PersonaChip
|
||||
platform="linkedin"
|
||||
userId={1}
|
||||
onPersonaUpdate={(personaData) => {
|
||||
console.log('Persona updated:', personaData);
|
||||
// You can add additional logic here to handle persona updates
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Research Chip with Hover Sub-chips */}
|
||||
{((researchSources && researchSources.length > 0) || (citations && citations.length > 0) || (searchQueries && searchQueries.length > 0)) && (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{/* Main Research Chip */}
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)',
|
||||
border: '1px solid #0284c7',
|
||||
borderRadius: '999px',
|
||||
padding: '6px 14px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
boxShadow: '0 2px 8px rgba(14, 165, 233, 0.3)',
|
||||
transform: 'translateZ(0)',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
title="Research data available. Hover to see sources, citations, and queries."
|
||||
onMouseEnter={(e) => {
|
||||
// Clear any existing timeout
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._researchTooltipTimeout) {
|
||||
clearTimeout(target._researchTooltipTimeout);
|
||||
target._researchTooltipTimeout = null;
|
||||
}
|
||||
|
||||
// Create and show research sub-chips tooltip
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 100000;
|
||||
background: white;
|
||||
border: 1px solid #cfe9f7;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
|
||||
padding: 16px;
|
||||
max-width: 400px;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: auto;
|
||||
`;
|
||||
|
||||
let subChipsHtml = '<div style="margin-bottom: 12px; font-weight: 600; color: #0a66c2; font-size: 14px;">Research Data</div>';
|
||||
|
||||
// Add Sources sub-chip
|
||||
if (researchSources && researchSources.length > 0) {
|
||||
subChipsHtml += `
|
||||
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
|
||||
onmouseenter="this.style.background='#e0f2fe'; this.style.transform='scale(1.05)'"
|
||||
onmouseleave="this.style.background='#f0f9ff'; this.style.transform='scale(1)'"
|
||||
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showResearchSourcesModal', { detail: 'sources' }))">
|
||||
<span style="display: inline-block; width: 6px; height: 6px; background: #10b981; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);"></span>
|
||||
Sources: ${researchSources.length}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Add Citations sub-chip
|
||||
if (citations && citations.length > 0) {
|
||||
subChipsHtml += `
|
||||
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #fef3c7; border: 1px solid #f59e0b; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
|
||||
onmouseenter="this.style.background='#fde68a'; this.style.transform='scale(1.05)'"
|
||||
onmouseleave="this.style.background='#fef3c7'; this.style.transform='scale(1)'"
|
||||
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showCitationsModal', { detail: 'citations' }))">
|
||||
<span style="display: inline-block; width: 6px; height: 6px; background: #f59e0b; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(245, 158, 11, 0.5);"></span>
|
||||
Citations: ${citations.length}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Add Queries sub-chip
|
||||
if (searchQueries && searchQueries.length > 0) {
|
||||
subChipsHtml += `
|
||||
<div style="display: inline-block; margin: 3px; padding: 6px 12px; background: #f3e8ff; border: 1px solid #8b5cf6; border-radius: 16px; font-size: 11px; cursor: pointer; font-weight: 600; transition: all 0.2s ease;"
|
||||
onmouseenter="this.style.background='#e9d5ff'; this.style.transform='scale(1.05)'"
|
||||
onmouseleave="this.style.background='#f3e8ff'; this.style.transform='scale(1)'"
|
||||
onclick="event.stopPropagation(); window.dispatchEvent(new CustomEvent('showSearchQueriesModal', { detail: 'queries' }))">
|
||||
<span style="display: inline-block; width: 6px; height: 6px; background: #8b5cf6; border-radius: 50%; margin-right: 6px; box-shadow: 0 0 4px rgba(139, 92, 246, 0.5);"></span>
|
||||
Queries: ${searchQueries.length}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
tooltip.innerHTML = subChipsHtml;
|
||||
|
||||
// Add mouse events to tooltip to keep it visible
|
||||
tooltip.addEventListener('mouseenter', () => {
|
||||
if (target._researchTooltipTimeout) {
|
||||
clearTimeout(target._researchTooltipTimeout);
|
||||
target._researchTooltipTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
tooltip.addEventListener('mouseleave', () => {
|
||||
target._researchTooltipTimeout = setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.style.opacity = '0';
|
||||
tooltip.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.remove();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
target._researchTooltip = null;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
tooltip.style.left = Math.min(rect.left, window.innerWidth - 420) + 'px';
|
||||
tooltip.style.top = (rect.bottom + 8) + 'px';
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
tooltip.style.opacity = '1';
|
||||
tooltip.style.transform = 'translateY(0)';
|
||||
}, 10);
|
||||
|
||||
target._researchTooltip = tooltip;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._researchTooltip) {
|
||||
// Add delay before hiding to allow moving to tooltip
|
||||
target._researchTooltipTimeout = setTimeout(() => {
|
||||
const tooltip = target._researchTooltip;
|
||||
if (tooltip && tooltip.parentNode) {
|
||||
tooltip.style.opacity = '0';
|
||||
tooltip.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.remove();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
target._researchTooltip = null;
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
// Keep tooltip visible when moving to sub-chips
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._researchTooltip) {
|
||||
const tooltip = target._researchTooltip;
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
tooltip.style.left = Math.min(rect.left, window.innerWidth - 420) + 'px';
|
||||
tooltip.style.top = (rect.bottom + 8) + 'px';
|
||||
}
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
// Add hover effect to the chip itself
|
||||
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 16px rgba(14, 165, 233, 0.4)';
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
// Remove hover effect
|
||||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(14, 165, 233, 0.3)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
flexShrink: 0,
|
||||
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
|
||||
}} />
|
||||
Research
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
{/* Quality Metrics Chip */}
|
||||
{chips.length > 0 && (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{/* Main Quality Metrics Chip */}
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
border: '1px solid #047857',
|
||||
borderRadius: '999px',
|
||||
padding: '6px 14px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
|
||||
transform: 'translateZ(0)',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
title="Quality metrics available. Hover to see detailed progress bars and explanations."
|
||||
onMouseEnter={(e) => {
|
||||
// Clear any existing timeout
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._qualityTooltipTimeout) {
|
||||
clearTimeout(target._qualityTooltipTimeout);
|
||||
target._qualityTooltipTimeout = null;
|
||||
}
|
||||
|
||||
// Create and show quality metrics tooltip with circular progress bars
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 100000;
|
||||
background: white;
|
||||
border: 1px solid #d1fae5;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: auto;
|
||||
`;
|
||||
|
||||
// Create circular progress bars for each metric
|
||||
const createCircularProgress = (label: string, value: number, description: string) => {
|
||||
const percentage = Math.round(value * 100);
|
||||
const color = getChipColor(value);
|
||||
const circumference = 2 * Math.PI * 45; // radius = 45
|
||||
const strokeDasharray = circumference;
|
||||
const strokeDashoffset = circumference - (percentage / 100) * circumference;
|
||||
|
||||
return `
|
||||
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding: 12px; background: #f8fafc; border-radius: 12px; border-left: 4px solid ${color};">
|
||||
<div style="position: relative; width: 60px; height: 60px;">
|
||||
<svg width="60" height="60" style="transform: rotate(-90deg);">
|
||||
<circle cx="30" cy="30" r="45" stroke="#e5e7eb" stroke-width="6" fill="none"/>
|
||||
<circle cx="30" cy="30" r="45" stroke="${color}" stroke-width="6" fill="none"
|
||||
stroke-dasharray="${strokeDasharray}" stroke-dashoffset="${strokeDashoffset}"
|
||||
style="transition: stroke-dashoffset 0.5s ease;"/>
|
||||
</svg>
|
||||
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-weight: 700; font-size: 14px; color: ${color};">
|
||||
${percentage}%
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 700; color: #1f2937; margin-bottom: 4px; font-size: 14px;">${label}</div>
|
||||
<div style="color: #6b7280; line-height: 1.4; font-size: 11px;">${description}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
let progressBarsHtml = '<div style="margin-bottom: 16px; font-weight: 700; color: #059669; font-size: 16px; text-align: center;">Quality Metrics</div>';
|
||||
|
||||
chips.forEach(chip => {
|
||||
progressBarsHtml += createCircularProgress(
|
||||
chip.label,
|
||||
chip.value || 0,
|
||||
chipDescriptions[chip.label] || ''
|
||||
);
|
||||
});
|
||||
|
||||
tooltip.innerHTML = progressBarsHtml;
|
||||
|
||||
// Add mouse events to tooltip to keep it visible
|
||||
tooltip.addEventListener('mouseenter', () => {
|
||||
if (target._qualityTooltipTimeout) {
|
||||
clearTimeout(target._qualityTooltipTimeout);
|
||||
target._qualityTooltipTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
tooltip.addEventListener('mouseleave', () => {
|
||||
target._qualityTooltipTimeout = setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.style.opacity = '0';
|
||||
tooltip.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.remove();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
target._qualityTooltip = null;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
tooltip.style.left = Math.min(rect.left, window.innerWidth - 520) + 'px';
|
||||
tooltip.style.top = (rect.bottom + 8) + 'px';
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
tooltip.style.opacity = '1';
|
||||
tooltip.style.transform = 'translateY(0)';
|
||||
}, 10);
|
||||
|
||||
target._qualityTooltip = tooltip;
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._qualityTooltip) {
|
||||
// Add delay before hiding to allow moving to tooltip
|
||||
target._qualityTooltipTimeout = setTimeout(() => {
|
||||
const tooltip = target._qualityTooltip;
|
||||
if (tooltip && tooltip.parentNode) {
|
||||
tooltip.style.opacity = '0';
|
||||
tooltip.style.transform = 'translateY(-8px)';
|
||||
setTimeout(() => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.remove();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
target._qualityTooltip = null;
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
// Keep tooltip visible when moving to progress bars
|
||||
const target = e.currentTarget as ExtendedDivElement;
|
||||
if (target._qualityTooltip) {
|
||||
const tooltip = target._qualityTooltip;
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
tooltip.style.left = Math.min(rect.left, window.innerWidth - 520) + 'px';
|
||||
tooltip.style.top = (rect.bottom + 8) + 'px';
|
||||
}
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
// Add hover effect to the chip itself
|
||||
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 16px rgba(16, 185, 129, 0.4)';
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
// Remove hover effect
|
||||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(16, 185, 129, 0.3)';
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
flexShrink: 0,
|
||||
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
|
||||
}} />
|
||||
Quality Metrics
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span style={{ fontSize: '10px', opacity: 0.8 }}>
|
||||
{draft.split(/\s+/).length} words • {Math.ceil(draft.split(/\s+/).length / 200)} min read
|
||||
</span>
|
||||
{/* Assistive Writing toggle */}
|
||||
{onAssistantToggle && (
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#666', cursor: 'pointer' }}
|
||||
title="Assistive Writing: Get real-time AI-powered writing suggestions as you type. Uses Exa.ai for web research and Gemini for intelligent content generation. Automatically enables editing mode to allow typing and content modification."
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assistantOn || false}
|
||||
onChange={(e) => onAssistantToggle(e.target.checked)}
|
||||
/>
|
||||
Assistive Writing
|
||||
</label>
|
||||
)}
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#666', cursor: 'pointer' }}
|
||||
title="Toggle preview visibility"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!showPreview}
|
||||
onChange={() => onPreviewToggle()}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
Hide Preview
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainContentPreviewHeader;
|
||||
@@ -0,0 +1,324 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PersonaEditorModal from './PersonaEditorModal';
|
||||
|
||||
interface PersonaData {
|
||||
id?: number;
|
||||
user_id?: number;
|
||||
persona_name: string;
|
||||
archetype: string;
|
||||
core_belief: string;
|
||||
brand_voice_description: string;
|
||||
linguistic_fingerprint: any;
|
||||
platform_adaptations: any;
|
||||
confidence_score: number;
|
||||
ai_analysis_version: string;
|
||||
platform_type: string;
|
||||
sentence_metrics: any;
|
||||
lexical_features: any;
|
||||
rhetorical_devices: any;
|
||||
tonal_range: any;
|
||||
stylistic_constraints: any;
|
||||
content_format_rules: any;
|
||||
engagement_patterns: any;
|
||||
posting_frequency: any;
|
||||
content_types: any;
|
||||
platform_best_practices: any;
|
||||
algorithm_considerations: any;
|
||||
}
|
||||
|
||||
interface PersonaChipProps {
|
||||
platform: string;
|
||||
userId?: number;
|
||||
onPersonaUpdate?: (personaData: PersonaData) => void;
|
||||
}
|
||||
|
||||
const PersonaChip: React.FC<PersonaChipProps> = ({
|
||||
platform,
|
||||
userId = 1,
|
||||
onPersonaUpdate
|
||||
}) => {
|
||||
const [personaData, setPersonaData] = useState<PersonaData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch persona data
|
||||
const fetchPersonaData = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Fetch core persona list (take most recent active) and platform-specific details
|
||||
const [coreRes, platformRes] = await Promise.all([
|
||||
fetch(`/api/personas/user/${userId}`),
|
||||
fetch(`/api/personas/platform/${platform}?user_id=${userId}`)
|
||||
]);
|
||||
|
||||
if (coreRes.ok && platformRes.ok) {
|
||||
const coreList = await coreRes.json();
|
||||
const platformData = await platformRes.json();
|
||||
const core = (coreList?.personas && coreList.personas.length > 0) ? coreList.personas[0] : {};
|
||||
|
||||
// Merge core + platform fields for editor convenience
|
||||
setPersonaData({
|
||||
id: core.id,
|
||||
user_id: core.user_id,
|
||||
persona_name: core.persona_name,
|
||||
archetype: core.archetype,
|
||||
core_belief: core.core_belief,
|
||||
brand_voice_description: core.brand_voice_description,
|
||||
linguistic_fingerprint: core.linguistic_fingerprint,
|
||||
platform_adaptations: core.platform_adaptations,
|
||||
confidence_score: core.confidence_score,
|
||||
ai_analysis_version: core.ai_analysis_version,
|
||||
platform_type: platform,
|
||||
sentence_metrics: platformData?.sentence_metrics,
|
||||
lexical_features: platformData?.lexical_features,
|
||||
rhetorical_devices: platformData?.rhetorical_devices,
|
||||
tonal_range: platformData?.tonal_range,
|
||||
stylistic_constraints: platformData?.stylistic_constraints,
|
||||
content_format_rules: platformData?.content_format_rules,
|
||||
engagement_patterns: platformData?.engagement_patterns,
|
||||
posting_frequency: platformData?.posting_frequency,
|
||||
content_types: platformData?.content_types,
|
||||
platform_best_practices: platformData?.platform_best_practices,
|
||||
algorithm_considerations: platformData?.algorithm_considerations,
|
||||
} as any);
|
||||
} else {
|
||||
setError('No persona found for this platform');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load persona data');
|
||||
console.error('Error fetching persona:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPersonaData();
|
||||
}, [platform, userId]);
|
||||
|
||||
const handleSavePersona = async (data: PersonaData, saveToDatabase: boolean) => {
|
||||
try {
|
||||
if (saveToDatabase) {
|
||||
// Save core persona simple fields
|
||||
if (data.id) {
|
||||
const corePayload: any = {
|
||||
persona_name: data.persona_name,
|
||||
archetype: data.archetype,
|
||||
core_belief: data.core_belief,
|
||||
brand_voice_description: data.brand_voice_description,
|
||||
linguistic_fingerprint: data.linguistic_fingerprint,
|
||||
platform_adaptations: data.platform_adaptations,
|
||||
};
|
||||
|
||||
const coreRes = await fetch(`/api/personas/${data.id}?user_id=${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(corePayload)
|
||||
});
|
||||
if (!coreRes.ok) throw new Error('Failed to update core persona');
|
||||
}
|
||||
|
||||
// Save platform persona fields
|
||||
const platformPayload: any = {
|
||||
sentence_metrics: data.sentence_metrics,
|
||||
lexical_features: data.lexical_features,
|
||||
rhetorical_devices: data.rhetorical_devices,
|
||||
tonal_range: data.tonal_range,
|
||||
stylistic_constraints: data.stylistic_constraints,
|
||||
content_format_rules: data.content_format_rules,
|
||||
engagement_patterns: data.engagement_patterns,
|
||||
posting_frequency: data.posting_frequency,
|
||||
content_types: data.content_types,
|
||||
platform_best_practices: data.platform_best_practices,
|
||||
algorithm_considerations: data.algorithm_considerations,
|
||||
};
|
||||
|
||||
const platRes = await fetch(`/api/personas/platform/${platform}?user_id=${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(platformPayload)
|
||||
});
|
||||
if (!platRes.ok) throw new Error('Failed to update platform persona');
|
||||
}
|
||||
|
||||
// Update local state
|
||||
setPersonaData(data);
|
||||
|
||||
// Notify parent component
|
||||
if (onPersonaUpdate) {
|
||||
onPersonaUpdate(data);
|
||||
}
|
||||
|
||||
console.log('Persona updated:', saveToDatabase ? 'saved to database' : 'session only');
|
||||
} catch (err) {
|
||||
console.error('Error saving persona:', err);
|
||||
setError('Failed to save persona changes');
|
||||
}
|
||||
};
|
||||
|
||||
const getPersonaColor = (confidence?: number) => {
|
||||
if (!confidence) return '#6b7280';
|
||||
if (confidence >= 0.8) return '#10b981';
|
||||
if (confidence >= 0.6) return '#f59e0b';
|
||||
return '#ef4444';
|
||||
};
|
||||
|
||||
const getPersonaIcon = (archetype?: string) => {
|
||||
if (!archetype) return '👤';
|
||||
|
||||
const archetypeIcons: Record<string, string> = {
|
||||
'pragmatic futurist': '🔮',
|
||||
'thoughtful educator': '📚',
|
||||
'innovative leader': '🚀',
|
||||
'analytical expert': '🔍',
|
||||
'creative storyteller': '✨',
|
||||
'strategic advisor': '🎯',
|
||||
'authentic connector': '🤝',
|
||||
'data-driven optimist': '📊'
|
||||
};
|
||||
|
||||
const lowerArchetype = archetype.toLowerCase();
|
||||
for (const [key, icon] of Object.entries(archetypeIcons)) {
|
||||
if (lowerArchetype.includes(key)) {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
return '👤';
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%)',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '999px',
|
||||
padding: '6px 14px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
color: '#6b7280',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: '#9ca3af',
|
||||
animation: 'pulse 2s infinite'
|
||||
}} />
|
||||
Loading Persona...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !personaData) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)',
|
||||
border: '1px solid #fca5a5',
|
||||
borderRadius: '999px',
|
||||
padding: '6px 14px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
color: '#dc2626',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => fetchPersonaData()}
|
||||
title="Click to retry loading persona data"
|
||||
>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: '#ef4444'
|
||||
}} />
|
||||
No Persona
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const confidence = personaData.confidence_score || 0;
|
||||
const confidenceColor = getPersonaColor(confidence);
|
||||
|
||||
// Debug: Log the confidence score to see what's being stored
|
||||
console.log('PersonaChip confidence_score:', personaData.confidence_score, 'processed:', confidence);
|
||||
const personaIcon = getPersonaIcon(personaData.archetype);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${confidenceColor} 0%, ${confidenceColor}dd 100%)`,
|
||||
border: `1px solid ${confidenceColor}`,
|
||||
borderRadius: '999px',
|
||||
padding: '6px 14px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
boxShadow: `0 2px 8px ${confidenceColor}40`,
|
||||
transform: 'translateZ(0)',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
title={`${personaData.persona_name} - ${personaData.archetype || 'No archetype'} (${Math.round(confidence * 100)}% confidence). Click to edit.`}
|
||||
onMouseOver={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)';
|
||||
e.currentTarget.style.boxShadow = `0 4px 16px ${confidenceColor}60`;
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
||||
e.currentTarget.style.boxShadow = `0 2px 8px ${confidenceColor}40`;
|
||||
}}
|
||||
onClick={() => setShowEditor(true)}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{personaIcon}
|
||||
</div>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
flexShrink: 0,
|
||||
boxShadow: '0 0 6px rgba(255, 255, 255, 0.5)'
|
||||
}} />
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
{personaData.persona_name || 'Untitled Persona'}
|
||||
</span>
|
||||
<div style={{
|
||||
fontSize: '10px',
|
||||
opacity: 0.8,
|
||||
marginLeft: '4px'
|
||||
}}>
|
||||
{Math.round(confidence * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PersonaEditorModal
|
||||
isOpen={showEditor}
|
||||
onClose={() => setShowEditor(false)}
|
||||
personaData={personaData}
|
||||
onSave={(data, saveToDatabase) => handleSavePersona(data, saveToDatabase)}
|
||||
platform={platform}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonaChip;
|
||||
@@ -0,0 +1,857 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface PersonaData {
|
||||
// Core WritingPersona fields
|
||||
id?: number;
|
||||
user_id?: number;
|
||||
persona_name: string;
|
||||
archetype: string;
|
||||
core_belief: string;
|
||||
brand_voice_description: string;
|
||||
linguistic_fingerprint: any;
|
||||
platform_adaptations: any;
|
||||
confidence_score: number;
|
||||
ai_analysis_version: string;
|
||||
|
||||
// PlatformPersona fields
|
||||
platform_type: string;
|
||||
sentence_metrics: any;
|
||||
lexical_features: any;
|
||||
rhetorical_devices: any;
|
||||
tonal_range: any;
|
||||
stylistic_constraints: any;
|
||||
content_format_rules: any;
|
||||
engagement_patterns: any;
|
||||
posting_frequency: any;
|
||||
content_types: any;
|
||||
platform_best_practices: any;
|
||||
algorithm_considerations: any;
|
||||
}
|
||||
|
||||
interface PersonaEditorModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
personaData: PersonaData | null;
|
||||
onSave: (data: PersonaData, saveToDatabase: boolean) => void;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
const PersonaEditorModal: React.FC<PersonaEditorModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
personaData,
|
||||
onSave,
|
||||
platform
|
||||
}) => {
|
||||
const [editedData, setEditedData] = useState<PersonaData | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'core' | 'linguistic' | 'platform' | 'optimization'>('core');
|
||||
const [saveToDatabase, setSaveToDatabase] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (personaData) {
|
||||
setEditedData({ ...personaData });
|
||||
}
|
||||
}, [personaData]);
|
||||
|
||||
if (!isOpen || !editedData) return null;
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(editedData, saveToDatabase);
|
||||
};
|
||||
|
||||
const updateField = (path: string, value: any) => {
|
||||
setEditedData(prev => {
|
||||
if (!prev) return prev;
|
||||
const newData = { ...prev };
|
||||
const keys = path.split('.');
|
||||
let current: any = newData;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!current[keys[i]]) {
|
||||
current[keys[i]] = {};
|
||||
}
|
||||
current = current[keys[i]];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]] = value;
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const getFieldValue = (path: string, defaultValue: any = '') => {
|
||||
const keys = path.split('.');
|
||||
let current: any = editedData;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === 'object' && key in current) {
|
||||
current = current[key];
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return current || defaultValue;
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'core', label: 'Core Identity', icon: '🎭' },
|
||||
{ id: 'linguistic', label: 'Linguistic', icon: '📝' },
|
||||
{ id: 'platform', label: 'Platform', icon: '🔗' },
|
||||
{ id: 'optimization', label: 'Optimization', icon: '⚡' }
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 10000,
|
||||
padding: '20px'
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
|
||||
width: '100%',
|
||||
maxWidth: '800px',
|
||||
maxHeight: '90vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
padding: '24px 24px 0 24px',
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, fontSize: '24px', fontWeight: '700' }}>
|
||||
Edit Persona: {getFieldValue('persona_name', 'Untitled Persona')}
|
||||
</h2>
|
||||
<p style={{ margin: '4px 0 0 0', fontSize: '14px', opacity: 0.9 }}>
|
||||
Platform: {platform} • Confidence: {(() => {
|
||||
const score = getFieldValue('confidence_score', 0) || 0;
|
||||
console.log('PersonaEditorModal confidence_score:', score);
|
||||
return score;
|
||||
})()}%
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.2)',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '18px',
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
style={{
|
||||
background: activeTab === tab.id ? 'rgba(255, 255, 255, 0.2)' : 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
padding: '12px 16px',
|
||||
color: 'white',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
>
|
||||
<span>{tab.icon}</span>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
padding: '24px'
|
||||
}}>
|
||||
{activeTab === 'core' && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Persona Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={getFieldValue('persona_name', '')}
|
||||
onChange={(e) => updateField('persona_name', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = '#667eea'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Archetype / Guide
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('archetype', '')}
|
||||
onChange={(e) => updateField('archetype', e.target.value)}
|
||||
rows={2}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '60px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = '#667eea'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Core Belief / Mission
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('core_belief', '')}
|
||||
onChange={(e) => updateField('core_belief', e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '80px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = '#667eea'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Brand Voice / Speaking Style
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('brand_voice_description', '')}
|
||||
onChange={(e) => updateField('brand_voice_description', e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '80px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = '#667eea'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Confidence Score
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={getFieldValue('confidence_score', '')}
|
||||
onChange={(e) => updateField('confidence_score', parseInt(e.target.value) || 0)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = '#667eea'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
AI Analysis Version
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={getFieldValue('ai_analysis_version', '')}
|
||||
onChange={(e) => updateField('ai_analysis_version', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = '#667eea'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'linguistic' && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
|
||||
Sentence Metrics
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Average Sentence Length (words)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={getFieldValue('linguistic_fingerprint.sentence_metrics.average_sentence_length_words', '')}
|
||||
onChange={(e) => updateField('linguistic_fingerprint.sentence_metrics.average_sentence_length_words', parseInt(e.target.value) || '')}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Preferred Sentence Type
|
||||
</label>
|
||||
<select
|
||||
value={getFieldValue('linguistic_fingerprint.sentence_metrics.preferred_sentence_type', '')}
|
||||
onChange={(e) => updateField('linguistic_fingerprint.sentence_metrics.preferred_sentence_type', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
>
|
||||
<option value="simple_and_compound">Simple and Compound</option>
|
||||
<option value="complex">Complex</option>
|
||||
<option value="mixed">Mixed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
|
||||
Lexical Features
|
||||
</h3>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Go-to Words (comma-separated)
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('linguistic_fingerprint.lexical_features.go_to_words', []).join(', ')}
|
||||
onChange={(e) => updateField('linguistic_fingerprint.lexical_features.go_to_words', e.target.value.split(',').map(w => w.trim()).filter(w => w))}
|
||||
rows={2}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '60px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
placeholder="e.g., innovative, strategic, transformative, impactful, leverage"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Avoid Words (comma-separated)
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('linguistic_fingerprint.lexical_features.avoid_words', []).join(', ')}
|
||||
onChange={(e) => updateField('linguistic_fingerprint.lexical_features.avoid_words', e.target.value.split(',').map(w => w.trim()).filter(w => w))}
|
||||
rows={2}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '60px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
placeholder="e.g., buzzwords, jargon, clichés, overly technical terms"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
|
||||
Tonal Range
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Default Tone
|
||||
</label>
|
||||
<select
|
||||
value={getFieldValue('tonal_range.default_tone', '')}
|
||||
onChange={(e) => updateField('tonal_range.default_tone', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
>
|
||||
<option value="professional">Professional</option>
|
||||
<option value="conversational">Conversational</option>
|
||||
<option value="authoritative">Authoritative</option>
|
||||
<option value="inspirational">Inspirational</option>
|
||||
<option value="educational">Educational</option>
|
||||
<option value="friendly">Friendly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Permissible Tones (comma-separated)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={getFieldValue('tonal_range.permissible_tones', []).join(', ')}
|
||||
onChange={(e) => updateField('tonal_range.permissible_tones', e.target.value.split(',').map(w => w.trim()).filter(w => w))}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
placeholder="e.g., professional, conversational, authoritative"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'platform' && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
|
||||
Content Format Rules
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Character Limit
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={getFieldValue('content_format_rules.character_limit', '')}
|
||||
onChange={(e) => updateField('content_format_rules.character_limit', parseInt(e.target.value) || 3000)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Optimal Length
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={getFieldValue('content_format_rules.optimal_length', '')}
|
||||
onChange={(e) => updateField('content_format_rules.optimal_length', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
placeholder="e.g., 150-200 words"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Paragraph Structure
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('content_format_rules.paragraph_structure', '')}
|
||||
onChange={(e) => updateField('content_format_rules.paragraph_structure', e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '80px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
|
||||
Engagement Patterns
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Posting Frequency
|
||||
</label>
|
||||
<select
|
||||
value={getFieldValue('engagement_patterns.posting_frequency', '')}
|
||||
onChange={(e) => updateField('engagement_patterns.posting_frequency', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="2-3 times per week">2-3 times per week</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="bi-weekly">Bi-weekly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Best Posting Times
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={getFieldValue('engagement_patterns.best_posting_times', '')}
|
||||
onChange={(e) => updateField('engagement_patterns.best_posting_times', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
placeholder="e.g., Tuesday-Thursday, 8-10 AM or 1-3 PM"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
|
||||
Content Types
|
||||
</h3>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Preferred Content Types (comma-separated)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={getFieldValue('content_types.preferred_types', []).join(', ')}
|
||||
onChange={(e) => updateField('content_types.preferred_types', e.target.value.split(',').map(w => w.trim()).filter(w => w))}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
placeholder="e.g., thought leadership, industry insights, case studies, tips"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'optimization' && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
|
||||
Platform Best Practices
|
||||
</h3>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Hashtag Strategy
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('platform_best_practices.hashtag_strategy', '')}
|
||||
onChange={(e) => updateField('platform_best_practices.hashtag_strategy', e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '80px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Call-to-Action Style
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('platform_best_practices.cta_style', '')}
|
||||
onChange={(e) => updateField('platform_best_practices.cta_style', e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '80px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
|
||||
Algorithm Considerations
|
||||
</h3>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Engagement Optimization
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('algorithm_considerations.engagement_optimization', '')}
|
||||
onChange={(e) => updateField('algorithm_considerations.engagement_optimization', e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '80px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Content Timing
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('algorithm_considerations.content_timing', '')}
|
||||
onChange={(e) => updateField('algorithm_considerations.content_timing', e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '80px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '18px', fontWeight: '600', color: '#374151' }}>
|
||||
Stylistic Constraints
|
||||
</h3>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '600', color: '#374151', marginBottom: '8px' }}>
|
||||
Forbidden Elements
|
||||
</label>
|
||||
<textarea
|
||||
value={getFieldValue('stylistic_constraints.forbidden_elements', '')}
|
||||
onChange={(e) => updateField('stylistic_constraints.forbidden_elements', e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
resize: 'vertical',
|
||||
minHeight: '80px',
|
||||
transition: 'border-color 0.2s ease',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
padding: '20px 24px',
|
||||
borderTop: '1px solid #e5e7eb',
|
||||
backgroundColor: '#f9fafb',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '14px', color: '#374151' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={saveToDatabase}
|
||||
onChange={(e) => setSaveToDatabase(e.target.checked)}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
Save changes to database
|
||||
<span style={{ fontSize: '12px', color: '#6b7280' }}>
|
||||
{saveToDatabase ? 'Changes will be permanent' : 'Changes will be session-only'}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'white',
|
||||
color: '#374151',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#d1d5db';
|
||||
e.currentTarget.style.backgroundColor = '#f9fafb';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = '#e5e7eb';
|
||||
e.currentTarget.style.backgroundColor = 'white';
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#667eea',
|
||||
color: 'white',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#5a67d8';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#667eea';
|
||||
}}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonaEditorModal;
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as MainContentPreviewHeader } from './MainContentPreviewHeader';
|
||||
export { default as ContentPreviewHeaderWithModals } from './ContentPreviewHeaderWithModals';
|
||||
export { default as PersonaEditorModal } from './PersonaEditorModal';
|
||||
export { default as PersonaChip } from './PersonaChip';
|
||||
@@ -2,7 +2,6 @@ export { default as CitationHoverHandler } from './CitationHoverHandler';
|
||||
export { default as useTextSelectionHandler } from './TextSelectionHandler';
|
||||
export { default as QuickEditToolbar } from './QuickEditToolbar';
|
||||
export { default as DiffPreviewModal } from './DiffPreviewModal';
|
||||
export { default as ContentPreviewHeader } from './ContentPreviewHeader';
|
||||
export { ContentPreviewHeaderWithModals } from './ContentPreviewHeader';
|
||||
export { MainContentPreviewHeader as ContentPreviewHeader, ContentPreviewHeaderWithModals } from './ContentPreviewHeaderComponents';
|
||||
export { default as WritingAssistantCard } from './WritingAssistantCard';
|
||||
export { default as ContentDisplayArea } from './ContentDisplayArea';
|
||||
|
||||
Reference in New Issue
Block a user