* feat: Add TreeCanvasNode inline editor card component Replaces modal-based node editing with inline expand/collapse cards. Each card shows node type, title, and options in compact mode, then renders the full edit form inline on expand — no modal required. Local draft state with save/cancel prevents premature store writes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: Add TreeCanvas layout with visual branching and orchestration Replaces NodeList + TreePreviewPanel with a single full-width canvas. Decision nodes branch horizontally; action/solution nodes flow vertically. Inline type picker adds nodes without modal interruption. Handles pending link resolution, inbound reference cleanup on delete, and selection sync. CSS dot-grid background + connector lines for structure clarity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: Update forms for inline safety, add MetadataSidePanel, update layout - NodeFormDecision: option reorder via onUpdate (no premature store writes) - NodePicker: add allowCreate prop (default true) to hide Create New options during inline canvas editing, preventing side-effect node creation - MetadataSidePanel: 320px right slide-in overlay wrapping TreeMetadataForm, closes on backdrop click, close button, and Escape key - TreeEditorLayout: Flow mode now renders full-width TreeCanvas + MetadataSidePanel overlay; Code mode unchanged (Monaco + preview split) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: Wire toolbar metadata toggle and integrate canvas layout - Add isMetadataOpen state in TreeEditorPage - Add Metadata toolbar button (visible in Flow mode only) - Auto-close metadata panel when switching to Code mode - Pass isMetadataOpen/onCloseMetadata props through TreeEditorLayout - Update Flow mode toggle tooltip to reflect new canvas editing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add canvas UX fixes design doc (scroll, tooltips, answer stubs) Captures approved design for three post-implementation UX improvements to the tree canvas editor: card scroll fix, info tooltip replacement for hint text, and the new 'answer' node type for sketching decision branches before assigning types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add implementation plan for canvas UX fixes 12-task plan covering scroll fix, info tooltips, and answer stub node type. Each task has exact file paths, code, and build verification steps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: make canvas card expanded area scrollable with sticky header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add fullscreen toggle to Modal, enable in NodeEditorModal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add reusable InfoTip component for field-level help Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace hint paragraphs with InfoTip tooltips in NodeFormDecision Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace hint paragraphs with InfoTip tooltips in NodeFormAction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace hint paragraphs with InfoTip tooltips in NodeFormResolution Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add 'answer' to NodeType union for branch placeholder stubs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add AnswerStubCard component for unresolved branch placeholders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: guard NODE_TYPE_CONFIG lookup against 'answer' type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: redesign NodeFormDecision to label-only options, remove NodePicker Users now type answer labels only. Stub nodes are created automatically by TreeCanvas when the decision node is saved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: auto-create answer stubs on decision save, render AnswerStubCard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add answer type to all Record<NodeType> icon and color maps Fixes NodeList, ContinuationModal, NodePicker, and TreePreviewNode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: allow 'answer' type in tree drafts, block on publish Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: block publish if unresolved answer stub nodes exist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: serialize 'answer' stub nodes in markdown output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add defensive guard for answer nodes in session navigation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add delete button with confirmation to AnswerStubCard Adds an inline delete flow to answer stub placeholder cards: - Trash icon button (top-right, subtle) visible in idle state - Click reveals "Delete this stub?" confirmation with Delete/Cancel - Confirmed delete calls onDelete(nodeId) wired to handleDelete in TreeCanvas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: prevent category Cancel overflow and add Tab/Enter to create options - TreeMetadataForm: add min-w-0 + shrink-0 to keep Cancel button in-panel - NodeFormDecision: Tab or Enter on the last non-empty option input adds a new option and auto-focuses it; empty last input lets Tab pass through normally Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: re-sync draft from store when canvas card is opened When a decision node is saved with new options, stub next_node_id values are written back to the store. But the local draft was initialized once at mount and never refreshed, so reopening the card gave a stale draft with empty next_node_ids — causing duplicate stubs on every subsequent save. Fix: reset draft from the live node whenever isExpanded transitions to true. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix+feat: blank options, stub card dismiss, collapsible subtrees - TreeCanvas: strip blank-label options on save so they don't generate stubs; also filter them from the unlinked-option add-button list - AnswerStubCard: collapse type-picker when clicking outside the card - TreeCanvasNode: add subtree collapse toggle button (ChevronsDownUp icon) visible in compact mode when the node has children - TreeCanvas: track collapsedNodeIds; hide subtree behind a clickable "N nodes hidden" pill when collapsed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: stop connector fork line from overlapping child cards Replace the two-element approach (separate fork line + child lanes div with mismatched maxWidth values) with a single relative-positioned container. The fork line is absolutely positioned and its left/right are calculated from the number of children so it spans exactly from the center of the first lane to the center of the last lane. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace Show Drafts checkbox with Drafts tab in Flow Library - Remove the out-of-place checkbox; add 'Drafts' as a tab alongside All | Troubleshooting | Projects | Maintenance - Drafts tab sets showDrafts=true + typeFilter='all' so the API filter still works correctly via include_drafts - Move SortDropdown to the right side next to ViewToggle, so both secondary controls are grouped together Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1004 lines
30 KiB
Markdown
1004 lines
30 KiB
Markdown
# Canvas UX Fixes Implementation Plan
|
||
|
||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||
|
||
**Goal:** Fix three UX problems in the TreeCanvas editor: card scroll, noisy hint text, and forced child-type selection when building decision nodes.
|
||
|
||
**Architecture:** Three independent fixes applied to the canvas editor components only. Fix 1 is a pure CSS change. Fix 2 replaces `<p>` hint text with native `title` tooltips on ⓘ badges. Fix 3 introduces a new `'answer'` NodeType — a branch placeholder that the user converts to a real type by clicking it.
|
||
|
||
**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand (existing `treeEditorStore`), FastAPI backend (`tree_validation.py`)
|
||
|
||
**Working directory:** `/home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas`
|
||
|
||
---
|
||
|
||
## Fix 1: Card Scroll
|
||
|
||
### Task 1: Make expanded card area scrollable with sticky header
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx:164-321`
|
||
|
||
**Step 1: Open the file and locate the card header div (expanded state)**
|
||
|
||
The card header is the `<div>` at line 165. When expanded it shows the action buttons (save/cancel/etc). We need this row to be sticky.
|
||
|
||
Find this block (around line 165):
|
||
```tsx
|
||
<div
|
||
className={cn(
|
||
'flex items-center gap-2 px-3 py-2.5',
|
||
!isExpanded && 'cursor-pointer hover:bg-accent/50 rounded-t-xl',
|
||
!isExpanded && 'rounded-xl'
|
||
)}
|
||
onClick={!isExpanded ? handleCardClick : undefined}
|
||
>
|
||
```
|
||
|
||
Change it to:
|
||
```tsx
|
||
<div
|
||
className={cn(
|
||
'flex items-center gap-2 px-3 py-2.5',
|
||
!isExpanded && 'cursor-pointer hover:bg-accent/50 rounded-t-xl',
|
||
!isExpanded && 'rounded-xl',
|
||
isExpanded && 'sticky top-0 z-10 bg-card rounded-t-xl'
|
||
)}
|
||
onClick={!isExpanded ? handleCardClick : undefined}
|
||
>
|
||
```
|
||
|
||
**Step 2: Make the expanded editing area scrollable**
|
||
|
||
Find the expanded editing area div (around line 324):
|
||
```tsx
|
||
{isExpanded && (
|
||
<div className="border-t border-border px-3 pb-3 pt-3">
|
||
```
|
||
|
||
Change it to:
|
||
```tsx
|
||
{isExpanded && (
|
||
<div className="border-t border-border px-3 pb-3 pt-3 max-h-[70vh] overflow-y-auto">
|
||
```
|
||
|
||
**Step 3: Build and verify no TypeScript errors**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | tail -20
|
||
```
|
||
|
||
Expected: Build exits with code 0, no errors mentioning TreeCanvasNode.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/components/tree-editor/TreeCanvasNode.tsx
|
||
git commit -m "fix: make canvas card expanded area scrollable with sticky header
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Fix 2: Info Tooltips
|
||
|
||
### Task 2: Replace hint text in NodeFormDecision
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx`
|
||
|
||
The ⓘ badge pattern to use throughout Fix 2:
|
||
```tsx
|
||
<span
|
||
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
|
||
title="YOUR TOOLTIP TEXT HERE"
|
||
>
|
||
i
|
||
</span>
|
||
```
|
||
|
||
**Step 1: Find the root node hint paragraph inside the Question field**
|
||
|
||
Around line 89–93:
|
||
```tsx
|
||
{isRootNode && (
|
||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||
What's the main question to diagnose the issue?
|
||
</p>
|
||
)}
|
||
```
|
||
|
||
Remove this `<p>` block entirely. The input's placeholder already conveys the intent.
|
||
|
||
**Step 2: Find the options hint paragraphs**
|
||
|
||
Around lines 136–143:
|
||
```tsx
|
||
{isRootNode ? (
|
||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||
Add as many options as needed (A, B, C, D...). Each option leads to a completely different troubleshooting path.
|
||
</p>
|
||
) : (
|
||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||
Each option can branch to a different next step.
|
||
</p>
|
||
)}
|
||
```
|
||
|
||
Replace both `<p>` tags with a ⓘ tooltip on the Options label. Change the label section (around line 133) from:
|
||
```tsx
|
||
<label className="block text-sm font-medium text-foreground">
|
||
{isRootNode ? 'Answer Options (Branches)' : 'Options'} <span className="text-red-400">*</span>
|
||
</label>
|
||
{isRootNode ? (
|
||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||
Add as many options as needed (A, B, C, D...). Each option leads to a completely different troubleshooting path.
|
||
</p>
|
||
) : (
|
||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||
Each option can branch to a different next step.
|
||
</p>
|
||
)}
|
||
```
|
||
|
||
To:
|
||
```tsx
|
||
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||
{isRootNode ? 'Answer Options (Branches)' : 'Options'} <span className="text-red-400">*</span>
|
||
<span
|
||
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
|
||
title={isRootNode
|
||
? "Add as many options as needed (A, B, C, D...). Each option leads to a different troubleshooting path."
|
||
: "Each option can branch to a different next step."}
|
||
>
|
||
i
|
||
</span>
|
||
</label>
|
||
```
|
||
|
||
**Step 3: Build to check for TS errors**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | tail -20
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/components/tree-editor/NodeFormDecision.tsx
|
||
git commit -m "fix: replace hint paragraphs with info tooltips in NodeFormDecision
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Replace hint text in NodeFormAction
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/NodeFormAction.tsx`
|
||
|
||
**Step 1: Find the description hint paragraph**
|
||
|
||
Around lines 91–93:
|
||
```tsx
|
||
<p className="mb-1 text-xs text-muted-foreground">
|
||
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
|
||
</p>
|
||
```
|
||
|
||
Change the Description label + remove the hint:
|
||
```tsx
|
||
// Before
|
||
<label className="block text-sm font-medium text-foreground">
|
||
Description
|
||
</label>
|
||
<p className="mb-1 text-xs text-muted-foreground">
|
||
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
|
||
</p>
|
||
|
||
// After
|
||
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||
Description
|
||
<span
|
||
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
|
||
title="Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`"
|
||
>
|
||
i
|
||
</span>
|
||
</label>
|
||
```
|
||
|
||
**Step 2: Find the commands hint paragraph**
|
||
|
||
Around lines 124–126:
|
||
```tsx
|
||
<p className="mb-2 text-xs text-muted-foreground">
|
||
PowerShell or CLI commands to execute
|
||
</p>
|
||
```
|
||
|
||
Change the Commands label + remove the hint:
|
||
```tsx
|
||
// Before
|
||
<label className="block text-sm font-medium text-foreground">
|
||
Commands
|
||
</label>
|
||
<p className="mb-2 text-xs text-muted-foreground">
|
||
PowerShell or CLI commands to execute
|
||
</p>
|
||
|
||
// After
|
||
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||
Commands
|
||
<span
|
||
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
|
||
title="PowerShell or CLI commands to execute"
|
||
>
|
||
i
|
||
</span>
|
||
</label>
|
||
```
|
||
|
||
**Step 3: Build to check for TS errors**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | tail -20
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/components/tree-editor/NodeFormAction.tsx
|
||
git commit -m "fix: replace hint paragraphs with info tooltips in NodeFormAction
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Replace hint text in NodeFormResolution
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/NodeFormResolution.tsx`
|
||
|
||
**Step 1: Find the description hint paragraph**
|
||
|
||
Around lines 86–88:
|
||
```tsx
|
||
<p className="mb-1 text-xs text-muted-foreground">
|
||
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
|
||
</p>
|
||
```
|
||
|
||
Change the Description label + remove the hint:
|
||
```tsx
|
||
// Before
|
||
<label className="block text-sm font-medium text-foreground">
|
||
Description
|
||
</label>
|
||
<p className="mb-1 text-xs text-muted-foreground">
|
||
Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`
|
||
</p>
|
||
|
||
// After
|
||
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||
Description
|
||
<span
|
||
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
|
||
title="Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`"
|
||
>
|
||
i
|
||
</span>
|
||
</label>
|
||
```
|
||
|
||
**Step 2: Find the resolution steps hint paragraph**
|
||
|
||
Around lines 118–120:
|
||
```tsx
|
||
<p className="mb-2 text-xs text-muted-foreground">
|
||
Step-by-step instructions for resolving the issue
|
||
</p>
|
||
```
|
||
|
||
Change the Resolution Steps label + remove the hint:
|
||
```tsx
|
||
// Before
|
||
<label className="block text-sm font-medium text-foreground">
|
||
Resolution Steps
|
||
</label>
|
||
<p className="mb-2 text-xs text-muted-foreground">
|
||
Step-by-step instructions for resolving the issue
|
||
</p>
|
||
|
||
// After
|
||
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||
Resolution Steps
|
||
<span
|
||
className="inline-flex items-center justify-center h-3.5 w-3.5 rounded-full border border-muted-foreground/40 text-[9px] text-muted-foreground cursor-help shrink-0"
|
||
title="Step-by-step instructions for resolving the issue"
|
||
>
|
||
i
|
||
</span>
|
||
</label>
|
||
```
|
||
|
||
**Step 3: Build to check for TS errors**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | tail -20
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/components/tree-editor/NodeFormResolution.tsx
|
||
git commit -m "fix: replace hint paragraphs with info tooltips in NodeFormResolution
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Fix 3: Answer Stubs
|
||
|
||
### Task 5: Add `'answer'` to the NodeType union
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/types/tree.ts:4`
|
||
|
||
**Step 1: Add `'answer'` to NodeType**
|
||
|
||
Current line 4:
|
||
```typescript
|
||
export type NodeType = 'decision' | 'action' | 'solution'
|
||
```
|
||
|
||
Change to:
|
||
```typescript
|
||
export type NodeType = 'decision' | 'action' | 'solution' | 'answer'
|
||
```
|
||
|
||
**Step 2: Check for TS exhaustiveness errors**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | grep -E "error TS|Type.*answer"
|
||
```
|
||
|
||
The build will likely show an error in `TreeCanvasNode.tsx` because `NODE_TYPE_CONFIG` only has keys for `decision`, `action`, `solution` — and `config = NODE_TYPE_CONFIG[node.type]` will fail when `node.type === 'answer'`. We fix that in Task 7. For now note the exact error and proceed.
|
||
|
||
**Step 3: Commit the type change**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/types/tree.ts
|
||
git commit -m "feat: add 'answer' to NodeType union for branch placeholder stubs
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: Create the AnswerStubCard component
|
||
|
||
**Files:**
|
||
- Create: `frontend/src/components/tree-editor/AnswerStubCard.tsx`
|
||
|
||
**Step 1: Create the file with the following content**
|
||
|
||
```tsx
|
||
import { useState } from 'react'
|
||
import { HelpCircle, Zap, CheckCircle } from 'lucide-react'
|
||
import { cn } from '@/lib/utils'
|
||
import type { TreeStructure } from '@/types'
|
||
|
||
interface AnswerStubCardProps {
|
||
node: TreeStructure // type === 'answer'
|
||
fromOption?: string
|
||
onSelectType: (nodeId: string, type: 'decision' | 'action' | 'solution') => void
|
||
}
|
||
|
||
export function AnswerStubCard({ node, fromOption, onSelectType }: AnswerStubCardProps) {
|
||
const [picking, setPicking] = useState(false)
|
||
const label = fromOption || node.title || 'Answer'
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
'min-w-[180px] max-w-[280px] rounded-xl border-2 border-dashed border-border bg-card/50',
|
||
'transition-all duration-150',
|
||
!picking && 'cursor-pointer hover:border-primary/40 hover:bg-accent/30'
|
||
)}
|
||
onClick={() => !picking && setPicking(true)}
|
||
>
|
||
{/* Label */}
|
||
<div className="px-3 pt-2.5 pb-1 text-sm font-heading font-medium text-foreground text-center">
|
||
{label}
|
||
</div>
|
||
|
||
{/* Prompt / type picker */}
|
||
{!picking ? (
|
||
<div className="pb-2.5 text-center text-[10px] text-muted-foreground font-label">
|
||
+ Choose Type
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center justify-center gap-1.5 pb-2.5 px-2">
|
||
<button
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onSelectType(node.id, 'decision')
|
||
}}
|
||
className={cn(
|
||
'flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-label',
|
||
'border border-blue-500/30 bg-blue-500/10 text-blue-400 hover:bg-blue-500/20'
|
||
)}
|
||
>
|
||
<HelpCircle className="h-2.5 w-2.5" />
|
||
Decision
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onSelectType(node.id, 'action')
|
||
}}
|
||
className={cn(
|
||
'flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-label',
|
||
'border border-yellow-500/30 bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20'
|
||
)}
|
||
>
|
||
<Zap className="h-2.5 w-2.5" />
|
||
Action
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onSelectType(node.id, 'solution')
|
||
}}
|
||
className={cn(
|
||
'flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-label',
|
||
'border border-green-500/30 bg-green-500/10 text-green-400 hover:bg-green-500/20'
|
||
)}
|
||
>
|
||
<CheckCircle className="h-2.5 w-2.5" />
|
||
Solution
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AnswerStubCard
|
||
```
|
||
|
||
**Step 2: Build to check for TS errors in the new file only**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | grep "AnswerStubCard"
|
||
```
|
||
|
||
Expected: No errors mentioning AnswerStubCard.
|
||
|
||
**Step 3: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/components/tree-editor/AnswerStubCard.tsx
|
||
git commit -m "feat: add AnswerStubCard component for unresolved branch placeholders
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: Update TreeCanvasNode to handle `'answer'` type
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx`
|
||
|
||
The `NODE_TYPE_CONFIG` object (line 47) only has entries for `decision`, `action`, `solution`. When `node.type === 'answer'`, calling `NODE_TYPE_CONFIG[node.type]` will cause a TypeScript error and runtime crash.
|
||
|
||
The fix: guard `config` access so answer nodes get a safe fallback. However, answer nodes should **never** be rendered by `TreeCanvasNode` — `TreeCanvas` will render them as `AnswerStubCard` instead. We still need to fix the TypeScript error.
|
||
|
||
**Step 1: Guard the config lookup**
|
||
|
||
Find around line 135:
|
||
```tsx
|
||
const config = NODE_TYPE_CONFIG[node.type]
|
||
const TypeIcon = config.icon
|
||
```
|
||
|
||
Change to:
|
||
```tsx
|
||
const config = node.type in NODE_TYPE_CONFIG
|
||
? NODE_TYPE_CONFIG[node.type as keyof typeof NODE_TYPE_CONFIG]
|
||
: NODE_TYPE_CONFIG.decision // fallback for 'answer' type (should be rendered by AnswerStubCard instead)
|
||
const TypeIcon = config.icon
|
||
```
|
||
|
||
**Step 2: Build to confirm the TS error from Task 5 is now resolved**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | tail -20
|
||
```
|
||
|
||
Expected: Clean build (zero errors).
|
||
|
||
**Step 3: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/components/tree-editor/TreeCanvasNode.tsx
|
||
git commit -m "fix: guard NODE_TYPE_CONFIG lookup against 'answer' type
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: Redesign NodeFormDecision to use answer labels only (no NodePicker)
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx`
|
||
|
||
This is the biggest change in the plan. We replace the per-option NodePicker with a simple label-only input. The `next_node_id` field on each option is **preserved** in the data model but no longer set via the form — it gets wired up automatically in TreeCanvas when the user saves (Task 9).
|
||
|
||
**Step 1: Remove the NodePicker import**
|
||
|
||
Current line 3:
|
||
```tsx
|
||
import { NodePicker } from './NodePicker'
|
||
```
|
||
|
||
Remove this line entirely.
|
||
|
||
**Step 2: Simplify handleAddOption — set next_node_id to empty string (not required by user)**
|
||
|
||
The current `handleAddOption` (line 30–39) is fine as-is — it creates options with `next_node_id: ''`. Leave it unchanged.
|
||
|
||
**Step 3: Replace the options renderItem to show only the label input**
|
||
|
||
Find the `DynamicArrayField` renderItem (lines 156–209). Replace the entire `renderItem` prop with a simpler version:
|
||
|
||
```tsx
|
||
renderItem={(option, index) => {
|
||
const optionLabelError = validationErrors.find(
|
||
e => e.nodeId === node.id && e.field === `options[${index}].label`
|
||
)
|
||
const letter = indexToLetter(index)
|
||
|
||
return (
|
||
<div className="flex items-center gap-2">
|
||
{/* Letter badge */}
|
||
<span className={cn(
|
||
'flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold',
|
||
isRootNode
|
||
? 'bg-blue-500/20 text-blue-400'
|
||
: 'bg-accent text-muted-foreground'
|
||
)}>
|
||
{letter}
|
||
</span>
|
||
<input
|
||
type="text"
|
||
value={option.label}
|
||
onChange={(e) => handleUpdateOption(index, { label: e.target.value })}
|
||
placeholder={isRootNode
|
||
? `Branch ${letter}: e.g., "Network Issues"...`
|
||
: `Option ${letter} label`}
|
||
className={cn(
|
||
'block flex-1 rounded-md border px-3 py-2 text-sm',
|
||
'bg-background text-foreground placeholder:text-muted-foreground',
|
||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
|
||
optionLabelError ? 'border-red-400' : 'border-border'
|
||
)}
|
||
/>
|
||
{optionLabelError && (
|
||
<p className="mt-1 text-xs text-red-400">{optionLabelError.message}</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}}
|
||
```
|
||
|
||
Note: The surrounding `<div className="rounded-md border border-border bg-accent/50 p-3">` wrapper from the old renderItem should also be removed — the new renderItem renders a flat row.
|
||
|
||
**Step 4: Remove the optionNextError validation lookup** (it's no longer displayed)
|
||
|
||
Find and remove:
|
||
```tsx
|
||
const optionNextError = validationErrors.find(
|
||
e => e.nodeId === node.id && e.field === `options[${index}].next_node_id`
|
||
)
|
||
```
|
||
|
||
**Step 5: Build**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | tail -20
|
||
```
|
||
|
||
Expected: Clean build. If there's an unused import warning for `NodePicker` even after removal, double-check Step 1.
|
||
|
||
**Step 6: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/components/tree-editor/NodeFormDecision.tsx
|
||
git commit -m "feat: redesign NodeFormDecision to use answer label list (no NodePicker)
|
||
|
||
Users now type answer labels only. Stub nodes are created automatically
|
||
by TreeCanvas when the decision node is saved.
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: Wire up answer stub creation and AnswerStubCard rendering in TreeCanvas
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/TreeCanvas.tsx`
|
||
|
||
Two changes: (1) when a decision node is saved, create answer stubs for any option without a `next_node_id`; (2) render `AnswerStubCard` for nodes with `type === 'answer'`.
|
||
|
||
**Step 1: Import AnswerStubCard and add handleSelectAnswerType**
|
||
|
||
At the top of the file, add the import after the existing `TreeCanvasNode` import:
|
||
```tsx
|
||
import { AnswerStubCard } from './AnswerStubCard'
|
||
```
|
||
|
||
**Step 2: Add a `handleSelectAnswerType` callback to the TreeCanvas component**
|
||
|
||
After the `handleDuplicate` callback (around line 278), add:
|
||
|
||
```tsx
|
||
// ── Convert answer stub to a real node type ──
|
||
const handleSelectAnswerType = useCallback(
|
||
(nodeId: string, type: 'decision' | 'action' | 'solution') => {
|
||
updateNode(nodeId, { type })
|
||
setExpandedNodeId(nodeId)
|
||
selectNode(nodeId)
|
||
},
|
||
[updateNode, selectNode]
|
||
)
|
||
```
|
||
|
||
**Step 3: Update handleSave to create answer stubs for unlinked options**
|
||
|
||
Find `handleSave` (around line 202). After the existing `updateNode(nodeId, updates)` call but before the pending link resolution, add answer stub creation logic:
|
||
|
||
The current `handleSave` starts:
|
||
```tsx
|
||
const handleSave = useCallback(
|
||
(nodeId: string, updates: Partial<TreeStructure>) => {
|
||
updateNode(nodeId, updates)
|
||
|
||
// Resolve pending link for new nodes
|
||
const link = pendingLinks.get(nodeId)
|
||
```
|
||
|
||
Change to:
|
||
```tsx
|
||
const handleSave = useCallback(
|
||
(nodeId: string, updates: Partial<TreeStructure>) => {
|
||
updateNode(nodeId, updates)
|
||
|
||
// For decision nodes: create answer stubs for any option without a next_node_id
|
||
if (updates.type === 'decision' || updates.options) {
|
||
const options = updates.options || []
|
||
options.forEach((opt) => {
|
||
if (!opt.next_node_id && opt.label.trim()) {
|
||
// Create a new answer stub node under this decision node
|
||
const stubId = addNode(nodeId, 'answer')
|
||
// Give it the label as its title so AnswerStubCard can display it
|
||
updateNode(stubId, { title: opt.label })
|
||
// Link the option to the stub
|
||
const updatedOptions = options.map((o) =>
|
||
o.id === opt.id ? { ...o, next_node_id: stubId } : o
|
||
)
|
||
updateNode(nodeId, { options: updatedOptions })
|
||
}
|
||
})
|
||
}
|
||
|
||
// Resolve pending link for new nodes
|
||
const link = pendingLinks.get(nodeId)
|
||
```
|
||
|
||
**Step 4: Add `handleSelectAnswerType` to the renderNode dependency array**
|
||
|
||
Find the `useCallback` dependency array at the end of `renderNode` (around line 580). Add `handleSelectAnswerType` to it:
|
||
|
||
```tsx
|
||
[
|
||
expandedNodeId,
|
||
newNodeIds,
|
||
dragOverTarget,
|
||
handleToggleExpand,
|
||
handleSave,
|
||
handleCancelNew,
|
||
handleDelete,
|
||
handleDuplicate,
|
||
handleDragStart,
|
||
handleDragOver,
|
||
handleDrop,
|
||
pendingAddKey,
|
||
handleAddNodeSelect,
|
||
handleSelectAnswerType, // ← add this
|
||
]
|
||
```
|
||
|
||
**Step 5: Render AnswerStubCard for answer-type nodes inside renderNode**
|
||
|
||
Find the section in `renderNode` where `<TreeCanvasNode>` is rendered (around line 468). Add a conditional before it:
|
||
|
||
```tsx
|
||
{/* The node card — answer stubs get their own component */}
|
||
{node.type === 'answer' ? (
|
||
<AnswerStubCard
|
||
node={node}
|
||
fromOption={optionLabel}
|
||
onSelectType={handleSelectAnswerType}
|
||
/>
|
||
) : (
|
||
<TreeCanvasNode
|
||
node={node}
|
||
depth={0}
|
||
fromOption={optionLabel}
|
||
isExpanded={isExpanded}
|
||
isNew={isNew}
|
||
onToggleExpand={() => handleToggleExpand(node.id)}
|
||
onSave={handleSave}
|
||
onCancelNew={handleCancelNew}
|
||
onDelete={handleDelete}
|
||
onDuplicate={handleDuplicate}
|
||
onDragStart={handleDragStart}
|
||
onDragOver={(e) => handleDragOver(e, parentId, index)}
|
||
onDrop={(e) => handleDrop(e, parentId, index)}
|
||
/>
|
||
)}
|
||
```
|
||
|
||
**Step 6: Build**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | tail -30
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 7: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/components/tree-editor/TreeCanvas.tsx
|
||
git commit -m "feat: auto-create answer stubs on decision save, render AnswerStubCard
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 10: Update backend to allow `'answer'` type in drafts and block on publish
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/core/tree_validation.py`
|
||
|
||
**Step 1: Allow `'answer'` type in `_validate_node` without structural validation**
|
||
|
||
Find the `else` branch at the end of `_validate_node` (around line 92–96):
|
||
```python
|
||
else:
|
||
errors.append({
|
||
"field": f"{path}.type",
|
||
"message": f"Unknown node type: {node_type}"
|
||
})
|
||
```
|
||
|
||
Change to:
|
||
```python
|
||
elif node_type == "answer":
|
||
# Answer nodes are draft-only placeholders — no structural validation needed
|
||
pass
|
||
else:
|
||
errors.append({
|
||
"field": f"{path}.type",
|
||
"message": f"Unknown node type: {node_type}"
|
||
})
|
||
```
|
||
|
||
**Step 2: Add publish-time answer node check in `validate_tree_structure`**
|
||
|
||
After the root node is validated and before returning, add a recursive check for answer nodes.
|
||
|
||
Find the end of `validate_tree_structure` (around line 53–56):
|
||
```python
|
||
# Validate all child nodes recursively
|
||
if "children" in tree_structure:
|
||
_validate_children(tree_structure["children"], "root.children", errors)
|
||
|
||
return len(errors) == 0, errors
|
||
```
|
||
|
||
Change to:
|
||
```python
|
||
# Validate all child nodes recursively
|
||
if "children" in tree_structure:
|
||
_validate_children(tree_structure["children"], "root.children", errors)
|
||
|
||
# Block publish if any answer placeholder nodes remain
|
||
if _has_answer_nodes(tree_structure):
|
||
errors.append({
|
||
"field": "tree_structure",
|
||
"message": "Answer placeholders must be resolved to a node type before publishing."
|
||
})
|
||
|
||
return len(errors) == 0, errors
|
||
```
|
||
|
||
**Step 3: Add the `_has_answer_nodes` helper function**
|
||
|
||
Add this function after `_validate_children` (around line 115):
|
||
|
||
```python
|
||
def _has_answer_nodes(node: dict[str, Any]) -> bool:
|
||
"""Recursively check if any node in the tree has type 'answer'."""
|
||
if node.get("type") == "answer":
|
||
return True
|
||
for child in node.get("children", []):
|
||
if _has_answer_nodes(child):
|
||
return True
|
||
return False
|
||
```
|
||
|
||
**Step 4: Verify the backend tests still pass**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/backend
|
||
source venv/bin/activate 2>/dev/null || true
|
||
pytest tests/ -k "tree_valid" --override-ini="addopts=" -q 2>&1 | tail -20
|
||
```
|
||
|
||
If no tests exist specifically for tree_validation, run the full suite:
|
||
```bash
|
||
pytest --override-ini="addopts=" -q 2>&1 | tail -20
|
||
```
|
||
|
||
Expected: All tests pass.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add backend/app/core/tree_validation.py
|
||
git commit -m "feat: allow 'answer' type in tree drafts, block on publish
|
||
|
||
Draft saves succeed with answer placeholder nodes. Publish is blocked
|
||
with a clear message if any answer nodes remain unresolved.
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 11: Add frontend publish guard for answer nodes
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/pages/TreeEditorPage.tsx`
|
||
|
||
**Step 1: Add a `hasAnswerNodes` utility**
|
||
|
||
At the top of `TreeEditorPage.tsx`, after the imports, add a small utility function (before the component function):
|
||
|
||
```typescript
|
||
/** Recursively check if any node in the tree has type 'answer' */
|
||
function hasAnswerNodes(node: TreeStructure): boolean {
|
||
if (node.type === 'answer') return true
|
||
return (node.children || []).some(hasAnswerNodes)
|
||
}
|
||
```
|
||
|
||
You'll need to ensure `TreeStructure` is imported — it should already be imported via `@/types`.
|
||
|
||
**Step 2: Add the guard in `handlePublish`**
|
||
|
||
Find `handlePublish` (around line 269). After the name check (around line 293) and before `validate()`, add:
|
||
|
||
```typescript
|
||
// Block publish if any answer placeholder nodes remain
|
||
const currentStructure = useTreeEditorStore.getState().treeStructure
|
||
if (currentStructure && hasAnswerNodes(currentStructure)) {
|
||
toast.error('Resolve all answer placeholders before publishing. Click each dashed stub card to assign a type.')
|
||
setSaving(false)
|
||
return
|
||
}
|
||
```
|
||
|
||
**Step 3: Build**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | tail -20
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas
|
||
git add frontend/src/pages/TreeEditorPage.tsx
|
||
git commit -m "feat: block publish if unresolved answer stub nodes exist
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Final Verification
|
||
|
||
### Task 12: Full build and manual test checklist
|
||
|
||
**Step 1: Run the full frontend build**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/frontend
|
||
npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: `✓ built in Xs` with zero errors.
|
||
|
||
**Step 2: Run backend tests**
|
||
|
||
```bash
|
||
cd /home/michaelchihlas/dev/patherly/.worktrees/tree-editor-canvas/backend
|
||
pytest --override-ini="addopts=" -q 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: All tests pass.
|
||
|
||
**Step 3: Manual test checklist (confirm with developer)**
|
||
|
||
1. Open a troubleshooting tree in the canvas editor
|
||
2. Click a decision node → card expands
|
||
3. Resize the browser to a short viewport — form should scroll, sticky header (save/cancel) stays visible
|
||
4. Hover over the `i` badge next to field labels — tooltip text appears
|
||
5. Type answer labels in the Options section (e.g. "Server", "Desktop") → click ✓ to save
|
||
6. Two dashed stub cards appear below the decision node labeled "Server" and "Desktop"
|
||
7. Click "Server" stub → three type buttons appear (Decision / Action / Solution)
|
||
8. Click "Decision" → stub converts to a full Decision card in expanded editing mode
|
||
9. Save draft → no backend error (answer nodes allowed in drafts)
|
||
10. Leave an unresolved stub and click Publish → blocked with: "Resolve all answer placeholders before publishing."
|
||
11. `npm run build` passes with no TypeScript errors
|
||
|
||
**Step 4: Complete the development branch**
|
||
|
||
Use `superpowers:finishing-a-development-branch` to present merge/PR options.
|