"use client"; import React, { useState, useEffect, useCallback } from "react"; import { useRouter, useParams } from "next/navigation"; import type { KnowledgeEntryWithTags, KnowledgeTag } from "@mosaic/shared"; import { EntryStatus, Visibility } from "@mosaic/shared"; import { EntryViewer } from "@/components/knowledge/EntryViewer"; import { EntryEditor } from "@/components/knowledge/EntryEditor"; import { EntryMetadata } from "@/components/knowledge/EntryMetadata"; import { EntryGraphViewer } from "@/components/knowledge/EntryGraphViewer"; import { fetchEntry, updateEntry, deleteEntry, fetchTags } from "@/lib/api/knowledge"; /** * Knowledge Entry Detail/Editor Page * View and edit mode for a single knowledge entry */ export default function EntryPage() { const router = useRouter(); const params = useParams(); const slug = params.slug as string; const [entry, setEntry] = useState(null); const [isEditing, setIsEditing] = useState(false); const [showGraph, setShowGraph] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); // Edit state const [editTitle, setEditTitle] = useState(""); const [editContent, setEditContent] = useState(""); const [editStatus, setEditStatus] = useState(EntryStatus.DRAFT); const [editVisibility, setEditVisibility] = useState(Visibility.WORKSPACE); const [editTags, setEditTags] = useState([]); const [availableTags, setAvailableTags] = useState([]); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // Load entry data useEffect(() => { async function loadEntry(): Promise { try { setIsLoading(true); const data = await fetchEntry(slug); setEntry(data); setEditTitle(data.title); setEditContent(data.content); setEditStatus(data.status); setEditVisibility(data.visibility); setEditTags(data.tags.map((tag: { id: string }) => tag.id)); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load entry"); } finally { setIsLoading(false); } } void loadEntry(); }, [slug]); // Load available tags useEffect(() => { async function loadTags(): Promise { try { const tags = await fetchTags(); setAvailableTags(tags); } catch (err) { console.error("Failed to load tags:", err); } } void loadTags(); }, []); // Track unsaved changes useEffect(() => { if (!entry || !isEditing) { setHasUnsavedChanges(false); return; } const changed = editTitle !== entry.title || editContent !== entry.content || editStatus !== entry.status || editVisibility !== entry.visibility || JSON.stringify(editTags.sort()) !== JSON.stringify(entry.tags.map((t: { id: string }) => t.id).sort()); setHasUnsavedChanges(changed); }, [entry, isEditing, editTitle, editContent, editStatus, editVisibility, editTags]); // Warn before leaving with unsaved changes useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent): string => { if (hasUnsavedChanges) { e.preventDefault(); return "You have unsaved changes. Are you sure you want to leave?"; } return ""; }; window.addEventListener("beforeunload", handleBeforeUnload); return () => window.removeEventListener("beforeunload", handleBeforeUnload); }, [hasUnsavedChanges]); // Save changes const handleSave = useCallback(async (): Promise => { if (!entry || isSaving || !editTitle.trim() || !editContent.trim()) { return; } setIsSaving(true); setError(null); try { const updated = await updateEntry(slug, { title: editTitle.trim(), content: editContent.trim(), status: editStatus, visibility: editVisibility, tags: editTags, }); setEntry(updated); setHasUnsavedChanges(false); setIsEditing(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to save changes"); } finally { setIsSaving(false); } }, [entry, slug, editTitle, editContent, editStatus, editVisibility, editTags, isSaving]); // Cmd+S / Ctrl+S to save useEffect(() => { const handleKeyDown = (e: KeyboardEvent): void => { if ((e.metaKey || e.ctrlKey) && e.key === "s" && isEditing) { e.preventDefault(); void handleSave(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [handleSave, isEditing]); const handleEdit = (): void => { if (!entry) return; setIsEditing(true); }; const handleCancel = (): void => { if (!entry) return; if ( !hasUnsavedChanges || confirm("You have unsaved changes. Are you sure you want to cancel?") ) { setEditTitle(entry.title); setEditContent(entry.content); setEditStatus(entry.status); setEditVisibility(entry.visibility); setEditTags(entry.tags.map((tag: { id: string }) => tag.id)); setIsEditing(false); setHasUnsavedChanges(false); } }; const handleDelete = async (): Promise => { if ( !confirm( "Are you sure you want to delete this entry? It will be archived and can be restored later." ) ) { return; } try { await deleteEntry(slug); router.push("/knowledge"); } catch (err) { setError(err instanceof Error ? err.message : "Failed to delete entry"); } }; if (isLoading) { return (
); } if (error && !entry) { return (

{error}

); } if (!entry) { return null; } return (
{/* Header */}
{isEditing ? (
) : ( <>

{entry.title}

{/* Status Badge */} {entry.status} {/* Visibility Badge */} {entry.visibility} {/* Tags */} {entry.tags.map((tag: { id: string; name: string; color: string | null }) => ( {tag.name} ))}
)}
{error && (

{error}

)} {/* View Tabs */} {!isEditing && (
)} {/* Content */}
{isEditing ? ( ) : showGraph ? (
) : ( )}
{/* Actions */}
{isEditing && ( )}
{isEditing ? ( <> ) : ( )}
{isEditing && (

Press Cmd+S{" "} or Ctrl+S to save

)}
); }