import { useState, useEffect, useRef, useMemo, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { Search, Loader2, Star, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react' import { treesApi } from '@/api/trees' import { sessionsApi } from '@/api/sessions' import type { TreeListItem, TreeFilters } from '@/types' import type { Session } from '@/types/session' import { getTreeNavigatePath } from '@/lib/routing' import { usePermissions } from '@/hooks/usePermissions' import { useAuthStore } from '@/store/authStore' import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore' import { useUserPreferencesStore } from '@/store/userPreferencesStore' import { usePaginationParams } from '@/hooks/usePaginationParams' import { useCachedQuota } from '@/hooks/useCachedQuota' import { QuickStats } from '@/components/dashboard/QuickStats' import { SessionsPanel } from '@/components/dashboard/SessionsPanel' import { TreeGridView } from '@/components/library/TreeGridView' import { TreeListView } from '@/components/library/TreeListView' import { TreeTableView } from '@/components/library/TreeTableView' import { ViewToggle } from '@/components/library/ViewToggle' import { AIFlowBuilderModal } from '@/components/ai-builder/AIFlowBuilderModal' import { CreateFlowDropdown } from '@/components/common/CreateFlowDropdown' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' function timeAgo(dateStr: string): string { const now = Date.now() const then = new Date(dateStr).getTime() const diffMs = now - then const minutes = Math.floor(diffMs / 60000) if (minutes < 1) return 'just now' if (minutes < 60) return `${minutes}m ago` const hours = Math.floor(minutes / 60) if (hours < 24) return `${hours}h ago` const days = Math.floor(hours / 24) if (days === 1) return 'Yesterday' return `${days}d ago` } export function QuickStartPage() { const navigate = useNavigate() const { canCreateTrees } = usePermissions() const user = useAuthStore((s) => s.user) // Search state const [query, setQuery] = useState('') const [searchResults, setSearchResults] = useState([]) const [isSearching, setIsSearching] = useState(false) const [showResults, setShowResults] = useState(false) const searchRef = useRef(null) const debounceRef = useRef | null>(null) // Sessions state const [activeSessions, setActiveSessions] = useState([]) const [allSessions, setAllSessions] = useState([]) // My Flows state const [myFlows, setMyFlows] = useState([]) const [isLoadingFlows, setIsLoadingFlows] = useState(true) const [hasNextPage, setHasNextPage] = useState(false) const [allFlowsCeiling, setAllFlowsCeiling] = useState(false) // Favorites state const [showAllFavorites, setShowAllFavorites] = useState(false) // AI Builder const [showAIBuilder, setShowAIBuilder] = useState(false) const { aiEnabled } = useCachedQuota() // Tab state type Tab = 'mine' | 'team' | 'public' | 'all' const hasTeam = Boolean(user?.account_id) const [activeTab, setActiveTab] = useState('mine') // Fork modal state const [forkTarget, setForkTarget] = useState(null) const [forkReason, setForkReason] = useState('') const [isForking, setIsForking] = useState(false) // Pin store const pinnedItems = usePinnedFlowsStore((s) => s.items) const pinnedIsLoading = usePinnedFlowsStore((s) => s.isLoading) const loadPinned = usePinnedFlowsStore((s) => s.load) const isMutatingByTreeId = usePinnedFlowsStore((s) => s.isMutatingByTreeId) const togglePin = usePinnedFlowsStore((s) => s.toggle) const pinnedTreeIds = useMemo(() => new Set(pinnedItems.map((f) => f.tree_id)), [pinnedItems]) const pinLoadingTreeIds = useMemo( () => new Set(Object.entries(isMutatingByTreeId).filter(([, v]) => v).map(([k]) => k)), [isMutatingByTreeId] ) // Preferences const { dashboardMyFlowsView, setDashboardMyFlowsView } = useUserPreferencesStore() // Pagination const { page, pageSize, setPage, setPageSize } = usePaginationParams({ defaultPageSize: 10, allowedPageSizes: [10, 25, 50, 'all'], }) // Load pinned flows useEffect(() => { loadPinned() }, [loadPinned]) // Load sessions on mount useEffect(() => { Promise.all([ sessionsApi.list({ completed: false, size: 5 }).catch(() => []), sessionsApi.list({ size: 10 }).catch(() => []), ]).then(([active, recent]) => { setActiveSessions(active) setAllSessions(recent) }) }, []) // Load flows β€” tab-aware const loadFlows = useCallback(async () => { if (!user?.id) return setIsLoadingFlows(true) setAllFlowsCeiling(false) try { if (pageSize === 'all') { let allItems: TreeListItem[] = [] let skip = 0 const CHUNK = 100 const MAX = 500 while (true) { const params: TreeFilters = { sort_by: 'updated_at', limit: CHUNK, skip } if (activeTab === 'mine') params.author_id = user.id if (activeTab === 'team') params.visibility = 'team' if (activeTab === 'public') { params.visibility = 'public'; params.sort_by = 'usage_count' } const chunk = await treesApi.list(params) allItems = [...allItems, ...chunk] if (chunk.length < CHUNK || allItems.length >= MAX) { if (allItems.length >= MAX) { allItems = allItems.slice(0, MAX); setAllFlowsCeiling(true) } break } skip += CHUNK } setMyFlows(allItems) setHasNextPage(false) } else { const numSize = pageSize as number const params: TreeFilters = { sort_by: activeTab === 'public' ? 'usage_count' : 'updated_at', limit: numSize + 1, skip: (page - 1) * numSize, } if (activeTab === 'mine') params.author_id = user.id if (activeTab === 'team') params.visibility = 'team' if (activeTab === 'public') params.visibility = 'public' const response = await treesApi.list(params) setHasNextPage(response.length > numSize) setMyFlows(response.slice(0, numSize)) } } catch { // silently fail } finally { setIsLoadingFlows(false) } }, [user?.id, page, pageSize, activeTab]) useEffect(() => { loadFlows() }, [loadFlows]) // Reload on window focus (fixes stale data after returning from editor) useEffect(() => { const onFocus = () => loadFlows() window.addEventListener('focus', onFocus) return () => window.removeEventListener('focus', onFocus) }, [loadFlows]) // Debounced search useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current) if (query.length < 2) { setSearchResults([]) setShowResults(false) setIsSearching(false) return } setIsSearching(true) setShowResults(true) debounceRef.current = setTimeout(async () => { try { const results = await treesApi.search(query, 8) setSearchResults(results) } catch { setSearchResults([]) } finally { setIsSearching(false) } }, 300) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [query]) // Close search dropdown on outside click useEffect(() => { function handleClick(e: MouseEvent) { if (searchRef.current && !searchRef.current.contains(e.target as Node)) { setShowResults(false) } } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, []) // Stats const openSessions = activeSessions.length const todaySessions = allSessions.filter(s => { const d = new Date(s.started_at) const now = new Date() return d.toDateString() === now.toDateString() }).length const completedSessions = allSessions.filter(s => s.completed_at).length const recentSessionItems = allSessions.slice(0, 5).map(s => ({ id: s.id, treeName: s.tree_snapshot?.name || 'Unknown', status: (s.completed_at ? 'completed' : 'in_progress') as 'completed' | 'in_progress', ticketNumber: s.ticket_number || undefined, timeAgo: timeAgo(s.started_at), })) // Favorites display const MAX_VISIBLE_FAVORITES = 8 const visibleFavorites = showAllFavorites ? pinnedItems : pinnedItems.slice(0, MAX_VISIBLE_FAVORITES) const hasMoreFavorites = pinnedItems.length > MAX_VISIBLE_FAVORITES // Handlers const handleStartSession = (treeId: string, treeType?: string) => { navigate(getTreeNavigatePath(treeId, treeType)) } const handleDeleteTree = () => {} // Not used on dashboard const handleTagClick = () => {} // Not used on dashboard const handleFolderCreated = () => {} // Not used on dashboard const handleFork = async () => { if (!forkTarget) return setIsForking(true) try { const forked = await treesApi.fork(forkTarget.id, { fork_reason: forkReason.trim() || undefined, }) toast.success(`"${forked.name}" added to your flows`) setForkTarget(null) setForkReason('') setActiveTab('mine') } catch { toast.error('Failed to fork flow') } finally { setIsForking(false) } } // Page size options const pageSizeOptions: (number | 'all')[] = [10, 25, 50, 'all'] // Tabs const tabs: { id: Tab; label: string }[] = [ { id: 'mine', label: 'My Flows' }, ...(hasTeam ? [{ id: 'team' as Tab, label: 'My Team' }] : []), { id: 'public', label: 'Public' }, { id: 'all', label: 'All' }, ] return (
{/* Page Header */}

Dashboard

Welcome back. Here's what's happening with your flows.

{/* Quick Stats */} {/* Search */}
setQuery(e.target.value)} onFocus={() => query.length >= 2 && setShowResults(true)} placeholder="Search flows, sessions, tags…" className="w-full rounded-lg border border-border bg-card py-2.5 pl-9 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20" /> {showResults && (
{isSearching ? (
) : searchResults.length === 0 ? (
No results found
) : (
    {searchResults.map((tree) => (
  • ))}
)}
)}
{/* Recent Sessions */} {/* Favorites Section */}

Favorites {pinnedItems.length > 0 && ( ({pinnedItems.length}) )}

{hasMoreFavorites && ( )}
{pinnedIsLoading ? (
{Array.from({ length: 4 }).map((_, i) => (
))}
) : pinnedItems.length === 0 ? (

Star a flow to pin it here for quick access.

) : (
{visibleFavorites.map((flow) => ( ))}
)}
{/* My Flows Section β€” tabbed */}
{tabs.map((tab) => ( ))}
{activeTab === 'mine' && canCreateTrees && ( setShowAIBuilder(true)} /> )}
{isLoadingFlows ? (
{Array.from({ length: 6 }).map((_, i) => (
))}
) : myFlows.length === 0 ? (

{activeTab === 'mine' ? "You haven't created any flows yet." : activeTab === 'team' ? 'No team flows found.' : activeTab === 'public' ? 'No public flows found.' : 'No flows found.'}

{activeTab === 'mine' && canCreateTrees && ( setShowAIBuilder(true)} label="Create your first flow" /> )}
) : ( <> {allFlowsCeiling && (

Showing first 500 flows. Use search or filters to find specific flows.

)} {dashboardMyFlowsView === 'grid' && ( )} {dashboardMyFlowsView === 'list' && ( )} {dashboardMyFlowsView === 'table' && ( )} {/* Pagination controls */} {pageSize !== 'all' && (
Page {page}
Show:
)} {pageSize === 'all' && (
Show:
)} )}
{/* Fork Modal */} {forkTarget && (

Fork this flow?

Creates a copy of “{forkTarget.name}” under your account that you can edit freely.

setForkReason(e.target.value)} placeholder="e.g. Adding Cisco Meraki steps for our network" maxLength={255} className="mb-4 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20" onKeyDown={(e) => e.key === 'Enter' && handleFork()} />
)} {/* AI Builder Modal */} {showAIBuilder && ( setShowAIBuilder(false)} /> )}
) } export default QuickStartPage