# Dashboard: My Flows, Favorites/Pin UI, and AI Builder — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Transform the dashboard into a structured personal workspace with a Favorites section (pinned flows), paginated "My Flows", shared pin store, and AI Builder access from the Library page. **Architecture:** Zustand store (`pinnedFlowsStore`) becomes single source of truth for pin state across Sidebar, Dashboard, and Library. URL-synced pagination hook manages My Flows paging. Cached quota hook avoids redundant AI quota fetches. All three library view components gain optional pin button props. **Tech Stack:** React 19, Zustand, React Router v7, Tailwind CSS, Lucide icons, Axios **Design doc:** `docs/plans/2026-02-20-final-dashboard-plan.md` --- ## Task 1: Add `pin()` to `pinnedFlowsApi` **Files:** - Modify: `frontend/src/api/pinnedFlows.ts` **Step 1: Add `pin` method** In `frontend/src/api/pinnedFlows.ts`, add after the `unpin` method: ```typescript pin: async (treeId: string): Promise => { await apiClient.post(`/trees/${treeId}/pin`) }, ``` **Step 2: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` Expected: No errors **Step 3: Commit** ```bash git add frontend/src/api/pinnedFlows.ts git commit -m "feat: add pin() method to pinnedFlowsApi" ``` --- ## Task 2: Create `pinnedFlowsStore` **Files:** - Create: `frontend/src/store/pinnedFlowsStore.ts` **Step 1: Create the store** Create `frontend/src/store/pinnedFlowsStore.ts`: ```typescript import { create } from 'zustand' import { pinnedFlowsApi } from '@/api/pinnedFlows' import type { PinnedFlow } from '@/api/pinnedFlows' import { toast } from '@/lib/toast' interface PinnedFlowsState { items: PinnedFlow[] isLoaded: boolean isLoading: boolean isMutatingByTreeId: Record error: string | null // Actions load: (force?: boolean) => Promise pin: (treeId: string) => Promise unpin: (treeId: string) => Promise toggle: (treeId: string) => void // Derived helpers isPinned: (treeId: string) => boolean } export const usePinnedFlowsStore = create()((set, get) => ({ items: [], isLoaded: false, isLoading: false, isMutatingByTreeId: {}, error: null, load: async (force = false) => { const state = get() if (state.isLoaded && !force) return if (state.isLoading) return set({ isLoading: true, error: null }) try { const data = await pinnedFlowsApi.list() set({ items: data.items, isLoaded: true, isLoading: false }) } catch { set({ error: 'Failed to load pinned flows', isLoading: false }) } }, pin: async (treeId: string) => { const state = get() if (state.isMutatingByTreeId[treeId]) return set({ isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true } }) try { await pinnedFlowsApi.pin(treeId) // Reload to get full PinnedFlow object with tree_name, tree_type, etc. const data = await pinnedFlowsApi.list() set((s) => ({ items: data.items, isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false }, })) } catch (err: unknown) { const status = (err as { response?: { status?: number } })?.response?.status if (status === 409) { toast.error('Maximum of 15 favorites reached. Unpin a flow to add a new one.') } else { toast.error('Failed to pin flow') } set((s) => ({ isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false }, })) } }, unpin: async (treeId: string) => { const state = get() if (state.isMutatingByTreeId[treeId]) return // Optimistic remove const prevItems = state.items set({ items: state.items.filter((f) => f.tree_id !== treeId), isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true }, }) try { await pinnedFlowsApi.unpin(treeId) set((s) => ({ isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false }, })) } catch { // Rollback toast.error('Failed to unpin flow') set((s) => ({ items: prevItems, isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false }, })) } }, toggle: (treeId: string) => { const state = get() if (state.isPinned(treeId)) { state.unpin(treeId) } else { state.pin(treeId) } }, isPinned: (treeId: string) => { return get().items.some((f) => f.tree_id === treeId) }, })) // Derived selectors (use outside component or in selectors) export const selectPinnedTreeIds = (state: PinnedFlowsState): Set => new Set(state.items.map((f) => f.tree_id)) export const selectPinLoadingTreeIds = (state: PinnedFlowsState): Set => new Set( Object.entries(state.isMutatingByTreeId) .filter(([, v]) => v) .map(([k]) => k) ) ``` **Step 2: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` Expected: No errors **Step 3: Commit** ```bash git add frontend/src/store/pinnedFlowsStore.ts git commit -m "feat: create pinnedFlowsStore — single source of truth for pin state" ``` --- ## Task 3: Add `dashboardMyFlowsView` to `userPreferencesStore` **Files:** - Modify: `frontend/src/store/userPreferencesStore.ts` **Step 1: Add preference** In the `UserPreferencesState` interface, add: ```typescript dashboardMyFlowsView: 'grid' | 'list' | 'table' setDashboardMyFlowsView: (view: 'grid' | 'list' | 'table') => void ``` In the store implementation, add alongside existing fields: ```typescript dashboardMyFlowsView: 'grid', setDashboardMyFlowsView: (view) => set({ dashboardMyFlowsView: view }), ``` **Step 2: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` **Step 3: Commit** ```bash git add frontend/src/store/userPreferencesStore.ts git commit -m "feat: add dashboardMyFlowsView preference (independent from Library)" ``` --- ## Task 4: Replace local pin state in Sidebar with store **Files:** - Modify: `frontend/src/components/layout/Sidebar.tsx` **Step 1: Swap to store** Remove from imports: - `pinnedFlowsApi` import - `PinnedFlow` type import Add import: ```typescript import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore' ``` Remove from component: - `const [pinnedFlows, setPinnedFlows] = useState([])` - The `pinnedFlowsApi.list()` call from the `useEffect` - The entire `handleUnpin` function Replace with: ```typescript const pinnedItems = usePinnedFlowsStore((s) => s.items) const loadPinned = usePinnedFlowsStore((s) => s.load) const unpinFlow = usePinnedFlowsStore((s) => s.unpin) ``` In the `useEffect` that fetches data, remove the `pinnedFlowsApi.list()` from `Promise.all`. Add a separate call: ```typescript useEffect(() => { loadPinned() }, [loadPinned]) ``` Update `PinnedFlowsSection` props: ```tsx ``` **Step 2: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` **Step 3: Commit** ```bash git add frontend/src/components/layout/Sidebar.tsx git commit -m "refactor: sidebar uses pinnedFlowsStore instead of local state" ``` --- ## Task 5: Create `usePaginationParams` hook **Files:** - Create: `frontend/src/hooks/usePaginationParams.ts` **Step 1: Create the hook** ```typescript import { useSearchParams } from 'react-router-dom' import { useCallback, useMemo } from 'react' type PageSize = number | 'all' interface UsePaginationParamsOptions { defaultPageSize?: number allowedPageSizes?: PageSize[] } export function usePaginationParams(options: UsePaginationParamsOptions = {}) { const { defaultPageSize = 10, allowedPageSizes = [10, 25, 50, 'all'] } = options const [searchParams, setSearchParams] = useSearchParams() const page = useMemo(() => { const raw = searchParams.get('page') const n = raw ? parseInt(raw, 10) : 1 return Number.isFinite(n) && n >= 1 ? n : 1 }, [searchParams]) const pageSize = useMemo((): PageSize => { const raw = searchParams.get('size') if (raw === 'all' && allowedPageSizes.includes('all')) return 'all' const n = raw ? parseInt(raw, 10) : defaultPageSize if (Number.isFinite(n) && allowedPageSizes.includes(n)) return n return defaultPageSize }, [searchParams, defaultPageSize, allowedPageSizes]) const setPage = useCallback( (newPage: number) => { setSearchParams((prev) => { const next = new URLSearchParams(prev) if (newPage <= 1) { next.delete('page') } else { next.set('page', String(newPage)) } return next }) }, [setSearchParams] ) const setPageSize = useCallback( (newSize: PageSize) => { setSearchParams((prev) => { const next = new URLSearchParams(prev) next.set('size', String(newSize)) next.delete('page') // reset to page 1 return next }) }, [setSearchParams] ) return { page, pageSize, setPage, setPageSize } } ``` **Step 2: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` **Step 3: Commit** ```bash git add frontend/src/hooks/usePaginationParams.ts git commit -m "feat: create usePaginationParams hook — URL-synced pagination" ``` --- ## Task 6: Create `useCachedQuota` hook **Files:** - Create: `frontend/src/hooks/useCachedQuota.ts` **Step 1: Create the hook** ```typescript import { useState, useEffect } from 'react' import { aiBuilderApi } from '@/api' const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes let cachedResult: { aiEnabled: boolean; timestamp: number } | null = null export function useCachedQuota() { const [aiEnabled, setAiEnabled] = useState(cachedResult?.aiEnabled ?? false) const [isLoading, setIsLoading] = useState(!cachedResult) useEffect(() => { if (cachedResult && Date.now() - cachedResult.timestamp < CACHE_TTL_MS) { setAiEnabled(cachedResult.aiEnabled) setIsLoading(false) return } setIsLoading(true) aiBuilderApi .getQuota() .then((q) => { cachedResult = { aiEnabled: q.ai_enabled, timestamp: Date.now() } setAiEnabled(q.ai_enabled) }) .catch(() => { // Leave as false }) .finally(() => setIsLoading(false)) }, []) return { aiEnabled, isLoading } } ``` **Step 2: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` **Step 3: Commit** ```bash git add frontend/src/hooks/useCachedQuota.ts git commit -m "feat: create useCachedQuota hook — 5-min TTL AI quota cache" ``` --- ## Task 7: Add pin button props to `TreeGridView` **Files:** - Modify: `frontend/src/components/library/TreeGridView.tsx` **Step 1: Read current file to understand structure** Read `frontend/src/components/library/TreeGridView.tsx` fully before editing. **Step 2: Add optional pin props to interface** Add to `TreeGridViewProps`: ```typescript pinnedTreeIds?: Set onTogglePin?: (treeId: string) => void pinLoadingTreeIds?: Set ``` **Step 3: Add star button to each card** Import `Star` from `lucide-react`. In each card's top-right area, render (only when `onTogglePin` is provided): ```tsx {onTogglePin && ( )} ``` **Step 4: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` **Step 5: Commit** ```bash git add frontend/src/components/library/TreeGridView.tsx git commit -m "feat: add optional pin/favorite button to TreeGridView" ``` --- ## Task 8: Add pin button props to `TreeListView` **Files:** - Modify: `frontend/src/components/library/TreeListView.tsx` **Step 1: Read the file, then add same optional props and star button pattern as Task 7.** Place the star button at the end of each row (before the actions area). Use the exact same props interface addition and button pattern from Task 7. **Step 2: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` **Step 3: Commit** ```bash git add frontend/src/components/library/TreeListView.tsx git commit -m "feat: add optional pin/favorite button to TreeListView" ``` --- ## Task 9: Add pin button props to `TreeTableView` **Files:** - Modify: `frontend/src/components/library/TreeTableView.tsx` **Step 1: Read the file, then add same optional props.** Add a narrow "Favorite" column as the leftmost column. Header: star icon. Cell: same button pattern from Task 7. Only render the column when `onTogglePin` is provided. **Step 2: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` **Step 3: Commit** ```bash git add frontend/src/components/library/TreeTableView.tsx git commit -m "feat: add optional pin/favorite column to TreeTableView" ``` --- ## Task 10: TreeLibraryPage — Create dropdown + AI Builder + pin wiring **Files:** - Modify: `frontend/src/pages/TreeLibraryPage.tsx` **Step 1: Add imports** ```typescript import { ChevronDown, Sparkles, FolderTree, ListOrdered, Wrench } from 'lucide-react' import { usePinnedFlowsStore, selectPinnedTreeIds, selectPinLoadingTreeIds } from '@/store/pinnedFlowsStore' import { useCachedQuota } from '@/hooks/useCachedQuota' import { AIFlowBuilderModal } from '@/components/ai-builder/AIFlowBuilderModal' ``` **Step 2: Add state and store selectors** Inside the component, add: ```typescript const [showCreateMenu, setShowCreateMenu] = useState(false) const [showAIBuilder, setShowAIBuilder] = useState(false) const { aiEnabled } = useCachedQuota() const pinnedTreeIds = usePinnedFlowsStore(selectPinnedTreeIds) const pinLoadingTreeIds = usePinnedFlowsStore(selectPinLoadingTreeIds) const togglePin = usePinnedFlowsStore((s) => s.toggle) const loadPinned = usePinnedFlowsStore((s) => s.load) useEffect(() => { loadPinned() }, [loadPinned]) ``` **Step 3: Replace the `` create button** Replace the single `` create button with the dropdown menu pattern from `MyTreesPage.tsx` (lines 134-197). Copy that exact pattern — it already has Troubleshooting Tree, Procedural Flow, Maintenance Flow, divider, and Build with AI (conditional on `aiEnabled`). **Step 4: Pass pin props to view components** Add to each `TreeGridView`, `TreeListView`, `TreeTableView` render: ```tsx pinnedTreeIds={pinnedTreeIds} onTogglePin={togglePin} pinLoadingTreeIds={pinLoadingTreeIds} ``` **Step 5: Add AI Builder modal** Before the closing `` of the component, add: ```tsx {showAIBuilder && ( setShowAIBuilder(false)} /> )} ``` **Step 6: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` **Step 7: Commit** ```bash git add frontend/src/pages/TreeLibraryPage.tsx git commit -m "feat: Library page — create dropdown with AI Builder + pin controls on all views" ``` --- ## Task 11: QuickStartPage — Favorites section + paginated My Flows **Files:** - Modify: `frontend/src/pages/QuickStartPage.tsx` This is the largest task. The page gets a major refactor. **Step 1: Read the current file fully (already done in exploration)** **Step 2: Rewrite QuickStartPage** The page structure becomes: 1. **Page header** with "Create" dropdown (same pattern as Task 10) 2. **QuickStats** (keep existing) 3. **Search** (keep existing inline search) 4. **Recent Sessions** (keep existing `SessionsPanel`) 5. **Favorites section** (NEW — from `pinnedFlowsStore.items`) 6. **My Flows section** (NEW — paginated, `author_id` filter, view toggle) Key implementation details: **Favorites section:** - Source: `usePinnedFlowsStore` items - Layout: wrapping grid, max 2 rows (~8 cards). "View all favorites" expands if > 8. - Each card: flow name + type emoji + unpin star button - Skeleton: 4 pulse placeholder cards while `isLoading` - Empty: "Star a flow to pin it here for quick access." **My Flows section:** - Source: `treesApi.list({ author_id: user.id, sort_by: 'updated_at', limit: pageSize + 1, skip: (page - 1) * pageSize })` - Use `usePaginationParams({ defaultPageSize: 10, allowedPageSizes: [10, 25, 50, 'all'] })` - `hasNextPage = response.length > pageSize; displayItems = response.slice(0, pageSize)` - For "All": fetch in chunks of 100 up to 500 max - View toggle: `ViewToggle` bound to `dashboardMyFlowsView` - Render: `TreeGridView` / `TreeListView` / `TreeTableView` with pin props - Pagination controls: `Prev` / `Next` / page label / size dropdown - Skeleton: 6 placeholder cards/rows - Empty: "You haven't created any flows yet." + "Create your first flow" CTA **Remove:** - The `FiltersBar` (All, Recently Used, My Flows, Team Flows) — replaced by the Favorites + My Flows structure - The `SectionGroup` "All Flows" wrapper - The hard-cap of 20 items **Step 3: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npm run build` Expected: Build succeeds with no errors **Step 4: Commit** ```bash git add frontend/src/pages/QuickStartPage.tsx git commit -m "feat: dashboard — Favorites grid + paginated My Flows + skeletons + empty states" ``` --- ## Task 12: PinnedFlowsSection — Dual collapse + smooth transitions **Files:** - Modify: `frontend/src/components/sidebar/PinnedFlowsSection.tsx` **Step 1: Read the file (already done)** **Step 2: Implement dual collapse** Add state: ```typescript const [showAll, setShowAll] = useState(false) const TRUNCATE_COUNT = 5 ``` Modify header collapse to reset truncation: ```typescript const handleToggleCollapse = () => { if (collapsed) { setShowAll(false) // Reset to truncated on re-expand } setCollapsed(!collapsed) } ``` Replace the flow list rendering: ```typescript const visibleFlows = showAll ? flows : flows.slice(0, TRUNCATE_COUNT) const hasMore = flows.length > TRUNCATE_COUNT ``` Add click handler on flow buttons that auto-collapses: ```typescript onClick={() => { setShowAll(false) // Collapse back to 5 navigate(getTreeNavigatePath(flow.tree_id, flow.tree_type)) }} ``` Add "Show more" / "Show less" link after the flow list: ```tsx {hasMore && !collapsed && ( )} ``` Add CSS transition on the list container: ```tsx
``` **Step 3: Verify build** Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit` **Step 4: Commit** ```bash git add frontend/src/components/sidebar/PinnedFlowsSection.tsx git commit -m "feat: sidebar pinned section — dual collapse + show more/less + smooth transitions" ``` --- ## Task 13: Final build validation **Step 1: Full build check** Run: `cd /c/Dev/Projects/patherly/frontend && npm run build` Expected: Build succeeds with zero errors **Step 2: Fix any type errors or import issues** If build fails, fix issues and amend the relevant commit. **Step 3: Commit any final fixes** ```bash git add -A git commit -m "fix: resolve build errors from dashboard implementation" ``` --- ## Summary of files changed | # | File | Action | |---|------|--------| | 1 | `frontend/src/api/pinnedFlows.ts` | Add `pin()` method | | 2 | `frontend/src/store/pinnedFlowsStore.ts` | **New** — Zustand pin store | | 3 | `frontend/src/store/userPreferencesStore.ts` | Add `dashboardMyFlowsView` | | 4 | `frontend/src/components/layout/Sidebar.tsx` | Use store instead of local state | | 5 | `frontend/src/hooks/usePaginationParams.ts` | **New** — URL-synced pagination | | 6 | `frontend/src/hooks/useCachedQuota.ts` | **New** — AI quota cache | | 7 | `frontend/src/components/library/TreeGridView.tsx` | Optional pin button | | 8 | `frontend/src/components/library/TreeListView.tsx` | Optional pin button | | 9 | `frontend/src/components/library/TreeTableView.tsx` | Optional pin column | | 10 | `frontend/src/pages/TreeLibraryPage.tsx` | Create dropdown + AI Builder + pins | | 11 | `frontend/src/pages/QuickStartPage.tsx` | Major refactor: Favorites + My Flows | | 12 | `frontend/src/components/sidebar/PinnedFlowsSection.tsx` | Dual collapse |