Browse Source

docs(03): create phase plans for GenerateImage node

Phase 03: Generate Node Refactor
- 3 plans, 9 tasks
- NanoBanana → GenerateImage (image models only)
- GenerateVideo deferred to Phase 6
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
bdd995e83f
  1. 130
      .planning/phases/03-generate-node-refactor/03-01-PLAN.md
  2. 162
      .planning/phases/03-generate-node-refactor/03-02-PLAN.md
  3. 147
      .planning/phases/03-generate-node-refactor/03-03-PLAN.md

130
.planning/phases/03-generate-node-refactor/03-01-PLAN.md

@ -7,10 +7,10 @@ type: execute
<objective>
Rename NanoBanana node to GenerateImage and add multi-provider model selector.
Purpose: Transform the hardcoded Gemini-only node into a multi-provider image generation node that filters to image-capable models only.
Output: GenerateImage node component with provider/model dropdown (image models only), updated types, store functions renamed.
Purpose: Transform the Gemini-only node into a multi-provider image generation node.
Output: GenerateImageNode component with provider/model dropdowns filtering to image-capable models only.
Note: GenerateVideo node will be added in Phase 6 (Video & Polish) as a separate node type.
Note: Video generation will use a separate GenerateVideo node in Phase 6.
</objective>
<execution_context>
@ -22,127 +22,93 @@ Note: GenerateVideo node will be added in Phase 6 (Video & Polish) as a separate
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
# Prior phase context:
@.planning/phases/01-provider-infrastructure/01-02-SUMMARY.md
@.planning/phases/02-model-discovery/02-03-SUMMARY.md
# Key source files:
@src/components/nodes/NanoBananaNode.tsx
@src/types/index.ts
@src/store/workflowStore.ts
@src/lib/providers/types.ts
**Tech stack available:** Zustand, React, ProviderModel interface, /api/models endpoint
**Established patterns:** Provider self-registration, localStorage settings persistence
**Constraining decisions:**
- Keep nanoBanana node type internally for backward compatibility
- Gemini models use internal names (nano-banana, nano-banana-pro)
**Tech available:** ProviderModel interface, /api/models endpoint, provider registry
**Patterns:** Provider self-registration, localStorage settings persistence
**Constraints:** Keep "nanoBanana" node type string for backward compatibility
</context>
<tasks>
<task type="auto">
<name>Task 1: Update types for multi-provider model selection</name>
<name>Task 1: Add SelectedModel type and update NanoBananaNodeData</name>
<files>src/types/index.ts</files>
<action>
1. Add new types for selected model:
- `SelectedModel` interface with { provider: ProviderType, modelId: string, displayName: string }
- Keep existing ModelType for backward compatibility
2. Update NanoBananaNodeData interface:
- Add optional `selectedModel?: SelectedModel` field
- Keep existing `model: ModelType` field for Gemini backward compatibility
3. Add "gemini" to ProviderType union if not already present (it should be the Gemini provider identifier)
WHY: NanoBananaNodeData needs to store either legacy Gemini model selection OR new multi-provider selection. The optional selectedModel field enables graceful migration.
Add SelectedModel interface:
```typescript
export interface SelectedModel {
provider: ProviderType;
modelId: string;
displayName: string;
}
```
Update NanoBananaNodeData to add optional selectedModel field (keep existing model field for Gemini backward compatibility).
Add "gemini" to ProviderType if not present.
</action>
<verify>npx tsc --noEmit passes</verify>
<done>Types compile, SelectedModel interface exists, NanoBananaNodeData has selectedModel field</done>
<verify>npx tsc --noEmit</verify>
<done>SelectedModel type exists, NanoBananaNodeData has selectedModel field, types compile</done>
</task>
<task type="auto">
<name>Task 2: Rename component to GenerateImageNode with model selector UI</name>
<name>Task 2: Create GenerateImageNode with provider/model selector</name>
<files>src/components/nodes/NanoBananaNode.tsx, src/components/nodes/index.ts</files>
<action>
1. Rename file from NanoBananaNode.tsx to GenerateImageNode.tsx
2. Rename component from NanoBananaNode to GenerateImageNode
3. Add model selector UI that shows:
- Provider dropdown (gemini, replicate, fal) - only show enabled providers
- Model dropdown (fetched from /api/models for selected provider)
- **Filter to image capabilities only**: text-to-image, image-to-image
- For Gemini, show existing hardcoded models (Nano Banana, Nano Banana Pro)
- For other providers, fetch models dynamically with capability filter
4. When provider/model changes:
- Update node data with selectedModel: { provider, modelId, displayName }
- For Gemini, also update legacy model field for backward compatibility
5. Keep existing aspect ratio/resolution controls for Gemini models
- Hide them for non-Gemini models (parameters will come from provider schema later)
6. Update index.ts to export GenerateImageNode and alias as NanoBananaNode:
1. Rename NanoBananaNode.tsx to GenerateImageNode.tsx
2. Rename component function to GenerateImageNode
3. Add provider dropdown showing enabled providers (gemini always, others if configured)
4. Add model dropdown:
- For gemini: hardcoded Nano Banana / Nano Banana Pro
- For replicate/fal: fetch from /api/models?provider=X&capabilities=text-to-image,image-to-image
5. Filter to image capabilities only (text-to-image, image-to-image)
6. Keep aspect ratio/resolution controls for Gemini, hide for others
7. Update index.ts exports:
```typescript
export { GenerateImageNode } from "./GenerateImageNode";
export { GenerateImageNode as NanoBananaNode } from "./GenerateImageNode";
```
WHY: The component rename reflects its image-specific purpose. Video generation will be a separate GenerateVideoNode in Phase 6. The alias maintains backward compatibility.
</action>
<verify>npm run build succeeds, component renders in browser</verify>
<done>GenerateImageNode.tsx exists, shows provider selector, model dropdown shows only image-capable models</done>
<verify>npm run build</verify>
<done>GenerateImageNode renders with provider/model dropdowns, shows only image models</done>
</task>
<task type="auto">
<name>Task 3: Update store and canvas references</name>
<files>src/store/workflowStore.ts, src/components/WorkflowCanvas.tsx</files>
<files>src/store/workflowStore.ts, src/components/WorkflowCanvas.tsx, src/components/ConnectionDropMenu.tsx, src/components/FloatingActionBar.tsx</files>
<action>
1. In workflowStore.ts:
- Rename saveNanoBananaDefaults to saveGenerateImageDefaults (keep old name as alias)
- Rename loadNanoBananaDefaults to loadGenerateImageDefaults (keep old name as alias)
- Update localStorage key comment but keep actual key for migration
- Update createDefaultNodeData for "nanoBanana" case to include default selectedModel for Gemini
2. In WorkflowCanvas.tsx:
- Update nodeTypes registration to use GenerateImageNode
- Keep "nanoBanana" as the type key (internal type name unchanged for now)
- Update minimap color mapping if it references "nanoBanana"
3. In any file using NanoBananaNode import:
- Update import to use GenerateImageNode
- Files: ConnectionDropMenu.tsx, FloatingActionBar.tsx, etc.
WHY: Internal references update to new names while keeping the node type string "nanoBanana" for workflow file compatibility.
1. workflowStore.ts: Rename helper functions (keep aliases for compatibility):
- loadNanoBananaDefaults → loadGenerateImageDefaults
- saveNanoBananaDefaults → saveGenerateImageDefaults
2. Update createDefaultNodeData "nanoBanana" case to set default selectedModel for Gemini
3. WorkflowCanvas.tsx: Update nodeTypes to use GenerateImageNode
4. Update imports in ConnectionDropMenu.tsx, FloatingActionBar.tsx
</action>
<verify>npm run build succeeds, existing workflows load correctly</verify>
<done>All imports updated, store functions renamed with aliases, workflows still load</done>
<verify>npm run build succeeds, app loads without errors</verify>
<done>All references updated, existing workflows load correctly</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] `npm run build` succeeds without errors
- [ ] `npx tsc --noEmit` passes type checking
- [ ] `npm run build` succeeds
- [ ] GenerateImageNode renders with provider dropdown
- [ ] Model selector shows only image-capable models (text-to-image, image-to-image)
- [ ] Gemini models show in model selector
- [ ] Existing workflows with nanoBanana nodes still load
- [ ] Model selector shows only image-capable models
- [ ] Existing workflows with nanoBanana nodes load
</verification>
<success_criteria>
- All tasks completed
- All verification checks pass
- GenerateImageNode component exists with multi-provider UI
- Model selector filters to image capabilities only
- Types support both legacy Gemini and new multi-provider selection
- Backward compatibility maintained for existing workflows
- Model selector filters to text-to-image and image-to-image only
- Backward compatibility maintained
</success_criteria>
<output>
After completion, create `.planning/phases/03-generate-node-refactor/03-01-SUMMARY.md`
Create `.planning/phases/03-generate-node-refactor/03-01-SUMMARY.md`
</output>

162
.planning/phases/03-generate-node-refactor/03-02-PLAN.md

@ -5,12 +5,10 @@ type: execute
---
<objective>
Add provider-specific execution logic to the generate API route for image generation.
Implement provider-specific image generation in API route.
Purpose: Enable the generate endpoint to route requests to the correct provider (Gemini, Replicate, fal.ai) based on the selected model, filtering to image outputs only.
Output: Updated /api/generate route that dispatches to provider-specific image generation logic.
Note: Video generation will use a separate endpoint in Phase 6.
Purpose: Route generation requests to correct provider (Gemini, Replicate, fal.ai) based on selected model.
Output: Working generate endpoint that dispatches to provider implementations.
</objective>
<execution_context>
@ -20,156 +18,82 @@ Note: Video generation will use a separate endpoint in Phase 6.
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
# Prior plan context:
@.planning/phases/03-generate-node-refactor/03-01-SUMMARY.md
# Key source files:
@src/app/api/generate/route.ts
@src/lib/providers/types.ts
@src/lib/providers/replicate.ts
@src/lib/providers/fal.ts
@src/lib/providers/index.ts
**Tech stack available:** ProviderInterface with generate() method, provider registry
**Established patterns:** API routes return { success, data/error }, provider self-registration
**Constraining decisions:**
- Gemini generation logic already works, preserve it
- Provider implementations have generate() method defined in ProviderInterface
**Tech available:** ProviderInterface.generate(), provider registry
**Constraints:** Preserve existing Gemini logic, image output only
</context>
<tasks>
<task type="auto">
<name>Task 1: Implement generate() method in Replicate provider</name>
<name>Task 1: Implement Replicate provider generate() method</name>
<files>src/lib/providers/replicate.ts</files>
<action>
1. Implement the generate() method that currently throws "not implemented":
- Accept GenerationInput with model, prompt, images, parameters
- Call Replicate's prediction API: POST https://api.replicate.com/v1/predictions
- Handle input format: { version: model.id, input: { prompt, image?, ...parameters } }
- Poll for completion (Replicate is async) or use webhook pattern
- Return GenerationOutput with image/video data
2. Handle image input:
- If images provided, check if Replicate model expects URLs
- For now, skip image input (Phase 5 adds image URL server)
- Log warning if images provided but skipped
3. Fetch output:
- Poll GET /predictions/{id} until status is "succeeded" or "failed"
- Convert output URL to base64 data URL
- Return in GenerationOutput format with type: "image"
4. Filter to image models only:
- Only accept models with text-to-image or image-to-image capabilities
- Return error if video model passed (video generation is Phase 6)
WHY: Replicate requires polling for async predictions. The generate() method encapsulates this complexity. Image-only filter keeps this endpoint focused.
Implement generate() that currently throws "not implemented":
1. POST to https://api.replicate.com/v1/predictions with { version: modelId, input: { prompt } }
2. Poll GET /predictions/{id} until status is "succeeded" or "failed"
3. Fetch output image URL and convert to base64 data URL
4. Return GenerationOutput with type: "image"
5. Skip image inputs for now (Phase 5 adds URL server)
</action>
<verify>TypeScript compiles, generate() method exists and doesn't throw immediately</verify>
<done>Replicate provider has working generate() method that calls prediction API for image models</done>
<verify>TypeScript compiles</verify>
<done>Replicate generate() calls prediction API and returns image</done>
</task>
<task type="auto">
<name>Task 2: Implement generate() method in fal.ai provider</name>
<name>Task 2: Implement fal.ai provider generate() method</name>
<files>src/lib/providers/fal.ts</files>
<action>
1. Implement the generate() method:
- Accept GenerationInput with model, prompt, images, parameters
- Call fal.ai's run endpoint: POST https://fal.run/{model.id}
- Include prompt and parameters in request body
- fal.ai is synchronous (waits for result)
2. Handle auth:
- Use "Key {apiKey}" header format (already established)
- API key optional but rate limited without it
3. Parse response:
- Extract image URL from response
- Convert to base64 data URL for consistency
- Handle images array output format
4. Filter to image models only:
- Only accept models with text-to-image or image-to-image capabilities
- Return error if video model passed (video generation is Phase 6)
WHY: fal.ai's synchronous API is simpler than Replicate. The generate() method normalizes the response format. Image-only filter keeps this endpoint focused.
Implement generate():
1. POST to https://fal.run/{modelId} with { prompt, ...parameters }
2. Use "Key {apiKey}" auth header
3. Extract image URL from response, convert to base64
4. Return GenerationOutput with type: "image"
</action>
<verify>TypeScript compiles, generate() method exists</verify>
<done>fal.ai provider has working generate() method that calls fal.run endpoint for image models</done>
<verify>TypeScript compiles</verify>
<done>fal.ai generate() calls fal.run and returns image</done>
</task>
<task type="auto">
<name>Task 3: Update generate API route to dispatch by provider</name>
<name>Task 3: Update generate API route for multi-provider dispatch</name>
<files>src/app/api/generate/route.ts</files>
<action>
1. Update request type to accept either:
- Legacy format: { model: ModelType, prompt, images, aspectRatio, resolution, useGoogleSearch }
- New format: { selectedModel: SelectedModel, prompt, images, parameters }
2. Add provider dispatch logic:
```typescript
if (body.selectedModel) {
// New multi-provider path
const { provider, modelId } = body.selectedModel;
if (provider === "gemini") {
// Use existing Gemini logic
} else {
// Get provider and call generate()
const providerImpl = getProvider(provider);
const model = await providerImpl.getModel(modelId);
const result = await providerImpl.generate({
model,
prompt: body.prompt,
images: body.images,
parameters: body.parameters
});
return NextResponse.json(result);
}
} else {
// Legacy Gemini path (existing code)
}
```
3. Extract Gemini-specific logic into helper function for clarity
4. Ensure API key handling:
- For Gemini: use GEMINI_API_KEY from env
- For Replicate/fal: get from provider.getApiKey() (localStorage via client header)
5. Add client headers for API keys:
- Accept X-Replicate-API-Key and X-Fal-API-Key headers
- Pass to provider implementations
WHY: The route becomes a dispatcher that preserves existing Gemini behavior while enabling new providers.
1. Accept both formats:
- Legacy: { model, prompt, images, aspectRatio, resolution }
- New: { selectedModel, prompt, images, parameters }
2. If selectedModel provided and provider !== "gemini":
- Get provider from registry
- Get API key from request headers (X-Replicate-API-Key, X-Fal-API-Key)
- Call provider.generate()
- Return result
3. Otherwise use existing Gemini logic
4. Extract Gemini code into helper function for clarity
</action>
<verify>npm run build succeeds, existing Gemini generation still works</verify>
<done>Generate route dispatches to correct provider, Gemini still works as before</done>
<verify>npm run build, test Gemini generation still works</verify>
<done>API route dispatches to correct provider, Gemini unchanged</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] `npm run build` succeeds without errors
- [ ] Existing Gemini generation works (test with existing workflow)
- [ ] Replicate provider generate() method implemented
- [ ] fal.ai provider generate() method implemented
- [ ] API route accepts both legacy and new request formats
- [ ] `npm run build` succeeds
- [ ] Gemini generation works as before
- [ ] Replicate provider has generate() implementation
- [ ] fal.ai provider has generate() implementation
</verification>
<success_criteria>
- All tasks completed
- All verification checks pass
- Generate API routes to correct provider based on selectedModel
- Gemini generation unchanged for backward compatibility
- Replicate and fal.ai providers have generate() implementations
- Generate API routes requests to correct provider
- Provider generate() methods implemented
- Existing Gemini functionality preserved
</success_criteria>
<output>
After completion, create `.planning/phases/03-generate-node-refactor/03-02-SUMMARY.md`
Create `.planning/phases/03-generate-node-refactor/03-02-SUMMARY.md`
</output>

147
.planning/phases/03-generate-node-refactor/03-03-PLAN.md

@ -5,10 +5,10 @@ type: execute
---
<objective>
Ensure backward compatibility for existing workflows containing nanoBanana nodes.
Ensure backward compatibility for existing workflows.
Purpose: Existing saved workflows must load and execute correctly without modification.
Output: Migration logic, verified backward compatibility, updated workflow execution.
Purpose: Existing saved workflows must load and execute without modification.
Output: Migration logic, working execution with both legacy and new formats.
</objective>
<execution_context>
@ -18,137 +18,88 @@ Output: Migration logic, verified backward compatibility, updated workflow execu
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
# Prior plan context:
@.planning/phases/03-generate-node-refactor/03-01-SUMMARY.md
@.planning/phases/03-generate-node-refactor/03-02-SUMMARY.md
# Key source files:
@src/store/workflowStore.ts
@src/types/index.ts
@src/components/nodes/GenerateImageNode.tsx
**Tech stack available:** Zustand, workflow JSON persistence
**Established patterns:** Workflow load/save in workflowStore
**Constraining decisions:**
- Node type "nanoBanana" must remain unchanged in workflow files
- Legacy ModelType ("nano-banana", "nano-banana-pro") must still work
**Constraints:** Node type "nanoBanana" unchanged, legacy ModelType must work
</context>
<tasks>
<task type="auto">
<name>Task 1: Add migration logic for loaded workflows</name>
<name>Task 1: Add workflow migration logic</name>
<files>src/store/workflowStore.ts</files>
<action>
1. In loadWorkflow() or where workflow JSON is parsed, add migration:
- When loading a nanoBanana node without selectedModel field
- Automatically set selectedModel based on legacy model field:
```typescript
if (node.type === "nanoBanana" && node.data.model && !node.data.selectedModel) {
node.data.selectedModel = {
provider: "gemini",
modelId: node.data.model,
displayName: node.data.model === "nano-banana" ? "Nano Banana" : "Nano Banana Pro"
};
}
```
2. Keep legacy model field synced for saves:
- When selectedModel is Gemini, also update the model field
- This ensures old versions can still read the workflow
3. Add version field to workflow if not present:
- Set workflowVersion in saved JSON for future migrations
- Not strictly necessary but good practice
WHY: Migration on load allows old workflows to work without manual editing. Syncing both fields maintains bidirectional compatibility.
In workflow loading (loadWorkflow or where JSON is parsed):
1. For nanoBanana nodes without selectedModel, derive it from legacy model field:
```typescript
if (node.type === "nanoBanana" && node.data.model && !node.data.selectedModel) {
node.data.selectedModel = {
provider: "gemini",
modelId: node.data.model,
displayName: node.data.model === "nano-banana" ? "Nano Banana" : "Nano Banana Pro"
};
}
```
2. Keep both fields synced on save for bidirectional compatibility
</action>
<verify>Load an existing workflow file, check node data has both model and selectedModel</verify>
<done>Old workflows load with selectedModel populated, new saves include both fields</done>
<verify>Load existing workflow file, check node has selectedModel populated</verify>
<done>Old workflows load with selectedModel auto-populated</done>
</task>
<task type="auto">
<name>Task 2: Update executeWorkflow for multi-provider generation</name>
<name>Task 2: Update executeWorkflow for multi-provider</name>
<files>src/store/workflowStore.ts</files>
<action>
1. In executeWorkflow()'s nanoBanana case:
- Check if node has selectedModel
- If selectedModel exists and provider !== "gemini", pass API keys in headers:
```typescript
const headers: Record<string, string> = {
"Content-Type": "application/json"
};
if (nodeData.selectedModel?.provider === "replicate") {
const settings = get().providerSettings;
const replicateConfig = settings.providers.find(p => p.id === "replicate");
if (replicateConfig?.apiKey) {
headers["X-Replicate-API-Key"] = replicateConfig.apiKey;
}
}
// Similar for fal
```
2. Build request payload:
- Include selectedModel in request body for new providers
- Keep existing fields for Gemini backward compatibility
3. Handle response format:
- Provider generate() returns GenerationOutput with outputs array
- Extract first image output as outputImage
- Validate output type is "image" (video handled by separate node in Phase 6)
WHY: The store's execution logic needs to pass provider API keys (stored client-side) to the server route.
In executeWorkflow nanoBanana case:
1. Check for selectedModel
2. If non-Gemini provider, add API key headers from providerSettings:
```typescript
const headers = { "Content-Type": "application/json" };
if (nodeData.selectedModel?.provider === "replicate") {
const config = get().providerSettings.providers.find(p => p.id === "replicate");
if (config?.apiKey) headers["X-Replicate-API-Key"] = config.apiKey;
}
// Similar for fal
```
3. Include selectedModel in request body
4. Handle GenerationOutput response format
</action>
<verify>Execute workflow with Gemini model, verify generation still works</verify>
<done>Workflow execution handles both legacy and new model selection formats</done>
<verify>Execute workflow with Gemini model</verify>
<done>Execution works for both legacy and new model selection</done>
</task>
<task type="auto">
<name>Task 3: Update GenerateImageNode to handle legacy data</name>
<name>Task 3: Handle legacy data in GenerateImageNode UI</name>
<files>src/components/nodes/GenerateImageNode.tsx</files>
<action>
1. When component mounts or data changes:
- If model exists but not selectedModel, derive selectedModel from model
- Display the correct provider/model in dropdowns
2. Ensure UI state reflects actual node data:
- Provider dropdown shows "gemini" for legacy nodes
- Model dropdown shows correct Gemini model name
3. When saving defaults, save in both formats:
- saveGenerateImageDefaults should save both model and selectedModel
- Ensures new nodes get correct defaults
WHY: The UI must correctly display legacy nodes that were migrated on load.
1. On mount/data change: if model exists but not selectedModel, derive selectedModel
2. Ensure dropdowns reflect correct provider/model for migrated nodes
3. Save defaults in both formats for compatibility
</action>
<verify>Open workflow with old nanoBanana node, verify dropdowns show correct selection</verify>
<done>GenerateImageNode displays correct provider/model for both legacy and new nodes</done>
<verify>Open workflow with old nanoBanana node, verify correct selection shown</verify>
<done>UI correctly displays legacy nodes</done>
</task>
</tasks>
<verification>
Before declaring plan complete:
- [ ] `npm run build` succeeds without errors
- [ ] Load existing workflow with nanoBanana nodes - nodes display correctly
- [ ] Execute existing workflow - Gemini generation works
- [ ] Create new Generate node - defaults are correct
- [ ] Save and reload workflow - data persists correctly
- [ ] `npm run build` succeeds
- [ ] Existing workflow loads without errors
- [ ] Legacy Gemini execution works
- [ ] New provider selection persists correctly
</verification>
<success_criteria>
- All tasks completed
- All verification checks pass
- Existing workflows load and execute without errors
- Migration logic populates selectedModel from legacy model field
- Both old and new format nodes work in same workflow
- Existing workflows load and execute unchanged
- Migration auto-populates selectedModel
- Both formats work in same workflow
- Phase 3 complete
</success_criteria>
<output>
After completion, create `.planning/phases/03-generate-node-refactor/03-03-SUMMARY.md`
Create `.planning/phases/03-generate-node-refactor/03-03-SUMMARY.md`
</output>

Loading…
Cancel
Save