wu981526092's picture
🚀 Deploy AgentGraph: Complete agent monitoring and knowledge graph system
c2ea5ed
/**
* Context Document Modal Component
*
* Modal for viewing and editing context documents with markdown rendering support
*/
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Edit,
Save,
Download,
FileText,
Trash2,
X,
Calendar,
AlertCircle,
} from "lucide-react";
import {
ContextDocument,
ContextDocumentType,
CONTEXT_DOCUMENT_TYPES,
} from "@/types/context";
import { shouldRenderAsMarkdown } from "@/lib/markdown-utils";
import Editor from "@monaco-editor/react";
import ReactMarkdown from "react-markdown";
interface ContextDocumentModalProps {
document: ContextDocument;
isOpen: boolean;
onClose: () => void;
onSave: (updates: {
title?: string;
content?: string;
document_type?: ContextDocumentType;
}) => Promise<boolean>;
onDelete: () => Promise<boolean>;
}
export function ContextDocumentModal({
document,
isOpen,
onClose,
onSave,
onDelete,
}: ContextDocumentModalProps) {
const [isEditing, setIsEditing] = useState(false);
const [editedTitle, setEditedTitle] = useState(document.title);
const [editedContent, setEditedContent] = useState(document.content);
const [editedType, setEditedType] = useState(document.document_type);
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isDirty, setIsDirty] = useState(false);
const [error, setError] = useState<string | null>(null);
// Update local state when document changes
useEffect(() => {
setEditedTitle(document.title);
setEditedContent(document.content);
setEditedType(document.document_type);
setIsDirty(false);
setError(null);
}, [document]);
// Check if content has changed
useEffect(() => {
const hasChanges =
editedTitle !== document.title ||
editedContent !== document.content ||
editedType !== document.document_type;
setIsDirty(hasChanges);
}, [editedTitle, editedContent, editedType, document]);
const handleEdit = () => {
setIsEditing(true);
setError(null);
};
const handleSave = async () => {
if (!isDirty) return;
setIsSaving(true);
setError(null);
try {
const updates: any = {};
if (editedTitle !== document.title) updates.title = editedTitle;
if (editedContent !== document.content) updates.content = editedContent;
if (editedType !== document.document_type)
updates.document_type = editedType;
const success = await onSave(updates);
if (success) {
setIsEditing(false);
setIsDirty(false);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save document");
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
setEditedTitle(document.title);
setEditedContent(document.content);
setEditedType(document.document_type);
setIsEditing(false);
setIsDirty(false);
setError(null);
};
const handleDownload = () => {
const blob = new Blob([document.content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = window.document.createElement("a");
a.href = url;
a.download = `${document.title}.${
shouldRenderAsMarkdown(document.content, document.file_name)
? "md"
: "txt"
}`;
window.document.body.appendChild(a);
a.click();
window.document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleDelete = async () => {
if (
!window.confirm(
"Are you sure you want to delete this context document? This action cannot be undone."
)
) {
return;
}
setIsDeleting(true);
setError(null);
try {
const success = await onDelete();
if (success) {
onClose();
}
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to delete document"
);
} finally {
setIsDeleting(false);
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const formatDocumentType = (type: ContextDocumentType) => {
const typeConfig = CONTEXT_DOCUMENT_TYPES.find((t) => t.value === type);
return typeConfig?.label || type.replace(/_/g, " ");
};
const isMarkdown = shouldRenderAsMarkdown(
document.content,
document.file_name
);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 flex-1 min-w-0">
<FileText className="h-5 w-5 text-primary" />
{isEditing ? (
<Input
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
className="text-lg font-semibold"
placeholder="Document title"
/>
) : (
<DialogTitle className="truncate">{document.title}</DialogTitle>
)}
</div>
<div className="flex items-center gap-2">
{!isEditing ? (
<>
<Button variant="outline" size="sm" onClick={handleEdit}>
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
<Button variant="outline" size="sm" onClick={handleDownload}>
<Download className="h-4 w-4 mr-2" />
Download
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDelete}
disabled={isDeleting}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</>
) : (
<>
<Button variant="outline" size="sm" onClick={handleCancel}>
<X className="h-4 w-4 mr-2" />
Cancel
</Button>
<Button
size="sm"
onClick={handleSave}
disabled={!isDirty || isSaving}
>
<Save className="h-4 w-4 mr-2" />
{isSaving ? "Saving..." : "Save"}
</Button>
</>
)}
</div>
</div>
{/* Document metadata */}
<div className="flex flex-wrap gap-2 text-sm">
{isEditing ? (
<div className="flex items-center gap-2">
<Label htmlFor="document-type">Type:</Label>
<Select
value={editedType}
onValueChange={(value: ContextDocumentType) =>
setEditedType(value)
}
>
<SelectTrigger className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
{CONTEXT_DOCUMENT_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : (
<Badge variant="outline" className="flex items-center gap-1">
<FileText className="h-3 w-3" />
{formatDocumentType(document.document_type)}
</Badge>
)}
<Badge variant="outline" className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{formatDate(document.created_at)}
</Badge>
<Badge variant="outline">
{document.content.length.toLocaleString()} chars
</Badge>
</div>
{error && (
<div className="flex items-center gap-2 text-destructive text-sm p-3 bg-destructive/10 rounded-lg">
<AlertCircle className="h-4 w-4" />
{error}
</div>
)}
</DialogHeader>
<div className="flex-1 overflow-hidden flex flex-col">
{/* Single content area - conditional rendering based on editing state */}
<div className="flex-1 overflow-hidden">
{isEditing ? (
/* Edit Mode */
<div className="border rounded-lg h-full overflow-hidden">
<Editor
height="100%"
defaultLanguage={isMarkdown ? "markdown" : "plaintext"}
value={editedContent}
onChange={(value) => setEditedContent(value || "")}
theme="vs"
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: "on",
lineNumbers: "on",
folding: false,
renderWhitespace: "selection",
fontSize: 14,
lineHeight: 20,
fontFamily:
"'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace",
padding: { top: 10, bottom: 10 },
automaticLayout: true,
}}
/>
</div>
) : (
/* View Mode */
<div className="border rounded-lg h-full overflow-auto">
<div className="p-4">
{isMarkdown ? (
<div className="prose prose-sm max-w-none">
<ReactMarkdown>{document.content}</ReactMarkdown>
</div>
) : (
<pre className="whitespace-pre-wrap font-mono text-sm text-foreground">
{document.content}
</pre>
)}
</div>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}