ficed signin popup from proxy server when pvt repo was cloned withot PAT

This commit is contained in:
abhigyanpatwari 2026-01-05 23:03:41 +05:30
parent b85ec11319
commit 71a2adf35d
8 changed files with 370 additions and 81 deletions

View file

@ -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);
}
});

View file

@ -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);
}

View file

@ -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<string | null>(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 = (
<WebGPUFallbackDialog
isOpen={showFallbackDialog}
onClose={() => setShowFallbackDialog(false)}
onUseCPU={handleUseCPU}
onSkip={handleSkipEmbeddings}
nodeCount={nodeCount}
/>
);
// Idle state - show button to start
if (embeddingStatus === 'idle') {
return (
<div className="flex items-center gap-2">
{/* Test button (dev only) */}
{import.meta.env.DEV && (
<>
<div className="flex items-center gap-2">
{/* Test button (dev only) */}
{import.meta.env.DEV && (
<button
onClick={handleTestArrayParams}
className="flex items-center gap-1 px-2 py-1.5 bg-surface border border-border-subtle rounded-lg text-xs text-text-muted hover:bg-hover hover:text-text-secondary transition-all"
title="Test if KuzuDB supports array params"
>
<FlaskConical className="w-3 h-3" />
{testResult || 'Test'}
</button>
)}
<button
onClick={handleTestArrayParams}
className="flex items-center gap-1 px-2 py-1.5 bg-surface border border-border-subtle rounded-lg text-xs text-text-muted hover:bg-hover hover:text-text-secondary transition-all"
title="Test if KuzuDB supports array params"
onClick={() => handleStartEmbeddings()}
className="flex items-center gap-2 px-3 py-1.5 bg-surface border border-border-subtle rounded-lg text-sm text-text-secondary hover:bg-hover hover:text-text-primary hover:border-accent/50 transition-all group"
title="Generate embeddings for semantic search"
>
<FlaskConical className="w-3 h-3" />
{testResult || 'Test'}
<Brain className="w-4 h-4 text-node-interface group-hover:text-accent transition-colors" />
<span className="hidden sm:inline">Enable Semantic Search</span>
<Zap className="w-3 h-3 text-text-muted" />
</button>
)}
<button
onClick={handleStartEmbeddings}
className="flex items-center gap-2 px-3 py-1.5 bg-surface border border-border-subtle rounded-lg text-sm text-text-secondary hover:bg-hover hover:text-text-primary hover:border-accent/50 transition-all group"
title="Generate embeddings for semantic search"
>
<Brain className="w-4 h-4 text-node-interface group-hover:text-accent transition-colors" />
<span className="hidden sm:inline">Enable Semantic Search</span>
<Zap className="w-3 h-3 text-text-muted" />
</button>
</div>
</div>
{fallbackDialog}
</>
);
}
@ -74,18 +108,21 @@ export const EmbeddingStatus = () => {
if (embeddingStatus === 'loading') {
const downloadPercent = embeddingProgress?.modelDownloadPercent ?? 0;
return (
<div className="flex items-center gap-2.5 px-3 py-1.5 bg-surface border border-accent/30 rounded-lg text-sm">
<Loader2 className="w-4 h-4 text-accent animate-spin" />
<div className="flex flex-col gap-0.5">
<span className="text-text-secondary text-xs">Loading AI model...</span>
<div className="w-24 h-1 bg-elevated rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-accent to-node-interface rounded-full transition-all duration-300"
style={{ width: `${downloadPercent}%` }}
/>
<>
<div className="flex items-center gap-2.5 px-3 py-1.5 bg-surface border border-accent/30 rounded-lg text-sm">
<Loader2 className="w-4 h-4 text-accent animate-spin" />
<div className="flex flex-col gap-0.5">
<span className="text-text-secondary text-xs">Loading AI model...</span>
<div className="w-24 h-1 bg-elevated rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-accent to-node-interface rounded-full transition-all duration-300"
style={{ width: `${downloadPercent}%` }}
/>
</div>
</div>
</div>
</div>
{fallbackDialog}
</>
);
}
@ -139,14 +176,17 @@ export const EmbeddingStatus = () => {
// Error
if (embeddingStatus === 'error') {
return (
<button
onClick={handleStartEmbeddings}
className="flex items-center gap-2 px-3 py-1.5 bg-red-500/10 border border-red-500/30 rounded-lg text-sm text-red-400 hover:bg-red-500/20 transition-colors"
title={embeddingProgress?.error || 'Embedding failed. Click to retry.'}
>
<AlertCircle className="w-4 h-4" />
<span className="text-xs">Failed - Retry</span>
</button>
<>
<button
onClick={() => handleStartEmbeddings()}
className="flex items-center gap-2 px-3 py-1.5 bg-red-500/10 border border-red-500/30 rounded-lg text-sm text-red-400 hover:bg-red-500/20 transition-colors"
title={embeddingProgress?.error || 'Embedding failed. Click to retry.'}
>
<AlertCircle className="w-4 h-4" />
<span className="text-xs">Failed - Retry</span>
</button>
{fallbackDialog}
</>
);
}

View file

@ -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 (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className={`absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-200 ${isVisible ? 'opacity-100' : 'opacity-0'}`}
onClick={onClose}
/>
{/* Dialog */}
<div
className={`relative bg-surface border border-border-subtle rounded-2xl shadow-2xl max-w-md w-full mx-4 overflow-hidden transition-all duration-200 ${isVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-95'}`}
>
{/* Header with scratching emoji */}
<div className="relative bg-gradient-to-r from-amber-500/20 to-orange-500/20 px-6 py-5 border-b border-border-subtle">
<button
onClick={onClose}
className="absolute top-4 right-4 p-1 text-text-muted hover:text-text-primary transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center gap-4">
{/* Animated emoji */}
<div
className={`text-5xl ${isAnimating ? 'animate-bounce' : ''}`}
onAnimationEnd={() => setIsAnimating(false)}
onClick={() => setIsAnimating(true)}
>
🤔
</div>
<div>
<h2 className="text-lg font-semibold text-text-primary">
WebGPU said "nope"
</h2>
<p className="text-sm text-text-muted mt-0.5">
Your browser doesn't support GPU acceleration
</p>
</div>
</div>
</div>
{/* Content */}
<div className="px-6 py-5 space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
Couldn't create embeddings with WebGPU, so semantic search (Graph RAG)
won't be as smart. The graph still works fine though!
</p>
<div className="bg-elevated/50 rounded-lg p-4 border border-border-subtle">
<p className="text-sm text-text-secondary">
<span className="font-medium text-text-primary">Your options:</span>
</p>
<ul className="mt-2 space-y-1.5 text-sm text-text-muted">
<li className="flex items-start gap-2">
<Snail className="w-4 h-4 mt-0.5 text-amber-400 flex-shrink-0" />
<span>
<strong className="text-text-secondary">Use CPU</strong> — Works but {isSmallCodebase ? 'a bit' : 'way'} slower
{nodeCount > 0 && (
<span className="text-text-muted"> (~{estimatedMinutes} min for {nodeCount} nodes)</span>
)}
</span>
</li>
<li className="flex items-start gap-2">
<SkipForward className="w-4 h-4 mt-0.5 text-blue-400 flex-shrink-0" />
<span>
<strong className="text-text-secondary">Skip it</strong> — Graph works, just no AI semantic search
</span>
</li>
</ul>
</div>
{isSmallCodebase && (
<p className="text-xs text-node-function flex items-center gap-1.5 bg-node-function/10 px-3 py-2 rounded-lg">
<Rocket className="w-3.5 h-3.5" />
Small codebase detected! CPU should be fine.
</p>
)}
<p className="text-xs text-text-muted">
💡 Tip: Try Chrome or Edge for WebGPU support
</p>
</div>
{/* Actions */}
<div className="px-6 py-4 bg-elevated/30 border-t border-border-subtle flex gap-3">
<button
onClick={onSkip}
className="flex-1 px-4 py-2.5 text-sm font-medium text-text-secondary bg-surface border border-border-subtle rounded-lg hover:bg-hover hover:text-text-primary transition-all flex items-center justify-center gap-2"
>
<SkipForward className="w-4 h-4" />
Skip Embeddings
</button>
<button
onClick={onUseCPU}
className={`flex-1 px-4 py-2.5 text-sm font-medium rounded-lg transition-all flex items-center justify-center gap-2 ${
isSmallCodebase
? 'bg-node-function text-white hover:bg-node-function/90'
: 'bg-amber-500/20 text-amber-300 border border-amber-500/30 hover:bg-amber-500/30'
}`}
>
<Snail className="w-4 h-4" />
Use CPU {isSmallCodebase ? '(Recommended)' : '(Slow)'}
</button>
</div>
</div>
</div>
);
};

View file

@ -14,23 +14,66 @@ import { DEFAULT_EMBEDDING_CONFIG, type EmbeddingConfig, type ModelProgress } fr
let embedderInstance: FeatureExtractionPipeline | null = null;
let isInitializing = false;
let initPromise: Promise<FeatureExtractionPipeline> | 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<boolean> => {
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<EmbeddingConfig> = {}
config: Partial<EmbeddingConfig> = {},
forceDevice?: 'webgpu' | 'wasm'
): Promise<FeatureExtractionPipeline> => {
// 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;

View file

@ -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,
};

View file

@ -75,7 +75,7 @@ interface AppState {
embeddingProgress: EmbeddingProgress | null;
// Embedding methods
startEmbeddings: () => Promise<void>;
startEmbeddings: (forceDevice?: 'webgpu' | 'wasm') => Promise<void>;
semanticSearch: (query: string, k?: number) => Promise<SemanticSearchResult[]>;
semanticSearchWithContext: (query: string, k?: number, hops?: number) => Promise<any[]>;
isEmbeddingReady: boolean;
@ -197,7 +197,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
}, []);
// Embedding methods
const startEmbeddings = useCallback(async (): Promise<void> => {
const startEmbeddings = useCallback(async (forceDevice?: 'webgpu' | 'wasm'): Promise<void> => {
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;
}
}, []);

View file

@ -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<void> {
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 } : {}
);
},
/**