Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/app/api/workflow-load/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
import * as fs from "fs/promises";
import * as path from "path";

// POST: Load workflow JSON from disk by path
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { directoryPath, filename } = body;

if (!directoryPath || !filename) {
return NextResponse.json(
{ success: false, error: "Missing required fields: directoryPath and filename" },
{ status: 400 }
);
}

// Same sanitization as the save route
const safeName = filename.replace(/[^a-zA-Z0-9-_]/g, "_");
const filePath = path.join(directoryPath, `${safeName}.json`);

// Prevent path traversal
const resolvedDir = path.resolve(directoryPath);
const resolvedFile = path.resolve(filePath);
if (!resolvedFile.startsWith(resolvedDir)) {
return NextResponse.json(
{ success: false, error: "Invalid file path" },
{ status: 400 }
);
}

const content = await fs.readFile(resolvedFile, "utf-8");
const workflow = JSON.parse(content);

return NextResponse.json({ success: true, workflow });
} catch (error) {
const isNotFound =
error instanceof Error &&
"code" in error &&
(error as NodeJS.ErrnoException).code === "ENOENT";

return NextResponse.json(
{
success: false,
error: isNotFound
? "Workflow file not found"
: error instanceof Error
? error.message
: "Failed to load workflow",
},
{ status: isNotFound ? 404 : 500 }
);
}
}
23 changes: 20 additions & 3 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export function Header() {
revertToSnapshot,
shortcutsDialogOpen,
setShortcutsDialogOpen,
setShowDashboard,
autoSaveEnabled,
} = useWorkflowStore();

const [showProjectModal, setShowProjectModal] = useState(false);
Expand Down Expand Up @@ -222,6 +224,15 @@ export function Header() {
<h1 className="text-2xl font-semibold text-neutral-100 tracking-tight">
Node Banana
</h1>
<button
onClick={() => setShowDashboard(true)}
className="p-1.5 text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 rounded transition-colors"
title="Project dashboard"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>
</button>

<div className="flex items-center gap-2 ml-4 pl-4 border-l border-neutral-700">
{isProjectConfigured ? (
Expand Down Expand Up @@ -363,12 +374,18 @@ export function Header() {
</button>
)}
<CommentsNavigationIcon />
<span className="text-neutral-400">
<span className="text-neutral-400 flex items-center gap-1.5">
{isProjectConfigured ? (
isSaving ? (
"Saving..."
<>
<span className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse" />
Saving...
</>
) : lastSavedAt ? (
`Saved ${formatTime(lastSavedAt)}`
<>
{autoSaveEnabled && <span className="w-1.5 h-1.5 rounded-full bg-green-500" title="Auto-save enabled" />}
Saved {formatTime(lastSavedAt)}
</>
) : (
"Not saved"
)
Expand Down
43 changes: 27 additions & 16 deletions src/components/WorkflowCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { detectAndSplitGrid } from "@/utils/gridSplitter";
import { logger } from "@/utils/logger";
import { WelcomeModal } from "./quickstart";
import { ProjectSetupModal } from "./ProjectSetupModal";
import { ProjectDashboard } from "./dashboard/ProjectDashboard";
import { ChatPanel } from "./ChatPanel";
import { EditOperation } from "@/lib/chat/editOperations";
import { stripBinaryData } from "@/lib/chat/contextBuilder";
Expand Down Expand Up @@ -239,7 +240,7 @@ const findScrollableAncestor = (target: HTMLElement, deltaX: number, deltaY: num
};

export function WorkflowCanvas() {
const { nodes, edges, groups, onNodesChange, onEdgesChange, onConnect, addNode, updateNodeData, loadWorkflow, getNodeById, addToGlobalHistory, setNodeGroupId, executeWorkflow, isModalOpen, showQuickstart, setShowQuickstart, navigationTarget, setNavigationTarget, captureSnapshot, applyEditOperations, setWorkflowMetadata, canvasNavigationSettings, setShortcutsDialogOpen } =
const { nodes, edges, groups, onNodesChange, onEdgesChange, onConnect, addNode, updateNodeData, loadWorkflow, getNodeById, addToGlobalHistory, setNodeGroupId, executeWorkflow, isModalOpen, showQuickstart, setShowQuickstart, showDashboard, setShowDashboard, navigationTarget, setNavigationTarget, captureSnapshot, applyEditOperations, setWorkflowMetadata, canvasNavigationSettings, setShortcutsDialogOpen } =
useWorkflowStore();
const { screenToFlowPosition, getViewport, zoomIn, zoomOut, setViewport, setCenter } = useReactFlow();
const { show: showToast } = useToast();
Expand All @@ -255,6 +256,13 @@ export function WorkflowCanvas() {
// Detect if canvas is empty for showing quickstart
const isCanvasEmpty = nodes.length === 0;

// Listen for dashboard:new-project event to open new project setup
useEffect(() => {
const handler = () => setShowNewProjectSetup(true);
window.addEventListener("dashboard:new-project", handler);
return () => window.removeEventListener("dashboard:new-project", handler);
}, []);

// Handle comment navigation - center viewport on target node
useEffect(() => {
if (navigationTarget) {
Expand Down Expand Up @@ -1605,20 +1613,8 @@ export function WorkflowCanvas() {
</div>
)}

{/* Welcome Modal */}
{isCanvasEmpty && showQuickstart && (
<WelcomeModal
onWorkflowGenerated={async (workflow) => {
await loadWorkflow(workflow);
setShowQuickstart(false);
}}
onClose={() => setShowQuickstart(false)}
onNewProject={() => {
setShowQuickstart(false);
setShowNewProjectSetup(true);
}}
/>
)}
{/* Project Dashboard */}
{showDashboard && <ProjectDashboard />}

{/* New Project Setup Modal */}
{showNewProjectSetup && (
Expand All @@ -1628,10 +1624,11 @@ export function WorkflowCanvas() {
onSave={(id, name, directoryPath) => {
setWorkflowMetadata(id, name, directoryPath);
setShowNewProjectSetup(false);
setShowDashboard(false);
}}
onClose={() => {
setShowNewProjectSetup(false);
setShowQuickstart(true);
setShowDashboard(true);
}}
/>
)}
Expand Down Expand Up @@ -1773,6 +1770,20 @@ export function WorkflowCanvas() {
{/* Global image history */}
<GlobalImageHistory />

{/* Floating projects button */}
{!showDashboard && (
<button
onClick={() => setShowDashboard(true)}
className="absolute bottom-4 left-14 z-20 flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-neutral-300 bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 rounded-lg shadow-lg transition-colors"
title="Open project dashboard"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>
Projects
</button>
)}

{/* Chat toggle button - hidden for now */}

{/* Chat panel */}
Expand Down
Loading