From 71a2adf35df42962d332a640c62866d5ce275b85 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Mon, 5 Jan 2026 23:03:41 +0530 Subject: [PATCH] ficed signin popup from proxy server when pvt repo was cloned withot PAT --- api/proxy.ts | 11 +- src/components/DropZone.tsx | 10 +- src/components/EmbeddingStatus.tsx | 124 +++++++++++++------- src/components/WebGPUFallbackDialog.tsx | 149 ++++++++++++++++++++++++ src/core/embeddings/embedder.ts | 122 +++++++++++++++---- src/core/embeddings/types.ts | 8 +- src/hooks/useAppState.tsx | 16 ++- src/workers/ingestion.worker.ts | 11 +- 8 files changed, 370 insertions(+), 81 deletions(-) create mode 100644 src/components/WebGPUFallbackDialog.tsx diff --git a/api/proxy.ts b/api/proxy.ts index f59a73e33e..76098144c0 100644 --- a/api/proxy.ts +++ b/api/proxy.ts @@ -78,9 +78,16 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Expose-Headers', '*'); - // Forward response headers + // Forward response headers (except ones that cause issues) + const skipHeaders = [ + 'content-encoding', + 'transfer-encoding', + 'connection', + 'www-authenticate', // IMPORTANT: Strip this to prevent browser's native auth popup! + ]; + response.headers.forEach((value, key) => { - if (!['content-encoding', 'transfer-encoding', 'connection'].includes(key.toLowerCase())) { + if (!skipHeaders.includes(key.toLowerCase())) { res.setHeader(key, value); } }); diff --git a/src/components/DropZone.tsx b/src/components/DropZone.tsx index ad286adfc0..b89dbdfbe8 100644 --- a/src/components/DropZone.tsx +++ b/src/components/DropZone.tsx @@ -91,8 +91,14 @@ export const DropZone = ({ onFileSelect, onGitClone }: DropZoneProps) => { console.error('Clone failed:', err); const message = err instanceof Error ? err.message : 'Failed to clone repository'; // Provide helpful error for auth failures - if (message.includes('401') || message.includes('403')) { - setError('Authentication failed. Check your token or ensure the repo is accessible.'); + if (message.includes('401') || message.includes('403') || message.includes('Authentication')) { + if (!githubToken) { + setError('🔒 This looks like a private repo. Add a GitHub PAT (Personal Access Token) to access it.'); + } else { + setError('🔑 Authentication failed. Check your token permissions (needs repo access).'); + } + } else if (message.includes('404') || message.includes('not found')) { + setError('Repository not found. Check the URL or it might be private (needs PAT).'); } else { setError(message); } diff --git a/src/components/EmbeddingStatus.tsx b/src/components/EmbeddingStatus.tsx index f7e2bed2a8..2af94c2841 100644 --- a/src/components/EmbeddingStatus.tsx +++ b/src/components/EmbeddingStatus.tsx @@ -1,6 +1,7 @@ import { Brain, Loader2, Check, AlertCircle, Zap, FlaskConical } from 'lucide-react'; import { useAppState } from '../hooks/useAppState'; import { useState } from 'react'; +import { WebGPUFallbackDialog } from './WebGPUFallbackDialog'; /** * Embedding status indicator and trigger button @@ -17,17 +18,36 @@ export const EmbeddingStatus = () => { } = useAppState(); const [testResult, setTestResult] = useState(null); + const [showFallbackDialog, setShowFallbackDialog] = useState(false); // Only show when exploring a loaded graph if (viewMode !== 'exploring' || !graph) return null; - const handleStartEmbeddings = async () => { + const nodeCount = graph.nodes.length; + + const handleStartEmbeddings = async (forceDevice?: 'webgpu' | 'wasm') => { try { - await startEmbeddings(); - } catch (error) { - console.error('Embedding failed:', error); + await startEmbeddings(forceDevice); + } catch (error: any) { + // Check if it's a WebGPU not available error + if (error?.name === 'WebGPUNotAvailableError' || + error?.message?.includes('WebGPU not available')) { + setShowFallbackDialog(true); + } else { + console.error('Embedding failed:', error); + } } }; + + const handleUseCPU = () => { + setShowFallbackDialog(false); + handleStartEmbeddings('wasm'); + }; + + const handleSkipEmbeddings = () => { + setShowFallbackDialog(false); + // Just close - user can try again later if they want + }; const handleTestArrayParams = async () => { setTestResult('Testing...'); @@ -41,32 +61,46 @@ export const EmbeddingStatus = () => { } }; + // WebGPU fallback dialog - rendered independently of state + const fallbackDialog = ( + setShowFallbackDialog(false)} + onUseCPU={handleUseCPU} + onSkip={handleSkipEmbeddings} + nodeCount={nodeCount} + /> + ); + // Idle state - show button to start if (embeddingStatus === 'idle') { return ( -
- {/* Test button (dev only) */} - {import.meta.env.DEV && ( + <> +
+ {/* Test button (dev only) */} + {import.meta.env.DEV && ( + + )} + - )} - - -
+
+ {fallbackDialog} + ); } @@ -74,18 +108,21 @@ export const EmbeddingStatus = () => { if (embeddingStatus === 'loading') { const downloadPercent = embeddingProgress?.modelDownloadPercent ?? 0; return ( -
- -
- Loading AI model... -
-
+ <> +
+ +
+ Loading AI model... +
+
+
-
+ {fallbackDialog} + ); } @@ -139,14 +176,17 @@ export const EmbeddingStatus = () => { // Error if (embeddingStatus === 'error') { return ( - + <> + + {fallbackDialog} + ); } diff --git a/src/components/WebGPUFallbackDialog.tsx b/src/components/WebGPUFallbackDialog.tsx new file mode 100644 index 0000000000..8bb28d9c45 --- /dev/null +++ b/src/components/WebGPUFallbackDialog.tsx @@ -0,0 +1,149 @@ +import { useState, useEffect } from 'react'; +import { X, Snail, Rocket, SkipForward } from 'lucide-react'; + +interface WebGPUFallbackDialogProps { + isOpen: boolean; + onClose: () => void; + onUseCPU: () => void; + onSkip: () => void; + nodeCount: number; +} + +/** + * Fun dialog shown when WebGPU isn't available + * Lets user choose: CPU fallback (slow) or skip embeddings + */ +export const WebGPUFallbackDialog = ({ + isOpen, + onClose, + onUseCPU, + onSkip, + nodeCount, +}: WebGPUFallbackDialogProps) => { + const [isAnimating, setIsAnimating] = useState(true); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (isOpen) { + // Trigger animation after mount + requestAnimationFrame(() => setIsVisible(true)); + } else { + setIsVisible(false); + } + }, [isOpen]); + + if (!isOpen) return null; + + // Estimate time based on node count (rough: ~50ms per node on CPU) + const estimatedMinutes = Math.ceil((nodeCount * 50) / 60000); + const isSmallCodebase = nodeCount < 200; + + return ( +
+ {/* Backdrop */} +
+ + {/* Dialog */} +
+ {/* Header with scratching emoji */} +
+ + +
+ {/* Animated emoji */} +
setIsAnimating(false)} + onClick={() => setIsAnimating(true)} + > + 🤔 +
+
+

+ WebGPU said "nope" +

+

+ Your browser doesn't support GPU acceleration +

+
+
+
+ + {/* Content */} +
+

+ Couldn't create embeddings with WebGPU, so semantic search (Graph RAG) + won't be as smart. The graph still works fine though! +

+ +
+

+ Your options: +

+
    +
  • + + + Use CPU — Works but {isSmallCodebase ? 'a bit' : 'way'} slower + {nodeCount > 0 && ( + (~{estimatedMinutes} min for {nodeCount} nodes) + )} + +
  • +
  • + + + Skip it — Graph works, just no AI semantic search + +
  • +
+
+ + {isSmallCodebase && ( +

+ + Small codebase detected! CPU should be fine. +

+ )} + +

+ 💡 Tip: Try Chrome or Edge for WebGPU support +

+
+ + {/* Actions */} +
+ + +
+
+
+ ); +}; + diff --git a/src/core/embeddings/embedder.ts b/src/core/embeddings/embedder.ts index 3744d0c6ce..81a09feddb 100644 --- a/src/core/embeddings/embedder.ts +++ b/src/core/embeddings/embedder.ts @@ -14,23 +14,66 @@ import { DEFAULT_EMBEDDING_CONFIG, type EmbeddingConfig, type ModelProgress } fr let embedderInstance: FeatureExtractionPipeline | null = null; let isInitializing = false; let initPromise: Promise | null = null; +let currentDevice: 'webgpu' | 'wasm' | null = null; /** * Progress callback type for model loading */ export type ModelProgressCallback = (progress: ModelProgress) => void; +/** + * Custom error thrown when WebGPU is not available + * Allows UI to prompt user for fallback choice + */ +export class WebGPUNotAvailableError extends Error { + constructor(originalError?: Error) { + super('WebGPU not available in this browser'); + this.name = 'WebGPUNotAvailableError'; + this.cause = originalError; + } +} + +/** + * Check if WebGPU is available in this browser + * Quick check without loading the model + */ +export const checkWebGPUAvailability = async (): Promise => { + try { + if (!navigator.gpu) { + return false; + } + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + return false; + } + // Try to get a device - this is where it usually fails + const device = await adapter.requestDevice(); + device.destroy(); // Clean up + return true; + } catch { + return false; + } +}; + +/** + * Get the current device being used for inference + */ +export const getCurrentDevice = (): 'webgpu' | 'wasm' | null => currentDevice; + /** * Initialize the embedding model * Uses singleton pattern - only loads once, subsequent calls return cached instance * * @param onProgress - Optional callback for model download progress * @param config - Optional configuration override + * @param forceDevice - Force a specific device (bypasses WebGPU check) * @returns Promise resolving to the embedder pipeline + * @throws WebGPUNotAvailableError if WebGPU is requested but unavailable */ export const initEmbedder = async ( onProgress?: ModelProgressCallback, - config: Partial = {} + config: Partial = {}, + forceDevice?: 'webgpu' | 'wasm' ): Promise => { // Return existing instance if available if (embedderInstance) { @@ -45,6 +88,7 @@ export const initEmbedder = async ( isInitializing = true; const finalConfig = { ...DEFAULT_EMBEDDING_CONFIG, ...config }; + const requestedDevice = forceDevice || finalConfig.device; initPromise = (async () => { try { @@ -55,8 +99,6 @@ export const initEmbedder = async ( console.log(`🧠 Loading embedding model: ${finalConfig.modelId}`); } - // Create the feature extraction pipeline - // Try WebGPU first for speed, fallback to WASM for compatibility const progressCallback = onProgress ? (data: any) => { const progress: ModelProgress = { status: data.status || 'progress', @@ -68,52 +110,84 @@ export const initEmbedder = async ( onProgress(progress); } : undefined; - // Try preferred device first, fallback to wasm if it fails - const devicesToTry: Array<'webgpu' | 'wasm'> = - finalConfig.device === 'webgpu' ? ['webgpu', 'wasm'] : ['wasm']; - - let lastError: Error | null = null; - - for (const device of devicesToTry) { + // If WebGPU is requested (default), check availability first + if (requestedDevice === 'webgpu') { + if (import.meta.env.DEV) { + console.log('🔧 Checking WebGPU availability...'); + } + + const webgpuAvailable = await checkWebGPUAvailability(); + + if (!webgpuAvailable) { + if (import.meta.env.DEV) { + console.warn('⚠️ WebGPU not available'); + } + isInitializing = false; + initPromise = null; + throw new WebGPUNotAvailableError(); + } + + // Try WebGPU try { - if (import.meta.env.DEV && devicesToTry.length > 1) { - console.log(`🔧 Trying ${device} backend...`); + if (import.meta.env.DEV) { + console.log('🔧 Initializing WebGPU backend...'); } embedderInstance = await pipeline( 'feature-extraction', finalConfig.modelId, { - device, + device: 'webgpu', dtype: 'fp32', progress_callback: progressCallback, } ); + currentDevice = 'webgpu'; if (import.meta.env.DEV) { - console.log(`✅ Using ${device} backend`); + console.log('✅ Using WebGPU backend'); } - break; // Success! - } catch (err) { - lastError = err as Error; if (import.meta.env.DEV) { - console.warn(`⚠️ ${device} backend failed:`, err); + console.warn('⚠️ WebGPU initialization failed:', err); } - // Continue to next device + isInitializing = false; + initPromise = null; + embedderInstance = null; + throw new WebGPUNotAvailableError(err as Error); + } + } else { + // WASM mode requested (user chose fallback) + if (import.meta.env.DEV) { + console.log('🔧 Initializing WASM backend (this will be slower)...'); + } + + embedderInstance = await pipeline( + 'feature-extraction', + finalConfig.modelId, + { + device: 'wasm', // WASM-based CPU execution + dtype: 'fp32', + progress_callback: progressCallback, + } + ); + currentDevice = 'wasm'; + + if (import.meta.env.DEV) { + console.log('✅ Using WASM backend'); } - } - - if (!embedderInstance) { - throw lastError || new Error('No backend available'); } if (import.meta.env.DEV) { console.log('✅ Embedding model loaded successfully'); } - return embedderInstance; + return embedderInstance!; } catch (error) { + // Re-throw WebGPUNotAvailableError as-is + if (error instanceof WebGPUNotAvailableError) { + throw error; + } isInitializing = false; initPromise = null; embedderInstance = null; diff --git a/src/core/embeddings/types.ts b/src/core/embeddings/types.ts index 314996660b..e4a04222b2 100644 --- a/src/core/embeddings/types.ts +++ b/src/core/embeddings/types.ts @@ -59,8 +59,8 @@ export interface EmbeddingConfig { batchSize: number; /** Embedding vector dimensions */ dimensions: number; - /** Device to use for inference */ - device: 'webgpu' | 'wasm' | 'cpu'; + /** Device to use for inference: 'webgpu' for GPU acceleration, 'wasm' for WASM-based CPU */ + device: 'webgpu' | 'wasm'; /** Maximum characters of code snippet to include */ maxSnippetLength: number; } @@ -68,13 +68,13 @@ export interface EmbeddingConfig { /** * Default embedding configuration * Uses snowflake-arctic-embed-xs for browser efficiency - * Tries WebGPU first (fast), auto-fallback to WASM if unavailable + * Tries WebGPU first (fast), user can choose WASM fallback if unavailable */ export const DEFAULT_EMBEDDING_CONFIG: EmbeddingConfig = { modelId: 'Snowflake/snowflake-arctic-embed-xs', batchSize: 16, dimensions: 384, - device: 'webgpu', // Try WebGPU first, auto-fallback to WASM + device: 'webgpu', // WebGPU preferred, WASM fallback available if user chooses maxSnippetLength: 500, }; diff --git a/src/hooks/useAppState.tsx b/src/hooks/useAppState.tsx index 59570932ad..6336e7a47c 100644 --- a/src/hooks/useAppState.tsx +++ b/src/hooks/useAppState.tsx @@ -75,7 +75,7 @@ interface AppState { embeddingProgress: EmbeddingProgress | null; // Embedding methods - startEmbeddings: () => Promise; + startEmbeddings: (forceDevice?: 'webgpu' | 'wasm') => Promise; semanticSearch: (query: string, k?: number) => Promise; semanticSearchWithContext: (query: string, k?: number, hops?: number) => Promise; isEmbeddingReady: boolean; @@ -197,7 +197,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { }, []); // Embedding methods - const startEmbeddings = useCallback(async (): Promise => { + const startEmbeddings = useCallback(async (forceDevice?: 'webgpu' | 'wasm'): Promise => { const api = apiRef.current; if (!api) throw new Error('Worker not initialized'); @@ -228,9 +228,15 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { } }); - await api.startEmbeddingPipeline(proxiedOnProgress); - } catch (error) { - setEmbeddingStatus('error'); + await api.startEmbeddingPipeline(proxiedOnProgress, forceDevice); + } catch (error: any) { + // Check if it's WebGPU not available - let caller handle the dialog + if (error?.name === 'WebGPUNotAvailableError' || + error?.message?.includes('WebGPU not available')) { + setEmbeddingStatus('idle'); // Reset to idle so user can try again + } else { + setEmbeddingStatus('error'); + } throw error; } }, []); diff --git a/src/workers/ingestion.worker.ts b/src/workers/ingestion.worker.ts index d56643acc4..1c6ccb86c6 100644 --- a/src/workers/ingestion.worker.ts +++ b/src/workers/ingestion.worker.ts @@ -166,9 +166,11 @@ const workerApi = { * Start the embedding pipeline in the background * Generates embeddings for all embeddable nodes and creates vector index * @param onProgress - Proxied callback for embedding progress updates + * @param forceDevice - Force a specific device ('webgpu' or 'wasm') */ async startEmbeddingPipeline( - onProgress: (progress: EmbeddingProgress) => void + onProgress: (progress: EmbeddingProgress) => void, + forceDevice?: 'webgpu' | 'wasm' ): Promise { const kuzu = await getKuzuAdapter(); if (!kuzu.isKuzuReady()) { @@ -187,7 +189,12 @@ const workerApi = { onProgress(progress); }; - await runEmbeddingPipeline(kuzu.executeQuery, kuzu.executeWithReusedStatement, progressCallback); + await runEmbeddingPipeline( + kuzu.executeQuery, + kuzu.executeWithReusedStatement, + progressCallback, + forceDevice ? { device: forceDevice } : {} + ); }, /**