filtering, cost/UX improvements
- Replace interval polling with signal-only updates for both workspace
panels and iterative editor (one-time initial check
as fallback)
- Only include reference images when user explicitly
@mentions a
character in the prompt (previously fell back to all
references)
- Return actual generation cost from server instead of
static estimates
- Add "Reset to original" button to revert to initial
source image
- Support generating multiple images (1-4) per request
with pick grid
- Fix across all endpoints: createPanel, enhancePanel,
iterateGenerate,
smartCreateChapter, bulkCreatePanels
12 KiB
Plan: Iterative Panel Editor (Chat-Based Panel Refinement)
Overview
A chat-bot-like experience for progressively refining comic panel images. Users start with a blank canvas or existing panel, describe changes via prompts and sketch annotations, and iteratively generate improved images until satisfied. Each "send" produces a new AI-generated image using the previous result as source. Users can revert to any earlier iteration and branch from there.
Architecture
New standalone component — not an extension of PanelModal (already ~1000 lines with 4 tabs). The iterative editor is a fundamentally different UX paradigm: persistent chat-like session vs. one-shot modal.
No server changes needed — the existing enhancePanel, createPanel, and replacePanelImage mutations are sufficient.
Implementation Status
Completed (Phases 1-6)
- Component shell —
IterativePanelEditor.tsx(~650 lines): fullscreen modal with chat history + controls sidebar - IterationMessage —
IterationMessage.tsx(~99 lines): chat entry with image thumbnail, source selection, cost/annotation badges - Styles —
IterativePanelEditor.module.scss(~270 lines): split layout, mobile responsive at 768px - Entry points wired — "Iterative Edit" in PanelCard context menu + PanelDetailDrawer actions
- Workspace integration —
iterativeEditorStateinindex.tsxwithhandleOpenIterativeEditor/handleOpenIterativeEditorNew - Generation flow — First send →
createPanelMutation(txt2img), subsequent →enhancePanelMutation(img2img withforceGenerate: true) - Polling — 1.5s interval via
utils.comics.pollPanelStatus.fetch, updates iteration entry on Ready/Failed - Iteration history — Chat display with auto-scroll, source tracking
- Revert — "Use as source" on any past iteration, purely client-side
- Annotations —
DrawingEditorModalintegration viadialogStore.trigger, uploads annotated blob to CF, resets after each generation - Commit — "Commit to Panel" calls
replacePanelImageif reverted, else just closes - Controls sidebar — Model selector (
COMIC_MODEL_OPTIONS),AspectRatioSelector, enhance prompt toggle, referenced characters display,ImageSelectionSectionfor over-budget references
Completed (Phase 7: Polish)
- Confirmation dialog on close —
openConfirmModalwarns when iterations exist and user tries to close - Keyboard shortcuts — Ctrl/Cmd+Enter to send via
sendButtonRefwrapper - Error retry — "Retry" button on failed iterations restores prompt/source and removes failed entry
- Cost running total — Session total buzz bar at top of chat area
- Empty source placeholder — Improved visual with
IconPhotoPlusand clearer copy - Generation progress indicator — Mantine
Loaderwith pulsing "Generating..." text and skeleton pulse animation
Remaining (Phase 7: Polish — Deferred)
- Mobile testing — Verify the collapsed single-column layout works well in practice
- Discard/cancel generation — Allow canceling a pending generation mid-flight
- Accessibility — Focus management when modal opens, screen reader labels on iterative messages
Future Enhancements (Not in scope)
- Branching tree view — When reverting and generating from an earlier iteration, show a tree instead of linear history
- Persist iteration history — Save history server-side so users can resume sessions across page loads
- Side-by-side compare — Compare two iterations visually before choosing one
- Prompt templates — Quick-action buttons like "Add more detail", "Change lighting", "Remove background"
UI Layout
┌─────────────────────────────────────────────────────┐
│ Iterative Panel Editor [X] │
├──────────────────────────┬──────────────────────────┤
│ Chat / History (60%) │ Controls Sidebar (40%) │
│ │ │
│ ┌─ Iteration 1 ──────┐ │ [Current source image] │
│ │ "A beach scene" │ │ │
│ │ [result image] │ │ Model: [Nano Banana ▼] │
│ │ ○ Use as source │ │ Aspect: [4:3] [16:9]...│
│ └─────────────────────┘ │ Enhance prompt: [✓] │
│ │ │
│ ┌─ Iteration 2 ──────┐ │ References: │
│ │ "Add @Hero here" │ │ [@Hero] [2/5 images] │
│ │ [result image] ★ │ │ [@Villain] [all] │
│ │ ● Current source │ │ │
│ └─────────────────────┘ │ │
│ │ │
│ ┌─ Input area ────────┐ │ │
│ │ @mention prompt... │ │ │
│ │ [✏ Annotate] [💎42] │ │ [Commit to Panel] │
│ └─────────────────────┘ │ │
└──────────────────────────┴──────────────────────────┘
On mobile: single column with controls in a collapsible section at the top.
State Management
All state is client-only during the session. The server already stores panels/workflows — iteration history is ephemeral UI state for the editing session.
interface IterationEntry {
id: string; // client-generated unique ID
prompt: string; // prompt used for this iteration
annotated: boolean; // whether sketch annotations were applied
sourceImage: SourceImage | null; // the source image fed INTO this generation
resultImage: SourceImage | null; // the image produced (null while generating)
cost: number; // buzz spent
timestamp: Date;
status: 'generating' | 'ready' | 'error';
errorMessage?: string;
}
interface SourceImage {
url: string; // CF image ID or URL
previewUrl: string; // thumbnail URL
width: number;
height: number;
}
// Core component state
const [iterations, setIterations] = useState<IterationEntry[]>([]);
const [currentSource, setCurrentSource] = useState<SourceImage | null>(initialSource);
const [annotationElements, setAnnotationElements] = useState<DrawingElement[]>([]);
const [originalSourceUrl, setOriginalSourceUrl] = useState<string | null>(null);
const [stagingPanelId, setStagingPanelId] = useState<number | null>(existingPanelId);
const [isGenerating, setIsGenerating] = useState(false);
currentSource starts as:
- The existing panel image (if editing an existing panel)
null(if starting fresh — first generation uses txt2img)
Generation Flow
- User types prompt, optionally annotates current source image
- Clicks BuzzTransactionButton (shows cost per generation)
- If first generation (no source): calls
createPanelMutation→ creates panel at target position - If subsequent: calls
enhancePanelMutationwith current source assourceImageUrl,forceGenerate: true - Polls panel status until Ready (1.5s interval via
pollPanelStatus) - Result image added to iteration history, becomes new
currentSource - Annotations and
originalSourceUrlreset to[]/null
The staging panel approach: first "send" creates the panel normally (it appears in the chapter at the target position). Subsequent sends use enhancePanelMutation which deletes the old panel and creates a new one at the same position. Only one panel exists in the chapter at any time.
Revert Mechanism
When the user clicks a previous iteration's "Use as source" button:
- Set
currentSourceto that iteration'sresultImage - Clear
annotationElements(annotations are baked into each generated image) - Clear
originalSourceUrl - The next "send" uses this reverted image as source
This is purely a client-side state change. No server calls. The iteration history remains intact — users can see the full history even after reverting.
Annotations
- Annotations reset after each generation — they're composited into the source image when generating, producing a new image with annotations "baked in"
- After generation completes:
annotationElements → [],originalSourceUrl → null - User can annotate the NEW result image for the next iteration
- Uses existing
DrawingEditorModalcomponent, opened viadialogStore.triggerwithconfirmLabel: 'Apply Annotations'
Commit to Panel
When user clicks "Commit to Panel":
if (currentSource.url !== latestPanelImageUrl) {
// User reverted to a non-latest image — replace the panel image
await replacePanelImageMutation.mutateAsync({
panelId: stagingPanelId,
imageUrl: currentSource.url,
});
}
onClose();
refetch(); // refresh project data
If the latest generation is already the current source, the staging panel already has the right image — just close.
Cost Display
- BuzzTransactionButton shows
panelCost + (enhancePrompt ? enhanceCost : 0)per send - Each
IterationMessageshows a small badge with the cost paid - Persistent info text: "Each generation costs ~{cost} Buzz"
Key Design Decisions
| Question | Decision | Rationale |
|---|---|---|
| New component vs extend PanelModal? | New component | Different UX paradigm; avoids prop explosion in already-complex modal |
| History persistence? | Client-only during session | Server stores panels/workflows; no separate history table needed |
| Staging approach? | First send creates panel, subsequent sends replace it | One panel exists in chapter at all times; no cleanup needed |
| Revert mechanism? | Change currentSource state |
Pure client-side; click any past image to use as next source |
| Annotations across iterations? | Reset after each generation | Annotations are baked into the generated image |
| First gen (no source image)? | createPanelMutation (txt2img), then enhancePanelMutation (img2img) |
Matches existing pipeline |
| Commit flow? | Close editor; replacePanelImage if reverted |
Staging panel already exists; minimal server work |
File Changes
New Files (Created)
| File | Description | Lines |
|---|---|---|
src/components/Comics/IterativePanelEditor.tsx |
Main modal: chat history, controls sidebar, generation orchestration | ~650 |
src/components/Comics/IterationMessage.tsx |
Single chat entry: prompt, image thumbnail, "Use as source", cost badge | ~99 |
src/components/Comics/IterativePanelEditor.module.scss |
Layout styles for chat, sidebar, messages, mobile responsive | ~270 |
Modified Files (Done)
| File | Changes |
|---|---|
src/pages/comics/project/[id]/index.tsx |
Added iterativeEditorState state, handleOpenIterativeEditor/handleOpenIterativeEditorNew handlers, renders <IterativePanelEditor> with all props |
src/components/Comics/PanelCard.tsx |
Added onIterativeEdit prop, "Iterative Edit" menu item (IconMessages, only on Ready panels with images) |
src/components/Comics/PanelDetailDrawer.tsx |
Added onIterativeEdit prop, "Iterative Edit" button in drawer actions |
Server — No Changes
Existing mutations are sufficient:
createPanel— first txt2img generationenhancePanel— subsequent img2img iterationsreplacePanelImage— commit a reverted imagegetCostEstimate/getPromptEnhanceCostEstimate— cost display
Components Reused
These existing components are imported directly:
MentionTextarea— prompt input with @mention supportAspectRatioSelector— aspect ratio pickerBuzzTransactionButton— cost-aware submit buttonDrawingEditorModal— sketch annotation overlay (viadialogStore.trigger)ImageSelectionSection(from PanelModal) — reference image picker when over budgetuseCFImageUpload— upload annotated images to CloudFlare