fix: WYSIWYG editor, content generation, and writing assistant bug fixes

- Fix text selection menu not showing: wire contentRef via inputRef on multiline TextField
- Fix blog title not truncating: add min-w-0 for flex item overflow
- Fix outline generation 500: escape curly braces in f-string prompt template
- Fix content generation 'NoneType not callable': replace SessionLocal() with get_session_for_user(), add db param to MediumBlogGenerator, fix signature mismatch in database_task_manager
- Fix writing assistant suggest 500: add auth + user_id to API endpoint and service, replace sync requests with httpx.AsyncClient
- Fix hallucination detector 404: explicitly include router in main.py and app.py
- Fix missing error_data in task failure responses
- Hide CopilotKit web inspector button
- Remove hardcoded fallback suggestions from SmartTypingAssist
- Fix stale closure refs in SmartTypingAssist handleTypingChange
- Add two-column editor layout, stats bar, section hover menu
- Various subscription, billing, and research module improvements
This commit is contained in:
ajaysi
2026-05-14 09:11:30 +05:30
parent 7385100017
commit 928c2f20aa
113 changed files with 4344 additions and 10064 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import {
Paper,
IconButton,
@@ -6,17 +6,7 @@ import {
TextField,
Tooltip,
CircularProgress,
Divider,
Button,
Menu,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
List,
ListItem,
ListItemText
Divider
} from '@mui/material';
import {
Edit as EditIcon,
@@ -24,12 +14,12 @@ import {
FileCopyOutlined as FileCopyOutlinedIcon,
Link as LinkIcon,
AutoAwesome as AutoAwesomeIcon,
Info as InfoIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
MoreHoriz as MoreHorizIcon,
} from '@mui/icons-material';
import useBlogTextSelectionHandler from './BlogTextSelectionHandler';
import { ContinuityBadge } from '../ContinuityBadge';
import HoverMenu from './HoverMenu';
import { blogWriterApi } from '../../../services/blogWriterApi';
interface BlogSectionProps {
@@ -57,7 +47,6 @@ const BlogSection: React.FC<BlogSectionProps> = ({
id,
title,
content: initialContent,
wordCount,
sources,
outlineData,
onContentUpdate,
@@ -80,205 +69,142 @@ const BlogSection: React.FC<BlogSectionProps> = ({
const [toolResult, setToolResult] = useState<any>(null);
const [toolDialogOpen, setToolDialogOpen] = useState(false);
// Initialize assistive writing handler
const wordCount_ = useMemo(() => content.split(/\s+/).filter(Boolean).length, [content]);
const assistiveWriting = useBlogTextSelectionHandler(
contentRef,
(originalText: string, newText: string, editType: string) => {
// Handle text replacement in the textarea
if (contentRef.current) {
const textarea = contentRef.current;
// For smart suggestions, newText is already the complete updated content with insertion
// For other edits (like text selection improvements), we need to replace originalText with newText
let updatedContent: string;
if (editType === 'smart-suggestion') {
// newText already contains the full content with suggestion inserted
updatedContent = newText;
} else {
// For other edits, replace the selected text
const currentContent = textarea.value;
updatedContent = currentContent.replace(originalText, newText);
updatedContent = textarea.value.replace(originalText, newText);
}
console.log('🔍 [BlogSection] Text updated, editType:', editType, 'New length:', updatedContent.length);
setContent(updatedContent);
// Update parent state
if (onContentUpdate) {
onContentUpdate([{ id, content: updatedContent }]);
}
// Note: Cursor positioning is handled by SmartTypingAssist for smart-suggestion edits
// For other edits, we may need to handle cursor positioning here if needed
if (onContentUpdate) onContentUpdate([{ id, content: updatedContent }]);
}
}
);
// Format content helper - ensures proper paragraph breaks
const formatContent = (rawContent: string) => {
if (!rawContent) return rawContent;
// Ensure double line breaks between paragraphs
// Replace single line breaks with double line breaks if they're not already double
let formatted = rawContent
.replace(/\n{3,}/g, '\n\n') // Replace 3+ line breaks with double
.replace(/\n(?!\n)/g, '\n\n') // Replace single line breaks with double
.trim();
return formatted;
return rawContent.replace(/\n{3,}/g, '\n\n').replace(/\n(?!\n)/g, '\n\n').trim();
};
// Sync content when initialContent changes (e.g., from AI generation)
useEffect(() => {
if (initialContent !== content) {
const formattedContent = formatContent(initialContent);
setContent(formattedContent);
setContent(formatContent(initialContent));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialContent]);
const handleContentChange = (e: any) => {
const newContent = e.target.value;
console.log('🔍 [BlogSection] handleContentChange called, content length:', newContent.length);
setContent(newContent);
// Trigger smart typing assist
assistiveWriting.handleTypingChange(newContent);
};
const handleFocus = () => setIsFocused(true);
const handleBlur = () => setIsFocused(false);
const openToolsMenu = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
event.stopPropagation();
setToolsAnchorEl(event.currentTarget);
};
const closeToolsMenu = () => {
setToolsAnchorEl(null);
};
const closeToolDialog = () => {
setToolDialogOpen(false);
setToolLoading(false);
};
const runSectionTool = async (tool: 'originality' | 'optimize' | 'fact' | 'links' | 'flow') => {
closeToolsMenu();
const runSectionTool = useCallback(async (tool: 'originality' | 'optimize' | 'fact' | 'links' | 'flow') => {
setActiveTool(tool);
setToolResult(null);
setToolLoading(true);
setToolDialogOpen(true);
try {
let res;
if (tool === 'originality') {
const res = await blogWriterApi.sectionOriginalityTools({
section_id: String(id),
title: sectionTitle,
content
res = await blogWriterApi.sectionOriginalityTools({ section_id: String(id), title: sectionTitle, content });
} else if (tool === 'links') {
res = await blogWriterApi.sectionInternalLinkTools({ section_id: String(id), title: sectionTitle, content });
} else if (tool === 'fact') {
res = await blogWriterApi.sectionFactCheckTools({ section_id: String(id), title: sectionTitle, content });
} else if (tool === 'optimize') {
res = await blogWriterApi.sectionOptimizeTools({
section_id: String(id), title: sectionTitle, content,
keywords: outlineData?.keywords || [], goal: 'readability'
});
setToolResult(res);
return;
}
if (tool === 'links') {
const res = await blogWriterApi.sectionInternalLinkTools({
section_id: String(id),
title: sectionTitle,
content
});
setToolResult(res);
return;
}
if (tool === 'fact') {
const res = await blogWriterApi.sectionFactCheckTools({
section_id: String(id),
title: sectionTitle,
content
});
setToolResult(res);
return;
}
if (tool === 'optimize') {
const res = await blogWriterApi.sectionOptimizeTools({
section_id: String(id),
title: sectionTitle,
content,
keywords: outlineData?.keywords || [],
goal: 'readability'
});
setToolResult(res);
return;
}
if (tool === 'flow') {
const res = await blogWriterApi.analyzeFlowAdvanced({
} else if (tool === 'flow') {
res = await blogWriterApi.analyzeFlowAdvanced({
title: sectionTitle,
sections: [{ id: String(id), heading: sectionTitle, content }]
});
setToolResult(res);
return;
}
setToolResult(res);
} catch (error: any) {
setToolResult({ success: false, error: error?.message || 'Request failed' });
} finally {
setToolLoading(false);
}
};
}, [id, sectionTitle, content, outlineData]);
const applyOptimizedContent = () => {
const next = toolResult?.optimized_content;
if (!next) return;
setContent(next);
if (onContentUpdate) {
onContentUpdate([{ id, content: next }]);
}
if (onContentUpdate) onContentUpdate([{ id, content: next }]);
closeToolDialog();
};
const insertLinkSuggestion = (url: string) => {
if (!url) return;
const insertion = `\n\n[Related](${url})`;
const next = `${content || ''}${insertion}`;
const next = `${content || ''}\n\n[Related](${url})`;
setContent(next);
if (onContentUpdate) {
onContentUpdate([{ id, content: next }]);
}
if (onContentUpdate) onContentUpdate([{ id, content: next }]);
};
const handleGenerateContent = async () => {
setIsGenerating(true);
try {
// This would call your AI service for content generation
await new Promise(resolve => setTimeout(resolve, 2000));
const generated = `This is AI-generated content for "${sectionTitle}" with engaging, well-structured paragraphs grounded in your research.`;
setContent(generated);
// Update parent state if needed
if (onContentUpdate) {
onContentUpdate([{ id, content: generated }]);
}
} catch (error) {
console.error('Failed to generate content:', error);
} finally {
setIsGenerating(false);
}
await new Promise(resolve => setTimeout(resolve, 2000));
setIsGenerating(false);
};
// HoverMenu action handler
const handleSectionAction = useCallback((action: string) => {
switch (action) {
case 'generate-content':
handleGenerateContent();
break;
case 'enhance-section':
runSectionTool('optimize');
break;
case 'fact-check':
runSectionTool('fact');
break;
case 'source-mapping':
runSectionTool('originality');
break;
case 'seo-analysis':
runSectionTool('flow');
break;
case 'add-subsection':
break;
case 'copy-section':
break;
case 'delete-section':
break;
default:
break;
}
}, []);
return (
<div
className="group relative mb-6"
className="group relative mb-8"
id={`section-${id}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="flex items-center gap-3 mb-4">
<span className="text-xs font-medium text-gray-300 select-none">{id}.</span>
{isEditing ? (
<TextField
fullWidth
@@ -286,182 +212,117 @@ const BlogSection: React.FC<BlogSectionProps> = ({
value={sectionTitle}
onChange={(e) => setSectionTitle(e.target.value)}
onBlur={() => setIsEditing(false)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === 'Escape') setIsEditing(false); }}
autoFocus
InputProps={{ className: 'text-2xl md:text-3xl font-bold !font-serif text-gray-800' }}
InputProps={{ disableUnderline: true, className: 'text-xl md:text-2xl font-bold font-serif text-gray-800' }}
/>
) : (
<h2
className="text-2xl md:text-3xl font-bold font-serif text-gray-800 cursor-pointer"
className="flex-1 text-xl md:text-2xl font-bold font-serif text-gray-800 cursor-text hover:text-indigo-600 transition-colors duration-150"
onClick={() => setIsEditing(true)}
>
{sectionTitle}
</h2>
)}
</div>
{/* Section Image Display */}
{sectionImage && (
<div style={{ marginBottom: '16px', marginTop: '8px' }}>
<div style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden',
maxWidth: '100%',
backgroundColor: '#fff'
}}>
<div className="mb-4">
<div className="rounded-lg overflow-hidden border border-gray-100 bg-white">
<img
src={`data:image/png;base64,${sectionImage}`}
alt={`Cover image for ${sectionTitle}`}
style={{
width: '100%',
height: 'auto',
display: 'block',
maxHeight: '400px',
objectFit: 'contain'
}}
className="w-full h-auto max-h-96 object-contain"
/>
</div>
</div>
)}
{isGenerating ? (
<div className="flex items-center justify-center p-8 bg-gray-50 rounded-lg animate-pulse">
<span className="text-gray-500 font-medium">Generating content based on research...</span>
</div>
) : (
<div className="relative">
{/* Image Placeholder */}
{outlineData?.keywords && outlineData.keywords.length > 0 && (
<div className="absolute -right-4 top-0 opacity-0 group-hover:opacity-100 transition-opacity">
<Tooltip title="Section image coming soon">
<IconButton size="small">
<img
src={`https://source.unsplash.com/random/800x600?${outlineData.keywords[0]}`}
alt=""
className="w-8 h-8 rounded object-cover"
/>
</IconButton>
</Tooltip>
</div>
)}
<TextField
multiline
fullWidth
variant="outlined"
placeholder="Start writing or use AI to generate content..."
value={content}
onChange={handleContentChange}
onFocus={handleFocus}
onBlur={handleBlur}
minRows={6}
InputProps={{
className: `font-serif text-lg leading-relaxed text-gray-700 p-0 border-none ${isFocused ? 'bg-white' : 'bg-transparent'} transition-colors duration-200`,
style: { lineHeight: '1.8' }
}}
sx={{
'& .MuiOutlinedInput-notchedOutline': { border: 'none' },
'& .MuiOutlinedInput-root': { padding: 0 }
}}
/>
</div>
)}
{isGenerating ? (
<div className="flex items-center gap-3 p-6 bg-indigo-50/50 rounded-lg border border-indigo-100/50 mb-3">
<CircularProgress size={20} className="text-indigo-400" />
<span className="text-sm text-indigo-600 font-medium">Generating content...</span>
</div>
) : (
<div className="relative">
<TextField
multiline
fullWidth
variant="outlined"
placeholder="Start writing..."
value={content}
onChange={handleContentChange}
onFocus={handleFocus}
onBlur={handleBlur}
onSelect={assistiveWriting.handleTextSelection}
inputRef={contentRef}
minRows={5}
InputProps={{
className: `font-serif text-base leading-relaxed text-gray-700 p-0 ${isFocused ? 'bg-white' : 'bg-transparent'}`,
style: { lineHeight: '1.8' }
}}
sx={{
'& .MuiOutlinedInput-notchedOutline': { border: 'none' },
'& .MuiOutlinedInput-root': { padding: 0 },
'& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline': { border: 'none' },
'& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' },
}}
/>
</div>
)}
{/* Outline Information Section */}
{/* Outline info */}
{outlineData && expandedSections.has(id) && (
<div className="mt-4">
<Paper elevation={0} sx={{ p: 2, bgcolor: '#f8f9fa', borderRadius: 2, mb: 2 }}>
<div className="flex flex-col gap-4">
{/* Key Points */}
{outlineData.keyPoints && outlineData.keyPoints.length > 0 && (
<div className="mt-3 mb-2">
<Paper elevation={0} sx={{ p: 3, bgcolor: '#f8f9fa', borderRadius: 2, border: '1px solid #f0f0f0' }}>
<div className="grid grid-cols-2 gap-4">
{outlineData.keyPoints?.length > 0 && (
<div>
<div className="text-sm font-bold text-blue-600 mb-2">Key Points:</div>
<div className="flex flex-wrap gap-1">
{outlineData.keyPoints.map((point: any, index: any) => (
<Chip
key={index}
label={point}
size="small"
variant="outlined"
sx={{ fontSize: '0.75rem' }}
/>
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Key Points</div>
<div className="flex flex-wrap gap-1.5">
{outlineData.keyPoints.map((point: any, i: any) => (
<Chip key={i} label={point} size="small" variant="outlined" sx={{ fontSize: '0.7rem', height: 24 }} />
))}
</div>
</div>
)}
{/* Subheadings */}
{outlineData.subheadings && outlineData.subheadings.length > 0 && (
{outlineData.subheadings?.length > 0 && (
<div>
<div className="text-sm font-bold text-blue-600 mb-2">Subheadings:</div>
<div className="flex flex-wrap gap-1">
{outlineData.subheadings.map((subheading: any, index: any) => (
<Chip
key={index}
label={subheading}
size="small"
variant="outlined"
color="secondary"
sx={{ fontSize: '0.75rem' }}
/>
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Subheadings</div>
<div className="flex flex-wrap gap-1.5">
{outlineData.subheadings.map((sub: any, i: any) => (
<Chip key={i} label={sub} size="small" variant="outlined" color="secondary" sx={{ fontSize: '0.7rem', height: 24 }} />
))}
</div>
</div>
)}
{/* Keywords */}
{outlineData.keywords && outlineData.keywords.length > 0 && (
<div>
<div className="text-sm font-bold text-blue-600 mb-2">Keywords:</div>
<div className="flex flex-wrap gap-1">
{outlineData.keywords.map((keyword: any, index: any) => (
<Chip
key={index}
label={keyword}
size="small"
variant="filled"
color="primary"
sx={{ fontSize: '0.75rem' }}
/>
))}
</div>
</div>
)}
{/* Target Words */}
{outlineData.targetWords > 0 && (
<div>
<div className="text-sm font-bold text-blue-600 mb-2">
Target Words: {outlineData.targetWords}
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Target words</div>
<div className="text-sm text-gray-700">{outlineData.targetWords}</div>
</div>
)}
{outlineData.keywords?.length > 0 && (
<div>
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Keywords</div>
<div className="flex flex-wrap gap-1.5">
{outlineData.keywords.map((kw: any, i: any) => (
<Chip key={i} label={kw} size="small" variant="filled" color="primary" sx={{ fontSize: '0.7rem', height: 24 }} />
))}
</div>
</div>
)}
{/* References */}
{outlineData.references && outlineData.references.length > 0 && (
<div>
<div className="text-sm font-bold text-blue-600 mb-2">
References ({outlineData.references.length}):
{outlineData.references?.length > 0 && (
<div className="col-span-2">
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
References ({outlineData.references.length})
</div>
<div className="flex flex-wrap gap-1">
{outlineData.references.slice(0, 3).map((ref: any, index: any) => (
<Chip
key={index}
label={ref.title || `Source ${index + 1}`}
size="small"
variant="outlined"
color="info"
sx={{ fontSize: '0.75rem' }}
/>
<div className="flex flex-wrap gap-1.5">
{outlineData.references.slice(0, 3).map((ref: any, i: any) => (
<Chip key={i} label={ref.title || `Source ${i + 1}`} size="small" variant="outlined" color="info" sx={{ fontSize: '0.7rem', height: 24 }} />
))}
{outlineData.references.length > 3 && (
<Chip
label={`+${outlineData.references.length - 3} more`}
size="small"
variant="outlined"
sx={{ fontSize: '0.75rem' }}
/>
<Chip label={`+${outlineData.references.length - 3} more`} size="small" variant="outlined" sx={{ fontSize: '0.7rem', height: 24 }} />
)}
</div>
</div>
@@ -471,180 +332,156 @@ const BlogSection: React.FC<BlogSectionProps> = ({
</div>
)}
<div className="absolute -bottom-4 right-0 flex items-center space-x-1" style={{ opacity: isHovered ? 1 : 0, transition: 'opacity 0.3s' }}>
<Chip label={`${content.split(' ').length} words`} size="small" variant="outlined" className="!text-gray-500" />
<Chip icon={<LinkIcon />} label={`${sources} sources`} size="small" variant="outlined" className="!text-gray-500" />
{outlineData && (
<Chip
icon={expandedSections.has(id) ? <ExpandLessIcon /> : <ExpandMoreIcon />}
label="Outline Info"
size="small"
variant="outlined"
clickable
onClick={() => toggleSectionExpansion(id)}
sx={{
fontSize: '0.75rem',
'&:hover': {
backgroundColor: 'rgba(25, 118, 210, 0.08)',
}
}}
/>
)}
<Tooltip title="Generate Content">
<IconButton size="small" onClick={handleGenerateContent}>
<AutoAwesomeIcon fontSize="small" />
{/* Bottom toolbar */}
<div className="flex items-center justify-between mt-2">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400">{wordCount_} words</span>
{outlineData?.targetWords && outlineData.targetWords > 0 && (
<>
<span className="text-gray-300 text-xs">/</span>
<span className="text-xs text-gray-400">{outlineData.targetWords} target</span>
</>
)}
</div>
<div className="flex items-center gap-0.5" style={{ opacity: isHovered ? 1 : 0, transition: 'opacity 0.2s' }}>
{outlineData && (
<Tooltip title={expandedSections.has(id) ? 'Hide outline info' : 'Show outline info'}>
<IconButton size="small" onClick={() => toggleSectionExpansion(id)} sx={{ width: 28, height: 28 }}>
{expandedSections.has(id) ? <ExpandLessIcon sx={{ fontSize: 16 }} /> : <ExpandMoreIcon sx={{ fontSize: 16 }} />}
</IconButton>
</Tooltip>
)}
<Tooltip title="Section actions">
<IconButton size="small" onClick={(e) => setToolsAnchorEl(e.currentTarget)} sx={{ width: 28, height: 28 }}>
<MoreHorizIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
<IconButton size="small" sx={{ width: 28, height: 28 }}>
<FileCopyOutlinedIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
{/* Flow Analysis Badge - Enabled when flow analysis results are available */}
<ContinuityBadge
sectionId={id}
refreshToken={refreshToken}
disabled={!flowAnalysisResults}
flowAnalysisResults={flowAnalysisResults}
/>
<Tooltip title="Section Tools">
<IconButton size="small" onClick={openToolsMenu}>
<InfoIcon fontSize="small" />
<IconButton size="small" sx={{ width: 28, height: 28 }}>
<DeleteOutlineIcon sx={{ fontSize: 16, color: '#9ca3af' }} />
</IconButton>
</Tooltip>
<Tooltip title="Copy Section"><IconButton size="small"><FileCopyOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Edit Metadata"><IconButton size="small"><EditIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Delete Section"><IconButton size="small" className="text-red-500"><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
</div>
</div>
<Menu
{/* HoverMenu for section-level actions */}
<HoverMenu
anchorEl={toolsAnchorEl}
open={Boolean(toolsAnchorEl)}
onClose={closeToolsMenu}
>
<MenuItem onClick={() => runSectionTool('originality')}>Originality Check</MenuItem>
<MenuItem onClick={() => runSectionTool('optimize')}>Optimize Section</MenuItem>
<MenuItem onClick={() => runSectionTool('fact')}>SIF Fact Check</MenuItem>
<MenuItem onClick={() => runSectionTool('links')}>Internal Link Suggestions</MenuItem>
<MenuItem onClick={() => runSectionTool('flow')}>Flow Analysis</MenuItem>
</Menu>
onClose={() => setToolsAnchorEl(null)}
type="section"
onAction={handleSectionAction}
context={{
sectionId: String(id),
hasContent: content.trim().length > 0,
sources,
wordCount: wordCount_,
}}
/>
<Dialog open={toolDialogOpen} onClose={closeToolDialog} fullWidth maxWidth="md">
<DialogTitle>
{activeTool === 'originality' && 'Originality Check'}
{activeTool === 'optimize' && 'Optimize Section'}
{activeTool === 'fact' && 'SIF Fact Check'}
{activeTool === 'links' && 'Internal Link Suggestions'}
{activeTool === 'flow' && 'Flow Analysis'}
</DialogTitle>
<DialogContent dividers>
{toolLoading && (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<CircularProgress size={18} />
<div>Working</div>
{/* Tool result dialog */}
{toolDialogOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/20" onClick={closeToolDialog}>
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-hidden flex flex-col" onClick={e => e.stopPropagation()}>
<div className="px-6 py-4 border-b border-gray-100">
<h3 className="text-lg font-semibold text-gray-800">
{activeTool === 'originality' && 'Originality Check'}
{activeTool === 'optimize' && 'Optimize Section'}
{activeTool === 'fact' && 'SIF Fact Check'}
{activeTool === 'links' && 'Internal Link Suggestions'}
{activeTool === 'flow' && 'Flow Analysis'}
</h3>
</div>
)}
{!toolLoading && toolResult?.error && (
<div style={{ color: '#b91c1c', fontWeight: 600 }}>{toolResult.error}</div>
)}
{!toolLoading && activeTool === 'optimize' && toolResult?.optimized_content && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{toolResult?.diff_summary && (
<div style={{ fontWeight: 600 }}>{toolResult.diff_summary}</div>
<div className="px-6 py-4 overflow-y-auto flex-1">
{toolLoading && (
<div className="flex items-center gap-3">
<CircularProgress size={18} />
<span className="text-sm text-gray-500">Working...</span>
</div>
)}
{Array.isArray(toolResult?.changes_made) && toolResult.changes_made.length > 0 && (
<List dense>
{toolResult.changes_made.map((c: string, idx: number) => (
<ListItem key={idx}>
<ListItemText primary={c} />
</ListItem>
))}
</List>
{!toolLoading && toolResult?.error && (
<div className="text-red-600 font-medium">{toolResult.error}</div>
)}
<TextField
multiline
minRows={10}
value={toolResult.optimized_content}
fullWidth
InputProps={{ readOnly: true }}
/>
</div>
)}
{!toolLoading && activeTool === 'links' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{Array.isArray(toolResult?.suggestions) && toolResult.suggestions.length > 0 ? (
<List>
{toolResult.suggestions.map((s: any, idx: number) => (
<ListItem key={idx} secondaryAction={
<Button size="small" onClick={() => insertLinkSuggestion(s.url)}>Insert</Button>
}>
<ListItemText
primary={s.url}
secondary={`confidence: ${(s.confidence ?? 0).toFixed?.(2) ?? s.confidence}${s.reason ?? ''}`}
/>
</ListItem>
))}
</List>
) : (
<div>No suggestions yet. Make sure SIF index has your website content.</div>
{!toolLoading && activeTool === 'optimize' && toolResult?.optimized_content && (
<div className="space-y-3">
{toolResult?.diff_summary && <p className="font-medium">{toolResult.diff_summary}</p>}
{Array.isArray(toolResult?.changes_made) && toolResult.changes_made.length > 0 && (
<ul className="list-disc pl-5 space-y-1">
{toolResult.changes_made.map((c: string, idx: number) => (
<li key={idx} className="text-sm text-gray-600">{c}</li>
))}
</ul>
)}
<TextField multiline minRows={10} value={toolResult.optimized_content} fullWidth InputProps={{ readOnly: true }} />
</div>
)}
{!toolLoading && activeTool === 'links' && (
<div className="space-y-2">
{Array.isArray(toolResult?.suggestions) && toolResult.suggestions.length > 0 ? (
toolResult.suggestions.map((s: any, idx: number) => (
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-700 truncate">{s.url}</p>
<p className="text-xs text-gray-500">confidence: {(s.confidence ?? 0).toFixed?.(2) ?? s.confidence}</p>
</div>
<button onClick={() => insertLinkSuggestion(s.url)} className="text-sm text-indigo-600 hover:text-indigo-800 ml-3 shrink-0">Insert</button>
</div>
))
) : (
<p className="text-sm text-gray-500">No suggestions yet. Make sure SIF index has your website content.</p>
)}
</div>
)}
{!toolLoading && activeTool === 'originality' && (
<div className="space-y-3">
{toolResult?.cannibalization && <pre className="text-sm whitespace-pre-wrap">{JSON.stringify(toolResult.cannibalization, null, 2)}</pre>}
{Array.isArray(toolResult?.matches) && toolResult.matches.length > 0 ? (
<div className="space-y-2">
{toolResult.matches.map((m: any, idx: number) => (
<div key={idx} className="p-3 bg-gray-50 rounded-lg">
<p className="text-sm font-medium">{m.id ?? 'unknown'} ({(m.score ?? 0).toFixed?.(3) ?? m.score})</p>
{m.excerpt && <p className="text-xs text-gray-500 mt-1">{m.excerpt}</p>}
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500">No close matches found.</p>
)}
</div>
)}
{!toolLoading && activeTool === 'fact' && (
<div className="space-y-3">
{toolResult?.verification && <pre className="text-sm whitespace-pre-wrap">{JSON.stringify(toolResult.verification, null, 2)}</pre>}
{Array.isArray(toolResult?.citations) && toolResult.citations.length > 0 && (
<div className="space-y-2">
{toolResult.citations.map((c: any, idx: number) => (
<div key={idx} className="p-3 bg-gray-50 rounded-lg">
<p className="text-sm">{c.citation_text || c.title || c.source}</p>
<p className="text-xs text-gray-500">{c.source}</p>
</div>
))}
</div>
)}
</div>
)}
{!toolLoading && activeTool === 'flow' && (
<pre className="text-sm whitespace-pre-wrap">{JSON.stringify(toolResult, null, 2)}</pre>
)}
</div>
)}
{!toolLoading && activeTool === 'originality' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{toolResult?.cannibalization && (
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(toolResult.cannibalization, null, 2)}</pre>
)}
{Array.isArray(toolResult?.matches) && toolResult.matches.length > 0 ? (
<List>
{toolResult.matches.map((m: any, idx: number) => (
<ListItem key={idx}>
<ListItemText
primary={`${m.id ?? 'unknown'} (${(m.score ?? 0).toFixed?.(3) ?? m.score})`}
secondary={m.excerpt}
/>
</ListItem>
))}
</List>
) : (
<div>No close matches found.</div>
<div className="px-6 py-3 border-t border-gray-100 flex justify-end gap-2">
<button onClick={closeToolDialog} className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 transition-colors">Close</button>
{activeTool === 'optimize' && toolResult?.optimized_content && (
<button onClick={applyOptimizedContent} className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition-colors">Replace Section Content</button>
)}
</div>
)}
{!toolLoading && activeTool === 'fact' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{toolResult?.verification && (
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(toolResult.verification, null, 2)}</pre>
)}
{Array.isArray(toolResult?.citations) && toolResult.citations.length > 0 && (
<List>
{toolResult.citations.map((c: any, idx: number) => (
<ListItem key={idx}>
<ListItemText primary={c.citation_text || c.title || c.source} secondary={c.source} />
</ListItem>
))}
</List>
)}
</div>
)}
{!toolLoading && activeTool === 'flow' && (
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(toolResult, null, 2)}</pre>
)}
</DialogContent>
<DialogActions>
<Button onClick={closeToolDialog}>Close</Button>
{activeTool === 'optimize' && toolResult?.optimized_content && (
<Button variant="contained" onClick={applyOptimizedContent}>Replace Section Content</Button>
)}
</DialogActions>
</Dialog>
</div>
</div>
)}
{/* Section Divider */}
<Divider sx={{ mt: 1.2, mb: 1, opacity: 0.3 }} />
<Divider sx={{ mt: 2, opacity: 0.2 }} />
{assistiveWriting.renderSelectionMenu()}
</div>
);
};