mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-04 12:21:29 +00:00
* Update DataTab.tsx ## API Key Import Fix We identified and fixed an issue with the API key import functionality in the DataTab component. The problem was that API keys were being stored in localStorage instead of cookies, and the key format was being incorrectly processed. ### Changes Made: 1. **Updated `handleImportAPIKeys` function**: - Changed to store API keys in cookies instead of localStorage - Modified to use provider names directly as keys (e.g., "OpenAI", "Google") - Added logic to skip comment fields (keys starting with "_") - Added page reload after successful import to apply changes immediately 2. **Updated `handleDownloadTemplate` function**: - Changed template format to use provider names as keys - Added explanatory comment in the template - Removed URL-related keys that weren't being used properly 3. **Fixed template format**: - Template now uses the correct format with provider names as keys - Added support for all available providers including Hyperbolic These changes ensure that when users download the template, fill it with their API keys, and import it back, the keys are properly stored in cookies with the correct format that the application expects. * backwards compatible old import template * Update the export / import settings Settings Export/Import Improvements We've completely redesigned the settings export and import functionality to ensure all application settings are properly backed up and restored: Key Improvements Comprehensive Export Format: Now captures ALL settings from both localStorage and cookies, organized into logical categories (core, providers, features, UI, connections, debug, updates) Robust Import System: Automatically detects format version and handles both new and legacy formats with detailed error handling Complete Settings Coverage: Properly exports and imports settings from ALL tabs including: Local provider configurations (Ollama, LMStudio, etc.) Cloud provider API keys (OpenAI, Anthropic, etc.) Feature toggles and preferences UI configurations and tab settings Connection settings (GitHub, Netlify) Debug configurations and logs Technical Details Added version tracking to export files for better compatibility Implemented fallback mechanisms if primary import methods fail Added detailed logging for troubleshooting import/export issues Created helper functions for safer data handling Maintained backward compatibility with older export formats Feature Settings: Feature flags and viewed features Developer mode settings Energy saver mode configurations User Preferences: User profile information Theme settings Tab configurations Connection Settings: Netlify connections Git authentication credentials Any other service connections Debug and System Settings: Debug flags and acknowledged issues Error logs and event logs Update settings and preferences * Update DataTab.tsx * Update GithubConnection.tsx revert the code back as asked * feat: enhance style to match the project * feat:small improvements * feat: add major improvements * Update Dialog.tsx * Delete DataTab.tsx.bak * feat: small updates * Update DataVisualization.tsx * feat: dark mode fix
556 lines
20 KiB
TypeScript
556 lines
20 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { useStore } from '@nanostores/react';
|
|
import { Switch } from '@radix-ui/react-switch';
|
|
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
import { classNames } from '~/utils/classNames';
|
|
import { TabManagement } from '~/components/@settings/shared/components/TabManagement';
|
|
import { TabTile } from '~/components/@settings/shared/components/TabTile';
|
|
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
|
|
import { useFeatures } from '~/lib/hooks/useFeatures';
|
|
import { useNotifications } from '~/lib/hooks/useNotifications';
|
|
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
|
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
|
import {
|
|
tabConfigurationStore,
|
|
developerModeStore,
|
|
setDeveloperMode,
|
|
resetTabConfiguration,
|
|
} from '~/lib/stores/settings';
|
|
import { profileStore } from '~/lib/stores/profile';
|
|
import type { TabType, TabVisibilityConfig, Profile } from './types';
|
|
import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
|
|
import { DialogTitle } from '~/components/ui/Dialog';
|
|
import { AvatarDropdown } from './AvatarDropdown';
|
|
import BackgroundRays from '~/components/ui/BackgroundRays';
|
|
|
|
// Import all tab components
|
|
import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab';
|
|
import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab';
|
|
import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab';
|
|
import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
|
|
import { DataTab } from '~/components/@settings/tabs/data/DataTab';
|
|
import DebugTab from '~/components/@settings/tabs/debug/DebugTab';
|
|
import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
|
|
import UpdateTab from '~/components/@settings/tabs/update/UpdateTab';
|
|
import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
|
|
import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
|
|
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
|
|
import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
|
|
import TaskManagerTab from '~/components/@settings/tabs/task-manager/TaskManagerTab';
|
|
|
|
interface ControlPanelProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
interface TabWithDevType extends TabVisibilityConfig {
|
|
isExtraDevTab?: boolean;
|
|
}
|
|
|
|
interface ExtendedTabConfig extends TabVisibilityConfig {
|
|
isExtraDevTab?: boolean;
|
|
}
|
|
|
|
interface BaseTabConfig {
|
|
id: TabType;
|
|
visible: boolean;
|
|
window: 'user' | 'developer';
|
|
order: number;
|
|
}
|
|
|
|
interface AnimatedSwitchProps {
|
|
checked: boolean;
|
|
onCheckedChange: (checked: boolean) => void;
|
|
id: string;
|
|
label: string;
|
|
}
|
|
|
|
const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
|
profile: 'Manage your profile and account settings',
|
|
settings: 'Configure application preferences',
|
|
notifications: 'View and manage your notifications',
|
|
features: 'Explore new and upcoming features',
|
|
data: 'Manage your data and storage',
|
|
'cloud-providers': 'Configure cloud AI providers and models',
|
|
'local-providers': 'Configure local AI providers and models',
|
|
'service-status': 'Monitor cloud LLM service status',
|
|
connection: 'Check connection status and settings',
|
|
debug: 'Debug tools and system information',
|
|
'event-logs': 'View system events and logs',
|
|
update: 'Check for updates and release notes',
|
|
'task-manager': 'Monitor system resources and processes',
|
|
'tab-management': 'Configure visible tabs and their order',
|
|
};
|
|
|
|
// Beta status for experimental features
|
|
const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']);
|
|
|
|
const BetaLabel = () => (
|
|
<div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20">
|
|
<span className="text-[10px] font-medium text-purple-600 dark:text-purple-400">BETA</span>
|
|
</div>
|
|
);
|
|
|
|
const AnimatedSwitch = ({ checked, onCheckedChange, id, label }: AnimatedSwitchProps) => {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
id={id}
|
|
checked={checked}
|
|
onCheckedChange={onCheckedChange}
|
|
className={classNames(
|
|
'relative inline-flex h-6 w-11 items-center rounded-full',
|
|
'transition-all duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)]',
|
|
'bg-gray-200 dark:bg-gray-700',
|
|
'data-[state=checked]:bg-purple-500',
|
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/20',
|
|
'cursor-pointer',
|
|
'group',
|
|
)}
|
|
>
|
|
<motion.span
|
|
className={classNames(
|
|
'absolute left-[2px] top-[2px]',
|
|
'inline-block h-5 w-5 rounded-full',
|
|
'bg-white shadow-lg',
|
|
'transition-shadow duration-300',
|
|
'group-hover:shadow-md group-active:shadow-sm',
|
|
'group-hover:scale-95 group-active:scale-90',
|
|
)}
|
|
initial={false}
|
|
transition={{
|
|
type: 'spring',
|
|
stiffness: 500,
|
|
damping: 30,
|
|
duration: 0.2,
|
|
}}
|
|
animate={{
|
|
x: checked ? '1.25rem' : '0rem',
|
|
}}
|
|
>
|
|
<motion.div
|
|
className="absolute inset-0 rounded-full bg-white"
|
|
initial={false}
|
|
animate={{
|
|
scale: checked ? 1 : 0.8,
|
|
}}
|
|
transition={{ duration: 0.2 }}
|
|
/>
|
|
</motion.span>
|
|
<span className="sr-only">Toggle {label}</span>
|
|
</Switch>
|
|
<div className="flex items-center gap-2">
|
|
<label
|
|
htmlFor={id}
|
|
className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
|
|
>
|
|
{label}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
// State
|
|
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
|
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
|
const [showTabManagement, setShowTabManagement] = useState(false);
|
|
|
|
// Store values
|
|
const tabConfiguration = useStore(tabConfigurationStore);
|
|
const developerMode = useStore(developerModeStore);
|
|
const profile = useStore(profileStore) as Profile;
|
|
|
|
// Status hooks
|
|
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
|
const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
|
|
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
|
|
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
|
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
|
|
|
// Memoize the base tab configurations to avoid recalculation
|
|
const baseTabConfig = useMemo(() => {
|
|
return new Map(DEFAULT_TAB_CONFIG.map((tab) => [tab.id, tab]));
|
|
}, []);
|
|
|
|
// Add visibleTabs logic using useMemo with optimized calculations
|
|
const visibleTabs = useMemo(() => {
|
|
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
|
console.warn('Invalid tab configuration, resetting to defaults');
|
|
resetTabConfiguration();
|
|
|
|
return [];
|
|
}
|
|
|
|
const notificationsDisabled = profile?.preferences?.notifications === false;
|
|
|
|
// In developer mode, show ALL tabs without restrictions
|
|
if (developerMode) {
|
|
const seenTabs = new Set<TabType>();
|
|
const devTabs: ExtendedTabConfig[] = [];
|
|
|
|
// Process tabs in order of priority: developer, user, default
|
|
const processTab = (tab: BaseTabConfig) => {
|
|
if (!seenTabs.has(tab.id)) {
|
|
seenTabs.add(tab.id);
|
|
devTabs.push({
|
|
id: tab.id,
|
|
visible: true,
|
|
window: 'developer',
|
|
order: tab.order || devTabs.length,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Process tabs in priority order
|
|
tabConfiguration.developerTabs?.forEach((tab) => processTab(tab as BaseTabConfig));
|
|
tabConfiguration.userTabs.forEach((tab) => processTab(tab as BaseTabConfig));
|
|
DEFAULT_TAB_CONFIG.forEach((tab) => processTab(tab as BaseTabConfig));
|
|
|
|
// Add Tab Management tile
|
|
devTabs.push({
|
|
id: 'tab-management' as TabType,
|
|
visible: true,
|
|
window: 'developer',
|
|
order: devTabs.length,
|
|
isExtraDevTab: true,
|
|
});
|
|
|
|
return devTabs.sort((a, b) => a.order - b.order);
|
|
}
|
|
|
|
// Optimize user mode tab filtering
|
|
return tabConfiguration.userTabs
|
|
.filter((tab) => {
|
|
if (!tab?.id) {
|
|
return false;
|
|
}
|
|
|
|
if (tab.id === 'notifications' && notificationsDisabled) {
|
|
return false;
|
|
}
|
|
|
|
return tab.visible && tab.window === 'user';
|
|
})
|
|
.sort((a, b) => a.order - b.order);
|
|
}, [tabConfiguration, developerMode, profile?.preferences?.notifications, baseTabConfig]);
|
|
|
|
// Optimize animation performance with layout animations
|
|
const gridLayoutVariants = {
|
|
hidden: { opacity: 0 },
|
|
visible: {
|
|
opacity: 1,
|
|
transition: {
|
|
staggerChildren: 0.05,
|
|
delayChildren: 0.1,
|
|
},
|
|
},
|
|
};
|
|
|
|
const itemVariants = {
|
|
hidden: { opacity: 0, scale: 0.8 },
|
|
visible: {
|
|
opacity: 1,
|
|
scale: 1,
|
|
transition: {
|
|
type: 'spring',
|
|
stiffness: 200,
|
|
damping: 20,
|
|
mass: 0.6,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Reset to default view when modal opens/closes
|
|
useEffect(() => {
|
|
if (!open) {
|
|
// Reset when closing
|
|
setActiveTab(null);
|
|
setLoadingTab(null);
|
|
setShowTabManagement(false);
|
|
} else {
|
|
// When opening, set to null to show the main view
|
|
setActiveTab(null);
|
|
}
|
|
}, [open]);
|
|
|
|
// Handle closing
|
|
const handleClose = () => {
|
|
setActiveTab(null);
|
|
setLoadingTab(null);
|
|
setShowTabManagement(false);
|
|
onClose();
|
|
};
|
|
|
|
// Handlers
|
|
const handleBack = () => {
|
|
if (showTabManagement) {
|
|
setShowTabManagement(false);
|
|
} else if (activeTab) {
|
|
setActiveTab(null);
|
|
}
|
|
};
|
|
|
|
const handleDeveloperModeChange = (checked: boolean) => {
|
|
console.log('Developer mode changed:', checked);
|
|
setDeveloperMode(checked);
|
|
};
|
|
|
|
// Add effect to log developer mode changes
|
|
useEffect(() => {
|
|
console.log('Current developer mode:', developerMode);
|
|
}, [developerMode]);
|
|
|
|
const getTabComponent = (tabId: TabType | 'tab-management') => {
|
|
if (tabId === 'tab-management') {
|
|
return <TabManagement />;
|
|
}
|
|
|
|
switch (tabId) {
|
|
case 'profile':
|
|
return <ProfileTab />;
|
|
case 'settings':
|
|
return <SettingsTab />;
|
|
case 'notifications':
|
|
return <NotificationsTab />;
|
|
case 'features':
|
|
return <FeaturesTab />;
|
|
case 'data':
|
|
return <DataTab />;
|
|
case 'cloud-providers':
|
|
return <CloudProvidersTab />;
|
|
case 'local-providers':
|
|
return <LocalProvidersTab />;
|
|
case 'connection':
|
|
return <ConnectionsTab />;
|
|
case 'debug':
|
|
return <DebugTab />;
|
|
case 'event-logs':
|
|
return <EventLogsTab />;
|
|
case 'update':
|
|
return <UpdateTab />;
|
|
case 'task-manager':
|
|
return <TaskManagerTab />;
|
|
case 'service-status':
|
|
return <ServiceStatusTab />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getTabUpdateStatus = (tabId: TabType): boolean => {
|
|
switch (tabId) {
|
|
case 'update':
|
|
return hasUpdate;
|
|
case 'features':
|
|
return hasNewFeatures;
|
|
case 'notifications':
|
|
return hasUnreadNotifications;
|
|
case 'connection':
|
|
return hasConnectionIssues;
|
|
case 'debug':
|
|
return hasActiveWarnings;
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const getStatusMessage = (tabId: TabType): string => {
|
|
switch (tabId) {
|
|
case 'update':
|
|
return `New update available (v${currentVersion})`;
|
|
case 'features':
|
|
return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
|
|
case 'notifications':
|
|
return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
|
|
case 'connection':
|
|
return currentIssue === 'disconnected'
|
|
? 'Connection lost'
|
|
: currentIssue === 'high-latency'
|
|
? 'High latency detected'
|
|
: 'Connection issues detected';
|
|
case 'debug': {
|
|
const warnings = activeIssues.filter((i) => i.type === 'warning').length;
|
|
const errors = activeIssues.filter((i) => i.type === 'error').length;
|
|
|
|
return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
|
|
}
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
const handleTabClick = (tabId: TabType) => {
|
|
setLoadingTab(tabId);
|
|
setActiveTab(tabId);
|
|
setShowTabManagement(false);
|
|
|
|
// Acknowledge notifications based on tab
|
|
switch (tabId) {
|
|
case 'update':
|
|
acknowledgeUpdate();
|
|
break;
|
|
case 'features':
|
|
acknowledgeAllFeatures();
|
|
break;
|
|
case 'notifications':
|
|
markAllAsRead();
|
|
break;
|
|
case 'connection':
|
|
acknowledgeIssue();
|
|
break;
|
|
case 'debug':
|
|
acknowledgeAllIssues();
|
|
break;
|
|
}
|
|
|
|
// Clear loading state after a delay
|
|
setTimeout(() => setLoadingTab(null), 500);
|
|
};
|
|
|
|
return (
|
|
<RadixDialog.Root open={open}>
|
|
<RadixDialog.Portal>
|
|
<div className="fixed inset-0 flex items-center justify-center z-[100]">
|
|
<RadixDialog.Overlay asChild>
|
|
<motion.div
|
|
className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
/>
|
|
</RadixDialog.Overlay>
|
|
|
|
<RadixDialog.Content
|
|
aria-describedby={undefined}
|
|
onEscapeKeyDown={handleClose}
|
|
onPointerDownOutside={handleClose}
|
|
className="relative z-[101]"
|
|
>
|
|
<motion.div
|
|
className={classNames(
|
|
'w-[1200px] h-[90vh]',
|
|
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
|
'rounded-2xl shadow-2xl',
|
|
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
|
'flex flex-col overflow-hidden',
|
|
'relative',
|
|
)}
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div className="absolute inset-0 overflow-hidden rounded-2xl">
|
|
<BackgroundRays />
|
|
</div>
|
|
<div className="relative z-10 flex flex-col h-full">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center space-x-4">
|
|
{(activeTab || showTabManagement) && (
|
|
<button
|
|
onClick={handleBack}
|
|
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
|
>
|
|
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
|
</button>
|
|
)}
|
|
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
{showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
|
|
</DialogTitle>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-6">
|
|
{/* Mode Toggle */}
|
|
<div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
|
|
<AnimatedSwitch
|
|
id="developer-mode"
|
|
checked={developerMode}
|
|
onCheckedChange={handleDeveloperModeChange}
|
|
label={developerMode ? 'Developer Mode' : 'User Mode'}
|
|
/>
|
|
</div>
|
|
|
|
{/* Avatar and Dropdown */}
|
|
<div className="border-l border-gray-200 dark:border-gray-800 pl-6">
|
|
<AvatarDropdown onSelectTab={handleTabClick} />
|
|
</div>
|
|
|
|
{/* Close Button */}
|
|
<button
|
|
onClick={handleClose}
|
|
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
|
>
|
|
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div
|
|
className={classNames(
|
|
'flex-1',
|
|
'overflow-y-auto',
|
|
'hover:overflow-y-auto',
|
|
'scrollbar scrollbar-w-2',
|
|
'scrollbar-track-transparent',
|
|
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
|
|
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
|
|
'will-change-scroll',
|
|
'touch-auto',
|
|
)}
|
|
>
|
|
<motion.div
|
|
key={activeTab || 'home'}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="p-6"
|
|
>
|
|
{showTabManagement ? (
|
|
<TabManagement />
|
|
) : activeTab ? (
|
|
getTabComponent(activeTab)
|
|
) : (
|
|
<motion.div
|
|
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative"
|
|
variants={gridLayoutVariants}
|
|
initial="hidden"
|
|
animate="visible"
|
|
>
|
|
<AnimatePresence mode="popLayout">
|
|
{(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
|
|
<motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]">
|
|
<TabTile
|
|
tab={tab}
|
|
onClick={() => handleTabClick(tab.id as TabType)}
|
|
isActive={activeTab === tab.id}
|
|
hasUpdate={getTabUpdateStatus(tab.id)}
|
|
statusMessage={getStatusMessage(tab.id)}
|
|
description={TAB_DESCRIPTIONS[tab.id]}
|
|
isLoading={loadingTab === tab.id}
|
|
className="h-full relative"
|
|
>
|
|
{BETA_TABS.has(tab.id) && <BetaLabel />}
|
|
</TabTile>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
)}
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</RadixDialog.Content>
|
|
</div>
|
|
</RadixDialog.Portal>
|
|
</RadixDialog.Root>
|
|
);
|
|
};
|