mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-11 17:16:57 -05:00
- Fix the clipboard functionality to work on non local hosts and https - Improvements in sockets on front-end and backend. Storing session in local browser storage for reconnect. Logic to prevent socket echos coausing rerender and performance issues. - Fixes and udpates to re-ordering logic in adding a new task, reordering items on the task table. - Allowing assignee to not be hardcoded enum. - Fix to Document Version Control (Improvements still needed in the Milkdown editor conversion to store in the docs. - Adding types to remove [any] typescript issues.
661 lines
24 KiB
TypeScript
661 lines
24 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { X, Clock, RotateCcw, Eye, Calendar, User, FileText, Diff, GitBranch, Layers, Plus, Minus, AlertTriangle } from 'lucide-react';
|
|
import { projectService } from '../../services/projectService';
|
|
import { Button } from '../ui/Button';
|
|
import { useToast } from '../../contexts/ToastContext';
|
|
|
|
interface Version {
|
|
id: string;
|
|
version_number: number;
|
|
change_summary: string;
|
|
change_type: string;
|
|
created_by: string;
|
|
created_at: string;
|
|
content: any;
|
|
document_id?: string;
|
|
}
|
|
|
|
interface VersionHistoryModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
projectId: string;
|
|
documentId?: string;
|
|
fieldName?: string;
|
|
onRestore?: () => void;
|
|
}
|
|
|
|
interface DiffLine {
|
|
type: 'added' | 'removed' | 'unchanged';
|
|
content: string;
|
|
lineNumber?: number;
|
|
}
|
|
|
|
interface RestoreConfirmModalProps {
|
|
isOpen: boolean;
|
|
versionNumber: number;
|
|
onConfirm: () => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
const RestoreConfirmModal: React.FC<RestoreConfirmModalProps> = ({
|
|
isOpen,
|
|
versionNumber,
|
|
onConfirm,
|
|
onCancel
|
|
}) => {
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[60]">
|
|
<div className="relative p-6 rounded-md backdrop-blur-md w-full max-w-md
|
|
bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30
|
|
border border-gray-200 dark:border-zinc-800/50
|
|
shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]
|
|
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-[2px]
|
|
before:rounded-t-[4px] before:bg-orange-500
|
|
before:shadow-[0_0_10px_2px_rgba(249,115,22,0.4)] dark:before:shadow-[0_0_20px_5px_rgba(249,115,22,0.7)]">
|
|
|
|
<div className="relative z-10">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="w-12 h-12 rounded-full bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center">
|
|
<AlertTriangle className="w-6 h-6 text-orange-600 dark:text-orange-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
|
Restore Version
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
This will create a new version
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-gray-700 dark:text-gray-300 mb-6">
|
|
Are you sure you want to restore to <span className="font-medium text-orange-600 dark:text-orange-400">version {versionNumber}</span>?
|
|
This will create a new version with the restored content.
|
|
</p>
|
|
|
|
<div className="flex justify-end gap-3">
|
|
<Button variant="ghost" onClick={onCancel}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
accentColor="orange"
|
|
onClick={onConfirm}
|
|
icon={<RotateCcw className="w-4 h-4" />}
|
|
>
|
|
Restore Version
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const VersionHistoryModal: React.FC<VersionHistoryModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
projectId,
|
|
documentId,
|
|
fieldName = 'docs',
|
|
onRestore
|
|
}) => {
|
|
const [versions, setVersions] = useState<Version[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [previewContent, setPreviewContent] = useState<any>(null);
|
|
const [previewVersion, setPreviewVersion] = useState<number | null>(null);
|
|
const [restoring, setRestoring] = useState<number | null>(null);
|
|
const [currentContent, setCurrentContent] = useState<any>(null);
|
|
const [viewMode, setViewMode] = useState<'diff' | 'rendered'>('diff');
|
|
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false);
|
|
const [versionToRestore, setVersionToRestore] = useState<number | null>(null);
|
|
|
|
const { showToast } = useToast();
|
|
|
|
useEffect(() => {
|
|
if (isOpen && projectId) {
|
|
loadVersionHistory();
|
|
loadCurrentContent();
|
|
}
|
|
}, [isOpen, projectId, fieldName, documentId]);
|
|
|
|
const loadVersionHistory = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const versionData = await projectService.getDocumentVersionHistory(projectId, fieldName);
|
|
|
|
// Filter versions by document if documentId is provided
|
|
let filteredVersions = versionData || [];
|
|
if (documentId) {
|
|
filteredVersions = versionData.filter((version: Version) => {
|
|
// Check if this version contains changes to the specific document
|
|
if (version.document_id === documentId) {
|
|
return true;
|
|
}
|
|
// Also check if the content contains the document
|
|
if (Array.isArray(version.content)) {
|
|
return version.content.some((doc: any) => doc.id === documentId);
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
setVersions(filteredVersions);
|
|
} catch (error) {
|
|
console.error('Error loading version history:', error);
|
|
setError('Failed to load version history');
|
|
showToast('Failed to load version history', 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadCurrentContent = async () => {
|
|
try {
|
|
const currentProject = await projectService.getProject(projectId);
|
|
setCurrentContent((currentProject as any)[fieldName] || []);
|
|
} catch (error) {
|
|
console.error('Error loading current content:', error);
|
|
showToast('Failed to load current content', 'error');
|
|
}
|
|
};
|
|
|
|
const handlePreview = async (versionNumber: number) => {
|
|
try {
|
|
setPreviewVersion(versionNumber);
|
|
const contentData = await projectService.getVersionContent(projectId, versionNumber, fieldName);
|
|
setPreviewContent(contentData.content);
|
|
} catch (error) {
|
|
console.error('Error loading version content:', error);
|
|
setError('Failed to load version content');
|
|
showToast('Failed to load version content', 'error');
|
|
}
|
|
};
|
|
|
|
const handleRestoreClick = (versionNumber: number) => {
|
|
setVersionToRestore(versionNumber);
|
|
setShowRestoreConfirm(true);
|
|
};
|
|
|
|
const handleRestoreConfirm = async () => {
|
|
if (!versionToRestore) return;
|
|
|
|
setRestoring(versionToRestore);
|
|
setError(null);
|
|
setShowRestoreConfirm(false);
|
|
|
|
try {
|
|
await projectService.restoreDocumentVersion(projectId, versionToRestore, fieldName);
|
|
await loadVersionHistory();
|
|
await loadCurrentContent();
|
|
|
|
if (onRestore) {
|
|
onRestore();
|
|
}
|
|
|
|
showToast(`Successfully restored to version ${versionToRestore}`, 'success');
|
|
} catch (error) {
|
|
console.error('Error restoring version:', error);
|
|
setError('Failed to restore version');
|
|
showToast('Failed to restore version', 'error');
|
|
} finally {
|
|
setRestoring(null);
|
|
setVersionToRestore(null);
|
|
}
|
|
};
|
|
|
|
const handleRestoreCancel = () => {
|
|
setShowRestoreConfirm(false);
|
|
setVersionToRestore(null);
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleString();
|
|
};
|
|
|
|
const getChangeTypeIcon = (changeType: string) => {
|
|
switch (changeType) {
|
|
case 'create':
|
|
return <FileText className="w-4 h-4 text-emerald-400" />;
|
|
case 'update':
|
|
return <Clock className="w-4 h-4 text-blue-400" />;
|
|
case 'delete':
|
|
return <X className="w-4 h-4 text-red-400" />;
|
|
case 'restore':
|
|
return <RotateCcw className="w-4 h-4 text-purple-400" />;
|
|
default:
|
|
return <Clock className="w-4 h-4 text-gray-400" />;
|
|
}
|
|
};
|
|
|
|
const extractTextContent = (content: any, docId?: string): string => {
|
|
if (!content) return '';
|
|
|
|
// If content is an array of documents
|
|
if (Array.isArray(content)) {
|
|
// If we have a documentId, filter to just that document
|
|
if (docId) {
|
|
const doc = content.find(d => d.id === docId);
|
|
if (doc) {
|
|
// If it has markdown content, return that
|
|
if (doc.content?.markdown) {
|
|
return doc.content.markdown;
|
|
}
|
|
// Otherwise try to extract text content
|
|
return extractDocumentText(doc);
|
|
}
|
|
return 'Document not found in this version';
|
|
}
|
|
// Otherwise show all documents
|
|
return content.map(doc => {
|
|
if (doc.content?.markdown) {
|
|
return `=== ${doc.title || 'Document'} ===\n${doc.content.markdown}`;
|
|
}
|
|
return `=== ${doc.title || 'Document'} ===\n${extractDocumentText(doc)}`;
|
|
}).join('\n\n');
|
|
}
|
|
|
|
// If content is an object with markdown
|
|
if (typeof content === 'object' && content.markdown) {
|
|
return content.markdown;
|
|
}
|
|
|
|
if (typeof content === 'object') {
|
|
return JSON.stringify(content, null, 2);
|
|
}
|
|
|
|
return String(content);
|
|
};
|
|
|
|
const extractDocumentText = (doc: any): string => {
|
|
let text = '';
|
|
if (doc.blocks) {
|
|
text = doc.blocks.map((block: any) => {
|
|
if (block.content) {
|
|
return block.content;
|
|
}
|
|
return '';
|
|
}).filter(Boolean).join('\n');
|
|
} else if (doc.content && typeof doc.content === 'string') {
|
|
text = doc.content;
|
|
} else if (doc.content && typeof doc.content === 'object') {
|
|
text = JSON.stringify(doc.content, null, 2);
|
|
}
|
|
return text;
|
|
};
|
|
|
|
const generateDiff = (oldContent: any, newContent: any): DiffLine[] => {
|
|
const oldText = extractTextContent(oldContent, documentId);
|
|
const newText = extractTextContent(newContent, documentId);
|
|
|
|
const oldLines = oldText.split('\n');
|
|
const newLines = newText.split('\n');
|
|
|
|
const diff: DiffLine[] = [];
|
|
|
|
// Simple line-by-line diff (in a real app you'd use a proper diff library)
|
|
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
|
|
for (let i = 0; i < maxLines; i++) {
|
|
const oldLine = oldLines[i] || '';
|
|
const newLine = newLines[i] || '';
|
|
|
|
if (oldLine === newLine) {
|
|
if (oldLine) {
|
|
diff.push({ type: 'unchanged', content: oldLine, lineNumber: i + 1 });
|
|
}
|
|
} else {
|
|
if (oldLine && !newLines.includes(oldLine)) {
|
|
diff.push({ type: 'removed', content: oldLine, lineNumber: i + 1 });
|
|
}
|
|
if (newLine && !oldLines.includes(newLine)) {
|
|
diff.push({ type: 'added', content: newLine, lineNumber: i + 1 });
|
|
}
|
|
}
|
|
}
|
|
|
|
return diff;
|
|
};
|
|
|
|
const renderInlineDiff = () => {
|
|
if (!previewContent || !currentContent) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<Diff className="w-16 h-16 text-gray-600 mx-auto mb-4 opacity-50" />
|
|
<p className="text-gray-500 text-lg">Select a version to see changes</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const diffLines = generateDiff(previewContent, currentContent);
|
|
|
|
// If filtering by document but no changes found
|
|
if (documentId && diffLines.length === 0) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<FileText className="w-16 h-16 text-gray-600 mx-auto mb-4 opacity-50" />
|
|
<p className="text-gray-500 text-lg">No changes found for this document in the selected version</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-sm text-gray-400 mb-4">
|
|
<Diff className="w-4 h-4" />
|
|
Comparing Version {previewVersion} → Current
|
|
</div>
|
|
|
|
<div className="bg-gray-900/50 rounded-lg border border-gray-700/50 overflow-hidden">
|
|
<div className="bg-gray-800/50 px-4 py-2 border-b border-gray-700/50">
|
|
<span className="text-gray-300 text-sm font-mono">Changes</span>
|
|
</div>
|
|
|
|
<div className="max-h-96 overflow-y-auto font-mono text-sm">
|
|
{diffLines.map((line, index) => (
|
|
<div
|
|
key={index}
|
|
className={`flex items-start px-4 py-1 ${
|
|
line.type === 'added'
|
|
? 'bg-green-500/10 border-l-2 border-green-500'
|
|
: line.type === 'removed'
|
|
? 'bg-red-500/10 border-l-2 border-red-500'
|
|
: 'hover:bg-gray-800/30'
|
|
}`}
|
|
>
|
|
<span className="text-gray-500 w-8 text-right mr-4 select-none">
|
|
{line.lineNumber}
|
|
</span>
|
|
<span className="mr-3 w-4 flex-shrink-0">
|
|
{line.type === 'added' && <Plus className="w-3 h-3 text-green-400" />}
|
|
{line.type === 'removed' && <Minus className="w-3 h-3 text-red-400" />}
|
|
</span>
|
|
<span className={`flex-1 ${
|
|
line.type === 'added'
|
|
? 'text-green-300'
|
|
: line.type === 'removed'
|
|
? 'text-red-300'
|
|
: 'text-gray-300'
|
|
}`}>
|
|
{line.content || ' '}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderDocumentContent = (content: any) => {
|
|
if (!content) return <div className="text-gray-500 text-center py-8">No content available</div>;
|
|
|
|
// Extract the markdown content for the specific document
|
|
const markdownContent = extractTextContent(content, documentId);
|
|
|
|
if (markdownContent === 'Document not found in this version') {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<FileText className="w-16 h-16 text-gray-600 mx-auto mb-4 opacity-50" />
|
|
<p className="text-gray-500 text-lg">Document not found in this version</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Render the markdown content
|
|
return (
|
|
<div className="prose prose-invert max-w-none">
|
|
<pre className="whitespace-pre-wrap bg-gray-900/50 p-6 rounded-lg border border-gray-700/50 text-gray-300 font-mono text-sm overflow-auto">
|
|
{markdownContent}
|
|
</pre>
|
|
</div>
|
|
);
|
|
|
|
// Old array handling code - keeping for reference but not using
|
|
if (Array.isArray(content) && false) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{content.map((doc, index) => (
|
|
<div key={index} className="border border-gray-700/50 rounded-lg p-4 bg-gray-900/30">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<FileText className="w-5 h-5 text-blue-400" />
|
|
<h3 className="text-lg font-semibold text-white">{doc.title || `Document ${index + 1}`}</h3>
|
|
</div>
|
|
{doc.blocks && (
|
|
<div className="prose prose-invert max-w-none">
|
|
{doc.blocks.map((block: any, blockIndex: number) => (
|
|
<div key={blockIndex} className="mb-4">
|
|
{block.type === 'heading_1' && (
|
|
<h1 className="text-2xl font-bold text-white mb-2">{block.content}</h1>
|
|
)}
|
|
{block.type === 'heading_2' && (
|
|
<h2 className="text-xl font-semibold text-white mb-2">{block.content}</h2>
|
|
)}
|
|
{block.type === 'heading_3' && (
|
|
<h3 className="text-lg font-medium text-white mb-2">{block.content}</h3>
|
|
)}
|
|
{block.type === 'paragraph' && (
|
|
<p className="text-gray-300 leading-relaxed mb-2">{block.content}</p>
|
|
)}
|
|
{block.type === 'bulletListItem' && (
|
|
<ul className="list-disc list-inside text-gray-300 mb-2">
|
|
<li>{block.content}</li>
|
|
</ul>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (typeof content === 'object') {
|
|
return (
|
|
<div className="border border-gray-700/50 rounded-lg p-4 bg-gray-900/30">
|
|
<pre className="text-sm text-gray-300 whitespace-pre-wrap overflow-x-auto">
|
|
{JSON.stringify(content, null, 2)}
|
|
</pre>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <div className="text-gray-500">Unsupported content type</div>;
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50">
|
|
<div className="relative w-full max-w-6xl h-5/6 flex flex-col bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border border-purple-500/30 rounded-lg overflow-hidden shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)] backdrop-blur-md">
|
|
{/* Neon top edge */}
|
|
<div className="absolute top-0 left-0 right-0 h-[2px] bg-purple-500 shadow-[0_0_20px_5px_rgba(168,85,247,0.7)]"></div>
|
|
|
|
{/* Header */}
|
|
<div className="relative z-10 flex items-center justify-between p-6 border-b border-purple-500/30">
|
|
<h2 className="text-xl font-semibold text-white flex items-center gap-3">
|
|
<Clock className="w-6 h-6 text-purple-400" />
|
|
Version History
|
|
<span className="text-purple-400">- {fieldName}{documentId ? ' (Document Filtered)' : ''}</span>
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 rounded-lg hover:bg-white/10 transition-all duration-300"
|
|
>
|
|
<X className="w-6 h-6 text-gray-400 hover:text-red-400" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 flex overflow-hidden relative z-10">
|
|
{/* Version List */}
|
|
<div className="w-1/3 border-r border-purple-500/30 overflow-y-auto">
|
|
<div className="p-6">
|
|
<h3 className="font-medium text-white mb-4 flex items-center gap-2">
|
|
<GitBranch className="w-5 h-5 text-purple-400" />
|
|
Versions
|
|
</h3>
|
|
|
|
{loading && (
|
|
<div className="text-center py-8">
|
|
<div className="w-8 h-8 border-2 border-purple-500/30 border-t-purple-500 rounded-full animate-spin mx-auto mb-4"></div>
|
|
<p className="text-gray-400">Loading versions...</p>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mb-4">
|
|
<p className="text-red-400 text-sm">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && versions.length === 0 && (
|
|
<div className="text-center py-8">
|
|
<Clock className="w-12 h-12 text-gray-600 mx-auto mb-4 opacity-50" />
|
|
<p className="text-gray-500">No versions found</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-3">
|
|
{versions.map((version) => (
|
|
<div
|
|
key={version.id}
|
|
className={`relative p-4 rounded-lg cursor-pointer transition-all duration-300 border ${
|
|
previewVersion === version.version_number
|
|
? 'bg-blue-500/20 border-blue-500/50'
|
|
: 'bg-white/5 border-gray-500/30 hover:bg-white/10 hover:border-gray-400/50'
|
|
}`}
|
|
onClick={() => handlePreview(version.version_number)}
|
|
>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-3">
|
|
{getChangeTypeIcon(version.change_type)}
|
|
<span className="font-medium text-white">
|
|
Version {version.version_number}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
accentColor="green"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleRestoreClick(version.version_number);
|
|
}}
|
|
disabled={restoring === version.version_number}
|
|
icon={restoring === version.version_number ?
|
|
<div className="w-4 h-4 border-2 border-emerald-500/30 border-t-emerald-500 rounded-full animate-spin" /> :
|
|
<RotateCcw className="w-4 h-4" />
|
|
}
|
|
>
|
|
Restore
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-300 mb-3">
|
|
{version.change_summary}
|
|
{version.document_id && documentId && version.document_id === documentId && (
|
|
<span className="ml-2 text-xs bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded">
|
|
This document
|
|
</span>
|
|
)}
|
|
</p>
|
|
|
|
<div className="flex items-center gap-4 text-xs text-gray-400">
|
|
<div className="flex items-center gap-1.5">
|
|
<User className="w-3 h-3" />
|
|
{version.created_by}
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<Calendar className="w-3 h-3" />
|
|
{formatDate(version.created_at)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Preview Panel */}
|
|
<div className="flex-1 overflow-y-auto">
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="font-medium text-white flex items-center gap-3">
|
|
<Eye className="w-5 h-5 text-blue-400" />
|
|
{viewMode === 'diff' ? 'Changes' : 'Content'}
|
|
</h3>
|
|
|
|
{previewVersion !== null && (
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant={viewMode === 'diff' ? 'primary' : 'ghost'}
|
|
size="sm"
|
|
accentColor="purple"
|
|
onClick={() => setViewMode('diff')}
|
|
icon={<Diff className="w-4 h-4" />}
|
|
>
|
|
Diff View
|
|
</Button>
|
|
<Button
|
|
variant={viewMode === 'rendered' ? 'primary' : 'ghost'}
|
|
size="sm"
|
|
accentColor="blue"
|
|
onClick={() => setViewMode('rendered')}
|
|
icon={<Layers className="w-4 h-4" />}
|
|
>
|
|
Rendered
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{previewVersion === null ? (
|
|
<div className="text-center py-12">
|
|
<Eye className="w-16 h-16 text-gray-600 mx-auto mb-6 opacity-50" />
|
|
<p className="text-gray-500 text-lg">Select a version to preview</p>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
{viewMode === 'diff' ? renderInlineDiff() : renderDocumentContent(previewContent)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="relative z-10 border-t border-purple-500/30 p-6">
|
|
<div className="flex justify-end">
|
|
<Button
|
|
variant="ghost"
|
|
accentColor="purple"
|
|
onClick={onClose}
|
|
>
|
|
Close
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Restore Confirmation Modal */}
|
|
<RestoreConfirmModal
|
|
isOpen={showRestoreConfirm}
|
|
versionNumber={versionToRestore || 0}
|
|
onConfirm={handleRestoreConfirm}
|
|
onCancel={handleRestoreCancel}
|
|
/>
|
|
</>
|
|
);
|
|
};
|