diff --git a/docs/plans/2026-02-18-canvas-ux-fixes-design.md b/docs/plans/2026-02-18-canvas-ux-fixes-design.md new file mode 100644 index 00000000..faa93698 --- /dev/null +++ b/docs/plans/2026-02-18-canvas-ux-fixes-design.md @@ -0,0 +1,258 @@ +# Canvas UX Fixes — Design Document + +**Date:** 2026-02-18 +**Branch:** `feature/tree-editor-canvas` +**Status:** Approved, pending implementation + +--- + +## Context + +The new TreeCanvas editor (Phase 1–4) was tested and three UX problems were identified: + +1. **Scroll**: Expanded card forms have no height limit — long forms are cut off and unreachable +2. **Busy forms**: Inline hint text (`

`) inside NodeForm components creates visual clutter +3. **Answer stubs**: When building a decision node, users must immediately pick a child node type — there's no way to sketch out answer options first and decide types later + +All three fixes apply exclusively to the canvas editor. No session, navigation, backend session-saving, or procedural flow code is affected. + +--- + +## Fix 1: Card Scroll + +### Problem + +`TreeCanvasNode.tsx` renders the expanded editing area as an unbounded `

`. On decision nodes with many options, or on any node when the browser viewport is short, the card overflows off-screen. There is no scrollbar — content is unreachable. Tab cycling doesn't scroll the canvas to bring hidden fields into view. + +### Design + +Apply `max-h-[70vh] overflow-y-auto` to the expanded editing `
` inside `TreeCanvasNode.tsx`. + +Make the save/cancel header row sticky (`sticky top-0 z-10 bg-card`) so the action buttons are always visible when the user scrolls the form content. + +**Files changed:** +- `frontend/src/components/tree-editor/TreeCanvasNode.tsx` + - Add `max-h-[70vh] overflow-y-auto` to the expanded area `
` (currently `border-t border-border px-3 pb-3 pt-3`) + - Add `sticky top-0 z-10 bg-card` to the card header `
` containing the save/cancel row when in expanded state + +**No other files affected.** + +--- + +## Fix 2: Info Tooltips + +### Problem + +`NodeFormDecision.tsx`, `NodeFormAction.tsx`, and `NodeFormResolution.tsx` each contain `

` hint paragraphs below field labels. These add vertical height and visual noise inside a card that's already compact. + +Examples of the current hint text: +- "Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, \`code\`" +- "PowerShell or CLI commands to execute" +- "Step-by-step instructions for resolving the issue" + +### Design + +Replace each hint `

` with a small `ⓘ` icon placed inline next to the field label. The icon shows a tooltip on hover containing the same text. + +**Tooltip implementation:** + +Use `title=""` on the icon element for a native browser tooltip. No third-party tooltip library needed — keeps the implementation minimal and consistent with the existing codebase pattern (the validation badge already uses `title={nodeErrors.map(...).join('\n')}`). + +```tsx +// Before + +

+ Supports markdown: **bold**, *italic*, - lists, 1. numbered lists, `code` +

+ +// After + +``` + +**Files changed:** +- `frontend/src/components/tree-editor/NodeFormDecision.tsx` — remove help_text hint `

`, replace with `ⓘ` on `Help Text` label +- `frontend/src/components/tree-editor/NodeFormAction.tsx` — remove description markdown hint `

` and commands hint `

`, add `ⓘ` on those labels +- `frontend/src/components/tree-editor/NodeFormResolution.tsx` — remove description markdown hint `

` and steps hint `

`, add `ⓘ` on those labels + +--- + +## Fix 3: Answer Stubs (New `answer` Node Type) + +### Problem + +Decision nodes require the user to pick child node types at the same time they're creating the decision. This is backwards — you naturally know the answer options before you know what each one should do. The NodePicker in NodeFormDecision forces a concrete type selection (decision / action / solution) or leaves the option disconnected (`next_node_id: null`). + +Users want to type answer labels first, see those answers appear as placeholder cards in the canvas, and then click each placeholder to assign a type and fill in details. + +### Design + +Introduce `'answer'` as a new internal NodeType that represents a typed-but-unresolved branch placeholder. Answer nodes are: +- Created when a user types an answer label in the decision node form +- Shown in the canvas as a dashed-border stub card with the answer label +- Clickable to open a type picker (decision / action / solution) and convert to a real node +- **Not publishable** — blocked by backend and frontend validation on publish + +Answer nodes persist to draft saves so users don't lose their sketch when they navigate away. + +### Data Model + +**`frontend/src/types/tree.ts`** + +```typescript +// Before +export type NodeType = 'decision' | 'action' | 'solution' + +// After +export type NodeType = 'decision' | 'action' | 'solution' | 'answer' +``` + +`TreeStructure` interface: no new fields needed. Answer nodes use: +- `id`: auto-generated UUID (same as other nodes) +- `type`: `'answer'` +- `title`: the answer label text (e.g. "Server", "Desktop") +- No other fields required + +### NodeFormDecision Redesign + +Replace the current options UI (NodePicker per option → picks existing or creates new) with a two-zone layout: + +**Zone 1 — Answer Labels** +A simple list of text inputs, one per answer option. Each input edits `options[i].label`. Add/remove/reorder via `DynamicArrayField` (already available). + +No `next_node_id` selection here. When the user saves, for each option that has a label but no `next_node_id`, a new answer-type stub node is created and linked automatically. + +``` +Options (answer labels): +[ Server ] [×] +[ Desktop ] [×] +[ + Add Answer ] +``` + +**Zone 2 — (removed) NodePicker per option** +The per-option NodePicker is removed entirely. The canvas becomes the way to traverse to a child and set its type. + +### TreeCanvas Changes + +**Rendering answer nodes:** + +When a node has `type === 'answer'`, render an `AnswerStubCard` instead of a full `TreeCanvasNode`. The stub card: +- Dashed border: `border-2 border-dashed border-border` +- Colored left accent: none (neutral/muted) +- Shows the answer label (`node.title`) centered +- Shows a "+ Choose Type" label below the title +- On click: opens an inline type picker (3 buttons: Decision / Action / Solution) +- On type selection: calls `updateNode(node.id, { type: selectedType })` and immediately expands the node for editing + +**New component:** `frontend/src/components/tree-editor/AnswerStubCard.tsx` + +Props: +```typescript +interface AnswerStubCardProps { + node: TreeStructure // type === 'answer' + fromOption?: string // the answer label (same as node.title) + onSelectType: (nodeId: string, type: 'decision' | 'action' | 'solution') => void +} +``` + +### Stub Creation Logic (TreeCanvas) + +When a decision node is saved (`onSave`), the canvas compares options before/after: + +For each option in the saved node: +- If `option.next_node_id` is null/undefined → create a new answer stub node with `title = option.label` and link `option.next_node_id` to its ID. +- If `option.next_node_id` already points to a node → leave it. + +This creation logic lives in `TreeCanvas.tsx`'s `handleNodeSave()` function, which already handles pending link resolution. + +### Backend Validation + +**`backend/app/core/tree_validation.py`** + +`_validate_node()` currently rejects unknown node types: +```python +if node_type not in ('decision', 'action', 'solution'): + errors.append(...) +``` + +Changes: +1. Allow `'answer'` type through without structural validation (answer nodes have no required fields beyond `id` and `type`). +2. Add a publish-time check in `can_publish_tree()` (or in `validate_tree_structure()` before publish): if any node has `type == 'answer'`, reject with a clear message: `"Answer placeholders must be resolved to a node type before publishing."` +3. The draft save endpoint (`PUT /trees/:id`) does not call `can_publish_tree()`, so draft saves continue to work with answer nodes present. + +### Frontend Publish Guard + +In `TreeEditorPage.tsx`, before calling the publish API, add a check: +```typescript +const hasAnswerNodes = findAllAnswerNodes(tree.tree_structure).length > 0 +if (hasAnswerNodes) { + // Show toast or inline error: "Resolve all answer placeholders before publishing." + return +} +``` + +`findAllAnswerNodes` is a simple recursive traversal (can be a small utility function in `TreeCanvas.tsx` or a new file `lib/treeUtils.ts`). + +### Visual Design (AnswerStubCard) + +``` +┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ + Server + [ ? Decision ] [ ⚡ Action ] [ ✓ Solution ] +└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +``` + +- Card: `min-w-[180px] max-w-[280px] rounded-xl border-2 border-dashed border-border bg-card/50` +- Title: `text-sm font-heading font-medium text-foreground text-center py-2 px-3` +- Type picker row (default state — not yet clicked): `text-xs text-muted-foreground text-center pb-2 cursor-pointer hover:text-foreground` + - Clicking the card reveals three compact buttons for type selection +- Type picker (expanded): three small buttons side-by-side in the card footer + +--- + +## Files Changed Summary + +| File | Change | +|------|--------| +| `frontend/src/components/tree-editor/TreeCanvasNode.tsx` | Fix 1: max-h + overflow-y + sticky header | +| `frontend/src/components/tree-editor/NodeFormDecision.tsx` | Fix 2: ⓘ tooltip on help_text; Fix 3: replace NodePicker with answer label list | +| `frontend/src/components/tree-editor/NodeFormAction.tsx` | Fix 2: ⓘ tooltips on description + commands fields | +| `frontend/src/components/tree-editor/NodeFormResolution.tsx` | Fix 2: ⓘ tooltips on description + steps fields | +| `frontend/src/components/tree-editor/TreeCanvas.tsx` | Fix 3: stub creation in handleNodeSave; AnswerStubCard rendering | +| `frontend/src/components/tree-editor/AnswerStubCard.tsx` | Fix 3: NEW — dashed stub card with inline type picker | +| `frontend/src/types/tree.ts` | Fix 3: add `'answer'` to NodeType union | +| `backend/app/core/tree_validation.py` | Fix 3: allow 'answer' in draft; block on publish | +| `frontend/src/pages/TreeEditorPage.tsx` | Fix 3: frontend publish guard | + +--- + +## Non-Goals + +- No changes to session navigation, procedural flows, or maintenance flows +- No changes to the Code mode editor +- No changes to `treeEditorStore.ts` store actions (addNode, updateNode, deleteNode are used as-is) +- No third-party tooltip library +- No new backend endpoints + +--- + +## Verification + +1. Open a troubleshooting tree in the canvas editor +2. Click a decision node → card expands, form is scrollable with sticky save/cancel header +3. Field labels show `ⓘ` icons; hovering reveals the hint text +4. Type answer labels in the Options section; click ✓ to save +5. Answer stub cards appear as dashed cards below the decision node +6. Click a stub card → type picker appears; select "Decision" → card converts and expands for editing +7. Draft save works with answer nodes present (no backend error) +8. Attempt to publish with unresolved answer nodes → blocked with a clear error message +9. `npm run build` passes with no TypeScript errors