From 2d587a1ad730f111980edef2098d4f39f80a9485 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Mon, 5 Jan 2026 16:33:53 -0500 Subject: [PATCH 1/2] feat(frontend): WebSocket real-time indexing progress (#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿš€ Features: - useIndexingWebSocket hook with auto-reconnect & polling fallback - Streaming file list - shows files appearing in real-time - Enhanced IndexingProgress with phase indicators & glow effects - IndexingComplete celebration screen with confetti & animated stats - Integrated WebSocket into HeroPlayground component ๐Ÿ“ Files: - NEW: hooks/useIndexingWebSocket.ts (WebSocket state machine) - NEW: components/playground/IndexingComplete.tsx (celebration UI) - UPDATED: components/playground/IndexingProgress.tsx (streaming file list) - UPDATED: components/playground/HeroPlayground.tsx (WS integration) - UPDATED: components/playground/index.ts (exports) ๐ŸŽจ UX Highlights: - Files stream in real-time as they're processed - Phase indicator: Clone โ†’ Index โ†’ Done - Glowing progress bar - Confetti on completion - Animated stat counters Connects to backend WebSocket endpoint from PR #150 --- .../components/playground/HeroPlayground.tsx | 306 +++++++++---- .../playground/IndexingComplete.tsx | 260 +++++++++++ .../playground/IndexingProgress.tsx | 368 ++++++++++++--- frontend/src/components/playground/index.ts | 4 +- frontend/src/hooks/useIndexingWebSocket.ts | 420 ++++++++++++++++++ 5 files changed, 1192 insertions(+), 166 deletions(-) create mode 100644 frontend/src/components/playground/IndexingComplete.tsx create mode 100644 frontend/src/hooks/useIndexingWebSocket.ts diff --git a/frontend/src/components/playground/HeroPlayground.tsx b/frontend/src/components/playground/HeroPlayground.tsx index 85e078f..279e614 100644 --- a/frontend/src/components/playground/HeroPlayground.tsx +++ b/frontend/src/components/playground/HeroPlayground.tsx @@ -1,21 +1,32 @@ /** - * HeroPlayground + * HeroPlayground (v2 - WebSocket Enhanced) * * Combined demo + custom repo experience for the landing page hero. - * Handles mode switching, URL validation, indexing, and search. + * Now with real-time WebSocket progress updates and streaming file list. + * + * Features: + * - Mode switching (demo/custom) + * - URL validation + * - Real-time indexing progress via WebSocket + * - Streaming file list (the "holy shit" moment) + * - Celebration screen on completion */ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; import { Button } from '@/components/ui/button'; import { RepoModeSelector, RepoUrlInput, ValidationStatus, IndexingProgress, - type RepoMode + IndexingComplete, + type RepoMode, + type IndexingPhase, } from '@/components/playground'; import { useAnonymousSession } from '@/hooks/useAnonymousSession'; +import { useIndexingWebSocket } from '@/hooks/useIndexingWebSocket'; import { cn } from '@/lib/utils'; // Demo repos config @@ -62,18 +73,46 @@ export function HeroPlayground({ const [selectedDemo, setSelectedDemo] = useState(DEMO_REPOS[0].id); const [customUrl, setCustomUrl] = useState(''); const [query, setQuery] = useState(''); + const [showCelebration, setShowCelebration] = useState(false); - // Anonymous session hook for custom repos - const { state, validateUrl, startIndexing, reset, session } = useAnonymousSession(); + // Anonymous session hook for validation and job creation + const { state, validateUrl, startIndexing, reset: resetSession, session } = useAnonymousSession(); + + // Extract jobId for WebSocket connection + const jobId = state.status === 'indexing' ? state.jobId : null; + const repoName = customUrl.split('/').pop()?.replace('.git', '') || 'repository'; + + // WebSocket hook for real-time progress + const wsState = useIndexingWebSocket(jobId, { + maxRecentFiles: 12, + onCompleted: (repoId, stats) => { + console.log('[HeroPlayground] Indexing completed:', repoId, stats); + setShowCelebration(true); + }, + onError: (error, recoverable) => { + console.error('[HeroPlayground] Indexing error:', error, recoverable); + }, + }); + + // Map WebSocket phase to IndexingProgress phase + const getIndexingPhase = (): IndexingPhase => { + if (wsState.phase === 'cloning') return 'cloning'; + if (wsState.phase === 'indexing') return 'indexing'; + if (wsState.phase === 'completed') return 'completed'; + if (wsState.phase === 'error') return 'error'; + return 'connecting'; + }; // Handle mode change const handleModeChange = useCallback((newMode: RepoMode) => { setMode(newMode); if (newMode === 'demo') { - reset(); + resetSession(); + wsState.reset(); setCustomUrl(''); + setShowCelebration(false); } - }, [reset]); + }, [resetSession, wsState]); // Handle search submit const handleSearch = useCallback((e?: React.FormEvent) => { @@ -84,8 +123,24 @@ export function HeroPlayground({ onSearch(query, selectedDemo, false); } else if (state.status === 'ready') { onSearch(query, state.repoId, true); + } else if (wsState.isCompleted && wsState.repoId) { + // Search using repo from completed WebSocket state + onSearch(query, wsState.repoId, true); } - }, [query, mode, selectedDemo, state, loading, onSearch]); + }, [query, mode, selectedDemo, state, wsState, loading, onSearch]); + + // Start searching after celebration + const handleStartSearching = useCallback(() => { + setShowCelebration(false); + }, []); + + // Index another repo + const handleIndexAnother = useCallback(() => { + resetSession(); + wsState.reset(); + setCustomUrl(''); + setShowCelebration(false); + }, [resetSession, wsState]); // Get validation state for ValidationStatus component const getValidationState = () => { @@ -96,44 +151,44 @@ export function HeroPlayground({ return { type: 'idle' as const }; }; + // Determine visibility states + const showDemoSelector = mode === 'demo'; + const showUrlInput = mode === 'custom' && !['indexing', 'ready'].includes(state.status) && !showCelebration && !wsState.isCompleted; + const showValidation = mode === 'custom' && ['validating', 'valid', 'invalid'].includes(state.status) && !showCelebration; + const showIndexing = mode === 'custom' && state.status === 'indexing' && !showCelebration && !wsState.isCompleted; + const showReady = (mode === 'custom' && state.status === 'ready') || (wsState.isCompleted && !showCelebration); + const isSearchDisabled = mode === 'custom' && state.status !== 'ready' && !wsState.isCompleted; + // Can search? const canSearch = mode === 'demo' ? remaining > 0 && query.trim().length > 0 - : state.status === 'ready' && remaining > 0 && query.trim().length > 0; - - // Determine what to show based on state - const showDemoSelector = mode === 'demo'; - const showUrlInput = mode === 'custom' && !['indexing', 'ready'].includes(state.status); - const showValidation = mode === 'custom' && ['validating', 'valid', 'invalid'].includes(state.status); - const showIndexing = mode === 'custom' && state.status === 'indexing'; - // Always show search - in custom mode it's disabled until repo is indexed - const showSearch = true; - const isSearchDisabled = mode === 'custom' && state.status !== 'ready'; + : (state.status === 'ready' || wsState.isCompleted) && remaining > 0 && query.trim().length > 0; // Get contextual placeholder text const getPlaceholder = () => { if (mode === 'demo') { return "Search for authentication, error handling..."; } - // Custom mode placeholders based on state - switch (state.status) { - case 'idle': - return "Enter a GitHub URL above to start..."; - case 'validating': - return "Validating repository..."; - case 'valid': - return "Click 'Index Repository' to continue..."; - case 'invalid': - return "Fix the URL above to continue..."; - case 'indexing': - return "Indexing in progress..."; - case 'ready': - return `Search in ${state.repoName}...`; - default: - return "Enter a GitHub URL to search..."; - } + if (state.status === 'idle') return "Enter a GitHub URL above to start..."; + if (state.status === 'validating') return "Validating repository..."; + if (state.status === 'valid') return "Click 'Index Repository' to continue..."; + if (state.status === 'invalid') return "Fix the URL above to continue..."; + if (state.status === 'indexing') return "Indexing in progress..."; + if (state.status === 'ready' || wsState.isCompleted) return `Search in ${repoName}...`; + return "Enter a GitHub URL to search..."; }; + // Compute ready state info + const readyInfo = wsState.isCompleted && wsState.completedStats ? { + repoName, + fileCount: wsState.completedStats.files_processed, + functionsFound: wsState.completedStats.functions_indexed, + } : state.status === 'ready' ? { + repoName: state.repoName, + fileCount: state.fileCount, + functionsFound: state.functionsFound, + } : null; + return (
{/* Mode Selector */} @@ -174,65 +229,114 @@ export function HeroPlayground({ )} {/* Custom URL Input */} - {showUrlInput && ( -
- -
- )} + + {showUrlInput && ( + + + + )} - {/* Validation Status */} - {showValidation && ( -
- -
- )} + {/* Validation Status */} + {showValidation && ( + + + + )} - {/* Indexing Progress */} - {showIndexing && ( -
- -
- )} + {/* Indexing Progress with WebSocket streaming */} + {showIndexing && ( + + { + resetSession(); + wsState.reset(); + }} + /> + + )} - {/* Ready State Banner */} - {mode === 'custom' && state.status === 'ready' && ( -
-
- โœ“ - - {state.repoName} indexed ยท {state.fileCount} files ยท {state.functionsFound} functions - -
- -
- )} + + + )} + + {/* Ready State Banner */} + {showReady && !showCelebration && readyInfo && ( + +
+ โœ“ + + {readyInfo.repoName} indexed ยท {readyInfo.fileCount} files ยท {readyInfo.functionsFound.toLocaleString()} functions + +
+ +
+ )} +
{/* Search Box */} - {showSearch && ( + {!showCelebration && ( <>
@@ -282,19 +386,23 @@ export function HeroPlayground({ )} {/* Error State */} - {state.status === 'error' && ( -
-

{state.message}

- {state.canRetry && ( - - )} -
+ {(state.status === 'error' || wsState.hasError) && ( + +

+ {state.status === 'error' ? state.message : wsState.error} +

+ +
)} {/* Upgrade CTA (when limit reached) */} @@ -312,3 +420,5 @@ export function HeroPlayground({
); } + +export default HeroPlayground; diff --git a/frontend/src/components/playground/IndexingComplete.tsx b/frontend/src/components/playground/IndexingComplete.tsx new file mode 100644 index 0000000..82c6ab4 --- /dev/null +++ b/frontend/src/components/playground/IndexingComplete.tsx @@ -0,0 +1,260 @@ +/** + * IndexingComplete + * + * Celebration screen shown when repository indexing finishes. + * Premium animations with confetti effect and stats showcase. + */ + +import { useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +interface IndexingCompleteProps { + repoName: string; + stats: { + files_processed: number; + functions_indexed: number; + indexing_time_seconds: number; + }; + onStartSearching: () => void; + onIndexAnother?: () => void; +} + +// Confetti particle component +function Confetti() { + const [particles] = useState(() => + Array.from({ length: 50 }, (_, i) => ({ + id: i, + x: Math.random() * 100, + delay: Math.random() * 0.5, + duration: 2 + Math.random() * 2, + color: ['#818cf8', '#34d399', '#fbbf24', '#f472b6', '#60a5fa'][Math.floor(Math.random() * 5)], + })) + ); + + return ( +
+ {particles.map((p) => ( + + ))} +
+ ); +} + +// Animated stat display +function StatDisplay({ + label, + value, + suffix = '', + delay = 0, + highlight = false +}: { + label: string; + value: number; + suffix?: string; + delay?: number; + highlight?: boolean; +}) { + const [displayValue, setDisplayValue] = useState(0); + + useEffect(() => { + const timeout = setTimeout(() => { + const duration = 1500; + const steps = 30; + const increment = value / steps; + let current = 0; + + const interval = setInterval(() => { + current += increment; + if (current >= value) { + setDisplayValue(value); + clearInterval(interval); + } else { + setDisplayValue(Math.floor(current)); + } + }, duration / steps); + + return () => clearInterval(interval); + }, delay); + + return () => clearTimeout(timeout); + }, [value, delay]); + + return ( + +
+ {displayValue.toLocaleString()}{suffix} +
+
{label}
+
+ ); +} + +export function IndexingComplete({ + repoName, + stats, + onStartSearching, + onIndexAnother +}: IndexingCompleteProps) { + const [showConfetti, setShowConfetti] = useState(true); + + useEffect(() => { + const timeout = setTimeout(() => setShowConfetti(false), 4000); + return () => clearTimeout(timeout); + }, []); + + return ( + + {/* Confetti */} + {showConfetti && } + + {/* Success glow */} + + + {/* Content */} +
+ {/* Success icon */} + + + โœ“ + + + + {/* Title */} + +

+ Ready to Search! +

+

+ {repoName} has been indexed +

+
+ + {/* Stats */} +
+ + + +
+ + {/* Actions */} +
+ + + {onIndexAnother && ( + + )} +
+ + {/* Pro tip */} + + ๐Ÿ’ก Pro tip: Try searching for "authentication", "error handling", or "database" + +
+
+ ); +} + +export default IndexingComplete; diff --git a/frontend/src/components/playground/IndexingProgress.tsx b/frontend/src/components/playground/IndexingProgress.tsx index 05e7e3d..deb2eb7 100644 --- a/frontend/src/components/playground/IndexingProgress.tsx +++ b/frontend/src/components/playground/IndexingProgress.tsx @@ -1,13 +1,26 @@ /** - * IndexingProgress + * IndexingProgress (v2 - WebSocket Enhanced) * - * Displays real-time progress during repository indexing. - * Shows progress bar, file stats, and current file being processed. + * Displays real-time progress during repository indexing with + * streaming file list that shows each file as it's processed. + * + * Features: + * - Phase indicators (cloning โ†’ indexing โ†’ completed) + * - Streaming file list with staggered animations + * - Progress bar with gradient glow + * - Real-time stats + * - Premium animations */ +import { useState, useEffect, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '@/lib/utils'; import { Progress } from '@/components/ui/progress'; +// ============================================================================= +// TYPES +// ============================================================================= + export interface ProgressData { percent: number; filesProcessed: number; @@ -16,121 +29,342 @@ export interface ProgressData { functionsFound: number; } +export type IndexingPhase = 'connecting' | 'cloning' | 'indexing' | 'completed' | 'error'; + interface IndexingProgressProps { progress: ProgressData; + phase?: IndexingPhase; repoName?: string; + recentFiles?: string[]; // Streaming file list from WebSocket onCancel?: () => void; } +// ============================================================================= +// SUB-COMPONENTS +// ============================================================================= + function AnimatedDots() { return ( -