Browse Source

Improved welcome modal, discord link correction, added example workflows

handoff-20260429-1057
shrimbly 6 months ago
parent
commit
bbb2396de3
  1. 2
      README.md
  2. 34
      src/app/api/open-directory/route.ts
  3. 21
      src/components/Header.tsx
  4. 6
      src/components/WorkflowCanvas.tsx
  5. 65
      src/components/quickstart/ContentLevelSelector.tsx
  6. 10
      src/components/quickstart/PromptWorkflowView.tsx
  7. 242
      src/components/quickstart/QuickstartInitialView.tsx
  8. 62
      src/components/quickstart/QuickstartPresetChips.tsx
  9. 57
      src/components/quickstart/WelcomeModal.tsx
  10. 6
      src/components/quickstart/index.ts

2
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.

34
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);

21
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<HTMLInputElement>(null);
@ -245,21 +241,6 @@ export function Header() {
</button>
</>
)}
{hasNodes && (
<>
<span className="text-neutral-500 ml-2">·</span>
<button
onClick={() => setShowQuickstart(true)}
className="flex items-center gap-1.5 text-neutral-400 hover:text-neutral-200 transition-colors"
title="AI Quickstart - Generate workflow from description"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span>Quickstart</span>
</button>
</>
)}
<span className="text-neutral-500 ml-2">·</span>
<a
href="https://x.com/ReflctWillie"
@ -271,7 +252,7 @@ export function Header() {
</a>
<span className="text-neutral-500">·</span>
<a
href="https://discord.gg/zBzGbtfDfB"
href="https://discord.com/invite/89Nr6EKkTf"
target="_blank"
rel="noopener noreferrer"
className="text-neutral-400 hover:text-neutral-200 transition-colors"

6
src/components/WorkflowCanvas.tsx

@ -35,7 +35,7 @@ import { GroupBackgroundsPortal, GroupControlsOverlay } from "./GroupsOverlay";
import { NodeType, NanoBananaNodeData } from "@/types";
import { detectAndSplitGrid } from "@/utils/gridSplitter";
import { logger } from "@/utils/logger";
import { AIQuickstartWelcome } from "./quickstart";
import { WelcomeModal } from "./quickstart";
const nodeTypes: NodeTypes = {
imageInput: ImageInputNode,
@ -1081,9 +1081,9 @@ export function WorkflowCanvas() {
</div>
)}
{/* AI Quickstart Welcome */}
{/* Welcome Modal */}
{isCanvasEmpty && showQuickstart && (
<AIQuickstartWelcome
<WelcomeModal
onWorkflowGenerated={(workflow) => {
loadWorkflow(workflow);
setShowQuickstart(false);

65
src/components/quickstart/ContentLevelSelector.tsx

@ -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 (
<div className="flex flex-col gap-2">
<label className="text-xs font-medium text-neutral-400">
Content Level
</label>
<div className="flex gap-1 p-1 bg-neutral-900 rounded-lg border border-neutral-700">
{CONTENT_LEVELS.map((level) => (
<button
key={level.value}
onClick={() => onChange(level.value)}
disabled={disabled}
className={`
flex-1 px-3 py-2 rounded-md text-xs font-medium transition-all
${
value === level.value
? "bg-neutral-700 text-neutral-100 shadow-sm"
: "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800"
}
${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
`}
title={level.description}
>
{level.label}
</button>
))}
</div>
<p className="text-[10px] text-neutral-500">
{CONTENT_LEVELS.find((l) => l.value === value)?.description}
</p>
</div>
);
}

10
src/components/quickstart/QuickstartVibeView.tsx → 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<string | null>(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({
<div className="px-6 py-4 border-b border-neutral-700 flex items-center gap-4">
<QuickstartBackButton onClick={onBack} disabled={isGenerating} />
<h2 className="text-lg font-semibold text-neutral-100">
Vibe Workflow
Prompt a Workflow
</h2>
</div>

242
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 (
<div className="p-6">
{/* Header */}
<h1 className="text-2xl font-semibold text-neutral-100 mb-6">
Get started
</h1>
<div className="p-8">
<div className="flex gap-10">
{/* Left column - Info */}
<div className="flex-1 flex flex-col">
<div className="mb-4">
<div className="flex items-center gap-2">
<img src="/banana_icon.png" alt="" className="w-7 h-7" />
<h1 className="text-2xl font-medium text-neutral-100">
Node Banana
</h1>
</div>
</div>
<p className="text-sm text-neutral-400 leading-relaxed mb-6">
A node based workflow editor for AI image generation. Connect nodes to build pipelines that transform and generate images.
</p>
{/* Grid layout: 2 columns */}
<div className="grid grid-cols-3 gap-4 min-h-[320px]">
{/* Blank Canvas - Primary (takes 2 columns, full height) */}
<button
onClick={onSelectBlankCanvas}
className="col-span-2 row-span-2 flex flex-col items-center justify-center gap-4 p-8
rounded-xl border-2 border-blue-500/30 bg-gradient-to-br from-blue-600/10 to-blue-500/5
hover:border-blue-500/50 hover:from-blue-600/15 hover:to-blue-500/10
transition-all duration-200 group"
>
{/* Empty canvas icon */}
<div className="w-16 h-16 rounded-xl bg-blue-500/20 flex items-center justify-center group-hover:bg-blue-500/30 transition-colors">
<svg
className="w-8 h-8 text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
<div className="flex flex-col gap-2.5 mt-auto">
<a
href="https://docs.nodebanana.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-neutral-400 hover:text-neutral-200 transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
/>
</svg>
Docs
</a>
<a
href="https://discord.com/invite/89Nr6EKkTf"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-neutral-400 hover:text-neutral-200 transition-colors"
>
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
</svg>
Discord
</a>
<a
href="https://x.com/ReflctWillie"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-neutral-400 hover:text-neutral-200 transition-colors"
>
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Willie
</a>
</div>
</div>
{/* Right column - Options */}
<div className="flex-1 flex flex-col gap-2 justify-end">
<OptionButton
onClick={onSelectBlankCanvas}
icon={
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m6.75 12H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
d="M12 4.5v15m7.5-7.5h-15"
/>
</svg>
</div>
<div className="text-center">
<h2 className="text-lg font-medium text-neutral-100 mb-1">
Blank canvas
</h2>
<p className="text-sm text-neutral-400">
Start from scratch with an empty workflow
</p>
</div>
</button>
}
title="Blank canvas"
description="Start from scratch"
/>
{/* Workflow Templates - Top right */}
<button
onClick={onSelectTemplates}
className="flex flex-col items-center justify-center gap-3 p-4
rounded-xl border border-neutral-700 bg-neutral-800/50
hover:border-neutral-600 hover:bg-neutral-800
transition-all duration-200 group"
>
{/* Grid icon */}
<div className="w-12 h-12 rounded-lg bg-neutral-700/50 flex items-center justify-center group-hover:bg-neutral-700 transition-colors">
<svg
className="w-6 h-6 text-neutral-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<OptionButton
onClick={onSelectTemplates}
icon={
<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>
</div>
<div className="text-center">
<h3 className="text-sm font-medium text-neutral-200">
Workflow templates
</h3>
<p className="text-xs text-neutral-500 mt-0.5">
Browse pre-built workflows
</p>
</div>
</button>
}
title="Templates"
description="Pre-built workflows"
/>
{/* Vibe Workflow - Bottom right */}
<button
onClick={onSelectVibe}
className="flex flex-col items-center justify-center gap-3 p-4
rounded-xl border border-neutral-700 bg-neutral-800/50
hover:border-neutral-600 hover:bg-neutral-800
transition-all duration-200 group"
>
{/* Sparkles icon */}
<div className="w-12 h-12 rounded-lg bg-neutral-700/50 flex items-center justify-center group-hover:bg-neutral-700 transition-colors">
<svg
className="w-6 h-6 text-neutral-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<OptionButton
onClick={onSelectVibe}
icon={
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z"
/>
</svg>
</div>
<div className="text-center">
<h3 className="text-sm font-medium text-neutral-200">
Vibe workflow
</h3>
<p className="text-xs text-neutral-500 mt-0.5">
Describe what you want
</p>
</div>
</button>
}
title="Prompt a workflow"
description="Describe what you want"
/>
<OptionButton
onClick={onSelectLoad}
icon={
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776"
/>
}
title="Load workflow"
description="Open existing file"
/>
</div>
</div>
</div>
);
}
function OptionButton({
onClick,
icon,
title,
description,
}: {
onClick: () => void;
icon: React.ReactNode;
title: string;
description: string;
}) {
return (
<button
onClick={onClick}
className="group text-left p-4 rounded-lg border border-neutral-700/50 hover:border-neutral-600 hover:bg-neutral-800/40 transition-all duration-150"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-md bg-neutral-700/50 flex items-center justify-center flex-shrink-0 group-hover:bg-neutral-700 transition-colors">
<svg
className="w-4 h-4 text-neutral-400 group-hover:text-neutral-300 transition-colors"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
{icon}
</svg>
</div>
<div>
<h3 className="text-sm font-medium text-neutral-200 group-hover:text-neutral-100 transition-colors">
{title}
</h3>
<p className="text-xs text-neutral-500">{description}</p>
</div>
</div>
</button>
);
}

62
src/components/quickstart/QuickstartPresetChips.tsx

@ -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 (
<div className="flex flex-col gap-3">
<label className="text-xs font-medium text-neutral-400">
Quick Start Templates
</label>
<div className="flex flex-wrap gap-2">
{presets.map((preset) => (
<button
key={preset.id}
onClick={() => onSelect(selectedId === preset.id ? null : preset.id)}
disabled={disabled}
className={`
group flex items-center gap-2 px-3 py-2 rounded-lg border transition-all
${
selectedId === preset.id
? "bg-blue-600/20 border-blue-500/50 text-blue-300"
: "bg-neutral-800/50 border-neutral-700 text-neutral-300 hover:border-neutral-600 hover:bg-neutral-800"
}
${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
`}
title={preset.description}
>
<svg
className={`w-4 h-4 ${
selectedId === preset.id ? "text-blue-400" : "text-neutral-500 group-hover:text-neutral-400"
}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d={preset.icon} />
</svg>
<span className="text-xs font-medium">{preset.name}</span>
</button>
))}
</div>
{selectedId && (
<p className="text-[10px] text-neutral-500">
{presets.find((p) => p.id === selectedId)?.description}
</p>
)}
</div>
);
}

57
src/components/quickstart/AIQuickstartWelcome.tsx → 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<QuickstartView>("initial");
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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 (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-neutral-900/80 backdrop-blur-sm">
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="w-full max-w-3xl mx-4 bg-neutral-800 rounded-xl border border-neutral-700 shadow-2xl overflow-hidden">
{currentView === "initial" && (
<QuickstartInitialView
onSelectBlankCanvas={handleSelectBlankCanvas}
onSelectTemplates={handleSelectTemplates}
onSelectVibe={handleSelectVibe}
onSelectLoad={handleSelectLoad}
/>
)}
{currentView === "templates" && (
@ -58,12 +92,21 @@ export function AIQuickstartWelcome({
/>
)}
{currentView === "vibe" && (
<QuickstartVibeView
<PromptWorkflowView
onBack={handleBack}
onWorkflowGenerated={handleWorkflowSelected}
/>
)}
</div>
{/* Hidden file input for loading workflows */}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept=".json"
className="hidden"
/>
</div>
);
}

6
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";

Loading…
Cancel
Save