美化
This commit is contained in:
@@ -230,24 +230,24 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
|
||||
<div className="flex gap-2 mb-4 pt-4">
|
||||
<Button onClick={handleCreateAgent} className="flex-1">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Agent
|
||||
{t('agents.createAgent')}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="flex-1">
|
||||
<Import className="w-4 h-4 mr-2" />
|
||||
Import Agent
|
||||
{t('agents.import')}
|
||||
<ChevronDown className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={handleImportFromFile}>
|
||||
<FileJson className="w-4 h-4 mr-2" />
|
||||
From File
|
||||
{t('agents.importFromFile')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleImportFromGitHub}>
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
From GitHub
|
||||
{t('agents.importFromGitHub')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -268,7 +268,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
|
||||
window.dispatchEvent(new CustomEvent('open-create-agent-tab'));
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Agent
|
||||
{t('agents.createAgent')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -299,7 +299,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
|
||||
onClick={() => handleExportAgent(agent)}
|
||||
>
|
||||
<Download className="w-3 h-3 mr-1" />
|
||||
Export
|
||||
{t('agents.export')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -308,14 +308,14 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-1" />
|
||||
Delete
|
||||
{t('app.delete')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleRunAgent(agent)}
|
||||
>
|
||||
<Play className="w-3 h-3 mr-1" />
|
||||
Run
|
||||
{t('agents.runAgent')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -331,9 +331,9 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
|
||||
{runningAgents.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<Clock className="w-12 h-12 text-muted-foreground mb-4" />
|
||||
<p className="text-lg font-medium mb-2">No running agents</p>
|
||||
<p className="text-lg font-medium mb-2">{t('agents.noRunningAgents')}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Agent executions will appear here when started
|
||||
{t('agents.agentExecutionsWillAppear')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -373,7 +373,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
|
||||
handleOpenAgentRun(run);
|
||||
}}
|
||||
>
|
||||
View
|
||||
{t('agents.view')}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -392,9 +392,9 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
|
||||
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Agent</DialogTitle>
|
||||
<DialogTitle>{t('agents.deleteAgentTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete "{agentToDelete?.name}"? This action cannot be undone.
|
||||
{t('agents.deleteConfirmation', { name: agentToDelete?.name })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-3 mt-4">
|
||||
@@ -405,13 +405,13 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
|
||||
setAgentToDelete(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t('app.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmDelete}
|
||||
>
|
||||
Delete
|
||||
{t('app.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
@@ -72,10 +72,10 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
|
||||
<div className="p-2 bg-purple-100 dark:bg-purple-900/20 rounded-lg">
|
||||
<BarChart3 className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<DialogTitle className="text-2xl">{t('analytics.helpImproveClaudia')}</DialogTitle>
|
||||
<DialogTitle className="text-2xl">{t('settings.analytics.helpImproveClaudia')}</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-base mt-2">
|
||||
{t('analytics.collectAnonymousData')}
|
||||
{t('settings.analytics.collectAnonymousData')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
@@ -86,12 +86,12 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
|
||||
<div className="flex gap-3">
|
||||
<Check className="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-green-900 dark:text-green-100">{t('analytics.whatWeCollect')}</p>
|
||||
<p className="font-medium text-green-900 dark:text-green-100">{t('settings.analytics.whatWeCollect')}</p>
|
||||
<ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
|
||||
<li>• {t('analytics.featureUsageDesc')}</li>
|
||||
<li>• {t('analytics.performanceMetricsDesc')}</li>
|
||||
<li>• {t('analytics.errorReportsDesc')}</li>
|
||||
<li>• {t('analytics.usagePatternsDesc')}</li>
|
||||
<li>• {t('settings.analytics.featureUsageDesc')}</li>
|
||||
<li>• {t('settings.analytics.performanceMetricsDesc')}</li>
|
||||
<li>• {t('settings.analytics.errorReportsDesc')}</li>
|
||||
<li>• {t('settings.analytics.usagePatternsDesc')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,13 +101,13 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
|
||||
<div className="flex gap-3">
|
||||
<Shield className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-100">{t('analytics.privacyProtected')}</p>
|
||||
<p className="font-medium text-blue-900 dark:text-blue-100">{t('settings.analytics.privacyProtected')}</p>
|
||||
<ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1">
|
||||
<li>• {t('analytics.noPersonalInfo')}</li>
|
||||
<li>• {t('analytics.noFileContents')}</li>
|
||||
<li>• {t('analytics.noApiKeys')}</li>
|
||||
<li>• {t('analytics.anonymousData')}</li>
|
||||
<li>• {t('analytics.canOptOut')}</li>
|
||||
<li>• {t('settings.analytics.noPersonalInfo')}</li>
|
||||
<li>• {t('settings.analytics.noFileContents')}</li>
|
||||
<li>• {t('settings.analytics.noApiKeys')}</li>
|
||||
<li>• {t('settings.analytics.anonymousData')}</li>
|
||||
<li>• {t('settings.analytics.canOptOut')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +118,7 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
|
||||
<div className="flex gap-2 items-start">
|
||||
<Info className="h-4 w-4 text-gray-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('analytics.dataHelpsUs')}
|
||||
{t('settings.analytics.dataHelpsUs')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,13 +130,13 @@ export const AnalyticsConsent: React.FC<AnalyticsConsentProps> = ({
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
{t('analytics.noThanks')}
|
||||
{t('settings.analytics.noThanks')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAccept}
|
||||
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white"
|
||||
>
|
||||
{t('analytics.allowAnalytics')}
|
||||
{t('settings.analytics.allowAnalytics')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -151,6 +151,7 @@ interface AnalyticsConsentBannerProps {
|
||||
export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [hasChecked, setHasChecked] = useState(false);
|
||||
|
||||
@@ -199,9 +200,9 @@ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
|
||||
<div className="flex items-start gap-3">
|
||||
<BarChart3 className="h-5 w-5 text-purple-600 dark:text-purple-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="space-y-2 flex-1">
|
||||
<p className="text-sm font-medium">Help improve Claudia</p>
|
||||
<p className="text-sm font-medium">{t('settings.analytics.helpImproveClaudia')}</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
We collect anonymous usage data to improve your experience. No personal data is collected.
|
||||
{t('settings.analytics.collectAnonymousData')}
|
||||
</p>
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button
|
||||
@@ -210,14 +211,14 @@ export const AnalyticsConsentBanner: React.FC<AnalyticsConsentBannerProps> = ({
|
||||
onClick={handleDecline}
|
||||
className="text-xs"
|
||||
>
|
||||
No Thanks
|
||||
{t('settings.analytics.noThanks')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleAccept}
|
||||
className="text-xs bg-purple-600 hover:bg-purple-700 text-white"
|
||||
>
|
||||
Allow
|
||||
{t('settings.analytics.allowAnalytics')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -134,8 +134,8 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('settings.claudeCodeInstallation')}</CardTitle>
|
||||
<CardDescription>{t('settings.loadingAvailableInstallations')}</CardDescription>
|
||||
<CardTitle>{t('settings.generalOptions.claudeCodeInstallation')}</CardTitle>
|
||||
<CardDescription>{t('settings.generalOptions.loadingAvailableInstallations')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center py-4">
|
||||
@@ -150,8 +150,8 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('settings.claudeCodeInstallation')}</CardTitle>
|
||||
<CardDescription>{t('settings.errorLoadingInstallations')}</CardDescription>
|
||||
<CardTitle>{t('settings.generalOptions.claudeCodeInstallation')}</CardTitle>
|
||||
<CardDescription>{t('settings.generalOptions.errorLoadingInstallations')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-sm text-destructive mb-4">{error}</div>
|
||||
@@ -171,19 +171,19 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CheckCircle className="h-5 w-5" />
|
||||
{t('settings.claudeCodeInstallation')}
|
||||
{t('settings.generalOptions.claudeCodeInstallation')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('settings.choosePreferredInstallation')}
|
||||
{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.availableInstallations')}</Label>
|
||||
<Label className="text-sm font-medium">{t('settings.generalOptions.availableInstallations')}</Label>
|
||||
<Select value={selectedInstallation?.path || ""} onValueChange={handleInstallationChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Claude installation">
|
||||
<SelectValue placeholder={t('pleaseSelectInstallation')}>
|
||||
{selectedInstallation && (
|
||||
<div className="flex items-center gap-2">
|
||||
{getInstallationIcon(selectedInstallation)}
|
||||
@@ -198,7 +198,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
<SelectContent>
|
||||
{systemInstallations.length > 0 && (
|
||||
<>
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">System Installations</div>
|
||||
<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">
|
||||
@@ -206,11 +206,11 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{installation.path}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{installation.version || "Version unknown"} • {installation.source}
|
||||
{installation.version || t('settings.versionUnknown')} • {installation.source}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
System
|
||||
{t('settings.system')}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
@@ -220,7 +220,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
|
||||
{customInstallations.length > 0 && (
|
||||
<>
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">Custom Installations</div>
|
||||
<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">
|
||||
@@ -228,11 +228,11 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{installation.path}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{installation.version || "Version unknown"} • {installation.source}
|
||||
{installation.version || t('settings.versionUnknown')} • {installation.source}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Custom
|
||||
{t('settings.custom')}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
@@ -247,16 +247,16 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
{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">Selected Installation</span>
|
||||
<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>Path:</strong> {selectedInstallation.path}</div>
|
||||
<div><strong>Source:</strong> {selectedInstallation.source}</div>
|
||||
<div><strong>{t('settings.path')}:</strong> {selectedInstallation.path}</div>
|
||||
<div><strong>{t('settings.source')}:</strong> {selectedInstallation.source}</div>
|
||||
{selectedInstallation.version && (
|
||||
<div><strong>Version:</strong> {selectedInstallation.version}</div>
|
||||
<div><strong>{t('settings.version')}:</strong> {selectedInstallation.version}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,7 +269,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
disabled={isSaving || !selectedInstallation}
|
||||
className="w-full"
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save Selection"}
|
||||
{isSaving ? t('saving') : t('saveSelection')}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
|
198
src/components/ClaudiaLogo.tsx
Normal file
198
src/components/ClaudiaLogo.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface ClaudiaLogoProps {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ClaudiaLogo({ size = 48, className = "" }: ClaudiaLogoProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className={`relative inline-flex items-center justify-center ${className}`}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
{/* Background glow animation */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-2xl"
|
||||
style={{
|
||||
background: "radial-gradient(circle, rgba(251, 146, 60, 0.3) 0%, transparent 70%)",
|
||||
}}
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
opacity: [0.5, 0.8, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Main logo with image */}
|
||||
<motion.div
|
||||
className="relative w-full h-full rounded-2xl overflow-hidden shadow-xl"
|
||||
animate={{
|
||||
rotateY: [0, 360],
|
||||
}}
|
||||
transition={{
|
||||
duration: 8,
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: "easeOut",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Use the actual Claudia icon */}
|
||||
<motion.img
|
||||
src="/icon.png"
|
||||
alt="Claudia"
|
||||
className="w-full h-full object-contain"
|
||||
animate={{
|
||||
scale: [1, 1.05, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Shimmer effect overlay */}
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/20 to-transparent"
|
||||
animate={{
|
||||
x: ["-200%", "200%"],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
repeatDelay: 1,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Orbiting particles */}
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute w-1.5 h-1.5 bg-orange-400 rounded-full"
|
||||
style={{
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
}}
|
||||
animate={{
|
||||
x: [
|
||||
0,
|
||||
Math.cos((i * Math.PI) / 2) * size * 0.6,
|
||||
0,
|
||||
-Math.cos((i * Math.PI) / 2) * size * 0.6,
|
||||
0,
|
||||
],
|
||||
y: [
|
||||
0,
|
||||
Math.sin((i * Math.PI) / 2) * size * 0.6,
|
||||
0,
|
||||
-Math.sin((i * Math.PI) / 2) * size * 0.6,
|
||||
0,
|
||||
],
|
||||
}}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
delay: i * 0.25,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// Alternative minimalist version
|
||||
export function ClaudiaLogoMinimal({ size = 48, className = "" }: ClaudiaLogoProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className={`relative inline-flex items-center justify-center ${className}`}
|
||||
style={{ width: size, height: size }}
|
||||
animate={{
|
||||
rotate: [0, 5, -5, 5, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
{/* Gradient background */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-2xl bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600"
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Inner light effect */}
|
||||
<motion.div
|
||||
className="absolute inset-1 rounded-xl bg-gradient-to-br from-orange-300/50 to-transparent"
|
||||
animate={{
|
||||
opacity: [0.5, 1, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Letter C with animation */}
|
||||
<motion.div
|
||||
className="relative z-10 text-white font-bold flex items-center justify-center"
|
||||
style={{ fontSize: size * 0.5 }}
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
textShadow: [
|
||||
"0 0 10px rgba(255,255,255,0.5)",
|
||||
"0 0 20px rgba(255,255,255,0.8)",
|
||||
"0 0 10px rgba(255,255,255,0.5)",
|
||||
],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
C
|
||||
</motion.div>
|
||||
|
||||
{/* Pulse rings */}
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute inset-0 rounded-2xl border-2 border-orange-400"
|
||||
animate={{
|
||||
scale: [1, 1.5, 2],
|
||||
opacity: [0.5, 0.2, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
delay: i * 1.5,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
@@ -81,33 +81,33 @@ interface EditableHookMatcher extends Omit<HookMatcher, 'hooks'> {
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
const EVENT_INFO: Record<HookEvent, { label: string; description: string; icon: React.ReactNode }> = {
|
||||
const getEventInfo = (t: any): Record<HookEvent, { label: string; description: string; icon: React.ReactNode }> => ({
|
||||
PreToolUse: {
|
||||
label: 'Pre Tool Use',
|
||||
description: 'Runs before tool calls, can block and provide feedback',
|
||||
label: t('hooks.preToolUse'),
|
||||
description: t('hooks.runsBeforeToolCalls'),
|
||||
icon: <Shield className="h-4 w-4" />
|
||||
},
|
||||
PostToolUse: {
|
||||
label: 'Post Tool Use',
|
||||
description: 'Runs after successful tool completion',
|
||||
label: t('hooks.postToolUse'),
|
||||
description: t('hooks.runsAfterToolCompletion'),
|
||||
icon: <PlayCircle className="h-4 w-4" />
|
||||
},
|
||||
Notification: {
|
||||
label: 'Notification',
|
||||
description: 'Customizes notifications when Claude needs attention',
|
||||
label: t('hooks.notification'),
|
||||
description: t('hooks.customizesNotifications'),
|
||||
icon: <Zap className="h-4 w-4" />
|
||||
},
|
||||
Stop: {
|
||||
label: 'Stop',
|
||||
description: 'Runs when Claude finishes responding',
|
||||
label: t('hooks.stop'),
|
||||
description: t('hooks.runsWhenClaudeFinishes'),
|
||||
icon: <Code2 className="h-4 w-4" />
|
||||
},
|
||||
SubagentStop: {
|
||||
label: 'Subagent Stop',
|
||||
description: 'Runs when a Claude subagent (Task) finishes',
|
||||
label: t('hooks.subagentStop'),
|
||||
description: t('hooks.runsWhenSubagentFinishes'),
|
||||
icon: <Terminal className="h-4 w-4" />
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
projectPath,
|
||||
@@ -118,6 +118,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
hideActions = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const EVENT_INFO = getEventInfo(t);
|
||||
const [selectedEvent, setSelectedEvent] = useState<HookEvent>('PreToolUse');
|
||||
const [showTemplateDialog, setShowTemplateDialog] = useState(false);
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||||
@@ -525,14 +526,14 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor={`matcher-${matcher.id}`}>Pattern</Label>
|
||||
<Label htmlFor={`matcher-${matcher.id}`}>{t('hooks.pattern')}</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3 w-3 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Tool name pattern (regex supported). Leave empty to match all tools.</p>
|
||||
<p>{t('hooks.toolNamePatternTooltip')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -558,7 +559,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
disabled={readOnly}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Common patterns" />
|
||||
<SelectValue placeholder={t('hooks.commonPatterns')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="custom">Custom</SelectItem>
|
||||
@@ -591,7 +592,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Commands</Label>
|
||||
<Label>{t('hooks.commands')}</Label>
|
||||
{!readOnly && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -599,7 +600,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
onClick={() => addCommand(event, matcher.id)}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Add Command
|
||||
{t('hooks.addCommand')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -613,7 +614,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1 space-y-2">
|
||||
<Textarea
|
||||
placeholder="Enter shell command..."
|
||||
placeholder={t('hooks.enterShellCommand')}
|
||||
value={hook.command || ''}
|
||||
onChange={(e) => updateCommand(event, matcher.id, hook.id, { command: e.target.value })}
|
||||
disabled={readOnly}
|
||||
@@ -633,7 +634,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
disabled={readOnly}
|
||||
className="w-20 h-8"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">seconds</span>
|
||||
<span className="text-sm text-muted-foreground">{t('hooks.seconds')}</span>
|
||||
</div>
|
||||
|
||||
{!readOnly && (
|
||||
@@ -679,7 +680,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1 space-y-2">
|
||||
<Textarea
|
||||
placeholder="Enter shell command..."
|
||||
placeholder={t('hooks.enterShellCommand')}
|
||||
value={command.command || ''}
|
||||
onChange={(e) => updateDirectCommand(event, command.id, { command: e.target.value })}
|
||||
disabled={readOnly}
|
||||
@@ -738,7 +739,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
<span className="text-sm text-muted-foreground">Loading hooks configuration...</span>
|
||||
<span className="text-sm text-muted-foreground">{t('hooks.loadingHooksConfiguration')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -759,7 +760,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
<h3 className="text-lg font-semibold">{t('hooks.hooksConfiguration')}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={scope === 'project' ? 'secondary' : scope === 'local' ? 'outline' : 'default'}>
|
||||
{scope === 'project' ? 'Project' : scope === 'local' ? 'Local' : 'User'} Scope
|
||||
{scope === 'project' ? t('hooks.projectScope') : scope === 'local' ? t('hooks.localScope') : t('hooks.userScope')}
|
||||
</Badge>
|
||||
{!readOnly && (
|
||||
<>
|
||||
@@ -769,7 +770,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
onClick={() => setShowTemplateDialog(true)}
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
Templates
|
||||
{t('hooks.templates')}
|
||||
</Button>
|
||||
{!hideActions && (
|
||||
<Button
|
||||
@@ -783,7 +784,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
) : (
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{isSaving ? "Saving..." : "Save"}
|
||||
{isSaving ? t('hooks.saving') : t('hooks.save')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -804,7 +805,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
{/* Validation Messages */}
|
||||
{validationErrors.length > 0 && (
|
||||
<div className="p-3 bg-red-500/10 rounded-md space-y-1">
|
||||
<p className="text-sm font-medium text-red-600">Validation Errors:</p>
|
||||
<p className="text-sm font-medium text-red-600">{t('hooks.validationErrors')}:</p>
|
||||
{validationErrors.map((error, i) => (
|
||||
<p key={i} className="text-xs text-red-600">• {error}</p>
|
||||
))}
|
||||
@@ -813,7 +814,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
|
||||
{validationWarnings.length > 0 && (
|
||||
<div className="p-3 bg-yellow-500/10 rounded-md space-y-1">
|
||||
<p className="text-sm font-medium text-yellow-600">Security Warnings:</p>
|
||||
<p className="text-sm font-medium text-yellow-600">{t('hooks.securityWarnings')}:</p>
|
||||
{validationWarnings.map((warning, i) => (
|
||||
<p key={i} className="text-xs text-yellow-600">• {warning}</p>
|
||||
))}
|
||||
@@ -859,11 +860,11 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
|
||||
{items.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<p className="text-muted-foreground mb-4">No hooks configured for this event</p>
|
||||
<p className="text-muted-foreground mb-4">{t('hooks.noHooksConfigured')}</p>
|
||||
{!readOnly && (
|
||||
<Button onClick={() => isMatcherEvent ? addMatcher(event) : addDirectCommand(event)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Hook
|
||||
{t('hooks.addHook')}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
@@ -881,7 +882,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
className="w-full"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Another {isMatcherEvent ? 'Matcher' : 'Command'}
|
||||
{isMatcherEvent ? t('hooks.addAnotherMatcher') : t('hooks.addAnotherCommand')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -895,9 +896,9 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
<Dialog open={showTemplateDialog} onOpenChange={setShowTemplateDialog}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Hook Templates</DialogTitle>
|
||||
<DialogTitle>{t('hooks.hookTemplates')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose a pre-configured hook template to get started quickly
|
||||
{t('hooks.quickStartTemplates')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -916,7 +917,7 @@ export const HooksEditor: React.FC<HooksEditorProps> = ({
|
||||
<p className="text-sm text-muted-foreground">{template.description}</p>
|
||||
{matcherEvents.includes(template.event as any) && template.matcher && (
|
||||
<p className="text-xs font-mono bg-muted px-2 py-1 rounded inline-block">
|
||||
Matcher: {template.matcher}
|
||||
{t('hooks.pattern')}: {template.matcher}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -145,6 +145,7 @@ import {
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
/**
|
||||
* Icon categories for better organization
|
||||
@@ -333,6 +334,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [hoveredIcon, setHoveredIcon] = useState<string | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Filter icons based on search query
|
||||
const filteredCategories = useMemo(() => {
|
||||
@@ -368,7 +370,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] p-0">
|
||||
<DialogHeader className="px-6 py-4 border-b">
|
||||
<DialogTitle>Choose an icon</DialogTitle>
|
||||
<DialogTitle>{t('agents.chooseAnIcon')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Search Bar */}
|
||||
@@ -376,7 +378,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search icons..."
|
||||
placeholder={t('agents.searchIcons')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
@@ -390,7 +392,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
|
||||
{Object.keys(filteredCategories).length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-32 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No icons found for "{searchQuery}"
|
||||
{t('agents.noIconsFound')} "{searchQuery}"
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -405,7 +407,15 @@ export const IconPicker: React.FC<IconPickerProps> = ({
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-3">
|
||||
{category}
|
||||
{category === "Interface & Navigation" ? t('agents.iconCategories.interfaceNavigation') :
|
||||
category === "Development & Tech" ? t('agents.iconCategories.developmentTech') :
|
||||
category === "Business & Finance" ? t('agents.iconCategories.businessFinance') :
|
||||
category === "Creative & Design" ? t('agents.iconCategories.creativeDesign') :
|
||||
category === "Nature & Science" ? t('agents.iconCategories.natureScience') :
|
||||
category === "Gaming & Entertainment" ? t('agents.iconCategories.gamingEntertainment') :
|
||||
category === "Communication" ? t('agents.iconCategories.communication') :
|
||||
category === "Miscellaneous" ? t('agents.iconCategories.miscellaneous') :
|
||||
category}
|
||||
</h3>
|
||||
<div className="grid grid-cols-10 gap-2">
|
||||
{icons.map((item: IconItem) => {
|
||||
@@ -444,7 +454,7 @@ export const IconPicker: React.FC<IconPickerProps> = ({
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-3 border-t bg-muted/50">
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
Click an icon to select • {allIcons.length} icons available
|
||||
{t('agents.clickToSelect')} • {allIcons.length} {t('agents.iconsAvailable')}
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
@@ -108,7 +108,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
}
|
||||
|
||||
if (!stdioCommand.trim()) {
|
||||
onError(t('commandRequired'));
|
||||
onError(t('mcp.commandRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
onError(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
onError(t('failedToAddServer'));
|
||||
onError(t('mcp.failedToAddServer'));
|
||||
console.error("Failed to add stdio server:", error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -171,7 +171,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
}
|
||||
|
||||
if (!sseUrl.trim()) {
|
||||
onError(t('urlRequired'));
|
||||
onError(t('mcp.urlRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
onError(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
onError(t('failedToAddServer'));
|
||||
onError(t('mcp.failedToAddServer'));
|
||||
console.error("Failed to add SSE server:", error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -227,7 +227,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">{t('environmentVariables')}</Label>
|
||||
<Label className="text-sm font-medium">{t('mcp.environmentVariables')}</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -235,7 +235,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
className="gap-2"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
{t('addVariable')}
|
||||
{t('mcp.addVariable')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -244,14 +244,14 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
{envVars.map((envVar) => (
|
||||
<div key={envVar.id} className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder="KEY"
|
||||
placeholder={t('settings.placeholders.envVarKey')}
|
||||
value={envVar.key}
|
||||
onChange={(e) => updateEnvVar(type, envVar.id, "key", e.target.value)}
|
||||
className="flex-1 font-mono text-sm"
|
||||
/>
|
||||
<span className="text-muted-foreground">=</span>
|
||||
<Input
|
||||
placeholder="value"
|
||||
placeholder={t('settings.placeholders.envVarValue')}
|
||||
value={envVar.value}
|
||||
onChange={(e) => updateEnvVar(type, envVar.id, "value", e.target.value)}
|
||||
className="flex-1 font-mono text-sm"
|
||||
@@ -275,9 +275,9 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">{t('addMcpServer')}</h3>
|
||||
<h3 className="text-base font-semibold">{t('mcp.addMcpServer')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('configureNewMcpServer')}
|
||||
{t('mcp.configureNewMcpServer')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -298,7 +298,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
<Card className="p-6 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="stdio-name">{t('serverName')}</Label>
|
||||
<Label htmlFor="stdio-name">{t('mcp.serverName')}</Label>
|
||||
<Input
|
||||
id="stdio-name"
|
||||
placeholder="my-server"
|
||||
@@ -306,12 +306,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
onChange={(e) => setStdioName(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('uniqueNameToIdentify')}
|
||||
{t('mcp.uniqueNameToIdentify')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="stdio-command">{t('command')}</Label>
|
||||
<Label htmlFor="stdio-command">{t('mcp.command')}</Label>
|
||||
<Input
|
||||
id="stdio-command"
|
||||
placeholder="/path/to/server"
|
||||
@@ -320,12 +320,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
className="font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('commandToExecuteServer')}
|
||||
{t('mcp.commandToExecuteServer')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="stdio-args">{t('argumentsOptional')}</Label>
|
||||
<Label htmlFor="stdio-args">{t('mcp.argumentsOptional')}</Label>
|
||||
<Input
|
||||
id="stdio-args"
|
||||
placeholder="arg1 arg2 arg3"
|
||||
@@ -334,19 +334,19 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
className="font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('spaceSeparatedArgs')}
|
||||
{t('mcp.spaceSeparatedArgs')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="stdio-scope">{t('scope')}</Label>
|
||||
<Label htmlFor="stdio-scope">{t('mcp.scope')}</Label>
|
||||
<SelectComponent
|
||||
value={stdioScope}
|
||||
onValueChange={(value: string) => setStdioScope(value)}
|
||||
options={[
|
||||
{ value: "local", label: t('localProjectOnly') },
|
||||
{ value: "project", label: t('projectSharedViaMcp') },
|
||||
{ value: "user", label: t('userAllProjects') },
|
||||
{ value: "local", label: t('mcp.localProjectOnly') },
|
||||
{ value: "project", label: t('mcp.projectSharedViaMcp') },
|
||||
{ value: "user", label: t('mcp.userAllProjects') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -363,12 +363,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
{saving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t('addingServer')}
|
||||
{t('mcp.addingServer')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('addStdioServer')}
|
||||
{t('mcp.addStdioServer')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -381,7 +381,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
<Card className="p-6 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sse-name">{t('serverName')}</Label>
|
||||
<Label htmlFor="sse-name">{t('mcp.serverName')}</Label>
|
||||
<Input
|
||||
id="sse-name"
|
||||
placeholder="sse-server"
|
||||
@@ -389,12 +389,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
onChange={(e) => setSseName(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('uniqueNameToIdentify')}
|
||||
{t('mcp.uniqueNameToIdentify')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sse-url">{t('url')}</Label>
|
||||
<Label htmlFor="sse-url">{t('mcp.url')}</Label>
|
||||
<Input
|
||||
id="sse-url"
|
||||
placeholder="https://example.com/sse-endpoint"
|
||||
@@ -403,19 +403,19 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
className="font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('sseEndpointUrl')}
|
||||
{t('mcp.sseEndpointUrl')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sse-scope">{t('scope')}</Label>
|
||||
<Label htmlFor="sse-scope">{t('mcp.scope')}</Label>
|
||||
<SelectComponent
|
||||
value={sseScope}
|
||||
onValueChange={(value: string) => setSseScope(value)}
|
||||
options={[
|
||||
{ value: "local", label: t('localProjectOnly') },
|
||||
{ value: "project", label: t('projectSharedViaMcp') },
|
||||
{ value: "user", label: t('userAllProjects') },
|
||||
{ value: "local", label: t('mcp.localProjectOnly') },
|
||||
{ value: "project", label: t('mcp.projectSharedViaMcp') },
|
||||
{ value: "user", label: t('mcp.userAllProjects') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -432,12 +432,12 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
{saving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t('addingServer')}
|
||||
{t('mcp.addingServer')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('addSseServer')}
|
||||
{t('mcp.addSseServer')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -451,7 +451,7 @@ export const MCPAddServer: React.FC<MCPAddServerProps> = ({
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Info className="h-4 w-4 text-primary" />
|
||||
<span>{t('exampleCommands')}</span>
|
||||
<span>{t('mcp.exampleCommands')}</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-xs text-muted-foreground">
|
||||
<div className="font-mono bg-background p-2 rounded">
|
||||
|
@@ -5,6 +5,7 @@ import { Card } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { SelectComponent } from "@/components/ui/select";
|
||||
import { api } from "@/lib/api";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
interface MCPImportExportProps {
|
||||
/**
|
||||
@@ -24,6 +25,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
onImportCompleted,
|
||||
onError,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [importingDesktop, setImportingDesktop] = useState(false);
|
||||
const [importingJson, setImportingJson] = useState(false);
|
||||
const [importScope, setImportScope] = useState("local");
|
||||
@@ -163,9 +165,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">Import & Export</h3>
|
||||
<h3 className="text-base font-semibold">{t('mcp.importExport')}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Import MCP servers from other sources or export your configuration
|
||||
{t('mcp.importExportDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -175,19 +177,19 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Settings2 className="h-4 w-4 text-slate-500" />
|
||||
<Label className="text-sm font-medium">Import Scope</Label>
|
||||
<Label className="text-sm font-medium">{t('mcp.importScope')}</Label>
|
||||
</div>
|
||||
<SelectComponent
|
||||
value={importScope}
|
||||
onValueChange={(value: string) => setImportScope(value)}
|
||||
options={[
|
||||
{ value: "local", label: "Local (this project only)" },
|
||||
{ value: "project", label: "Project (shared via .mcp.json)" },
|
||||
{ value: "user", label: "User (all projects)" },
|
||||
{ value: "local", label: t('mcp.localProjectOnly') },
|
||||
{ value: "project", label: t('mcp.projectShared') },
|
||||
{ value: "user", label: t('mcp.userAllProjects') },
|
||||
]}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose where to save imported servers from JSON files
|
||||
{t('mcp.chooseImportLocation')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -200,9 +202,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
<Download className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium">Import from Claude Desktop</h4>
|
||||
<h4 className="text-sm font-medium">{t('mcp.importFromClaudeDesktop')}</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Automatically imports all MCP servers from Claude Desktop. Installs to user scope (available across all projects).
|
||||
{t('mcp.importFromClaudeDesktopDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,12 +216,12 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
{importingDesktop ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Importing...
|
||||
{t('mcp.importing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Import from Claude Desktop
|
||||
{t('mcp.importFromClaudeDesktop')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -234,9 +236,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
<FileText className="h-5 w-5 text-purple-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium">Import from JSON</h4>
|
||||
<h4 className="text-sm font-medium">{t('mcp.importFromJSON')}</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Import server configuration from a JSON file
|
||||
{t('mcp.importFromJSONDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,12 +260,12 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
{importingJson ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Importing...
|
||||
{t('mcp.importing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="h-4 w-4" />
|
||||
Choose JSON File
|
||||
{t('mcp.chooseJSONFile')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -279,9 +281,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
<Upload className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium">Export Configuration</h4>
|
||||
<h4 className="text-sm font-medium">{t('mcp.exportConfiguration')}</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Export your MCP server configuration
|
||||
{t('mcp.exportConfigurationDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,7 +294,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
Export (Coming Soon)
|
||||
{t('mcp.exportComingSoon')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -305,9 +307,9 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
<Network className="h-5 w-5 text-green-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium">Use Claude Code as MCP Server</h4>
|
||||
<h4 className="text-sm font-medium">{t('mcp.useClaudeCodeAsMCPServer')}</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Start Claude Code as an MCP server that other applications can connect to
|
||||
{t('mcp.useClaudeCodeAsMCPServerDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,7 +319,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
className="w-full gap-2 border-green-500/20 hover:bg-green-500/10 hover:text-green-600 hover:border-green-500/50"
|
||||
>
|
||||
<Network className="h-4 w-4" />
|
||||
Start MCP Server
|
||||
{t('mcp.startMCPServer')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -328,11 +330,11 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Info className="h-4 w-4 text-primary" />
|
||||
<span>JSON Format Examples</span>
|
||||
<span>{t('mcp.jsonFormatExamples')}</span>
|
||||
</div>
|
||||
<div className="space-y-3 text-xs">
|
||||
<div>
|
||||
<p className="font-medium text-muted-foreground mb-1">Single server:</p>
|
||||
<p className="font-medium text-muted-foreground mb-1">{t('mcp.singleServer')}:</p>
|
||||
<pre className="bg-background p-3 rounded-lg overflow-x-auto">
|
||||
{`{
|
||||
"type": "stdio",
|
||||
@@ -343,7 +345,7 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-muted-foreground mb-1">Multiple servers (.mcp.json format):</p>
|
||||
<p className="font-medium text-muted-foreground mb-1">{t('mcp.multipleServers')}:</p>
|
||||
<pre className="bg-background p-3 rounded-lg overflow-x-auto">
|
||||
{`{
|
||||
"mcpServers": {
|
||||
|
@@ -155,15 +155,15 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
|
||||
<TabsList className="grid w-full max-w-md grid-cols-3">
|
||||
<TabsTrigger value="servers" className="gap-2">
|
||||
<Network className="h-4 w-4 text-blue-500" />
|
||||
{t('servers')}
|
||||
{t('mcp.servers')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="add" className="gap-2">
|
||||
<Plus className="h-4 w-4 text-green-500" />
|
||||
{t('addServer')}
|
||||
{t('mcp.addServer')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="import" className="gap-2">
|
||||
<Download className="h-4 w-4 text-purple-500" />
|
||||
{t('importExport')}
|
||||
{t('mcp.importExport')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
@@ -186,11 +186,11 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
const getScopeDisplayName = (scope: string) => {
|
||||
switch (scope) {
|
||||
case "local":
|
||||
return t('localProjectSpecific');
|
||||
return t('mcp.localProjectSpecific');
|
||||
case "project":
|
||||
return t('projectSharedMcp');
|
||||
return t('mcp.projectSharedMcp');
|
||||
case "user":
|
||||
return t('userAllProjects');
|
||||
return t('mcp.userAllProjects');
|
||||
default:
|
||||
return scope;
|
||||
}
|
||||
@@ -222,7 +222,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
{server.status?.running && (
|
||||
<Badge variant="outline" className="gap-1 flex-shrink-0 border-green-500/50 text-green-600 bg-green-500/10">
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
{t('running')}
|
||||
{t('mcp.running')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -239,7 +239,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
className="h-6 px-2 text-xs hover:bg-primary/10"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3 mr-1" />
|
||||
{t('showFull')}
|
||||
{t('mcp.showFull')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -254,7 +254,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
|
||||
{Object.keys(server.env).length > 0 && !isExpanded && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground pl-9">
|
||||
<span>Environment variables: {Object.keys(server.env).length}</span>
|
||||
<span>{t('mcp.environmentVariablesCount', { count: Object.keys(server.env).length })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -301,7 +301,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
{server.command && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-muted-foreground">{t('command')}</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">{t('mcp.command')}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -310,7 +310,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
className="h-6 px-2 text-xs hover:bg-primary/10"
|
||||
>
|
||||
<Copy className="h-3 w-3 mr-1" />
|
||||
{isCopied ? t('copied') : t('copy')}
|
||||
{isCopied ? t('mcp.copied') : t('mcp.copy')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -319,7 +319,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
className="h-6 px-2 text-xs hover:bg-primary/10"
|
||||
>
|
||||
<ChevronUp className="h-3 w-3 mr-1" />
|
||||
{t('hide')}
|
||||
{t('mcp.hide')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -331,7 +331,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
|
||||
{server.args && server.args.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium text-muted-foreground">{t('arguments')}</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">{t('mcp.arguments')}</p>
|
||||
<div className="text-xs font-mono bg-muted/50 p-2 rounded space-y-1">
|
||||
{server.args.map((arg, idx) => (
|
||||
<div key={idx} className="break-all">
|
||||
@@ -345,7 +345,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
|
||||
{server.transport === "sse" && server.url && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium text-muted-foreground">{t('url')}</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">{t('mcp.url')}</p>
|
||||
<p className="text-xs font-mono bg-muted/50 p-2 rounded break-all">
|
||||
{server.url}
|
||||
</p>
|
||||
@@ -354,7 +354,7 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
|
||||
{Object.keys(server.env).length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium text-muted-foreground">{t('environmentVariables')}</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">{t('mcp.environmentVariables')}</p>
|
||||
<div className="text-xs font-mono bg-muted/50 p-2 rounded space-y-1">
|
||||
{Object.entries(server.env).map(([key, value]) => (
|
||||
<div key={key} className="break-all">
|
||||
@@ -386,9 +386,9 @@ export const MCPServerList: React.FC<MCPServerListProps> = ({
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">{t('configuredServers')}</h3>
|
||||
<h3 className="text-base font-semibold">{t('mcp.configuredServers')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{servers.length} {servers.length !== 1 ? t('servers') : 'server'} {t('serversConfigured')}
|
||||
{servers.length} {t('mcp.serversCount', { count: servers.length })}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
|
@@ -53,7 +53,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
|
||||
setOriginalContent(prompt);
|
||||
} catch (err) {
|
||||
console.error("Failed to load system prompt:", err);
|
||||
setError(t('loadClaudemdFailed'));
|
||||
setError(t('usage.loadClaudemdFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -66,11 +66,11 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
|
||||
setToast(null);
|
||||
await api.saveSystemPrompt(content);
|
||||
setOriginalContent(content);
|
||||
setToast({ message: t('claudemdSavedSuccess'), type: "success" });
|
||||
setToast({ message: t('usage.claudemdSavedSuccess'), type: "success" });
|
||||
} catch (err) {
|
||||
console.error("Failed to save system prompt:", err);
|
||||
setError(t('saveClaudemdFailed'));
|
||||
setToast({ message: t('saveClaudemdFailed'), type: "error" });
|
||||
setError(t('usage.saveClaudemdFailed'));
|
||||
setToast({ message: t('usage.saveClaudemdFailed'), type: "error" });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
|
||||
const handleBack = () => {
|
||||
if (hasChanges) {
|
||||
const confirmLeave = window.confirm(
|
||||
t('unsavedChangesConfirm')
|
||||
t('usage.unsavedChangesConfirm')
|
||||
);
|
||||
if (!confirmLeave) return;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">CLAUDE.md</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('editSystemPrompt')}
|
||||
{t('usage.editSystemPrompt')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -85,8 +85,7 @@ export const ProjectSettings: React.FC<ProjectSettingsProps> = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" onClick={onBack}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-muted-foreground" />
|
||||
|
@@ -315,6 +315,22 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
|
||||
export const TabContent: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { tabs, activeTabId, createChatTab, findTabBySessionId, createClaudeFileTab, createAgentExecutionTab, createCreateAgentTab, createImportAgentTab, closeTab, updateTab } = useTabState();
|
||||
const [hasInitialized, setHasInitialized] = React.useState(false);
|
||||
|
||||
// Auto redirect to home when no tabs (but not on initial load)
|
||||
useEffect(() => {
|
||||
if (hasInitialized && tabs.length === 0) {
|
||||
// Dispatch event to switch back to welcome view
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('switch-to-welcome'));
|
||||
}, 100);
|
||||
}
|
||||
}, [tabs.length, hasInitialized]);
|
||||
|
||||
// Mark as initialized after first render
|
||||
useEffect(() => {
|
||||
setHasInitialized(true);
|
||||
}, []);
|
||||
|
||||
// Listen for events to open sessions in tabs
|
||||
useEffect(() => {
|
||||
|
@@ -142,6 +142,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
||||
createProjectsTab,
|
||||
closeTab,
|
||||
switchToTab,
|
||||
updateTab,
|
||||
canAddTab
|
||||
} = useTabState();
|
||||
|
||||
@@ -209,11 +210,30 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenSessionTab = (event: CustomEvent) => {
|
||||
const { session, projectPath } = event.detail;
|
||||
if (session && canAddTab()) {
|
||||
// Create a new chat tab with the session data
|
||||
const tabId = createChatTab();
|
||||
// Update the tab with session data
|
||||
setTimeout(() => {
|
||||
updateTab(tabId, {
|
||||
type: 'chat',
|
||||
title: session.project_path.split('/').pop() || 'Session',
|
||||
sessionId: session.id,
|
||||
sessionData: session,
|
||||
initialProjectPath: projectPath || session.project_path,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('create-chat-tab', handleCreateTab);
|
||||
window.addEventListener('close-current-tab', handleCloseTab);
|
||||
window.addEventListener('switch-to-next-tab', handleNextTab);
|
||||
window.addEventListener('switch-to-previous-tab', handlePreviousTab);
|
||||
window.addEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener);
|
||||
window.addEventListener('open-session-tab', handleOpenSessionTab as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('create-chat-tab', handleCreateTab);
|
||||
@@ -221,8 +241,9 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
||||
window.removeEventListener('switch-to-next-tab', handleNextTab);
|
||||
window.removeEventListener('switch-to-previous-tab', handlePreviousTab);
|
||||
window.removeEventListener('switch-to-tab-by-index', handleTabByIndex as EventListener);
|
||||
window.removeEventListener('open-session-tab', handleOpenSessionTab as EventListener);
|
||||
};
|
||||
}, [tabs, activeTabId, createChatTab, closeTab, switchToTab]);
|
||||
}, [tabs, activeTabId, createChatTab, closeTab, switchToTab, updateTab, canAddTab]);
|
||||
|
||||
// Check scroll buttons visibility
|
||||
const checkScrollButtons = () => {
|
||||
@@ -319,7 +340,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
||||
className={cn(
|
||||
"p-1.5 hover:bg-muted/80 rounded-sm z-20 ml-1",
|
||||
"transition-colors duration-200 flex items-center justify-center",
|
||||
"bg-background/80 backdrop-blur-sm shadow-sm border border-border/50"
|
||||
"bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] shadow-sm border border-border/60"
|
||||
)}
|
||||
title="Scroll tabs left"
|
||||
>
|
||||
@@ -373,7 +394,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
||||
className={cn(
|
||||
"p-1.5 hover:bg-muted/80 rounded-sm z-20 mr-1",
|
||||
"transition-colors duration-200 flex items-center justify-center",
|
||||
"bg-background/80 backdrop-blur-sm shadow-sm border border-border/50"
|
||||
"bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] shadow-sm border border-border/60"
|
||||
)}
|
||||
title="Scroll tabs right"
|
||||
>
|
||||
@@ -390,7 +411,7 @@ export const TabManager: React.FC<TabManagerProps> = ({ className }) => {
|
||||
disabled={!canAddTab()}
|
||||
className={cn(
|
||||
"p-2 mx-2 rounded-md transition-all duration-200 flex items-center justify-center",
|
||||
"border border-border/50 bg-background/50 backdrop-blur-sm",
|
||||
"border border-border/60 bg-background/85 backdrop-blur-xl backdrop-saturate-[1.8]",
|
||||
canAddTab()
|
||||
? "hover:bg-muted/80 hover:border-border text-muted-foreground hover:text-foreground hover:shadow-sm"
|
||||
: "opacity-50 cursor-not-allowed bg-muted/30"
|
||||
|
@@ -107,7 +107,11 @@ export const Topbar: React.FC<TopbarProps> = ({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto py-1 px-2 hover:bg-accent"
|
||||
onClick={onSettingsClick}
|
||||
onClick={() => {
|
||||
// Emit event to return to home
|
||||
window.dispatchEvent(new CustomEvent('switch-to-welcome'));
|
||||
}}
|
||||
title="Return to Home"
|
||||
>
|
||||
<div className="flex items-center space-x-2 text-xs">
|
||||
<Circle
|
||||
@@ -172,7 +176,7 @@ export const Topbar: React.FC<TopbarProps> = ({
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className={cn(
|
||||
"flex items-center justify-between px-4 py-3 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60",
|
||||
"flex items-center justify-between px-4 py-3 border-b border-border bg-background/98 backdrop-blur-xl backdrop-saturate-[1.8] supports-[backdrop-filter]:bg-background/85 shadow-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
@@ -16,6 +16,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
interface WebviewPreviewProps {
|
||||
/**
|
||||
@@ -61,6 +62,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
onUrlChange,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentUrl, setCurrentUrl] = useState(initialUrl);
|
||||
const [inputUrl, setInputUrl] = useState(initialUrl);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -132,7 +134,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
onUrlChange?.(finalUrl);
|
||||
} catch (err) {
|
||||
setHasError(true);
|
||||
setErrorMessage("Invalid URL");
|
||||
setErrorMessage(t('webview.invalidUrl'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -182,7 +184,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Preview</span>
|
||||
<span className="text-sm font-medium">{t('webview.preview')}</span>
|
||||
{isLoading && (
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
@@ -207,7 +209,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isMaximized ? "Exit full screen (ESC)" : "Enter full screen"}
|
||||
{isMaximized ? t('webview.exitFullScreen') : t('webview.enterFullScreen')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -270,7 +272,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
value={inputUrl}
|
||||
onChange={(e) => setInputUrl(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Enter URL..."
|
||||
placeholder={t('webview.enterUrl')}
|
||||
className="pr-10 h-8 text-sm font-mono"
|
||||
/>
|
||||
{inputUrl !== currentUrl && (
|
||||
@@ -300,7 +302,7 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<p className="text-sm text-muted-foreground">Loading preview...</p>
|
||||
<p className="text-sm text-muted-foreground">{t('webview.loadingPreview')}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -310,12 +312,12 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
{hasError ? (
|
||||
<div className="flex flex-col items-center justify-center h-full p-8">
|
||||
<AlertCircle className="h-12 w-12 text-destructive mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">Failed to load preview</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('webview.failedToLoad')}</h3>
|
||||
<p className="text-sm text-muted-foreground text-center mb-4">
|
||||
{errorMessage || "The page could not be loaded. Please check the URL and try again."}
|
||||
{errorMessage || t('webview.pageCouldNotLoad')}
|
||||
</p>
|
||||
<Button onClick={handleRefresh} variant="outline" size="sm">
|
||||
Try Again
|
||||
{t('app.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
) : currentUrl ? (
|
||||
@@ -336,14 +338,14 @@ const WebviewPreviewComponent: React.FC<WebviewPreviewProps> = ({
|
||||
// Empty state when no URL is provided
|
||||
<div className="flex flex-col items-center justify-center h-full p-8 text-foreground">
|
||||
<Globe className="h-16 w-16 text-muted-foreground/50 mb-6" />
|
||||
<h3 className="text-xl font-semibold mb-3">Enter a URL to preview</h3>
|
||||
<h3 className="text-xl font-semibold mb-3">{t('webview.enterUrlToPreview')}</h3>
|
||||
<p className="text-sm text-muted-foreground text-center mb-6 max-w-md">
|
||||
Enter a URL in the address bar above to preview a website.
|
||||
{t('webview.enterUrlDescription')}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>Try entering</span>
|
||||
<span>{t('webview.tryEntering')}</span>
|
||||
<code className="px-2 py-1 bg-muted/50 text-foreground rounded font-mono text-xs">localhost:3000</code>
|
||||
<span>or any other URL</span>
|
||||
<span>{t('webview.orAnyOtherUrl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
207
src/components/WelcomePage.tsx
Normal file
207
src/components/WelcomePage.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Bot, FolderCode, BarChart, ServerCog, FileText, Settings } from "lucide-react";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ClaudiaLogoMinimal } from "@/components/ClaudiaLogo";
|
||||
import { BorderGlowCard } from "@/components/ui/glow-card";
|
||||
|
||||
interface WelcomePageProps {
|
||||
onNavigate: (view: string) => void;
|
||||
onNewSession: () => void;
|
||||
}
|
||||
|
||||
export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mainFeatures = [
|
||||
{
|
||||
id: "agents",
|
||||
icon: Bot,
|
||||
title: t("welcome.agentManagement"),
|
||||
subtitle: t("welcome.agentManagementDesc"),
|
||||
color: "text-orange-500",
|
||||
bgColor: "bg-orange-500/10",
|
||||
view: "cc-agents"
|
||||
},
|
||||
{
|
||||
id: "projects",
|
||||
icon: FolderCode,
|
||||
title: t("welcome.projectManagement"),
|
||||
subtitle: t("welcome.projectManagementDesc"),
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-500/10",
|
||||
view: "projects"
|
||||
}
|
||||
];
|
||||
|
||||
const bottomFeatures = [
|
||||
{
|
||||
id: "usage",
|
||||
icon: BarChart,
|
||||
title: t("welcome.usageStatistics"),
|
||||
subtitle: t("welcome.usageStatisticsDesc"),
|
||||
color: "text-green-500",
|
||||
bgColor: "bg-green-500/10",
|
||||
view: "usage-dashboard"
|
||||
},
|
||||
{
|
||||
id: "mcp",
|
||||
icon: ServerCog,
|
||||
title: t("welcome.mcpBroker"),
|
||||
subtitle: t("welcome.mcpBrokerDesc"),
|
||||
color: "text-purple-500",
|
||||
bgColor: "bg-purple-500/10",
|
||||
view: "mcp"
|
||||
},
|
||||
{
|
||||
id: "claude-md",
|
||||
icon: FileText,
|
||||
title: t("welcome.claudeMd"),
|
||||
subtitle: t("welcome.claudeMdDesc"),
|
||||
color: "text-cyan-500",
|
||||
bgColor: "bg-cyan-500/10",
|
||||
view: "editor"
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
icon: Settings,
|
||||
title: t("welcome.settings"),
|
||||
subtitle: t("welcome.settingsDesc"),
|
||||
color: "text-gray-500",
|
||||
bgColor: "bg-gray-500/10",
|
||||
view: "settings"
|
||||
}
|
||||
];
|
||||
|
||||
const handleCardClick = (view: string) => {
|
||||
onNavigate(view);
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
onNewSession();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-background overflow-hidden">
|
||||
<div className="w-full max-w-6xl px-8">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
className="text-center mb-16"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h1 className="text-5xl font-bold mb-4 flex items-center justify-center gap-4 bg-gradient-to-r from-orange-400 via-pink-500 to-purple-600 bg-clip-text text-transparent">
|
||||
<ClaudiaLogoMinimal size={56} />
|
||||
{t("app.welcome")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-xl">
|
||||
{t("app.tagline")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Main Feature Cards */}
|
||||
<div className="grid grid-cols-2 gap-8 mb-12">
|
||||
{mainFeatures.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.id}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
delay: 0.1 * index,
|
||||
type: "spring",
|
||||
stiffness: 100
|
||||
}}
|
||||
>
|
||||
<BorderGlowCard
|
||||
className="h-full group"
|
||||
onClick={() => handleCardClick(feature.view)}
|
||||
>
|
||||
<div className="p-10">
|
||||
<div className="flex items-start gap-6">
|
||||
<div className={`p-4 ${feature.bgColor} rounded-2xl transition-transform duration-300 group-hover:scale-110 group-hover:rotate-3`}>
|
||||
<feature.icon className={`h-10 w-10 ${feature.color}`} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-3 group-hover:text-primary transition-colors">
|
||||
{feature.title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-base leading-relaxed">
|
||||
{feature.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BorderGlowCard>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom Feature Cards */}
|
||||
<div className="grid grid-cols-4 gap-6 mb-12">
|
||||
{bottomFeatures.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
delay: 0.3 + 0.05 * index,
|
||||
type: "spring",
|
||||
stiffness: 100
|
||||
}}
|
||||
>
|
||||
<BorderGlowCard
|
||||
className="h-36 group"
|
||||
onClick={() => handleCardClick(feature.view)}
|
||||
>
|
||||
<div className="h-full flex flex-col items-center justify-center p-6">
|
||||
<div className={`p-3 ${feature.bgColor} rounded-xl mb-3 transition-transform duration-300 group-hover:scale-110 group-hover:rotate-6`}>
|
||||
<feature.icon className={`h-8 w-8 ${feature.color}`} strokeWidth={1.5} />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold mb-1 group-hover:text-primary transition-colors">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground text-center line-clamp-2">
|
||||
{feature.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</BorderGlowCard>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick Action Button */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: 0.6,
|
||||
type: "spring",
|
||||
stiffness: 100
|
||||
}}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
className="relative px-10 py-7 text-lg font-semibold bg-gradient-to-r from-orange-500 to-pink-500 hover:from-orange-600 hover:to-pink-600 text-white border-0 shadow-2xl hover:shadow-orange-500/25 transition-all duration-300 hover:scale-105 rounded-2xl group overflow-hidden"
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
{/* Shimmer effect on button */}
|
||||
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer" />
|
||||
</div>
|
||||
|
||||
<span className="relative z-10 flex items-center gap-3">
|
||||
<span className="text-2xl">✨</span>
|
||||
{t("welcome.quickStartSession")}
|
||||
<span className="text-2xl">🚀</span>
|
||||
</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
309
src/components/ui/glow-card.tsx
Normal file
309
src/components/ui/glow-card.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface GlowCardProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
glowClassName?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const GlowCard: React.FC<GlowCardProps> = ({
|
||||
children,
|
||||
className,
|
||||
glowClassName,
|
||||
onClick,
|
||||
}) => {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
setMousePosition({ x, y });
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-xl transition-all duration-300",
|
||||
"bg-card hover:shadow-2xl",
|
||||
className
|
||||
)}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Glow effect layer */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 opacity-0 transition-opacity duration-300",
|
||||
isHovered && "opacity-100",
|
||||
"pointer-events-none"
|
||||
)}
|
||||
style={{
|
||||
background: `radial-gradient(600px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(120, 119, 198, 0.1), transparent 40%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Border glow effect */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-xl opacity-0 transition-opacity duration-300",
|
||||
isHovered && "opacity-100",
|
||||
"pointer-events-none"
|
||||
)}
|
||||
style={{
|
||||
background: `radial-gradient(400px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(120, 119, 198, 0.3), transparent 40%)`,
|
||||
padding: "1px",
|
||||
maskImage: "linear-gradient(#000, #000)",
|
||||
maskClip: "content-box, border-box",
|
||||
maskComposite: "exclude",
|
||||
WebkitMaskImage: "linear-gradient(#000, #000)",
|
||||
WebkitMaskClip: "content-box, border-box",
|
||||
WebkitMaskComposite: "xor",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Animated gradient border */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-xl",
|
||||
"bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500",
|
||||
"opacity-0 transition-opacity duration-500",
|
||||
isHovered && "opacity-20 animate-pulse",
|
||||
"pointer-events-none",
|
||||
glowClassName
|
||||
)}
|
||||
style={{
|
||||
padding: "2px",
|
||||
maskImage: "linear-gradient(#000, #000)",
|
||||
maskClip: "content-box, border-box",
|
||||
maskComposite: "exclude",
|
||||
WebkitMaskImage: "linear-gradient(#000, #000)",
|
||||
WebkitMaskClip: "content-box, border-box",
|
||||
WebkitMaskComposite: "xor",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 高级炫光卡片组件,带有更多动画效果
|
||||
export const AdvancedGlowCard: React.FC<GlowCardProps> = ({
|
||||
children,
|
||||
className,
|
||||
glowClassName,
|
||||
onClick,
|
||||
}) => {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const [gradientAngle, setGradientAngle] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHovered) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setGradientAngle((prev) => (prev + 1) % 360);
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isHovered]);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
setMousePosition({ x, y });
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
setGradientAngle(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-xl transition-all duration-500",
|
||||
"bg-card/80 backdrop-blur-sm",
|
||||
"hover:shadow-2xl hover:shadow-primary/20",
|
||||
"hover:scale-[1.02] hover:-translate-y-1",
|
||||
"cursor-pointer",
|
||||
className
|
||||
)}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Main glow effect */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 opacity-0 transition-opacity duration-500",
|
||||
isHovered && "opacity-100",
|
||||
"pointer-events-none"
|
||||
)}
|
||||
style={{
|
||||
background: `radial-gradient(800px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(147, 51, 234, 0.15), transparent 40%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Secondary glow effect */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 opacity-0 transition-opacity duration-500",
|
||||
isHovered && "opacity-60",
|
||||
"pointer-events-none"
|
||||
)}
|
||||
style={{
|
||||
background: `radial-gradient(600px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(59, 130, 246, 0.1), transparent 40%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Animated gradient border */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-xl",
|
||||
"opacity-0 transition-opacity duration-300",
|
||||
isHovered && "opacity-100",
|
||||
"pointer-events-none",
|
||||
glowClassName
|
||||
)}
|
||||
style={{
|
||||
background: `linear-gradient(${gradientAngle}deg, #3b82f6, #8b5cf6, #ec4899, #3b82f6)`,
|
||||
padding: "2px",
|
||||
maskImage: "linear-gradient(#000, #000)",
|
||||
maskClip: "content-box, border-box",
|
||||
maskComposite: "exclude",
|
||||
WebkitMaskImage: "linear-gradient(#000, #000)",
|
||||
WebkitMaskClip: "content-box, border-box",
|
||||
WebkitMaskComposite: "xor",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Shimmer effect */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 opacity-0",
|
||||
isHovered && "opacity-100 animate-shimmer",
|
||||
"pointer-events-none"
|
||||
)}
|
||||
style={{
|
||||
background: "linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.1) 50%, transparent 60%)",
|
||||
animation: isHovered ? "shimmer 1.5s infinite" : "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 简约边框跑马灯卡片组件
|
||||
export const BorderGlowCard: React.FC<GlowCardProps> = ({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-xl transition-all duration-500",
|
||||
"bg-card",
|
||||
"hover:shadow-lg hover:shadow-orange-500/10",
|
||||
"hover:scale-[1.01]",
|
||||
"cursor-pointer",
|
||||
className
|
||||
)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* 橙色边框跑马灯效果 - 只有一小段 */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-xl",
|
||||
"opacity-0 transition-opacity duration-300",
|
||||
isHovered && "opacity-100",
|
||||
"pointer-events-none"
|
||||
)}
|
||||
>
|
||||
{/* 旋转的渐变边框 - 一小段光带 */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-xl",
|
||||
isHovered && "animate-spin-slow"
|
||||
)}
|
||||
style={{
|
||||
background: `conic-gradient(from 0deg,
|
||||
transparent 0deg,
|
||||
transparent 85deg,
|
||||
#fb923c 90deg,
|
||||
#f97316 95deg,
|
||||
#fb923c 100deg,
|
||||
transparent 105deg,
|
||||
transparent 360deg
|
||||
)`,
|
||||
}}
|
||||
/>
|
||||
{/* 内部遮罩,只显示边框 */}
|
||||
<div
|
||||
className="absolute inset-[2px] rounded-xl bg-card"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 静态边框 */}
|
||||
<div className="absolute inset-0 rounded-xl border border-border/50" />
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user