mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-01 04:09:08 -05:00
Merge pull request #232 from coleam00/fix/supabase-key-validation-and-state-consolidation
Fix Supabase key validation and consolidate frontend state management
This commit is contained in:
@@ -22,4 +22,4 @@ COPY . .
|
||||
EXPOSE 5173
|
||||
|
||||
# Start Vite dev server with host binding for Docker
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
|
||||
74
archon-ui-main/src/components/BackendStartupError.tsx
Normal file
74
archon-ui-main/src/components/BackendStartupError.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { AlertCircle, Terminal, RefreshCw } from 'lucide-react';
|
||||
|
||||
export const BackendStartupError: React.FC = () => {
|
||||
const handleRetry = () => {
|
||||
// Reload the page to retry
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[10000] bg-black/90 backdrop-blur-sm flex items-center justify-center p-8">
|
||||
<div className="max-w-2xl w-full">
|
||||
<div className="bg-red-950/50 border-2 border-red-500/50 rounded-lg p-8 shadow-2xl backdrop-blur-md">
|
||||
<div className="flex items-start gap-4">
|
||||
<AlertCircle className="w-8 h-8 text-red-500 flex-shrink-0 mt-1" />
|
||||
<div className="space-y-4 flex-1">
|
||||
<h2 className="text-2xl font-bold text-red-100">
|
||||
Backend Service Startup Failure
|
||||
</h2>
|
||||
|
||||
<p className="text-red-200">
|
||||
The Archon backend service failed to start. This is typically due to a configuration issue.
|
||||
</p>
|
||||
|
||||
<div className="bg-black/50 rounded-lg p-4 border border-red-900/50">
|
||||
<div className="flex items-center gap-2 mb-3 text-red-300">
|
||||
<Terminal className="w-5 h-5" />
|
||||
<span className="font-semibold">Check Docker Logs</span>
|
||||
</div>
|
||||
<p className="text-red-100 font-mono text-sm mb-3">
|
||||
Check the <span className="text-red-400 font-bold">Archon-Server</span> logs in Docker Desktop for detailed error information.
|
||||
</p>
|
||||
<div className="space-y-2 text-xs text-red-300">
|
||||
<p>1. Open Docker Desktop</p>
|
||||
<p>2. Go to Containers tab</p>
|
||||
<p>3. Click on <span className="text-red-400 font-semibold">Archon-Server</span></p>
|
||||
<p>4. View the logs for the specific error message</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-950/30 border border-yellow-700/30 rounded-lg p-3">
|
||||
<p className="text-yellow-200 text-sm">
|
||||
<strong>Common issue:</strong> Using an ANON key instead of SERVICE key in your .env file
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-red-900/30">
|
||||
<p className="text-red-300 text-sm">
|
||||
After fixing the issue in your .env file, recreate the Docker containers:
|
||||
</p>
|
||||
<code className="block mt-2 p-2 bg-black/70 rounded text-red-100 font-mono text-sm">
|
||||
docker compose down && docker compose up -d
|
||||
</code>
|
||||
<p className="text-red-300 text-xs mt-2">
|
||||
Note: Use 'down' and 'up', not 'restart' - containers need to be recreated to load new environment variables
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center pt-4">
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-red-600/20 hover:bg-red-600/30 border border-red-500/50 rounded-lg text-red-100 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Retry Connection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { X } from 'lucide-react';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { credentialsService } from '../../services/credentialsService';
|
||||
import { isLmConfigured } from '../../utils/onboarding';
|
||||
import { BackendStartupError } from '../BackendStartupError';
|
||||
/**
|
||||
* Props for the MainLayout component
|
||||
*/
|
||||
@@ -29,13 +30,14 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [backendReady, setBackendReady] = useState(false);
|
||||
const [backendStartupFailed, setBackendStartupFailed] = useState(false);
|
||||
|
||||
// Check backend readiness
|
||||
useEffect(() => {
|
||||
|
||||
const checkBackendHealth = async (retryCount = 0) => {
|
||||
const maxRetries = 10; // Increased retries for initialization
|
||||
const retryDelay = 1000;
|
||||
const maxRetries = 3; // 3 retries total
|
||||
const retryDelay = 1500; // 1.5 seconds between retries
|
||||
|
||||
try {
|
||||
// Create AbortController for proper timeout handling
|
||||
@@ -58,6 +60,7 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
if (healthData.ready === true) {
|
||||
console.log('✅ Backend is fully initialized');
|
||||
setBackendReady(true);
|
||||
setBackendStartupFailed(false);
|
||||
} else {
|
||||
// Backend is starting up but not ready yet
|
||||
console.log(`🔄 Backend initializing... (attempt ${retryCount + 1}/${maxRetries}):`, healthData.message || 'Loading credentials...');
|
||||
@@ -66,9 +69,10 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
if (retryCount < maxRetries) {
|
||||
setTimeout(() => {
|
||||
checkBackendHealth(retryCount + 1);
|
||||
}, retryDelay); // Constant 1s retry during initialization
|
||||
}, retryDelay); // Constant 1.5s retry during initialization
|
||||
} else {
|
||||
console.warn('Backend initialization taking too long - skipping credential check');
|
||||
console.warn('Backend initialization taking too long - proceeding anyway');
|
||||
// Don't mark as failed yet, just not fully ready
|
||||
setBackendReady(false);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +84,10 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
const errorMessage = error instanceof Error
|
||||
? (error.name === 'AbortError' ? 'Request timeout (5s)' : error.message)
|
||||
: 'Unknown error';
|
||||
console.log(`Backend not ready yet (attempt ${retryCount + 1}/${maxRetries}):`, errorMessage);
|
||||
// Only log after first attempt to reduce noise during normal startup
|
||||
if (retryCount > 0) {
|
||||
console.log(`Backend not ready yet (attempt ${retryCount + 1}/${maxRetries}):`, errorMessage);
|
||||
}
|
||||
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries) {
|
||||
@@ -88,8 +95,9 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
checkBackendHealth(retryCount + 1);
|
||||
}, retryDelay * Math.pow(1.5, retryCount)); // Exponential backoff for connection errors
|
||||
} else {
|
||||
console.warn('Backend not ready after maximum retries - skipping credential check');
|
||||
console.error('Backend startup failed after maximum retries - showing error message');
|
||||
setBackendReady(false);
|
||||
setBackendStartupFailed(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -99,11 +107,16 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
setTimeout(() => {
|
||||
checkBackendHealth();
|
||||
}, 1000); // Wait 1 second for initial app startup
|
||||
}, [showToast, navigate]); // Removed backendReady from dependencies to prevent double execution
|
||||
}, []); // Empty deps - only run once on mount
|
||||
|
||||
// Check for onboarding redirect after backend is ready
|
||||
useEffect(() => {
|
||||
const checkOnboarding = async () => {
|
||||
// Skip if backend failed to start
|
||||
if (backendStartupFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if not ready, already on onboarding, or already dismissed
|
||||
if (!backendReady || location.pathname === '/onboarding') {
|
||||
return;
|
||||
@@ -152,9 +165,12 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
};
|
||||
|
||||
checkOnboarding();
|
||||
}, [backendReady, location.pathname, navigate, showToast]);
|
||||
}, [backendReady, backendStartupFailed, location.pathname, navigate, showToast]);
|
||||
|
||||
return <div className="relative min-h-screen bg-white dark:bg-black overflow-hidden">
|
||||
{/* Show backend startup error if backend failed to start */}
|
||||
{backendStartupFailed && <BackendStartupError />}
|
||||
|
||||
{/* Fixed full-page background grid that doesn't scroll */}
|
||||
<div className="fixed inset-0 neon-grid pointer-events-none z-0"></div>
|
||||
{/* Floating Navigation */}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { Key, ExternalLink, Save, Loader } from 'lucide-react';
|
||||
import { Input } from '../ui/Input';
|
||||
import { Button } from '../ui/Button';
|
||||
import { Select } from '../ui/Select';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { credentialsService } from '../../services/credentialsService';
|
||||
import { useState } from "react";
|
||||
import { Key, ExternalLink, Save, Loader } from "lucide-react";
|
||||
import { Input } from "../ui/Input";
|
||||
import { Button } from "../ui/Button";
|
||||
import { Select } from "../ui/Select";
|
||||
import { useToast } from "../../contexts/ToastContext";
|
||||
import { credentialsService } from "../../services/credentialsService";
|
||||
|
||||
interface ProviderStepProps {
|
||||
onSaved: () => void;
|
||||
@@ -12,14 +12,14 @@ interface ProviderStepProps {
|
||||
}
|
||||
|
||||
export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
|
||||
const [provider, setProvider] = useState('openai');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [provider, setProvider] = useState("openai");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { showToast } = useToast();
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!apiKey.trim()) {
|
||||
showToast('Please enter an API key', 'error');
|
||||
showToast("Please enter an API key", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,60 +27,50 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
|
||||
try {
|
||||
// Save the API key
|
||||
await credentialsService.createCredential({
|
||||
key: 'OPENAI_API_KEY',
|
||||
key: "OPENAI_API_KEY",
|
||||
value: apiKey,
|
||||
is_encrypted: true,
|
||||
category: 'api_keys'
|
||||
category: "api_keys",
|
||||
});
|
||||
|
||||
// Update the provider setting if needed
|
||||
await credentialsService.updateCredential({
|
||||
key: 'LLM_PROVIDER',
|
||||
value: 'openai',
|
||||
key: "LLM_PROVIDER",
|
||||
value: "openai",
|
||||
is_encrypted: false,
|
||||
category: 'rag_strategy'
|
||||
category: "rag_strategy",
|
||||
});
|
||||
|
||||
showToast('API key saved successfully!', 'success');
|
||||
showToast("API key saved successfully!", "success");
|
||||
// Mark onboarding as dismissed when API key is saved
|
||||
localStorage.setItem('onboardingDismissed', 'true');
|
||||
localStorage.setItem("onboardingDismissed", "true");
|
||||
onSaved();
|
||||
} catch (error) {
|
||||
// Detailed error handling for critical configuration per alpha principles
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
const errorDetails = {
|
||||
context: 'API key configuration',
|
||||
operation: 'save_openai_key',
|
||||
provider: 'openai',
|
||||
error: errorMessage,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Log with full context and stack trace
|
||||
console.error('API_KEY_SAVE_FAILED:', errorDetails, error);
|
||||
|
||||
// Log error for debugging per alpha principles
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
console.error("Failed to save API key:", error);
|
||||
|
||||
// Show specific error details to help user resolve the issue
|
||||
if (errorMessage.includes('duplicate') || errorMessage.includes('already exists')) {
|
||||
if (
|
||||
errorMessage.includes("duplicate") ||
|
||||
errorMessage.includes("already exists")
|
||||
) {
|
||||
showToast(
|
||||
'API key already exists. Please update it in Settings if you want to change it.',
|
||||
'warning'
|
||||
"API key already exists. Please update it in Settings if you want to change it.",
|
||||
"warning",
|
||||
);
|
||||
} else if (errorMessage.includes('network') || errorMessage.includes('fetch')) {
|
||||
} else if (
|
||||
errorMessage.includes("network") ||
|
||||
errorMessage.includes("fetch")
|
||||
) {
|
||||
showToast(
|
||||
`Network error while saving API key: ${errorMessage}. Please check your connection.`,
|
||||
'error'
|
||||
);
|
||||
} else if (errorMessage.includes('unauthorized') || errorMessage.includes('forbidden')) {
|
||||
showToast(
|
||||
`Permission error: ${errorMessage}. Please check backend configuration.`,
|
||||
'error'
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
// Show the actual error for unknown issues
|
||||
showToast(
|
||||
`Failed to save API key: ${errorMessage}`,
|
||||
'error'
|
||||
);
|
||||
showToast(`Failed to save API key: ${errorMessage}`, "error");
|
||||
}
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -88,9 +78,9 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
showToast('You can configure your provider in Settings', 'info');
|
||||
showToast("You can configure your provider in Settings", "info");
|
||||
// Mark onboarding as dismissed when skipping
|
||||
localStorage.setItem('onboardingDismissed', 'true');
|
||||
localStorage.setItem("onboardingDismissed", "true");
|
||||
onSkip();
|
||||
};
|
||||
|
||||
@@ -103,21 +93,24 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
|
||||
value={provider}
|
||||
onChange={(e) => setProvider(e.target.value)}
|
||||
options={[
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'google', label: 'Google Gemini' },
|
||||
{ value: 'ollama', label: 'Ollama (Local)' },
|
||||
{ value: "openai", label: "OpenAI" },
|
||||
{ value: "google", label: "Google Gemini" },
|
||||
{ value: "ollama", label: "Ollama (Local)" },
|
||||
]}
|
||||
accentColor="green"
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-zinc-400">
|
||||
{provider === 'openai' && 'OpenAI provides powerful models like GPT-4. You\'ll need an API key from OpenAI.'}
|
||||
{provider === 'google' && 'Google Gemini offers advanced AI capabilities. Configure in Settings after setup.'}
|
||||
{provider === 'ollama' && 'Ollama runs models locally on your machine. Configure in Settings after setup.'}
|
||||
{provider === "openai" &&
|
||||
"OpenAI provides powerful models like GPT-4. You'll need an API key from OpenAI."}
|
||||
{provider === "google" &&
|
||||
"Google Gemini offers advanced AI capabilities. Configure in Settings after setup."}
|
||||
{provider === "ollama" &&
|
||||
"Ollama runs models locally on your machine. Configure in Settings after setup."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* OpenAI API Key Input */}
|
||||
{provider === 'openai' && (
|
||||
{provider === "openai" && (
|
||||
<>
|
||||
<div>
|
||||
<Input
|
||||
@@ -152,10 +145,16 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
|
||||
size="lg"
|
||||
onClick={handleSave}
|
||||
disabled={saving || !apiKey.trim()}
|
||||
icon={saving ? <Loader className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
icon={
|
||||
saving ? (
|
||||
<Loader className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-4 h-4" />
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save & Continue'}
|
||||
{saving ? "Saving..." : "Save & Continue"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -171,15 +170,17 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
|
||||
)}
|
||||
|
||||
{/* Non-OpenAI Provider Message */}
|
||||
{provider !== 'openai' && (
|
||||
{provider !== "openai" && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
{provider === 'google' && 'Google Gemini configuration will be available in Settings after setup.'}
|
||||
{provider === 'ollama' && 'Ollama configuration will be available in Settings after setup. Make sure Ollama is running locally.'}
|
||||
{provider === "google" &&
|
||||
"Google Gemini configuration will be available in Settings after setup."}
|
||||
{provider === "ollama" &&
|
||||
"Ollama configuration will be available in Settings after setup. Make sure Ollama is running locally."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -188,27 +189,30 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
|
||||
// Save the provider selection for non-OpenAI providers
|
||||
try {
|
||||
await credentialsService.updateCredential({
|
||||
key: 'LLM_PROVIDER',
|
||||
key: "LLM_PROVIDER",
|
||||
value: provider,
|
||||
is_encrypted: false,
|
||||
category: 'rag_strategy'
|
||||
category: "rag_strategy",
|
||||
});
|
||||
showToast(`${provider === 'google' ? 'Google Gemini' : 'Ollama'} selected as provider`, 'success');
|
||||
showToast(
|
||||
`${provider === "google" ? "Google Gemini" : "Ollama"} selected as provider`,
|
||||
"success",
|
||||
);
|
||||
// Mark onboarding as dismissed
|
||||
localStorage.setItem('onboardingDismissed', 'true');
|
||||
localStorage.setItem("onboardingDismissed", "true");
|
||||
onSaved();
|
||||
} catch (error) {
|
||||
console.error('Failed to save provider selection:', error);
|
||||
showToast('Failed to save provider selection', 'error');
|
||||
console.error("Failed to save provider selection:", error);
|
||||
showToast("Failed to save provider selection", "error");
|
||||
}
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
Continue with {provider === 'google' ? 'Gemini' : 'Ollama'}
|
||||
Continue with {provider === "google" ? "Gemini" : "Ollama"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -70,23 +70,15 @@ export const TestStatus = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Check for test results availability
|
||||
// Test results availability - not implemented yet
|
||||
useEffect(() => {
|
||||
const checkResults = async () => {
|
||||
const hasTestResults = await testService.hasTestResults();
|
||||
setHasResults(hasTestResults);
|
||||
};
|
||||
checkResults();
|
||||
setHasResults(false);
|
||||
}, []);
|
||||
|
||||
// Check for results when UI tests complete
|
||||
useEffect(() => {
|
||||
if (!uiTest.isRunning && uiTest.exitCode === 0) {
|
||||
// Small delay to ensure files are written
|
||||
setTimeout(async () => {
|
||||
const hasTestResults = await testService.hasTestResults();
|
||||
setHasResults(hasTestResults);
|
||||
}, 2000);
|
||||
setHasResults(false);
|
||||
}
|
||||
}, [uiTest.isRunning, uiTest.exitCode]);
|
||||
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Loader, Settings, ChevronDown, ChevronUp, Palette, Key, Brain, Code, Activity, FileCode, Bug } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useSettings } from '../contexts/SettingsContext';
|
||||
import { useStaggeredEntrance } from '../hooks/useStaggeredEntrance';
|
||||
import { FeaturesSection } from '../components/settings/FeaturesSection';
|
||||
import { APIKeysSection } from '../components/settings/APIKeysSection';
|
||||
import { RAGSettings } from '../components/settings/RAGSettings';
|
||||
import { CodeExtractionSettings } from '../components/settings/CodeExtractionSettings';
|
||||
import { TestStatus } from '../components/settings/TestStatus';
|
||||
import { IDEGlobalRules } from '../components/settings/IDEGlobalRules';
|
||||
import { ButtonPlayground } from '../components/settings/ButtonPlayground';
|
||||
import { CollapsibleSettingsCard } from '../components/ui/CollapsibleSettingsCard';
|
||||
import { BugReportButton } from '../components/bug-report/BugReportButton';
|
||||
import { credentialsService, RagSettings, CodeExtractionSettings as CodeExtractionSettingsType } from '../services/credentialsService';
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Loader,
|
||||
Settings,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Palette,
|
||||
Key,
|
||||
Brain,
|
||||
Code,
|
||||
Activity,
|
||||
FileCode,
|
||||
Bug,
|
||||
} from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useToast } from "../contexts/ToastContext";
|
||||
import { useSettings } from "../contexts/SettingsContext";
|
||||
import { useStaggeredEntrance } from "../hooks/useStaggeredEntrance";
|
||||
import { FeaturesSection } from "../components/settings/FeaturesSection";
|
||||
import { APIKeysSection } from "../components/settings/APIKeysSection";
|
||||
import { RAGSettings } from "../components/settings/RAGSettings";
|
||||
import { CodeExtractionSettings } from "../components/settings/CodeExtractionSettings";
|
||||
import { TestStatus } from "../components/settings/TestStatus";
|
||||
import { IDEGlobalRules } from "../components/settings/IDEGlobalRules";
|
||||
import { ButtonPlayground } from "../components/settings/ButtonPlayground";
|
||||
import { CollapsibleSettingsCard } from "../components/ui/CollapsibleSettingsCard";
|
||||
import { BugReportButton } from "../components/bug-report/BugReportButton";
|
||||
import {
|
||||
credentialsService,
|
||||
RagSettings,
|
||||
CodeExtractionSettings as CodeExtractionSettingsType,
|
||||
} from "../services/credentialsService";
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const [ragSettings, setRagSettings] = useState<RagSettings>({
|
||||
@@ -22,56 +38,56 @@ export const SettingsPage = () => {
|
||||
USE_HYBRID_SEARCH: true,
|
||||
USE_AGENTIC_RAG: true,
|
||||
USE_RERANKING: true,
|
||||
MODEL_CHOICE: 'gpt-4.1-nano'
|
||||
});
|
||||
const [codeExtractionSettings, setCodeExtractionSettings] = useState<CodeExtractionSettingsType>({
|
||||
MIN_CODE_BLOCK_LENGTH: 250,
|
||||
MAX_CODE_BLOCK_LENGTH: 5000,
|
||||
ENABLE_COMPLETE_BLOCK_DETECTION: true,
|
||||
ENABLE_LANGUAGE_SPECIFIC_PATTERNS: true,
|
||||
ENABLE_PROSE_FILTERING: true,
|
||||
MAX_PROSE_RATIO: 0.15,
|
||||
MIN_CODE_INDICATORS: 3,
|
||||
ENABLE_DIAGRAM_FILTERING: true,
|
||||
ENABLE_CONTEXTUAL_LENGTH: true,
|
||||
CODE_EXTRACTION_MAX_WORKERS: 3,
|
||||
CONTEXT_WINDOW_SIZE: 1000,
|
||||
ENABLE_CODE_SUMMARIES: true
|
||||
MODEL_CHOICE: "gpt-4.1-nano",
|
||||
});
|
||||
const [codeExtractionSettings, setCodeExtractionSettings] =
|
||||
useState<CodeExtractionSettingsType>({
|
||||
MIN_CODE_BLOCK_LENGTH: 250,
|
||||
MAX_CODE_BLOCK_LENGTH: 5000,
|
||||
ENABLE_COMPLETE_BLOCK_DETECTION: true,
|
||||
ENABLE_LANGUAGE_SPECIFIC_PATTERNS: true,
|
||||
ENABLE_PROSE_FILTERING: true,
|
||||
MAX_PROSE_RATIO: 0.15,
|
||||
MIN_CODE_INDICATORS: 3,
|
||||
ENABLE_DIAGRAM_FILTERING: true,
|
||||
ENABLE_CONTEXTUAL_LENGTH: true,
|
||||
CODE_EXTRACTION_MAX_WORKERS: 3,
|
||||
CONTEXT_WINDOW_SIZE: 1000,
|
||||
ENABLE_CODE_SUMMARIES: true,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showButtonPlayground, setShowButtonPlayground] = useState(false);
|
||||
|
||||
const { showToast } = useToast();
|
||||
const { projectsEnabled } = useSettings();
|
||||
|
||||
|
||||
// Use staggered entrance animation
|
||||
const { isVisible, containerVariants, itemVariants, titleVariants } = useStaggeredEntrance(
|
||||
[1, 2, 3, 4],
|
||||
0.15
|
||||
);
|
||||
const { isVisible, containerVariants, itemVariants, titleVariants } =
|
||||
useStaggeredEntrance([1, 2, 3, 4], 0.15);
|
||||
|
||||
// Load settings on mount
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const loadSettings = async () => {
|
||||
const loadSettings = async (isRetry = false) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
||||
// Load RAG settings
|
||||
const ragSettingsData = await credentialsService.getRagSettings();
|
||||
setRagSettings(ragSettingsData);
|
||||
|
||||
|
||||
// Load Code Extraction settings
|
||||
const codeExtractionSettingsData = await credentialsService.getCodeExtractionSettings();
|
||||
const codeExtractionSettingsData =
|
||||
await credentialsService.getCodeExtractionSettings();
|
||||
setCodeExtractionSettings(codeExtractionSettingsData);
|
||||
} catch (err) {
|
||||
setError('Failed to load settings');
|
||||
setError("Failed to load settings");
|
||||
console.error(err);
|
||||
showToast('Failed to load settings', 'error');
|
||||
showToast("Failed to load settings", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -88,12 +104,15 @@ export const SettingsPage = () => {
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={isVisible ? 'visible' : 'hidden'}
|
||||
animate={isVisible ? "visible" : "hidden"}
|
||||
variants={containerVariants}
|
||||
className="w-full"
|
||||
>
|
||||
{/* Header */}
|
||||
<motion.div className="flex justify-between items-center mb-8" variants={itemVariants}>
|
||||
<motion.div
|
||||
className="flex justify-between items-center mb-8"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<motion.h1
|
||||
className="text-3xl font-bold text-gray-800 dark:text-white flex items-center gap-3"
|
||||
variants={titleVariants}
|
||||
@@ -103,6 +122,7 @@ export const SettingsPage = () => {
|
||||
</motion.h1>
|
||||
</motion.div>
|
||||
|
||||
|
||||
{/* Main content with two-column layout */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left Column */}
|
||||
@@ -165,7 +185,10 @@ export const SettingsPage = () => {
|
||||
storageKey="rag-settings"
|
||||
defaultExpanded={true}
|
||||
>
|
||||
<RAGSettings ragSettings={ragSettings} setRagSettings={setRagSettings} />
|
||||
<RAGSettings
|
||||
ragSettings={ragSettings}
|
||||
setRagSettings={setRagSettings}
|
||||
/>
|
||||
</CollapsibleSettingsCard>
|
||||
</motion.div>
|
||||
<motion.div variants={itemVariants}>
|
||||
@@ -176,9 +199,9 @@ export const SettingsPage = () => {
|
||||
storageKey="code-extraction"
|
||||
defaultExpanded={true}
|
||||
>
|
||||
<CodeExtractionSettings
|
||||
codeExtractionSettings={codeExtractionSettings}
|
||||
setCodeExtractionSettings={setCodeExtractionSettings}
|
||||
<CodeExtractionSettings
|
||||
codeExtractionSettings={codeExtractionSettings}
|
||||
setCodeExtractionSettings={setCodeExtractionSettings}
|
||||
/>
|
||||
</CollapsibleSettingsCard>
|
||||
</motion.div>
|
||||
@@ -194,7 +217,8 @@ export const SettingsPage = () => {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Found a bug or issue? Report it to help improve Archon V2 Alpha.
|
||||
Found a bug or issue? Report it to help improve Archon V2
|
||||
Alpha.
|
||||
</p>
|
||||
<div className="flex justify-start">
|
||||
<BugReportButton variant="secondary" size="md">
|
||||
@@ -234,7 +258,7 @@ export const SettingsPage = () => {
|
||||
{showButtonPlayground && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="overflow-hidden"
|
||||
@@ -257,4 +281,4 @@ export const SettingsPage = () => {
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -53,56 +53,81 @@ export interface CodeExtractionSettings {
|
||||
ENABLE_CODE_SUMMARIES: boolean;
|
||||
}
|
||||
|
||||
import { getApiUrl } from '../config/api';
|
||||
import { getApiUrl } from "../config/api";
|
||||
|
||||
class CredentialsService {
|
||||
private baseUrl = getApiUrl();
|
||||
|
||||
private handleCredentialError(error: any, context: string): Error {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Check for network errors
|
||||
if (
|
||||
errorMessage.toLowerCase().includes("network") ||
|
||||
errorMessage.includes("fetch") ||
|
||||
errorMessage.includes("Failed to fetch")
|
||||
) {
|
||||
return new Error(
|
||||
`Network error while ${context.toLowerCase()}: ${errorMessage}. ` +
|
||||
`Please check your connection and server status.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Return original error with context
|
||||
return new Error(`${context} failed: ${errorMessage}`);
|
||||
}
|
||||
|
||||
async getAllCredentials(): Promise<Credential[]> {
|
||||
const response = await fetch(`${this.baseUrl}/api/credentials`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch credentials');
|
||||
throw new Error("Failed to fetch credentials");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getCredentialsByCategory(category: string): Promise<Credential[]> {
|
||||
const response = await fetch(`${this.baseUrl}/api/credentials/categories/${category}`);
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/api/credentials/categories/${category}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch credentials for category: ${category}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
// The API returns {credentials: {...}} where credentials is a dict
|
||||
// Convert to array format expected by frontend
|
||||
if (result.credentials && typeof result.credentials === 'object') {
|
||||
return Object.entries(result.credentials).map(([key, value]: [string, any]) => {
|
||||
if (value && typeof value === 'object' && value.is_encrypted) {
|
||||
return {
|
||||
key,
|
||||
value: undefined,
|
||||
encrypted_value: value.encrypted_value,
|
||||
is_encrypted: true,
|
||||
category,
|
||||
description: value.description
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
key,
|
||||
value: value,
|
||||
encrypted_value: undefined,
|
||||
is_encrypted: false,
|
||||
category,
|
||||
description: ''
|
||||
};
|
||||
}
|
||||
});
|
||||
if (result.credentials && typeof result.credentials === "object") {
|
||||
return Object.entries(result.credentials).map(
|
||||
([key, value]: [string, any]) => {
|
||||
if (value && typeof value === "object" && value.is_encrypted) {
|
||||
return {
|
||||
key,
|
||||
value: undefined,
|
||||
encrypted_value: value.encrypted_value,
|
||||
is_encrypted: true,
|
||||
category,
|
||||
description: value.description,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
key,
|
||||
value: value,
|
||||
encrypted_value: undefined,
|
||||
is_encrypted: false,
|
||||
category,
|
||||
description: "",
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async getCredential(key: string): Promise<{ key: string; value?: string; is_encrypted?: boolean }> {
|
||||
async getCredential(
|
||||
key: string,
|
||||
): Promise<{ key: string; value?: string; is_encrypted?: boolean }> {
|
||||
const response = await fetch(`${this.baseUrl}/api/credentials/${key}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
@@ -115,24 +140,24 @@ class CredentialsService {
|
||||
}
|
||||
|
||||
async getRagSettings(): Promise<RagSettings> {
|
||||
const ragCredentials = await this.getCredentialsByCategory('rag_strategy');
|
||||
const apiKeysCredentials = await this.getCredentialsByCategory('api_keys');
|
||||
|
||||
const ragCredentials = await this.getCredentialsByCategory("rag_strategy");
|
||||
const apiKeysCredentials = await this.getCredentialsByCategory("api_keys");
|
||||
|
||||
const settings: RagSettings = {
|
||||
USE_CONTEXTUAL_EMBEDDINGS: false,
|
||||
CONTEXTUAL_EMBEDDINGS_MAX_WORKERS: 3,
|
||||
USE_HYBRID_SEARCH: true,
|
||||
USE_AGENTIC_RAG: true,
|
||||
USE_RERANKING: true,
|
||||
MODEL_CHOICE: 'gpt-4.1-nano',
|
||||
LLM_PROVIDER: 'openai',
|
||||
LLM_BASE_URL: '',
|
||||
EMBEDDING_MODEL: '',
|
||||
MODEL_CHOICE: "gpt-4.1-nano",
|
||||
LLM_PROVIDER: "openai",
|
||||
LLM_BASE_URL: "",
|
||||
EMBEDDING_MODEL: "",
|
||||
// Crawling Performance Settings defaults
|
||||
CRAWL_BATCH_SIZE: 50,
|
||||
CRAWL_MAX_CONCURRENT: 10,
|
||||
CRAWL_WAIT_STRATEGY: 'domcontentloaded',
|
||||
CRAWL_PAGE_TIMEOUT: 60000, // Increased from 30s to 60s for documentation sites
|
||||
CRAWL_WAIT_STRATEGY: "domcontentloaded",
|
||||
CRAWL_PAGE_TIMEOUT: 60000, // Increased from 30s to 60s for documentation sites
|
||||
CRAWL_DELAY_BEFORE_HTML: 0.5,
|
||||
// Storage Performance Settings defaults
|
||||
DOCUMENT_STORAGE_BATCH_SIZE: 50,
|
||||
@@ -143,30 +168,50 @@ class CredentialsService {
|
||||
MEMORY_THRESHOLD_PERCENT: 80,
|
||||
DISPATCHER_CHECK_INTERVAL: 30,
|
||||
CODE_EXTRACTION_BATCH_SIZE: 50,
|
||||
CODE_SUMMARY_MAX_WORKERS: 3
|
||||
CODE_SUMMARY_MAX_WORKERS: 3,
|
||||
};
|
||||
|
||||
// Map credentials to settings
|
||||
[...ragCredentials, ...apiKeysCredentials].forEach(cred => {
|
||||
[...ragCredentials, ...apiKeysCredentials].forEach((cred) => {
|
||||
if (cred.key in settings) {
|
||||
// String fields
|
||||
if (['MODEL_CHOICE', 'LLM_PROVIDER', 'LLM_BASE_URL', 'EMBEDDING_MODEL', 'CRAWL_WAIT_STRATEGY'].includes(cred.key)) {
|
||||
(settings as any)[cred.key] = cred.value || '';
|
||||
}
|
||||
if (
|
||||
[
|
||||
"MODEL_CHOICE",
|
||||
"LLM_PROVIDER",
|
||||
"LLM_BASE_URL",
|
||||
"EMBEDDING_MODEL",
|
||||
"CRAWL_WAIT_STRATEGY",
|
||||
].includes(cred.key)
|
||||
) {
|
||||
(settings as any)[cred.key] = cred.value || "";
|
||||
}
|
||||
// Number fields
|
||||
else if (['CONTEXTUAL_EMBEDDINGS_MAX_WORKERS', 'CRAWL_BATCH_SIZE', 'CRAWL_MAX_CONCURRENT',
|
||||
'CRAWL_PAGE_TIMEOUT', 'DOCUMENT_STORAGE_BATCH_SIZE', 'EMBEDDING_BATCH_SIZE',
|
||||
'DELETE_BATCH_SIZE', 'MEMORY_THRESHOLD_PERCENT', 'DISPATCHER_CHECK_INTERVAL',
|
||||
'CODE_EXTRACTION_BATCH_SIZE', 'CODE_SUMMARY_MAX_WORKERS'].includes(cred.key)) {
|
||||
(settings as any)[cred.key] = parseInt(cred.value || '0', 10) || (settings as any)[cred.key];
|
||||
else if (
|
||||
[
|
||||
"CONTEXTUAL_EMBEDDINGS_MAX_WORKERS",
|
||||
"CRAWL_BATCH_SIZE",
|
||||
"CRAWL_MAX_CONCURRENT",
|
||||
"CRAWL_PAGE_TIMEOUT",
|
||||
"DOCUMENT_STORAGE_BATCH_SIZE",
|
||||
"EMBEDDING_BATCH_SIZE",
|
||||
"DELETE_BATCH_SIZE",
|
||||
"MEMORY_THRESHOLD_PERCENT",
|
||||
"DISPATCHER_CHECK_INTERVAL",
|
||||
"CODE_EXTRACTION_BATCH_SIZE",
|
||||
"CODE_SUMMARY_MAX_WORKERS",
|
||||
].includes(cred.key)
|
||||
) {
|
||||
(settings as any)[cred.key] =
|
||||
parseInt(cred.value || "0", 10) || (settings as any)[cred.key];
|
||||
}
|
||||
// Float fields
|
||||
else if (cred.key === 'CRAWL_DELAY_BEFORE_HTML') {
|
||||
settings[cred.key] = parseFloat(cred.value || '0.5') || 0.5;
|
||||
else if (cred.key === "CRAWL_DELAY_BEFORE_HTML") {
|
||||
settings[cred.key] = parseFloat(cred.value || "0.5") || 0.5;
|
||||
}
|
||||
// Boolean fields
|
||||
else {
|
||||
(settings as any)[cred.key] = cred.value === 'true';
|
||||
(settings as any)[cred.key] = cred.value === "true";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -175,71 +220,96 @@ class CredentialsService {
|
||||
}
|
||||
|
||||
async updateCredential(credential: Credential): Promise<Credential> {
|
||||
const response = await fetch(`${this.baseUrl}/api/credentials/${credential.key}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credential),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update credential');
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/api/credentials/${credential.key}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(credential),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
throw this.handleCredentialError(
|
||||
error,
|
||||
`Updating credential '${credential.key}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createCredential(credential: Credential): Promise<Credential> {
|
||||
const response = await fetch(`${this.baseUrl}/api/credentials`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credential),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create credential');
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/credentials`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(credential),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
throw this.handleCredentialError(
|
||||
error,
|
||||
`Creating credential '${credential.key}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async deleteCredential(key: string): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/api/credentials/${key}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete credential');
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/credentials/${key}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw this.handleCredentialError(error, `Deleting credential '${key}'`);
|
||||
}
|
||||
}
|
||||
|
||||
async updateRagSettings(settings: RagSettings): Promise<void> {
|
||||
const promises = [];
|
||||
|
||||
|
||||
// Update all RAG strategy settings
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
// Skip undefined values
|
||||
if (value === undefined) continue;
|
||||
|
||||
|
||||
promises.push(
|
||||
this.updateCredential({
|
||||
key,
|
||||
value: value.toString(),
|
||||
is_encrypted: false,
|
||||
category: 'rag_strategy',
|
||||
})
|
||||
category: "rag_strategy",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async getCodeExtractionSettings(): Promise<CodeExtractionSettings> {
|
||||
const codeExtractionCredentials = await this.getCredentialsByCategory('code_extraction');
|
||||
|
||||
const codeExtractionCredentials =
|
||||
await this.getCredentialsByCategory("code_extraction");
|
||||
|
||||
const settings: CodeExtractionSettings = {
|
||||
MIN_CODE_BLOCK_LENGTH: 250,
|
||||
MAX_CODE_BLOCK_LENGTH: 5000,
|
||||
@@ -252,21 +322,24 @@ class CredentialsService {
|
||||
ENABLE_CONTEXTUAL_LENGTH: true,
|
||||
CODE_EXTRACTION_MAX_WORKERS: 3,
|
||||
CONTEXT_WINDOW_SIZE: 1000,
|
||||
ENABLE_CODE_SUMMARIES: true
|
||||
ENABLE_CODE_SUMMARIES: true,
|
||||
};
|
||||
|
||||
// Map credentials to settings
|
||||
codeExtractionCredentials.forEach(cred => {
|
||||
codeExtractionCredentials.forEach((cred) => {
|
||||
if (cred.key in settings) {
|
||||
const key = cred.key as keyof CodeExtractionSettings;
|
||||
if (typeof settings[key] === 'number') {
|
||||
if (key === 'MAX_PROSE_RATIO') {
|
||||
settings[key] = parseFloat(cred.value || '0.15');
|
||||
if (typeof settings[key] === "number") {
|
||||
if (key === "MAX_PROSE_RATIO") {
|
||||
settings[key] = parseFloat(cred.value || "0.15");
|
||||
} else {
|
||||
settings[key] = parseInt(cred.value || settings[key].toString(), 10);
|
||||
settings[key] = parseInt(
|
||||
cred.value || settings[key].toString(),
|
||||
10,
|
||||
);
|
||||
}
|
||||
} else if (typeof settings[key] === 'boolean') {
|
||||
settings[key] = cred.value === 'true';
|
||||
} else if (typeof settings[key] === "boolean") {
|
||||
settings[key] = cred.value === "true";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -274,9 +347,11 @@ class CredentialsService {
|
||||
return settings;
|
||||
}
|
||||
|
||||
async updateCodeExtractionSettings(settings: CodeExtractionSettings): Promise<void> {
|
||||
async updateCodeExtractionSettings(
|
||||
settings: CodeExtractionSettings,
|
||||
): Promise<void> {
|
||||
const promises = [];
|
||||
|
||||
|
||||
// Update all code extraction settings
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
promises.push(
|
||||
@@ -284,13 +359,13 @@ class CredentialsService {
|
||||
key,
|
||||
value: value.toString(),
|
||||
is_encrypted: false,
|
||||
category: 'code_extraction',
|
||||
})
|
||||
category: "code_extraction",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
export const credentialsService = new CredentialsService();
|
||||
export const credentialsService = new CredentialsService();
|
||||
|
||||
@@ -242,18 +242,6 @@ class TestService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if test results are available
|
||||
*/
|
||||
async hasTestResults(): Promise<boolean> {
|
||||
try {
|
||||
// Check for latest test results via API
|
||||
const response = await fetch(`${API_BASE_URL}/api/tests/latest-results`);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coverage data for Test Results Modal from new API endpoints with fallback
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { describe, test, expect, vi } from 'vitest'
|
||||
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import React from 'react'
|
||||
import { credentialsService } from '../src/services/credentialsService'
|
||||
|
||||
describe('Error Handling Tests', () => {
|
||||
test('api error simulation', () => {
|
||||
@@ -196,4 +197,40 @@ describe('Error Handling Tests', () => {
|
||||
fireEvent.click(screen.getByText('500 Error'))
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong on our end')
|
||||
})
|
||||
})
|
||||
|
||||
describe('CredentialsService Error Handling', () => {
|
||||
const originalFetch = global.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
global.fetch = vi.fn() as any
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch
|
||||
})
|
||||
|
||||
test('should handle network errors with context', async () => {
|
||||
const mockError = new Error('Network request failed')
|
||||
;(global.fetch as any).mockRejectedValueOnce(mockError)
|
||||
|
||||
await expect(credentialsService.createCredential({
|
||||
key: 'TEST_KEY',
|
||||
value: 'test',
|
||||
is_encrypted: false,
|
||||
category: 'test'
|
||||
})).rejects.toThrow(/Network error while creating credential 'test_key'/)
|
||||
})
|
||||
|
||||
test('should preserve context in error messages', async () => {
|
||||
const mockError = new Error('database error')
|
||||
;(global.fetch as any).mockRejectedValueOnce(mockError)
|
||||
|
||||
await expect(credentialsService.updateCredential({
|
||||
key: 'OPENAI_API_KEY',
|
||||
value: 'sk-test',
|
||||
is_encrypted: true,
|
||||
category: 'api_keys'
|
||||
})).rejects.toThrow(/Updating credential 'OPENAI_API_KEY' failed/)
|
||||
})
|
||||
})
|
||||
@@ -74,7 +74,7 @@ describe('Onboarding Detection Tests', () => {
|
||||
{ key: 'LLM_PROVIDER', value: 'openai', category: 'rag_strategy' }
|
||||
]
|
||||
const apiKeyCreds: NormalizedCredential[] = [
|
||||
{ key: 'OPENAI_API_KEY', is_encrypted: true, category: 'api_keys' }
|
||||
{ key: 'OPENAI_API_KEY', is_encrypted: true, encrypted_value: 'encrypted_sk-test123', category: 'api_keys' }
|
||||
]
|
||||
|
||||
expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true)
|
||||
|
||||
@@ -2,6 +2,9 @@ import { expect, afterEach, vi } from 'vitest'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
// Set required environment variables for tests
|
||||
process.env.ARCHON_SERVER_PORT = '8181'
|
||||
|
||||
// Clean up after each test
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
@@ -15,7 +18,7 @@ global.fetch = vi.fn(() =>
|
||||
text: () => Promise.resolve(''),
|
||||
status: 200,
|
||||
} as Response)
|
||||
)
|
||||
) as any
|
||||
|
||||
// Mock WebSocket
|
||||
class MockWebSocket {
|
||||
|
||||
Reference in New Issue
Block a user