mirror of
https://github.com/run-llama/auto_rfp.git
synced 2026-07-01 21:54:05 -04:00
a9431b3ba0
* Refactor environment variable management with centralized validation - Replace plain object with Map-based env structure - Add all environment variables (not just LlamaCloud ones) - Use sentinel value 'mandatory-env-not-set' for required vars - Add helper functions: isDevelopment(), isProduction(), logEnv() - Create instrumentation.ts for one-time startup validation - Validate environment once at server startup using Next.js hook - Exit with error code 1 if validation fails This ensures the app doesn't start with missing required env vars and provides better error messages during startup. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Update services to use centralized env.get() pattern Replace direct process.env access with env.get() in: - OpenAI services (openai-question-extractor, multi-step-response) - Supabase utilities (4 files) - Database client (db.ts) - Login actions and API routes Also use helper functions: - isDevelopment() and isProduction() instead of NODE_ENV checks - Keep Vercel platform vars (VERCEL_URL, VERCEL_ENV) as process.env Remove constructor-level env validation checks since validation now happens once at startup. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Remove redundant validateEnv() calls from services Remove 9 validateEnv() calls across: - lib/llama-index-service.ts - lib/llamaparse-service.ts - lib/services/llamacloud-documents-service.ts - lib/services/llamacloud-connection-service.ts - app/api/llamacloud/projects/route.ts - app/api/organizations/route.ts Environment validation now happens once at startup via instrumentation.ts, so these per-request checks are no longer needed. Also updated env access to use env.get() pattern consistently. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Simplify next.config.ts by removing explicit env var exposure Remove explicit env var configuration from next.config.ts. Next.js automatically makes NEXT_PUBLIC_* variables available to the client, so explicit exposure is unnecessary. Keeps only essential config: - reactStrictMode - output: 'standalone' (for Docker deployment) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix remaining env.PROPERTY to env.get() conversions Updated remaining files that were still using env.PROPERTY pattern: - lib/llama-index-service.ts - lib/llamaparse-service.ts - lib/services/llamacloud-client.ts - lib/services/llamacloud-documents-service.ts - app/api/llamacloud/projects/route.ts - app/api/organizations/route.ts - app/api/projects/[projectId]/indexes/route.ts All files now consistently use env.get('VARIABLE_NAME')! pattern. TypeScript compilation now passes successfully. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Refactor LlamaIndexService import paths and consolidate service files * Fix ESLint warnings in React components Fix React Hook dependency warnings: - ProjectDocuments: Add fetchProjectDocuments to useEffect deps - ProjectIndexSelector: Wrap hasChanges and handleSave in useCallback and add all dependencies to the debounced auto-save useEffect All ESLint checks now pass with no warnings or errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Add comprehensive environment variable documentation Add detailed documentation explaining build-time vs runtime variables, especially for NEXT_PUBLIC_* variables and Docker deployments. Key additions: - New "Environment Variables" section in README with comprehensive guide - Explanation of build-time (NEXT_PUBLIC_*) vs runtime variables - Docker deployment workflow (correct vs incorrect approaches) - Why NEXT_PUBLIC_* variables require Docker rebuild - Best practices for multi-environment deployments - Code examples showing proper env.get() usage - Startup validation explanation This documentation is critical for users deploying with Docker, as changing NEXT_PUBLIC_* variables without rebuilding will not work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Implement auth for all api routes * NODE_ENV has a default * Make the env lookup type-safe. * Restrict API health check path in session update logic * Improve UI consistency --------- Co-authored-by: Roland Tritsch <roland@tritsch.email> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
330 lines
11 KiB
TypeScript
330 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { useToast } from '@/components/ui/use-toast';
|
|
import {
|
|
Database,
|
|
Settings,
|
|
Check,
|
|
AlertCircle,
|
|
RefreshCw,
|
|
FolderOpen
|
|
} from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface ProjectIndex {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
created_at?: string;
|
|
}
|
|
|
|
interface ProjectIndexSelectorProps {
|
|
projectId: string;
|
|
onSaveSuccess?: () => void;
|
|
}
|
|
|
|
export function ProjectIndexSelector({ projectId, onSaveSuccess }: ProjectIndexSelectorProps) {
|
|
const [currentIndexes, setCurrentIndexes] = useState<ProjectIndex[]>([]);
|
|
const [availableIndexes, setAvailableIndexes] = useState<ProjectIndex[]>([]);
|
|
const [selectedIndexId, setSelectedIndexId] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [organizationConnected, setOrganizationConnected] = useState(false);
|
|
const [organizationName, setOrganizationName] = useState('');
|
|
const [llamaCloudProjectName, setLlamaCloudProjectName] = useState('');
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
const { toast } = useToast();
|
|
|
|
const fetchProjectIndexes = useCallback(async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
const response = await fetch(`/api/projects/${projectId}/indexes`);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to fetch project indexes');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
setCurrentIndexes(data.currentIndexes || []);
|
|
setAvailableIndexes(data.availableIndexes || []);
|
|
// Take first index if any exist (single select)
|
|
const currentIds = data.currentIndexes?.map((index: ProjectIndex) => index.id) || [];
|
|
setSelectedIndexId(currentIds.length > 0 ? currentIds[0] : null);
|
|
setOrganizationConnected(data.organizationConnected);
|
|
setOrganizationName(data.organizationName || '');
|
|
setLlamaCloudProjectName(data.llamaCloudProjectName || '');
|
|
setIsInitialized(true);
|
|
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch project indexes';
|
|
setError(errorMessage);
|
|
console.error('Error fetching project indexes:', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [projectId]);
|
|
|
|
useEffect(() => {
|
|
fetchProjectIndexes();
|
|
}, [fetchProjectIndexes]);
|
|
|
|
const handleIndexSelect = (indexId: string) => {
|
|
if (isSaving) return;
|
|
|
|
// Toggle: if already selected, deselect; otherwise select this one
|
|
setSelectedIndexId(prev => prev === indexId ? null : indexId);
|
|
};
|
|
|
|
const hasChanges = useCallback(() => {
|
|
const currentId = currentIndexes.length > 0 ? currentIndexes[0].id : null;
|
|
return selectedIndexId !== currentId;
|
|
}, [currentIndexes, selectedIndexId]);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
try {
|
|
setIsSaving(true);
|
|
|
|
const response = await fetch(`/api/projects/${projectId}/indexes`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
indexIds: selectedIndexId ? [selectedIndexId] : [],
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to update project indexes');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
setCurrentIndexes(data.projectIndexes || []);
|
|
|
|
// Notify parent that save succeeded
|
|
onSaveSuccess?.();
|
|
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : 'Failed to update project indexes';
|
|
toast({
|
|
title: 'Error',
|
|
description: errorMessage,
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}, [projectId, selectedIndexId, onSaveSuccess, toast]);
|
|
|
|
// Debounced auto-save effect
|
|
useEffect(() => {
|
|
// Don't save on initial load or while loading
|
|
if (!isInitialized || isLoading) return;
|
|
|
|
// Only save if there are actual changes
|
|
if (!hasChanges()) return;
|
|
|
|
const timer = setTimeout(() => {
|
|
handleSave();
|
|
}, 800);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [selectedIndexId, isInitialized, isLoading, hasChanges, handleSave]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="h-5 w-5" />
|
|
<Skeleton className="h-5 w-32" />
|
|
</CardTitle>
|
|
<CardDescription>
|
|
<Skeleton className="h-4 w-64" />
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{[1, 2, 3].map((i) => (
|
|
<div key={i} className="flex items-center space-x-3">
|
|
<Skeleton className="h-4 w-4 rounded-full" />
|
|
<Skeleton className="h-4 w-48" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (!organizationConnected) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="h-5 w-5" />
|
|
Project Index
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Select an index for this project
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-center py-8">
|
|
<AlertCircle className="mx-auto h-8 w-8 text-muted-foreground mb-3" />
|
|
<h3 className="text-lg font-medium mb-2">No LlamaCloud Connection</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
Your organization needs to be connected to LlamaCloud to select an index for this project.
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Ask your organization admin to connect to LlamaCloud in the organization settings.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="h-5 w-5" />
|
|
Project Index
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Select an index for this project
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-center py-8">
|
|
<AlertCircle className="mx-auto h-8 w-8 text-red-500 mb-3" />
|
|
<h3 className="text-lg font-medium text-red-900 mb-2">Error Loading Indexes</h3>
|
|
<p className="text-red-600 mb-4">{error}</p>
|
|
<Button variant="outline" onClick={fetchProjectIndexes}>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
Try Again
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="h-5 w-5" />
|
|
Project Index
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Select an index from {organizationName}'s LlamaCloud project "{llamaCloudProjectName}"
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{availableIndexes.length === 0 ? (
|
|
<div className="text-center py-6">
|
|
<FolderOpen className="mx-auto h-8 w-8 text-muted-foreground mb-3" />
|
|
<h3 className="text-lg font-medium mb-2">No Indexes Available</h3>
|
|
<p className="text-muted-foreground">
|
|
No indexes were found in your organization's LlamaCloud project.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="space-y-3">
|
|
{availableIndexes.map((index) => {
|
|
const isSelected = selectedIndexId === index.id;
|
|
|
|
return (
|
|
<div
|
|
key={index.id}
|
|
onClick={() => handleIndexSelect(index.id)}
|
|
className={cn(
|
|
"flex items-start space-x-3 p-3 border rounded-lg transition-colors cursor-pointer",
|
|
isSelected
|
|
? "border-blue-500 bg-blue-50"
|
|
: "hover:bg-muted/50",
|
|
isSaving && "opacity-50 pointer-events-none"
|
|
)}
|
|
>
|
|
{/* Radio-style indicator */}
|
|
<div className={cn(
|
|
"w-4 h-4 rounded-full border-2 flex items-center justify-center mt-0.5 shrink-0",
|
|
isSelected ? "border-blue-500 bg-blue-500" : "border-gray-300"
|
|
)}>
|
|
{isSelected && <div className="w-2 h-2 rounded-full bg-white" />}
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="block font-medium">
|
|
{index.name}
|
|
</div>
|
|
{index.description && (
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{index.description}
|
|
</p>
|
|
)}
|
|
{index.created_at && (
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Created {new Date(index.created_at).toLocaleDateString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{isSelected && (
|
|
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
|
|
<Check className="mr-1 h-3 w-3" />
|
|
Active
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-4 border-t">
|
|
<div className="text-sm text-muted-foreground">
|
|
{selectedIndexId
|
|
? `Selected: ${availableIndexes.find(i => i.id === selectedIndexId)?.name}`
|
|
: "No index selected"}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{isSaving && (
|
|
<span className="text-sm text-muted-foreground flex items-center gap-1">
|
|
<Settings className="h-3 w-3 animate-spin" />
|
|
Saving...
|
|
</span>
|
|
)}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={fetchProjectIndexes}
|
|
disabled={isSaving}
|
|
>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|