diff --git a/CLAUDE.md b/CLAUDE.md index 03c5b27..e98c5ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,22 +3,23 @@ ## Code Style ### General -- NO emojis anywhere - not in code, comments, docs, or commit messages +- NO emojis anywhere -- not in code, comments, docs, or commit messages - Prefer files under 200 lines. Larger files allowed when logically cohesive. - Comments explain WHY, not WHAT. Keep them brief. - No decorative headers or ASCII art -- Casual tone in comments - write like a human, not a robot +- Casual tone in comments -- write like a human, not a robot ### JSDoc Policy - JSDoc allowed for public API functions and exported hooks -- Keep JSDoc minimal - document params and return types, not obvious behavior -- Approved files using JSDoc: `frontend/src/services/playground-api.ts`, `frontend/src/config/api.ts`, `frontend/src/hooks/useViewTransition.ts` +- Keep JSDoc minimal -- document params and return types, not obvious behavior ### Large File Exceptions These files exceed 200 lines but are approved due to logical cohesion: - `backend/routes/playground.py` - `backend/services/dna_extractor.py` -- `frontend/src/components/DependencyGraph/index.tsx` +- `backend/services/dependency_analyzer.py` +- `frontend/src/components/DependencyGraph/GraphView/index.tsx` +- `frontend/src/components/DependencyGraph/MatrixView/index.tsx` ### Frontend (TypeScript/React) - Package manager: **Bun only**. Never npm, never yarn. @@ -27,6 +28,26 @@ These files exceed 200 lines but are approved due to logical cohesion: - Use shadcn/ui components over custom UI - Tailwind for styling, no CSS files - Functional components with hooks, no class components + (ErrorBoundary is the only exception -- React requires class for error boundaries) +- Use React Query (`useQuery`) for all server data fetching, never raw fetch in useEffect +- Custom hooks go in `frontend/src/hooks/` with `use` prefix +- Shared types go in `frontend/src/types.ts` +- Keep components focused -- one concern per file + +### React Patterns +- Derive state during render when possible, don't sync with useEffect +- Define components at module scope, never inside other components (causes remounts) +- Use stable keys (item.id), not array indices, for lists that change +- Colocate state with the component that uses it, lift only when needed +- Use `cn()` from `@/lib/utils` for conditional class merging + +### Testing +- Vitest + happy-dom for unit/component tests +- Tests live in `frontend/src/test/` using `@/` alias imports +- Run with `bun run test`, runs in CI on every PR +- Test file naming: `ComponentName.test.tsx` or `hookName.test.ts` +- Smoke tests for critical paths: does it render, does the hook return correct data +- Backend tests: pytest in `backend/tests/`, run with `cd backend && pytest tests/ -v` ### Backend (Python) - Python 3.11+ required @@ -34,35 +55,81 @@ These files exceed 200 lines but are approved due to logical cohesion: - Async/await for I/O operations - PEP 8 style, max 120 char lines - Use existing patterns from `services/` directory +- All new services should follow the singleton pattern used in `dependency_analyzer.py` +- Startup validation in `config/startup_checks.py` -- add new required/optional env vars there ### Commits - Format: `type: description` (e.g., `fix: remove broken link`) - Types: feat, fix, docs, refactor, test, chore - No emojis in commit messages -- Keep commits focused - one change per commit +- Keep commits focused -- one change per commit +- PR scope: one concern per PR. Don't mix bug fixes with features. + +### Code Review +- All PRs go through CodeRabbit automated review +- Fix real findings (bugs, type safety, missing deps). Skip nitpicks unless trivial. +- PRs target `OpenCodeIntel/opencodeintel:main` from your fork ## Architecture ### Project Structure -```plaintext -backend/ # FastAPI, Python 3.11+ -frontend/ # React 18, TypeScript, Vite, Bun -mcp-server/ # MCP protocol server +```text +backend/ # FastAPI, Python 3.11+ + config/ # API config, startup checks + middleware/ # Auth, rate limiting + routes/ # API endpoints (all use /api/v1/ prefix) + services/ # Business logic (singleton services) + tests/ # pytest test files +frontend/ # React 18, TypeScript, Vite, Bun + src/ + components/ # UI components (one concern per file) + dashboard/ # Sidebar, TopNav, DashboardHome, RepoListView, RepoDetailView + DependencyGraph/ # GraphView (Sigma.js), MatrixView (DSM), ImpactPanel + ui/ # shadcn/ui primitives + landing/ # Marketing pages + docs/ # Documentation components + hooks/ # Custom React hooks (useCachedQuery, useRepos, etc.) + contexts/ # React contexts (AuthContext) + config/ # API URL config, demo repos + pages/ # Route-level page components + test/ # Vitest test files + types.ts # Shared TypeScript interfaces +mcp-server/ # MCP protocol server for Claude/Cursor ``` ### API Versioning - All endpoints use `/api/v1/` prefix - Version config in `backend/config/api.py` +### Data Flow +- Frontend fetches via React Query hooks in `hooks/useCachedQuery.ts` +- Dual-layer caching: React Query (memory) + localStorage (persistence) +- Backend caches dependency graphs and DNA in Supabase +- Real-time indexing progress via WebSocket + ### Key Services -- `indexer_optimized.py` - Code parsing and embedding -- `dependency_analyzer.py` - Import graph extraction -- `style_analyzer.py` - Convention detection -- `dna_extractor.py` - Architectural pattern extraction +- `indexer_optimized.py` -- Code parsing and embedding (tree-sitter + OpenAI/Voyage) +- `dependency_analyzer.py` -- Import graph extraction (tree-sitter AST) +- `style_analyzer.py` -- Convention detection +- `dna_extractor.py` -- Architectural pattern extraction + team rules detection +- `supabase_service.py` -- Database operations (uses service role key) +- `auth.py` -- JWT verification (uses anon key + JWT secret) + +### Environment Variables +- Root `.env.example` is the source of truth for all env vars +- Docker passes vars via `docker-compose.yml` environment block +- Frontend vars must be prefixed with `VITE_` +- New required vars: add to `config/startup_checks.py` REQUIRED_VARS +- New optional vars: add to `config/startup_checks.py` OPTIONAL_VARS ## What NOT to Do -- Don't use npm (use Bun) +- Don't use npm or yarn (use Bun) - Don't add emojis - Don't write verbose comments - Don't add "AI-looking" badges or decorations +- Don't define components inside other components +- Don't use raw fetch in useEffect (use React Query) +- Don't put inline types when a shared interface exists in types.ts +- Don't create PRs with multiple unrelated concerns +- Don't merge PRs without CI passing diff --git a/frontend/src/components/dashboard/DashboardHome.tsx b/frontend/src/components/dashboard/DashboardHome.tsx index 3b0346d..15c1a1d 100644 --- a/frontend/src/components/dashboard/DashboardHome.tsx +++ b/frontend/src/components/dashboard/DashboardHome.tsx @@ -1,158 +1,105 @@ +// DashboardHome -- orchestrates repo list and detail views +// State management and API handlers live here, rendering delegated to children + import { useState, useEffect } from 'react' import { useSearchParams } from 'react-router-dom' -import { motion, AnimatePresence } from 'framer-motion' +import { AnimatePresence } from 'framer-motion' import { toast } from 'sonner' -import { - LayoutDashboard, - Search, - GitFork, - Code2, - Zap, - ArrowLeft, - FolderGit2, - ExternalLink, - Plus, - Github -} from 'lucide-react' import { useAuth } from '../../contexts/AuthContext' -import { Button } from '../ui/button' -import { RepoList } from '../RepoList' +import { useRepos } from '../../hooks/useCachedQuery' +import { API_URL, MAX_FREE_REPOS } from '../../config/api' +import { extractErrorMessage, isUpgradeError } from '../../lib/api-errors' +import { RepoListView } from './RepoListView' +import { RepoDetailView } from './RepoDetailView' import { AddRepoForm } from '../AddRepoForm' import { GitHubRepoSelector } from '../GitHubRepoSelector' -import { SearchPanel } from '../SearchPanel' -import { DependencyGraph } from '../DependencyGraph' -import { RepoOverview } from '../RepoOverview' -import { StyleInsights } from '../StyleInsights' -import { ImpactAnalyzer } from '../ImpactAnalyzer' -import { DashboardStats } from './DashboardStats' import { IndexingProgressModal } from '../IndexingProgressModal' import { UpgradeLimitModal } from '../UpgradeLimitModal' -import type { Repository } from '../../types' import type { GitHubRepo } from '../../hooks/useGitHubRepos' -import { API_URL } from '../../config/api' -import { useRepos } from '../../hooks/useCachedQuery' - -const MAX_FREE_REPOS = 3 - -// Safe stringify that won't crash on circular refs -function safeStringify(obj: unknown, maxLen = 200): string { - try { - return JSON.stringify(obj).slice(0, maxLen) - } catch { - return String(obj).slice(0, maxLen) - } -} - -// Extract error message from API response (handles nested detail objects) -function extractErrorMessage(err: any, fallback: string): string { - // FastAPI wraps in detail, but handle both cases - const detail = err?.detail || err - - if (typeof detail === 'string') return detail - if (typeof detail?.message === 'string') return detail.message - if (typeof err?.message === 'string') return err.message - - // Last resort: stringify (but keep it short, safe from circular refs) - if (detail && typeof detail === 'object') { - const msg = detail.message || detail.error - if (msg) return String(msg) - return safeStringify(detail) - } - return fallback -} - -// Check if error is a limit/upgrade error (handles both wrapped and unwrapped) -function isUpgradeError(err: any): boolean { - const detail = err?.detail || err - const code = detail?.error || detail?.error_code - return ['REPO_TOO_LARGE', 'REPO_LIMIT_REACHED'].includes(code) -} - -type RepoTab = 'overview' | 'search' | 'dependencies' | 'insights' | 'impact' +import type { RepoTab } from '../../types' export function DashboardHome() { const { session } = useAuth() const [searchParams, setSearchParams] = useSearchParams() const { data: repos = [], isLoading: reposLoading, invalidate: refreshRepos } = useRepos(session?.access_token) + const [selectedRepo, setSelectedRepo] = useState(null) const [activeTab, setActiveTab] = useState('overview') const [loading, setLoading] = useState(false) const [showAddForm, setShowAddForm] = useState(false) const [showGitHubSelector, setShowGitHubSelector] = useState(false) - - // Indexing progress modal state + + // indexing modal const [indexingRepoId, setIndexingRepoId] = useState(null) - const [indexingRepoName, setIndexingRepoName] = useState('') + const [indexingRepoName, setIndexingRepoName] = useState('') const [showIndexingModal, setShowIndexingModal] = useState(false) - - // Upgrade prompt modal state - const [upgradeModal, setUpgradeModal] = useState<{ show: boolean; message: string; repoName?: string }>({ show: false, message: '' }) - // Helper to show upgrade modal with context - const showUpgradeModal = (err: any, repoName?: string) => { - setUpgradeModal({ show: true, message: extractErrorMessage(err, 'Repository exceeds free tier limits'), repoName }) - } + // upgrade modal + const [upgradeModal, setUpgradeModal] = useState<{ show: boolean; message: string; repoName?: string }>({ + show: false, message: '', + }) - // Auto-open GitHub import modal if redirected from OAuth callback + // auto-open GitHub import if redirected from OAuth useEffect(() => { if (searchParams.get('openGitHubImport') === 'true') { setShowGitHubSelector(true) - // Clear the param from URL without triggering navigation - searchParams.delete('openGitHubImport') - setSearchParams(searchParams, { replace: true }) + const cleaned = new URLSearchParams(searchParams) + cleaned.delete('openGitHubImport') + setSearchParams(cleaned, { replace: true }) } }, [searchParams, setSearchParams]) + const showUpgradeModal = (err: any, repoName?: string) => { + setUpgradeModal({ show: true, message: extractErrorMessage(err, 'Repository exceeds free tier limits'), repoName }) + } + + // shared helper: add a repo and start indexing + const addAndIndex = async (name: string, gitUrl: string, branch: string): Promise => { + const response = await fetch(`${API_URL}/repos`, { + method: 'POST', + headers: { Authorization: `Bearer ${session?.access_token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, git_url: gitUrl, branch }), + }) + + if (!response.ok) { + const err = await response.json().catch(() => ({})) + if (isUpgradeError(err)) { showUpgradeModal(err, name); return null } + throw new Error(extractErrorMessage(err, 'Failed to add repository')) + } + + const data = await response.json() + if (!data.repo_id) throw new Error('Missing repo_id in response') + + const indexResponse = await fetch(`${API_URL}/repos/${data.repo_id}/index/async`, { + method: 'POST', + headers: { Authorization: `Bearer ${session?.access_token}` }, + }) + + if (!indexResponse.ok) { + const err = await indexResponse.json().catch(() => ({})) + if (isUpgradeError(err)) { showUpgradeModal(err, name); return null } + throw new Error(extractErrorMessage(err, 'Failed to start indexing')) + } + + return data.repo_id + } + const handleAddRepo = async (gitUrl: string, branch: string) => { try { setLoading(true) - const name = gitUrl.split('/').pop()?.replace('.git', '') || 'unknown' - const response = await fetch(`${API_URL}/repos`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${session?.access_token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ name, git_url: gitUrl, branch }) - }) - - if (!response.ok) { - const err = await response.json().catch(() => ({})) - if (isUpgradeError(err)) { - showUpgradeModal(err, name) - return - } - throw new Error(extractErrorMessage(err, 'Failed to add repository')) - } - - const data = await response.json() - if (!data.repo_id) throw new Error('Missing repo_id in response') - - // Trigger async indexing - const indexResponse = await fetch(`${API_URL}/repos/${data.repo_id}/index/async`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${session?.access_token}` } - }) - - if (!indexResponse.ok) { - const err = await indexResponse.json().catch(() => ({})) - if (isUpgradeError(err)) { - showUpgradeModal(err, name) - return - } - throw new Error(extractErrorMessage(err, 'Failed to start indexing')) - } - - // Show indexing progress modal - setIndexingRepoId(data.repo_id) - setIndexingRepoName(name) - setShowIndexingModal(true) + const name = gitUrl.split('/').pop()?.replace(/\.git$/, '') || 'unknown' + const repoId = await addAndIndex(name, gitUrl, branch) setShowAddForm(false) - + if (repoId) { + setIndexingRepoId(repoId) + setIndexingRepoName(name) + setShowIndexingModal(true) + } refreshRepos() } catch (error) { - console.error('Error adding repo:', error) - toast.error('Failed to add repository', { description: error instanceof Error ? error.message : 'Please check the Git URL and try again' }) + toast.error('Failed to add repository', { + description: error instanceof Error ? error.message : 'Please check the Git URL and try again', + }) } finally { setLoading(false) } @@ -160,100 +107,37 @@ export function DashboardHome() { const handleGitHubImport = async (githubRepos: GitHubRepo[]) => { if (githubRepos.length === 0) return - - // Import repos one at a time - for (const repo of githubRepos) { - try { - setLoading(true) - const response = await fetch(`${API_URL}/repos`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${session?.access_token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - name: repo.name, - git_url: repo.clone_url, - branch: repo.default_branch - }) - }) - - if (!response.ok) { - const err = await response.json().catch(() => ({})) - if (isUpgradeError(err)) { - showUpgradeModal(err, repo.name) - continue - } - throw new Error(extractErrorMessage(err, `Failed to add ${repo.name}`)) - } - - const data = await response.json() - if (!data.repo_id) throw new Error('Missing repo_id in response') - - // Trigger async indexing - const indexResponse = await fetch(`${API_URL}/repos/${data.repo_id}/index/async`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${session?.access_token}` } - }) - - if (!indexResponse.ok) { - const err = await indexResponse.json().catch(() => ({})) - if (isUpgradeError(err)) { - showUpgradeModal(err, repo.name) - continue + + let lastSuccessId: string | null = null + let lastSuccessName = '' + + try { + setLoading(true) + + for (const repo of githubRepos) { + try { + const repoId = await addAndIndex(repo.name, repo.clone_url, repo.default_branch) + + if (repoId) { + lastSuccessId = repoId + lastSuccessName = repo.name + toast.success(`Added ${repo.name}`) } - const errMsg = extractErrorMessage(err, 'Indexing failed to start') - console.error(`Failed to start indexing for ${repo.name}:`, err) - toast.warning(`${repo.name} added but indexing failed`, { - description: errMsg + } catch (error) { + toast.error(`Failed to import ${repo.name}`, { + description: error instanceof Error ? error.message : 'Please try again', }) - } else if (repo === githubRepos[githubRepos.length - 1]) { - // Only show progress modal for last repo if indexing started successfully - setIndexingRepoId(data.repo_id) - setIndexingRepoName(repo.name) - setShowIndexingModal(true) } - - toast.success(`Added ${repo.name}`) - } catch (error) { - console.error(`Error importing ${repo.name}:`, error) - toast.error(`Failed to import ${repo.name}`, { - description: error instanceof Error ? error.message : 'Please try again' - }) } - } - - setLoading(false) - refreshRepos() - } - - const handleIndexingComplete = async () => { - refreshRepos() - toast.success('Indexing complete!', { description: `${indexingRepoName} is ready for search` }) - } - - const handleCloseIndexingModal = () => { - setShowIndexingModal(false) - setIndexingRepoId(null) - setIndexingRepoName('') - } - const handleRetryIndexing = async () => { - if (!indexingRepoId) return - - try { - const response = await fetch(`${API_URL}/repos/${indexingRepoId}/index/async`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${session?.access_token}` } - }) - - if (!response.ok) { - throw new Error('Failed to restart indexing') + if (lastSuccessId) { + setIndexingRepoId(lastSuccessId) + setIndexingRepoName(lastSuccessName) + setShowIndexingModal(true) } - - // Modal will reconnect via WebSocket - } catch (error) { - toast.error('Failed to retry indexing') + } finally { + setLoading(false) + refreshRepos() } } @@ -263,212 +147,73 @@ export function DashboardHome() { setLoading(true) const response = await fetch(`${API_URL}/repos/${selectedRepo}/index/async`, { method: 'POST', - headers: { 'Authorization': `Bearer ${session?.access_token}` } + headers: { Authorization: `Bearer ${session?.access_token}` }, }) - - if (!response.ok) { - throw new Error('Failed to start re-indexing') - } - - // Show indexing progress modal + if (!response.ok) throw new Error('Failed to start re-indexing') setIndexingRepoId(selectedRepo) setIndexingRepoName(selectedRepoData.name) setShowIndexingModal(true) - } catch (error) { + } catch { toast.error('Re-indexing failed', { description: 'Please check the console for details' }) } finally { setLoading(false) } } - const selectedRepoData = repos.find(r => r.id === selectedRepo) + const selectedRepoData = repos.find((r) => r.id === selectedRepo) const isRepoView = selectedRepo && selectedRepoData - const tabs = [ - { id: 'overview', label: 'Overview', icon: LayoutDashboard }, - { id: 'search', label: 'Search', icon: Search }, - { id: 'dependencies', label: 'Dependencies', icon: GitFork }, - { id: 'insights', label: 'Code Style', icon: Code2 }, - { id: 'impact', label: 'Impact', icon: Zap }, - ] as const - return (
- {/* Repository List View */} - {!isRepoView && ( - - {/* Header */} -
-
-

Repositories

-

- Semantic code search powered by AI -

-
-
- - -
- -
- - {/* Stats */} - {(reposLoading || repos.length > 0) && } - - {/* Repo Grid */} - { - setSelectedRepo(id) - setActiveTab('overview') - }} - onAddClick={() => setShowAddForm(true)} - /> -
+ repos={repos} + loading={loading} + reposLoading={reposLoading} + selectedRepo={selectedRepo} + onSelectRepo={(id) => { setSelectedRepo(id); setActiveTab('overview') }} + onAddClick={() => setShowAddForm(true)} + onGitHubClick={() => setShowGitHubSelector(true)} + /> + ) : ( + { setSelectedRepo(null); setActiveTab('overview') }} + onReindex={handleReindex} + /> )} +
- {/* Single Repo View */} - {isRepoView && ( - - {/* Header */} -
- - -
-
-
- -
-
-

{selectedRepoData.name}

- - {selectedRepoData.git_url} - - -
-
-
-
- - {/* Tabs */} -
- {tabs.map(tab => ( - - ))} -
+ - {/* Tab Content */} -
- {activeTab === 'overview' && ( - setActiveTab(tab as RepoTab)} - /> - )} - {activeTab === 'search' && ( - - )} - {activeTab === 'dependencies' && ( - - )} - {activeTab === 'insights' && ( - - )} - {activeTab === 'impact' && ( - - )} -
-
- )} - - - {/* Indexing Progress Modal */} { setShowIndexingModal(false); setIndexingRepoId(null); setIndexingRepoName('') }} + onCompleted={() => { refreshRepos(); toast.success('Indexing complete!', { description: `${indexingRepoName} is ready for search` }) }} + onRetry={async () => { + if (!indexingRepoId) return + try { + await fetch(`${API_URL}/repos/${indexingRepoId}/index/async`, { + method: 'POST', headers: { Authorization: `Bearer ${session?.access_token}` }, + }) + } catch { toast.error('Failed to retry indexing') } + }} /> - - {/* GitHub Repo Selector */} + setShowGitHubSelector(false)} @@ -476,8 +221,7 @@ export function DashboardHome() { maxSelectable={MAX_FREE_REPOS} currentRepoCount={repos.length} /> - - {/* Upgrade Limit Modal */} + setUpgradeModal({ show: false, message: '' })} diff --git a/frontend/src/components/dashboard/RepoDetailView.tsx b/frontend/src/components/dashboard/RepoDetailView.tsx new file mode 100644 index 0000000..dcc88d2 --- /dev/null +++ b/frontend/src/components/dashboard/RepoDetailView.tsx @@ -0,0 +1,138 @@ +// Single repo detail view with tabs (Overview, Search, Dependencies, etc.) +// Receives repo data and callbacks from DashboardHome + +import { motion } from 'framer-motion' +import { + LayoutDashboard, + Search, + GitFork, + Code2, + Zap, + ArrowLeft, + FolderGit2, + ExternalLink, +} from 'lucide-react' +import { SearchPanel } from '../SearchPanel' +import { DependencyGraph } from '../DependencyGraph' +import { RepoOverview } from '../RepoOverview' +import { StyleInsights } from '../StyleInsights' +import { ImpactAnalyzer } from '../ImpactAnalyzer' +import type { Repository, RepoTab } from '../../types' +import { API_URL } from '../../config/api' + +const TABS = [ + { id: 'overview', label: 'Overview', icon: LayoutDashboard }, + { id: 'search', label: 'Search', icon: Search }, + { id: 'dependencies', label: 'Dependencies', icon: GitFork }, + { id: 'insights', label: 'Code Style', icon: Code2 }, + { id: 'impact', label: 'Impact', icon: Zap }, +] as const + +interface RepoDetailViewProps { + repo: Repository + repoId: string + activeTab: RepoTab + apiKey: string + onTabChange: (tab: RepoTab) => void + onBack: () => void + onReindex: () => void +} + +export function RepoDetailView({ + repo, + repoId, + activeTab, + apiKey, + onTabChange, + onBack, + onReindex, +}: RepoDetailViewProps) { + return ( + + {/* back nav + repo header */} +
+ + +
+
+
+ +
+
+

{repo.name}

+ + {repo.git_url} + + +
+
+
+
+ + {/* tab bar */} +
+ {TABS.map((tab) => ( + + ))} +
+ + {/* tab content */} +
+ {activeTab === 'overview' && ( + onTabChange(tab as RepoTab)} + /> + )} + {activeTab === 'search' && ( + + )} + {activeTab === 'dependencies' && ( + + )} + {activeTab === 'insights' && ( + + )} + {activeTab === 'impact' && ( + + )} +
+
+ ) +} diff --git a/frontend/src/components/dashboard/RepoListView.tsx b/frontend/src/components/dashboard/RepoListView.tsx new file mode 100644 index 0000000..8c345f7 --- /dev/null +++ b/frontend/src/components/dashboard/RepoListView.tsx @@ -0,0 +1,80 @@ +// Repo list header (title, buttons) and grid +// Shown when no repo is selected + +import { motion } from 'framer-motion' +import { Plus, Github } from 'lucide-react' +import { Button } from '../ui/button' +import { RepoList } from '../RepoList' +import { DashboardStats } from './DashboardStats' +import { MAX_FREE_REPOS } from '../../config/api' +import type { Repository } from '../../types' + +interface RepoListViewProps { + repos: Repository[] + loading: boolean + reposLoading: boolean + selectedRepo: string | null + onSelectRepo: (id: string) => void + onAddClick: () => void + onGitHubClick: () => void +} + +export function RepoListView({ + repos, + loading, + reposLoading, + selectedRepo, + onSelectRepo, + onAddClick, + onGitHubClick, +}: RepoListViewProps) { + return ( + +
+
+

Repositories

+

+ Semantic code search powered by AI +

+
+
+ + +
+
+ + {(reposLoading || repos.length > 0) && ( + + )} + + +
+ ) +} diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts index b52f347..2fcaae0 100644 --- a/frontend/src/config/api.ts +++ b/frontend/src/config/api.ts @@ -57,3 +57,6 @@ export const buildWsUrl = (path: string): string => { const cleanPath = path.startsWith('/') ? path : `/${path}` return `${WS_URL}${cleanPath}` } + +// free tier repo limit -- used in dashboard and GitHub import +export const MAX_FREE_REPOS = 3 diff --git a/frontend/src/lib/api-errors.ts b/frontend/src/lib/api-errors.ts new file mode 100644 index 0000000..deb70c6 --- /dev/null +++ b/frontend/src/lib/api-errors.ts @@ -0,0 +1,32 @@ +// API error handling utilities +// Safely extract messages from nested FastAPI error responses + +export function safeStringify(obj: unknown, maxLen = 200): string { + try { + return JSON.stringify(obj).slice(0, maxLen) + } catch { + return String(obj).slice(0, maxLen) + } +} + +export function extractErrorMessage(err: any, fallback: string): string { + const detail = err?.detail || err + + if (typeof detail === 'string') return detail + if (typeof detail?.message === 'string') return detail.message + if (typeof err?.message === 'string') return err.message + + if (detail && typeof detail === 'object') { + const msg = detail.message || detail.error + if (msg) return String(msg) + return safeStringify(detail) + } + return fallback +} + +// checks for repo limit or size errors from the backend +export function isUpgradeError(err: any): boolean { + const detail = err?.detail || err + const code = detail?.error || detail?.error_code + return ['REPO_TOO_LARGE', 'REPO_LIMIT_REACHED'].includes(code) +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index ea71757..7f6de78 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -17,3 +17,5 @@ export interface SearchResult { line_start: number line_end: number } + +export type RepoTab = 'overview' | 'search' | 'dependencies' | 'insights' | 'impact'