"use client"; import { useState, useMemo, useCallback, type ReactNode } from "react"; import { useWorkflowStore } from "@/store/workflowStore"; import { useShallow } from "zustand/shallow"; import { ProjectSetupModal } from "./ProjectSetupModal"; import { CostIndicator } from "./CostIndicator"; import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog"; import { WorkflowBrowserModal } from "./WorkflowBrowserModal"; import { LANGUAGE_OPTIONS, useI18n } from "@/i18n"; import { isBrowserFileSystemPath } from "@/utils/browserFileSystem"; const SHOW_UNUSED_HEADER_ACTIONS = false; function CommentsNavigationIcon() { const { t } = useI18n(); // Subscribe to nodes so we re-render when comments change const nodes = useWorkflowStore((state) => state.nodes); const getNodesWithComments = useWorkflowStore((state) => state.getNodesWithComments); const viewedCommentNodeIds = useWorkflowStore((state) => state.viewedCommentNodeIds); const markCommentViewed = useWorkflowStore((state) => state.markCommentViewed); const setNavigationTarget = useWorkflowStore((state) => state.setNavigationTarget); // Recalculate when nodes change (nodes in dependency triggers re-render) const nodesWithComments = useMemo(() => getNodesWithComments(), [getNodesWithComments, nodes]); const unviewedCount = useMemo(() => { return nodesWithComments.filter((node) => !viewedCommentNodeIds.has(node.id)).length; }, [nodesWithComments, viewedCommentNodeIds]); const totalCount = nodesWithComments.length; const handleClick = useCallback(() => { if (totalCount === 0) return; // Find first unviewed comment, or first comment if all viewed const targetNode = nodesWithComments.find((node) => !viewedCommentNodeIds.has(node.id)) || nodesWithComments[0]; if (targetNode) { markCommentViewed(targetNode.id); setNavigationTarget(targetNode.id); } }, [totalCount, nodesWithComments, viewedCommentNodeIds, markCommentViewed, setNavigationTarget]); // Don't render if no comments if (totalCount === 0) { return null; } const displayCount = unviewedCount > 9 ? "9+" : unviewedCount.toString(); return ( ); } function HeaderIconButton({ label, children, onClick, }: { label: string; children: ReactNode; onClick?: () => void; }) { return ( ); } export function Header() { const { language, setLanguage, t } = useI18n(); const { workflowName, workflowId, saveDirectoryPath, hasUnsavedChanges, lastSavedAt, isSaving, setWorkflowMetadata, saveToFile, loadWorkflow, previousWorkflowSnapshot, revertToSnapshot, shortcutsDialogOpen, setShortcutsDialogOpen, setShowQuickstart, } = useWorkflowStore(useShallow((state) => ({ workflowName: state.workflowName, workflowId: state.workflowId, saveDirectoryPath: state.saveDirectoryPath, hasUnsavedChanges: state.hasUnsavedChanges, lastSavedAt: state.lastSavedAt, isSaving: state.isSaving, setWorkflowMetadata: state.setWorkflowMetadata, saveToFile: state.saveToFile, loadWorkflow: state.loadWorkflow, previousWorkflowSnapshot: state.previousWorkflowSnapshot, revertToSnapshot: state.revertToSnapshot, shortcutsDialogOpen: state.shortcutsDialogOpen, setShortcutsDialogOpen: state.setShortcutsDialogOpen, setShowQuickstart: state.setShowQuickstart, }))); const [showProjectModal, setShowProjectModal] = useState(false); const [projectModalMode, setProjectModalMode] = useState<"new" | "settings">("new"); const [showWorkflowBrowser, setShowWorkflowBrowser] = useState(false); const isProjectConfigured = !!workflowName; const canSave = !!(workflowId && workflowName && saveDirectoryPath); const formatTime = (timestamp: number) => { return new Date(timestamp).toLocaleTimeString(language, { hour: "numeric", minute: "2-digit", }); }; const handleNewProject = () => { setProjectModalMode("new"); setShowProjectModal(true); }; const handleOpenSettings = () => { setProjectModalMode("settings"); setShowProjectModal(true); }; const handleOpenFile = () => { setShowWorkflowBrowser(true); }; const handleProjectSave = async (id: string, name: string, path: string) => { setWorkflowMetadata(id, name, path); // generationsPath is auto-derived setShowProjectModal(false); // Small delay to let state update setTimeout(() => { saveToFile().catch((error) => { console.error("Failed to save project:", error); alert(t("header.saveFailedRetry")); }); }, 50); }; const handleOpenDirectory = async () => { if (!saveDirectoryPath) return; if (isBrowserFileSystemPath(saveDirectoryPath)) { alert(t("header.openFolderFailed", { error: "Browser-selected folders cannot be opened from the server." })); return; } try { const response = await fetch("/api/open-directory", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ path: saveDirectoryPath }), }); const result = await response.json(); if (!response.ok || !result.success) { console.error("Failed to open directory:", result.error); alert(t("header.openFolderFailed", { error: result.error || "Unknown error" })); return; } } catch (error) { console.error("Failed to open directory:", error); alert(t("header.openFolderFailedRetry")); } }; const handleRevertAIChanges = useCallback(() => { const confirmed = window.confirm( t("header.revertConfirm") ); if (confirmed) { revertToSnapshot(); } }, [revertToSnapshot, t]); const settingsButtons = (