ContentGuardianAgent consolidation:
- Merge 3 duplicate classes into single source in specialized/content_guardian.py
- Watchdog audit_committee() with heuristic scoring, coverage gaps, overlaps, alerts
- Remove misleading rejection_rate() helper; use acceptance_rate directly
- Integrate audit + alerts + trend signals into today_workflow_service.py
Team Activity page:
- QualityAuditPanel: health ring, per-agent critiques, coverage gaps, overlaps
- TrendSignalsPanel: opportunity cards with urgency/impact/coverage bars
- AlertBanner: persistent dismiss via POST /alerts/{id}/mark-read
- AgentHelpModal: dialog showing all 8 agents with descriptions, tools, schedule
- QualityAuditPanel action buttons: Fill gap -> /content-planning, Resolve overlap, View CTA on alerts/issues
- TrendSignalsPanel action buttons: Create content from this trend -> /blog-writer with trend context state
Onboarding system:
- Step 4 validation: no auto-pass via basic_ready; requires persona data or explicit progression
- Step 5 validation: logs warning on auto-pass without integration data
- OnboardingCompletionService: single DB session, transactional task creation, upsert pattern
- Business-without-website: nullable website_url on SIFIndexingTask and MarketTrendsTask
- DeepCompetitorAnalysisExecutor: 5-min timeout, 10-competitor cap, asyncio.wait_for
- Persona generation: async with 30s timeout, falls back to scheduler
- OnboardingProgressService.reset_onboarding(): resets session + pauses all DB tasks
- OnboardingControlService.reset_onboarding(): also cancels APScheduler jobs
- FinalStep TaskSchedulingPanel: shows scheduled/failed tasks after completion, 8s auto-redirect
- onboarding_completed agent activity event logged to feed
Documentation:
- docs-site/features/onboarding/: overview, steps, scheduler-tasks, technical-reference (4 pages)
- docs-site/mkdocs.yml: added Onboarding System nav section
- docs-site/features/sif-agents/: overview, agent-directory, committee-system, content-guardian (4 pages)
- docs-site/features/team-activity/: overview, quality-audit, trend-signals, alert-system (4 pages)
- docs-site/features/todays-workflow/: updated overview, technical-architecture, workflow-guide, api-reference
165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Dialog, DialogTitle, DialogContent, DialogActions,
|
|
Button, Typography, Box, Chip, IconButton, Divider
|
|
} from '@mui/material';
|
|
import { Close as CloseIcon, Check as CheckIcon } from '@mui/icons-material';
|
|
import type { DiffPreviewData, DiffSegment } from '../../../utils/getSectionDiffs';
|
|
|
|
interface DiffPreviewModalProps {
|
|
isOpen: boolean;
|
|
diffData: DiffPreviewData | null;
|
|
onAccept: () => void;
|
|
onReject: () => void;
|
|
loading?: boolean;
|
|
}
|
|
|
|
function renderDiffSegments(segments: DiffSegment[]): React.ReactNode {
|
|
return segments.map((seg, i) => {
|
|
if (seg.added) {
|
|
return (
|
|
<Box
|
|
component="span"
|
|
key={i}
|
|
sx={{
|
|
bgcolor: '#dcfce7',
|
|
color: '#166534',
|
|
px: 0.5,
|
|
borderRadius: '3px',
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{seg.value}
|
|
</Box>
|
|
);
|
|
}
|
|
if (seg.removed) {
|
|
return (
|
|
<Box
|
|
component="span"
|
|
key={i}
|
|
sx={{
|
|
bgcolor: '#fee2e2',
|
|
color: '#991b1b',
|
|
px: 0.5,
|
|
borderRadius: '3px',
|
|
textDecoration: 'line-through',
|
|
textDecorationColor: '#dc2626',
|
|
}}
|
|
>
|
|
{seg.value}
|
|
</Box>
|
|
);
|
|
}
|
|
return <span key={i}>{seg.value}</span>;
|
|
});
|
|
}
|
|
|
|
export const DiffPreviewModal: React.FC<DiffPreviewModalProps> = ({
|
|
isOpen,
|
|
diffData,
|
|
onAccept,
|
|
onReject,
|
|
loading = false,
|
|
}) => {
|
|
if (!diffData) return null;
|
|
|
|
const hasAnyChange = diffData.introductionChanged || diffData.sectionDiffs.some(s => s.changed);
|
|
|
|
return (
|
|
<Dialog open={isOpen} maxWidth="lg" fullWidth fullScreen>
|
|
<Box
|
|
sx={{
|
|
position: 'sticky',
|
|
top: 0,
|
|
zIndex: 10,
|
|
bgcolor: 'white',
|
|
borderBottom: '2px solid #e2e8f0',
|
|
}}
|
|
>
|
|
<DialogTitle sx={{ pb: 1, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
<Typography variant="h6" sx={{ fontWeight: 700, flexGrow: 1 }}>
|
|
SEO Recommendations — Review Changes
|
|
</Typography>
|
|
<IconButton onClick={onReject} size="small"><CloseIcon /></IconButton>
|
|
</DialogTitle>
|
|
<Box sx={{ px: 3, pb: 1.5, display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<Chip
|
|
icon={<CheckIcon />}
|
|
label={`${diffData.sectionDiffs.filter(s => s.changed).length} section(s) changed`}
|
|
color="warning"
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', ml: 'auto' }}>
|
|
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center', fontSize: '0.75rem', color: '#166534' }}>
|
|
<Box sx={{ width: 14, height: 14, bgcolor: '#dcfce7', border: '1px solid #86efac', borderRadius: '2px' }} />
|
|
<span>Added</span>
|
|
</Box>
|
|
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center', fontSize: '0.75rem', color: '#991b1b' }}>
|
|
<Box sx={{ width: 14, height: 14, bgcolor: '#fee2e2', border: '1px solid #fca5a5', borderRadius: '2px' }} />
|
|
<span>Removed</span>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
<DialogContent sx={{ py: 3, bgcolor: '#f8fafc' }}>
|
|
{!hasAnyChange && (
|
|
<Typography sx={{ textAlign: 'center', py: 4, color: '#64748b' }}>
|
|
No changes detected between original and recommended content.
|
|
</Typography>
|
|
)}
|
|
|
|
{diffData.introductionChanged && (
|
|
<Box sx={{ mb: 4 }}>
|
|
<Typography sx={{ fontWeight: 700, fontSize: '0.85rem', textTransform: 'uppercase', letterSpacing: '0.05em', color: '#475569', mb: 1 }}>
|
|
Introduction
|
|
</Typography>
|
|
<Box sx={{ bgcolor: 'white', border: '1px solid #e2e8f0', borderRadius: 2, p: 2.5, fontFamily: 'Georgia, serif', fontSize: '1rem', lineHeight: 1.8, color: '#1e293b' }}>
|
|
{renderDiffSegments(diffData.introductionDiff!)}
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
|
|
{diffData.sectionDiffs.map((section, idx) => {
|
|
if (!section.changed) return null;
|
|
return (
|
|
<Box key={section.heading || idx} sx={{ mb: 3 }}>
|
|
<Typography sx={{ fontWeight: 700, fontSize: '0.85rem', textTransform: 'uppercase', letterSpacing: '0.05em', color: '#475569', mb: 0.5 }}>
|
|
{section.heading}
|
|
</Typography>
|
|
<Box sx={{ bgcolor: 'white', border: '1px solid #e2e8f0', borderRadius: 2, p: 2.5, fontFamily: 'Georgia, serif', fontSize: '1rem', lineHeight: 1.8, color: '#1e293b' }}>
|
|
{renderDiffSegments(section.segments)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
})}
|
|
</DialogContent>
|
|
|
|
<DialogActions sx={{ px: 3, py: 2, borderTop: '1px solid #e2e8f0', bgcolor: 'white' }}>
|
|
<Button
|
|
onClick={onReject}
|
|
disabled={loading}
|
|
variant="outlined"
|
|
color="error"
|
|
sx={{ textTransform: 'none', fontWeight: 600 }}
|
|
>
|
|
Reject Changes
|
|
</Button>
|
|
<Button
|
|
onClick={onAccept}
|
|
disabled={loading}
|
|
variant="contained"
|
|
color="primary"
|
|
sx={{ textTransform: 'none', fontWeight: 600 }}
|
|
>
|
|
Accept Changes
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default DiffPreviewModal;
|