Skip to content
Closed
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
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,6 @@ All routes in `src/app/api/`:
- Each commit should be atomic and self-contained: one task = one commit.
- The .planning directory is untracked, do not attempt to commit any changes to the files in this directory.

## Web Crawling
- Always use `cf crawl` when needing to crawl or fetch website content.

127 changes: 107 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
},
"dependencies": {
"@ai-sdk/google": "^3.0.13",
"@ai-sdk/react": "^3.0.51",
"@ai-sdk/react": "^3.0.118",
"@google/genai": "^1.30.0",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@tailwindcss/postcss": "^4.1.17",
"@xyflow/react": "^12.9.3",
"ai": "^6.0.49",
"ai": "^6.0.116",
Comment on lines +16 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== npm registry metadata =="
npm view [email protected] version peerDependencies dependencies --json
npm view `@ai-sdk/react`@3.0.118 version peerDependencies dependencies --json
npm view `@ai-sdk/google`@3.0.13 version peerDependencies dependencies --json

echo
echo "== lockfile entries =="
fd -HI '^(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$' . -x sh -c '
  echo ">>> {}"
  rg -n -C2 "\"ai\"|@ai-sdk/react|@ai-sdk/google" "{}" || true
'

Repository: shrimbly/node-banana

Length of output: 2523


Keep the timeout hotfix isolated; these SDK bumps appear unrelated.

This PR is scoped as a /api/generate timeout change, but these two dependency bumps widen the runtime surface area without clear justification. Since the bumps are not required for the timeout fix, split them into a separate PR. The lockfile does resolve cleanly, though it brings multiple versions of internal provider packages (@ai-sdk/[email protected] from Google, 3.0.8 from core AI SDK). This managed duplication is not a blocker, but the scope creep itself is worth avoiding in a hotfix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 16 - 22, Revert the unrelated dependency version
changes in package.json (the bumps to "@ai-sdk/react" and "ai" shown in the
diff) so this PR only contains the /api/generate timeout change; restore those
entries to the versions from main (or remove the two updated entries) and
regenerate the lockfile (npm/yarn install) to match, then create a separate
branch/PR that contains the dependency bumps and related lockfile changes for
review; ensure tests/build pass after restoring the original package.json before
merging the hotfix.

"autoprefixer": "^10.4.22",
"jszip": "^3.10.1",
"konva": "^10.0.12",
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { generateWithWaveSpeed } from "./providers/wavespeed";
// Re-export for backward compatibility (test file imports from route)
export const clearFalInputMappingCache = _clearFalInputMappingCache;

export const maxDuration = 300; // 5 minute timeout (Vercel hobby plan limit)
export const maxDuration = 800; // ~13 minute timeout (Vercel Pro limit)
export const dynamic = 'force-dynamic'; // Ensure this route is always dynamic


Expand Down
134 changes: 78 additions & 56 deletions src/components/ProjectSetupModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ export function ProjectSetupModal({
// Inline parameters hook
const { inlineParametersEnabled, setInlineParameters } = useInlineParameters();

// Detect web deployment (not localhost)
const isWebDeployment = typeof window !== "undefined" &&
!window.location.hostname.includes("localhost") &&
!window.location.hostname.includes("127.0.0.1");

// Tab state
const [activeTab, setActiveTab] = useState<"project" | "providers" | "nodeDefaults" | "canvas">("project");

Expand Down Expand Up @@ -269,14 +274,15 @@ export function ProjectSetupModal({
return;
}

if (!directoryPath.trim()) {
// Skip directory validation for web deployments
if (!isWebDeployment && !directoryPath.trim()) {
setError("Project directory is required");
return;
}

const fullProjectPath = ensureProjectSubfolderPath(directoryPath, name);
const fullProjectPath = isWebDeployment ? "" : ensureProjectSubfolderPath(directoryPath, name);

if (!(fullProjectPath.startsWith("/") || /^[A-Za-z]:[\\\/]/.test(fullProjectPath) || fullProjectPath.startsWith("\\\\"))) {
if (!isWebDeployment && !(fullProjectPath.startsWith("/") || /^[A-Za-z]:[\\\/]/.test(fullProjectPath) || fullProjectPath.startsWith("\\\\"))) {
setError("Project directory must be an absolute path (starting with /, a drive letter, or a UNC path)");
return;
}
Expand All @@ -285,21 +291,24 @@ export function ProjectSetupModal({
setError(null);

try {
// Validate path shape when it already exists
const response = await fetch(
`/api/workflow?path=${encodeURIComponent(fullProjectPath)}`
);
const result = await response.json();

if (result.exists && !result.isDirectory) {
setError("Project path is not a directory");
setIsValidating(false);
return;
// Skip path validation for web deployments
if (!isWebDeployment) {
// Validate path shape when it already exists
const response = await fetch(
`/api/workflow?path=${encodeURIComponent(fullProjectPath)}`
);
const result = await response.json();

if (result.exists && !result.isDirectory) {
setError("Project path is not a directory");
setIsValidating(false);
return;
}
}

const id = mode === "new" ? generateWorkflowId() : useWorkflowStore.getState().workflowId || generateWorkflowId();
// Update external storage setting
setUseExternalImageStorage(externalStorage);
// Update external storage setting - force embedded images for web deployments
setUseExternalImageStorage(isWebDeployment ? false : externalStorage);
onSave(id, name.trim(), fullProjectPath);
setIsValidating(false);
} catch (err) {
Expand Down Expand Up @@ -449,51 +458,64 @@ export function ProjectSetupModal({
/>
</div>

<div>
<label className="block text-sm text-neutral-400 mb-1">
Project Directory
</label>
<div className="flex gap-2">
<input
type="text"
value={directoryPath}
onChange={(e) => setDirectoryPath(e.target.value)}
placeholder="/Users/username/projects/my-project"
className="flex-1 px-3 py-2 bg-neutral-900 border border-neutral-600 rounded-lg text-neutral-100 text-sm focus:outline-none focus:border-neutral-500"
/>
<button
type="button"
onClick={handleBrowse}
disabled={isBrowsing}
className="px-3 py-2 bg-neutral-700 hover:bg-neutral-600 disabled:bg-neutral-700 disabled:opacity-50 text-neutral-200 text-sm rounded-lg transition-colors"
>
{isBrowsing ? "..." : "Browse"}
</button>
{isWebDeployment ? (
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
<p className="text-sm text-blue-300">
Web Mode: Workflows run in memory only. Files cannot be saved to disk.
</p>
<p className="text-xs text-blue-300/70 mt-1">
Images will be embedded as base64. Use the local app for file saving.
</p>
</div>
<p className="text-xs text-neutral-400 mt-1">
Workflow files and images will be saved here. Subfolders for inputs and generations will be auto-created.
</p>
</div>

<div className="pt-2 border-t border-neutral-700">
<label className="flex items-center justify-between gap-3 cursor-pointer">
) : (
<>
<div>
<span className="text-sm text-neutral-200">Embed images as base64</span>
<p className="text-xs text-neutral-400">
Embeds all images in workflow, larger workflow files. Can hit memory limits on very large workflows.
<label className="block text-sm text-neutral-400 mb-1">
Project Directory
</label>
<div className="flex gap-2">
<input
type="text"
value={directoryPath}
onChange={(e) => setDirectoryPath(e.target.value)}
placeholder="/Users/username/projects/my-project"
className="flex-1 px-3 py-2 bg-neutral-900 border border-neutral-600 rounded-lg text-neutral-100 text-sm focus:outline-none focus:border-neutral-500"
/>
<button
type="button"
onClick={handleBrowse}
disabled={isBrowsing}
className="px-3 py-2 bg-neutral-700 hover:bg-neutral-600 disabled:bg-neutral-700 disabled:opacity-50 text-neutral-200 text-sm rounded-lg transition-colors"
>
{isBrowsing ? "..." : "Browse"}
</button>
</div>
<p className="text-xs text-neutral-400 mt-1">
Workflow files and images will be saved here. Subfolders for inputs and generations will be auto-created.
</p>
</div>
<button
type="button"
role="switch"
aria-checked={!externalStorage}
onClick={() => setExternalStorage(externalStorage ? false : true)}
className={`relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors ${!externalStorage ? "bg-blue-500" : "bg-neutral-600"}`}
>
<span className={`inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform ${!externalStorage ? "translate-x-[18px]" : "translate-x-[3px]"}`} />
</button>
</label>
</div>

<div className="pt-2 border-t border-neutral-700">
<label className="flex items-center justify-between gap-3 cursor-pointer">
<div>
<span className="text-sm text-neutral-200">Embed images as base64</span>
<p className="text-xs text-neutral-400">
Embeds all images in workflow, larger workflow files. Can hit memory limits on very large workflows.
</p>
</div>
<button
type="button"
role="switch"
aria-checked={!externalStorage}
onClick={() => setExternalStorage(externalStorage ? false : true)}
className={`relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors ${!externalStorage ? "bg-blue-500" : "bg-neutral-600"}`}
>
<span className={`inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform ${!externalStorage ? "translate-x-[18px]" : "translate-x-[3px]"}`} />
</button>
</label>
</div>
</>
)}

<div className="pt-2 border-t border-neutral-700">
<label className="flex items-center justify-between gap-3 cursor-pointer">
Expand Down
Loading