refactor(ui): organize shared components into shared/ directory

- move ccs-logo, code-editor, command-builder, confirm-dialog

- move connection-indicator, docs-link, github-link, etc.

- add barrel export in shared/index.ts
This commit is contained in:
kaitranntt
2025-12-19 19:43:28 -05:00
parent bef9955123
commit 3c7b0e7a65
19 changed files with 1811 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
import { cn } from '@/lib/utils';
interface CcsLogoProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
showText?: boolean;
}
const sizeMap = {
sm: 24,
md: 32,
lg: 48,
};
export function CcsLogo({ size = 'md', className, showText = true }: CcsLogoProps) {
const dimension = sizeMap[size];
return (
<div className={cn('flex items-center gap-2', className)}>
<img
src="/logo/ccs-logo-256.png"
alt="CCS Logo"
width={dimension}
height={dimension}
className="rounded"
/>
{showText && <span className="font-bold text-lg">CCS Config</span>}
</div>
);
}
@@ -0,0 +1,49 @@
/**
* ClaudeKit Badge Button
*
* "Powered by ClaudeKit" badge for navbar, inspired by landing page design.
* Compact version optimized for header placement.
*/
import { cn } from '@/lib/utils';
const CLAUDEKIT_URL = 'https://claudekit.cc?ref=HMNKXOHN';
export function ClaudeKitBadge() {
return (
<a
href={CLAUDEKIT_URL}
target="_blank"
rel="noopener noreferrer"
className={cn(
'group inline-flex items-center gap-2 px-3 py-1.5 rounded-lg',
'bg-accent/10 border-2 border-accent/40',
'hover:bg-accent hover:border-accent',
'transition-all duration-200 shadow-sm hover:shadow-md'
)}
title="Powered by ClaudeKit Framework"
>
<img src="/logos/claudekit-logo.png" alt="ClaudeKit" className="w-5 h-5" />
<span className="flex items-baseline gap-1.5 whitespace-nowrap">
<span
className={cn(
'text-[10px] font-medium uppercase tracking-wide',
'text-muted-foreground group-hover:text-accent-foreground/80',
'transition-colors'
)}
>
Powered by
</span>
<span
className={cn(
'text-xs font-bold text-foreground',
'group-hover:text-accent-foreground',
'transition-colors'
)}
>
ClaudeKit
</span>
</span>
</a>
);
}
+221
View File
@@ -0,0 +1,221 @@
/**
* Code Editor Component
* Lightweight JSON editor with syntax highlighting, line numbers, and validation
* Uses react-simple-code-editor + prism-react-renderer for minimal bundle size (~18KB)
*/
import { useState, useCallback, useMemo } from 'react';
import Editor from 'react-simple-code-editor';
import { Highlight, themes } from 'prism-react-renderer';
import { useTheme } from '@/hooks/use-theme';
import { cn } from '@/lib/utils';
import { isSensitiveKey } from '@/lib/sensitive-keys';
import { AlertCircle, CheckCircle2, Eye, EyeOff } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface CodeEditorProps {
value: string;
onChange: (value: string) => void;
language?: 'json' | 'yaml';
readonly?: boolean;
className?: string;
minHeight?: string;
}
interface ValidationResult {
valid: boolean;
error?: string;
line?: number;
}
/**
* Validate JSON and extract error location
*/
function validateJson(code: string): ValidationResult {
if (!code.trim()) {
return { valid: true };
}
try {
JSON.parse(code);
return { valid: true };
} catch (e) {
const error = e as SyntaxError;
const message = error.message;
// Try to extract line number from error message
// Format: "... at position X" or "... at line Y column Z"
const posMatch = message.match(/position (\d+)/);
if (posMatch) {
const pos = parseInt(posMatch[1], 10);
const lines = code.substring(0, pos).split('\n');
return {
valid: false,
error: message,
line: lines.length,
};
}
return {
valid: false,
error: message,
};
}
}
export function CodeEditor({
value,
onChange,
language = 'json',
readonly = false,
className,
minHeight = '300px',
}: CodeEditorProps) {
const { isDark } = useTheme();
const [isFocused, setIsFocused] = useState(false);
const [isMasked, setIsMasked] = useState(true);
// Validate on every change for JSON
const validation = useMemo(() => {
if (language === 'json') {
return validateJson(value);
}
return { valid: true };
}, [value, language]);
// Highlight function using prism-react-renderer
// Note: Line numbers removed - they break textarea/pre alignment in react-simple-code-editor
const highlightCode = useCallback(
(code: string) => (
<Highlight theme={isDark ? themes.nightOwl : themes.github} code={code} language={language}>
{({ tokens, getLineProps, getTokenProps }) => {
let nextValueIsSensitive = false;
return (
<>
{tokens.map((line, i) => (
<div
key={i}
{...getLineProps({ line })}
className={cn(validation.line === i + 1 && 'bg-destructive/20')}
>
{line.map((token, key) => {
let isSensitive = false;
// Check for sensitive keys
if (token.types.includes('property')) {
const content = token.content.replace(/['"]/g, '');
// Use shared sensitive key detection utility
if (isSensitiveKey(content)) {
nextValueIsSensitive = true;
} else {
nextValueIsSensitive = false;
}
}
// Apply masking to values following sensitive keys
else if (
(token.types.includes('string') ||
token.types.includes('number') ||
token.types.includes('boolean')) &&
nextValueIsSensitive
) {
isSensitive = true;
// Consumes the flag for this value
nextValueIsSensitive = false;
}
// Reset flag on commas or new keys (handled by property check),
// but persist through colons and whitespace
else if (token.types.includes('punctuation')) {
if (token.content !== ':' && token.content !== '[' && token.content !== '{') {
nextValueIsSensitive = false;
}
}
const tokenProps = getTokenProps({ token });
if (isSensitive && isMasked) {
tokenProps.className = cn(
tokenProps.className,
'blur-[3px] select-none opacity-70 transition-all duration-200'
);
}
return <span key={key} {...tokenProps} />;
})}
</div>
))}
</>
);
}}
</Highlight>
),
[isDark, language, validation.line, isMasked]
);
return (
<div className={cn('flex flex-col', className)}>
{/* Editor container */}
<div
className={cn(
'relative rounded-md border overflow-hidden',
'bg-muted/30',
isFocused && 'ring-2 ring-ring ring-offset-2 ring-offset-background',
readonly && 'opacity-70 cursor-not-allowed',
!validation.valid && 'border-destructive'
)}
style={{ minHeight }}
>
<Editor
value={value}
onValueChange={readonly ? () => {} : onChange}
highlight={highlightCode}
key={isDark ? 'dark-editor' : 'light-editor'}
padding={12}
disabled={readonly}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
textareaClassName={cn(
'focus:outline-none font-mono text-sm',
readonly && 'cursor-not-allowed'
)}
preClassName="font-mono text-sm"
style={{
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
fontSize: '0.875rem',
minHeight,
}}
/>
{/* Secrets Toggle Overlay */}
<div className="absolute top-2 right-2 z-10 opacity-50 hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 bg-background/50 hover:bg-background border shadow-sm rounded-full"
onClick={() => setIsMasked(!isMasked)}
title={isMasked ? 'Reveal sensitive values' : 'Mask sensitive values'}
>
{isMasked ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3" />}
</Button>
</div>
</div>
{/* Validation status */}
<div className="flex items-center gap-2 mt-2 text-xs">
{validation.valid ? (
<span className="flex items-center gap-1 text-muted-foreground">
<CheckCircle2 className="w-3 h-3 text-green-500" />
Valid {language.toUpperCase()}
</span>
) : (
<span className="flex items-center gap-1 text-destructive">
<AlertCircle className="w-3 h-3" />
{validation.error}
{validation.line && ` (line ${validation.line})`}
</span>
)}
{readonly && <span className="ml-auto text-muted-foreground">(Read-only)</span>}
</div>
</div>
);
}
@@ -0,0 +1,148 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { CopyIcon, PlayIcon, TerminalIcon } from 'lucide-react';
interface Command {
id: string;
command: string;
description: string;
category: string;
}
const commonCommands: Command[] = [
{
id: '1',
command: 'ccs config',
description: 'Open configuration interface',
category: 'Config',
},
{
id: '2',
command: 'ccs profile create --name my-profile',
description: 'Create a new profile',
category: 'Profile',
},
{
id: '3',
command: 'ccs profile switch --name my-profile',
description: 'Switch to a profile',
category: 'Profile',
},
{
id: '4',
command: 'ccs doctor',
description: 'Check system health',
category: 'Diagnostics',
},
{
id: '5',
command: 'ccs cliproxy list',
description: 'List available CLIProxy providers',
category: 'CLIProxy',
},
{
id: '6',
command: 'ccs cliproxy add --provider gemini --token YOUR_TOKEN',
description: 'Add CLIProxy provider',
category: 'CLIProxy',
},
];
export function CommandBuilder() {
const [command, setCommand] = useState('');
const [filteredCommands, setFilteredCommands] = useState(commonCommands);
const handleCommandChange = (value: string) => {
setCommand(value);
const filtered = commonCommands.filter(
(cmd) =>
cmd.command.toLowerCase().includes(value.toLowerCase()) ||
cmd.description.toLowerCase().includes(value.toLowerCase())
);
setFilteredCommands(filtered);
};
const handleCommandSelect = (cmd: string) => {
setCommand(cmd);
setFilteredCommands(commonCommands);
};
const handleCopy = () => {
navigator.clipboard.writeText(command);
};
const handleRun = () => {
console.log('Running command:', command);
};
const categories = Array.from(new Set(commonCommands.map((cmd) => cmd.category)));
return (
<Card className="h-[400px] flex flex-col">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TerminalIcon className="w-5 h-5" />
Command Builder
</CardTitle>
</CardHeader>
<CardContent className="flex-1 flex flex-col space-y-4">
<div className="space-y-2">
<Input
placeholder="Type or select a command..."
value={command}
onChange={(e) => handleCommandChange(e.target.value)}
className="font-mono"
/>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleCopy} disabled={!command}>
<CopyIcon className="w-4 h-4 mr-1" />
Copy
</Button>
<Button size="sm" onClick={handleRun} disabled={!command}>
<PlayIcon className="w-4 h-4 mr-1" />
Run
</Button>
</div>
</div>
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<Badge
key={category}
variant="secondary"
className="text-xs cursor-pointer hover:bg-secondary/80"
onClick={() => {
const categoryCommands = commonCommands.filter((cmd) => cmd.category === category);
console.log(`${category} commands:`, categoryCommands);
}}
>
{category}
</Badge>
))}
</div>
<ScrollArea className="flex-1">
<div className="space-y-2">
{filteredCommands.map((cmd) => (
<div
key={cmd.id}
className="p-3 rounded-lg border cursor-pointer hover:bg-accent/50 transition-colors"
onClick={() => handleCommandSelect(cmd.command)}
>
<div className="font-mono text-sm">{cmd.command}</div>
<div className="text-xs text-muted-foreground mt-1">{cmd.description}</div>
<Badge variant="outline" className="mt-2 text-xs">
{cmd.category}
</Badge>
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
);
}
@@ -0,0 +1,50 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
interface ConfirmDialogProps {
open: boolean;
onConfirm: () => void;
onCancel: () => void;
title: string;
description: string;
confirmText?: string;
variant?: 'default' | 'destructive';
}
export function ConfirmDialog({
open,
onConfirm,
onCancel,
title,
description,
confirmText = 'Confirm',
variant = 'default',
}: ConfirmDialogProps) {
return (
<AlertDialog open={open} onOpenChange={(isOpen) => !isOpen && onCancel()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirm}
className={variant === 'destructive' ? 'bg-red-600 hover:bg-red-700' : ''}
>
{confirmText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
@@ -0,0 +1,28 @@
/**
* Connection Indicator (Phase 04)
*
* Shows WebSocket connection status in the header.
*/
import { Wifi, WifiOff } from 'lucide-react';
import { useWebSocket } from '@/hooks/use-websocket';
export function ConnectionIndicator() {
const { status } = useWebSocket();
const statusConfig = {
connected: { icon: Wifi, color: 'text-green-600', label: 'Connected' },
connecting: { icon: Wifi, color: 'text-yellow-500', label: 'Connecting...' },
disconnected: { icon: WifiOff, color: 'text-red-500', label: 'Disconnected' },
};
const config = statusConfig[status];
const Icon = config.icon;
return (
<div className={`flex items-center gap-1 text-sm ${config.color}`}>
<Icon className="w-4 h-4" />
<span className="hidden sm:inline">{config.label}</span>
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Docs Link Button
*
* Links to CCS documentation site for guides and reference.
*/
import { BookOpen } from 'lucide-react';
import { Button } from '@/components/ui/button';
const DOCS_URL = 'https://docs.ccs.kaitran.ca';
export function DocsLink() {
return (
<Button variant="ghost" size="icon" asChild title="View documentation">
<a href={DOCS_URL} target="_blank" rel="noopener noreferrer">
<BookOpen className="w-4 h-4" />
</a>
</Button>
);
}
+20
View File
@@ -0,0 +1,20 @@
/**
* GitHub Link Button
*
* Links to CCS GitHub issues page for bug reports and feature requests.
*/
import { Github } from 'lucide-react';
import { Button } from '@/components/ui/button';
const GITHUB_REPO_URL = 'https://github.com/kaitranntt/ccs/issues';
export function GitHubLink() {
return (
<Button variant="ghost" size="icon" asChild title="Report an issue on GitHub">
<a href={GITHUB_REPO_URL} target="_blank" rel="noopener noreferrer">
<Github className="w-4 h-4" />
</a>
</Button>
);
}
@@ -0,0 +1,132 @@
/**
* Global Environment Variables Indicator
*
* Shows which env vars from global_env will be injected at runtime.
* Displayed below the Raw Configuration (JSON) section in profile editors.
*/
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Settings2, ChevronDown, ChevronUp, ExternalLink, Info } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface GlobalEnvConfig {
enabled: boolean;
env: Record<string, string>;
}
interface GlobalEnvIndicatorProps {
/** Current profile's env vars (to show which are overridden) */
profileEnv?: Record<string, string>;
}
export function GlobalEnvIndicator({ profileEnv = {} }: GlobalEnvIndicatorProps) {
const [config, setConfig] = useState<GlobalEnvConfig | null>(null);
const [loading, setLoading] = useState(true);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
fetchConfig();
}, []);
const fetchConfig = async () => {
try {
setLoading(true);
const res = await fetch('/api/global-env');
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
setConfig(data);
} catch {
setConfig(null);
} finally {
setLoading(false);
}
};
// Don't render if loading or disabled or no vars
if (loading) return null;
if (!config?.enabled) return null;
const envVars = config.env || {};
const envKeys = Object.keys(envVars);
if (envKeys.length === 0) return null;
// Check which keys are already in profile (won't be overridden)
const injectedKeys = envKeys.filter((key) => !(key in profileEnv));
const overriddenKeys = envKeys.filter((key) => key in profileEnv);
return (
<div className="border-t bg-muted/20">
{/* Header - clickable to expand */}
<button
onClick={() => setExpanded(!expanded)}
className="w-full px-4 py-2 flex items-center gap-2 hover:bg-muted/30 transition-colors"
>
<Info className="w-4 h-4 text-blue-500" />
<span className="text-xs text-muted-foreground flex-1 text-left">
<span className="font-medium text-foreground">{injectedKeys.length}</span> global env var
{injectedKeys.length !== 1 ? 's' : ''} will be injected at runtime
{overriddenKeys.length > 0 && (
<span className="text-amber-600 dark:text-amber-400 ml-1">
({overriddenKeys.length} overridden by profile)
</span>
)}
</span>
{expanded ? (
<ChevronUp className="w-4 h-4 text-muted-foreground" />
) : (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
)}
</button>
{/* Expanded content */}
{expanded && (
<div className="px-4 pb-3 space-y-2">
{/* Injected vars */}
{injectedKeys.length > 0 && (
<div className="space-y-1">
{injectedKeys.map((key) => (
<div
key={key}
className="flex items-center gap-2 text-xs font-mono bg-green-500/10 text-green-700 dark:text-green-400 px-2 py-1 rounded"
>
<span className="text-green-500">+</span>
<span className="truncate">
{key}={envVars[key]}
</span>
</div>
))}
</div>
)}
{/* Overridden vars (profile takes precedence) */}
{overriddenKeys.length > 0 && (
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Skipped (profile already defines):</p>
{overriddenKeys.map((key) => (
<div
key={key}
className="flex items-center gap-2 text-xs font-mono bg-amber-500/10 text-amber-700 dark:text-amber-400 px-2 py-1 rounded"
>
<span className="text-amber-500">~</span>
<span className="truncate">{key}</span>
</div>
))}
</div>
)}
{/* Link to settings */}
<div className="pt-2 border-t border-border/50">
<Button variant="ghost" size="sm" asChild className="h-7 text-xs gap-1.5 -ml-2">
<Link to="/settings?tab=globalenv">
<Settings2 className="w-3.5 h-3.5" />
Configure in Settings
<ExternalLink className="w-3 h-3 opacity-50" />
</Link>
</Button>
</div>
</div>
)}
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
/**
* Shared Components Barrel Export
*/
export { CcsLogo } from './ccs-logo';
export { ClaudeKitBadge } from './claudekit-badge';
export { CodeEditor } from './code-editor';
export { CommandBuilder } from './command-builder';
export { ConfirmDialog } from './confirm-dialog';
export { ConnectionIndicator } from './connection-indicator';
export { DocsLink } from './docs-link';
export { GitHubLink } from './github-link';
export { GlobalEnvIndicator } from './global-env-indicator';
export { LocalhostDisclaimer } from './localhost-disclaimer';
export { PrivacyToggle } from './privacy-toggle';
export { ProjectSelectionDialog } from './project-selection-dialog';
export { ProviderIcon } from './provider-icon';
export { QuickCommands } from './quick-commands';
export { SettingsDialog } from './settings-dialog';
export { SponsorButton } from './sponsor-button';
export { StatCard } from './stat-card';
export { ValueMetrics } from './value-metrics';
@@ -0,0 +1,29 @@
import { Shield, X } from 'lucide-react';
import { useState } from 'react';
export function LocalhostDisclaimer() {
const [dismissed, setDismissed] = useState(false);
if (dismissed) return null;
return (
<div className="w-full bg-yellow-50 dark:bg-yellow-900/20 border-t border-yellow-200 dark:border-yellow-800 px-4 py-2 transition-colors duration-200">
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2 text-sm text-yellow-800 dark:text-yellow-200">
<Shield className="w-4 h-4 flex-shrink-0" />
<span className="hidden sm:inline">
This dashboard runs locally. All data stays on your machine.
</span>
<span className="sm:hidden">Local dashboard - data stays on your device.</span>
</div>
<button
onClick={() => setDismissed(true)}
className="text-yellow-600 hover:text-yellow-800 dark:text-yellow-400 flex-shrink-0 p-1 rounded hover:bg-yellow-100 dark:hover:bg-yellow-800/30 transition-colors"
aria-label="Dismiss disclaimer"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
);
}
@@ -0,0 +1,39 @@
/**
* Privacy Toggle Button
* Toggles demo mode to blur personal information (emails, account IDs)
*/
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { usePrivacy } from '@/contexts/privacy-context';
import { Eye, EyeOff } from 'lucide-react';
import { cn } from '@/lib/utils';
export function PrivacyToggle() {
const { privacyMode, togglePrivacyMode } = usePrivacy();
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={togglePrivacyMode}
className={cn(
'h-8 w-8 transition-colors',
privacyMode && 'text-amber-600 hover:text-amber-700 dark:text-amber-400'
)}
>
{privacyMode ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{privacyMode
? 'Privacy mode ON - Click to show data'
: 'Privacy mode OFF - Click to hide data'}
</p>
</TooltipContent>
</Tooltip>
);
}
@@ -0,0 +1,203 @@
/**
* Project Selection Dialog Component
*
* Displays during OAuth flow when CLIProxyAPI requires user to select
* a Google Cloud project. Shows list of available projects with option
* to select one or ALL.
*/
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Loader2, FolderOpen, Check, Circle, CheckCircle } from 'lucide-react';
interface GCloudProject {
id: string;
name: string;
index: number;
}
interface ProjectSelectionDialogProps {
open: boolean;
onClose: () => void;
sessionId: string;
provider: string;
projects: GCloudProject[];
defaultProjectId: string;
supportsAll: boolean;
onSelect: (selectedId: string) => Promise<void>;
/** Timeout in seconds before auto-selecting default */
timeoutSeconds?: number;
}
export function ProjectSelectionDialog({
open,
onClose,
sessionId,
provider,
projects,
defaultProjectId,
supportsAll,
onSelect,
timeoutSeconds = 30,
}: ProjectSelectionDialogProps) {
const [selectedId, setSelectedId] = useState(defaultProjectId);
const [isSubmitting, setIsSubmitting] = useState(false);
const [countdown, setCountdown] = useState(timeoutSeconds);
// Countdown timer for auto-selection
useEffect(() => {
if (!open) {
setCountdown(timeoutSeconds);
return;
}
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
// Auto-submit on timeout
handleSubmit(true);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, timeoutSeconds]);
// Reset state when dialog opens
useEffect(() => {
if (open) {
setSelectedId(defaultProjectId);
setIsSubmitting(false);
setCountdown(timeoutSeconds);
}
}, [open, defaultProjectId, timeoutSeconds]);
const handleSubmit = async (isTimeout = false) => {
if (isSubmitting) return;
setIsSubmitting(true);
try {
// Empty string means use default (press Enter behavior)
const submitId = isTimeout ? '' : selectedId;
await onSelect(submitId);
onClose();
} catch (error) {
console.error('Failed to submit project selection:', error);
// On error, submit empty to use default
try {
await onSelect('');
} catch {
// Ignore double-error
}
onClose();
}
};
const providerDisplay = provider.charAt(0).toUpperCase() + provider.slice(1);
// Suppress unused variable warning - sessionId used for identification
void sessionId;
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && !isSubmitting && onClose()}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderOpen className="w-5 h-5" />
Select Google Cloud Project
</DialogTitle>
<DialogDescription>
Choose which project to use for {providerDisplay} authentication.
{countdown > 0 && (
<span className="text-muted-foreground ml-1">
(Auto-selecting default in {countdown}s)
</span>
)}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
{projects.map((project) => (
<div
key={project.id}
className={`flex items-center space-x-3 p-3 rounded-lg border cursor-pointer transition-colors ${
selectedId === project.id
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
}`}
onClick={() => !isSubmitting && setSelectedId(project.id)}
>
{selectedId === project.id ? (
<CheckCircle className="w-5 h-5 text-primary" />
) : (
<Circle className="w-5 h-5 text-muted-foreground" />
)}
<div className="flex-1">
<div className="font-medium">{project.name}</div>
<div className="text-sm text-muted-foreground font-mono">{project.id}</div>
</div>
{project.id === defaultProjectId && (
<span className="text-xs px-2 py-1 bg-secondary rounded">Default</span>
)}
</div>
))}
{supportsAll && (
<div
className={`flex items-center space-x-3 p-3 rounded-lg border cursor-pointer transition-colors ${
selectedId === 'ALL'
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
}`}
onClick={() => !isSubmitting && setSelectedId('ALL')}
>
{selectedId === 'ALL' ? (
<CheckCircle className="w-5 h-5 text-primary" />
) : (
<Circle className="w-5 h-5 text-muted-foreground" />
)}
<div className="flex-1">
<div className="font-medium">All Projects</div>
<div className="text-sm text-muted-foreground">
Onboard all {projects.length} listed projects
</div>
</div>
</div>
)}
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<Button variant="ghost" onClick={() => handleSubmit(true)} disabled={isSubmitting}>
Use Default
</Button>
<Button onClick={() => handleSubmit(false)} disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Selecting...
</>
) : (
<>
<Check className="w-4 h-4 mr-2" />
Confirm Selection
</>
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,95 @@
/**
* Provider Icon Component
* Renders provider logos from /assets/providers/
* Supports white background circle variant for dark themes
*/
import { cn } from '@/lib/utils';
import { PROVIDER_ASSETS, PROVIDER_COLORS } from '@/lib/provider-config';
interface ProviderIconProps {
provider: string;
className?: string;
size?: number;
/** White background circle variant for better visibility */
withBackground?: boolean;
}
export function ProviderIcon({
provider,
className,
size = 18,
withBackground = false,
}: ProviderIconProps) {
const normalized = provider.toLowerCase();
const assetPath = PROVIDER_ASSETS[normalized];
// Icon size is smaller when inside background circle
const iconSize = withBackground ? Math.floor(size * 0.65) : size;
const iconElement = assetPath ? (
<img
src={assetPath}
alt={`${provider} icon`}
width={iconSize}
height={iconSize}
className="shrink-0 object-contain"
/>
) : (
// Fallback: colored text letter
<span
className="font-bold"
style={{
color: PROVIDER_COLORS[normalized] || '#6b7280',
fontSize: iconSize * 0.6,
}}
>
{provider.charAt(0).toUpperCase()}
</span>
);
if (withBackground) {
return (
<div
className={cn(
'shrink-0 rounded-full bg-white border border-border flex items-center justify-center shadow-sm',
className
)}
style={{ width: size, height: size }}
>
{iconElement}
</div>
);
}
// Without background - original behavior for logos, colored circle for fallback
if (assetPath) {
return (
<img
src={assetPath}
alt={`${provider} icon`}
width={size}
height={size}
className={cn('shrink-0 rounded-sm object-contain', className)}
/>
);
}
const bgColor = PROVIDER_COLORS[normalized] || '#6b7280';
return (
<div
className={cn(
'shrink-0 rounded-full flex items-center justify-center text-white font-bold',
className
)}
style={{
width: size,
height: size,
backgroundColor: bgColor,
fontSize: size * 0.5,
}}
>
{provider.charAt(0).toUpperCase()}
</div>
);
}
@@ -0,0 +1,92 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Copy, Check, Terminal } from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/lib/utils';
interface CommandSnippet {
label: string;
command: string;
description: string;
}
const defaultSnippets: CommandSnippet[] = [
{
label: 'Start Default',
command: 'ccs',
description: 'Launch Claude with default profile',
},
{
label: 'GLM Profile',
command: 'ccs glm',
description: 'Switch to GLM model',
},
{
label: 'Health Check',
command: 'ccs doctor',
description: 'Run system diagnostics',
},
{
label: 'Delegate Task',
command: 'ccs glm -p "your task"',
description: 'Delegate to GLM profile',
},
];
interface QuickCommandsProps {
snippets?: CommandSnippet[];
}
export function QuickCommands({ snippets = defaultSnippets }: QuickCommandsProps) {
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const copyToClipboard = async (text: string, index: number) => {
await navigator.clipboard.writeText(text);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
};
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Terminal className="w-5 h-5 text-muted-foreground" />
Quick Commands
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{snippets.map((snippet, index) => (
<div
key={index}
className={cn(
'group flex items-center justify-between gap-2 px-3 py-2',
'rounded-lg border bg-muted/30 hover:bg-muted/50 transition-colors'
)}
>
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground">{snippet.label}</p>
<code className="text-sm font-mono font-medium truncate block">
{snippet.command}
</code>
</div>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => copyToClipboard(snippet.command, index)}
title={snippet.description}
>
{copiedIndex === index ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
))}
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,385 @@
/**
* Settings Dialog Component
* Reusable dialog for editing profile environment variables
* Features: masked inputs for sensitive keys, conflict detection, save/cancel, raw JSON editor
*/
import { useState, useMemo, useCallback, lazy, Suspense } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { MaskedInput } from '@/components/ui/masked-input';
import { ConfirmDialog } from './confirm-dialog';
import { Save, X, Loader2, Code2 } from 'lucide-react';
import { toast } from 'sonner';
// Lazy load CodeEditor to reduce initial bundle size
const CodeEditor = lazy(() => import('./code-editor').then((m) => ({ default: m.CodeEditor })));
interface Settings {
env?: Record<string, string>;
}
interface SettingsResponse {
profile: string;
settings: Settings;
mtime: number;
path: string;
}
interface SettingsDialogProps {
open: boolean;
onClose: () => void;
profileName: string | null;
}
/**
* Inner component that manages local edits state
* Gets unmounted/remounted via key prop when dialog closes/opens
*/
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
function SettingsDialogContent({
profileName,
onClose,
}: {
profileName: string;
onClose: () => void;
}) {
const [localEdits, setLocalEdits] = useState<Record<string, string>>({});
const [conflictDialog, setConflictDialog] = useState(false);
const [rawJsonEdits, setRawJsonEdits] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState('env');
const queryClient = useQueryClient();
// Fetch settings for selected profile
const { data, isLoading, refetch } = useQuery<SettingsResponse>({
queryKey: ['settings', profileName],
queryFn: () => fetch(`/api/settings/${profileName}/raw`).then((r) => r.json()),
});
// Derive raw JSON content: use edits if available, otherwise serialize from data
const settings = data?.settings;
const rawJsonContent = useMemo(() => {
if (rawJsonEdits !== null) {
return rawJsonEdits;
}
if (settings) {
return JSON.stringify(settings, null, 2);
}
return '';
}, [rawJsonEdits, settings]);
// Update raw JSON when user edits
const handleRawJsonChange = useCallback((value: string) => {
setRawJsonEdits(value);
}, []);
// Derive current settings by merging original data with local edits
const currentSettings = useMemo((): Settings | undefined => {
const settings = data?.settings;
if (!settings) return undefined;
return {
...settings,
env: {
...settings.env,
...localEdits,
},
};
}, [data?.settings, localEdits]);
// Check if raw JSON is valid
const isRawJsonValid = useMemo(() => {
try {
JSON.parse(rawJsonContent);
return true;
} catch {
return false;
}
}, [rawJsonContent]);
// Save mutation
const saveMutation = useMutation({
mutationFn: async () => {
let settingsToSave: Settings;
// Determine what to save based on active tab
if (activeTab === 'raw') {
// Parse raw JSON content
try {
settingsToSave = JSON.parse(rawJsonContent);
} catch {
throw new Error('Invalid JSON');
}
} else {
// Use form-based edits
settingsToSave = {
...data?.settings,
env: {
...data?.settings?.env,
...localEdits,
},
};
}
const res = await fetch(`/api/settings/${profileName}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
settings: settingsToSave,
expectedMtime: data?.mtime,
}),
});
if (res.status === 409) {
throw new Error('CONFLICT');
}
if (!res.ok) {
throw new Error('Failed to save');
}
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['settings', profileName] });
queryClient.invalidateQueries({ queryKey: ['profiles'] });
toast.success('Settings saved');
onClose();
},
onError: (error: Error) => {
if (error.message === 'CONFLICT') {
setConflictDialog(true);
} else {
toast.error(error.message);
}
},
});
const handleSave = () => {
saveMutation.mutate();
};
const handleConflictResolve = async (overwrite: boolean) => {
setConflictDialog(false);
if (overwrite) {
// Refetch to get new mtime, then save
await refetch();
saveMutation.mutate();
} else {
// Discard local changes and close
onClose();
}
};
const updateEnvValue = (key: string, value: string) => {
setLocalEdits((prev) => ({
...prev,
[key]: value,
}));
};
const isSensitiveKey = (key: string): boolean => {
// Pattern-based matching for sensitive keys (same as backend)
const sensitivePatterns = [
/^ANTHROPIC_AUTH_TOKEN$/, // Exact match for Anthropic auth token
/_API_KEY$/, // Keys ending with _API_KEY
/_AUTH_TOKEN$/, // Keys ending with _AUTH_TOKEN
/^API_KEY$/, // Exact match for API_KEY
/^AUTH_TOKEN$/, // Exact match for AUTH_TOKEN
/_SECRET$/, // Keys ending with _SECRET
/^SECRET$/, // Exact match for SECRET
];
return sensitivePatterns.some((pattern) => pattern.test(key));
};
return (
<>
<DialogHeader>
<DialogTitle>Edit Profile: {profileName}</DialogTitle>
<DialogDescription>
Configure environment variables and settings for this profile.
</DialogDescription>
</DialogHeader>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
<span className="ml-3 text-muted-foreground">Loading settings...</span>
</div>
) : (
<div className="flex flex-col h-[60vh]">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex-1 flex flex-col overflow-hidden"
>
<TabsList className="w-full justify-start border-b rounded-none p-0 h-auto bg-transparent">
<TabsTrigger
value="env"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
Environment
</TabsTrigger>
<TabsTrigger
value="raw"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
<Code2 className="w-4 h-4 mr-1" />
Raw JSON
</TabsTrigger>
<TabsTrigger
value="general"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
General
</TabsTrigger>
</TabsList>
<TabsContent value="env" className="flex-1 overflow-hidden p-4 pt-4 m-0">
<ScrollArea className="h-full pr-4">
{currentSettings?.env && Object.keys(currentSettings.env).length > 0 ? (
<div className="space-y-6">
{Object.entries(currentSettings.env).map(([key, value]) => (
<div key={key} className="space-y-2">
<Label className="text-sm font-medium text-foreground">{key}</Label>
{isSensitiveKey(key) ? (
<MaskedInput
value={value}
onChange={(e) => updateEnvValue(key, e.target.value)}
className="font-mono text-sm"
/>
) : (
<Input
value={value}
onChange={(e) => updateEnvValue(key, e.target.value)}
className="font-mono text-sm"
/>
)}
</div>
))}
</div>
) : (
<div className="py-12 text-center text-muted-foreground bg-muted/30 rounded-lg border border-dashed">
<p>No environment variables configured.</p>
<p className="text-xs mt-1">Add variables in your settings.json file.</p>
</div>
)}
</ScrollArea>
</TabsContent>
<TabsContent value="raw" className="flex-1 overflow-hidden p-4 pt-4 m-0">
<Suspense
fallback={
<div className="flex items-center justify-center h-full">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">Loading editor...</span>
</div>
}
>
<CodeEditor
value={rawJsonContent}
onChange={handleRawJsonChange}
language="json"
minHeight="calc(60vh - 120px)"
/>
</Suspense>
</TabsContent>
<TabsContent value="general" className="p-4 m-0">
<Card>
<CardHeader>
<CardTitle className="text-base">Profile Information</CardTitle>
<CardDescription>Details about this configuration file.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{data && (
<>
<div className="grid grid-cols-3 gap-2 text-sm">
<span className="font-medium text-muted-foreground">Path</span>
<code className="col-span-2 bg-muted p-1 rounded text-xs break-all">
{data.path}
</code>
</div>
<div className="grid grid-cols-3 gap-2 text-sm">
<span className="font-medium text-muted-foreground">Last Modified</span>
<span className="col-span-2">{new Date(data.mtime).toLocaleString()}</span>
</div>
</>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
<div className="pt-4 mt-auto border-t flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onClose}>
<X className="w-4 h-4 mr-2" /> Cancel
</Button>
<Button
onClick={handleSave}
disabled={saveMutation.isPending || (activeTab === 'raw' && !isRawJsonValid)}
>
{saveMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> Saving...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" /> Save Changes
</>
)}
</Button>
</div>
</div>
)}
<ConfirmDialog
open={conflictDialog}
title="File Modified Externally"
description="This settings file was modified by another process. Overwrite with your changes or discard?"
confirmText="Overwrite"
variant="destructive"
onConfirm={() => handleConflictResolve(true)}
onCancel={() => handleConflictResolve(false)}
/>
</>
);
}
export function SettingsDialog({ open, onClose, profileName }: SettingsDialogProps) {
// Handle dialog open/close state changes
const handleOpenChange = useCallback(
(isOpen: boolean) => {
if (!isOpen) {
onClose();
}
},
[onClose]
);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-3xl max-h-[80vh] overflow-y-auto">
{/* Key prop ensures fresh state on each open */}
{open && profileName && (
<SettingsDialogContent
key={`${profileName}-${open}`}
profileName={profileName}
onClose={onClose}
/>
)}
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,46 @@
/**
* Sponsor Button
*
* GitHub Sponsors button for navbar.
* Heart icon with hover animation.
*/
import { Heart } from 'lucide-react';
import { cn } from '@/lib/utils';
const SPONSOR_URL = 'https://github.com/sponsors/kaitranntt';
export function SponsorButton() {
return (
<a
href={SPONSOR_URL}
target="_blank"
rel="noopener noreferrer"
className={cn(
'group inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg',
'bg-pink-500/10 border-2 border-pink-500/40',
'hover:bg-pink-400 hover:border-pink-400',
'transition-all duration-200 shadow-sm hover:shadow-md'
)}
title="Sponsor this project on GitHub"
>
<Heart
className={cn(
'w-4 h-4 text-pink-500',
'group-hover:text-white group-hover:fill-white',
'group-hover:animate-pulse',
'transition-colors'
)}
/>
<span
className={cn(
'text-xs font-bold text-pink-600 dark:text-pink-300',
'group-hover:text-white dark:group-hover:text-white',
'transition-colors'
)}
>
Sponsor
</span>
</a>
);
}
+88
View File
@@ -0,0 +1,88 @@
import { Card, CardContent } from '@/components/ui/card';
import type { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
interface StatCardProps {
title: string;
value: number | string;
icon: LucideIcon;
color?: string;
variant?: 'default' | 'success' | 'warning' | 'error' | 'accent';
subtitle?: string;
onClick?: () => void;
}
const variantStyles = {
default: {
iconBg: 'bg-muted',
iconColor: 'text-muted-foreground',
borderHover: 'hover:border-primary',
},
success: {
iconBg: 'bg-green-500/10',
iconColor: 'text-green-600',
borderHover: 'hover:border-green-500/50',
},
warning: {
iconBg: 'bg-yellow-500/10',
iconColor: 'text-yellow-500',
borderHover: 'hover:border-yellow-500/50',
},
error: {
iconBg: 'bg-red-500/10',
iconColor: 'text-red-500',
borderHover: 'hover:border-red-500/50',
},
accent: {
iconBg: 'bg-accent/10',
iconColor: 'text-accent',
borderHover: 'hover:border-accent/50',
},
};
export function StatCard({
title,
value,
icon: Icon,
color,
variant = 'default',
subtitle,
onClick,
}: StatCardProps) {
const styles = variantStyles[variant];
const iconColorClass = color || styles.iconColor;
return (
<Card
className={cn(
'cursor-pointer transition-all duration-200',
'hover:shadow-lg hover:-translate-y-0.5',
styles.borderHover,
onClick && 'active:scale-[0.98]'
)}
onClick={onClick}
>
<CardContent className="pt-4 pb-4">
<div className="flex items-center gap-4">
{/* Icon Container with background */}
<div
className={cn(
'flex items-center justify-center w-12 h-12 rounded-lg transition-transform duration-200',
styles.iconBg,
'group-hover:scale-105'
)}
>
<Icon className={cn('w-6 h-6', iconColorClass)} />
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<p className="text-sm text-muted-foreground truncate">{title}</p>
<p className={cn('text-2xl font-bold font-mono', iconColorClass)}>{value}</p>
{subtitle && <p className="text-xs text-muted-foreground mt-0.5">{subtitle}</p>}
</div>
</div>
</CardContent>
</Card>
);
}
+114
View File
@@ -0,0 +1,114 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { TrendingUpIcon, TrendingDownIcon, DollarSignIcon, ZapIcon } from 'lucide-react';
interface MetricCardProps {
title: string;
value: string | number;
change?: number;
changeLabel?: string;
icon: React.ReactNode;
trend?: 'up' | 'down' | 'neutral';
}
function MetricCard({ title, value, change, changeLabel, icon, trend }: MetricCardProps) {
const TrendIcon = trend === 'up' ? TrendingUpIcon : trend === 'down' ? TrendingDownIcon : null;
return (
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{icon}
<h3 className="font-medium text-sm text-muted-foreground">{title}</h3>
</div>
{trend && TrendIcon && (
<Badge variant={trend === 'up' ? 'default' : 'destructive'} className="text-xs">
<TrendIcon className="w-3 h-3 mr-1" />
{change && `${Math.abs(change)}%`}
</Badge>
)}
</div>
<div className="mt-2">
<div className="text-2xl font-bold">{value}</div>
{changeLabel && <div className="text-xs text-muted-foreground mt-1">{changeLabel}</div>}
</div>
</CardContent>
</Card>
);
}
export function ValueMetrics() {
// Mock data for demonstration
const metrics = [
{
title: 'API Cost Saved',
value: '$127.50',
change: 23,
changeLabel: 'vs last month',
icon: <DollarSignIcon className="w-4 h-4 text-green-600" />,
trend: 'up' as const,
},
{
title: 'Tokens Saved',
value: '2.4M',
change: 18,
changeLabel: 'through caching',
icon: <ZapIcon className="w-4 h-4 text-blue-600" />,
trend: 'up' as const,
},
{
title: 'Queries Faster',
value: '43%',
change: 12,
changeLabel: 'average speedup',
icon: <TrendingUpIcon className="w-4 h-4 text-purple-600" />,
trend: 'up' as const,
},
{
title: 'Errors Reduced',
value: '-67%',
change: 67,
changeLabel: 'with retry logic',
icon: <TrendingDownIcon className="w-4 h-4 text-red-600" />,
trend: 'down' as const,
},
];
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold">Performance Metrics</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{metrics.map((metric, index) => (
<MetricCard key={index} {...metric} />
))}
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">Monthly Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
<div>
<div className="text-lg font-semibold">$342.10</div>
<div className="text-xs text-muted-foreground">Total Saved</div>
</div>
<div>
<div className="text-lg font-semibold">8.7M</div>
<div className="text-xs text-muted-foreground">Tokens Processed</div>
</div>
<div>
<div className="text-lg font-semibold">1,247</div>
<div className="text-xs text-muted-foreground">Queries Handled</div>
</div>
<div>
<div className="text-lg font-semibold">99.8%</div>
<div className="text-xs text-muted-foreground">Uptime</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}