mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
ficed signin popup from proxy server when pvt repo was cloned withot PAT
This commit is contained in:
parent
b85ec11319
commit
71a2adf35d
8 changed files with 370 additions and 81 deletions
11
api/proxy.ts
11
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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
149
src/components/WebGPUFallbackDialog.tsx
Normal file
149
src/components/WebGPUFallbackDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -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 } : {}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue