- Updated IMPLEMENTATION-PLAN-STEP-LIBRARY-FRONTEND.md with design decisions: - Custom steps persistence: separate `custom_steps` field in sessions - Custom step navigation: full step type support (decision/action/solution) - Validation warnings: inline dismissible, no confirmation modal - Added backend migration task (B.10) for custom_steps field - Updated file count: 10 new, 8 modified, 1 migration - Clarified acceptance criteria for validation behavior - Created docs/plans/2026-02-03-draft-trees-feature.md: - Comprehensive design for draft trees and custom steps - Database schema, API changes, frontend UX patterns - Implementation phases and success metrics - Related to Issue #25 (planned for Phase 3) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
24 KiB
Implementation Plan: Step Library Frontend + Tree Editor Validation
Date: February 3, 2026 (Updated after refinement session) Scope: Issues #1, #8, #9, #10 Estimated Components: 10 new files, 8 modified files, 1 migration Parallel Workstreams: 2 Related Planning: Issue #25 (Draft Trees - Phase 3)
Plan Refinements (Feb 3, 2026)
This plan was refined through collaborative design discussion. Key changes from initial draft:
-
Custom Steps Persistence - Changed from storing in
decisionsarray to separatecustom_stepsJSONB field- Added: Backend migration task (B.10)
- Added: Session schema updates
- Enables: Session resuming with custom steps
-
Custom Step Navigation - Expanded to support all step types (decision/action/solution)
- Added: Detailed rendering logic for each type in B.11
- Enables: Custom branching decisions during troubleshooting
-
Validation Warnings - Clarified as inline/dismissible (no confirmation modal)
- Updated: Acceptance criteria to specify warning behavior
- Deferred: "Save as Draft" feature to Issue #25 (Phase 3)
Overview
This plan covers two parallel workstreams:
| Workstream | Issues | Description |
|---|---|---|
| A | #1 | Tree Editor Validation UI |
| B | #10 → #9 → #8 | Step Library Frontend (sequential) |
Both workstreams are independent and can be developed simultaneously.
Workstream A: Tree Editor Validation (Issue #1)
Current State
The validation logic already exists in treeEditorStore.ts:519-658. It checks:
- ✅ Tree name required
- ✅ Decision nodes need question + options
- ✅ Option labels required
- ✅ Action/Solution nodes need titles
- ✅ At least one solution node
- ✅ Orphan node detection
- ✅ Invalid node reference detection
What's Missing
- UI to display validation errors - No visual feedback shown to user
- Prevent save on errors - Save button should be disabled or show confirmation
- Circular reference detection - Not implemented in current validation
- Real-time validation - Currently only validates on explicit call
Tasks
A.1: Add Circular Reference Detection
File: frontend/src/store/treeEditorStore.ts
Add to the validate() function after line 624:
// Check for circular references in next_node_id chains
const detectCircularRefs = (startId: string, visited: Set<string> = new Set()): boolean => {
if (visited.has(startId)) return true
visited.add(startId)
const node = findNodeInTree(startId, state.treeStructure)
if (!node) return false
// Check options
if (node.options) {
for (const opt of node.options) {
if (opt.next_node_id && detectCircularRefs(opt.next_node_id, new Set(visited))) {
errors.push({
nodeId: node.id,
message: `Circular reference detected: "${opt.label}" creates a loop`,
severity: 'error'
})
return true
}
}
}
// Check next_node_id
if (node.next_node_id && detectCircularRefs(node.next_node_id, new Set(visited))) {
errors.push({
nodeId: node.id,
message: `Circular reference detected in node "${node.title || node.id}"`,
severity: 'error'
})
return true
}
return false
}
// Run from root
detectCircularRefs('root')
A.2: Create Validation Summary Component
File: frontend/src/components/tree-editor/ValidationSummary.tsx (NEW)
interface ValidationSummaryProps {
errors: ValidationError[]
onSelectNode: (nodeId: string) => void
}
Features:
- Collapsible panel showing error/warning count
- Click error to select the problematic node
- Color-coded: red for errors, yellow for warnings
- Icon indicators (AlertCircle, AlertTriangle from lucide-react)
A.3: Integrate Validation UI into TreeEditorPage
File: frontend/src/pages/TreeEditorPage.tsx
Modifications:
- Call
validate()before save attempt - Show ValidationSummary when errors or warnings exist
- Block save if any severity='error' exists (disable save button)
- Warnings are informational only - do not block save, no confirmation modal
- Add "Validate" button in toolbar for manual check
- Auto-validate on blur from form fields (debounced)
A.4: Visual Node Error Indicators
File: frontend/src/components/tree-editor/NodeList.tsx
Modifications:
- Add red border/highlight to nodes with validation errors
- Show error icon badge on problem nodes
- Tooltip on hover showing the specific error
Acceptance Criteria - Workstream A
- Cannot save tree without a name (error blocks save)
- Cannot save tree without at least one solution node (error blocks save)
- Validation errors display in a clear, clickable list
- Clicking an error selects the problematic node
- Circular references are detected and blocked (error)
- Orphan nodes show as warnings (informational, do not block save)
- Save button disabled when errors exist
- Warnings are dismissible/ignorable - user can save with warnings present
Workstream B: Step Library Frontend (Issues #10, #9, #8)
Dependencies
- Backend API complete ✅ (
/api/v1/steps/*,/api/v1/step-categories/*) - No frontend API client exists yet
Execution Order
#10 Step Library Browser Component
↓
#9 Custom Step Creation Modal (embeds browser)
↓
#8 Add Custom Step Button (triggers modal)
Issue #10: Step Library Browser Component
B.1: Create Steps API Client
File: frontend/src/api/steps.ts (NEW)
// API client for step library endpoints
export const stepsApi = {
list: (params?: StepListParams) => Promise<StepListItem[]>
get: (id: string) => Promise<Step>
create: (data: StepCreate) => Promise<Step>
update: (id: string, data: StepUpdate) => Promise<Step>
delete: (id: string) => Promise<void>
search: (query: string) => Promise<StepListItem[]>
getPopularTags: () => Promise<PopularTag[]>
rate: (id: string, data: RatingCreate) => Promise<Rating>
updateRating: (id: string, data: RatingUpdate) => Promise<Rating>
deleteRating: (id: string) => Promise<void>
getReviews: (id: string) => Promise<Review[]>
}
B.2: Create Step Categories API Client
File: frontend/src/api/stepCategories.ts (NEW)
export const stepCategoriesApi = {
list: () => Promise<StepCategory[]>
get: (id: string) => Promise<StepCategory>
}
B.3: Add TypeScript Types
File: frontend/src/types/step.ts (NEW)
export interface StepCommand {
label: string
command: string
command_type?: string
}
export interface StepContent {
instructions: string
help_text?: string
commands?: StepCommand[]
}
export interface Step {
id: string
title: string
step_type: 'decision' | 'action' | 'solution'
content: StepContent
visibility: 'private' | 'team' | 'public'
category_id?: string
category_name?: string
tags: string[]
usage_count: number
rating_average: number
rating_count: number
helpful_yes: number
helpful_no: number
is_featured: boolean
is_verified: boolean
created_by: string
author_name?: string
created_at: string
updated_at: string
}
export interface StepListItem {
id: string
title: string
step_type: string
visibility: string
category_id?: string
category_name?: string
tags: string[]
usage_count: number
rating_average: number
rating_count: number
is_featured: boolean
created_by: string
author_name?: string
created_at: string
}
export interface StepCategory {
id: string
name: string
description?: string
display_order: number
team_id?: string
is_active: boolean
}
export interface StepListParams {
visibility?: 'private' | 'team' | 'public'
category_id?: string
tags?: string[]
min_rating?: number
step_type?: 'decision' | 'action' | 'solution'
sort_by?: 'recent' | 'popular' | 'highest_rated' | 'most_used'
limit?: number
offset?: number
}
export interface PopularTag {
tag: string
count: number
}
B.4: Create Step Card Component
File: frontend/src/components/step-library/StepCard.tsx (NEW)
A card displaying:
- Step title and type badge (decision/action/solution)
- Category name
- Star rating (if rated) or "Not rated" placeholder
- Tags as chips (max 3 visible, "+N more" overflow)
- Author name and created date
- Usage count
- [Preview] and [Insert] buttons
B.5: Create Step Detail Modal
File: frontend/src/components/step-library/StepDetailModal.tsx (NEW)
Full step preview showing:
- Title, type, category, tags
- Full rating breakdown (star distribution chart)
- Instructions (markdown rendered)
- Commands (copyable code blocks)
- Help text
- Top reviews (2-3 shown, "See all" link)
- [Cancel] and [Insert Into Session] buttons
B.6: Create Step Library Browser Component
File: frontend/src/components/step-library/StepLibraryBrowser.tsx (NEW)
Main browser component with:
Header:
- Search input (full-text, debounced 300ms)
- Filter dropdowns: Category, Type, Min Rating, Sort By
- Popular tags as clickable chips
Body:
- Grouped sections: "My Steps", "Team Steps", "Community"
- Each section collapsible
- Virtualized list for performance (if >50 items)
- Loading skeletons while fetching
- Empty state: "No steps found. Create your first step!"
Footer:
- [+ Create New Step] button (optional, for standalone use)
Props:
interface StepLibraryBrowserProps {
onInsert: (step: Step) => void
onCreateNew?: () => void
showCreateButton?: boolean
}
B.7: Update API Index
File: frontend/src/api/index.ts
Add exports for stepsApi and stepCategoriesApi.
Acceptance Criteria - Issue #10
- Can search steps with full-text query
- Can filter by category, type, minimum rating
- Can sort by recent, popular, highest rated, most used
- Steps grouped by visibility (My Steps, Team, Community)
- Can click step to see full preview modal
- Can copy commands from preview
- Can click "Insert" to select a step
- Popular tags shown and clickable as quick filters
Issue #9: Custom Step Creation Modal
B.8: Create Step Form Component
File: frontend/src/components/step-library/StepForm.tsx (NEW)
Form fields:
- Step Type: Radio group (Decision / Action / Solution)
- Title: Text input (required)
- Instructions: Textarea with markdown support (required)
- Help Text: Textarea (optional)
- Commands: Dynamic array field (optional)
- Each command: label + command + type dropdown
- Category: Dropdown (optional)
- Tags: Tag input with autocomplete (optional)
- Visibility: Dropdown (Private / Team / Public)
- Checkbox: "Save to My Step Library for reuse"
B.9: Create Custom Step Modal
File: frontend/src/components/step-library/CustomStepModal.tsx (NEW)
Tabbed modal with:
- Tab 1: "Type My Own" - StepForm component
- Tab 2: "Browse Library" - StepLibraryBrowser component
Props:
interface CustomStepModalProps {
isOpen: boolean
onClose: () => void
onInsertStep: (step: Step | CustomStepDraft) => void
}
Behavior:
- Tab 1: User fills form, clicks [Insert Step]
- If "Save to library" checked, POST to
/api/v1/stepsfirst - Returns step data to parent
- If "Save to library" checked, POST to
- Tab 2: User browses, clicks Insert on a step
- Returns selected step to parent
Acceptance Criteria - Issue #9
- Modal has two tabs: "Type My Own" and "Browse Library"
- Can create custom step with type, title, instructions
- Can optionally add commands, help text, category, tags
- Can optionally save step to personal library
- Can switch to Browse tab and select existing step
- Insert button returns step data to parent component
Issue #8: Add Custom Step Button in Tree Navigation
B.10: Add Custom Steps Backend Support
Files:
backend/alembic/versions/XXXX_add_custom_steps_to_sessions.py(NEW - migration)backend/app/schemas/session.py(MODIFIED)backend/app/api/endpoints/sessions.py(MODIFIED)
Migration:
def upgrade():
# Add custom_steps JSONB column to sessions table
op.add_column('sessions',
sa.Column('custom_steps', JSONB, nullable=False, server_default='[]')
)
def downgrade():
op.drop_column('sessions', 'custom_steps')
Schema Updates:
# In DecisionRecord - no changes needed (still uses node_id)
# In SessionResponse - add custom_steps field
class SessionResponse(BaseModel):
# ... existing fields
custom_steps: list[dict[str, Any]] = Field(default_factory=list) # NEW
# In SessionUpdate - add custom_steps field
class SessionUpdate(BaseModel):
# ... existing fields
custom_steps: Optional[list[dict[str, Any]]] = None # NEW
Custom Step Structure in Database:
# Each item in custom_steps array:
{
"id": "uuid-string",
"inserted_after_node_id": "parent-node-id",
"step_data": {
"title": "Check Additional Logs",
"step_type": "action", # decision | action | solution
"content": {
"instructions": "Check /var/log/...",
"help_text": "Optional help",
"commands": [...] # optional
},
"category_id": "optional-uuid",
"tags": ["optional"]
},
"timestamp": "2026-02-03T10:30:00Z"
}
API Logic:
PUT /api/v1/sessions/{id}- Acceptcustom_stepsin request body- Validation: Ensure
step_data.step_typeis valid enum value - No cascade delete needed - custom steps are session-scoped
B.11: Modify TreeNavigationPage
File: frontend/src/pages/TreeNavigationPage.tsx
Add state:
const [showCustomStepModal, setShowCustomStepModal] = useState(false)
const [customSteps, setCustomSteps] = useState<CustomStep[]>([])
UI Changes:
- After each decision node's options, add:
<button
onClick={() => setShowCustomStepModal(true)}
className="mt-2 text-sm text-primary hover:underline"
>
+ Add Custom Step
</button>
-
Add CustomStepModal at bottom of component
-
Handle insert:
const handleInsertCustomStep = (step: Step | CustomStepDraft) => {
// Create custom step object
const customStep: CustomStep = {
id: crypto.randomUUID(),
inserted_after_node_id: currentNodeId,
step_data: step,
timestamp: new Date().toISOString()
}
// Add to local state
const newCustomSteps = [...customSteps, customStep]
setCustomSteps(newCustomSteps)
// Navigate to custom step (becomes current)
setCurrentNodeId(customStep.id)
setShowCustomStepModal(false)
// Save to backend
if (session) {
sessionsApi.update(session.id, {
custom_steps: newCustomSteps
}).catch(err => console.error('Failed to save custom step:', err))
}
}
- Render custom steps based on type:
{/* Render custom step in navigation flow */}
{currentNode?.id.startsWith('custom-') && (
<div className="rounded-lg border border-purple-200 bg-purple-50 p-4 dark:border-purple-800 dark:bg-purple-900/20">
{/* Custom Step Badge */}
<span className="mb-2 inline-block rounded-full bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-100">
Custom Step
</span>
{/* Render based on step type */}
{customStepNode.step_data.step_type === 'decision' && (
<>
<h2>{customStepNode.step_data.title}</h2>
<MarkdownContent content={customStepNode.step_data.content.instructions} />
{/* Render decision options - user creates them inline or uses predefined */}
<div className="mt-4 space-y-2">
<button onClick={() => handleCustomDecisionOption('yes')}>Yes</button>
<button onClick={() => handleCustomDecisionOption('no')}>No</button>
</div>
</>
)}
{customStepNode.step_data.step_type === 'action' && (
<>
<h2>{customStepNode.step_data.title}</h2>
<MarkdownContent content={customStepNode.step_data.content.instructions} />
{customStepNode.step_data.content.commands?.map(cmd => (
<pre key={cmd.label}>{cmd.command}</pre>
))}
<button onClick={handleContinueFromCustomAction}>Continue</button>
</>
)}
{customStepNode.step_data.step_type === 'solution' && (
<>
<h2>{customStepNode.step_data.title}</h2>
<MarkdownContent content={customStepNode.step_data.content.instructions} />
<button onClick={handleComplete}>Complete Session</button>
</>
)}
</div>
)}
Navigation Logic:
- Custom decision: User selects option → continues to next tree node or another custom step
- Custom action: User clicks Continue → returns to tree flow (next_node_id of original tree node)
- Custom solution: User clicks Complete → ends session
B.12: Update Session Export
File: frontend/src/components/session/ExportPreviewModal.tsx or backend/app/api/endpoints/sessions.py
Ensure custom steps are included in export with clear marking:
## Step 5: [CUSTOM] Check Additional Logs
*Custom step added by user*
Instructions: ...
B.13: Update Session Types
File: frontend/src/types/index.ts
Add:
export interface CustomStep {
id: string
inserted_after_node_id: string
step_data: Step | CustomStepDraft
timestamp: string
}
export interface CustomStepDraft {
title: string
step_type: 'decision' | 'action' | 'solution'
content: StepContent
category_id?: string
tags?: string[]
}
Acceptance Criteria - Issue #8
- "+ Add Custom Step" button visible at each decision point
- Button opens CustomStepModal
- Can insert step from "Type My Own" tab
- Can insert step from "Browse Library" tab
- Custom steps appear in session flow with visual indicator
- Custom steps included in session export
- Session continues after custom step
File Summary
New Files (10)
| File | Issue | Description |
|---|---|---|
frontend/components/tree-editor/ValidationSummary.tsx |
#1 | Error/warning display |
frontend/api/steps.ts |
#10 | Steps API client |
frontend/api/stepCategories.ts |
#10 | Categories API client |
frontend/types/step.ts |
#10 | Step TypeScript types |
frontend/components/step-library/StepCard.tsx |
#10 | Step list item card |
frontend/components/step-library/StepDetailModal.tsx |
#10 | Step preview modal |
frontend/components/step-library/StepLibraryBrowser.tsx |
#10 | Main browser component |
frontend/components/step-library/StepForm.tsx |
#9 | Step creation form |
frontend/components/step-library/CustomStepModal.tsx |
#9 | Tabbed modal wrapper |
backend/alembic/versions/XXXX_add_custom_steps.py |
#8 | Add custom_steps to sessions |
Modified Files (6)
| File | Issue | Changes |
|---|---|---|
frontend/store/treeEditorStore.ts |
#1 | Add circular reference detection |
frontend/pages/TreeEditorPage.tsx |
#1 | Integrate validation UI |
frontend/components/tree-editor/NodeList.tsx |
#1 | Node error indicators |
frontend/pages/TreeNavigationPage.tsx |
#8 | Add custom step button + rendering |
frontend/api/index.ts |
#10 | Export new API clients |
frontend/types/index.ts |
#8 | Add CustomStep types |
backend/app/schemas/session.py |
#8 | Add custom_steps field |
backend/app/api/endpoints/sessions.py |
#8 | Handle custom_steps in update |
Execution Plan
Phase 1: Foundation (Do First)
Workstream A Workstream B
───────────── ─────────────
A.1 Circular ref detection B.1 Steps API client
B.2 Categories API client
B.3 TypeScript types
Phase 2: Core Components
Workstream A Workstream B
───────────── ─────────────
A.2 ValidationSummary B.4 StepCard
A.3 TreeEditorPage integration B.5 StepDetailModal
A.4 NodeList indicators B.6 StepLibraryBrowser
Phase 3: Integration
Workstream A Workstream B
───────────── ─────────────
Testing & polish B.8 StepForm
B.9 CustomStepModal
B.10 Backend custom steps support (migration + schemas)
B.11 TreeNavigationPage integration
B.12-13 Export & types updates
Testing Checklist
Workstream A - Validation
- Create tree without name → Error shown, save blocked
- Create tree without solution → Error shown, save blocked
- Create decision without options → Error shown
- Create circular reference (A → B → A) → Error detected
- Create orphan node → Warning shown (can still save)
- Click error → Node selected
- Fix all errors → Save enabled
Workstream B - Step Library
- Open browser → Steps load, grouped by visibility
- Search "citrix" → Filtered results appear
- Filter by category → Results filtered
- Click step → Preview modal opens
- Click copy command → Copied to clipboard
- Click Insert → Modal closes, step returned
- Create custom step → Form validates, step inserted
- Save to library checked → Step saved to API
- Add custom step in navigation → Step appears with badge
- Complete session → Export includes custom steps
Notes for Implementation
- Use existing patterns: Follow Modal.tsx, TagInput.tsx, and existing page structures
- Dark mode: All components must support light/dark themes via Tailwind classes
- Keyboard navigation: Support Escape to close modals, Enter to submit
- Loading states: Show skeletons or spinners during API calls
- Error handling: Display user-friendly error messages, log details to console
- Accessibility: Use proper ARIA labels, maintain focus management in modals
Design Decisions (Resolved)
1. Custom Steps Persistence ✅
Decision: Separate custom_steps JSONB field in sessions table
Rationale:
- Clean separation of concerns (decisions track actions taken, custom_steps store step content)
- Enables session resuming with custom steps intact
- Easier to query and export custom steps separately
- Follows normalized data structure patterns
Implementation: Task B.10 adds migration + schema updates
2. Custom Step Navigation Flow ✅
Decision: Full step type support (decision/action/solution)
Rationale:
- Real-world troubleshooting requires branching after custom actions
- Example: "Try clearing cache" (action) → "Did it work?" (decision)
- Consistent with tree structure mental model
- Maximum flexibility for engineers
Implementation: Task B.11 renders custom steps based on step_type
3. Validation Warning UX ✅
Decision: Inline dismissible warnings, no confirmation modal
Rationale:
- Warnings are informational, not critical
- Orphan nodes might be intentional (work in progress)
- Trust users to manage their own tree quality
- Simpler UX, fewer clicks
- Draft feature (Issue #25) handles "save incomplete work" use case
Implementation: Task A.2-A.3 show warnings in ValidationSummary, don't block save
Future Enhancements (Out of Scope)
-
Draft Trees Feature - Documented in Issue #25
- Save incomplete trees without validation errors blocking
- Planned for Phase 3 after Step Library is complete
-
Standalone Step Library Page -
/stepsroute- Browse/manage personal step library outside of active sessions
- Add after Issue #10 is complete and usage patterns are understood
-
Rate Limiting - Backend concern
- Prevent step library spam
- Add if abuse is observed in production