Files
claudia/src/components/ClaudeVersionSelector.tsx
2025-08-07 12:28:47 +08:00

279 lines
10 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { api, type ClaudeInstallation } from "@/lib/api";
import { cn } from "@/lib/utils";
import { CheckCircle, HardDrive, Settings } from "lucide-react";
import { useTranslation } from "@/hooks/useTranslation";
interface ClaudeVersionSelectorProps {
/**
* Currently selected installation path
*/
selectedPath?: string | null;
/**
* Callback when an installation is selected
*/
onSelect: (installation: ClaudeInstallation) => void;
/**
* Optional className for styling
*/
className?: string;
/**
* Whether to show the save button
*/
showSaveButton?: boolean;
/**
* Callback when save is clicked
*/
onSave?: () => void;
/**
* Whether save is in progress
*/
isSaving?: boolean;
}
/**
* ClaudeVersionSelector component for selecting Claude Code installations
* Supports system installations and user preferences
*
* @example
* <ClaudeVersionSelector
* selectedPath={currentPath}
* onSelect={(installation) => setSelectedInstallation(installation)}
* />
*/
export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
selectedPath,
onSelect,
className,
showSaveButton = false,
onSave,
isSaving = false,
}) => {
const { t } = useTranslation();
const [installations, setInstallations] = useState<ClaudeInstallation[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedInstallation, setSelectedInstallation] = useState<ClaudeInstallation | null>(null);
useEffect(() => {
loadInstallations();
}, []);
useEffect(() => {
// Update selected installation when selectedPath changes
if (selectedPath && installations.length > 0) {
const found = installations.find(i => i.path === selectedPath);
if (found) {
setSelectedInstallation(found);
}
}
}, [selectedPath, installations]);
const loadInstallations = async () => {
try {
setLoading(true);
setError(null);
const foundInstallations = await api.listClaudeInstallations();
setInstallations(foundInstallations);
// If we have a selected path, find and select it
if (selectedPath) {
const found = foundInstallations.find(i => i.path === selectedPath);
if (found) {
setSelectedInstallation(found);
}
} else if (foundInstallations.length > 0) {
// Auto-select the first (best) installation
setSelectedInstallation(foundInstallations[0]);
onSelect(foundInstallations[0]);
}
} catch (err) {
console.error("Failed to load Claude installations:", err);
setError(err instanceof Error ? err.message : "Failed to load Claude installations");
} finally {
setLoading(false);
}
};
const handleInstallationChange = (installationPath: string) => {
const installation = installations.find(i => i.path === installationPath);
if (installation) {
setSelectedInstallation(installation);
onSelect(installation);
}
};
const getInstallationIcon = (installation: ClaudeInstallation) => {
switch (installation.installation_type) {
case "System":
return <HardDrive className="h-4 w-4" />;
case "Custom":
return <Settings className="h-4 w-4" />;
default:
return <HardDrive className="h-4 w-4" />;
}
};
const getInstallationTypeColor = (installation: ClaudeInstallation) => {
switch (installation.installation_type) {
case "System":
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
case "Custom":
return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300";
default:
return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300";
}
};
if (loading) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>{t('settings.generalOptions.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.generalOptions.loadingAvailableInstallations')}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className={className}>
<CardHeader>
<CardTitle>{t('settings.generalOptions.claudeCodeInstallation')}</CardTitle>
<CardDescription>{t('settings.generalOptions.errorLoadingInstallations')}</CardDescription>
</CardHeader>
<CardContent>
<div className="text-sm text-destructive mb-4">{error}</div>
<Button onClick={loadInstallations} variant="outline" size="sm">
{t('app.retry')}
</Button>
</CardContent>
</Card>
);
}
const systemInstallations = installations.filter(i => i.installation_type === "System");
const customInstallations = installations.filter(i => i.installation_type === "Custom");
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle className="h-5 w-5" />
{t('settings.generalOptions.claudeCodeInstallation')}
</CardTitle>
<CardDescription>
{t('settings.generalOptions.choosePreferredInstallation')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Available Installations */}
<div className="space-y-3">
<Label className="text-sm font-medium">{t('settings.generalOptions.availableInstallations')}</Label>
<Select value={selectedInstallation?.path || ""} onValueChange={handleInstallationChange}>
<SelectTrigger>
<SelectValue placeholder={t('pleaseSelectInstallation')}>
{selectedInstallation && (
<div className="flex items-center gap-2">
{getInstallationIcon(selectedInstallation)}
<span className="truncate">{selectedInstallation.path}</span>
<Badge variant="secondary" className={cn("text-xs", getInstallationTypeColor(selectedInstallation))}>
{selectedInstallation.installation_type}
</Badge>
</div>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{systemInstallations.length > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">{t('settings.systemInstallations')}</div>
{systemInstallations.map((installation) => (
<SelectItem key={installation.path} value={installation.path}>
<div className="flex items-center gap-2 w-full">
{getInstallationIcon(installation)}
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{installation.path}</div>
<div className="text-xs text-muted-foreground">
{installation.version || t('settings.versionUnknown')} {installation.source}
</div>
</div>
<Badge variant="outline" className="text-xs">
{t('settings.system')}
</Badge>
</div>
</SelectItem>
))}
</>
)}
{customInstallations.length > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">{t('settings.customInstallations')}</div>
{customInstallations.map((installation) => (
<SelectItem key={installation.path} value={installation.path}>
<div className="flex items-center gap-2 w-full">
{getInstallationIcon(installation)}
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{installation.path}</div>
<div className="text-xs text-muted-foreground">
{installation.version || t('settings.versionUnknown')} {installation.source}
</div>
</div>
<Badge variant="outline" className="text-xs">
{t('settings.custom')}
</Badge>
</div>
</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>
{/* Installation Details */}
{selectedInstallation && (
<div className="p-3 bg-muted rounded-lg space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{t('settings.selectedInstallation')}</span>
<Badge className={cn("text-xs", getInstallationTypeColor(selectedInstallation))}>
{selectedInstallation.installation_type}
</Badge>
</div>
<div className="text-sm text-muted-foreground">
<div><strong>{t('settings.path')}:</strong> {selectedInstallation.path}</div>
<div><strong>{t('settings.source')}:</strong> {selectedInstallation.source}</div>
{selectedInstallation.version && (
<div><strong>{t('settings.version')}:</strong> {selectedInstallation.version}</div>
)}
</div>
</div>
)}
{/* Save Button */}
{showSaveButton && (
<Button
onClick={onSave}
disabled={isSaving || !selectedInstallation}
className="w-full"
>
{isSaving ? t('saving') : t('saveSelection')}
</Button>
)}
</CardContent>
</Card>
);
};