"use client"; import { useState, useRef, useMemo, useCallback } from "react"; import { useWorkflowStore, WorkflowFile } from "@/store/workflowStore"; import { useShallow } from "zustand/shallow"; import { ProjectSetupModal } from "./ProjectSetupModal"; import { CostIndicator } from "./CostIndicator"; import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog"; function CommentsNavigationIcon() { // 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 ( ); } export function Header() { 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 fileInputRef = useRef(null); const isProjectConfigured = !!workflowName; const canSave = !!(workflowId && workflowName && saveDirectoryPath); const formatTime = (timestamp: number) => { return new Date(timestamp).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", }); }; const handleNewProject = () => { setProjectModalMode("new"); setShowProjectModal(true); }; const handleOpenSettings = () => { setProjectModalMode("settings"); setShowProjectModal(true); }; const handleOpenFile = () => { fileInputRef.current?.click(); }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (event) => { try { const workflow = JSON.parse(event.target?.result as string) as WorkflowFile; if (workflow.version && workflow.nodes && workflow.edges) { await loadWorkflow(workflow); } else { alert("Invalid workflow file format"); } } catch { alert("Failed to parse workflow file"); } }; reader.readAsText(file); // Reset input so same file can be loaded again e.target.value = ""; }; 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("Failed to save project. Please try again."); }); }, 50); }; const handleOpenDirectory = async () => { if (!saveDirectoryPath) 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(`Failed to open project folder: ${result.error || "Unknown error"}`); return; } } catch (error) { console.error("Failed to open directory:", error); alert("Failed to open project folder. Please try again."); } }; const handleRevertAIChanges = useCallback(() => { const confirmed = window.confirm( "Are you sure? This will restore your previous workflow." ); if (confirmed) { revertToSnapshot(); } }, [revertToSnapshot]); const settingsButtons = (
); return ( <> setShowProjectModal(false)} onSave={handleProjectSave} mode={projectModalMode} />
{isProjectConfigured ? ( <> {workflowName} | {/* File operations group */}
{saveDirectoryPath && ( )}
{settingsButtons} ) : ( <> Untitled {/* File operations group */}
{settingsButtons} )}
{previousWorkflowSnapshot && ( )} {isProjectConfigured ? ( isSaving ? ( "Saving..." ) : lastSavedAt ? ( `Saved ${formatTime(lastSavedAt)}` ) : ( "Not saved" ) ) : ( "Not saved" )} · Made by Willie · ·
setShortcutsDialogOpen(false)} /> ); }