refactor: move from next js to vite and tanstack router

This commit is contained in:
Kacper
2025-12-17 20:11:16 +01:00
parent 9954feafd8
commit 5136c32b68
263 changed files with 11148 additions and 10276 deletions

View File

@@ -0,0 +1,117 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AlertCircle, CheckCircle2, Eye, EyeOff, Loader2, Zap } from "lucide-react";
import type { ProviderConfig } from "@/config/api-providers";
interface ApiKeyFieldProps {
config: ProviderConfig;
}
export function ApiKeyField({ config }: ApiKeyFieldProps) {
const {
label,
inputId,
placeholder,
value,
setValue,
showValue,
setShowValue,
hasStoredKey,
inputTestId,
toggleTestId,
testButton,
result,
resultTestId,
resultMessageTestId,
descriptionPrefix,
descriptionLinkHref,
descriptionLinkText,
descriptionSuffix,
} = config;
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label htmlFor={inputId} className="text-foreground">
{label}
</Label>
{hasStoredKey && <CheckCircle2 className="w-4 h-4 text-brand-500" />}
</div>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id={inputId}
type={showValue ? "text" : "password"}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground"
data-testid={inputTestId}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground hover:bg-transparent"
onClick={() => setShowValue(!showValue)}
data-testid={toggleTestId}
>
{showValue ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</Button>
</div>
<Button
type="button"
variant="secondary"
onClick={testButton.onClick}
disabled={testButton.disabled}
className="bg-secondary hover:bg-accent text-secondary-foreground border border-border"
data-testid={testButton.testId}
>
{testButton.loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Testing...
</>
) : (
<>
<Zap className="w-4 h-4 mr-2" />
Test
</>
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
{descriptionPrefix}{" "}
<a
href={descriptionLinkHref}
target="_blank"
rel="noopener noreferrer"
className="text-brand-500 hover:text-brand-400 hover:underline"
>
{descriptionLinkText}
</a>
{descriptionSuffix}
</p>
{result && (
<div
className={`flex items-center gap-2 p-3 rounded-lg ${
result.success
? "bg-green-500/10 border border-green-500/20 text-green-400"
: "bg-red-500/10 border border-red-500/20 text-red-400"
}`}
data-testid={resultTestId}
>
{result.success ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<AlertCircle className="w-4 h-4" />
)}
<span className="text-sm" data-testid={resultMessageTestId}>
{result.message}
</span>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,153 @@
import { useAppStore } from "@/store/app-store";
import { useSetupStore } from "@/store/setup-store";
import { Button } from "@/components/ui/button";
import { Key, CheckCircle2, Settings, Trash2, Loader2 } from "lucide-react";
import { ApiKeyField } from "./api-key-field";
import { buildProviderConfigs } from "@/config/api-providers";
import { AuthenticationStatusDisplay } from "./authentication-status-display";
import { SecurityNotice } from "./security-notice";
import { useApiKeyManagement } from "./hooks/use-api-key-management";
import { cn } from "@/lib/utils";
import { useState, useCallback } from "react";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
import { useNavigate } from "@tanstack/react-router";
export function ApiKeysSection() {
const { apiKeys, setApiKeys } = useAppStore();
const { claudeAuthStatus, setClaudeAuthStatus, setSetupComplete } = useSetupStore();
const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false);
const navigate = useNavigate();
const { providerConfigParams, apiKeyStatus, handleSave, saved } =
useApiKeyManagement();
const providerConfigs = buildProviderConfigs(providerConfigParams);
// Delete Anthropic API key
const deleteAnthropicKey = useCallback(async () => {
setIsDeletingAnthropicKey(true);
try {
const api = getElectronAPI();
if (!api.setup?.deleteApiKey) {
toast.error("Delete API not available");
return;
}
const result = await api.setup.deleteApiKey("anthropic");
if (result.success) {
setApiKeys({ ...apiKeys, anthropic: "" });
setClaudeAuthStatus({
authenticated: false,
method: "none",
hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false,
});
toast.success("Anthropic API key deleted");
} else {
toast.error(result.error || "Failed to delete API key");
}
} catch (error) {
toast.error("Failed to delete API key");
} finally {
setIsDeletingAnthropicKey(false);
}
}, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]);
// Open setup wizard
const openSetupWizard = useCallback(() => {
setSetupComplete(false);
navigate({ to: "/setup" });
}, [setSetupComplete, navigate]);
return (
<div
className={cn(
"rounded-2xl overflow-hidden",
"border border-border/50",
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
"shadow-sm shadow-black/5"
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<Key className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">API Keys</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Configure your AI provider API keys. Keys are stored locally in your browser.
</p>
</div>
<div className="p-6 space-y-6">
{/* API Key Fields */}
{providerConfigs.map((provider) => (
<ApiKeyField key={provider.key} config={provider} />
))}
{/* Authentication Status Display */}
<AuthenticationStatusDisplay
claudeAuthStatus={claudeAuthStatus}
apiKeyStatus={apiKeyStatus}
apiKeys={apiKeys}
/>
{/* Security Notice */}
<SecurityNotice />
{/* Action Buttons */}
<div className="flex flex-wrap items-center gap-3 pt-2">
<Button
onClick={handleSave}
data-testid="save-settings"
className={cn(
"min-w-[140px] h-10",
"bg-gradient-to-r from-brand-500 to-brand-600",
"hover:from-brand-600 hover:to-brand-600",
"text-white font-medium border-0",
"shadow-md shadow-brand-500/20 hover:shadow-lg hover:shadow-brand-500/25",
"transition-all duration-200 ease-out",
"hover:scale-[1.02] active:scale-[0.98]"
)}
>
{saved ? (
<>
<CheckCircle2 className="w-4 h-4 mr-2" />
Saved!
</>
) : (
"Save API Keys"
)}
</Button>
<Button
onClick={openSetupWizard}
variant="outline"
className="h-10 border-border"
data-testid="run-setup-wizard"
>
<Settings className="w-4 h-4 mr-2" />
Run Setup Wizard
</Button>
{apiKeys.anthropic && (
<Button
onClick={deleteAnthropicKey}
disabled={isDeletingAnthropicKey}
variant="outline"
className="h-10 border-red-500/30 text-red-500 hover:bg-red-500/10 hover:border-red-500/50"
data-testid="delete-anthropic-key"
>
{isDeletingAnthropicKey ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Trash2 className="w-4 h-4 mr-2" />
)}
Delete Anthropic Key
</Button>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,95 @@
import { Label } from "@/components/ui/label";
import {
CheckCircle2,
AlertCircle,
Info,
Terminal,
Sparkles,
} from "lucide-react";
import type { ClaudeAuthStatus } from "@/store/setup-store";
interface AuthenticationStatusDisplayProps {
claudeAuthStatus: ClaudeAuthStatus | null;
apiKeyStatus: {
hasAnthropicKey: boolean;
hasGoogleKey: boolean;
} | null;
apiKeys: {
anthropic: string;
google: string;
};
}
export function AuthenticationStatusDisplay({
claudeAuthStatus,
apiKeyStatus,
apiKeys,
}: AuthenticationStatusDisplayProps) {
return (
<div className="space-y-4 pt-4 border-t border-border">
<div className="flex items-center gap-2 mb-3">
<Info className="w-4 h-4 text-brand-500" />
<Label className="text-foreground font-semibold">
Current Authentication Configuration
</Label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Claude Authentication Status */}
<div className="p-3 rounded-lg bg-card border border-border">
<div className="flex items-center gap-2 mb-1.5">
<Terminal className="w-4 h-4 text-brand-500" />
<span className="text-sm font-medium text-foreground">
Claude (Anthropic)
</span>
</div>
<div className="space-y-1.5 text-xs min-h-12">
{claudeAuthStatus?.authenticated ? (
<>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
<span className="text-green-400 font-medium">
Authenticated
</span>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<Info className="w-3 h-3 shrink-0" />
<span>
{claudeAuthStatus.method === "oauth_token"
? "Using stored OAuth token (subscription)"
: claudeAuthStatus.method === "api_key_env"
? "Using ANTHROPIC_API_KEY"
: claudeAuthStatus.method === "api_key"
? "Using stored API key"
: claudeAuthStatus.method === "credentials_file"
? "Using credentials file"
: claudeAuthStatus.method === "cli_authenticated"
? "Using Claude CLI authentication"
: `Using ${
claudeAuthStatus.method || "detected"
} authentication`}
</span>
</div>
</>
) : apiKeyStatus?.hasAnthropicKey ? (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Using environment variable (ANTHROPIC_API_KEY)</span>
</div>
) : apiKeys.anthropic ? (
<div className="flex items-center gap-2 text-blue-400">
<Info className="w-3 h-3 shrink-0" />
<span>Using manual API key from settings</span>
</div>
) : (
<div className="flex items-center gap-1.5 text-yellow-500 py-0.5">
<AlertCircle className="w-3 h-3 shrink-0" />
<span className="text-xs">Not configured</span>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,190 @@
import { useState, useEffect } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import type { ProviderConfigParams } from "@/config/api-providers";
interface TestResult {
success: boolean;
message: string;
}
interface ApiKeyStatus {
hasAnthropicKey: boolean;
hasGoogleKey: boolean;
}
/**
* Custom hook for managing API key state and operations
* Handles input values, visibility toggles, connection testing, and saving
*/
export function useApiKeyManagement() {
const { apiKeys, setApiKeys } = useAppStore();
// API key values
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
const [googleKey, setGoogleKey] = useState(apiKeys.google);
// Visibility toggles
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
const [showGoogleKey, setShowGoogleKey] = useState(false);
// Test connection states
const [testingConnection, setTestingConnection] = useState(false);
const [testResult, setTestResult] = useState<TestResult | null>(null);
const [testingGeminiConnection, setTestingGeminiConnection] = useState(false);
const [geminiTestResult, setGeminiTestResult] = useState<TestResult | null>(
null
);
// API key status from environment
const [apiKeyStatus, setApiKeyStatus] = useState<ApiKeyStatus | null>(null);
// Save state
const [saved, setSaved] = useState(false);
// Sync local state with store
useEffect(() => {
setAnthropicKey(apiKeys.anthropic);
setGoogleKey(apiKeys.google);
}, [apiKeys]);
// Check API key status from environment on mount
useEffect(() => {
const checkApiKeyStatus = async () => {
const api = getElectronAPI();
if (api?.setup?.getApiKeys) {
try {
const status = await api.setup.getApiKeys();
if (status.success) {
setApiKeyStatus({
hasAnthropicKey: status.hasAnthropicKey,
hasGoogleKey: status.hasGoogleKey,
});
}
} catch (error) {
console.error("Failed to check API key status:", error);
}
}
};
checkApiKeyStatus();
}, []);
// Test Anthropic/Claude connection
const handleTestAnthropicConnection = async () => {
setTestingConnection(true);
setTestResult(null);
try {
const response = await fetch("/api/claude/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ apiKey: anthropicKey }),
});
const data = await response.json();
if (response.ok && data.success) {
setTestResult({
success: true,
message: data.message || "Connection successful! Claude responded.",
});
} else {
setTestResult({
success: false,
message: data.error || "Failed to connect to Claude API.",
});
}
} catch {
setTestResult({
success: false,
message: "Network error. Please check your connection.",
});
} finally {
setTestingConnection(false);
}
};
// Test Google/Gemini connection
const handleTestGeminiConnection = async () => {
setTestingGeminiConnection(true);
setGeminiTestResult(null);
try {
const response = await fetch("/api/gemini/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ apiKey: googleKey }),
});
const data = await response.json();
if (response.ok && data.success) {
setGeminiTestResult({
success: true,
message: data.message || "Connection successful! Gemini responded.",
});
} else {
setGeminiTestResult({
success: false,
message: data.error || "Failed to connect to Gemini API.",
});
}
} catch {
setGeminiTestResult({
success: false,
message: "Network error. Please check your connection.",
});
} finally {
setTestingGeminiConnection(false);
}
};
// Save API keys
const handleSave = () => {
setApiKeys({
anthropic: anthropicKey,
google: googleKey,
});
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
// Build provider config params for buildProviderConfigs
const providerConfigParams: ProviderConfigParams = {
apiKeys,
anthropic: {
value: anthropicKey,
setValue: setAnthropicKey,
show: showAnthropicKey,
setShow: setShowAnthropicKey,
testing: testingConnection,
onTest: handleTestAnthropicConnection,
result: testResult,
},
google: {
value: googleKey,
setValue: setGoogleKey,
show: showGoogleKey,
setShow: setShowGoogleKey,
testing: testingGeminiConnection,
onTest: handleTestGeminiConnection,
result: geminiTestResult,
},
};
return {
// Provider config params for buildProviderConfigs
providerConfigParams,
// API key status from environment
apiKeyStatus,
// Save handler and state
handleSave,
saved,
};
}

View File

@@ -0,0 +1,21 @@
import { AlertCircle } from "lucide-react";
interface SecurityNoticeProps {
title?: string;
message?: string;
}
export function SecurityNotice({
title = "Security Notice",
message = "API keys are stored in your browser's local storage. Never share your API keys or commit them to version control.",
}: SecurityNoticeProps) {
return (
<div className="flex items-start gap-3 p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
<div className="text-sm">
<p className="font-medium text-yellow-500">{title}</p>
<p className="text-yellow-500/80 text-xs mt-1">{message}</p>
</div>
</div>
);
}