diff --git a/frontend/src/pages/QuickStartPage.tsx b/frontend/src/pages/QuickStartPage.tsx index e07059d0..9afbbc85 100644 --- a/frontend/src/pages/QuickStartPage.tsx +++ b/frontend/src/pages/QuickStartPage.tsx @@ -1,17 +1,25 @@ import { useState, useEffect, useRef } from 'react' import { useNavigate, Link } from 'react-router-dom' -import { Search, Plus, Loader2 } from 'lucide-react' +import { Search, Plus, Loader2, Star, ChevronDown, ChevronLeft, ChevronRight, Sparkles, FolderTree, ListOrdered, Wrench } from 'lucide-react' import { treesApi } from '@/api/trees' import { sessionsApi } from '@/api/sessions' import type { TreeListItem } 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, selectPinnedTreeIds, selectPinLoadingTreeIds } 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 { FiltersBar } from '@/components/dashboard/FiltersBar' -import { SectionGroup } from '@/components/dashboard/SectionGroup' import { SessionsPanel } from '@/components/dashboard/SessionsPanel' -import { TreeListItem as TreeListItemComponent } from '@/components/dashboard/TreeListItem' +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 { cn } from '@/lib/utils' function timeAgo(dateStr: string): string { const now = Date.now() @@ -30,7 +38,9 @@ function timeAgo(dateStr: string): string { 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) @@ -38,34 +48,106 @@ export function QuickStartPage() { const searchRef = useRef(null) const debounceRef = useRef | null>(null) - const [trees, setTrees] = useState([]) + // Sessions state const [activeSessions, setActiveSessions] = useState([]) const [allSessions, setAllSessions] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [activeFilter, setActiveFilter] = useState('all') + // My Flows state + const [myFlows, setMyFlows] = useState([]) + const [isLoadingFlows, setIsLoadingFlows] = useState(true) + const [hasNextPage, setHasNextPage] = useState(false) + const [allFlowsCeiling, setAllFlowsCeiling] = useState(false) - // Load data on mount + // Favorites state + const [showAllFavorites, setShowAllFavorites] = useState(false) + + // Create menu + AI Builder + const [showCreateMenu, setShowCreateMenu] = useState(false) + const [showAIBuilder, setShowAIBuilder] = useState(false) + const { aiEnabled } = useCachedQuota() + + // Pin store + const pinnedItems = usePinnedFlowsStore((s) => s.items) + const pinnedIsLoading = usePinnedFlowsStore((s) => s.isLoading) + const loadPinned = usePinnedFlowsStore((s) => s.load) + const pinnedTreeIds = usePinnedFlowsStore(selectPinnedTreeIds) + const pinLoadingTreeIds = usePinnedFlowsStore(selectPinLoadingTreeIds) + const togglePin = usePinnedFlowsStore((s) => s.toggle) + + // 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(() => { - async function loadData() { - try { - const [treeList, active, recent] = await Promise.all([ - treesApi.list({ sort_by: 'updated_at' }), - sessionsApi.list({ completed: false, size: 5 }), - sessionsApi.list({ size: 10 }), - ]) - setTrees(treeList) - setActiveSessions(active) - setAllSessions(recent) - } catch (err) { - console.error('Failed to load dashboard data:', err) - } finally { - setIsLoading(false) - } - } - loadData() + Promise.all([ + sessionsApi.list({ completed: false, size: 5 }).catch(() => []), + sessionsApi.list({ size: 10 }).catch(() => []), + ]).then(([active, recent]) => { + setActiveSessions(active) + setAllSessions(recent) + }) }, []) + // Load my flows when page/size or user changes + useEffect(() => { + if (!user?.id) return + + const loadFlows = async () => { + setIsLoadingFlows(true) + setAllFlowsCeiling(false) + + if (pageSize === 'all') { + // Fetch in chunks of 100, max 500 + let allItems: TreeListItem[] = [] + let skip = 0 + const CHUNK = 100 + const MAX = 500 + + while (true) { + const chunk = await treesApi.list({ + author_id: user.id, + sort_by: 'updated_at', + limit: CHUNK, + skip, + }) + 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 response = await treesApi.list({ + author_id: user.id, + sort_by: 'updated_at', + limit: numSize + 1, + skip: (page - 1) * numSize, + }) + setHasNextPage(response.length > numSize) + setMyFlows(response.slice(0, numSize)) + } + setIsLoadingFlows(false) + } + + loadFlows().catch(() => setIsLoadingFlows(false)) + }, [user?.id, page, pageSize]) + // Debounced search useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current) @@ -90,7 +172,7 @@ export function QuickStartPage() { return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [query]) - // Close dropdown on outside click + // Close search dropdown on outside click useEffect(() => { function handleClick(e: MouseEvent) { if (searchRef.current && !searchRef.current.contains(e.target as Node)) { @@ -101,8 +183,7 @@ export function QuickStartPage() { return () => document.removeEventListener('mousedown', handleClick) }, []) - // Compute stats - const totalTrees = trees.length + // Stats const openSessions = activeSessions.length const todaySessions = allSessions.filter(s => { const d = new Date(s.started_at) @@ -111,14 +192,6 @@ export function QuickStartPage() { }).length const completedSessions = allSessions.filter(s => s.completed_at).length - // Filter trees - const filteredTrees = activeFilter === 'all' - ? trees - : activeFilter === 'recent' - ? trees.slice(0, 10) - : trees - - // Map sessions for SessionsPanel const recentSessionItems = allSessions.slice(0, 5).map(s => ({ id: s.id, treeName: s.tree_snapshot?.name || 'Unknown', @@ -127,12 +200,28 @@ export function QuickStartPage() { timeAgo: timeAgo(s.started_at), })) - const filters = [ - { id: 'all', label: 'All' }, - { id: 'recent', label: 'Recently Used' }, - { id: 'my', label: 'My Flows' }, - { id: 'team', label: 'Team Flows' }, - ] + // 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) => { + if (treeType === 'maintenance') { + navigate(`/flows/${treeId}/maintenance`) + } else if (treeType === 'procedural') { + navigate(`/flows/${treeId}/navigate`) + } else { + navigate(`/trees/${treeId}/navigate`) + } + } + + const handleDeleteTree = () => {} // Not used on dashboard + const handleTagClick = () => {} // Not used on dashboard + const handleFolderCreated = () => {} // Not used on dashboard + + // Page size options + const pageSizeOptions: (number | 'all')[] = [10, 25, 50, 'all'] return (
@@ -148,13 +237,75 @@ export function QuickStartPage() {
{canCreateTrees && ( - - - Create Flow - +
+ + {showCreateMenu && ( + <> +
setShowCreateMenu(false)} /> +
+ setShowCreateMenu(false)} + className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent" + > + +
+
Troubleshooting Tree
+
Branching decision flow
+
+ + setShowCreateMenu(false)} + className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent" + > + +
+
Procedural Flow
+
Step-by-step procedure
+
+ + setShowCreateMenu(false)} + className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent" + > + +
+
Maintenance Flow
+
Scheduled multi-target tasks
+
+ + {aiEnabled && ( + <> +
+ + + )} +
+ + )} +
)}
@@ -162,17 +313,14 @@ export function QuickStartPage() { {/* Quick Stats */} - {/* Filters */} - - - {/* Search (inline, not hero) */} + {/* Search */}
- {/* Tree/Flow List */} - {isLoading ? ( -
- -
- ) : ( - - {filteredTrees.slice(0, 20).map(tree => ( - - ))} - {filteredTrees.length > 20 && ( - +
+

+ 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 */} +
+
+

My Flows

+
+ +
+
+ + {isLoadingFlows ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ ) : myFlows.length === 0 ? ( +
+

You haven't created any flows yet.

+ {canCreateTrees && ( + + )} +
+ ) : ( + <> + {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: + +
+
+ )} + + )} +
+ + {/* AI Builder Modal */} + {showAIBuilder && ( + setShowAIBuilder(false)} + /> )}
)