diff --git a/frontend/src/components/DirectoryPicker.tsx b/frontend/src/components/DirectoryPicker.tsx index 3ea2ff7..2b37f98 100644 --- a/frontend/src/components/DirectoryPicker.tsx +++ b/frontend/src/components/DirectoryPicker.tsx @@ -1,20 +1,21 @@ /** * DirectoryPicker -- monorepo package selection before indexing. * - * Shows an interactive card grid where each package is a clickable card - * sized proportionally to its file count. Users select which packages - * to index instead of the entire repo. + * Shows a clean vertical list where each package is a row with + * checkbox, name, file count, and function estimate. Users select + * which packages to index instead of the entire repo. */ import { useState, useMemo } from 'react' import { motion, AnimatePresence } from 'framer-motion' -import { FolderGit2, X, Files, FunctionSquare } from 'lucide-react' +import { FolderGit2, X, Files, FunctionSquare, ArrowUpDown } from 'lucide-react' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' -import { ScrollArea } from '@/components/ui/scroll-area' import { cn } from '@/lib/utils' import type { AnalyzeResult, DirectoryEntry } from '@/types' +type SortKey = 'name' | 'files' | 'functions' + interface DirectoryPickerProps { isOpen: boolean onClose: () => void @@ -33,11 +34,25 @@ export function DirectoryPicker({ functionLimit, }: DirectoryPickerProps) { const [selected, setSelected] = useState>(new Set()) + const [sortBy, setSortBy] = useState('files') + const [sortAsc, setSortAsc] = useState(false) - const maxFiles = useMemo( - () => Math.max(...repoInfo.directories.map((d) => d.file_count), 1), - [repoInfo.directories], - ) + const sortedDirs = useMemo(() => { + const dirs = [...repoInfo.directories] + dirs.sort((a, b) => { + let cmp = 0 + if (sortBy === 'name') cmp = a.name.localeCompare(b.name) + else if (sortBy === 'files') cmp = a.file_count - b.file_count + else cmp = a.estimated_functions - b.estimated_functions + return sortAsc ? cmp : -cmp + }) + return dirs + }, [repoInfo.directories, sortBy, sortAsc]) + + function toggleSort(key: SortKey) { + if (sortBy === key) setSortAsc((prev) => !prev) + else { setSortBy(key); setSortAsc(key === 'name') } + } const stats = useMemo(() => { const dirs = repoInfo.directories.filter((d) => selected.has(d.path)) @@ -90,11 +105,8 @@ export function DirectoryPicker({ loading={loading} /> -
-

- Select the packages you need for faster indexing and more focused results. -

-
+
+
+ + {repoInfo.directories.length} packages + +
+ +
+ + + +
- +
- {repoInfo.directories.map((dir) => ( + {sortedDirs.map((dir) => ( - toggleDir(dir.path)} /> ))} - +
{functionLimit && ( @@ -164,7 +185,7 @@ function PickerHeader({ loading: boolean }) { return ( -
+
@@ -198,38 +219,80 @@ function PickerHeader({ } -function PackageCard({ +function SortButton({ + label, + sortKey, + current, + asc, + onToggle, + className, +}: { + label: string + sortKey: SortKey + current: SortKey + asc: boolean + onToggle: (key: SortKey) => void + className?: string +}) { + const active = current === sortKey + return ( + + ) +} + + +function PackageRow({ dir, isSelected, - maxFiles, onToggle, }: { dir: DirectoryEntry isSelected: boolean - maxFiles: number onToggle: () => void }) { - // Scale card width: smallest = 120px, largest = 240px - const scale = dir.file_count / maxFiles - const minWidth = Math.round(120 + scale * 120) - return ( - +
+ + {dir.name} + + + {dir.file_count.toLocaleString()} files + + + ~{dir.estimated_functions.toLocaleString()} fn + +
) } diff --git a/frontend/src/components/dashboard/DashboardHome.tsx b/frontend/src/components/dashboard/DashboardHome.tsx index 4fdf346..1f20356 100644 --- a/frontend/src/components/dashboard/DashboardHome.tsx +++ b/frontend/src/components/dashboard/DashboardHome.tsx @@ -6,7 +6,7 @@ import { useSearchParams } from 'react-router-dom' import { AnimatePresence } from 'framer-motion' import { toast } from 'sonner' import { useAuth } from '../../contexts/AuthContext' -import { useRepos } from '../../hooks/useCachedQuery' +import { useRepos, useUserUsage } from '../../hooks/useCachedQuery' import { API_URL, MAX_FREE_REPOS } from '../../config/api' import { extractErrorMessage, isUpgradeError } from '../../lib/api-errors' import { RepoListView } from './RepoListView' @@ -16,7 +16,6 @@ import { DirectoryPicker } from '../DirectoryPicker' import { GitHubRepoSelector } from '../GitHubRepoSelector' import { IndexingProgressModal } from '../IndexingProgressModal' import { UpgradeLimitModal } from '../UpgradeLimitModal' -import { TIER_FUNCTION_LIMITS, type TierName } from '../../config/api' import type { GitHubRepo } from '../../hooks/useGitHubRepos' import type { AnalyzeResult, RepoTab } from '../../types' @@ -24,10 +23,7 @@ export function DashboardHome() { const { session } = useAuth() const [searchParams, setSearchParams] = useSearchParams() const { data: repos = [], isLoading: reposLoading, invalidate: refreshRepos } = useRepos(session?.access_token) - - // User tier -- validate against known tiers, fall back to free for unknown values - const rawTier = session?.user?.user_metadata?.tier as string - const userTier: TierName = rawTier && rawTier in TIER_FUNCTION_LIMITS ? (rawTier as TierName) : 'free' + const { data: usage } = useUserUsage(session?.access_token, session?.user?.id) const [selectedRepo, setSelectedRepo] = useState(null) const [activeTab, setActiveTab] = useState('overview') @@ -260,8 +256,7 @@ export function DashboardHome() { repoInfo={analyzeResult} onConfirm={handleDirectoryConfirm} loading={loading} - // TODO: replace with actual user tier once GET /users/me returns tier - functionLimit={TIER_FUNCTION_LIMITS[userTier]} + functionLimit={usage?.limits?.max_functions_per_repo} /> )} diff --git a/frontend/src/hooks/useCachedQuery.ts b/frontend/src/hooks/useCachedQuery.ts index a694ed1..22555be 100644 --- a/frontend/src/hooks/useCachedQuery.ts +++ b/frontend/src/hooks/useCachedQuery.ts @@ -189,3 +189,24 @@ export function useRepos(apiKey: string | undefined) { return { ...query, invalidate } } + + +/** User usage and tier limits from backend -- single source of truth */ +export function useUserUsage(apiKey: string | undefined, userId?: string) { + return useQuery({ + queryKey: ['user', 'usage', userId], + queryFn: async () => { + const data = await fetchWithAuth(`${API_URL}/users/usage`, apiKey!) + return data as { + tier: string + limits: { + max_files_per_repo: number + max_functions_per_repo: number + playground_searches_per_day: number | null + } + } + }, + enabled: !!apiKey, + staleTime: 60_000, + }) +}