From bbb2396de397a431fcbab244bb8ad46e4cb57ab1 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 7 Jan 2026 22:11:05 +1300 Subject: [PATCH] Improved welcome modal, discord link correction, added example workflows --- README.md | 2 +- src/app/api/open-directory/route.ts | 34 ++- src/components/Header.tsx | 21 +- src/components/WorkflowCanvas.tsx | 6 +- .../quickstart/ContentLevelSelector.tsx | 65 ----- ...artVibeView.tsx => PromptWorkflowView.tsx} | 10 +- .../quickstart/QuickstartInitialView.tsx | 242 +++++++++++------- .../quickstart/QuickstartPresetChips.tsx | 62 ----- ...QuickstartWelcome.tsx => WelcomeModal.tsx} | 57 ++++- src/components/quickstart/index.ts | 6 +- 10 files changed, 239 insertions(+), 266 deletions(-) delete mode 100644 src/components/quickstart/ContentLevelSelector.tsx rename src/components/quickstart/{QuickstartVibeView.tsx => PromptWorkflowView.tsx} (97%) delete mode 100644 src/components/quickstart/QuickstartPresetChips.tsx rename src/components/quickstart/{AIQuickstartWelcome.tsx => WelcomeModal.tsx} (52%) diff --git a/README.md b/README.md index 2491bc6b..42bdc9cb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Node Banana -> **Important note:** This is in early development, it probably has some issues. Use Chrome. For support or raising any issues join the [discord](https://discord.gg/zBzGbtfDfB). +> **Important note:** This is in early development, it probably has some issues. Use Chrome. For support or raising any issues join the [discord](https://discord.com/invite/89Nr6EKkTf). Node Banana is node-based workflow application for generating images with NBP. Build image generation pipelines by connecting nodes on a visual canvas. Built mainly with Opus 4.5. diff --git a/src/app/api/open-directory/route.ts b/src/app/api/open-directory/route.ts index 1b6ad731..2f0758c6 100644 --- a/src/app/api/open-directory/route.ts +++ b/src/app/api/open-directory/route.ts @@ -1,7 +1,8 @@ - import { NextRequest, NextResponse } from "next/server"; import { execFile } from "child_process"; import { promisify } from "util"; +import { stat } from "fs/promises"; +import path from "path"; import os from "os"; const execFileAsync = promisify(execFile); @@ -9,15 +10,34 @@ const execFileAsync = promisify(execFile); export async function POST(req: NextRequest) { try { const body = await req.json(); - const { path } = body; + const { path: inputPath } = body; - if (!path) { + if (!inputPath || typeof inputPath !== "string") { return NextResponse.json( { success: false, error: "Path is required" }, { status: 400 } ); } + // Normalize and resolve the path to prevent traversal attacks + const normalizedPath = path.resolve(inputPath); + + // Validate that the path exists and is a directory + try { + const stats = await stat(normalizedPath); + if (!stats.isDirectory()) { + return NextResponse.json( + { success: false, error: "Path is not a directory" }, + { status: 400 } + ); + } + } catch { + return NextResponse.json( + { success: false, error: "Directory does not exist" }, + { status: 400 } + ); + } + let command = ""; let args: string[] = []; const platform = os.platform(); @@ -25,20 +45,20 @@ export async function POST(req: NextRequest) { switch (platform) { case "darwin": command = "open"; - args = [path]; + args = [normalizedPath]; break; case "win32": command = "explorer"; - args = [path]; + args = [normalizedPath]; break; case "linux": command = "xdg-open"; - args = [path]; + args = [normalizedPath]; break; default: // Fallback for other Unix-like systems command = "xdg-open"; - args = [path]; + args = [normalizedPath]; } await execFileAsync(command, args); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 8a66c44a..a5fe1dd9 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -16,12 +16,8 @@ export function Header() { setWorkflowMetadata, saveToFile, loadWorkflow, - nodes, - setShowQuickstart, } = useWorkflowStore(); - const hasNodes = nodes.length > 0; - const [showProjectModal, setShowProjectModal] = useState(false); const [projectModalMode, setProjectModalMode] = useState<"new" | "settings">("new"); const fileInputRef = useRef(null); @@ -245,21 +241,6 @@ export function Header() { )} - {hasNodes && ( - <> - · - - - )} · · )} - {/* AI Quickstart Welcome */} + {/* Welcome Modal */} {isCanvasEmpty && showQuickstart && ( - { loadWorkflow(workflow); setShowQuickstart(false); diff --git a/src/components/quickstart/ContentLevelSelector.tsx b/src/components/quickstart/ContentLevelSelector.tsx deleted file mode 100644 index b9d25ea0..00000000 --- a/src/components/quickstart/ContentLevelSelector.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client"; - -import { ContentLevel } from "@/lib/quickstart/templates"; - -interface ContentLevelSelectorProps { - value: ContentLevel; - onChange: (level: ContentLevel) => void; - disabled?: boolean; -} - -const CONTENT_LEVELS: { value: ContentLevel; label: string; description: string }[] = [ - { - value: "empty", - label: "Empty", - description: "Structure only, no prompts", - }, - { - value: "minimal", - label: "Minimal", - description: "Placeholder prompts", - }, - { - value: "full", - label: "Full", - description: "Complete example prompts", - }, -]; - -export function ContentLevelSelector({ - value, - onChange, - disabled = false, -}: ContentLevelSelectorProps) { - return ( -
- -
- {CONTENT_LEVELS.map((level) => ( - - ))} -
-

- {CONTENT_LEVELS.find((l) => l.value === value)?.description} -

-
- ); -} diff --git a/src/components/quickstart/QuickstartVibeView.tsx b/src/components/quickstart/PromptWorkflowView.tsx similarity index 97% rename from src/components/quickstart/QuickstartVibeView.tsx rename to src/components/quickstart/PromptWorkflowView.tsx index b841752f..3f98681b 100644 --- a/src/components/quickstart/QuickstartVibeView.tsx +++ b/src/components/quickstart/PromptWorkflowView.tsx @@ -4,15 +4,15 @@ import { useState, useCallback } from "react"; import { WorkflowFile } from "@/store/workflowStore"; import { QuickstartBackButton } from "./QuickstartBackButton"; -interface QuickstartVibeViewProps { +interface PromptWorkflowViewProps { onBack: () => void; onWorkflowGenerated: (workflow: WorkflowFile) => void; } -export function QuickstartVibeView({ +export function PromptWorkflowView({ onBack, onWorkflowGenerated, -}: QuickstartVibeViewProps) { +}: PromptWorkflowViewProps) { const [description, setDescription] = useState(""); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); @@ -46,7 +46,7 @@ export function QuickstartVibeView({ onWorkflowGenerated(result.workflow); } } catch (err) { - console.error("Vibe workflow error:", err); + console.error("Prompt workflow error:", err); setError( err instanceof Error ? err.message : "Failed to generate workflow" ); @@ -63,7 +63,7 @@ export function QuickstartVibeView({

- Vibe Workflow + Prompt a Workflow

diff --git a/src/components/quickstart/QuickstartInitialView.tsx b/src/components/quickstart/QuickstartInitialView.tsx index 3144c7e0..765bbfca 100644 --- a/src/components/quickstart/QuickstartInitialView.tsx +++ b/src/components/quickstart/QuickstartInitialView.tsx @@ -4,124 +4,182 @@ interface QuickstartInitialViewProps { onSelectBlankCanvas: () => void; onSelectTemplates: () => void; onSelectVibe: () => void; + onSelectLoad: () => void; } export function QuickstartInitialView({ onSelectBlankCanvas, onSelectTemplates, onSelectVibe, + onSelectLoad, }: QuickstartInitialViewProps) { return ( -
- {/* Header */} -

- Get started -

+
+
+ {/* Left column - Info */} +
+
+
+ +

+ Node Banana +

+
+
+ +

+ A node based workflow editor for AI image generation. Connect nodes to build pipelines that transform and generate images. +

- {/* Grid layout: 2 columns */} -
- {/* Blank Canvas - Primary (takes 2 columns, full height) */} -
+ + {/* Right column - Options */} +
+ - -
-
-

- Blank canvas -

-

- Start from scratch with an empty workflow -

-
- + } + title="Blank canvas" + description="Start from scratch" + /> - {/* Workflow Templates - Top right */} - + } + title="Templates" + description="Pre-built workflows" + /> - {/* Vibe Workflow - Bottom right */} - + } + title="Prompt a workflow" + description="Describe what you want" + /> + + + } + title="Load workflow" + description="Open existing file" + /> +
); } + +function OptionButton({ + onClick, + icon, + title, + description, +}: { + onClick: () => void; + icon: React.ReactNode; + title: string; + description: string; +}) { + return ( + + ); +} diff --git a/src/components/quickstart/QuickstartPresetChips.tsx b/src/components/quickstart/QuickstartPresetChips.tsx deleted file mode 100644 index d9890fe4..00000000 --- a/src/components/quickstart/QuickstartPresetChips.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; - -import { getAllPresets } from "@/lib/quickstart/templates"; - -interface QuickstartPresetChipsProps { - selectedId: string | null; - onSelect: (templateId: string | null) => void; - disabled?: boolean; -} - -export function QuickstartPresetChips({ - selectedId, - onSelect, - disabled = false, -}: QuickstartPresetChipsProps) { - const presets = getAllPresets(); - - return ( -
- -
- {presets.map((preset) => ( - - ))} -
- {selectedId && ( -

- {presets.find((p) => p.id === selectedId)?.description} -

- )} -
- ); -} diff --git a/src/components/quickstart/AIQuickstartWelcome.tsx b/src/components/quickstart/WelcomeModal.tsx similarity index 52% rename from src/components/quickstart/AIQuickstartWelcome.tsx rename to src/components/quickstart/WelcomeModal.tsx index 095da5ae..9e244a4d 100644 --- a/src/components/quickstart/AIQuickstartWelcome.tsx +++ b/src/components/quickstart/WelcomeModal.tsx @@ -1,22 +1,23 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useRef } from "react"; import { WorkflowFile } from "@/store/workflowStore"; import { QuickstartView } from "@/types/quickstart"; import { QuickstartInitialView } from "./QuickstartInitialView"; import { QuickstartTemplatesView } from "./QuickstartTemplatesView"; -import { QuickstartVibeView } from "./QuickstartVibeView"; +import { PromptWorkflowView } from "./PromptWorkflowView"; -interface AIQuickstartWelcomeProps { +interface WelcomeModalProps { onWorkflowGenerated: (workflow: WorkflowFile) => void; onClose: () => void; } -export function AIQuickstartWelcome({ +export function WelcomeModal({ onWorkflowGenerated, onClose, -}: AIQuickstartWelcomeProps) { +}: WelcomeModalProps) { const [currentView, setCurrentView] = useState("initial"); + const fileInputRef = useRef(null); const handleSelectBlankCanvas = useCallback(() => { onClose(); @@ -30,6 +31,38 @@ export function AIQuickstartWelcome({ setCurrentView("vibe"); }, []); + const handleSelectLoad = useCallback(() => { + fileInputRef.current?.click(); + }, []); + + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const workflow = JSON.parse( + event.target?.result as string + ) as WorkflowFile; + if (workflow.version && workflow.nodes && workflow.edges) { + onWorkflowGenerated(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 = ""; + }, + [onWorkflowGenerated] + ); + const handleBack = useCallback(() => { setCurrentView("initial"); }, []); @@ -42,13 +75,14 @@ export function AIQuickstartWelcome({ ); return ( -
+
{currentView === "initial" && ( )} {currentView === "templates" && ( @@ -58,12 +92,21 @@ export function AIQuickstartWelcome({ /> )} {currentView === "vibe" && ( - )}
+ + {/* Hidden file input for loading workflows */} +
); } diff --git a/src/components/quickstart/index.ts b/src/components/quickstart/index.ts index 89756b41..f5b29296 100644 --- a/src/components/quickstart/index.ts +++ b/src/components/quickstart/index.ts @@ -1,7 +1,5 @@ -export { AIQuickstartWelcome } from "./AIQuickstartWelcome"; -export { ContentLevelSelector } from "./ContentLevelSelector"; -export { QuickstartPresetChips } from "./QuickstartPresetChips"; +export { WelcomeModal } from "./WelcomeModal"; export { QuickstartBackButton } from "./QuickstartBackButton"; export { QuickstartInitialView } from "./QuickstartInitialView"; export { QuickstartTemplatesView } from "./QuickstartTemplatesView"; -export { QuickstartVibeView } from "./QuickstartVibeView"; +export { PromptWorkflowView } from "./PromptWorkflowView";