"use client"; import { SidebarContent, ResizableSidebar } from "@/components/sidebar"; import { Editor } from "@/components/editor"; import { useEditorStore } from "@/lib/store"; import { Hash, X, Plus, ChevronLeft, ChevronRight, ChevronDown, Sparkles, Lock, Unlock, FolderOpen, History, RotateCcw } from "lucide-react"; import { useSettingsStore } from "@/lib/settings-store"; import { exportPageAsMarkdown } from "@/lib/export"; import { useMemo, useState } from "react"; import { useEffect } from "react"; import { cn, getTagColor } from "@/lib/utils"; import { useSearchStore } from "@/lib/search-store"; import { type Editor as TiptapEditor } from "@tiptap/react"; import { ImportProvider } from "@/components/import-context"; import { AIChatPanel } from "@/components/chat/ai-chat-panel"; import { SearchCommand } from "@/components/search-command"; import { getBacklinks, getOutgoingLinks, getUnresolvedLinkTitles } from "@/lib/wiki-links"; import { getPageHistory } from "@/lib/page-history"; const ICONS = [ "📄", "📝", "📒", "📘", "📙", "📕", "📗", "📚", "💡", "🔥", "✅", "❗", "⭐", "📌", "📎", "🧠", "🎯", "🚀", "⚙️", "🧪", "💼", "📊", "📈", "🧩", "🌱", "🌟", "🎨", "🎵", "🎬", "📷", "💬", "🔖", "⌛", "🗂️", "📦", "🏷️", "🗓️", "🕒", "🔍", "🧾", ]; export default function Home() { const { activePageId, pages, pageSync, updatePage, setActivePageId, addPage, retryPageSync, retryPendingSyncs, flushPendingSyncs } = useEditorStore(); const { openSearchWithTag } = useSearchStore(); const { timezone } = useSettingsStore(); const activePage = pages.find((p) => p.id === activePageId); const activePageSync = activePageId ? pageSync[activePageId] : undefined; const [isChatOpen, setIsChatOpen] = useState(false); const [editor, setEditor] = useState(null); const [isAddingTag, setIsAddingTag] = useState(false); const [tagInput, setTagInput] = useState(""); const [isIconPickerOpen, setIsIconPickerOpen] = useState(false); const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [selectedHistoryIndex, setSelectedHistoryIndex] = useState(0); const breadcrumbs = useMemo(() => { if (!activePage) return []; const result = [] as typeof pages; let current = activePage; while (current) { result.unshift(current); if (!current.parentId) break; const parent = pages.find((p) => p.id === current.parentId); if (!parent) break; current = parent; } return result; }, [activePage, pages]); const outgoingLinks = useMemo(() => { if (!activePage || activePage.type !== "file") return []; return getOutgoingLinks(activePage, pages); }, [activePage, pages]); const backlinks = useMemo(() => { if (!activePage || activePage.type !== "file") return []; return getBacklinks(activePage, pages); }, [activePage, pages]); const unresolvedLinks = useMemo(() => { if (!activePage || activePage.type !== "file") return []; return getUnresolvedLinkTitles(activePage, pages); }, [activePage, pages]); const localHistory = useMemo(() => { if (!activePage || activePage.type !== "file") return []; return getPageHistory(activePage.id); }, [activePage]); const effectiveHistoryIndex = Math.min(selectedHistoryIndex, Math.max(localHistory.length - 1, 0)); const addTag = () => { if (!activePage) return; const next = tagInput.trim(); if (!next) return; const merged = Array.from(new Set([...(activePage.tags || []), next])); updatePage(activePage.id, { tags: merged }); setTagInput(""); setIsAddingTag(false); }; const handleOpenWikiLink = async (title: string) => { const target = pages.find((p) => p.type === "file" && (p.title || "").trim() === title); if (target) { setActivePageId(target.id); return; } const shouldCreate = window.confirm(`未找到页面“${title}”。是否立即创建?`); if (!shouldCreate) return; await addPage(null, "file", { title, content: "" }); }; useEffect(() => { const onOnline = () => { retryPendingSyncs(); }; window.addEventListener("online", onOnline); return () => window.removeEventListener("online", onOnline); }, [retryPendingSyncs]); useEffect(() => { const flush = () => flushPendingSyncs(); const onVisibilityChange = () => { if (document.visibilityState === "hidden") { flush(); } }; window.addEventListener("beforeunload", flush); window.addEventListener("pagehide", flush); document.addEventListener("visibilitychange", onVisibilityChange); return () => { window.removeEventListener("beforeunload", flush); window.removeEventListener("pagehide", flush); document.removeEventListener("visibilitychange", onVisibilityChange); }; }, [flushPendingSyncs]); return (
{activePage ? (
{breadcrumbs.map((crumb, index) => (
{index > 0 && }
))}
{activePage.icon && {activePage.icon}} updatePage(activePage.id, { title: e.target.value })} placeholder="无标题" disabled={activePage.isLocked} className={cn( "w-full border-none bg-transparent text-2xl font-bold leading-tight text-foreground outline-none placeholder:text-muted-foreground/30 md:text-3xl", activePage.isLocked && "cursor-not-allowed select-none opacity-80" )} />
{activePage.tags?.map((tag) => { const colors = getTagColor(tag); return ( openSearchWithTag(tag)} className={cn( "group/tag inline-flex items-center gap-1 rounded-[4px] border px-2.5 py-1 text-[11px] font-medium shadow-sm", colors.bg, colors.text, colors.border )} > {tag} ); })} {isAddingTag ? ( setTagInput(e.target.value)} onBlur={addTag} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addTag(); } if (e.key === "Escape") { setTagInput(""); setIsAddingTag(false); } }} className="w-28 rounded-md border border-primary/40 bg-background px-2 py-1 text-xs outline-none focus:ring-2 focus:ring-primary/20" placeholder="输入标签" /> ) : ( )}
{isIconPickerOpen && (
{ICONS.map((icon) => ( ))}
)}
{activePageSync && (
{activePageSync.message || "已保存"}
{(activePageSync.status === "pending" || activePageSync.status === "error") && ( )}
)} {activePage.updatedAt && (
{new Date(activePage.updatedAt).toLocaleString("zh-CN", { timeZone: timezone || "Asia/Shanghai", hour12: false, })}
)}
{activePage.type === "folder" ? (

当前是文件夹

你可以在左侧新建文档,或把文档拖拽到此文件夹。

) : (
updatePage(activePage.id, { content })} onEditorReady={setEditor} onToggleAI={() => setIsChatOpen(!isChatOpen)} onExport={() => exportPageAsMarkdown(activePage)} onOpenWikiLink={handleOpenWikiLink} editable={!activePage.isLocked} />

关联页面

{outgoingLinks.length}

正文写 `[[页面名]]` 可自动关联。

{outgoingLinks.length > 0 ? ( outgoingLinks.map((item) => ( )) ) : ( 暂无 )}

反向链接

{backlinks.length}

被哪些页面引用。

{backlinks.length > 0 ? ( backlinks.map((item) => ( )) ) : ( 暂无 )}

未解析链接

{unresolvedLinks.length}

可一键创建缺失页面。

{unresolvedLinks.length > 0 ? ( unresolvedLinks.map((title) => ( )) ) : ( 全部已解析 )}

自动保存最近修改快照(当前浏览器本地)。

{isHistoryOpen && (
{localHistory.length > 0 ? ( <>
{localHistory[effectiveHistoryIndex]?.title || "无标题"}
{localHistory[effectiveHistoryIndex] ? new Date(localHistory[effectiveHistoryIndex].timestamp).toLocaleString("zh-CN", { timeZone: timezone || "Asia/Shanghai", hour12: false, }) : "-"}
) : ( 暂无历史记录 )}
)}
)}
) : (
🧠

欢迎使用 NoteAI

在左侧选择一个页面,或新建文档开始写作。

)}
setIsChatOpen(false)} />
); }