docs: add AI builder UX improvements implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
354
docs/plans/2026-02-24-ai-builder-ux-improvements-plan.md
Normal file
354
docs/plans/2026-02-24-ai-builder-ux-improvements-plan.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# AI Builder UX Improvements Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Three frontend-only UX improvements: always-available Publish button, "Generate All" branches in the AI wizard, and rotating activity messages during generation.
|
||||
|
||||
**Architecture:** All changes are frontend-only. Feature 1 is a one-line fix in `TreeEditorPage.tsx`. Features 2 and 3 involve adding state to `aiFlowBuilderStore.ts` and updating `BranchDetailView.tsx` and `GeneratingAnimation.tsx`. No backend changes.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, Zustand, Tailwind CSS v3, Lucide React icons.
|
||||
|
||||
**Design doc:** `docs/plans/2026-02-24-ai-builder-ux-improvements.md`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Fix Publish button — remove `!isDirty` gate
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/pages/TreeEditorPage.tsx` (line ~654)
|
||||
|
||||
**Step 1: Make the change**
|
||||
|
||||
Find this line in `TreeEditorPage.tsx`:
|
||||
```tsx
|
||||
disabled={isSaving || !isDirty || hasBlockingErrors}
|
||||
```
|
||||
Change to:
|
||||
```tsx
|
||||
disabled={isSaving || hasBlockingErrors}
|
||||
```
|
||||
That's the only change needed. The button's `title` text references "Ctrl+S when no errors" which is still accurate — leave it.
|
||||
|
||||
**Step 2: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build 2>&1 | tail -20
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/pages/TreeEditorPage.tsx
|
||||
git commit -m "fix: allow publishing clean drafts without requiring local edits"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add rotating activity messages to `GeneratingAnimation`
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/components/ai-builder/GeneratingAnimation.tsx`
|
||||
|
||||
**Step 1: Read the current file**
|
||||
|
||||
Read `frontend/src/components/ai-builder/GeneratingAnimation.tsx` to understand current structure before changing it.
|
||||
|
||||
**Step 2: Rewrite with rotating messages**
|
||||
|
||||
Replace the entire file content with:
|
||||
|
||||
```tsx
|
||||
import { useEffect, useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const MESSAGES = [
|
||||
'Setting up your flow...',
|
||||
'Building diagnostic paths...',
|
||||
'Putting the pieces in place...',
|
||||
'Almost there...',
|
||||
] as const
|
||||
|
||||
const MESSAGE_DURATIONS = [4000, 8000, 8000, Infinity] // ms each message shows
|
||||
|
||||
interface GeneratingAnimationProps {
|
||||
branchContext?: { current: number; total: number }
|
||||
}
|
||||
|
||||
export function GeneratingAnimation({ branchContext }: GeneratingAnimationProps) {
|
||||
const [messageIndex, setMessageIndex] = useState(0)
|
||||
|
||||
// Reset and advance message on mount/remount
|
||||
useEffect(() => {
|
||||
setMessageIndex(0)
|
||||
let current = 0
|
||||
|
||||
const advance = () => {
|
||||
current += 1
|
||||
if (current < MESSAGES.length - 1) {
|
||||
setMessageIndex(current)
|
||||
timer = setTimeout(advance, MESSAGE_DURATIONS[current])
|
||||
} else {
|
||||
setMessageIndex(MESSAGES.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
let timer = setTimeout(advance, MESSAGE_DURATIONS[0])
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-10">
|
||||
{/* Spinner */}
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-primary" />
|
||||
|
||||
{/* Branch context (Generate All mode) */}
|
||||
{branchContext && (
|
||||
<p className="text-xs font-label uppercase tracking-wide text-muted-foreground">
|
||||
Branch {branchContext.current} of {branchContext.total}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Rotating message */}
|
||||
<p
|
||||
key={messageIndex}
|
||||
className={cn(
|
||||
'text-sm text-muted-foreground transition-opacity duration-500',
|
||||
)}
|
||||
>
|
||||
{MESSAGES[messageIndex]}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build 2>&1 | tail -20
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/ai-builder/GeneratingAnimation.tsx
|
||||
git commit -m "feat: add rotating activity messages to generation loading state"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add `generateAllBranchDetails` and cancel to the store
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/store/aiFlowBuilderStore.ts`
|
||||
|
||||
**Step 1: Read the current store**
|
||||
|
||||
Read `frontend/src/store/aiFlowBuilderStore.ts` fully to understand current state shape before modifying.
|
||||
|
||||
**Step 2: Add new state fields and actions**
|
||||
|
||||
Add to the `AIFlowBuilderState` interface (after `isLoading: boolean`):
|
||||
```tsx
|
||||
isGeneratingAll: boolean
|
||||
stopGeneratingAll: boolean
|
||||
generateAllBranchDetails: () => Promise<void>
|
||||
cancelGenerateAll: () => void
|
||||
```
|
||||
|
||||
Add to the initial state in `create()(...)` (after `isLoading: false`):
|
||||
```tsx
|
||||
isGeneratingAll: false,
|
||||
stopGeneratingAll: false,
|
||||
```
|
||||
|
||||
Add the two new actions after `assemble`:
|
||||
|
||||
```tsx
|
||||
generateAllBranchDetails: async () => {
|
||||
const { selectedBranches, generateBranchDetail } = get()
|
||||
const undetailed = selectedBranches.filter((b) => !b.steps)
|
||||
if (undetailed.length === 0) return
|
||||
|
||||
set({ isGeneratingAll: true, stopGeneratingAll: false, error: null })
|
||||
|
||||
for (const branch of undetailed) {
|
||||
if (get().stopGeneratingAll) break
|
||||
// Set currentBranchIndex so tabs show the active branch
|
||||
const idx = get().selectedBranches.findIndex((b) => b.name === branch.name)
|
||||
if (idx !== -1) set({ currentBranchIndex: idx })
|
||||
await generateBranchDetail(branch.name)
|
||||
// If generateBranchDetail set phase to 'error', stop
|
||||
if (get().phase === 'error') break
|
||||
}
|
||||
|
||||
set({ isGeneratingAll: false })
|
||||
},
|
||||
|
||||
cancelGenerateAll: () => {
|
||||
set({ stopGeneratingAll: true })
|
||||
},
|
||||
```
|
||||
|
||||
Also add `isGeneratingAll: false, stopGeneratingAll: false` to the `reset()` action's `set({...})` call.
|
||||
|
||||
**Step 3: Verify TypeScript compiles**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build 2>&1 | tail -20
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/store/aiFlowBuilderStore.ts
|
||||
git commit -m "feat: add generateAllBranchDetails and cancelGenerateAll to AI builder store"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Update `BranchDetailView` with Generate All UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/components/ai-builder/BranchDetailView.tsx`
|
||||
|
||||
**Step 1: Read the current file**
|
||||
|
||||
Read `frontend/src/components/ai-builder/BranchDetailView.tsx` fully.
|
||||
|
||||
**Step 2: Add imports and pull new store state**
|
||||
|
||||
Add `Zap, Square` to the lucide-react import (Zap = lightning bolt for "Generate All", Square = stop).
|
||||
|
||||
Pull new state from store in the component:
|
||||
```tsx
|
||||
const {
|
||||
// existing...
|
||||
isGeneratingAll,
|
||||
generateAllBranchDetails,
|
||||
cancelGenerateAll,
|
||||
} = useAIFlowBuilderStore()
|
||||
```
|
||||
|
||||
**Step 3: Add Generate All / Stop button above branch tabs**
|
||||
|
||||
After the opening `<div className="flex flex-col gap-4">` and before the branch tabs div, add:
|
||||
|
||||
```tsx
|
||||
{/* Generate All / Stop control */}
|
||||
{(() => {
|
||||
const undetailedCount = selectedBranches.filter((b) => !b.steps).length
|
||||
if (undetailedCount === 0) return null
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{undetailedCount} branch{undetailedCount !== 1 ? 'es' : ''} need detail
|
||||
</span>
|
||||
{isGeneratingAll ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={cancelGenerateAll}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-red-400/30 bg-red-400/10 px-3 py-1.5 text-xs font-medium text-red-400 hover:bg-red-400/20"
|
||||
>
|
||||
<Square className="h-3 w-3" />
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={generateAllBranchDetails}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-gradient-brand px-3 py-1.5 text-xs font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
<Zap className="h-3 w-3" />
|
||||
Generate All
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
```
|
||||
|
||||
**Step 4: Disable individual controls during `isGeneratingAll`**
|
||||
|
||||
On the "Generate Detail" button (the primary one in the empty-state section):
|
||||
```tsx
|
||||
disabled={isLoading || isGeneratingAll}
|
||||
```
|
||||
|
||||
On the "Skip" button:
|
||||
```tsx
|
||||
disabled={isGeneratingAll}
|
||||
// also add: className includes opacity-50 when disabled
|
||||
```
|
||||
|
||||
On the "Regenerate" button:
|
||||
```tsx
|
||||
disabled={isLoading || isGeneratingAll}
|
||||
```
|
||||
|
||||
**Step 5: Pass `branchContext` to `GeneratingAnimation`**
|
||||
|
||||
The `GeneratingAnimation` is rendered when `phase === 'generating' && isLoading`. Update that render:
|
||||
|
||||
```tsx
|
||||
if (phase === 'generating' && isLoading) {
|
||||
return (
|
||||
<GeneratingAnimation
|
||||
branchContext={
|
||||
isGeneratingAll
|
||||
? {
|
||||
current: selectedBranches.filter((b) => b.steps).length + 1,
|
||||
total: selectedBranches.length,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Step 6: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build 2>&1 | tail -20
|
||||
```
|
||||
Expected: no errors.
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/ai-builder/BranchDetailView.tsx
|
||||
git commit -m "feat: add Generate All button and per-branch progress to AI builder detail stage"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Push and verify
|
||||
|
||||
**Step 1: Push branch**
|
||||
|
||||
```bash
|
||||
git push
|
||||
```
|
||||
|
||||
**Step 2: Verify CI passes**
|
||||
|
||||
```bash
|
||||
gh pr checks 88 2>&1 | head -20
|
||||
```
|
||||
|
||||
Expected: all checks passing (or wait for them to run).
|
||||
|
||||
**Step 3: Manual smoke test checklist**
|
||||
|
||||
- [ ] Open a fresh AI-generated draft in the tree editor → Publish button is enabled
|
||||
- [ ] Open AI Flow Builder, complete foundation → scaffold → select branches
|
||||
- [ ] On detail stage: "Generate All" button is visible
|
||||
- [ ] Click "Generate All" → branches generate one at a time, tabs show progress, "Branch X of Y" appears in animation
|
||||
- [ ] "Stop" button appears during run, clicking it halts after current branch
|
||||
- [ ] Activity messages cycle: "Setting up your flow..." → "Building diagnostic paths..." → "Putting the pieces in place..." → "Almost there..."
|
||||
- [ ] Single-branch generate still works as before
|
||||
Reference in New Issue
Block a user