This commit is contained in:
2025-08-07 12:28:47 +08:00
parent 6798be3b42
commit 5910362683
30 changed files with 1606 additions and 469 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>
);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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": {

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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(() => {

View File

@@ -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"

View File

@@ -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
)}
>

View File

@@ -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>
)}

View 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>
);
}

View 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>
);
};