From b127b287115c7564c7d322b190b1f9ed0a0c5bf7 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Feb 2026 00:55:36 -0500 Subject: [PATCH 01/16] docs: add implementation plan for flow editor UX fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6-phase, 16-task plan covering: canvas card scroll + fullscreen modal, InfoTip component + tooltip replacements in all 3 NodeForm components, answer stub type system (types → AnswerStubCard → TreeCanvas wiring → NodeList guard), backend draft/publish validation, markdown serializer compatibility, and session navigation defensive guard. Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/2026-02-18-flow-editor-ux-impl.md | 1327 ++++++++++++++++++ 1 file changed, 1327 insertions(+) create mode 100644 docs/plans/2026-02-18-flow-editor-ux-impl.md diff --git a/docs/plans/2026-02-18-flow-editor-ux-impl.md b/docs/plans/2026-02-18-flow-editor-ux-impl.md new file mode 100644 index 00000000..4eb4c2b6 --- /dev/null +++ b/docs/plans/2026-02-18-flow-editor-ux-impl.md @@ -0,0 +1,1327 @@ +# 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 `
` (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 `
` (around line 324) — it's the one that appears under `{isExpanded && (`: + +```tsx +
+``` + +Change it to: + +```tsx +
+``` + +**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 " +``` + +--- + +### 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 ( +
+ {/* Backdrop */} + + ) +} + +export default Modal +``` + +**Step 2: Pass `allowFullScreen` to NodeEditorModal** + +In `frontend/src/components/tree-editor/NodeEditorModal.tsx`, find line 86: + +```tsx + +``` + +Change to: + +```tsx + +``` + +**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 " +``` + +--- + +## 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 ( + + i + + ) +} +``` + +**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 " +``` + +--- + +### 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 && ( +

+ What's the main question to diagnose the issue? +

+)} +``` + +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 + +{isRootNode ? ( +

+ Add as many options as needed (A, B, C, D...). Each option leads to a completely different troubleshooting path. +

+) : ( +

+ Each option can branch to a different next step. +

+)} +``` + +Replace with: + +```tsx + +``` + +**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 " +``` + +--- + +### 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 +

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

+``` + +And the Description label above it (around line 77–79): + +```tsx + +``` + +Replace both with (combine label + infotip, remove paragraph): + +```tsx + +``` + +**Step 3: Replace the Commands hint paragraph** + +Around lines 124–126: + +```tsx +

+ PowerShell or CLI commands to execute +

+``` + +And the Commands label above it: + +```tsx + +``` + +Replace with: + +```tsx + +``` + +**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 " +``` + +--- + +### 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 + +

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

+``` + +With: + +```tsx + +``` + +**Step 3: Replace the Resolution Steps hint paragraph** + +Around lines 118–120: + +```tsx + +

+ Step-by-step instructions for resolving the issue +

+``` + +Replace with: + +```tsx + +``` + +**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 " +``` + +--- + +## 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` 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 " +``` + +--- + +### 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 ( +
!picking && setPicking(true)} + > + {/* Label */} +
+ {label} +
+ + {/* Prompt / type picker */} + {!picking ? ( +
+ + Choose Type +
+ ) : ( +
+ + + +
+ )} +
+ ) +} + +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 " +``` + +--- + +### 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 " +``` + +--- + +### 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 ``. 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 ( +
+ + {letter} + +
+ 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 && ( +

{optionLabelError.message}

+ )} +
+
+ ) +}} +``` + +**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 " +``` + +--- + +### 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) => { + 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 `` is rendered (it's the card component, around line 468). Wrap it with a conditional: + +Replace: + +```tsx +{/* The node card itself */} + +``` + +With: + +```tsx +{/* The node card — answer stubs get their own component */} +{node.type === 'answer' ? ( + +) : ( + 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 " +``` + +--- + +### 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` which now requires an `'answer'` entry. + +**Step 1: Add `'answer'` entries to both records** + +Find: + +```tsx +const nodeTypeIcons: Record = { + decision: , + action: , + solution: +} + +const nodeTypeColors: Record = { + 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 = { + decision: , + action: , + solution: , + answer: +} + +const nodeTypeColors: Record = { + 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 " +``` + +--- + +## 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 " +``` + +--- + +### 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 " +``` + +--- + +## 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 " +``` + +--- + +### 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 `
`. Inside this `
`, 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' && ( +
+

+ This tree contains an unresolved placeholder node. Please contact the tree author to complete it before use. +

+
+)} +``` + +**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 " +``` + +--- + +## 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 | -- 2.49.1 From 346b99ff31577e95d23486c3ab6d7f32861fa2e6 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Feb 2026 17:19:24 -0500 Subject: [PATCH 02/16] docs: add feedback form design document Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-02-18-feedback-form-design.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/plans/2026-02-18-feedback-form-design.md diff --git a/docs/plans/2026-02-18-feedback-form-design.md b/docs/plans/2026-02-18-feedback-form-design.md new file mode 100644 index 00000000..d59b6ea5 --- /dev/null +++ b/docs/plans/2026-02-18-feedback-form-design.md @@ -0,0 +1,117 @@ +# Feedback Form — Design Document + +> **Date:** 2026-02-18 + +## Overview + +A feedback form page where logged-in users can submit bug reports, feature requests, and general feedback. Submissions are emailed to a configurable address via the existing Resend infrastructure. No database storage. + +## Feedback Types + +1. Bug Report +2. Feature Request +3. Usability Issue +4. Documentation +5. General Feedback + +## Email Format + +**Subject:** +``` +[ResolutionFlow Feedback] Bug Report — 2026-02-18 — ACC-7X3K +``` + +Where `ACC-7X3K` is the user's account display code. + +**Body:** +``` +Feedback Type: Bug Report +Submitted By: engineer@example.com +Account: Contoso IT Services (ACC-7X3K) +Date: February 18, 2026 + +--- + +User's written feedback text goes here... +``` + +Reply-to is set to the submitter's email for direct replies. + +## Frontend + +### Page + +`FeedbackPage.tsx` — form page inside the app shell. + +### Access Points + +- Sidebar nav item (icon + "Feedback" label) — visible to all roles +- Link card on `AccountSettingsPage` + +### Route + +`/feedback` — top-level app shell route (not nested under `/account`). + +### Form Fields + +| Field | Details | +|-------|---------| +| Email | Auto-filled from logged-in user, editable | +| Feedback Type | Dropdown select (5 types) | +| Message | Textarea, required, min 10 chars | +| Submit | `bg-gradient-brand`, disabled while submitting | + +### UX Flow + +1. Form loads with email pre-filled +2. User selects type, writes message, submits +3. Button shows loading state during submission +4. On success: success message, form resets +5. On error: inline error, form stays populated for retry + +### Styling + +Standard page layout — `container mx-auto`, `bg-card border border-border rounded-xl` form card, `max-w-2xl` width. + +### API Client + +`feedbackApi.submitFeedback()` in a new `api/feedback.ts` module. + +## Backend + +### Endpoint + +`POST /feedback` in `endpoints/feedback.py` + +### Schema + +`FeedbackSubmission` in `schemas/feedback.py`: +- `email: str` — validated as email +- `feedback_type: str` — enum-validated against the 5 types +- `message: str` — min 10 chars + +### Auth + +Requires `get_current_active_user`. Account display code pulled from user's account relationship server-side. + +### Config + +`FEEDBACK_EMAIL: Optional[str] = None` in `config.py`. Endpoint returns 503 if not configured. + +### Email Service + +New `EmailService.send_feedback_email()` static method: +- Uses existing Resend client +- Sets `reply_to` to submitter's email +- Dark-themed HTML matching existing email templates + +### Rate Limiting + +One submission per minute per user. + +## Not Included (YAGNI) + +- No DB persistence +- No admin view +- No file attachments +- No public (unauthenticated) access -- 2.49.1 From b0b77d66456a8bca1f1ea36b64b654bd0572549e Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Feb 2026 17:22:09 -0500 Subject: [PATCH 03/16] docs: add feedback form implementation plan Co-Authored-By: Claude Opus 4.6 --- ...2026-02-18-feedback-form-implementation.md | 817 ++++++++++++++++++ 1 file changed, 817 insertions(+) create mode 100644 docs/plans/2026-02-18-feedback-form-implementation.md diff --git a/docs/plans/2026-02-18-feedback-form-implementation.md b/docs/plans/2026-02-18-feedback-form-implementation.md new file mode 100644 index 00000000..80135d1d --- /dev/null +++ b/docs/plans/2026-02-18-feedback-form-implementation.md @@ -0,0 +1,817 @@ +# Feedback Form Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add a feedback form page where logged-in users can submit feedback that gets emailed to a configurable address via the existing Resend infrastructure. + +**Architecture:** New `POST /feedback` backend endpoint validates input and sends an HTML email via the existing `EmailService`. Frontend is a single `FeedbackPage.tsx` form page accessible from the sidebar nav and account settings. No database storage — email-only. + +**Tech Stack:** FastAPI + Pydantic (backend), React + TypeScript + Tailwind (frontend), Resend (email delivery), slowapi (rate limiting) + +**Design doc:** `docs/plans/2026-02-18-feedback-form-design.md` + +--- + +## Task 1: Backend Schema + +**Files:** +- Create: `backend/app/schemas/feedback.py` + +**Step 1: Create the Pydantic schema** + +```python +from enum import Enum +from pydantic import BaseModel, EmailStr, Field + + +class FeedbackType(str, Enum): + BUG_REPORT = "Bug Report" + FEATURE_REQUEST = "Feature Request" + USABILITY_ISSUE = "Usability Issue" + DOCUMENTATION = "Documentation" + GENERAL = "General Feedback" + + +class FeedbackSubmission(BaseModel): + email: EmailStr + feedback_type: FeedbackType + message: str = Field(..., min_length=10, max_length=5000) + + +class FeedbackResponse(BaseModel): + success: bool + message: str +``` + +**Step 2: Commit** + +```bash +git add backend/app/schemas/feedback.py +git commit -m "feat: add feedback submission schema" +``` + +--- + +## Task 2: Config — Add FEEDBACK_EMAIL + +**Files:** +- Modify: `backend/app/core/config.py` + +**Step 1: Add the FEEDBACK_EMAIL setting** + +In `backend/app/core/config.py`, add this line in the `Settings` class after the `FROM_EMAIL` line (line 57): + +```python + FEEDBACK_EMAIL: Optional[str] = None +``` + +**Step 2: Commit** + +```bash +git add backend/app/core/config.py +git commit -m "feat: add FEEDBACK_EMAIL config setting" +``` + +--- + +## Task 3: Email Service — Add send_feedback_email + +**Files:** +- Modify: `backend/app/core/email.py` + +**Step 1: Add the send_feedback_email method** + +Add this method to the `EmailService` class (after `send_account_invite_email`, before the helper functions): + +```python + @staticmethod + async def send_feedback_email( + to_email: str, + reply_to_email: str, + feedback_type: str, + message: str, + user_email: str, + account_name: str | None = None, + account_code: str | None = None, + ) -> bool: + if not settings.email_enabled: + logger.warning("Email not sent — RESEND_API_KEY not configured") + return False + + try: + import resend + from datetime import datetime, timezone + + resend.api_key = settings.RESEND_API_KEY + + date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d") + code_suffix = f" — {account_code}" if account_code else "" + subject = f"[ResolutionFlow Feedback] {feedback_type} — {date_str}{code_suffix}" + + html = _render_feedback_html( + feedback_type=feedback_type, + message=message, + user_email=user_email, + account_name=account_name, + account_code=account_code, + ) + + resend.Emails.send( + { + "from": settings.FROM_EMAIL, + "to": [to_email], + "reply_to": reply_to_email, + "subject": subject, + "html": html, + } + ) + logger.info("Feedback email sent from %s (type: %s)", user_email, feedback_type) + return True + + except Exception: + logger.exception("Failed to send feedback email from %s", user_email) + return False +``` + +**Step 2: Add the HTML renderer** + +Add this function at the bottom of the file (after the other `_render_*` functions): + +```python +def _render_feedback_html( + feedback_type: str, + message: str, + user_email: str, + account_name: str | None, + account_code: str | None, +) -> str: + from datetime import datetime, timezone + import html + + date_str = datetime.now(timezone.utc).strftime("%B %d, %Y") + safe_message = html.escape(message).replace("\n", "
") + + account_line = "" + if account_name and account_code: + account_line = f""" + +

+ Account: {html.escape(account_name)} ({html.escape(account_code)}) +

+ """ + + return f""" + + + + +
+ + + + + {account_line} + + + +
+

ResolutionFlow Feedback

+
+

+ Type: {html.escape(feedback_type)} +

+
+

+ From: {html.escape(user_email)} +

+
+

+ Date: {date_str} +

+
+
+

{safe_message}

+
+
+

+ Reply directly to this email to respond to the user. +

+
+
+""" +``` + +**Step 3: Commit** + +```bash +git add backend/app/core/email.py +git commit -m "feat: add send_feedback_email to EmailService" +``` + +--- + +## Task 4: Backend Endpoint + +**Files:** +- Create: `backend/app/api/endpoints/feedback.py` +- Modify: `backend/app/api/router.py` + +**Step 1: Create the endpoint** + +Create `backend/app/api/endpoints/feedback.py`: + +```python +import logging +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select + +from app.api.deps import get_current_active_user +from app.core.config import settings +from app.core.database import get_db +from app.core.email import EmailService +from app.core.rate_limit import limiter +from app.models.user import User +from app.models.account import Account +from app.schemas.feedback import FeedbackSubmission, FeedbackResponse + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=["feedback"]) + + +@router.post("/feedback", response_model=FeedbackResponse) +@limiter.limit("1/minute") +async def submit_feedback( + request: Request, + data: FeedbackSubmission, + current_user: Annotated[User, Depends(get_current_active_user)], + db: Annotated[AsyncSession, Depends(get_db)], +): + """Submit user feedback via email.""" + if not settings.FEEDBACK_EMAIL: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Feedback submission is not configured", + ) + + # Get account info for the email + account_name = None + account_code = None + if current_user.account_id: + result = await db.execute( + select(Account).where(Account.id == current_user.account_id) + ) + account = result.scalar_one_or_none() + if account: + account_name = account.name + account_code = account.display_code + + sent = await EmailService.send_feedback_email( + to_email=settings.FEEDBACK_EMAIL, + reply_to_email=data.email, + feedback_type=data.feedback_type.value, + message=data.message, + user_email=current_user.email, + account_name=account_name, + account_code=account_code, + ) + + if not sent: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to send feedback. Please try again later.", + ) + + return FeedbackResponse(success=True, message="Thank you! Your feedback has been submitted.") +``` + +**Step 2: Register the router** + +In `backend/app/api/router.py`, add the import and include: + +Add to imports (line 6, after the existing imports): +```python +from app.api.endpoints import feedback +``` + +Add at the end of the router registrations: +```python +api_router.include_router(feedback.router) +``` + +**Step 3: Commit** + +```bash +git add backend/app/api/endpoints/feedback.py backend/app/api/router.py +git commit -m "feat: add POST /feedback endpoint" +``` + +--- + +## Task 5: Backend Test + +**Files:** +- Create: `backend/tests/test_feedback.py` + +**Step 1: Write the test** + +```python +import pytest +from unittest.mock import patch, AsyncMock + + +@pytest.mark.asyncio +async def test_submit_feedback(async_client, engineer_token, monkeypatch): + """Test successful feedback submission.""" + monkeypatch.setenv("FEEDBACK_EMAIL", "support@test.com") + # Reload settings to pick up the env var + from app.core.config import Settings + test_settings = Settings() + + with patch("app.api.endpoints.feedback.settings") as mock_settings, \ + patch("app.api.endpoints.feedback.EmailService") as mock_email: + mock_settings.FEEDBACK_EMAIL = "support@test.com" + mock_email.send_feedback_email = AsyncMock(return_value=True) + + response = await async_client.post( + "/api/v1/feedback", + json={ + "email": "engineer@resolutionflow.example.com", + "feedback_type": "Bug Report", + "message": "Something is broken in the tree editor when I try to save.", + }, + headers={"Authorization": f"Bearer {engineer_token}"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["success"] is True + assert "submitted" in data["message"].lower() + + +@pytest.mark.asyncio +async def test_submit_feedback_not_configured(async_client, engineer_token): + """Test 503 when FEEDBACK_EMAIL is not set.""" + with patch("app.api.endpoints.feedback.settings") as mock_settings: + mock_settings.FEEDBACK_EMAIL = None + + response = await async_client.post( + "/api/v1/feedback", + json={ + "email": "engineer@resolutionflow.example.com", + "feedback_type": "General Feedback", + "message": "This is a general feedback message for testing.", + }, + headers={"Authorization": f"Bearer {engineer_token}"}, + ) + + assert response.status_code == 503 + + +@pytest.mark.asyncio +async def test_submit_feedback_validation(async_client, engineer_token): + """Test validation — message too short.""" + with patch("app.api.endpoints.feedback.settings") as mock_settings: + mock_settings.FEEDBACK_EMAIL = "support@test.com" + + response = await async_client.post( + "/api/v1/feedback", + json={ + "email": "engineer@resolutionflow.example.com", + "feedback_type": "Bug Report", + "message": "short", + }, + headers={"Authorization": f"Bearer {engineer_token}"}, + ) + + assert response.status_code == 422 + + +@pytest.mark.asyncio +async def test_submit_feedback_invalid_type(async_client, engineer_token): + """Test validation — invalid feedback type.""" + with patch("app.api.endpoints.feedback.settings") as mock_settings: + mock_settings.FEEDBACK_EMAIL = "support@test.com" + + response = await async_client.post( + "/api/v1/feedback", + json={ + "email": "engineer@resolutionflow.example.com", + "feedback_type": "Invalid Type", + "message": "This should fail because the type is invalid.", + }, + headers={"Authorization": f"Bearer {engineer_token}"}, + ) + + assert response.status_code == 422 + + +@pytest.mark.asyncio +async def test_submit_feedback_requires_auth(async_client): + """Test that unauthenticated requests are rejected.""" + response = await async_client.post( + "/api/v1/feedback", + json={ + "email": "anon@example.com", + "feedback_type": "General Feedback", + "message": "This should fail because I'm not logged in.", + }, + ) + assert response.status_code == 401 +``` + +**Step 2: Run the tests** + +```bash +cd backend && pytest tests/test_feedback.py -v --override-ini="addopts=" +``` + +Expected: All 5 tests pass. If any fail, debug and fix before proceeding. + +**Step 3: Commit** + +```bash +git add backend/tests/test_feedback.py +git commit -m "test: add feedback endpoint tests" +``` + +--- + +## Task 6: Frontend API Client + +**Files:** +- Create: `frontend/src/api/feedback.ts` +- Modify: `frontend/src/api/index.ts` + +**Step 1: Create the API module** + +Create `frontend/src/api/feedback.ts`: + +```typescript +import { apiClient } from './client' + +export interface FeedbackSubmission { + email: string + feedback_type: string + message: string +} + +export interface FeedbackResponse { + success: boolean + message: string +} + +export const feedbackApi = { + submit: async (data: FeedbackSubmission): Promise => { + const { data: response } = await apiClient.post('/feedback', data) + return response + }, +} + +export default feedbackApi +``` + +**Step 2: Export from index** + +In `frontend/src/api/index.ts`, add at the end: + +```typescript +export { default as feedbackApi } from './feedback' +``` + +**Step 3: Commit** + +```bash +git add frontend/src/api/feedback.ts frontend/src/api/index.ts +git commit -m "feat: add feedback API client" +``` + +--- + +## Task 7: Frontend Page + +**Files:** +- Create: `frontend/src/pages/FeedbackPage.tsx` + +**Step 1: Create the page component** + +```tsx +import { useState } from 'react' +import { MessageSquareText, Send, CheckCircle2 } from 'lucide-react' +import { useAuthStore } from '@/store/authStore' +import { feedbackApi } from '@/api' +import { cn } from '@/lib/utils' +import { toast } from '@/lib/toast' + +const FEEDBACK_TYPES = [ + 'Bug Report', + 'Feature Request', + 'Usability Issue', + 'Documentation', + 'General Feedback', +] as const + +export function FeedbackPage() { + const user = useAuthStore(s => s.user) + + const [email, setEmail] = useState(user?.email ?? '') + const [feedbackType, setFeedbackType] = useState('') + const [message, setMessage] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + const [submitted, setSubmitted] = useState(false) + + const canSubmit = email.trim() && feedbackType && message.trim().length >= 10 + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!canSubmit || isSubmitting) return + + setIsSubmitting(true) + try { + const response = await feedbackApi.submit({ + email: email.trim(), + feedback_type: feedbackType, + message: message.trim(), + }) + if (response.success) { + setSubmitted(true) + setFeedbackType('') + setMessage('') + } + } catch (err: unknown) { + const error = err as { response?: { data?: { detail?: string } } } + toast.error(error.response?.data?.detail || 'Failed to submit feedback. Please try again.') + } finally { + setIsSubmitting(false) + } + } + + const handleNewFeedback = () => { + setSubmitted(false) + setEmail(user?.email ?? '') + } + + return ( +
+ {/* Page header */} +
+
+ +

Send Feedback

+
+

+ Help us improve ResolutionFlow. Report bugs, request features, or share your thoughts. +

+
+ +
+ {submitted ? ( +
+ +

Thank you for your feedback!

+

+ We've received your submission and will review it shortly. +

+ +
+ ) : ( +
+ {/* Email */} +
+ + setEmail(e.target.value)} + placeholder="your@email.com" + required + className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none" + /> +

We'll reply to this address if we need more details.

+
+ + {/* Feedback Type */} +
+ + +
+ + {/* Message */} +
+ +