Move completed plan docs to docs/plans/archive/. Add survey migration 046 and reference HTML/plan files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1328 lines
38 KiB
Markdown
1328 lines
38 KiB
Markdown
# Flow Editor 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 flow editor — unreachable card content, noisy hint text, and forced child-type selection while naming answer options.
|
||
|
||
**Architecture:** Five phases in order: scrollability + fullscreen modal, reusable InfoTip component + tooltip replacements, answer stub type system (frontend types → new component → canvas wiring → NodeList guard), backend draft/publish validation, then markdown serializer and runtime navigation guard. Each phase builds on the previous and must produce a clean `npm run build` before the next begins.
|
||
|
||
**Tech Stack:** React 19, TypeScript, Tailwind CSS, Zustand (`treeEditorStore`), FastAPI (`tree_validation.py`), `frontend/src/lib/treeMarkdownSync.ts`
|
||
|
||
**Working directory:** `/home/michaelchihlas/dev/patherly` (main branch — this plan targets the main codebase, not the worktree, since the canvas code was already merged or will be)
|
||
|
||
> **Note on worktree vs main:** If the `feature/tree-editor-canvas` branch has not yet been merged to main, run all frontend tasks in `.worktrees/tree-editor-canvas/frontend/` and all backend tasks in `.worktrees/tree-editor-canvas/backend/`. If it has been merged, use the repo root. Check with `git branch --show-current` at the start.
|
||
|
||
---
|
||
|
||
## Phase 1: Scrollability + Fullscreen Editor
|
||
|
||
### Task 1.1: Fix canvas inline card scroll (TreeCanvasNode)
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx`
|
||
|
||
**Step 1: Make the card header sticky when expanded**
|
||
|
||
Open the file. Find the card header `<div>` (around line 165) — it's the one with class `flex items-center gap-2 px-3 py-2.5`. It currently has a `cn()` call like this:
|
||
|
||
```tsx
|
||
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'
|
||
)}
|
||
```
|
||
|
||
Add a sticky class when expanded:
|
||
|
||
```tsx
|
||
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'
|
||
)}
|
||
```
|
||
|
||
**Step 2: Make the expanded editing area scrollable**
|
||
|
||
Find the expanded content `<div>` (around line 324) — it's the one that appears under `{isExpanded && (`:
|
||
|
||
```tsx
|
||
<div className="border-t border-border px-3 pb-3 pt-3">
|
||
```
|
||
|
||
Change it to:
|
||
|
||
```tsx
|
||
<div className="border-t border-border px-3 pb-3 pt-3 max-h-[70vh] overflow-y-auto">
|
||
```
|
||
|
||
**Step 3: Build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: `✓ built in Xs` with zero errors.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
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>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1.2: Add fullscreen toggle to Modal component
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/common/Modal.tsx`
|
||
- Modify: `frontend/src/components/tree-editor/NodeEditorModal.tsx`
|
||
|
||
**Step 1: Update Modal.tsx**
|
||
|
||
The current `Modal.tsx` is ~103 lines. The `ModalProps` interface (lines 5–13) and the component signature (line 15) need a new `allowFullScreen` optional prop.
|
||
|
||
Replace the entire `Modal.tsx` content with the following (it's short enough to replace in full to be safe):
|
||
|
||
```tsx
|
||
import { useState, useEffect, useCallback, type ReactNode } from 'react'
|
||
import { X, Maximize2, Minimize2 } from 'lucide-react'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
interface ModalProps {
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
title: string
|
||
children: ReactNode
|
||
/** Optional footer content that stays fixed at bottom (doesn't scroll) */
|
||
footer?: ReactNode
|
||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||
/** If true, a fullscreen toggle button appears in the modal header */
|
||
allowFullScreen?: boolean
|
||
}
|
||
|
||
export function Modal({ isOpen, onClose, title, children, footer, size = 'md', allowFullScreen = false }: ModalProps) {
|
||
const [isFullScreen, setIsFullScreen] = useState(() => {
|
||
if (!allowFullScreen) return false
|
||
try {
|
||
return localStorage.getItem('rf-editor-fullscreen') === 'true'
|
||
} catch {
|
||
return false
|
||
}
|
||
})
|
||
|
||
const toggleFullScreen = () => {
|
||
const next = !isFullScreen
|
||
setIsFullScreen(next)
|
||
try {
|
||
localStorage.setItem('rf-editor-fullscreen', String(next))
|
||
} catch {}
|
||
}
|
||
|
||
// Close on Escape key
|
||
const handleKeyDown = useCallback(
|
||
(e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') {
|
||
onClose()
|
||
}
|
||
},
|
||
[onClose]
|
||
)
|
||
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
document.addEventListener('keydown', handleKeyDown)
|
||
document.body.style.overflow = 'hidden'
|
||
}
|
||
return () => {
|
||
document.removeEventListener('keydown', handleKeyDown)
|
||
document.body.style.overflow = ''
|
||
}
|
||
}, [isOpen, handleKeyDown])
|
||
|
||
if (!isOpen) return null
|
||
|
||
const sizeClasses = {
|
||
sm: 'max-w-sm',
|
||
md: 'max-w-md',
|
||
lg: 'max-w-full sm:max-w-lg',
|
||
xl: 'max-w-full sm:max-w-4xl',
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-4"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="modal-title"
|
||
>
|
||
{/* Backdrop */}
|
||
<div
|
||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||
onClick={onClose}
|
||
aria-hidden="true"
|
||
/>
|
||
|
||
{/* Modal Content */}
|
||
<div
|
||
className={cn(
|
||
'relative flex w-full flex-col border border-border bg-card shadow-lg',
|
||
'animate-scale-in transition-all duration-200',
|
||
isFullScreen
|
||
? 'fixed inset-4 max-w-none w-auto h-auto rounded-2xl'
|
||
: cn(
|
||
'max-h-[100vh] rounded-t-2xl sm:max-h-[85vh] sm:rounded-2xl',
|
||
sizeClasses[size]
|
||
)
|
||
)}
|
||
>
|
||
{/* Header - Fixed at top */}
|
||
<div className="flex flex-shrink-0 items-center justify-between border-b border-border px-4 py-3 sm:px-6 sm:py-4">
|
||
<h2 id="modal-title" className="text-lg font-semibold text-foreground">
|
||
{title}
|
||
</h2>
|
||
<div className="flex items-center gap-1">
|
||
{allowFullScreen && (
|
||
<button
|
||
type="button"
|
||
onClick={toggleFullScreen}
|
||
className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||
title={isFullScreen ? 'Exit full screen' : 'Full screen'}
|
||
>
|
||
{isFullScreen
|
||
? <Minimize2 className="h-4 w-4" />
|
||
: <Maximize2 className="h-4 w-4" />
|
||
}
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={onClose}
|
||
className={cn(
|
||
'rounded-md p-1.5 text-muted-foreground transition-colors sm:p-1',
|
||
'hover:bg-accent hover:text-foreground',
|
||
'focus:outline-none focus:ring-2 focus:ring-primary/20'
|
||
)}
|
||
aria-label="Close modal"
|
||
>
|
||
<X className="h-5 w-5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Body - Scrollable */}
|
||
<div className="flex-1 overflow-y-auto px-4 py-4 sm:px-6">
|
||
{children}
|
||
</div>
|
||
|
||
{/* Footer - Fixed at bottom */}
|
||
{footer && (
|
||
<div className="flex-shrink-0 border-t border-border px-4 py-3 sm:px-6 sm:py-4">
|
||
{footer}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default Modal
|
||
```
|
||
|
||
**Step 2: Pass `allowFullScreen` to NodeEditorModal**
|
||
|
||
In `frontend/src/components/tree-editor/NodeEditorModal.tsx`, find line 86:
|
||
|
||
```tsx
|
||
<Modal isOpen={true} onClose={onClose} title={getTitle()} size="lg" footer={footerContent}>
|
||
```
|
||
|
||
Change to:
|
||
|
||
```tsx
|
||
<Modal isOpen={true} onClose={onClose} title={getTitle()} size="lg" footer={footerContent} allowFullScreen={true}>
|
||
```
|
||
|
||
**Step 3: Build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: Clean build, zero errors.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/components/common/Modal.tsx frontend/src/components/tree-editor/NodeEditorModal.tsx
|
||
git commit -m "feat: add fullscreen toggle to Modal, enable in NodeEditorModal
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 2: Info-On-Demand Tooltips
|
||
|
||
### Task 2.1: Create the InfoTip component
|
||
|
||
**Files:**
|
||
- Create: `frontend/src/components/common/InfoTip.tsx`
|
||
|
||
**Step 1: Create the file**
|
||
|
||
```tsx
|
||
interface InfoTipProps {
|
||
text: string
|
||
}
|
||
|
||
export function InfoTip({ text }: InfoTipProps) {
|
||
return (
|
||
<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={text}
|
||
>
|
||
i
|
||
</span>
|
||
)
|
||
}
|
||
```
|
||
|
||
**Step 2: Build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 3: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/components/common/InfoTip.tsx
|
||
git commit -m "feat: add reusable InfoTip component for field-level help
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2.2: Replace hint text in NodeFormDecision
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx`
|
||
|
||
**Step 1: Add the InfoTip import**
|
||
|
||
After the existing imports at the top of the file, add:
|
||
|
||
```tsx
|
||
import { InfoTip } from '@/components/common/InfoTip'
|
||
```
|
||
|
||
**Step 2: Remove the root node question hint paragraph**
|
||
|
||
Around line 89–93 there is:
|
||
|
||
```tsx
|
||
{isRootNode && (
|
||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||
What's the main question to diagnose the issue?
|
||
</p>
|
||
)}
|
||
```
|
||
|
||
Delete this entire block. The input placeholder `"e.g., What type of issue are you experiencing?"` already conveys the intent.
|
||
|
||
**Step 3: Replace the options hint paragraphs with an InfoTip on the label**
|
||
|
||
Around lines 133–144, the Options section label and hints look like:
|
||
|
||
```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>
|
||
)}
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```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>
|
||
<InfoTip text={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."} />
|
||
</label>
|
||
```
|
||
|
||
**Step 4: Build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/components/tree-editor/NodeFormDecision.tsx
|
||
git commit -m "fix: replace hint paragraphs with InfoTip tooltips in NodeFormDecision
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2.3: Replace hint text in NodeFormAction
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/NodeFormAction.tsx`
|
||
|
||
**Step 1: Add the InfoTip import**
|
||
|
||
Add at the top after existing imports:
|
||
|
||
```tsx
|
||
import { InfoTip } from '@/components/common/InfoTip'
|
||
```
|
||
|
||
**Step 2: Replace 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>
|
||
```
|
||
|
||
And the Description label above it (around line 77–79):
|
||
|
||
```tsx
|
||
<label className="block text-sm font-medium text-foreground">
|
||
Description
|
||
</label>
|
||
```
|
||
|
||
Replace both with (combine label + infotip, remove paragraph):
|
||
|
||
```tsx
|
||
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||
Description
|
||
<InfoTip text="Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`" />
|
||
</label>
|
||
```
|
||
|
||
**Step 3: Replace 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>
|
||
```
|
||
|
||
And the Commands label above it:
|
||
|
||
```tsx
|
||
<label className="block text-sm font-medium text-foreground">
|
||
Commands
|
||
</label>
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```tsx
|
||
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||
Commands
|
||
<InfoTip text="PowerShell or CLI commands to execute" />
|
||
</label>
|
||
```
|
||
|
||
**Step 4: Build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/components/tree-editor/NodeFormAction.tsx
|
||
git commit -m "fix: replace hint paragraphs with InfoTip tooltips in NodeFormAction
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2.4: Replace hint text in NodeFormResolution
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/NodeFormResolution.tsx`
|
||
|
||
**Step 1: Add the InfoTip import**
|
||
|
||
```tsx
|
||
import { InfoTip } from '@/components/common/InfoTip'
|
||
```
|
||
|
||
**Step 2: Replace the Description hint paragraph**
|
||
|
||
Around lines 86–88 (same markdown hint as NodeFormAction). Replace:
|
||
|
||
```tsx
|
||
<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>
|
||
```
|
||
|
||
With:
|
||
|
||
```tsx
|
||
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||
Description
|
||
<InfoTip text="Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code`" />
|
||
</label>
|
||
```
|
||
|
||
**Step 3: Replace the Resolution Steps hint paragraph**
|
||
|
||
Around lines 118–120:
|
||
|
||
```tsx
|
||
<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>
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```tsx
|
||
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||
Resolution Steps
|
||
<InfoTip text="Step-by-step instructions for resolving the issue" />
|
||
</label>
|
||
```
|
||
|
||
**Step 4: Build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/components/tree-editor/NodeFormResolution.tsx
|
||
git commit -m "fix: replace hint paragraphs with InfoTip tooltips in NodeFormResolution
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 3: Answer Stub Placeholder System
|
||
|
||
### Task 3.1: Add `'answer'` to the NodeType union
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/types/tree.ts:4`
|
||
|
||
**Step 1: Edit the NodeType line**
|
||
|
||
Find line 4:
|
||
|
||
```typescript
|
||
export type NodeType = 'decision' | 'action' | 'solution'
|
||
```
|
||
|
||
Change to:
|
||
|
||
```typescript
|
||
export type NodeType = 'decision' | 'action' | 'solution' | 'answer'
|
||
```
|
||
|
||
**Step 2: Run build — note the expected error**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | grep "error TS" | head -10
|
||
```
|
||
|
||
Expected: You will see TypeScript errors in `TreeCanvasNode.tsx` (and possibly `NodeList.tsx`) because their `Record<NodeType, ...>` maps don't include `'answer'`. This is expected and will be fixed in Tasks 3.3 and 3.6.
|
||
|
||
**Step 3: Commit the type change now** (before fixing downstream errors)
|
||
|
||
```bash
|
||
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 3.2: Create the AnswerStubCard component
|
||
|
||
**Files:**
|
||
- Create: `frontend/src/components/tree-editor/AnswerStubCard.tsx`
|
||
|
||
**Step 1: Create the file**
|
||
|
||
```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 confirm no errors in this new file**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | grep "AnswerStubCard"
|
||
```
|
||
|
||
Expected: No errors mentioning AnswerStubCard (the earlier `TreeCanvasNode.tsx` errors from Task 3.1 are still present, that's fine).
|
||
|
||
**Step 3: Commit**
|
||
|
||
```bash
|
||
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 3.3: Guard TreeCanvasNode against `'answer'` type
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/TreeCanvasNode.tsx`
|
||
|
||
**Step 1: Fix the NODE_TYPE_CONFIG lookup**
|
||
|
||
Find (around line 135):
|
||
|
||
```tsx
|
||
const config = NODE_TYPE_CONFIG[node.type]
|
||
const TypeIcon = config.icon
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```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' (rendered by AnswerStubCard)
|
||
const TypeIcon = config.icon
|
||
```
|
||
|
||
**Step 2: Build — confirm the TS error from Task 3.1 is now gone**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | grep "TreeCanvasNode"
|
||
```
|
||
|
||
Expected: No errors mentioning TreeCanvasNode.
|
||
|
||
**Step 3: Confirm full clean build (NodeList errors may still exist)**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | grep "error TS" | head -5
|
||
```
|
||
|
||
Note: NodeList errors will be fixed in Task 3.6. Only TreeCanvasNode should be clean now.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
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 3.4: Redesign NodeFormDecision — label-only options (remove NodePicker)
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/NodeFormDecision.tsx`
|
||
|
||
The old form had a NodePicker per option that forced users to pick a child node type during the same editing session as writing the question. The new form is label-only — stubs are created automatically on save.
|
||
|
||
**Step 1: Remove the NodePicker import**
|
||
|
||
Find and delete:
|
||
|
||
```tsx
|
||
import { NodePicker } from './NodePicker'
|
||
```
|
||
|
||
**Step 2: Replace the DynamicArrayField renderItem**
|
||
|
||
Find the existing `renderItem` prop inside `<DynamicArrayField>`. The current version renders a box with a letter badge + label input + NodePicker. Replace the entire `renderItem` callback with:
|
||
|
||
```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">
|
||
<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>
|
||
<div className="flex-1">
|
||
<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 w-full 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>
|
||
</div>
|
||
)
|
||
}}
|
||
```
|
||
|
||
**Step 3: Remove the optionNextError validation lookup** (it referenced `options[N].next_node_id`, no longer needed since there's no NodePicker)
|
||
|
||
Inside the old renderItem, there was:
|
||
|
||
```tsx
|
||
const optionNextError = validationErrors.find(
|
||
e => e.nodeId === node.id && e.field === `options[${index}].next_node_id`
|
||
)
|
||
```
|
||
|
||
This is now gone (it was inside the old renderItem you just replaced). Verify there are no remaining references.
|
||
|
||
**Step 4: Build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | grep -E "NodeFormDecision|NodePicker" | head -10
|
||
```
|
||
|
||
Expected: No errors. If you see "Cannot find module './NodePicker'" that means the import wasn't fully removed — double-check Step 1.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/components/tree-editor/NodeFormDecision.tsx
|
||
git commit -m "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>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3.5: Wire up auto-creation and AnswerStubCard rendering in TreeCanvas
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/TreeCanvas.tsx`
|
||
|
||
**Step 1: Add the AnswerStubCard import**
|
||
|
||
After the existing `TreeCanvasNode` import, add:
|
||
|
||
```tsx
|
||
import { AnswerStubCard } from './AnswerStubCard'
|
||
```
|
||
|
||
**Step 2: Add `handleSelectAnswerType` callback**
|
||
|
||
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 auto-create stubs for unlinked options**
|
||
|
||
Find `handleSave` (around line 202). It currently starts:
|
||
|
||
```tsx
|
||
const handleSave = useCallback(
|
||
(nodeId: string, updates: Partial<TreeStructure>) => {
|
||
updateNode(nodeId, updates)
|
||
|
||
// Resolve pending link for new nodes
|
||
const link = pendingLinks.get(nodeId)
|
||
```
|
||
|
||
After `updateNode(nodeId, updates)` and before the pending link resolution, insert:
|
||
|
||
```tsx
|
||
// For decision nodes: create answer stubs for any option without a next_node_id
|
||
if (updates.options) {
|
||
const options = updates.options
|
||
const stubsToCreate: Array<{ opt: typeof options[number]; stubId: string }> = []
|
||
|
||
options.forEach((opt) => {
|
||
if (!opt.next_node_id && opt.label.trim()) {
|
||
const stubId = addNode(nodeId, 'answer')
|
||
updateNode(stubId, { title: opt.label })
|
||
stubsToCreate.push({ opt, stubId })
|
||
}
|
||
})
|
||
|
||
if (stubsToCreate.length > 0) {
|
||
const updatedOptions = options.map((o) => {
|
||
const stub = stubsToCreate.find((s) => s.opt.id === o.id)
|
||
return stub ? { ...o, next_node_id: stub.stubId } : o
|
||
})
|
||
updateNode(nodeId, { options: updatedOptions })
|
||
}
|
||
}
|
||
```
|
||
|
||
> **Why this shape:** We build the list of stubs first, then do a single `updateNode` with the fully updated options array, to avoid multiple sequential calls stomping on each other.
|
||
|
||
**Step 4: Add `handleSelectAnswerType` to the `renderNode` dependency array**
|
||
|
||
Find the `useCallback` dependency array at the end of `renderNode` (around line 580–594). Add `handleSelectAnswerType` to the array.
|
||
|
||
**Step 5: Render AnswerStubCard for answer-type nodes in `renderNode`**
|
||
|
||
In `renderNode`, find where `<TreeCanvasNode>` is rendered (it's the card component, around line 468). Wrap it with a conditional:
|
||
|
||
Replace:
|
||
|
||
```tsx
|
||
{/* The node card itself */}
|
||
<TreeCanvasNode
|
||
node={node}
|
||
...
|
||
/>
|
||
```
|
||
|
||
With:
|
||
|
||
```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 frontend && npm run build 2>&1 | tail -15
|
||
```
|
||
|
||
Expected: Clean build, zero errors (NodeList may still have errors — check next task).
|
||
|
||
**Step 7: Commit**
|
||
|
||
```bash
|
||
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 3.6: Guard NodeList against `'answer'` type
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/components/tree-editor/NodeList.tsx`
|
||
|
||
The `nodeTypeIcons` and `nodeTypeColors` objects (lines 91–101) use `Record<NodeType, ...>` which now requires an `'answer'` entry.
|
||
|
||
**Step 1: Add `'answer'` entries to both records**
|
||
|
||
Find:
|
||
|
||
```tsx
|
||
const nodeTypeIcons: Record<NodeType, React.ReactNode> = {
|
||
decision: <HelpCircle className="h-4 w-4" />,
|
||
action: <Zap className="h-4 w-4" />,
|
||
solution: <CheckCircle className="h-4 w-4" />
|
||
}
|
||
|
||
const nodeTypeColors: Record<NodeType, string> = {
|
||
decision: 'bg-blue-500/20 text-blue-400',
|
||
action: 'bg-yellow-500/20 text-yellow-400',
|
||
solution: 'bg-green-500/20 text-green-400'
|
||
}
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```tsx
|
||
const nodeTypeIcons: Record<NodeType, React.ReactNode> = {
|
||
decision: <HelpCircle className="h-4 w-4" />,
|
||
action: <Zap className="h-4 w-4" />,
|
||
solution: <CheckCircle className="h-4 w-4" />,
|
||
answer: <HelpCircle className="h-4 w-4 opacity-50" />
|
||
}
|
||
|
||
const nodeTypeColors: Record<NodeType, string> = {
|
||
decision: 'bg-blue-500/20 text-blue-400',
|
||
action: 'bg-yellow-500/20 text-yellow-400',
|
||
solution: 'bg-green-500/20 text-green-400',
|
||
answer: 'bg-muted text-muted-foreground border border-dashed border-border'
|
||
}
|
||
```
|
||
|
||
**Step 2: Build — confirm full clean build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: `✓ built in Xs` — zero TypeScript errors.
|
||
|
||
**Step 3: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/components/tree-editor/NodeList.tsx
|
||
git commit -m "fix: add answer type to NodeList icon and color maps
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 4: Backend + Frontend Validation
|
||
|
||
### Task 4.1: Backend — allow `'answer'` in drafts, block on publish
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/core/tree_validation.py`
|
||
|
||
**Step 1: Add the `'answer'` elif in `_validate_node`**
|
||
|
||
Find the `_validate_node` function. Inside it, find the `else` branch at the end (around lines 92–96):
|
||
|
||
```python
|
||
else:
|
||
errors.append({
|
||
"field": f"{path}.type",
|
||
"message": f"Unknown node type: {node_type}"
|
||
})
|
||
```
|
||
|
||
Insert a new `elif` before the `else`:
|
||
|
||
```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 the `_has_answer_nodes` helper**
|
||
|
||
After the `_validate_children` function (ends around line 115), add a new function:
|
||
|
||
```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 3: Add publish-time check in `validate_tree_structure`**
|
||
|
||
Find `validate_tree_structure`. After the `_validate_children` call and before `return len(errors) == 0, errors` (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 4: Run backend tests**
|
||
|
||
```bash
|
||
cd backend
|
||
pytest --override-ini="addopts=" -q 2>&1 | tail -15
|
||
```
|
||
|
||
Expected: All existing tests pass. No new failures.
|
||
|
||
**Step 5: Commit**
|
||
|
||
```bash
|
||
git add backend/app/core/tree_validation.py
|
||
git commit -m "feat: allow 'answer' type in tree drafts, block on publish
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4.2: Frontend publish guard
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/pages/TreeEditorPage.tsx`
|
||
|
||
**Step 1: Add utility function before the component**
|
||
|
||
Find the component declaration (`export function TreeEditorPage` or similar). Immediately before it, add:
|
||
|
||
```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)
|
||
}
|
||
```
|
||
|
||
Ensure `TreeStructure` is imported from `@/types` — check the existing imports at the top of the file (it should already be there).
|
||
|
||
**Step 2: Add the guard in `handlePublish`**
|
||
|
||
Find `handlePublish` (around line 269). It starts with a code-mode markdown validation check. After the tree name check (around line 293, after `if (!currentState.name.trim()) {...}`) and before `const errors = validate()`, insert:
|
||
|
||
```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 frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 4: Commit**
|
||
|
||
```bash
|
||
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>"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 5: Markdown Serializer + Runtime Guard
|
||
|
||
### Task 5.1: Handle `'answer'` in the markdown serializer
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/lib/treeMarkdownSync.ts`
|
||
|
||
**Step 1: Locate the `serializeNode` function**
|
||
|
||
In `treeMarkdownSync.ts`, find `serializeNode`. It has a chain of `if (node.type === 'decision') ... else if (node.type === 'action') ... else if (node.type === 'solution')`. After the final `else if` (around line 75–81), add an `else if` for `'answer'`:
|
||
|
||
```typescript
|
||
} else if (node.type === 'answer') {
|
||
// Answer placeholder — render as a clearly marked stub
|
||
body.push(`## [ANSWER PLACEHOLDER] ${node.title || 'Untitled'}`, '')
|
||
body.push('> This is an unresolved answer stub. Convert it to a Decision, Action, or Solution before publishing.')
|
||
}
|
||
```
|
||
|
||
**Step 2: Build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 3: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/lib/treeMarkdownSync.ts
|
||
git commit -m "feat: serialize 'answer' stub nodes in markdown output
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5.2: Add runtime defensive guard in TreeNavigationPage
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/pages/TreeNavigationPage.tsx`
|
||
|
||
**Step 1: Find the "Current Node" rendering block**
|
||
|
||
Around line 758–760 there is a comment `{/* Current Node */}` followed by a `<div>`. Inside this `<div>`, node type is dispatched via conditionals:
|
||
|
||
```tsx
|
||
{currentNode && currentNode.type === 'decision' && (
|
||
...
|
||
)}
|
||
```
|
||
|
||
Before any of these existing conditionals (before the `decision` block), add a guard for `'answer'` nodes:
|
||
|
||
```tsx
|
||
{currentNode && currentNode.type === 'answer' && (
|
||
<div className="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-6 text-center">
|
||
<p className="text-sm font-medium text-yellow-400">
|
||
This tree contains an unresolved placeholder node. Please contact the tree author to complete it before use.
|
||
</p>
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
**Step 2: Build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: Clean build.
|
||
|
||
**Step 3: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/pages/TreeNavigationPage.tsx
|
||
git commit -m "fix: add defensive guard for answer nodes in session navigation
|
||
|
||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 6: Final Verification
|
||
|
||
### Task 6.1: Full build and backend test suite
|
||
|
||
**Step 1: Frontend build**
|
||
|
||
```bash
|
||
cd frontend && npm run build 2>&1 | tail -5
|
||
```
|
||
|
||
Expected: `✓ built in Xs` — zero errors.
|
||
|
||
**Step 2: Backend tests**
|
||
|
||
```bash
|
||
cd backend && pytest --override-ini="addopts=" -q 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: All existing tests pass.
|
||
|
||
---
|
||
|
||
### Task 6.2: Manual test checklist
|
||
|
||
Confirm all of the following in the browser:
|
||
|
||
1. **Canvas scroll** — Open a decision node in the canvas editor → resize browser to a short viewport → form content scrolls → sticky header (save/cancel) stays visible at top
|
||
2. **Modal scroll** — Open a node via the modal editor (`NodeEditorModal`) → content scrolls, header and footer are fixed
|
||
3. **Fullscreen toggle** — Click the expand icon in the modal header → modal fills viewport with margin → click again → returns to normal size smoothly → refresh → preference is remembered
|
||
4. **Other modals unaffected** — Open any other modal (step library, share session, etc.) → no fullscreen button appears
|
||
5. **InfoTip tooltips** — Hover over `ⓘ` badges on NodeFormDecision / NodeFormAction / NodeFormResolution labels → tooltip text appears → no always-visible hint paragraphs remain
|
||
6. **Answer stubs — creation** — Create or edit a decision node → type a question → type answer labels ("Server", "Desktop") → save → two dashed stub cards appear below the decision
|
||
7. **Answer stubs — conversion** — Click a dashed stub → three type buttons appear (Decision / Action / Solution) → click one → stub converts to a real node card in expanded editing mode
|
||
8. **Draft save with stubs** — Save draft with unresolved stubs → no backend error
|
||
9. **Publish blocked** — Leave an unresolved stub → click Publish → toast: "Resolve all answer placeholders before publishing."
|
||
10. **Publish succeeds after resolution** — Convert all stubs → Publish → succeeds
|
||
|
||
---
|
||
|
||
## Summary of All Files Changed
|
||
|
||
### New Files
|
||
| File | Description |
|
||
|------|-------------|
|
||
| `frontend/src/components/common/InfoTip.tsx` | Reusable info tooltip badge |
|
||
| `frontend/src/components/tree-editor/AnswerStubCard.tsx` | Dashed stub card with inline type picker |
|
||
|
||
### Modified Files
|
||
| File | Changes |
|
||
|------|---------|
|
||
| `frontend/src/components/tree-editor/TreeCanvasNode.tsx` | Sticky header + scrollable area + answer type guard |
|
||
| `frontend/src/components/common/Modal.tsx` | `allowFullScreen` prop + toggle button + localStorage |
|
||
| `frontend/src/components/tree-editor/NodeEditorModal.tsx` | Pass `allowFullScreen={true}` |
|
||
| `frontend/src/components/common/InfoTip.tsx` | (new) |
|
||
| `frontend/src/components/tree-editor/NodeFormDecision.tsx` | InfoTip tooltips + label-only options |
|
||
| `frontend/src/components/tree-editor/NodeFormAction.tsx` | InfoTip tooltips |
|
||
| `frontend/src/components/tree-editor/NodeFormResolution.tsx` | InfoTip tooltips |
|
||
| `frontend/src/types/tree.ts` | Add `'answer'` to NodeType union |
|
||
| `frontend/src/components/tree-editor/TreeCanvas.tsx` | Auto-create stubs + AnswerStubCard rendering |
|
||
| `frontend/src/components/tree-editor/NodeList.tsx` | Add answer type to icon/color maps |
|
||
| `frontend/src/pages/TreeEditorPage.tsx` | Publish guard |
|
||
| `frontend/src/pages/TreeNavigationPage.tsx` | Runtime guard for answer nodes |
|
||
| `frontend/src/lib/treeMarkdownSync.ts` | Serialize answer nodes |
|
||
| `backend/app/core/tree_validation.py` | Allow answer in drafts, block on publish |
|