Complete Phase 2: Frontend implementation with React + TypeScript

Frontend Features:
- React 18 + Vite + TypeScript + Tailwind CSS + Zustand
- JWT authentication with automatic token refresh
- Tree library with search and category filtering
- Full tree navigation (decision/action/solution nodes)
- Session management with notes and completion
- Session history with export (Markdown/Text/HTML)
- ErrorBoundary for graceful error handling

Backend Fixes:
- CORS: Added port 5174 to allowed origins
- Sessions: Fixed JSONB datetime serialization (mode='json')

Documentation:
- Updated PROGRESS.md with Phase 2 completion
- Updated 03-DEVELOPMENT-ROADMAP.md with checked items
- Added PHASE-2.5-PERSONAL-BRANCHING.md spec

Seed Data:
- Added backend/scripts/seed_data.py with Password Reset tree

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-01-27 22:42:22 -05:00
parent 7d96807fb1
commit cd10ecd42c
51 changed files with 9014 additions and 111 deletions

View File

@@ -0,0 +1,168 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { treesApi } from '@/api'
import type { TreeListItem } from '@/types'
import { cn } from '@/lib/utils'
export function TreeLibraryPage() {
const navigate = useNavigate()
const [trees, setTrees] = useState<TreeListItem[]>([])
const [categories, setCategories] = useState<string[]>([])
const [selectedCategory, setSelectedCategory] = useState<string>('')
const [searchQuery, setSearchQuery] = useState('')
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
loadData()
}, [selectedCategory])
const loadData = async () => {
setIsLoading(true)
setError(null)
try {
const [treesData, categoriesData] = await Promise.all([
treesApi.list({ category: selectedCategory || undefined }),
treesApi.categories(),
])
setTrees(treesData)
setCategories(categoriesData)
} catch (err) {
setError('Failed to load trees')
console.error(err)
} finally {
setIsLoading(false)
}
}
const handleSearch = async () => {
if (!searchQuery.trim()) {
loadData()
return
}
setIsLoading(true)
setError(null)
try {
const results = await treesApi.search(searchQuery, selectedCategory || undefined)
setTrees(results)
} catch (err) {
setError('Search failed')
console.error(err)
} finally {
setIsLoading(false)
}
}
const handleStartSession = (treeId: string) => {
navigate(`/trees/${treeId}/navigate`)
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground">Decision Trees</h1>
<p className="mt-2 text-muted-foreground">
Select a troubleshooting tree to start a new session
</p>
</div>
{/* Search and Filter */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row">
<div className="flex flex-1 gap-2">
<input
type="text"
placeholder="Search trees..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className={cn(
'flex-1 rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
/>
<button
onClick={handleSearch}
className={cn(
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
)}
>
Search
</button>
</div>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className={cn(
'rounded-md border border-input bg-background px-3 py-2',
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
>
<option value="">All Categories</option>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
</div>
{/* Error State */}
{error && (
<div className="mb-6 rounded-md bg-destructive/10 p-4 text-destructive">
{error}
</div>
)}
{/* Loading State */}
{isLoading ? (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
) : trees.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
No trees found. {searchQuery && 'Try adjusting your search.'}
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{trees.map((tree) => (
<div
key={tree.id}
className="rounded-lg border border-border bg-card p-6 shadow-sm transition-shadow hover:shadow-md"
>
<div className="mb-2 flex items-start justify-between">
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
{tree.category && (
<span className="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">
{tree.category}
</span>
)}
</div>
<p className="mb-4 text-sm text-muted-foreground line-clamp-2">
{tree.description || 'No description available'}
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">
v{tree.version} · {tree.usage_count} uses
</span>
<button
onClick={() => handleStartSession(tree.id)}
className={cn(
'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
)}
>
Start Session
</button>
</div>
</div>
))}
</div>
)}
</div>
)
}
export default TreeLibraryPage