Final UI V3
Browse files# UI V3 Changelog
Major updates and improvements in this release:
## Core Changes
- Complete NEW REWRITTEN UI system overhaul (V3) with semantic design tokens
- New settings management system with drag-and-drop capabilities
- Enhanced provider system supporting multiple AI services
- Improved theme system with better dark mode support
- New component library with consistent design patterns
## Technical Updates
- Reorganized project architecture for better maintainability
- Performance optimizations and bundle size improvements
- Enhanced security features and access controls
- Improved developer experience with better tooling
- Comprehensive testing infrastructure
## New Features
- Background rays effect for improved visual feedback
- Advanced tab management system
- Automatic and manual update support
- Enhanced error handling and visualization
- Improved accessibility across all components
For detailed information about all changes and improvements, please see the full changelog.
- .gitignore +1 -0
- app/components/@settings/core/AvatarDropdown.tsx +181 -0
- app/components/{settings β @settings/core}/ControlPanel.tsx +197 -345
- app/components/@settings/core/constants.ts +88 -0
- app/components/{settings/settings.types.ts β @settings/core/types.ts} +31 -38
- app/components/@settings/index.ts +14 -0
- app/components/{settings/shared β @settings/shared/components}/DraggableTabList.tsx +2 -2
- app/components/@settings/shared/components/TabManagement.tsx +259 -0
- app/components/{settings/shared β @settings/shared/components}/TabTile.tsx +86 -158
- app/components/{settings β @settings/tabs}/connections/ConnectionsTab.tsx +0 -0
- app/components/{settings β @settings/tabs}/connections/components/ConnectionForm.tsx +1 -1
- app/components/{settings β @settings/tabs}/connections/components/CreateBranchDialog.tsx +1 -1
- app/components/{settings β @settings/tabs}/connections/components/PushToGitHubDialog.tsx +0 -0
- app/components/{settings β @settings/tabs}/connections/components/RepositorySelectionDialog.tsx +0 -0
- app/components/{settings β @settings/tabs}/connections/types/GitHub.ts +0 -0
- app/components/{settings β @settings/tabs}/data/DataTab.tsx +0 -0
- app/components/{settings β @settings/tabs}/debug/DebugTab.tsx +133 -36
- app/components/@settings/tabs/event-logs/EventLogsTab.tsx +613 -0
- app/components/{settings β @settings/tabs}/features/FeaturesTab.tsx +39 -38
- app/components/{settings β @settings/tabs}/notifications/NotificationsTab.tsx +0 -0
- app/components/@settings/tabs/profile/ProfileTab.tsx +174 -0
- app/components/{settings/providers β @settings/tabs/providers/cloud}/CloudProvidersTab.tsx +0 -0
- app/components/{settings/providers β @settings/tabs/providers/local}/LocalProvidersTab.tsx +362 -558
- app/components/@settings/tabs/providers/local/OllamaModelInstaller.tsx +597 -0
- app/components/{settings β @settings/tabs}/providers/service-status/ServiceStatusTab.tsx +0 -0
- app/components/{settings β @settings/tabs}/providers/service-status/base-provider.ts +0 -0
- app/components/{settings β @settings/tabs}/providers/service-status/provider-factory.ts +0 -0
- app/components/{settings β @settings/tabs}/providers/service-status/providers/amazon-bedrock.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/anthropic.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/cohere.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/deepseek.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/google.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/groq.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/huggingface.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/hyperbolic.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/mistral.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/openai.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/openrouter.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/perplexity.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/together.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/providers/xai.ts +2 -2
- app/components/{settings β @settings/tabs}/providers/service-status/types.ts +0 -0
- app/components/{settings/providers β @settings/tabs/providers/status}/ServiceStatusTab.tsx +0 -0
- app/components/{settings β @settings/tabs}/settings/SettingsTab.tsx +1 -1
- app/components/{settings β @settings/tabs}/task-manager/TaskManagerTab.tsx +336 -174
- app/components/{settings β @settings/tabs}/update/UpdateTab.tsx +84 -94
- app/components/@settings/utils/animations.ts +41 -0
- app/components/@settings/utils/tab-helpers.ts +89 -0
- app/components/chat/Chat.client.tsx +32 -25
- app/components/chat/GitCloneButton.tsx +1 -1
|
@@ -44,3 +44,4 @@ changelogUI.md
|
|
| 44 |
docs/instructions/Roadmap.md
|
| 45 |
.cursorrules
|
| 46 |
.cursorrules
|
|
|
|
|
|
| 44 |
docs/instructions/Roadmap.md
|
| 45 |
.cursorrules
|
| 46 |
.cursorrules
|
| 47 |
+
*.md
|
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
| 2 |
+
import { motion } from 'framer-motion';
|
| 3 |
+
import { useStore } from '@nanostores/react';
|
| 4 |
+
import { classNames } from '~/utils/classNames';
|
| 5 |
+
import { profileStore } from '~/lib/stores/profile';
|
| 6 |
+
import type { TabType, Profile } from './types';
|
| 7 |
+
|
| 8 |
+
interface AvatarDropdownProps {
|
| 9 |
+
onSelectTab: (tab: TabType) => void;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
|
| 13 |
+
const profile = useStore(profileStore) as Profile;
|
| 14 |
+
|
| 15 |
+
return (
|
| 16 |
+
<DropdownMenu.Root>
|
| 17 |
+
<DropdownMenu.Trigger asChild>
|
| 18 |
+
<motion.button
|
| 19 |
+
className="group flex items-center justify-center"
|
| 20 |
+
whileHover={{ scale: 1.02 }}
|
| 21 |
+
whileTap={{ scale: 0.98 }}
|
| 22 |
+
>
|
| 23 |
+
<div
|
| 24 |
+
className={classNames(
|
| 25 |
+
'w-10 h-10',
|
| 26 |
+
'rounded-full overflow-hidden',
|
| 27 |
+
'bg-gray-100/50 dark:bg-gray-800/50',
|
| 28 |
+
'flex items-center justify-center',
|
| 29 |
+
'ring-1 ring-gray-200/50 dark:ring-gray-700/50',
|
| 30 |
+
'group-hover:ring-purple-500/50 dark:group-hover:ring-purple-500/50',
|
| 31 |
+
'group-hover:bg-purple-500/10 dark:group-hover:bg-purple-500/10',
|
| 32 |
+
'transition-all duration-200',
|
| 33 |
+
'relative',
|
| 34 |
+
)}
|
| 35 |
+
>
|
| 36 |
+
{profile?.avatar ? (
|
| 37 |
+
<div className="w-full h-full">
|
| 38 |
+
<img
|
| 39 |
+
src={profile.avatar}
|
| 40 |
+
alt={profile?.username || 'Profile'}
|
| 41 |
+
className={classNames(
|
| 42 |
+
'w-full h-full',
|
| 43 |
+
'object-cover',
|
| 44 |
+
'transform-gpu',
|
| 45 |
+
'image-rendering-crisp',
|
| 46 |
+
'group-hover:brightness-110',
|
| 47 |
+
'group-hover:scale-105',
|
| 48 |
+
'transition-all duration-200',
|
| 49 |
+
)}
|
| 50 |
+
loading="eager"
|
| 51 |
+
decoding="sync"
|
| 52 |
+
/>
|
| 53 |
+
<div
|
| 54 |
+
className={classNames(
|
| 55 |
+
'absolute inset-0',
|
| 56 |
+
'ring-1 ring-inset ring-black/5 dark:ring-white/5',
|
| 57 |
+
'group-hover:ring-purple-500/20 dark:group-hover:ring-purple-500/20',
|
| 58 |
+
'group-hover:bg-purple-500/5 dark:group-hover:bg-purple-500/5',
|
| 59 |
+
'transition-colors duration-200',
|
| 60 |
+
)}
|
| 61 |
+
/>
|
| 62 |
+
</div>
|
| 63 |
+
) : (
|
| 64 |
+
<div className="i-ph:robot-fill w-6 h-6 text-gray-400 dark:text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
|
| 65 |
+
)}
|
| 66 |
+
</div>
|
| 67 |
+
</motion.button>
|
| 68 |
+
</DropdownMenu.Trigger>
|
| 69 |
+
|
| 70 |
+
<DropdownMenu.Portal>
|
| 71 |
+
<DropdownMenu.Content
|
| 72 |
+
className={classNames(
|
| 73 |
+
'min-w-[240px] z-[250]',
|
| 74 |
+
'bg-white dark:bg-[#141414]',
|
| 75 |
+
'rounded-lg shadow-lg',
|
| 76 |
+
'border border-gray-200/50 dark:border-gray-800/50',
|
| 77 |
+
'animate-in fade-in-0 zoom-in-95',
|
| 78 |
+
'py-1',
|
| 79 |
+
)}
|
| 80 |
+
sideOffset={5}
|
| 81 |
+
align="end"
|
| 82 |
+
>
|
| 83 |
+
<div
|
| 84 |
+
className={classNames(
|
| 85 |
+
'px-4 py-3 flex items-center gap-3',
|
| 86 |
+
'border-b border-gray-200/50 dark:border-gray-800/50',
|
| 87 |
+
)}
|
| 88 |
+
>
|
| 89 |
+
<div className="w-10 h-10 rounded-full overflow-hidden bg-gray-100/50 dark:bg-gray-800/50 flex-shrink-0">
|
| 90 |
+
{profile?.avatar ? (
|
| 91 |
+
<img
|
| 92 |
+
src={profile.avatar}
|
| 93 |
+
alt={profile?.username || 'Profile'}
|
| 94 |
+
className={classNames('w-full h-full', 'object-cover', 'transform-gpu', 'image-rendering-crisp')}
|
| 95 |
+
loading="eager"
|
| 96 |
+
decoding="sync"
|
| 97 |
+
/>
|
| 98 |
+
) : (
|
| 99 |
+
<div className="w-full h-full flex items-center justify-center">
|
| 100 |
+
<div className="i-ph:robot-fill w-6 h-6 text-gray-400 dark:text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
|
| 101 |
+
</div>
|
| 102 |
+
)}
|
| 103 |
+
</div>
|
| 104 |
+
<div className="flex-1 min-w-0">
|
| 105 |
+
<div className="font-medium text-sm text-gray-900 dark:text-white truncate">
|
| 106 |
+
{profile?.username || 'Guest User'}
|
| 107 |
+
</div>
|
| 108 |
+
{profile?.bio && <div className="text-xs text-gray-500 dark:text-gray-400 truncate">{profile.bio}</div>}
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<DropdownMenu.Item
|
| 113 |
+
className={classNames(
|
| 114 |
+
'flex items-center gap-2 px-4 py-2.5',
|
| 115 |
+
'text-sm text-gray-700 dark:text-gray-200',
|
| 116 |
+
'hover:bg-purple-50 dark:hover:bg-purple-500/10',
|
| 117 |
+
'hover:text-purple-500 dark:hover:text-purple-400',
|
| 118 |
+
'cursor-pointer transition-all duration-200',
|
| 119 |
+
'outline-none',
|
| 120 |
+
'group',
|
| 121 |
+
)}
|
| 122 |
+
onClick={() => onSelectTab('profile')}
|
| 123 |
+
>
|
| 124 |
+
<div className="i-ph:robot-fill w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
|
| 125 |
+
Edit Profile
|
| 126 |
+
</DropdownMenu.Item>
|
| 127 |
+
|
| 128 |
+
<DropdownMenu.Item
|
| 129 |
+
className={classNames(
|
| 130 |
+
'flex items-center gap-2 px-4 py-2.5',
|
| 131 |
+
'text-sm text-gray-700 dark:text-gray-200',
|
| 132 |
+
'hover:bg-purple-50 dark:hover:bg-purple-500/10',
|
| 133 |
+
'hover:text-purple-500 dark:hover:text-purple-400',
|
| 134 |
+
'cursor-pointer transition-all duration-200',
|
| 135 |
+
'outline-none',
|
| 136 |
+
'group',
|
| 137 |
+
)}
|
| 138 |
+
onClick={() => onSelectTab('settings')}
|
| 139 |
+
>
|
| 140 |
+
<div className="i-ph:gear-six-fill w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
|
| 141 |
+
Settings
|
| 142 |
+
</DropdownMenu.Item>
|
| 143 |
+
|
| 144 |
+
<div className="my-1 border-t border-gray-200/50 dark:border-gray-800/50" />
|
| 145 |
+
|
| 146 |
+
<DropdownMenu.Item
|
| 147 |
+
className={classNames(
|
| 148 |
+
'flex items-center gap-2 px-4 py-2.5',
|
| 149 |
+
'text-sm text-gray-700 dark:text-gray-200',
|
| 150 |
+
'hover:bg-purple-50 dark:hover:bg-purple-500/10',
|
| 151 |
+
'hover:text-purple-500 dark:hover:text-purple-400',
|
| 152 |
+
'cursor-pointer transition-all duration-200',
|
| 153 |
+
'outline-none',
|
| 154 |
+
'group',
|
| 155 |
+
)}
|
| 156 |
+
onClick={() => onSelectTab('task-manager')}
|
| 157 |
+
>
|
| 158 |
+
<div className="i-ph:activity-fill w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
|
| 159 |
+
Task Manager
|
| 160 |
+
</DropdownMenu.Item>
|
| 161 |
+
|
| 162 |
+
<DropdownMenu.Item
|
| 163 |
+
className={classNames(
|
| 164 |
+
'flex items-center gap-2 px-4 py-2.5',
|
| 165 |
+
'text-sm text-gray-700 dark:text-gray-200',
|
| 166 |
+
'hover:bg-purple-50 dark:hover:bg-purple-500/10',
|
| 167 |
+
'hover:text-purple-500 dark:hover:text-purple-400',
|
| 168 |
+
'cursor-pointer transition-all duration-200',
|
| 169 |
+
'outline-none',
|
| 170 |
+
'group',
|
| 171 |
+
)}
|
| 172 |
+
onClick={() => onSelectTab('service-status')}
|
| 173 |
+
>
|
| 174 |
+
<div className="i-ph:heartbeat-fill w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
|
| 175 |
+
Service Status
|
| 176 |
+
</DropdownMenu.Item>
|
| 177 |
+
</DropdownMenu.Content>
|
| 178 |
+
</DropdownMenu.Portal>
|
| 179 |
+
</DropdownMenu.Root>
|
| 180 |
+
);
|
| 181 |
+
};
|
|
@@ -3,37 +3,36 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|
| 3 |
import { useStore } from '@nanostores/react';
|
| 4 |
import { Switch } from '@radix-ui/react-switch';
|
| 5 |
import * as RadixDialog from '@radix-ui/react-dialog';
|
| 6 |
-
import { DndProvider } from 'react-dnd';
|
| 7 |
-
import { HTML5Backend } from 'react-dnd-html5-backend';
|
| 8 |
import { classNames } from '~/utils/classNames';
|
| 9 |
-
import { TabManagement } from '
|
| 10 |
-
import { TabTile } from '
|
| 11 |
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
|
| 12 |
import { useFeatures } from '~/lib/hooks/useFeatures';
|
| 13 |
import { useNotifications } from '~/lib/hooks/useNotifications';
|
| 14 |
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
| 15 |
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
| 16 |
import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings';
|
| 17 |
-
import
|
| 18 |
-
import {
|
|
|
|
| 19 |
import { resetTabConfiguration } from '~/lib/stores/settings';
|
| 20 |
import { DialogTitle } from '~/components/ui/Dialog';
|
| 21 |
-
import {
|
| 22 |
|
| 23 |
// Import all tab components
|
| 24 |
-
import ProfileTab from '
|
| 25 |
-
import SettingsTab from '
|
| 26 |
-
import NotificationsTab from '
|
| 27 |
-
import FeaturesTab from '
|
| 28 |
-
import DataTab from '
|
| 29 |
-
import DebugTab from '
|
| 30 |
-
import { EventLogsTab } from '
|
| 31 |
-
import UpdateTab from '
|
| 32 |
-
import ConnectionsTab from '
|
| 33 |
-
import CloudProvidersTab from '
|
| 34 |
-
import ServiceStatusTab from '
|
| 35 |
-
import LocalProvidersTab from '
|
| 36 |
-
import TaskManagerTab from '
|
| 37 |
|
| 38 |
interface ControlPanelProps {
|
| 39 |
open: boolean;
|
|
@@ -58,124 +57,7 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
|
| 58 |
'event-logs': 'View system events and logs',
|
| 59 |
update: 'Check for updates and release notes',
|
| 60 |
'task-manager': 'Monitor system resources and processes',
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
// Add DraggableTabTile component before the ControlPanel component
|
| 64 |
-
const DraggableTabTile = ({
|
| 65 |
-
tab,
|
| 66 |
-
index,
|
| 67 |
-
moveTab,
|
| 68 |
-
...props
|
| 69 |
-
}: {
|
| 70 |
-
tab: TabWithDevType;
|
| 71 |
-
index: number;
|
| 72 |
-
moveTab: (dragIndex: number, hoverIndex: number) => void;
|
| 73 |
-
onClick: () => void;
|
| 74 |
-
isActive: boolean;
|
| 75 |
-
hasUpdate: boolean;
|
| 76 |
-
statusMessage: string;
|
| 77 |
-
description: string;
|
| 78 |
-
isLoading?: boolean;
|
| 79 |
-
}) => {
|
| 80 |
-
const [{ isDragging }, drag] = useDrag({
|
| 81 |
-
type: 'tab',
|
| 82 |
-
item: { index, id: tab.id },
|
| 83 |
-
collect: (monitor) => ({
|
| 84 |
-
isDragging: monitor.isDragging(),
|
| 85 |
-
}),
|
| 86 |
-
});
|
| 87 |
-
|
| 88 |
-
const [{ isOver, canDrop }, drop] = useDrop({
|
| 89 |
-
accept: 'tab',
|
| 90 |
-
hover: (item: { index: number; id: string }, monitor) => {
|
| 91 |
-
if (!monitor.isOver({ shallow: true })) {
|
| 92 |
-
return;
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
if (item.id === tab.id) {
|
| 96 |
-
return;
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
if (item.index === index) {
|
| 100 |
-
return;
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
// Only move when hovering over the middle section
|
| 104 |
-
const hoverBoundingRect = monitor.getSourceClientOffset();
|
| 105 |
-
const clientOffset = monitor.getClientOffset();
|
| 106 |
-
|
| 107 |
-
if (!hoverBoundingRect || !clientOffset) {
|
| 108 |
-
return;
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
const hoverMiddleX = hoverBoundingRect.x + 150; // Half of typical card width
|
| 112 |
-
const hoverClientX = clientOffset.x;
|
| 113 |
-
|
| 114 |
-
// Only perform the move when the mouse has crossed half of the items width
|
| 115 |
-
if (item.index < index && hoverClientX < hoverMiddleX) {
|
| 116 |
-
return;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
if (item.index > index && hoverClientX > hoverMiddleX) {
|
| 120 |
-
return;
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
moveTab(item.index, index);
|
| 124 |
-
item.index = index;
|
| 125 |
-
},
|
| 126 |
-
collect: (monitor) => ({
|
| 127 |
-
isOver: monitor.isOver({ shallow: true }),
|
| 128 |
-
canDrop: monitor.canDrop(),
|
| 129 |
-
}),
|
| 130 |
-
});
|
| 131 |
-
|
| 132 |
-
const dropIndicatorClasses = classNames('rounded-xl border-2 border-transparent transition-all duration-200', {
|
| 133 |
-
'ring-2 ring-purple-500 ring-opacity-50 bg-purple-50 dark:bg-purple-900/20': isOver,
|
| 134 |
-
'hover:ring-2 hover:ring-purple-500/30': canDrop && !isOver,
|
| 135 |
-
});
|
| 136 |
-
|
| 137 |
-
return (
|
| 138 |
-
<motion.div
|
| 139 |
-
ref={(node) => drag(drop(node))}
|
| 140 |
-
style={{
|
| 141 |
-
opacity: isDragging ? 0.5 : 1,
|
| 142 |
-
cursor: 'move',
|
| 143 |
-
position: 'relative',
|
| 144 |
-
zIndex: isDragging ? 100 : isOver ? 50 : 1,
|
| 145 |
-
}}
|
| 146 |
-
animate={{
|
| 147 |
-
scale: isDragging ? 1.02 : isOver ? 1.05 : 1,
|
| 148 |
-
boxShadow: isDragging
|
| 149 |
-
? '0 8px 24px rgba(0, 0, 0, 0.15)'
|
| 150 |
-
: isOver
|
| 151 |
-
? '0 4px 12px rgba(147, 51, 234, 0.3)'
|
| 152 |
-
: '0 0 0 rgba(0, 0, 0, 0)',
|
| 153 |
-
borderColor: isOver ? 'rgb(147, 51, 234)' : isDragging ? 'rgba(147, 51, 234, 0.5)' : 'transparent',
|
| 154 |
-
y: isOver ? -2 : 0,
|
| 155 |
-
}}
|
| 156 |
-
transition={{
|
| 157 |
-
type: 'spring',
|
| 158 |
-
stiffness: 500,
|
| 159 |
-
damping: 30,
|
| 160 |
-
mass: 0.8,
|
| 161 |
-
}}
|
| 162 |
-
className={dropIndicatorClasses}
|
| 163 |
-
>
|
| 164 |
-
<TabTile {...props} tab={tab} />
|
| 165 |
-
{isOver && (
|
| 166 |
-
<motion.div
|
| 167 |
-
className="absolute inset-0 rounded-xl pointer-events-none"
|
| 168 |
-
initial={{ opacity: 0 }}
|
| 169 |
-
animate={{ opacity: 1 }}
|
| 170 |
-
exit={{ opacity: 0 }}
|
| 171 |
-
transition={{ duration: 0.2 }}
|
| 172 |
-
>
|
| 173 |
-
<div className="absolute inset-0 bg-gradient-to-r from-purple-500/10 to-purple-500/20 rounded-xl" />
|
| 174 |
-
<div className="absolute inset-0 border-2 border-purple-500/50 rounded-xl" />
|
| 175 |
-
</motion.div>
|
| 176 |
-
)}
|
| 177 |
-
</motion.div>
|
| 178 |
-
);
|
| 179 |
};
|
| 180 |
|
| 181 |
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
@@ -183,11 +65,11 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 183 |
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
| 184 |
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
| 185 |
const [showTabManagement, setShowTabManagement] = useState(false);
|
| 186 |
-
const [profile, setProfile] = useState({ avatar: null, notifications: true });
|
| 187 |
|
| 188 |
// Store values
|
| 189 |
const tabConfiguration = useStore(tabConfigurationStore);
|
| 190 |
const developerMode = useStore(developerModeStore);
|
|
|
|
| 191 |
|
| 192 |
// Status hooks
|
| 193 |
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
|
@@ -196,24 +78,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 196 |
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
| 197 |
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
| 198 |
|
| 199 |
-
// Initialize profile from localStorage on mount
|
| 200 |
-
useEffect(() => {
|
| 201 |
-
if (typeof window === 'undefined') {
|
| 202 |
-
return;
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
const saved = localStorage.getItem('bolt_user_profile');
|
| 206 |
-
|
| 207 |
-
if (saved) {
|
| 208 |
-
try {
|
| 209 |
-
const parsedProfile = JSON.parse(saved);
|
| 210 |
-
setProfile(parsedProfile);
|
| 211 |
-
} catch (error) {
|
| 212 |
-
console.warn('Failed to parse profile from localStorage:', error);
|
| 213 |
-
}
|
| 214 |
-
}
|
| 215 |
-
}, []);
|
| 216 |
-
|
| 217 |
// Add visibleTabs logic using useMemo
|
| 218 |
const visibleTabs = useMemo(() => {
|
| 219 |
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
|
@@ -248,10 +112,22 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 248 |
};
|
| 249 |
});
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
return devTabs.sort((a, b) => a.order - b.order);
|
| 252 |
}
|
| 253 |
|
| 254 |
// In user mode, only show visible user tabs
|
|
|
|
|
|
|
| 255 |
return tabConfiguration.userTabs
|
| 256 |
.filter((tab) => {
|
| 257 |
if (!tab || typeof tab.id !== 'string') {
|
|
@@ -259,8 +135,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 259 |
return false;
|
| 260 |
}
|
| 261 |
|
| 262 |
-
// Hide notifications tab if notifications are disabled
|
| 263 |
-
if (tab.id === 'notifications' &&
|
| 264 |
return false;
|
| 265 |
}
|
| 266 |
|
|
@@ -268,38 +144,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 268 |
return tab.visible && tab.window === 'user';
|
| 269 |
})
|
| 270 |
.sort((a, b) => a.order - b.order);
|
| 271 |
-
}, [tabConfiguration, profile
|
| 272 |
-
|
| 273 |
-
// Add moveTab handler
|
| 274 |
-
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
| 275 |
-
const newTabs = [...visibleTabs];
|
| 276 |
-
const dragTab = newTabs[dragIndex];
|
| 277 |
-
newTabs.splice(dragIndex, 1);
|
| 278 |
-
newTabs.splice(hoverIndex, 0, dragTab);
|
| 279 |
-
|
| 280 |
-
// Update the order of the tabs
|
| 281 |
-
const updatedTabs = newTabs.map((tab, index) => ({
|
| 282 |
-
...tab,
|
| 283 |
-
order: index,
|
| 284 |
-
window: 'developer' as const,
|
| 285 |
-
visible: true,
|
| 286 |
-
}));
|
| 287 |
-
|
| 288 |
-
// Update the tab configuration store directly
|
| 289 |
-
if (developerMode) {
|
| 290 |
-
// In developer mode, update developerTabs while preserving configuration
|
| 291 |
-
tabConfigurationStore.set({
|
| 292 |
-
...tabConfiguration,
|
| 293 |
-
developerTabs: updatedTabs,
|
| 294 |
-
});
|
| 295 |
-
} else {
|
| 296 |
-
// In user mode, update userTabs
|
| 297 |
-
tabConfigurationStore.set({
|
| 298 |
-
...tabConfiguration,
|
| 299 |
-
userTabs: updatedTabs.map((tab) => ({ ...tab, window: 'user' as const })),
|
| 300 |
-
});
|
| 301 |
-
}
|
| 302 |
-
};
|
| 303 |
|
| 304 |
// Handlers
|
| 305 |
const handleBack = () => {
|
|
@@ -320,8 +165,12 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 320 |
console.log('Current developer mode:', developerMode);
|
| 321 |
}, [developerMode]);
|
| 322 |
|
| 323 |
-
const getTabComponent = () => {
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
case 'profile':
|
| 326 |
return <ProfileTab />;
|
| 327 |
case 'settings':
|
|
@@ -398,6 +247,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 398 |
const handleTabClick = (tabId: TabType) => {
|
| 399 |
setLoadingTab(tabId);
|
| 400 |
setActiveTab(tabId);
|
|
|
|
| 401 |
|
| 402 |
// Acknowledge notifications based on tab
|
| 403 |
switch (tabId) {
|
|
@@ -423,84 +273,75 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 423 |
};
|
| 424 |
|
| 425 |
return (
|
| 426 |
-
<
|
| 427 |
-
<RadixDialog.
|
| 428 |
-
<
|
| 429 |
-
<
|
| 430 |
-
<
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
>
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
duration: 2,
|
| 478 |
-
ease: 'easeInOut',
|
| 479 |
-
}}
|
| 480 |
-
/>
|
| 481 |
-
)}
|
| 482 |
-
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
|
| 483 |
-
{showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
|
| 484 |
-
</DialogTitle>
|
| 485 |
-
</div>
|
| 486 |
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
| 493 |
-
whileHover={{ scale: 1.05 }}
|
| 494 |
-
whileTap={{ scale: 0.95 }}
|
| 495 |
-
>
|
| 496 |
-
<div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 497 |
-
<span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
|
| 498 |
-
Manage Tabs
|
| 499 |
-
</span>
|
| 500 |
-
</motion.button>
|
| 501 |
-
)}
|
| 502 |
-
|
| 503 |
-
<div className="flex items-center gap-2">
|
| 504 |
<Switch
|
| 505 |
id="developer-mode"
|
| 506 |
checked={developerMode}
|
|
@@ -521,87 +362,98 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 521 |
)}
|
| 522 |
/>
|
| 523 |
</Switch>
|
| 524 |
-
<
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
|
|
|
|
|
|
| 530 |
</div>
|
|
|
|
| 531 |
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
>
|
| 536 |
-
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 537 |
-
</button>
|
| 538 |
</div>
|
| 539 |
-
</div>
|
| 540 |
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
'overflow-y-auto',
|
| 546 |
-
'hover:overflow-y-auto',
|
| 547 |
-
'scrollbar scrollbar-w-2',
|
| 548 |
-
'scrollbar-track-transparent',
|
| 549 |
-
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
|
| 550 |
-
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
|
| 551 |
-
'will-change-scroll',
|
| 552 |
-
'touch-auto',
|
| 553 |
-
)}
|
| 554 |
-
>
|
| 555 |
-
<motion.div
|
| 556 |
-
key={activeTab || 'home'}
|
| 557 |
-
initial={{ opacity: 0 }}
|
| 558 |
-
animate={{ opacity: 1 }}
|
| 559 |
-
exit={{ opacity: 0 }}
|
| 560 |
-
transition={{ duration: 0.2 }}
|
| 561 |
-
className="p-6"
|
| 562 |
>
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
) : activeTab ? (
|
| 566 |
-
getTabComponent()
|
| 567 |
-
) : (
|
| 568 |
-
<motion.div className="grid grid-cols-4 gap-4">
|
| 569 |
-
<AnimatePresence mode="popLayout">
|
| 570 |
-
{visibleTabs.map((tab: TabWithDevType, index: number) => (
|
| 571 |
-
<motion.div
|
| 572 |
-
key={tab.id}
|
| 573 |
-
layout
|
| 574 |
-
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
| 575 |
-
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 576 |
-
exit={{ opacity: 0, scale: 0.8, y: -20 }}
|
| 577 |
-
transition={{
|
| 578 |
-
duration: 0.2,
|
| 579 |
-
delay: index * 0.05,
|
| 580 |
-
}}
|
| 581 |
-
>
|
| 582 |
-
<DraggableTabTile
|
| 583 |
-
tab={tab}
|
| 584 |
-
index={index}
|
| 585 |
-
moveTab={moveTab}
|
| 586 |
-
onClick={() => handleTabClick(tab.id)}
|
| 587 |
-
isActive={activeTab === tab.id}
|
| 588 |
-
hasUpdate={getTabUpdateStatus(tab.id)}
|
| 589 |
-
statusMessage={getStatusMessage(tab.id)}
|
| 590 |
-
description={TAB_DESCRIPTIONS[tab.id]}
|
| 591 |
-
isLoading={loadingTab === tab.id}
|
| 592 |
-
/>
|
| 593 |
-
</motion.div>
|
| 594 |
-
))}
|
| 595 |
-
</AnimatePresence>
|
| 596 |
-
</motion.div>
|
| 597 |
-
)}
|
| 598 |
-
</motion.div>
|
| 599 |
</div>
|
| 600 |
-
</
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
);
|
| 607 |
};
|
|
|
|
| 3 |
import { useStore } from '@nanostores/react';
|
| 4 |
import { Switch } from '@radix-ui/react-switch';
|
| 5 |
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
|
|
|
|
|
| 6 |
import { classNames } from '~/utils/classNames';
|
| 7 |
+
import { TabManagement } from '~/components/@settings/shared/components/TabManagement';
|
| 8 |
+
import { TabTile } from '~/components/@settings/shared/components/TabTile';
|
| 9 |
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
|
| 10 |
import { useFeatures } from '~/lib/hooks/useFeatures';
|
| 11 |
import { useNotifications } from '~/lib/hooks/useNotifications';
|
| 12 |
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
| 13 |
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
| 14 |
import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings';
|
| 15 |
+
import { profileStore } from '~/lib/stores/profile';
|
| 16 |
+
import type { TabType, TabVisibilityConfig, DevTabConfig, Profile } from './types';
|
| 17 |
+
import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
|
| 18 |
import { resetTabConfiguration } from '~/lib/stores/settings';
|
| 19 |
import { DialogTitle } from '~/components/ui/Dialog';
|
| 20 |
+
import { AvatarDropdown } from './AvatarDropdown';
|
| 21 |
|
| 22 |
// Import all tab components
|
| 23 |
+
import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab';
|
| 24 |
+
import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab';
|
| 25 |
+
import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab';
|
| 26 |
+
import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
|
| 27 |
+
import DataTab from '~/components/@settings/tabs/data/DataTab';
|
| 28 |
+
import DebugTab from '~/components/@settings/tabs/debug/DebugTab';
|
| 29 |
+
import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
|
| 30 |
+
import UpdateTab from '~/components/@settings/tabs/update/UpdateTab';
|
| 31 |
+
import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
|
| 32 |
+
import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
|
| 33 |
+
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
|
| 34 |
+
import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
|
| 35 |
+
import TaskManagerTab from '~/components/@settings/tabs/task-manager/TaskManagerTab';
|
| 36 |
|
| 37 |
interface ControlPanelProps {
|
| 38 |
open: boolean;
|
|
|
|
| 57 |
'event-logs': 'View system events and logs',
|
| 58 |
update: 'Check for updates and release notes',
|
| 59 |
'task-manager': 'Monitor system resources and processes',
|
| 60 |
+
'tab-management': 'Configure visible tabs and their order',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
};
|
| 62 |
|
| 63 |
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
|
|
| 65 |
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
| 66 |
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
| 67 |
const [showTabManagement, setShowTabManagement] = useState(false);
|
|
|
|
| 68 |
|
| 69 |
// Store values
|
| 70 |
const tabConfiguration = useStore(tabConfigurationStore);
|
| 71 |
const developerMode = useStore(developerModeStore);
|
| 72 |
+
const profile = useStore(profileStore) as Profile;
|
| 73 |
|
| 74 |
// Status hooks
|
| 75 |
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
|
|
|
| 78 |
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
| 79 |
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
// Add visibleTabs logic using useMemo
|
| 82 |
const visibleTabs = useMemo(() => {
|
| 83 |
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
|
|
|
| 112 |
};
|
| 113 |
});
|
| 114 |
|
| 115 |
+
// Add Tab Management tile for developer mode
|
| 116 |
+
const tabManagementConfig: DevTabConfig = {
|
| 117 |
+
id: 'tab-management',
|
| 118 |
+
visible: true,
|
| 119 |
+
window: 'developer',
|
| 120 |
+
order: devTabs.length,
|
| 121 |
+
isExtraDevTab: true,
|
| 122 |
+
};
|
| 123 |
+
devTabs.push(tabManagementConfig);
|
| 124 |
+
|
| 125 |
return devTabs.sort((a, b) => a.order - b.order);
|
| 126 |
}
|
| 127 |
|
| 128 |
// In user mode, only show visible user tabs
|
| 129 |
+
const notificationsDisabled = profile?.preferences?.notifications === false;
|
| 130 |
+
|
| 131 |
return tabConfiguration.userTabs
|
| 132 |
.filter((tab) => {
|
| 133 |
if (!tab || typeof tab.id !== 'string') {
|
|
|
|
| 135 |
return false;
|
| 136 |
}
|
| 137 |
|
| 138 |
+
// Hide notifications tab if notifications are disabled in user preferences
|
| 139 |
+
if (tab.id === 'notifications' && notificationsDisabled) {
|
| 140 |
return false;
|
| 141 |
}
|
| 142 |
|
|
|
|
| 144 |
return tab.visible && tab.window === 'user';
|
| 145 |
})
|
| 146 |
.sort((a, b) => a.order - b.order);
|
| 147 |
+
}, [tabConfiguration, developerMode, profile?.preferences?.notifications]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
// Handlers
|
| 150 |
const handleBack = () => {
|
|
|
|
| 165 |
console.log('Current developer mode:', developerMode);
|
| 166 |
}, [developerMode]);
|
| 167 |
|
| 168 |
+
const getTabComponent = (tabId: TabType | 'tab-management') => {
|
| 169 |
+
if (tabId === 'tab-management') {
|
| 170 |
+
return <TabManagement />;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
switch (tabId) {
|
| 174 |
case 'profile':
|
| 175 |
return <ProfileTab />;
|
| 176 |
case 'settings':
|
|
|
|
| 247 |
const handleTabClick = (tabId: TabType) => {
|
| 248 |
setLoadingTab(tabId);
|
| 249 |
setActiveTab(tabId);
|
| 250 |
+
setShowTabManagement(false);
|
| 251 |
|
| 252 |
// Acknowledge notifications based on tab
|
| 253 |
switch (tabId) {
|
|
|
|
| 273 |
};
|
| 274 |
|
| 275 |
return (
|
| 276 |
+
<RadixDialog.Root open={open}>
|
| 277 |
+
<RadixDialog.Portal>
|
| 278 |
+
<div className="fixed inset-0 flex items-center justify-center z-[100]">
|
| 279 |
+
<RadixDialog.Overlay asChild>
|
| 280 |
+
<motion.div
|
| 281 |
+
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
| 282 |
+
initial={{ opacity: 0 }}
|
| 283 |
+
animate={{ opacity: 1 }}
|
| 284 |
+
exit={{ opacity: 0 }}
|
| 285 |
+
transition={{ duration: 0.2 }}
|
| 286 |
+
/>
|
| 287 |
+
</RadixDialog.Overlay>
|
| 288 |
+
|
| 289 |
+
<RadixDialog.Content
|
| 290 |
+
aria-describedby={undefined}
|
| 291 |
+
onEscapeKeyDown={onClose}
|
| 292 |
+
onPointerDownOutside={onClose}
|
| 293 |
+
className="relative z-[101]"
|
| 294 |
+
>
|
| 295 |
+
<motion.div
|
| 296 |
+
className={classNames(
|
| 297 |
+
'w-[1200px] h-[90vh]',
|
| 298 |
+
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
| 299 |
+
'rounded-2xl shadow-2xl',
|
| 300 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 301 |
+
'flex flex-col overflow-hidden',
|
| 302 |
+
)}
|
| 303 |
+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 304 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 305 |
+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 306 |
+
transition={{ duration: 0.2 }}
|
| 307 |
>
|
| 308 |
+
{/* Header */}
|
| 309 |
+
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
| 310 |
+
<div className="flex items-center space-x-4">
|
| 311 |
+
{activeTab || showTabManagement ? (
|
| 312 |
+
<button
|
| 313 |
+
onClick={handleBack}
|
| 314 |
+
className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
| 315 |
+
>
|
| 316 |
+
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 317 |
+
</button>
|
| 318 |
+
) : (
|
| 319 |
+
<motion.div
|
| 320 |
+
className="w-7 h-7"
|
| 321 |
+
initial={{ rotate: -5 }}
|
| 322 |
+
animate={{ rotate: 5 }}
|
| 323 |
+
transition={{
|
| 324 |
+
repeat: Infinity,
|
| 325 |
+
repeatType: 'reverse',
|
| 326 |
+
duration: 2,
|
| 327 |
+
ease: 'easeInOut',
|
| 328 |
+
}}
|
| 329 |
+
>
|
| 330 |
+
<div className="w-full h-full flex items-center justify-center bg-gray-100/50 dark:bg-gray-800/50 rounded-full">
|
| 331 |
+
<div className="i-ph:robot-fill w-5 h-5 text-gray-400 dark:text-gray-400 transition-colors" />
|
| 332 |
+
</div>
|
| 333 |
+
</motion.div>
|
| 334 |
+
)}
|
| 335 |
+
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
|
| 336 |
+
{showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
|
| 337 |
+
</DialogTitle>
|
| 338 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
| 340 |
+
<div className="flex items-center gap-6">
|
| 341 |
+
{/* Developer Mode Controls */}
|
| 342 |
+
<div className="flex items-center gap-6">
|
| 343 |
+
{/* Mode Toggle */}
|
| 344 |
+
<div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
<Switch
|
| 346 |
id="developer-mode"
|
| 347 |
checked={developerMode}
|
|
|
|
| 362 |
)}
|
| 363 |
/>
|
| 364 |
</Switch>
|
| 365 |
+
<div className="flex items-center gap-2">
|
| 366 |
+
<label
|
| 367 |
+
htmlFor="developer-mode"
|
| 368 |
+
className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
|
| 369 |
+
>
|
| 370 |
+
{developerMode ? 'Developer Mode' : 'User Mode'}
|
| 371 |
+
</label>
|
| 372 |
+
</div>
|
| 373 |
</div>
|
| 374 |
+
</div>
|
| 375 |
|
| 376 |
+
{/* Avatar and Dropdown */}
|
| 377 |
+
<div className="border-l border-gray-200 dark:border-gray-800 pl-6">
|
| 378 |
+
<AvatarDropdown onSelectTab={handleTabClick} />
|
|
|
|
|
|
|
|
|
|
| 379 |
</div>
|
|
|
|
| 380 |
|
| 381 |
+
{/* Close Button */}
|
| 382 |
+
<button
|
| 383 |
+
onClick={onClose}
|
| 384 |
+
className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
>
|
| 386 |
+
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 387 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
</div>
|
| 389 |
+
</div>
|
| 390 |
+
|
| 391 |
+
{/* Content */}
|
| 392 |
+
<div
|
| 393 |
+
className={classNames(
|
| 394 |
+
'flex-1',
|
| 395 |
+
'overflow-y-auto',
|
| 396 |
+
'hover:overflow-y-auto',
|
| 397 |
+
'scrollbar scrollbar-w-2',
|
| 398 |
+
'scrollbar-track-transparent',
|
| 399 |
+
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
|
| 400 |
+
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
|
| 401 |
+
'will-change-scroll',
|
| 402 |
+
'touch-auto',
|
| 403 |
+
)}
|
| 404 |
+
>
|
| 405 |
+
<motion.div
|
| 406 |
+
key={activeTab || 'home'}
|
| 407 |
+
initial={{ opacity: 0 }}
|
| 408 |
+
animate={{ opacity: 1 }}
|
| 409 |
+
exit={{ opacity: 0 }}
|
| 410 |
+
transition={{ duration: 0.2 }}
|
| 411 |
+
className="p-6"
|
| 412 |
+
>
|
| 413 |
+
{showTabManagement ? (
|
| 414 |
+
<TabManagement />
|
| 415 |
+
) : activeTab ? (
|
| 416 |
+
getTabComponent(activeTab)
|
| 417 |
+
) : (
|
| 418 |
+
<motion.div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
|
| 419 |
+
<AnimatePresence mode="popLayout">
|
| 420 |
+
{(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
|
| 421 |
+
<motion.div
|
| 422 |
+
key={tab.id}
|
| 423 |
+
layout
|
| 424 |
+
initial={{ opacity: 0, scale: 0.8 }}
|
| 425 |
+
animate={{ opacity: 1, scale: 1 }}
|
| 426 |
+
exit={{ opacity: 0, scale: 0.8 }}
|
| 427 |
+
transition={{
|
| 428 |
+
type: 'spring',
|
| 429 |
+
stiffness: 400,
|
| 430 |
+
damping: 30,
|
| 431 |
+
mass: 0.8,
|
| 432 |
+
duration: 0.3,
|
| 433 |
+
}}
|
| 434 |
+
className="aspect-[1.5/1]"
|
| 435 |
+
>
|
| 436 |
+
<TabTile
|
| 437 |
+
tab={tab}
|
| 438 |
+
onClick={() => handleTabClick(tab.id as TabType)}
|
| 439 |
+
isActive={activeTab === tab.id}
|
| 440 |
+
hasUpdate={getTabUpdateStatus(tab.id)}
|
| 441 |
+
statusMessage={getStatusMessage(tab.id)}
|
| 442 |
+
description={TAB_DESCRIPTIONS[tab.id]}
|
| 443 |
+
isLoading={loadingTab === tab.id}
|
| 444 |
+
className="h-full"
|
| 445 |
+
/>
|
| 446 |
+
</motion.div>
|
| 447 |
+
))}
|
| 448 |
+
</AnimatePresence>
|
| 449 |
+
</motion.div>
|
| 450 |
+
)}
|
| 451 |
+
</motion.div>
|
| 452 |
+
</div>
|
| 453 |
+
</motion.div>
|
| 454 |
+
</RadixDialog.Content>
|
| 455 |
+
</div>
|
| 456 |
+
</RadixDialog.Portal>
|
| 457 |
+
</RadixDialog.Root>
|
| 458 |
);
|
| 459 |
};
|
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { TabType } from './types';
|
| 2 |
+
|
| 3 |
+
export const TAB_ICONS: Record<TabType, string> = {
|
| 4 |
+
profile: 'i-ph:user-circle-fill',
|
| 5 |
+
settings: 'i-ph:gear-six-fill',
|
| 6 |
+
notifications: 'i-ph:bell-fill',
|
| 7 |
+
features: 'i-ph:star-fill',
|
| 8 |
+
data: 'i-ph:database-fill',
|
| 9 |
+
'cloud-providers': 'i-ph:cloud-fill',
|
| 10 |
+
'local-providers': 'i-ph:desktop-fill',
|
| 11 |
+
'service-status': 'i-ph:activity-bold',
|
| 12 |
+
connection: 'i-ph:wifi-high-fill',
|
| 13 |
+
debug: 'i-ph:bug-fill',
|
| 14 |
+
'event-logs': 'i-ph:list-bullets-fill',
|
| 15 |
+
update: 'i-ph:arrow-clockwise-fill',
|
| 16 |
+
'task-manager': 'i-ph:chart-line-fill',
|
| 17 |
+
'tab-management': 'i-ph:squares-four-fill',
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
export const TAB_LABELS: Record<TabType, string> = {
|
| 21 |
+
profile: 'Profile',
|
| 22 |
+
settings: 'Settings',
|
| 23 |
+
notifications: 'Notifications',
|
| 24 |
+
features: 'Features',
|
| 25 |
+
data: 'Data Management',
|
| 26 |
+
'cloud-providers': 'Cloud Providers',
|
| 27 |
+
'local-providers': 'Local Providers',
|
| 28 |
+
'service-status': 'Service Status',
|
| 29 |
+
connection: 'Connection',
|
| 30 |
+
debug: 'Debug',
|
| 31 |
+
'event-logs': 'Event Logs',
|
| 32 |
+
update: 'Updates',
|
| 33 |
+
'task-manager': 'Task Manager',
|
| 34 |
+
'tab-management': 'Tab Management',
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
export const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
| 38 |
+
profile: 'Manage your profile and account settings',
|
| 39 |
+
settings: 'Configure application preferences',
|
| 40 |
+
notifications: 'View and manage your notifications',
|
| 41 |
+
features: 'Explore new and upcoming features',
|
| 42 |
+
data: 'Manage your data and storage',
|
| 43 |
+
'cloud-providers': 'Configure cloud AI providers and models',
|
| 44 |
+
'local-providers': 'Configure local AI providers and models',
|
| 45 |
+
'service-status': 'Monitor cloud LLM service status',
|
| 46 |
+
connection: 'Check connection status and settings',
|
| 47 |
+
debug: 'Debug tools and system information',
|
| 48 |
+
'event-logs': 'View system events and logs',
|
| 49 |
+
update: 'Check for updates and release notes',
|
| 50 |
+
'task-manager': 'Monitor system resources and processes',
|
| 51 |
+
'tab-management': 'Configure visible tabs and their order',
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
export const DEFAULT_TAB_CONFIG = [
|
| 55 |
+
// User Window Tabs (Always visible by default)
|
| 56 |
+
{ id: 'features', visible: true, window: 'user' as const, order: 0 },
|
| 57 |
+
{ id: 'data', visible: true, window: 'user' as const, order: 1 },
|
| 58 |
+
{ id: 'cloud-providers', visible: true, window: 'user' as const, order: 2 },
|
| 59 |
+
{ id: 'local-providers', visible: true, window: 'user' as const, order: 3 },
|
| 60 |
+
{ id: 'connection', visible: true, window: 'user' as const, order: 4 },
|
| 61 |
+
{ id: 'notifications', visible: true, window: 'user' as const, order: 5 },
|
| 62 |
+
{ id: 'event-logs', visible: true, window: 'user' as const, order: 6 },
|
| 63 |
+
|
| 64 |
+
// User Window Tabs (In dropdown, initially hidden)
|
| 65 |
+
{ id: 'profile', visible: false, window: 'user' as const, order: 7 },
|
| 66 |
+
{ id: 'settings', visible: false, window: 'user' as const, order: 8 },
|
| 67 |
+
{ id: 'task-manager', visible: false, window: 'user' as const, order: 9 },
|
| 68 |
+
{ id: 'service-status', visible: false, window: 'user' as const, order: 10 },
|
| 69 |
+
|
| 70 |
+
// User Window Tabs (Hidden, controlled by TaskManagerTab)
|
| 71 |
+
{ id: 'debug', visible: false, window: 'user' as const, order: 11 },
|
| 72 |
+
{ id: 'update', visible: false, window: 'user' as const, order: 12 },
|
| 73 |
+
|
| 74 |
+
// Developer Window Tabs (All visible by default)
|
| 75 |
+
{ id: 'features', visible: true, window: 'developer' as const, order: 0 },
|
| 76 |
+
{ id: 'data', visible: true, window: 'developer' as const, order: 1 },
|
| 77 |
+
{ id: 'cloud-providers', visible: true, window: 'developer' as const, order: 2 },
|
| 78 |
+
{ id: 'local-providers', visible: true, window: 'developer' as const, order: 3 },
|
| 79 |
+
{ id: 'connection', visible: true, window: 'developer' as const, order: 4 },
|
| 80 |
+
{ id: 'notifications', visible: true, window: 'developer' as const, order: 5 },
|
| 81 |
+
{ id: 'event-logs', visible: true, window: 'developer' as const, order: 6 },
|
| 82 |
+
{ id: 'profile', visible: true, window: 'developer' as const, order: 7 },
|
| 83 |
+
{ id: 'settings', visible: true, window: 'developer' as const, order: 8 },
|
| 84 |
+
{ id: 'task-manager', visible: true, window: 'developer' as const, order: 9 },
|
| 85 |
+
{ id: 'service-status', visible: true, window: 'developer' as const, order: 10 },
|
| 86 |
+
{ id: 'debug', visible: true, window: 'developer' as const, order: 11 },
|
| 87 |
+
{ id: 'update', visible: true, window: 'developer' as const, order: 12 },
|
| 88 |
+
];
|
|
@@ -10,12 +10,13 @@ export type TabType =
|
|
| 10 |
| 'data'
|
| 11 |
| 'cloud-providers'
|
| 12 |
| 'local-providers'
|
|
|
|
| 13 |
| 'connection'
|
| 14 |
| 'debug'
|
| 15 |
| 'event-logs'
|
| 16 |
| 'update'
|
| 17 |
| 'task-manager'
|
| 18 |
-
| '
|
| 19 |
|
| 20 |
export type WindowType = 'user' | 'developer';
|
| 21 |
|
|
@@ -46,14 +47,23 @@ export interface SettingItem {
|
|
| 46 |
export interface TabVisibilityConfig {
|
| 47 |
id: TabType;
|
| 48 |
visible: boolean;
|
| 49 |
-
window:
|
| 50 |
order: number;
|
|
|
|
| 51 |
locked?: boolean;
|
| 52 |
}
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
export interface TabWindowConfig {
|
| 55 |
-
userTabs:
|
| 56 |
-
developerTabs:
|
| 57 |
}
|
| 58 |
|
| 59 |
export const TAB_LABELS: Record<TabType, string> = {
|
|
@@ -61,47 +71,18 @@ export const TAB_LABELS: Record<TabType, string> = {
|
|
| 61 |
settings: 'Settings',
|
| 62 |
notifications: 'Notifications',
|
| 63 |
features: 'Features',
|
| 64 |
-
data: 'Data',
|
| 65 |
'cloud-providers': 'Cloud Providers',
|
| 66 |
'local-providers': 'Local Providers',
|
| 67 |
-
|
|
|
|
| 68 |
debug: 'Debug',
|
| 69 |
'event-logs': 'Event Logs',
|
| 70 |
-
update: '
|
| 71 |
'task-manager': 'Task Manager',
|
| 72 |
-
'
|
| 73 |
};
|
| 74 |
|
| 75 |
-
export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
|
| 76 |
-
// User Window Tabs (Visible by default)
|
| 77 |
-
{ id: 'features', visible: true, window: 'user', order: 0 },
|
| 78 |
-
{ id: 'data', visible: true, window: 'user', order: 1 },
|
| 79 |
-
{ id: 'cloud-providers', visible: true, window: 'user', order: 2 },
|
| 80 |
-
{ id: 'local-providers', visible: true, window: 'user', order: 3 },
|
| 81 |
-
{ id: 'connection', visible: true, window: 'user', order: 4 },
|
| 82 |
-
{ id: 'debug', visible: true, window: 'user', order: 5 },
|
| 83 |
-
|
| 84 |
-
// User Window Tabs (Hidden by default)
|
| 85 |
-
{ id: 'profile', visible: false, window: 'user', order: 6 },
|
| 86 |
-
{ id: 'settings', visible: false, window: 'user', order: 7 },
|
| 87 |
-
{ id: 'notifications', visible: false, window: 'user', order: 8 },
|
| 88 |
-
{ id: 'event-logs', visible: false, window: 'user', order: 9 },
|
| 89 |
-
{ id: 'update', visible: false, window: 'user', order: 10 },
|
| 90 |
-
{ id: 'service-status', visible: false, window: 'user', order: 11 },
|
| 91 |
-
|
| 92 |
-
// Developer Window Tabs (All visible by default)
|
| 93 |
-
{ id: 'features', visible: true, window: 'developer', order: 0 },
|
| 94 |
-
{ id: 'data', visible: true, window: 'developer', order: 1 },
|
| 95 |
-
{ id: 'cloud-providers', visible: true, window: 'developer', order: 2 },
|
| 96 |
-
{ id: 'local-providers', visible: true, window: 'developer', order: 3 },
|
| 97 |
-
{ id: 'connection', visible: true, window: 'developer', order: 4 },
|
| 98 |
-
{ id: 'debug', visible: true, window: 'developer', order: 5 },
|
| 99 |
-
{ id: 'task-manager', visible: true, window: 'developer', order: 6 },
|
| 100 |
-
{ id: 'settings', visible: true, window: 'developer', order: 7 },
|
| 101 |
-
{ id: 'notifications', visible: true, window: 'developer', order: 8 },
|
| 102 |
-
{ id: 'service-status', visible: true, window: 'developer', order: 9 },
|
| 103 |
-
];
|
| 104 |
-
|
| 105 |
export const categoryLabels: Record<SettingCategory, string> = {
|
| 106 |
profile: 'Profile & Account',
|
| 107 |
file_sharing: 'File Sharing',
|
|
@@ -119,3 +100,15 @@ export const categoryIcons: Record<SettingCategory, string> = {
|
|
| 119 |
services: 'i-ph:cube',
|
| 120 |
preferences: 'i-ph:sliders',
|
| 121 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
| 'data'
|
| 11 |
| 'cloud-providers'
|
| 12 |
| 'local-providers'
|
| 13 |
+
| 'service-status'
|
| 14 |
| 'connection'
|
| 15 |
| 'debug'
|
| 16 |
| 'event-logs'
|
| 17 |
| 'update'
|
| 18 |
| 'task-manager'
|
| 19 |
+
| 'tab-management';
|
| 20 |
|
| 21 |
export type WindowType = 'user' | 'developer';
|
| 22 |
|
|
|
|
| 47 |
export interface TabVisibilityConfig {
|
| 48 |
id: TabType;
|
| 49 |
visible: boolean;
|
| 50 |
+
window: WindowType;
|
| 51 |
order: number;
|
| 52 |
+
isExtraDevTab?: boolean;
|
| 53 |
locked?: boolean;
|
| 54 |
}
|
| 55 |
|
| 56 |
+
export interface DevTabConfig extends TabVisibilityConfig {
|
| 57 |
+
window: 'developer';
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export interface UserTabConfig extends TabVisibilityConfig {
|
| 61 |
+
window: 'user';
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
export interface TabWindowConfig {
|
| 65 |
+
userTabs: UserTabConfig[];
|
| 66 |
+
developerTabs: DevTabConfig[];
|
| 67 |
}
|
| 68 |
|
| 69 |
export const TAB_LABELS: Record<TabType, string> = {
|
|
|
|
| 71 |
settings: 'Settings',
|
| 72 |
notifications: 'Notifications',
|
| 73 |
features: 'Features',
|
| 74 |
+
data: 'Data Management',
|
| 75 |
'cloud-providers': 'Cloud Providers',
|
| 76 |
'local-providers': 'Local Providers',
|
| 77 |
+
'service-status': 'Service Status',
|
| 78 |
+
connection: 'Connections',
|
| 79 |
debug: 'Debug',
|
| 80 |
'event-logs': 'Event Logs',
|
| 81 |
+
update: 'Updates',
|
| 82 |
'task-manager': 'Task Manager',
|
| 83 |
+
'tab-management': 'Tab Management',
|
| 84 |
};
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
export const categoryLabels: Record<SettingCategory, string> = {
|
| 87 |
profile: 'Profile & Account',
|
| 88 |
file_sharing: 'File Sharing',
|
|
|
|
| 100 |
services: 'i-ph:cube',
|
| 101 |
preferences: 'i-ph:sliders',
|
| 102 |
};
|
| 103 |
+
|
| 104 |
+
export interface Profile {
|
| 105 |
+
username?: string;
|
| 106 |
+
bio?: string;
|
| 107 |
+
avatar?: string;
|
| 108 |
+
preferences?: {
|
| 109 |
+
notifications?: boolean;
|
| 110 |
+
theme?: 'light' | 'dark' | 'system';
|
| 111 |
+
language?: string;
|
| 112 |
+
timezone?: string;
|
| 113 |
+
};
|
| 114 |
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Core exports
|
| 2 |
+
export { ControlPanel } from './core/ControlPanel';
|
| 3 |
+
export type { TabType, TabVisibilityConfig } from './core/types';
|
| 4 |
+
|
| 5 |
+
// Constants
|
| 6 |
+
export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constants';
|
| 7 |
+
|
| 8 |
+
// Shared components
|
| 9 |
+
export { TabTile } from './shared/components/TabTile';
|
| 10 |
+
export { TabManagement } from './shared/components/TabManagement';
|
| 11 |
+
|
| 12 |
+
// Utils
|
| 13 |
+
export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers';
|
| 14 |
+
export * from './utils/animations';
|
|
@@ -1,8 +1,8 @@
|
|
| 1 |
import { useDrag, useDrop } from 'react-dnd';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
-
import type { TabVisibilityConfig } from '~/components
|
| 5 |
-
import { TAB_LABELS } from '~/components
|
| 6 |
import { Switch } from '~/components/ui/Switch';
|
| 7 |
|
| 8 |
interface DraggableTabListProps {
|
|
|
|
| 1 |
import { useDrag, useDrop } from 'react-dnd';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
+
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
|
| 5 |
+
import { TAB_LABELS } from '~/components/@settings/core/types';
|
| 6 |
import { Switch } from '~/components/ui/Switch';
|
| 7 |
|
| 8 |
interface DraggableTabListProps {
|
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { motion } from 'framer-motion';
|
| 3 |
+
import { useStore } from '@nanostores/react';
|
| 4 |
+
import { Switch } from '@radix-ui/react-switch';
|
| 5 |
+
import { classNames } from '~/utils/classNames';
|
| 6 |
+
import { tabConfigurationStore } from '~/lib/stores/settings';
|
| 7 |
+
import { TAB_LABELS } from '~/components/@settings/core/constants';
|
| 8 |
+
import type { TabType } from '~/components/@settings/core/types';
|
| 9 |
+
import { toast } from 'react-toastify';
|
| 10 |
+
import { TbLayoutGrid } from 'react-icons/tb';
|
| 11 |
+
|
| 12 |
+
// Define tab icons mapping
|
| 13 |
+
const TAB_ICONS: Record<TabType, string> = {
|
| 14 |
+
profile: 'i-ph:user-circle-fill',
|
| 15 |
+
settings: 'i-ph:gear-six-fill',
|
| 16 |
+
notifications: 'i-ph:bell-fill',
|
| 17 |
+
features: 'i-ph:star-fill',
|
| 18 |
+
data: 'i-ph:database-fill',
|
| 19 |
+
'cloud-providers': 'i-ph:cloud-fill',
|
| 20 |
+
'local-providers': 'i-ph:desktop-fill',
|
| 21 |
+
'service-status': 'i-ph:activity-fill',
|
| 22 |
+
connection: 'i-ph:wifi-high-fill',
|
| 23 |
+
debug: 'i-ph:bug-fill',
|
| 24 |
+
'event-logs': 'i-ph:list-bullets-fill',
|
| 25 |
+
update: 'i-ph:arrow-clockwise-fill',
|
| 26 |
+
'task-manager': 'i-ph:chart-line-fill',
|
| 27 |
+
'tab-management': 'i-ph:squares-four-fill',
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
// Define which tabs are default in user mode
|
| 31 |
+
const DEFAULT_USER_TABS: TabType[] = [
|
| 32 |
+
'features',
|
| 33 |
+
'data',
|
| 34 |
+
'cloud-providers',
|
| 35 |
+
'local-providers',
|
| 36 |
+
'connection',
|
| 37 |
+
'notifications',
|
| 38 |
+
'event-logs',
|
| 39 |
+
];
|
| 40 |
+
|
| 41 |
+
// Define which tabs can be added to user mode
|
| 42 |
+
const OPTIONAL_USER_TABS: TabType[] = ['profile', 'settings', 'task-manager', 'service-status', 'debug', 'update'];
|
| 43 |
+
|
| 44 |
+
// All available tabs for user mode
|
| 45 |
+
const ALL_USER_TABS = [...DEFAULT_USER_TABS, ...OPTIONAL_USER_TABS];
|
| 46 |
+
|
| 47 |
+
export const TabManagement = () => {
|
| 48 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 49 |
+
const tabConfiguration = useStore(tabConfigurationStore);
|
| 50 |
+
|
| 51 |
+
const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => {
|
| 52 |
+
// Get current tab configuration
|
| 53 |
+
const currentTab = tabConfiguration.userTabs.find((tab) => tab.id === tabId);
|
| 54 |
+
|
| 55 |
+
// If tab doesn't exist in configuration, create it
|
| 56 |
+
if (!currentTab) {
|
| 57 |
+
const newTab = {
|
| 58 |
+
id: tabId,
|
| 59 |
+
visible: checked,
|
| 60 |
+
window: 'user' as const,
|
| 61 |
+
order: tabConfiguration.userTabs.length,
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
const updatedTabs = [...tabConfiguration.userTabs, newTab];
|
| 65 |
+
|
| 66 |
+
tabConfigurationStore.set({
|
| 67 |
+
...tabConfiguration,
|
| 68 |
+
userTabs: updatedTabs,
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`);
|
| 72 |
+
|
| 73 |
+
return;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Check if tab can be enabled in user mode
|
| 77 |
+
const canBeEnabled = DEFAULT_USER_TABS.includes(tabId) || OPTIONAL_USER_TABS.includes(tabId);
|
| 78 |
+
|
| 79 |
+
if (!canBeEnabled && checked) {
|
| 80 |
+
toast.error('This tab cannot be enabled in user mode');
|
| 81 |
+
return;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Update tab visibility
|
| 85 |
+
const updatedTabs = tabConfiguration.userTabs.map((tab) => {
|
| 86 |
+
if (tab.id === tabId) {
|
| 87 |
+
return { ...tab, visible: checked };
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
return tab;
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
// Update store
|
| 94 |
+
tabConfigurationStore.set({
|
| 95 |
+
...tabConfiguration,
|
| 96 |
+
userTabs: updatedTabs,
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
// Show success message
|
| 100 |
+
toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`);
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
// Create a map of existing tab configurations
|
| 104 |
+
const tabConfigMap = new Map(tabConfiguration.userTabs.map((tab) => [tab.id, tab]));
|
| 105 |
+
|
| 106 |
+
// Generate the complete list of tabs, including those not in the configuration
|
| 107 |
+
const allTabs = ALL_USER_TABS.map((tabId) => {
|
| 108 |
+
return (
|
| 109 |
+
tabConfigMap.get(tabId) || {
|
| 110 |
+
id: tabId,
|
| 111 |
+
visible: false,
|
| 112 |
+
window: 'user' as const,
|
| 113 |
+
order: -1,
|
| 114 |
+
}
|
| 115 |
+
);
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
// Filter tabs based on search query
|
| 119 |
+
const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()));
|
| 120 |
+
|
| 121 |
+
return (
|
| 122 |
+
<div className="space-y-6">
|
| 123 |
+
<motion.div
|
| 124 |
+
className="space-y-4"
|
| 125 |
+
initial={{ opacity: 0, y: 20 }}
|
| 126 |
+
animate={{ opacity: 1, y: 0 }}
|
| 127 |
+
transition={{ duration: 0.3 }}
|
| 128 |
+
>
|
| 129 |
+
{/* Header */}
|
| 130 |
+
<div className="flex items-center justify-between gap-4 mt-8 mb-4">
|
| 131 |
+
<div className="flex items-center gap-2">
|
| 132 |
+
<div
|
| 133 |
+
className={classNames(
|
| 134 |
+
'w-8 h-8 flex items-center justify-center rounded-lg',
|
| 135 |
+
'bg-bolt-elements-background-depth-3',
|
| 136 |
+
'text-purple-500',
|
| 137 |
+
)}
|
| 138 |
+
>
|
| 139 |
+
<TbLayoutGrid className="w-5 h-5" />
|
| 140 |
+
</div>
|
| 141 |
+
<div>
|
| 142 |
+
<h4 className="text-md font-medium text-bolt-elements-textPrimary">Tab Management</h4>
|
| 143 |
+
<p className="text-sm text-bolt-elements-textSecondary">Configure visible tabs and their order</p>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
{/* Search */}
|
| 148 |
+
<div className="relative w-64">
|
| 149 |
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 150 |
+
<div className="i-ph:magnifying-glass w-4 h-4 text-gray-400" />
|
| 151 |
+
</div>
|
| 152 |
+
<input
|
| 153 |
+
type="text"
|
| 154 |
+
value={searchQuery}
|
| 155 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 156 |
+
placeholder="Search tabs..."
|
| 157 |
+
className={classNames(
|
| 158 |
+
'w-full pl-10 pr-4 py-2 rounded-lg',
|
| 159 |
+
'bg-bolt-elements-background-depth-2',
|
| 160 |
+
'border border-bolt-elements-borderColor',
|
| 161 |
+
'text-bolt-elements-textPrimary',
|
| 162 |
+
'placeholder-bolt-elements-textTertiary',
|
| 163 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
| 164 |
+
'transition-all duration-200',
|
| 165 |
+
)}
|
| 166 |
+
/>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
{/* Tab Grid */}
|
| 171 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 172 |
+
{filteredTabs.map((tab, index) => (
|
| 173 |
+
<motion.div
|
| 174 |
+
key={tab.id}
|
| 175 |
+
className={classNames(
|
| 176 |
+
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary',
|
| 177 |
+
'bg-bolt-elements-background-depth-2',
|
| 178 |
+
'hover:bg-bolt-elements-background-depth-3',
|
| 179 |
+
'transition-all duration-200',
|
| 180 |
+
'relative overflow-hidden group',
|
| 181 |
+
)}
|
| 182 |
+
initial={{ opacity: 0, y: 20 }}
|
| 183 |
+
animate={{ opacity: 1, y: 0 }}
|
| 184 |
+
transition={{ delay: index * 0.1 }}
|
| 185 |
+
whileHover={{ scale: 1.02 }}
|
| 186 |
+
>
|
| 187 |
+
{/* Status Badges */}
|
| 188 |
+
<div className="absolute top-2 right-2 flex gap-1">
|
| 189 |
+
{DEFAULT_USER_TABS.includes(tab.id) && (
|
| 190 |
+
<span className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium">
|
| 191 |
+
Default
|
| 192 |
+
</span>
|
| 193 |
+
)}
|
| 194 |
+
{OPTIONAL_USER_TABS.includes(tab.id) && (
|
| 195 |
+
<span className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium">
|
| 196 |
+
Optional
|
| 197 |
+
</span>
|
| 198 |
+
)}
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<div className="flex items-start gap-4 p-4">
|
| 202 |
+
<motion.div
|
| 203 |
+
className={classNames(
|
| 204 |
+
'w-10 h-10 flex items-center justify-center rounded-xl',
|
| 205 |
+
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
|
| 206 |
+
'transition-all duration-200',
|
| 207 |
+
tab.visible ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
|
| 208 |
+
)}
|
| 209 |
+
whileHover={{ scale: 1.1 }}
|
| 210 |
+
whileTap={{ scale: 0.9 }}
|
| 211 |
+
>
|
| 212 |
+
<div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
|
| 213 |
+
<div className={classNames(TAB_ICONS[tab.id], 'w-full h-full')} />
|
| 214 |
+
</div>
|
| 215 |
+
</motion.div>
|
| 216 |
+
|
| 217 |
+
<div className="flex-1 min-w-0">
|
| 218 |
+
<div className="flex items-center justify-between gap-4">
|
| 219 |
+
<div>
|
| 220 |
+
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
|
| 221 |
+
{TAB_LABELS[tab.id]}
|
| 222 |
+
</h4>
|
| 223 |
+
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
|
| 224 |
+
{tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
|
| 225 |
+
</p>
|
| 226 |
+
</div>
|
| 227 |
+
<Switch
|
| 228 |
+
checked={tab.visible}
|
| 229 |
+
onCheckedChange={(checked) => handleTabVisibilityChange(tab.id, checked)}
|
| 230 |
+
disabled={!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id)}
|
| 231 |
+
className={classNames(
|
| 232 |
+
'relative inline-flex h-5 w-9 items-center rounded-full',
|
| 233 |
+
'transition-colors duration-200',
|
| 234 |
+
tab.visible ? 'bg-purple-500' : 'bg-bolt-elements-background-depth-4',
|
| 235 |
+
{
|
| 236 |
+
'opacity-50 cursor-not-allowed':
|
| 237 |
+
!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id),
|
| 238 |
+
},
|
| 239 |
+
)}
|
| 240 |
+
/>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<motion.div
|
| 246 |
+
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
|
| 247 |
+
animate={{
|
| 248 |
+
borderColor: tab.visible ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
|
| 249 |
+
scale: tab.visible ? 1 : 0.98,
|
| 250 |
+
}}
|
| 251 |
+
transition={{ duration: 0.2 }}
|
| 252 |
+
/>
|
| 253 |
+
</motion.div>
|
| 254 |
+
))}
|
| 255 |
+
</div>
|
| 256 |
+
</motion.div>
|
| 257 |
+
</div>
|
| 258 |
+
);
|
| 259 |
+
};
|
|
@@ -1,134 +1,104 @@
|
|
| 1 |
import { motion } from 'framer-motion';
|
| 2 |
import * as Tooltip from '@radix-ui/react-tooltip';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
-
import type { TabVisibilityConfig } from '~/components
|
| 5 |
-
import { TAB_LABELS } from '~/components
|
| 6 |
-
|
| 7 |
-
const TAB_ICONS = {
|
| 8 |
-
profile: 'i-ph:user',
|
| 9 |
-
settings: 'i-ph:gear',
|
| 10 |
-
notifications: 'i-ph:bell',
|
| 11 |
-
features: 'i-ph:star',
|
| 12 |
-
data: 'i-ph:database',
|
| 13 |
-
providers: 'i-ph:plug',
|
| 14 |
-
connection: 'i-ph:wifi-high',
|
| 15 |
-
debug: 'i-ph:bug',
|
| 16 |
-
'event-logs': 'i-ph:list-bullets',
|
| 17 |
-
update: 'i-ph:arrow-clockwise',
|
| 18 |
-
'task-manager': 'i-ph:activity',
|
| 19 |
-
'cloud-providers': 'i-ph:cloud',
|
| 20 |
-
'local-providers': 'i-ph:desktop',
|
| 21 |
-
'service-status': 'i-ph:activity-bold',
|
| 22 |
-
};
|
| 23 |
|
| 24 |
interface TabTileProps {
|
| 25 |
tab: TabVisibilityConfig;
|
| 26 |
-
onClick
|
| 27 |
isActive?: boolean;
|
| 28 |
hasUpdate?: boolean;
|
| 29 |
statusMessage?: string;
|
| 30 |
description?: string;
|
| 31 |
isLoading?: boolean;
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
export const TabTile = ({
|
| 35 |
tab,
|
| 36 |
onClick,
|
| 37 |
-
isActive
|
| 38 |
-
hasUpdate
|
| 39 |
statusMessage,
|
| 40 |
description,
|
| 41 |
-
isLoading
|
|
|
|
| 42 |
}: TabTileProps) => {
|
| 43 |
return (
|
| 44 |
<Tooltip.Provider delayDuration={200}>
|
| 45 |
<Tooltip.Root>
|
| 46 |
<Tooltip.Trigger asChild>
|
| 47 |
-
<motion.
|
| 48 |
onClick={onClick}
|
| 49 |
-
disabled={isLoading}
|
| 50 |
className={classNames(
|
| 51 |
-
'relative flex flex-col items-center
|
| 52 |
'w-full h-full min-h-[160px]',
|
| 53 |
-
|
| 54 |
-
// Background and border styles
|
| 55 |
'bg-white dark:bg-[#141414]',
|
| 56 |
-
'border border-[#E5E5E5]
|
| 57 |
-
|
| 58 |
-
// Shadow and glass effect
|
| 59 |
-
'shadow-sm',
|
| 60 |
-
'dark:shadow-[0_0_15px_rgba(0,0,0,0.1)]',
|
| 61 |
-
'dark:bg-opacity-50',
|
| 62 |
-
|
| 63 |
-
// Hover effects
|
| 64 |
-
'hover:border-purple-500/30 dark:hover:border-purple-500/30',
|
| 65 |
-
'hover:bg-gradient-to-br hover:from-purple-50/50 hover:to-white dark:hover:from-purple-500/5 dark:hover:to-[#141414]',
|
| 66 |
-
'hover:shadow-md hover:shadow-purple-500/5',
|
| 67 |
-
'dark:hover:shadow-purple-500/10',
|
| 68 |
-
|
| 69 |
-
// Focus states for keyboard navigation
|
| 70 |
-
'focus:outline-none',
|
| 71 |
-
'focus:ring-2 focus:ring-purple-500/50 focus:ring-offset-2',
|
| 72 |
-
'dark:focus:ring-offset-[#141414]',
|
| 73 |
-
'focus:border-purple-500/30',
|
| 74 |
-
|
| 75 |
-
// Active state
|
| 76 |
-
isActive
|
| 77 |
-
? [
|
| 78 |
-
'border-purple-500/50 dark:border-purple-500/50',
|
| 79 |
-
'bg-gradient-to-br from-purple-50 to-white dark:from-purple-500/10 dark:to-[#141414]',
|
| 80 |
-
'shadow-md shadow-purple-500/10',
|
| 81 |
-
]
|
| 82 |
-
: '',
|
| 83 |
-
|
| 84 |
-
// Loading state
|
| 85 |
-
isLoading ? 'cursor-wait opacity-70' : '',
|
| 86 |
-
|
| 87 |
-
// Transitions
|
| 88 |
-
'transition-all duration-300 ease-out',
|
| 89 |
'group',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
)}
|
| 91 |
-
whileHover={
|
| 92 |
-
!isLoading
|
| 93 |
-
? {
|
| 94 |
-
scale: 1.02,
|
| 95 |
-
transition: { duration: 0.2, ease: 'easeOut' },
|
| 96 |
-
}
|
| 97 |
-
: {}
|
| 98 |
-
}
|
| 99 |
-
whileTap={
|
| 100 |
-
!isLoading
|
| 101 |
-
? {
|
| 102 |
-
scale: 0.98,
|
| 103 |
-
transition: { duration: 0.1, ease: 'easeIn' },
|
| 104 |
-
}
|
| 105 |
-
: {}
|
| 106 |
-
}
|
| 107 |
>
|
| 108 |
-
{/*
|
| 109 |
-
|
|
|
|
| 110 |
<motion.div
|
| 111 |
className={classNames(
|
| 112 |
-
'
|
| 113 |
-
'
|
| 114 |
-
'backdrop-blur-sm',
|
| 115 |
'flex items-center justify-center',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
)}
|
| 117 |
-
initial={{ opacity: 0 }}
|
| 118 |
-
animate={{ opacity: 1 }}
|
| 119 |
-
transition={{ duration: 0.2 }}
|
| 120 |
>
|
| 121 |
<motion.div
|
| 122 |
-
className={classNames(
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
}
|
| 129 |
/>
|
| 130 |
</motion.div>
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
{/* Status Indicator */}
|
| 134 |
{hasUpdate && (
|
|
@@ -136,9 +106,8 @@ export const TabTile = ({
|
|
| 136 |
className={classNames(
|
| 137 |
'absolute top-3 right-3',
|
| 138 |
'w-2.5 h-2.5 rounded-full',
|
| 139 |
-
'bg-
|
| 140 |
-
'
|
| 141 |
-
'ring-4 ring-green-500/20',
|
| 142 |
)}
|
| 143 |
initial={{ scale: 0 }}
|
| 144 |
animate={{ scale: 1 }}
|
|
@@ -146,70 +115,30 @@ export const TabTile = ({
|
|
| 146 |
/>
|
| 147 |
)}
|
| 148 |
|
| 149 |
-
{/*
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
'absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100',
|
| 153 |
-
'bg-gradient-to-br from-purple-500/5 to-transparent dark:from-purple-500/10',
|
| 154 |
-
'transition-opacity duration-300',
|
| 155 |
-
isActive ? 'opacity-100' : '',
|
| 156 |
-
)}
|
| 157 |
-
/>
|
| 158 |
-
|
| 159 |
-
{/* Icon */}
|
| 160 |
-
<div
|
| 161 |
-
className={classNames(
|
| 162 |
-
TAB_ICONS[tab.id],
|
| 163 |
-
'w-12 h-12',
|
| 164 |
-
'relative',
|
| 165 |
-
'text-gray-600 dark:text-gray-300',
|
| 166 |
-
'group-hover:text-purple-500 dark:group-hover:text-purple-400',
|
| 167 |
-
'transition-all duration-300',
|
| 168 |
-
isActive ? 'text-purple-500 dark:text-purple-400 scale-110' : '',
|
| 169 |
-
)}
|
| 170 |
-
/>
|
| 171 |
-
|
| 172 |
-
{/* Label and Description */}
|
| 173 |
-
<div className="relative flex flex-col items-center text-center">
|
| 174 |
-
<div
|
| 175 |
className={classNames(
|
| 176 |
-
'
|
| 177 |
-
'
|
| 178 |
-
'
|
| 179 |
-
'transition-colors duration-300',
|
| 180 |
-
isActive ? 'text-purple-500 dark:text-purple-400' : '',
|
| 181 |
)}
|
|
|
|
|
|
|
|
|
|
| 182 |
>
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
>
|
| 196 |
-
{description}
|
| 197 |
-
</div>
|
| 198 |
-
)}
|
| 199 |
-
</div>
|
| 200 |
-
|
| 201 |
-
{/* Bottom indicator line */}
|
| 202 |
-
<div
|
| 203 |
-
className={classNames(
|
| 204 |
-
'absolute bottom-0 left-1/2 -translate-x-1/2',
|
| 205 |
-
'w-12 h-0.5 rounded-full',
|
| 206 |
-
'bg-purple-500/0 group-hover:bg-purple-500/50',
|
| 207 |
-
'transition-all duration-300 ease-out',
|
| 208 |
-
'transform scale-x-0 group-hover:scale-x-100',
|
| 209 |
-
isActive ? 'bg-purple-500 scale-x-100' : '',
|
| 210 |
-
)}
|
| 211 |
-
/>
|
| 212 |
-
</motion.button>
|
| 213 |
</Tooltip.Trigger>
|
| 214 |
<Tooltip.Portal>
|
| 215 |
<Tooltip.Content
|
|
@@ -217,7 +146,6 @@ export const TabTile = ({
|
|
| 217 |
'px-3 py-1.5 rounded-lg',
|
| 218 |
'bg-[#18181B] text-white',
|
| 219 |
'text-sm font-medium',
|
| 220 |
-
'shadow-xl',
|
| 221 |
'select-none',
|
| 222 |
'z-[100]',
|
| 223 |
)}
|
|
|
|
| 1 |
import { motion } from 'framer-motion';
|
| 2 |
import * as Tooltip from '@radix-ui/react-tooltip';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
+
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
|
| 5 |
+
import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
interface TabTileProps {
|
| 8 |
tab: TabVisibilityConfig;
|
| 9 |
+
onClick?: () => void;
|
| 10 |
isActive?: boolean;
|
| 11 |
hasUpdate?: boolean;
|
| 12 |
statusMessage?: string;
|
| 13 |
description?: string;
|
| 14 |
isLoading?: boolean;
|
| 15 |
+
className?: string;
|
| 16 |
}
|
| 17 |
|
| 18 |
export const TabTile = ({
|
| 19 |
tab,
|
| 20 |
onClick,
|
| 21 |
+
isActive,
|
| 22 |
+
hasUpdate,
|
| 23 |
statusMessage,
|
| 24 |
description,
|
| 25 |
+
isLoading,
|
| 26 |
+
className,
|
| 27 |
}: TabTileProps) => {
|
| 28 |
return (
|
| 29 |
<Tooltip.Provider delayDuration={200}>
|
| 30 |
<Tooltip.Root>
|
| 31 |
<Tooltip.Trigger asChild>
|
| 32 |
+
<motion.div
|
| 33 |
onClick={onClick}
|
|
|
|
| 34 |
className={classNames(
|
| 35 |
+
'relative flex flex-col items-center p-6 rounded-xl',
|
| 36 |
'w-full h-full min-h-[160px]',
|
|
|
|
|
|
|
| 37 |
'bg-white dark:bg-[#141414]',
|
| 38 |
+
'border border-[#E5E5E5] dark:border-[#333333]',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
'group',
|
| 40 |
+
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
| 41 |
+
'hover:border-purple-200 dark:hover:border-purple-900/30',
|
| 42 |
+
isActive ? 'border-purple-500 dark:border-purple-500/50 bg-purple-500/5 dark:bg-purple-500/10' : '',
|
| 43 |
+
isLoading ? 'cursor-wait opacity-70' : '',
|
| 44 |
+
className || '',
|
| 45 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
>
|
| 47 |
+
{/* Main Content */}
|
| 48 |
+
<div className="flex flex-col items-center justify-center flex-1 w-full">
|
| 49 |
+
{/* Icon */}
|
| 50 |
<motion.div
|
| 51 |
className={classNames(
|
| 52 |
+
'relative',
|
| 53 |
+
'w-14 h-14',
|
|
|
|
| 54 |
'flex items-center justify-center',
|
| 55 |
+
'rounded-xl',
|
| 56 |
+
'bg-gray-100 dark:bg-gray-800',
|
| 57 |
+
'ring-1 ring-gray-200 dark:ring-gray-700',
|
| 58 |
+
'group-hover:bg-purple-100 dark:group-hover:bg-gray-700/80',
|
| 59 |
+
'group-hover:ring-purple-200 dark:group-hover:ring-purple-800/30',
|
| 60 |
+
isActive ? 'bg-purple-500/10 dark:bg-purple-500/10 ring-purple-500/30 dark:ring-purple-500/20' : '',
|
| 61 |
)}
|
|
|
|
|
|
|
|
|
|
| 62 |
>
|
| 63 |
<motion.div
|
| 64 |
+
className={classNames(
|
| 65 |
+
TAB_ICONS[tab.id],
|
| 66 |
+
'w-8 h-8',
|
| 67 |
+
'text-gray-600 dark:text-gray-300',
|
| 68 |
+
'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
|
| 69 |
+
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
|
| 70 |
+
)}
|
| 71 |
/>
|
| 72 |
</motion.div>
|
| 73 |
+
|
| 74 |
+
{/* Label and Description */}
|
| 75 |
+
<div className="flex flex-col items-center mt-5 w-full">
|
| 76 |
+
<h3
|
| 77 |
+
className={classNames(
|
| 78 |
+
'text-[15px] font-medium leading-snug mb-2',
|
| 79 |
+
'text-gray-700 dark:text-gray-200',
|
| 80 |
+
'group-hover:text-purple-600 dark:group-hover:text-purple-300/90',
|
| 81 |
+
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
|
| 82 |
+
)}
|
| 83 |
+
>
|
| 84 |
+
{TAB_LABELS[tab.id]}
|
| 85 |
+
</h3>
|
| 86 |
+
{description && (
|
| 87 |
+
<p
|
| 88 |
+
className={classNames(
|
| 89 |
+
'text-[13px] leading-relaxed',
|
| 90 |
+
'text-gray-500 dark:text-gray-400',
|
| 91 |
+
'max-w-[85%]',
|
| 92 |
+
'text-center',
|
| 93 |
+
'group-hover:text-purple-500 dark:group-hover:text-purple-400/70',
|
| 94 |
+
isActive ? 'text-purple-400 dark:text-purple-400/80' : '',
|
| 95 |
+
)}
|
| 96 |
+
>
|
| 97 |
+
{description}
|
| 98 |
+
</p>
|
| 99 |
+
)}
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
|
| 103 |
{/* Status Indicator */}
|
| 104 |
{hasUpdate && (
|
|
|
|
| 106 |
className={classNames(
|
| 107 |
'absolute top-3 right-3',
|
| 108 |
'w-2.5 h-2.5 rounded-full',
|
| 109 |
+
'bg-purple-500',
|
| 110 |
+
'ring-4 ring-purple-500',
|
|
|
|
| 111 |
)}
|
| 112 |
initial={{ scale: 0 }}
|
| 113 |
animate={{ scale: 1 }}
|
|
|
|
| 115 |
/>
|
| 116 |
)}
|
| 117 |
|
| 118 |
+
{/* Loading Overlay */}
|
| 119 |
+
{isLoading && (
|
| 120 |
+
<motion.div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
className={classNames(
|
| 122 |
+
'absolute inset-0 rounded-xl z-10',
|
| 123 |
+
'bg-white dark:bg-black',
|
| 124 |
+
'flex items-center justify-center',
|
|
|
|
|
|
|
| 125 |
)}
|
| 126 |
+
initial={{ opacity: 0 }}
|
| 127 |
+
animate={{ opacity: 1 }}
|
| 128 |
+
transition={{ duration: 0.2 }}
|
| 129 |
>
|
| 130 |
+
<motion.div
|
| 131 |
+
className={classNames('w-8 h-8 rounded-full', 'border-2 border-purple-500', 'border-t-purple-500')}
|
| 132 |
+
animate={{ rotate: 360 }}
|
| 133 |
+
transition={{
|
| 134 |
+
duration: 1,
|
| 135 |
+
repeat: Infinity,
|
| 136 |
+
ease: 'linear',
|
| 137 |
+
}}
|
| 138 |
+
/>
|
| 139 |
+
</motion.div>
|
| 140 |
+
)}
|
| 141 |
+
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
</Tooltip.Trigger>
|
| 143 |
<Tooltip.Portal>
|
| 144 |
<Tooltip.Content
|
|
|
|
| 146 |
'px-3 py-1.5 rounded-lg',
|
| 147 |
'bg-[#18181B] text-white',
|
| 148 |
'text-sm font-medium',
|
|
|
|
| 149 |
'select-none',
|
| 150 |
'z-[100]',
|
| 151 |
)}
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import React, { useEffect } from 'react';
|
| 2 |
import { classNames } from '~/utils/classNames';
|
| 3 |
-
import type { GitHubAuthState } from '~/components
|
| 4 |
import Cookies from 'js-cookie';
|
| 5 |
import { getLocalStorage } from '~/lib/persistence';
|
| 6 |
|
|
|
|
| 1 |
import React, { useEffect } from 'react';
|
| 2 |
import { classNames } from '~/utils/classNames';
|
| 3 |
+
import type { GitHubAuthState } from '~/components/@settings/tabs/connections/types/GitHub';
|
| 4 |
import Cookies from 'js-cookie';
|
| 5 |
import { getLocalStorage } from '~/lib/persistence';
|
| 6 |
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { useState } from 'react';
|
| 2 |
import * as Dialog from '@radix-ui/react-dialog';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
-
import type { GitHubRepoInfo } from '~/components
|
| 5 |
import { GitBranch } from '@phosphor-icons/react';
|
| 6 |
|
| 7 |
interface GitHubBranch {
|
|
|
|
| 1 |
import { useState } from 'react';
|
| 2 |
import * as Dialog from '@radix-ui/react-dialog';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
+
import type { GitHubRepoInfo } from '~/components/@settings/tabs/connections/types/GitHub';
|
| 5 |
import { GitBranch } from '@phosphor-icons/react';
|
| 6 |
|
| 7 |
interface GitHubBranch {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -131,6 +131,13 @@ interface WebAppInfo {
|
|
| 131 |
gitInfo: GitInfo;
|
| 132 |
}
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
const DependencySection = ({
|
| 135 |
title,
|
| 136 |
deps,
|
|
@@ -146,7 +153,17 @@ const DependencySection = ({
|
|
| 146 |
|
| 147 |
return (
|
| 148 |
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
| 149 |
-
<CollapsibleTrigger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
<div className="flex items-center gap-3">
|
| 151 |
<div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
|
| 152 |
<span className="text-base text-bolt-elements-textPrimary">
|
|
@@ -157,15 +174,22 @@ const DependencySection = ({
|
|
| 157 |
<span className="text-sm text-bolt-elements-textSecondary">{isOpen ? 'Hide' : 'Show'}</span>
|
| 158 |
<div
|
| 159 |
className={classNames(
|
| 160 |
-
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
|
| 161 |
isOpen ? 'rotate-180' : '',
|
| 162 |
)}
|
| 163 |
/>
|
| 164 |
</div>
|
| 165 |
</CollapsibleTrigger>
|
| 166 |
<CollapsibleContent>
|
| 167 |
-
<ScrollArea
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
{deps.map((dep) => (
|
| 170 |
<div key={dep.name} className="flex items-center justify-between text-sm">
|
| 171 |
<span className="text-bolt-elements-textPrimary">{dep.name}</span>
|
|
@@ -182,6 +206,10 @@ const DependencySection = ({
|
|
| 182 |
export default function DebugTab() {
|
| 183 |
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
| 184 |
const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
const [loading, setLoading] = useState({
|
| 186 |
systemInfo: false,
|
| 187 |
webAppInfo: false,
|
|
@@ -259,7 +287,8 @@ export default function DebugTab() {
|
|
| 259 |
return undefined;
|
| 260 |
}
|
| 261 |
|
| 262 |
-
|
|
|
|
| 263 |
try {
|
| 264 |
const response = await fetch('/api/system/git-info');
|
| 265 |
const updatedGitInfo = (await response.json()) as GitInfo;
|
|
@@ -269,21 +298,27 @@ export default function DebugTab() {
|
|
| 269 |
return null;
|
| 270 |
}
|
| 271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
return {
|
| 273 |
...prev,
|
| 274 |
gitInfo: updatedGitInfo,
|
| 275 |
};
|
| 276 |
});
|
| 277 |
} catch (error) {
|
| 278 |
-
console.error('Failed to
|
| 279 |
}
|
| 280 |
-
}, 5000);
|
| 281 |
-
|
| 282 |
-
const cleanup = () => {
|
| 283 |
-
clearInterval(interval);
|
| 284 |
};
|
| 285 |
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
}, [openSections.webapp]);
|
| 288 |
|
| 289 |
const getSystemInfo = async () => {
|
|
@@ -616,11 +651,68 @@ export default function DebugTab() {
|
|
| 616 |
}
|
| 617 |
};
|
| 618 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
return (
|
| 620 |
<div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
|
| 621 |
{/* Quick Stats Banner */}
|
| 622 |
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
<div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
|
| 625 |
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
|
| 626 |
{systemInfo?.memory.percentage}%
|
|
@@ -628,7 +720,7 @@ export default function DebugTab() {
|
|
| 628 |
<Progress value={systemInfo?.memory.percentage || 0} className="mt-2" />
|
| 629 |
</div>
|
| 630 |
|
| 631 |
-
<div className="p-4 rounded-xl bg-
|
| 632 |
<div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
|
| 633 |
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
|
| 634 |
{systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) + 's' : '-'}
|
|
@@ -638,7 +730,7 @@ export default function DebugTab() {
|
|
| 638 |
</div>
|
| 639 |
</div>
|
| 640 |
|
| 641 |
-
<div className="p-4 rounded-xl bg-
|
| 642 |
<div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
|
| 643 |
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
|
| 644 |
{systemInfo?.network.downlink || '-'} Mbps
|
|
@@ -646,7 +738,7 @@ export default function DebugTab() {
|
|
| 646 |
<div className="text-xs text-bolt-elements-textSecondary mt-2">RTT: {systemInfo?.network.rtt || '-'} ms</div>
|
| 647 |
</div>
|
| 648 |
|
| 649 |
-
<div className="p-4 rounded-xl bg-
|
| 650 |
<div className="text-sm text-bolt-elements-textSecondary">Errors</div>
|
| 651 |
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLogs.length}</div>
|
| 652 |
</div>
|
|
@@ -659,10 +751,11 @@ export default function DebugTab() {
|
|
| 659 |
disabled={loading.systemInfo}
|
| 660 |
className={classNames(
|
| 661 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 662 |
-
'bg-
|
| 663 |
-
'
|
| 664 |
-
'
|
| 665 |
-
'
|
|
|
|
| 666 |
{ 'opacity-50 cursor-not-allowed': loading.systemInfo },
|
| 667 |
)}
|
| 668 |
>
|
|
@@ -679,10 +772,11 @@ export default function DebugTab() {
|
|
| 679 |
disabled={loading.performance}
|
| 680 |
className={classNames(
|
| 681 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 682 |
-
'bg-
|
| 683 |
-
'
|
| 684 |
-
'
|
| 685 |
-
'
|
|
|
|
| 686 |
{ 'opacity-50 cursor-not-allowed': loading.performance },
|
| 687 |
)}
|
| 688 |
>
|
|
@@ -699,10 +793,11 @@ export default function DebugTab() {
|
|
| 699 |
disabled={loading.errors}
|
| 700 |
className={classNames(
|
| 701 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 702 |
-
'bg-
|
| 703 |
-
'
|
| 704 |
-
'
|
| 705 |
-
'
|
|
|
|
| 706 |
{ 'opacity-50 cursor-not-allowed': loading.errors },
|
| 707 |
)}
|
| 708 |
>
|
|
@@ -719,10 +814,11 @@ export default function DebugTab() {
|
|
| 719 |
disabled={loading.webAppInfo}
|
| 720 |
className={classNames(
|
| 721 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 722 |
-
'bg-
|
| 723 |
-
'
|
| 724 |
-
'
|
| 725 |
-
'
|
|
|
|
| 726 |
{ 'opacity-50 cursor-not-allowed': loading.webAppInfo },
|
| 727 |
)}
|
| 728 |
>
|
|
@@ -738,10 +834,11 @@ export default function DebugTab() {
|
|
| 738 |
onClick={exportDebugInfo}
|
| 739 |
className={classNames(
|
| 740 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 741 |
-
'bg-
|
| 742 |
-
'
|
| 743 |
-
'
|
| 744 |
-
'
|
|
|
|
| 745 |
)}
|
| 746 |
>
|
| 747 |
<div className="i-ph:download w-4 h-4" />
|
|
@@ -1152,7 +1249,7 @@ export default function DebugTab() {
|
|
| 1152 |
{webAppInfo && (
|
| 1153 |
<div className="mt-6">
|
| 1154 |
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Dependencies</h3>
|
| 1155 |
-
<div className="
|
| 1156 |
<DependencySection title="Production" deps={webAppInfo.dependencies.production} />
|
| 1157 |
<DependencySection title="Development" deps={webAppInfo.dependencies.development} />
|
| 1158 |
<DependencySection title="Peer" deps={webAppInfo.dependencies.peer} />
|
|
|
|
| 131 |
gitInfo: GitInfo;
|
| 132 |
}
|
| 133 |
|
| 134 |
+
// Add Ollama service status interface
|
| 135 |
+
interface OllamaServiceStatus {
|
| 136 |
+
isRunning: boolean;
|
| 137 |
+
lastChecked: Date;
|
| 138 |
+
error?: string;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
const DependencySection = ({
|
| 142 |
title,
|
| 143 |
deps,
|
|
|
|
| 153 |
|
| 154 |
return (
|
| 155 |
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
| 156 |
+
<CollapsibleTrigger
|
| 157 |
+
className={classNames(
|
| 158 |
+
'flex w-full items-center justify-between p-4',
|
| 159 |
+
'bg-white dark:bg-[#0A0A0A]',
|
| 160 |
+
'hover:bg-purple-50/50 dark:hover:bg-[#1a1a1a]',
|
| 161 |
+
'border-b border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 162 |
+
'transition-colors duration-200',
|
| 163 |
+
'first:rounded-t-lg last:rounded-b-lg',
|
| 164 |
+
{ 'hover:rounded-lg': !isOpen },
|
| 165 |
+
)}
|
| 166 |
+
>
|
| 167 |
<div className="flex items-center gap-3">
|
| 168 |
<div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
|
| 169 |
<span className="text-base text-bolt-elements-textPrimary">
|
|
|
|
| 174 |
<span className="text-sm text-bolt-elements-textSecondary">{isOpen ? 'Hide' : 'Show'}</span>
|
| 175 |
<div
|
| 176 |
className={classNames(
|
| 177 |
+
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200 text-bolt-elements-textSecondary',
|
| 178 |
isOpen ? 'rotate-180' : '',
|
| 179 |
)}
|
| 180 |
/>
|
| 181 |
</div>
|
| 182 |
</CollapsibleTrigger>
|
| 183 |
<CollapsibleContent>
|
| 184 |
+
<ScrollArea
|
| 185 |
+
className={classNames(
|
| 186 |
+
'h-[200px] w-full',
|
| 187 |
+
'bg-white dark:bg-[#0A0A0A]',
|
| 188 |
+
'border-b border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 189 |
+
'last:rounded-b-lg last:border-b-0',
|
| 190 |
+
)}
|
| 191 |
+
>
|
| 192 |
+
<div className="space-y-2 p-4">
|
| 193 |
{deps.map((dep) => (
|
| 194 |
<div key={dep.name} className="flex items-center justify-between text-sm">
|
| 195 |
<span className="text-bolt-elements-textPrimary">{dep.name}</span>
|
|
|
|
| 206 |
export default function DebugTab() {
|
| 207 |
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
| 208 |
const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
|
| 209 |
+
const [ollamaStatus, setOllamaStatus] = useState<OllamaServiceStatus>({
|
| 210 |
+
isRunning: false,
|
| 211 |
+
lastChecked: new Date(),
|
| 212 |
+
});
|
| 213 |
const [loading, setLoading] = useState({
|
| 214 |
systemInfo: false,
|
| 215 |
webAppInfo: false,
|
|
|
|
| 287 |
return undefined;
|
| 288 |
}
|
| 289 |
|
| 290 |
+
// Initial fetch
|
| 291 |
+
const fetchGitInfo = async () => {
|
| 292 |
try {
|
| 293 |
const response = await fetch('/api/system/git-info');
|
| 294 |
const updatedGitInfo = (await response.json()) as GitInfo;
|
|
|
|
| 298 |
return null;
|
| 299 |
}
|
| 300 |
|
| 301 |
+
// Only update if the data has changed
|
| 302 |
+
if (JSON.stringify(prev.gitInfo) === JSON.stringify(updatedGitInfo)) {
|
| 303 |
+
return prev;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
return {
|
| 307 |
...prev,
|
| 308 |
gitInfo: updatedGitInfo,
|
| 309 |
};
|
| 310 |
});
|
| 311 |
} catch (error) {
|
| 312 |
+
console.error('Failed to fetch git info:', error);
|
| 313 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
};
|
| 315 |
|
| 316 |
+
fetchGitInfo();
|
| 317 |
+
|
| 318 |
+
// Refresh every 5 minutes instead of every second
|
| 319 |
+
const interval = setInterval(fetchGitInfo, 5 * 60 * 1000);
|
| 320 |
+
|
| 321 |
+
return () => clearInterval(interval);
|
| 322 |
}, [openSections.webapp]);
|
| 323 |
|
| 324 |
const getSystemInfo = async () => {
|
|
|
|
| 651 |
}
|
| 652 |
};
|
| 653 |
|
| 654 |
+
// Add Ollama health check function
|
| 655 |
+
const checkOllamaHealth = async () => {
|
| 656 |
+
try {
|
| 657 |
+
const response = await fetch('http://127.0.0.1:11434/api/version');
|
| 658 |
+
const isHealthy = response.ok;
|
| 659 |
+
|
| 660 |
+
setOllamaStatus({
|
| 661 |
+
isRunning: isHealthy,
|
| 662 |
+
lastChecked: new Date(),
|
| 663 |
+
error: isHealthy ? undefined : 'Ollama service is not responding',
|
| 664 |
+
});
|
| 665 |
+
|
| 666 |
+
return isHealthy;
|
| 667 |
+
} catch {
|
| 668 |
+
setOllamaStatus({
|
| 669 |
+
isRunning: false,
|
| 670 |
+
lastChecked: new Date(),
|
| 671 |
+
error: 'Failed to connect to Ollama service',
|
| 672 |
+
});
|
| 673 |
+
return false;
|
| 674 |
+
}
|
| 675 |
+
};
|
| 676 |
+
|
| 677 |
+
// Add Ollama health check effect
|
| 678 |
+
useEffect(() => {
|
| 679 |
+
const checkHealth = async () => {
|
| 680 |
+
await checkOllamaHealth();
|
| 681 |
+
};
|
| 682 |
+
|
| 683 |
+
checkHealth();
|
| 684 |
+
|
| 685 |
+
const interval = setInterval(checkHealth, 30000); // Check every 30 seconds
|
| 686 |
+
|
| 687 |
+
return () => clearInterval(interval);
|
| 688 |
+
}, []);
|
| 689 |
+
|
| 690 |
return (
|
| 691 |
<div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
|
| 692 |
{/* Quick Stats Banner */}
|
| 693 |
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
| 694 |
+
{/* Add Ollama Service Status Card */}
|
| 695 |
+
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
| 696 |
+
<div className="text-sm text-bolt-elements-textSecondary">Ollama Service</div>
|
| 697 |
+
<div className="flex items-center gap-2 mt-2">
|
| 698 |
+
<div
|
| 699 |
+
className={classNames(
|
| 700 |
+
'w-2 h-2 rounded-full animate-pulse',
|
| 701 |
+
ollamaStatus.isRunning ? 'bg-green-500' : 'bg-red-500',
|
| 702 |
+
)}
|
| 703 |
+
/>
|
| 704 |
+
<span
|
| 705 |
+
className={classNames('text-sm font-medium', ollamaStatus.isRunning ? 'text-green-500' : 'text-red-500')}
|
| 706 |
+
>
|
| 707 |
+
{ollamaStatus.isRunning ? 'Running' : 'Not Running'}
|
| 708 |
+
</span>
|
| 709 |
+
</div>
|
| 710 |
+
<div className="text-xs text-bolt-elements-textSecondary mt-2">
|
| 711 |
+
Last checked: {ollamaStatus.lastChecked.toLocaleTimeString()}
|
| 712 |
+
</div>
|
| 713 |
+
</div>
|
| 714 |
+
|
| 715 |
+
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
| 716 |
<div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
|
| 717 |
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
|
| 718 |
{systemInfo?.memory.percentage}%
|
|
|
|
| 720 |
<Progress value={systemInfo?.memory.percentage || 0} className="mt-2" />
|
| 721 |
</div>
|
| 722 |
|
| 723 |
+
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
| 724 |
<div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
|
| 725 |
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
|
| 726 |
{systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) + 's' : '-'}
|
|
|
|
| 730 |
</div>
|
| 731 |
</div>
|
| 732 |
|
| 733 |
+
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
| 734 |
<div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
|
| 735 |
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
|
| 736 |
{systemInfo?.network.downlink || '-'} Mbps
|
|
|
|
| 738 |
<div className="text-xs text-bolt-elements-textSecondary mt-2">RTT: {systemInfo?.network.rtt || '-'} ms</div>
|
| 739 |
</div>
|
| 740 |
|
| 741 |
+
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
| 742 |
<div className="text-sm text-bolt-elements-textSecondary">Errors</div>
|
| 743 |
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLogs.length}</div>
|
| 744 |
</div>
|
|
|
|
| 751 |
disabled={loading.systemInfo}
|
| 752 |
className={classNames(
|
| 753 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 754 |
+
'bg-white dark:bg-[#0A0A0A]',
|
| 755 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 756 |
+
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
| 757 |
+
'hover:border-purple-200 dark:hover:border-purple-900/30',
|
| 758 |
+
'text-bolt-elements-textPrimary',
|
| 759 |
{ 'opacity-50 cursor-not-allowed': loading.systemInfo },
|
| 760 |
)}
|
| 761 |
>
|
|
|
|
| 772 |
disabled={loading.performance}
|
| 773 |
className={classNames(
|
| 774 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 775 |
+
'bg-white dark:bg-[#0A0A0A]',
|
| 776 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 777 |
+
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
| 778 |
+
'hover:border-purple-200 dark:hover:border-purple-900/30',
|
| 779 |
+
'text-bolt-elements-textPrimary',
|
| 780 |
{ 'opacity-50 cursor-not-allowed': loading.performance },
|
| 781 |
)}
|
| 782 |
>
|
|
|
|
| 793 |
disabled={loading.errors}
|
| 794 |
className={classNames(
|
| 795 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 796 |
+
'bg-white dark:bg-[#0A0A0A]',
|
| 797 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 798 |
+
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
| 799 |
+
'hover:border-purple-200 dark:hover:border-purple-900/30',
|
| 800 |
+
'text-bolt-elements-textPrimary',
|
| 801 |
{ 'opacity-50 cursor-not-allowed': loading.errors },
|
| 802 |
)}
|
| 803 |
>
|
|
|
|
| 814 |
disabled={loading.webAppInfo}
|
| 815 |
className={classNames(
|
| 816 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 817 |
+
'bg-white dark:bg-[#0A0A0A]',
|
| 818 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 819 |
+
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
| 820 |
+
'hover:border-purple-200 dark:hover:border-purple-900/30',
|
| 821 |
+
'text-bolt-elements-textPrimary',
|
| 822 |
{ 'opacity-50 cursor-not-allowed': loading.webAppInfo },
|
| 823 |
)}
|
| 824 |
>
|
|
|
|
| 834 |
onClick={exportDebugInfo}
|
| 835 |
className={classNames(
|
| 836 |
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 837 |
+
'bg-white dark:bg-[#0A0A0A]',
|
| 838 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 839 |
+
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
|
| 840 |
+
'hover:border-purple-200 dark:hover:border-purple-900/30',
|
| 841 |
+
'text-bolt-elements-textPrimary',
|
| 842 |
)}
|
| 843 |
>
|
| 844 |
<div className="i-ph:download w-4 h-4" />
|
|
|
|
| 1249 |
{webAppInfo && (
|
| 1250 |
<div className="mt-6">
|
| 1251 |
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Dependencies</h3>
|
| 1252 |
+
<div className="bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded-lg divide-y divide-[#E5E5E5] dark:divide-[#1A1A1A]">
|
| 1253 |
<DependencySection title="Production" deps={webAppInfo.dependencies.production} />
|
| 1254 |
<DependencySection title="Development" deps={webAppInfo.dependencies.development} />
|
| 1255 |
<DependencySection title="Peer" deps={webAppInfo.dependencies.peer} />
|
|
@@ -0,0 +1,613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
| 2 |
+
import { motion } from 'framer-motion';
|
| 3 |
+
import { Switch } from '~/components/ui/Switch';
|
| 4 |
+
import { logStore, type LogEntry } from '~/lib/stores/logs';
|
| 5 |
+
import { useStore } from '@nanostores/react';
|
| 6 |
+
import { classNames } from '~/utils/classNames';
|
| 7 |
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
| 8 |
+
|
| 9 |
+
interface SelectOption {
|
| 10 |
+
value: string;
|
| 11 |
+
label: string;
|
| 12 |
+
icon?: string;
|
| 13 |
+
color?: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const logLevelOptions: SelectOption[] = [
|
| 17 |
+
{
|
| 18 |
+
value: 'all',
|
| 19 |
+
label: 'All Types',
|
| 20 |
+
icon: 'i-ph:funnel',
|
| 21 |
+
color: '#9333ea',
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
value: 'provider',
|
| 25 |
+
label: 'LLM',
|
| 26 |
+
icon: 'i-ph:robot',
|
| 27 |
+
color: '#10b981',
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
value: 'api',
|
| 31 |
+
label: 'API',
|
| 32 |
+
icon: 'i-ph:cloud',
|
| 33 |
+
color: '#3b82f6',
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
value: 'error',
|
| 37 |
+
label: 'Errors',
|
| 38 |
+
icon: 'i-ph:warning-circle',
|
| 39 |
+
color: '#ef4444',
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
value: 'warning',
|
| 43 |
+
label: 'Warnings',
|
| 44 |
+
icon: 'i-ph:warning',
|
| 45 |
+
color: '#f59e0b',
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
value: 'info',
|
| 49 |
+
label: 'Info',
|
| 50 |
+
icon: 'i-ph:info',
|
| 51 |
+
color: '#3b82f6',
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
value: 'debug',
|
| 55 |
+
label: 'Debug',
|
| 56 |
+
icon: 'i-ph:bug',
|
| 57 |
+
color: '#6b7280',
|
| 58 |
+
},
|
| 59 |
+
];
|
| 60 |
+
|
| 61 |
+
interface LogEntryItemProps {
|
| 62 |
+
log: LogEntry;
|
| 63 |
+
isExpanded: boolean;
|
| 64 |
+
use24Hour: boolean;
|
| 65 |
+
showTimestamp: boolean;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
const LogEntryItem = ({ log, isExpanded: forceExpanded, use24Hour, showTimestamp }: LogEntryItemProps) => {
|
| 69 |
+
const [localExpanded, setLocalExpanded] = useState(forceExpanded);
|
| 70 |
+
|
| 71 |
+
useEffect(() => {
|
| 72 |
+
setLocalExpanded(forceExpanded);
|
| 73 |
+
}, [forceExpanded]);
|
| 74 |
+
|
| 75 |
+
const timestamp = useMemo(() => {
|
| 76 |
+
const date = new Date(log.timestamp);
|
| 77 |
+
return date.toLocaleTimeString('en-US', { hour12: !use24Hour });
|
| 78 |
+
}, [log.timestamp, use24Hour]);
|
| 79 |
+
|
| 80 |
+
const style = useMemo(() => {
|
| 81 |
+
if (log.category === 'provider') {
|
| 82 |
+
return {
|
| 83 |
+
icon: 'i-ph:robot',
|
| 84 |
+
color: 'text-emerald-500 dark:text-emerald-400',
|
| 85 |
+
bg: 'hover:bg-emerald-500/10 dark:hover:bg-emerald-500/20',
|
| 86 |
+
badge: 'text-emerald-500 bg-emerald-50 dark:bg-emerald-500/10',
|
| 87 |
+
};
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
if (log.category === 'api') {
|
| 91 |
+
return {
|
| 92 |
+
icon: 'i-ph:cloud',
|
| 93 |
+
color: 'text-blue-500 dark:text-blue-400',
|
| 94 |
+
bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20',
|
| 95 |
+
badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10',
|
| 96 |
+
};
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
switch (log.level) {
|
| 100 |
+
case 'error':
|
| 101 |
+
return {
|
| 102 |
+
icon: 'i-ph:warning-circle',
|
| 103 |
+
color: 'text-red-500 dark:text-red-400',
|
| 104 |
+
bg: 'hover:bg-red-500/10 dark:hover:bg-red-500/20',
|
| 105 |
+
badge: 'text-red-500 bg-red-50 dark:bg-red-500/10',
|
| 106 |
+
};
|
| 107 |
+
case 'warning':
|
| 108 |
+
return {
|
| 109 |
+
icon: 'i-ph:warning',
|
| 110 |
+
color: 'text-yellow-500 dark:text-yellow-400',
|
| 111 |
+
bg: 'hover:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
|
| 112 |
+
badge: 'text-yellow-500 bg-yellow-50 dark:bg-yellow-500/10',
|
| 113 |
+
};
|
| 114 |
+
case 'debug':
|
| 115 |
+
return {
|
| 116 |
+
icon: 'i-ph:bug',
|
| 117 |
+
color: 'text-gray-500 dark:text-gray-400',
|
| 118 |
+
bg: 'hover:bg-gray-500/10 dark:hover:bg-gray-500/20',
|
| 119 |
+
badge: 'text-gray-500 bg-gray-50 dark:bg-gray-500/10',
|
| 120 |
+
};
|
| 121 |
+
default:
|
| 122 |
+
return {
|
| 123 |
+
icon: 'i-ph:info',
|
| 124 |
+
color: 'text-blue-500 dark:text-blue-400',
|
| 125 |
+
bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20',
|
| 126 |
+
badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10',
|
| 127 |
+
};
|
| 128 |
+
}
|
| 129 |
+
}, [log.level, log.category]);
|
| 130 |
+
|
| 131 |
+
const renderDetails = (details: any) => {
|
| 132 |
+
if (log.category === 'provider') {
|
| 133 |
+
return (
|
| 134 |
+
<div className="flex flex-col gap-2">
|
| 135 |
+
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
| 136 |
+
<span>Model: {details.model}</span>
|
| 137 |
+
<span>β’</span>
|
| 138 |
+
<span>Tokens: {details.totalTokens}</span>
|
| 139 |
+
<span>β’</span>
|
| 140 |
+
<span>Duration: {details.duration}ms</span>
|
| 141 |
+
</div>
|
| 142 |
+
{details.prompt && (
|
| 143 |
+
<div className="flex flex-col gap-1">
|
| 144 |
+
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">Prompt:</div>
|
| 145 |
+
<pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
|
| 146 |
+
{details.prompt}
|
| 147 |
+
</pre>
|
| 148 |
+
</div>
|
| 149 |
+
)}
|
| 150 |
+
{details.response && (
|
| 151 |
+
<div className="flex flex-col gap-1">
|
| 152 |
+
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">Response:</div>
|
| 153 |
+
<pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
|
| 154 |
+
{details.response}
|
| 155 |
+
</pre>
|
| 156 |
+
</div>
|
| 157 |
+
)}
|
| 158 |
+
</div>
|
| 159 |
+
);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
if (log.category === 'api') {
|
| 163 |
+
return (
|
| 164 |
+
<div className="flex flex-col gap-2">
|
| 165 |
+
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
| 166 |
+
<span className={details.method === 'GET' ? 'text-green-500' : 'text-blue-500'}>{details.method}</span>
|
| 167 |
+
<span>β’</span>
|
| 168 |
+
<span>Status: {details.statusCode}</span>
|
| 169 |
+
<span>β’</span>
|
| 170 |
+
<span>Duration: {details.duration}ms</span>
|
| 171 |
+
</div>
|
| 172 |
+
<div className="text-xs text-gray-600 dark:text-gray-400 break-all">{details.url}</div>
|
| 173 |
+
{details.request && (
|
| 174 |
+
<div className="flex flex-col gap-1">
|
| 175 |
+
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">Request:</div>
|
| 176 |
+
<pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
|
| 177 |
+
{JSON.stringify(details.request, null, 2)}
|
| 178 |
+
</pre>
|
| 179 |
+
</div>
|
| 180 |
+
)}
|
| 181 |
+
{details.response && (
|
| 182 |
+
<div className="flex flex-col gap-1">
|
| 183 |
+
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">Response:</div>
|
| 184 |
+
<pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
|
| 185 |
+
{JSON.stringify(details.response, null, 2)}
|
| 186 |
+
</pre>
|
| 187 |
+
</div>
|
| 188 |
+
)}
|
| 189 |
+
{details.error && (
|
| 190 |
+
<div className="flex flex-col gap-1">
|
| 191 |
+
<div className="text-xs font-medium text-red-500">Error:</div>
|
| 192 |
+
<pre className="text-xs text-red-400 bg-red-50 dark:bg-red-500/10 rounded p-2 whitespace-pre-wrap">
|
| 193 |
+
{JSON.stringify(details.error, null, 2)}
|
| 194 |
+
</pre>
|
| 195 |
+
</div>
|
| 196 |
+
)}
|
| 197 |
+
</div>
|
| 198 |
+
);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
return (
|
| 202 |
+
<pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded whitespace-pre-wrap">
|
| 203 |
+
{JSON.stringify(details, null, 2)}
|
| 204 |
+
</pre>
|
| 205 |
+
);
|
| 206 |
+
};
|
| 207 |
+
|
| 208 |
+
return (
|
| 209 |
+
<motion.div
|
| 210 |
+
initial={{ opacity: 0, y: 20 }}
|
| 211 |
+
animate={{ opacity: 1, y: 0 }}
|
| 212 |
+
className={classNames(
|
| 213 |
+
'flex flex-col gap-2',
|
| 214 |
+
'rounded-lg p-4',
|
| 215 |
+
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
| 216 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 217 |
+
style.bg,
|
| 218 |
+
'transition-all duration-200',
|
| 219 |
+
)}
|
| 220 |
+
>
|
| 221 |
+
<div className="flex items-start justify-between gap-4">
|
| 222 |
+
<div className="flex items-start gap-3">
|
| 223 |
+
<span className={classNames('text-lg', style.icon, style.color)} />
|
| 224 |
+
<div className="flex flex-col gap-1">
|
| 225 |
+
<div className="text-sm font-medium text-gray-900 dark:text-white">{log.message}</div>
|
| 226 |
+
{log.details && (
|
| 227 |
+
<>
|
| 228 |
+
<button
|
| 229 |
+
onClick={() => setLocalExpanded(!localExpanded)}
|
| 230 |
+
className="text-xs text-gray-500 dark:text-gray-400 hover:text-purple-500 dark:hover:text-purple-400 transition-colors"
|
| 231 |
+
>
|
| 232 |
+
{localExpanded ? 'Hide' : 'Show'} Details
|
| 233 |
+
</button>
|
| 234 |
+
{localExpanded && renderDetails(log.details)}
|
| 235 |
+
</>
|
| 236 |
+
)}
|
| 237 |
+
<div className="flex items-center gap-2">
|
| 238 |
+
<div className={classNames('px-2 py-0.5 rounded text-xs font-medium uppercase', style.badge)}>
|
| 239 |
+
{log.level}
|
| 240 |
+
</div>
|
| 241 |
+
{log.category && (
|
| 242 |
+
<div className="px-2 py-0.5 rounded-full text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
|
| 243 |
+
{log.category}
|
| 244 |
+
</div>
|
| 245 |
+
)}
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
{showTimestamp && <time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">{timestamp}</time>}
|
| 250 |
+
</div>
|
| 251 |
+
</motion.div>
|
| 252 |
+
);
|
| 253 |
+
};
|
| 254 |
+
|
| 255 |
+
export function EventLogsTab() {
|
| 256 |
+
const logs = useStore(logStore.logs);
|
| 257 |
+
const [selectedLevel, setSelectedLevel] = useState<'all' | string>('all');
|
| 258 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 259 |
+
const [use24Hour, setUse24Hour] = useState(false);
|
| 260 |
+
const [autoExpand, setAutoExpand] = useState(false);
|
| 261 |
+
const [showTimestamps, setShowTimestamps] = useState(true);
|
| 262 |
+
const [showLevelFilter, setShowLevelFilter] = useState(false);
|
| 263 |
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
| 264 |
+
const levelFilterRef = useRef<HTMLDivElement>(null);
|
| 265 |
+
|
| 266 |
+
const filteredLogs = useMemo(() => {
|
| 267 |
+
const allLogs = Object.values(logs);
|
| 268 |
+
|
| 269 |
+
if (selectedLevel === 'all') {
|
| 270 |
+
return allLogs.filter((log) =>
|
| 271 |
+
searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true,
|
| 272 |
+
);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
return allLogs.filter((log) => {
|
| 276 |
+
const matchesType = log.category === selectedLevel || log.level === selectedLevel;
|
| 277 |
+
const matchesSearch = searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true;
|
| 278 |
+
|
| 279 |
+
return matchesType && matchesSearch;
|
| 280 |
+
});
|
| 281 |
+
}, [logs, selectedLevel, searchQuery]);
|
| 282 |
+
|
| 283 |
+
// Add performance tracking on mount
|
| 284 |
+
useEffect(() => {
|
| 285 |
+
const startTime = performance.now();
|
| 286 |
+
|
| 287 |
+
logStore.logInfo('Event Logs tab mounted', {
|
| 288 |
+
type: 'component_mount',
|
| 289 |
+
message: 'Event Logs tab component mounted',
|
| 290 |
+
component: 'EventLogsTab',
|
| 291 |
+
});
|
| 292 |
+
|
| 293 |
+
return () => {
|
| 294 |
+
const duration = performance.now() - startTime;
|
| 295 |
+
logStore.logPerformanceMetric('EventLogsTab', 'mount-duration', duration);
|
| 296 |
+
};
|
| 297 |
+
}, []);
|
| 298 |
+
|
| 299 |
+
// Log filter changes
|
| 300 |
+
const handleLevelFilterChange = useCallback(
|
| 301 |
+
(newLevel: string) => {
|
| 302 |
+
logStore.logInfo('Log level filter changed', {
|
| 303 |
+
type: 'filter_change',
|
| 304 |
+
message: `Log level filter changed from ${selectedLevel} to ${newLevel}`,
|
| 305 |
+
component: 'EventLogsTab',
|
| 306 |
+
previousLevel: selectedLevel,
|
| 307 |
+
newLevel,
|
| 308 |
+
});
|
| 309 |
+
setSelectedLevel(newLevel as string);
|
| 310 |
+
setShowLevelFilter(false);
|
| 311 |
+
},
|
| 312 |
+
[selectedLevel],
|
| 313 |
+
);
|
| 314 |
+
|
| 315 |
+
// Log search changes with debounce
|
| 316 |
+
useEffect(() => {
|
| 317 |
+
const timeoutId = setTimeout(() => {
|
| 318 |
+
if (searchQuery) {
|
| 319 |
+
logStore.logInfo('Log search performed', {
|
| 320 |
+
type: 'search',
|
| 321 |
+
message: `Search performed with query "${searchQuery}" (${filteredLogs.length} results)`,
|
| 322 |
+
component: 'EventLogsTab',
|
| 323 |
+
query: searchQuery,
|
| 324 |
+
resultsCount: filteredLogs.length,
|
| 325 |
+
});
|
| 326 |
+
}
|
| 327 |
+
}, 1000);
|
| 328 |
+
|
| 329 |
+
return () => clearTimeout(timeoutId);
|
| 330 |
+
}, [searchQuery, filteredLogs.length]);
|
| 331 |
+
|
| 332 |
+
// Enhanced export logs handler
|
| 333 |
+
const handleExportLogs = useCallback(() => {
|
| 334 |
+
const startTime = performance.now();
|
| 335 |
+
|
| 336 |
+
try {
|
| 337 |
+
const exportData = {
|
| 338 |
+
timestamp: new Date().toISOString(),
|
| 339 |
+
logs: filteredLogs,
|
| 340 |
+
filters: {
|
| 341 |
+
level: selectedLevel,
|
| 342 |
+
searchQuery,
|
| 343 |
+
},
|
| 344 |
+
};
|
| 345 |
+
|
| 346 |
+
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
| 347 |
+
const url = URL.createObjectURL(blob);
|
| 348 |
+
const a = document.createElement('a');
|
| 349 |
+
a.href = url;
|
| 350 |
+
a.download = `bolt-logs-${new Date().toISOString()}.json`;
|
| 351 |
+
document.body.appendChild(a);
|
| 352 |
+
a.click();
|
| 353 |
+
document.body.removeChild(a);
|
| 354 |
+
URL.revokeObjectURL(url);
|
| 355 |
+
|
| 356 |
+
const duration = performance.now() - startTime;
|
| 357 |
+
logStore.logSuccess('Logs exported successfully', {
|
| 358 |
+
type: 'export',
|
| 359 |
+
message: `Successfully exported ${filteredLogs.length} logs`,
|
| 360 |
+
component: 'EventLogsTab',
|
| 361 |
+
exportedCount: filteredLogs.length,
|
| 362 |
+
filters: {
|
| 363 |
+
level: selectedLevel,
|
| 364 |
+
searchQuery,
|
| 365 |
+
},
|
| 366 |
+
duration,
|
| 367 |
+
});
|
| 368 |
+
} catch (error) {
|
| 369 |
+
logStore.logError('Failed to export logs', error, {
|
| 370 |
+
type: 'export_error',
|
| 371 |
+
message: 'Failed to export logs',
|
| 372 |
+
component: 'EventLogsTab',
|
| 373 |
+
});
|
| 374 |
+
}
|
| 375 |
+
}, [filteredLogs, selectedLevel, searchQuery]);
|
| 376 |
+
|
| 377 |
+
// Enhanced refresh handler
|
| 378 |
+
const handleRefresh = useCallback(async () => {
|
| 379 |
+
const startTime = performance.now();
|
| 380 |
+
setIsRefreshing(true);
|
| 381 |
+
|
| 382 |
+
try {
|
| 383 |
+
await logStore.refreshLogs();
|
| 384 |
+
|
| 385 |
+
const duration = performance.now() - startTime;
|
| 386 |
+
|
| 387 |
+
logStore.logSuccess('Logs refreshed successfully', {
|
| 388 |
+
type: 'refresh',
|
| 389 |
+
message: `Successfully refreshed ${Object.keys(logs).length} logs`,
|
| 390 |
+
component: 'EventLogsTab',
|
| 391 |
+
duration,
|
| 392 |
+
logsCount: Object.keys(logs).length,
|
| 393 |
+
});
|
| 394 |
+
} catch (error) {
|
| 395 |
+
logStore.logError('Failed to refresh logs', error, {
|
| 396 |
+
type: 'refresh_error',
|
| 397 |
+
message: 'Failed to refresh logs',
|
| 398 |
+
component: 'EventLogsTab',
|
| 399 |
+
});
|
| 400 |
+
} finally {
|
| 401 |
+
setTimeout(() => setIsRefreshing(false), 500);
|
| 402 |
+
}
|
| 403 |
+
}, [logs]);
|
| 404 |
+
|
| 405 |
+
// Log preference changes
|
| 406 |
+
const handlePreferenceChange = useCallback((type: string, value: boolean) => {
|
| 407 |
+
logStore.logInfo('Log preference changed', {
|
| 408 |
+
type: 'preference_change',
|
| 409 |
+
message: `Log preference "${type}" changed to ${value}`,
|
| 410 |
+
component: 'EventLogsTab',
|
| 411 |
+
preference: type,
|
| 412 |
+
value,
|
| 413 |
+
});
|
| 414 |
+
|
| 415 |
+
switch (type) {
|
| 416 |
+
case 'timestamps':
|
| 417 |
+
setShowTimestamps(value);
|
| 418 |
+
break;
|
| 419 |
+
case '24hour':
|
| 420 |
+
setUse24Hour(value);
|
| 421 |
+
break;
|
| 422 |
+
case 'autoExpand':
|
| 423 |
+
setAutoExpand(value);
|
| 424 |
+
break;
|
| 425 |
+
}
|
| 426 |
+
}, []);
|
| 427 |
+
|
| 428 |
+
// Close filters when clicking outside
|
| 429 |
+
useEffect(() => {
|
| 430 |
+
const handleClickOutside = (event: MouseEvent) => {
|
| 431 |
+
if (levelFilterRef.current && !levelFilterRef.current.contains(event.target as Node)) {
|
| 432 |
+
setShowLevelFilter(false);
|
| 433 |
+
}
|
| 434 |
+
};
|
| 435 |
+
|
| 436 |
+
document.addEventListener('mousedown', handleClickOutside);
|
| 437 |
+
|
| 438 |
+
return () => {
|
| 439 |
+
document.removeEventListener('mousedown', handleClickOutside);
|
| 440 |
+
};
|
| 441 |
+
}, []);
|
| 442 |
+
|
| 443 |
+
const selectedLevelOption = logLevelOptions.find((opt) => opt.value === selectedLevel);
|
| 444 |
+
|
| 445 |
+
return (
|
| 446 |
+
<div className="flex h-full flex-col gap-6">
|
| 447 |
+
<div className="flex items-center justify-between">
|
| 448 |
+
<DropdownMenu.Root open={showLevelFilter} onOpenChange={setShowLevelFilter}>
|
| 449 |
+
<DropdownMenu.Trigger asChild>
|
| 450 |
+
<button
|
| 451 |
+
className={classNames(
|
| 452 |
+
'flex items-center gap-2',
|
| 453 |
+
'rounded-lg px-3 py-1.5',
|
| 454 |
+
'text-sm text-gray-900 dark:text-white',
|
| 455 |
+
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
| 456 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 457 |
+
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
| 458 |
+
'transition-all duration-200',
|
| 459 |
+
)}
|
| 460 |
+
>
|
| 461 |
+
<span
|
| 462 |
+
className={classNames('text-lg', selectedLevelOption?.icon || 'i-ph:funnel')}
|
| 463 |
+
style={{ color: selectedLevelOption?.color }}
|
| 464 |
+
/>
|
| 465 |
+
{selectedLevelOption?.label || 'All Types'}
|
| 466 |
+
<span className="i-ph:caret-down text-lg text-gray-500 dark:text-gray-400" />
|
| 467 |
+
</button>
|
| 468 |
+
</DropdownMenu.Trigger>
|
| 469 |
+
|
| 470 |
+
<DropdownMenu.Portal>
|
| 471 |
+
<DropdownMenu.Content
|
| 472 |
+
className="min-w-[200px] bg-white dark:bg-[#0A0A0A] rounded-lg shadow-lg py-1 z-[250] animate-in fade-in-0 zoom-in-95 border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
| 473 |
+
sideOffset={5}
|
| 474 |
+
align="start"
|
| 475 |
+
side="bottom"
|
| 476 |
+
>
|
| 477 |
+
{logLevelOptions.map((option) => (
|
| 478 |
+
<DropdownMenu.Item
|
| 479 |
+
key={option.value}
|
| 480 |
+
className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
|
| 481 |
+
onClick={() => handleLevelFilterChange(option.value)}
|
| 482 |
+
>
|
| 483 |
+
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
| 484 |
+
<div
|
| 485 |
+
className={classNames(option.icon, 'text-lg group-hover:text-purple-500 transition-colors')}
|
| 486 |
+
style={{ color: option.color }}
|
| 487 |
+
/>
|
| 488 |
+
</div>
|
| 489 |
+
<span className="group-hover:text-purple-500 transition-colors">{option.label}</span>
|
| 490 |
+
</DropdownMenu.Item>
|
| 491 |
+
))}
|
| 492 |
+
</DropdownMenu.Content>
|
| 493 |
+
</DropdownMenu.Portal>
|
| 494 |
+
</DropdownMenu.Root>
|
| 495 |
+
|
| 496 |
+
<div className="flex items-center gap-4">
|
| 497 |
+
<div className="flex items-center gap-2">
|
| 498 |
+
<Switch
|
| 499 |
+
checked={showTimestamps}
|
| 500 |
+
onCheckedChange={(value) => handlePreferenceChange('timestamps', value)}
|
| 501 |
+
className="data-[state=checked]:bg-purple-500"
|
| 502 |
+
/>
|
| 503 |
+
<span className="text-sm text-gray-500 dark:text-gray-400">Show Timestamps</span>
|
| 504 |
+
</div>
|
| 505 |
+
|
| 506 |
+
<div className="flex items-center gap-2">
|
| 507 |
+
<Switch
|
| 508 |
+
checked={use24Hour}
|
| 509 |
+
onCheckedChange={(value) => handlePreferenceChange('24hour', value)}
|
| 510 |
+
className="data-[state=checked]:bg-purple-500"
|
| 511 |
+
/>
|
| 512 |
+
<span className="text-sm text-gray-500 dark:text-gray-400">24h Time</span>
|
| 513 |
+
</div>
|
| 514 |
+
|
| 515 |
+
<div className="flex items-center gap-2">
|
| 516 |
+
<Switch
|
| 517 |
+
checked={autoExpand}
|
| 518 |
+
onCheckedChange={(value) => handlePreferenceChange('autoExpand', value)}
|
| 519 |
+
className="data-[state=checked]:bg-purple-500"
|
| 520 |
+
/>
|
| 521 |
+
<span className="text-sm text-gray-500 dark:text-gray-400">Auto Expand</span>
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<div className="w-px h-4 bg-gray-200 dark:bg-gray-700" />
|
| 525 |
+
|
| 526 |
+
<button
|
| 527 |
+
onClick={handleRefresh}
|
| 528 |
+
className={classNames(
|
| 529 |
+
'group flex items-center gap-2',
|
| 530 |
+
'rounded-lg px-3 py-1.5',
|
| 531 |
+
'text-sm text-gray-900 dark:text-white',
|
| 532 |
+
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
| 533 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 534 |
+
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
| 535 |
+
'transition-all duration-200',
|
| 536 |
+
{ 'animate-spin': isRefreshing },
|
| 537 |
+
)}
|
| 538 |
+
>
|
| 539 |
+
<span className="i-ph:arrows-clockwise text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 540 |
+
Refresh
|
| 541 |
+
</button>
|
| 542 |
+
|
| 543 |
+
<button
|
| 544 |
+
onClick={handleExportLogs}
|
| 545 |
+
className={classNames(
|
| 546 |
+
'group flex items-center gap-2',
|
| 547 |
+
'rounded-lg px-3 py-1.5',
|
| 548 |
+
'text-sm text-gray-900 dark:text-white',
|
| 549 |
+
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
| 550 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 551 |
+
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
| 552 |
+
'transition-all duration-200',
|
| 553 |
+
)}
|
| 554 |
+
>
|
| 555 |
+
<span className="i-ph:download text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 556 |
+
Export
|
| 557 |
+
</button>
|
| 558 |
+
</div>
|
| 559 |
+
</div>
|
| 560 |
+
|
| 561 |
+
<div className="flex flex-col gap-4">
|
| 562 |
+
<div className="relative">
|
| 563 |
+
<input
|
| 564 |
+
type="text"
|
| 565 |
+
placeholder="Search logs..."
|
| 566 |
+
value={searchQuery}
|
| 567 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 568 |
+
className={classNames(
|
| 569 |
+
'w-full px-4 py-2 pl-10 rounded-lg',
|
| 570 |
+
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
| 571 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 572 |
+
'text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400',
|
| 573 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500',
|
| 574 |
+
'transition-all duration-200',
|
| 575 |
+
)}
|
| 576 |
+
/>
|
| 577 |
+
<div className="absolute left-3 top-1/2 -translate-y-1/2">
|
| 578 |
+
<div className="i-ph:magnifying-glass text-lg text-gray-500 dark:text-gray-400" />
|
| 579 |
+
</div>
|
| 580 |
+
</div>
|
| 581 |
+
|
| 582 |
+
{filteredLogs.length === 0 ? (
|
| 583 |
+
<motion.div
|
| 584 |
+
initial={{ opacity: 0, y: 20 }}
|
| 585 |
+
animate={{ opacity: 1, y: 0 }}
|
| 586 |
+
className={classNames(
|
| 587 |
+
'flex flex-col items-center justify-center gap-4',
|
| 588 |
+
'rounded-lg p-8 text-center',
|
| 589 |
+
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
| 590 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 591 |
+
)}
|
| 592 |
+
>
|
| 593 |
+
<span className="i-ph:clipboard-text text-4xl text-gray-400 dark:text-gray-600" />
|
| 594 |
+
<div className="flex flex-col gap-1">
|
| 595 |
+
<h3 className="text-sm font-medium text-gray-900 dark:text-white">No Logs Found</h3>
|
| 596 |
+
<p className="text-sm text-gray-500 dark:text-gray-400">Try adjusting your search or filters</p>
|
| 597 |
+
</div>
|
| 598 |
+
</motion.div>
|
| 599 |
+
) : (
|
| 600 |
+
filteredLogs.map((log) => (
|
| 601 |
+
<LogEntryItem
|
| 602 |
+
key={log.id}
|
| 603 |
+
log={log}
|
| 604 |
+
isExpanded={autoExpand}
|
| 605 |
+
use24Hour={use24Hour}
|
| 606 |
+
showTimestamp={showTimestamps}
|
| 607 |
+
/>
|
| 608 |
+
))
|
| 609 |
+
)}
|
| 610 |
+
</div>
|
| 611 |
+
</div>
|
| 612 |
+
);
|
| 613 |
+
}
|
|
@@ -111,44 +111,66 @@ export default function FeaturesTab() {
|
|
| 111 |
isLatestBranch,
|
| 112 |
contextOptimizationEnabled,
|
| 113 |
eventLogs,
|
| 114 |
-
isLocalModel,
|
| 115 |
setAutoSelectTemplate,
|
| 116 |
enableLatestBranch,
|
| 117 |
enableContextOptimization,
|
| 118 |
setEventLogs,
|
| 119 |
-
enableLocalModels,
|
| 120 |
setPromptId,
|
| 121 |
promptId,
|
| 122 |
} = useSettings();
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
const handleToggleFeature = useCallback(
|
| 125 |
(id: string, enabled: boolean) => {
|
| 126 |
switch (id) {
|
| 127 |
-
case 'latestBranch':
|
| 128 |
enableLatestBranch(enabled);
|
| 129 |
toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
|
| 130 |
break;
|
| 131 |
-
|
|
|
|
|
|
|
| 132 |
setAutoSelectTemplate(enabled);
|
| 133 |
toast.success(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
|
| 134 |
break;
|
| 135 |
-
|
|
|
|
|
|
|
| 136 |
enableContextOptimization(enabled);
|
| 137 |
toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
|
| 138 |
break;
|
| 139 |
-
|
|
|
|
|
|
|
| 140 |
setEventLogs(enabled);
|
| 141 |
toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
|
| 142 |
break;
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
|
| 146 |
-
break;
|
| 147 |
default:
|
| 148 |
break;
|
| 149 |
}
|
| 150 |
},
|
| 151 |
-
[enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs
|
| 152 |
);
|
| 153 |
|
| 154 |
const features = {
|
|
@@ -159,7 +181,7 @@ export default function FeaturesTab() {
|
|
| 159 |
description: 'Get the latest updates from the main branch',
|
| 160 |
icon: 'i-ph:git-branch',
|
| 161 |
enabled: isLatestBranch,
|
| 162 |
-
tooltip: '
|
| 163 |
},
|
| 164 |
{
|
| 165 |
id: 'autoSelectTemplate',
|
|
@@ -167,7 +189,7 @@ export default function FeaturesTab() {
|
|
| 167 |
description: 'Automatically select starter template',
|
| 168 |
icon: 'i-ph:selection',
|
| 169 |
enabled: autoSelectTemplate,
|
| 170 |
-
tooltip: '
|
| 171 |
},
|
| 172 |
{
|
| 173 |
id: 'contextOptimization',
|
|
@@ -175,7 +197,7 @@ export default function FeaturesTab() {
|
|
| 175 |
description: 'Optimize context for better responses',
|
| 176 |
icon: 'i-ph:brain',
|
| 177 |
enabled: contextOptimizationEnabled,
|
| 178 |
-
tooltip: '
|
| 179 |
},
|
| 180 |
{
|
| 181 |
id: 'eventLogs',
|
|
@@ -183,30 +205,19 @@ export default function FeaturesTab() {
|
|
| 183 |
description: 'Enable detailed event logging and history',
|
| 184 |
icon: 'i-ph:list-bullets',
|
| 185 |
enabled: eventLogs,
|
| 186 |
-
tooltip: '
|
| 187 |
},
|
| 188 |
],
|
| 189 |
beta: [],
|
| 190 |
-
experimental: [
|
| 191 |
-
{
|
| 192 |
-
id: 'localModels',
|
| 193 |
-
title: 'Experimental Providers',
|
| 194 |
-
description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
|
| 195 |
-
icon: 'i-ph:robot',
|
| 196 |
-
enabled: isLocalModel,
|
| 197 |
-
experimental: true,
|
| 198 |
-
tooltip: 'Try out new AI providers and models in development',
|
| 199 |
-
},
|
| 200 |
-
],
|
| 201 |
};
|
| 202 |
|
| 203 |
return (
|
| 204 |
<div className="flex flex-col gap-8">
|
| 205 |
<FeatureSection
|
| 206 |
-
title="
|
| 207 |
features={features.stable}
|
| 208 |
icon="i-ph:check-circle"
|
| 209 |
-
description="
|
| 210 |
onToggleFeature={handleToggleFeature}
|
| 211 |
/>
|
| 212 |
|
|
@@ -220,16 +231,6 @@ export default function FeaturesTab() {
|
|
| 220 |
/>
|
| 221 |
)}
|
| 222 |
|
| 223 |
-
{features.experimental.length > 0 && (
|
| 224 |
-
<FeatureSection
|
| 225 |
-
title="Experimental Features"
|
| 226 |
-
features={features.experimental}
|
| 227 |
-
icon="i-ph:flask"
|
| 228 |
-
description="Features in early development that may be unstable or require additional setup"
|
| 229 |
-
onToggleFeature={handleToggleFeature}
|
| 230 |
-
/>
|
| 231 |
-
)}
|
| 232 |
-
|
| 233 |
<motion.div
|
| 234 |
layout
|
| 235 |
className={classNames(
|
|
|
|
| 111 |
isLatestBranch,
|
| 112 |
contextOptimizationEnabled,
|
| 113 |
eventLogs,
|
|
|
|
| 114 |
setAutoSelectTemplate,
|
| 115 |
enableLatestBranch,
|
| 116 |
enableContextOptimization,
|
| 117 |
setEventLogs,
|
|
|
|
| 118 |
setPromptId,
|
| 119 |
promptId,
|
| 120 |
} = useSettings();
|
| 121 |
|
| 122 |
+
// Enable features by default on first load
|
| 123 |
+
React.useEffect(() => {
|
| 124 |
+
// Only enable if they haven't been explicitly set before
|
| 125 |
+
if (isLatestBranch === undefined) {
|
| 126 |
+
enableLatestBranch(true);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
if (contextOptimizationEnabled === undefined) {
|
| 130 |
+
enableContextOptimization(true);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
if (autoSelectTemplate === undefined) {
|
| 134 |
+
setAutoSelectTemplate(true);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
if (eventLogs === undefined) {
|
| 138 |
+
setEventLogs(true);
|
| 139 |
+
}
|
| 140 |
+
}, []); // Only run once on component mount
|
| 141 |
+
|
| 142 |
const handleToggleFeature = useCallback(
|
| 143 |
(id: string, enabled: boolean) => {
|
| 144 |
switch (id) {
|
| 145 |
+
case 'latestBranch': {
|
| 146 |
enableLatestBranch(enabled);
|
| 147 |
toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
|
| 148 |
break;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
case 'autoSelectTemplate': {
|
| 152 |
setAutoSelectTemplate(enabled);
|
| 153 |
toast.success(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
|
| 154 |
break;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
case 'contextOptimization': {
|
| 158 |
enableContextOptimization(enabled);
|
| 159 |
toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
|
| 160 |
break;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
case 'eventLogs': {
|
| 164 |
setEventLogs(enabled);
|
| 165 |
toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
|
| 166 |
break;
|
| 167 |
+
}
|
| 168 |
+
|
|
|
|
|
|
|
| 169 |
default:
|
| 170 |
break;
|
| 171 |
}
|
| 172 |
},
|
| 173 |
+
[enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs],
|
| 174 |
);
|
| 175 |
|
| 176 |
const features = {
|
|
|
|
| 181 |
description: 'Get the latest updates from the main branch',
|
| 182 |
icon: 'i-ph:git-branch',
|
| 183 |
enabled: isLatestBranch,
|
| 184 |
+
tooltip: 'Enabled by default to receive updates from the main development branch',
|
| 185 |
},
|
| 186 |
{
|
| 187 |
id: 'autoSelectTemplate',
|
|
|
|
| 189 |
description: 'Automatically select starter template',
|
| 190 |
icon: 'i-ph:selection',
|
| 191 |
enabled: autoSelectTemplate,
|
| 192 |
+
tooltip: 'Enabled by default to automatically select the most appropriate starter template',
|
| 193 |
},
|
| 194 |
{
|
| 195 |
id: 'contextOptimization',
|
|
|
|
| 197 |
description: 'Optimize context for better responses',
|
| 198 |
icon: 'i-ph:brain',
|
| 199 |
enabled: contextOptimizationEnabled,
|
| 200 |
+
tooltip: 'Enabled by default for improved AI responses',
|
| 201 |
},
|
| 202 |
{
|
| 203 |
id: 'eventLogs',
|
|
|
|
| 205 |
description: 'Enable detailed event logging and history',
|
| 206 |
icon: 'i-ph:list-bullets',
|
| 207 |
enabled: eventLogs,
|
| 208 |
+
tooltip: 'Enabled by default to record detailed logs of system events and user actions',
|
| 209 |
},
|
| 210 |
],
|
| 211 |
beta: [],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
};
|
| 213 |
|
| 214 |
return (
|
| 215 |
<div className="flex flex-col gap-8">
|
| 216 |
<FeatureSection
|
| 217 |
+
title="Core Features"
|
| 218 |
features={features.stable}
|
| 219 |
icon="i-ph:check-circle"
|
| 220 |
+
description="Essential features that are enabled by default for optimal performance"
|
| 221 |
onToggleFeature={handleToggleFeature}
|
| 222 |
/>
|
| 223 |
|
|
|
|
| 231 |
/>
|
| 232 |
)}
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
<motion.div
|
| 235 |
layout
|
| 236 |
className={classNames(
|
|
File without changes
|
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { useStore } from '@nanostores/react';
|
| 3 |
+
import { classNames } from '~/utils/classNames';
|
| 4 |
+
import { profileStore, updateProfile } from '~/lib/stores/profile';
|
| 5 |
+
import { toast } from 'react-toastify';
|
| 6 |
+
|
| 7 |
+
export default function ProfileTab() {
|
| 8 |
+
const profile = useStore(profileStore);
|
| 9 |
+
const [isUploading, setIsUploading] = useState(false);
|
| 10 |
+
|
| 11 |
+
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 12 |
+
const file = e.target.files?.[0];
|
| 13 |
+
|
| 14 |
+
if (!file) {
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
try {
|
| 19 |
+
setIsUploading(true);
|
| 20 |
+
|
| 21 |
+
// Convert the file to base64
|
| 22 |
+
const reader = new FileReader();
|
| 23 |
+
|
| 24 |
+
reader.onloadend = () => {
|
| 25 |
+
const base64String = reader.result as string;
|
| 26 |
+
updateProfile({ avatar: base64String });
|
| 27 |
+
setIsUploading(false);
|
| 28 |
+
toast.success('Profile picture updated');
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
reader.onerror = () => {
|
| 32 |
+
console.error('Error reading file:', reader.error);
|
| 33 |
+
setIsUploading(false);
|
| 34 |
+
toast.error('Failed to update profile picture');
|
| 35 |
+
};
|
| 36 |
+
reader.readAsDataURL(file);
|
| 37 |
+
} catch (error) {
|
| 38 |
+
console.error('Error uploading avatar:', error);
|
| 39 |
+
setIsUploading(false);
|
| 40 |
+
toast.error('Failed to update profile picture');
|
| 41 |
+
}
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
const handleProfileUpdate = (field: 'username' | 'bio', value: string) => {
|
| 45 |
+
updateProfile({ [field]: value });
|
| 46 |
+
|
| 47 |
+
// Only show toast for completed typing (after 1 second of no typing)
|
| 48 |
+
const debounceToast = setTimeout(() => {
|
| 49 |
+
toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`);
|
| 50 |
+
}, 1000);
|
| 51 |
+
|
| 52 |
+
return () => clearTimeout(debounceToast);
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
return (
|
| 56 |
+
<div className="max-w-2xl mx-auto">
|
| 57 |
+
<div className="space-y-6">
|
| 58 |
+
{/* Personal Information Section */}
|
| 59 |
+
<div>
|
| 60 |
+
{/* Avatar Upload */}
|
| 61 |
+
<div className="flex items-start gap-6 mb-8">
|
| 62 |
+
<div
|
| 63 |
+
className={classNames(
|
| 64 |
+
'w-24 h-24 rounded-full overflow-hidden',
|
| 65 |
+
'bg-gray-100 dark:bg-gray-800/50',
|
| 66 |
+
'flex items-center justify-center',
|
| 67 |
+
'ring-1 ring-gray-200 dark:ring-gray-700',
|
| 68 |
+
'relative group',
|
| 69 |
+
'transition-all duration-300 ease-out',
|
| 70 |
+
'hover:ring-purple-500/30 dark:hover:ring-purple-500/30',
|
| 71 |
+
'hover:shadow-lg hover:shadow-purple-500/10',
|
| 72 |
+
)}
|
| 73 |
+
>
|
| 74 |
+
{profile.avatar ? (
|
| 75 |
+
<img
|
| 76 |
+
src={profile.avatar}
|
| 77 |
+
alt="Profile"
|
| 78 |
+
className={classNames(
|
| 79 |
+
'w-full h-full object-cover',
|
| 80 |
+
'transition-all duration-300 ease-out',
|
| 81 |
+
'group-hover:scale-105 group-hover:brightness-90',
|
| 82 |
+
)}
|
| 83 |
+
/>
|
| 84 |
+
) : (
|
| 85 |
+
<div className="i-ph:robot-fill w-16 h-16 text-gray-400 dark:text-gray-500 transition-colors group-hover:text-purple-500/70 transform -translate-y-1" />
|
| 86 |
+
)}
|
| 87 |
+
|
| 88 |
+
<label
|
| 89 |
+
className={classNames(
|
| 90 |
+
'absolute inset-0',
|
| 91 |
+
'flex items-center justify-center',
|
| 92 |
+
'bg-black/0 group-hover:bg-black/40',
|
| 93 |
+
'cursor-pointer transition-all duration-300 ease-out',
|
| 94 |
+
isUploading ? 'cursor-wait' : '',
|
| 95 |
+
)}
|
| 96 |
+
>
|
| 97 |
+
<input
|
| 98 |
+
type="file"
|
| 99 |
+
accept="image/*"
|
| 100 |
+
className="hidden"
|
| 101 |
+
onChange={handleAvatarUpload}
|
| 102 |
+
disabled={isUploading}
|
| 103 |
+
/>
|
| 104 |
+
{isUploading ? (
|
| 105 |
+
<div className="i-ph:spinner-gap w-6 h-6 text-white animate-spin" />
|
| 106 |
+
) : (
|
| 107 |
+
<div className="i-ph:camera-plus w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-all duration-300 ease-out transform group-hover:scale-110" />
|
| 108 |
+
)}
|
| 109 |
+
</label>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<div className="flex-1 pt-1">
|
| 113 |
+
<label className="block text-base font-medium text-gray-900 dark:text-gray-100 mb-1">
|
| 114 |
+
Profile Picture
|
| 115 |
+
</label>
|
| 116 |
+
<p className="text-sm text-gray-500 dark:text-gray-400">Upload a profile picture or avatar</p>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
{/* Username Input */}
|
| 121 |
+
<div className="mb-6">
|
| 122 |
+
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Username</label>
|
| 123 |
+
<div className="relative group">
|
| 124 |
+
<div className="absolute left-3.5 top-1/2 -translate-y-1/2">
|
| 125 |
+
<div className="i-ph:user-circle-fill w-5 h-5 text-gray-400 dark:text-gray-500 transition-colors group-focus-within:text-purple-500" />
|
| 126 |
+
</div>
|
| 127 |
+
<input
|
| 128 |
+
type="text"
|
| 129 |
+
value={profile.username}
|
| 130 |
+
onChange={(e) => handleProfileUpdate('username', e.target.value)}
|
| 131 |
+
className={classNames(
|
| 132 |
+
'w-full pl-11 pr-4 py-2.5 rounded-xl',
|
| 133 |
+
'bg-white dark:bg-gray-800/50',
|
| 134 |
+
'border border-gray-200 dark:border-gray-700/50',
|
| 135 |
+
'text-gray-900 dark:text-white',
|
| 136 |
+
'placeholder-gray-400 dark:placeholder-gray-500',
|
| 137 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50',
|
| 138 |
+
'transition-all duration-300 ease-out',
|
| 139 |
+
)}
|
| 140 |
+
placeholder="Enter your username"
|
| 141 |
+
/>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
{/* Bio Input */}
|
| 146 |
+
<div className="mb-8">
|
| 147 |
+
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Bio</label>
|
| 148 |
+
<div className="relative group">
|
| 149 |
+
<div className="absolute left-3.5 top-3">
|
| 150 |
+
<div className="i-ph:text-aa w-5 h-5 text-gray-400 dark:text-gray-500 transition-colors group-focus-within:text-purple-500" />
|
| 151 |
+
</div>
|
| 152 |
+
<textarea
|
| 153 |
+
value={profile.bio}
|
| 154 |
+
onChange={(e) => handleProfileUpdate('bio', e.target.value)}
|
| 155 |
+
className={classNames(
|
| 156 |
+
'w-full pl-11 pr-4 py-2.5 rounded-xl',
|
| 157 |
+
'bg-white dark:bg-gray-800/50',
|
| 158 |
+
'border border-gray-200 dark:border-gray-700/50',
|
| 159 |
+
'text-gray-900 dark:text-white',
|
| 160 |
+
'placeholder-gray-400 dark:placeholder-gray-500',
|
| 161 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50',
|
| 162 |
+
'transition-all duration-300 ease-out',
|
| 163 |
+
'resize-none',
|
| 164 |
+
'h-32',
|
| 165 |
+
)}
|
| 166 |
+
placeholder="Tell us about yourself"
|
| 167 |
+
/>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
);
|
| 174 |
+
}
|
|
File without changes
|
|
@@ -4,7 +4,7 @@ import { useSettings } from '~/lib/hooks/useSettings';
|
|
| 4 |
import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
|
| 5 |
import type { IProviderConfig } from '~/types/model';
|
| 6 |
import { logStore } from '~/lib/stores/logs';
|
| 7 |
-
import { motion } from 'framer-motion';
|
| 8 |
import { classNames } from '~/utils/classNames';
|
| 9 |
import { BsRobot } from 'react-icons/bs';
|
| 10 |
import type { IconType } from 'react-icons';
|
|
@@ -12,6 +12,8 @@ import { BiChip } from 'react-icons/bi';
|
|
| 12 |
import { TbBrandOpenai } from 'react-icons/tb';
|
| 13 |
import { providerBaseUrlEnvKeys } from '~/utils/constants';
|
| 14 |
import { useToast } from '~/components/ui/use-toast';
|
|
|
|
|
|
|
| 15 |
|
| 16 |
// Add type for provider names to ensure type safety
|
| 17 |
type ProviderName = 'Ollama' | 'LMStudio' | 'OpenAILike';
|
|
@@ -53,12 +55,6 @@ interface OllamaModel {
|
|
| 53 |
};
|
| 54 |
}
|
| 55 |
|
| 56 |
-
interface OllamaServiceStatus {
|
| 57 |
-
isRunning: boolean;
|
| 58 |
-
lastChecked: Date;
|
| 59 |
-
error?: string;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
interface OllamaPullResponse {
|
| 63 |
status: string;
|
| 64 |
completed?: number;
|
|
@@ -75,33 +71,14 @@ const isOllamaPullResponse = (data: unknown): data is OllamaPullResponse => {
|
|
| 75 |
);
|
| 76 |
};
|
| 77 |
|
| 78 |
-
|
| 79 |
-
isOpen: boolean;
|
| 80 |
-
modelString: string;
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
export function LocalProvidersTab() {
|
| 84 |
-
const { success, error } = useToast();
|
| 85 |
const { providers, updateProviderSettings } = useSettings();
|
| 86 |
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
|
| 87 |
-
const [categoryEnabled, setCategoryEnabled] = useState
|
| 88 |
-
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
| 89 |
const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
|
| 90 |
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
| 91 |
-
const [
|
| 92 |
-
|
| 93 |
-
lastChecked: new Date(),
|
| 94 |
-
});
|
| 95 |
-
const [isInstallingModel, setIsInstallingModel] = useState<string | null>(null);
|
| 96 |
-
const [installProgress, setInstallProgress] = useState<{
|
| 97 |
-
model: string;
|
| 98 |
-
progress: number;
|
| 99 |
-
status: string;
|
| 100 |
-
} | null>(null);
|
| 101 |
-
const [manualInstall, setManualInstall] = useState<ManualInstallState>({
|
| 102 |
-
isOpen: false,
|
| 103 |
-
modelString: '',
|
| 104 |
-
});
|
| 105 |
|
| 106 |
// Effect to filter and sort providers
|
| 107 |
useEffect(() => {
|
|
@@ -166,12 +143,6 @@ export function LocalProvidersTab() {
|
|
| 166 |
setFilteredProviders(sorted);
|
| 167 |
}, [providers, updateProviderSettings]);
|
| 168 |
|
| 169 |
-
// Helper function to safely get environment URL
|
| 170 |
-
const getEnvUrl = (provider: IProviderConfig): string | undefined => {
|
| 171 |
-
const envKey = providerBaseUrlEnvKeys[provider.name]?.baseUrlKey;
|
| 172 |
-
return envKey ? (import.meta.env[envKey] as string | undefined) : undefined;
|
| 173 |
-
};
|
| 174 |
-
|
| 175 |
// Add effect to update category toggle state based on provider states
|
| 176 |
useEffect(() => {
|
| 177 |
const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
|
|
@@ -207,7 +178,7 @@ export function LocalProvidersTab() {
|
|
| 207 |
}
|
| 208 |
};
|
| 209 |
|
| 210 |
-
const updateOllamaModel = async (modelName: string): Promise<
|
| 211 |
try {
|
| 212 |
const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
|
| 213 |
method: 'POST',
|
|
@@ -265,74 +236,54 @@ export function LocalProvidersTab() {
|
|
| 265 |
const updatedData = (await updatedResponse.json()) as { models: OllamaModel[] };
|
| 266 |
const updatedModel = updatedData.models.find((m) => m.name === modelName);
|
| 267 |
|
| 268 |
-
return
|
| 269 |
} catch (error) {
|
| 270 |
console.error(`Error updating ${modelName}:`, error);
|
| 271 |
-
return
|
| 272 |
}
|
| 273 |
};
|
| 274 |
|
| 275 |
const handleToggleCategory = useCallback(
|
| 276 |
-
(enabled: boolean) => {
|
| 277 |
-
setCategoryEnabled(enabled);
|
| 278 |
filteredProviders.forEach((provider) => {
|
| 279 |
updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
| 280 |
});
|
| 281 |
-
|
| 282 |
},
|
| 283 |
-
[filteredProviders, updateProviderSettings
|
| 284 |
);
|
| 285 |
|
| 286 |
const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
|
| 287 |
-
updateProviderSettings(provider.name, {
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
if (enabled) {
|
| 290 |
logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
|
| 291 |
-
|
| 292 |
} else {
|
| 293 |
logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
|
| 294 |
-
|
| 295 |
}
|
| 296 |
};
|
| 297 |
|
| 298 |
-
const handleUpdateBaseUrl = (provider: IProviderConfig,
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
if (newBaseUrl && newBaseUrl.trim().length === 0) {
|
| 302 |
-
newBaseUrl = undefined;
|
| 303 |
-
}
|
| 304 |
-
|
| 305 |
-
updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
|
| 306 |
-
logStore.logProvider(`Base URL updated for ${provider.name}`, {
|
| 307 |
-
provider: provider.name,
|
| 308 |
baseUrl: newBaseUrl,
|
| 309 |
});
|
| 310 |
-
|
| 311 |
setEditingProvider(null);
|
| 312 |
};
|
| 313 |
|
| 314 |
const handleUpdateOllamaModel = async (modelName: string) => {
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
const { success: updateSuccess, newDigest } = await updateOllamaModel(modelName);
|
| 318 |
-
|
| 319 |
-
setOllamaModels((current) =>
|
| 320 |
-
current.map((m) =>
|
| 321 |
-
m.name === modelName
|
| 322 |
-
? {
|
| 323 |
-
...m,
|
| 324 |
-
status: updateSuccess ? 'updated' : 'error',
|
| 325 |
-
error: updateSuccess ? undefined : 'Update failed',
|
| 326 |
-
newDigest,
|
| 327 |
-
}
|
| 328 |
-
: m,
|
| 329 |
-
),
|
| 330 |
-
);
|
| 331 |
|
| 332 |
if (updateSuccess) {
|
| 333 |
-
|
| 334 |
} else {
|
| 335 |
-
|
| 336 |
}
|
| 337 |
};
|
| 338 |
|
|
@@ -351,336 +302,194 @@ export function LocalProvidersTab() {
|
|
| 351 |
}
|
| 352 |
|
| 353 |
setOllamaModels((current) => current.filter((m) => m.name !== modelName));
|
| 354 |
-
|
| 355 |
} catch (err) {
|
| 356 |
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
| 357 |
console.error(`Error deleting ${modelName}:`, errorMessage);
|
| 358 |
-
|
| 359 |
-
}
|
| 360 |
-
};
|
| 361 |
-
|
| 362 |
-
// Health check function
|
| 363 |
-
const checkOllamaHealth = async () => {
|
| 364 |
-
try {
|
| 365 |
-
// Use the root endpoint instead of /api/health
|
| 366 |
-
const response = await fetch(OLLAMA_API_URL);
|
| 367 |
-
const text = await response.text();
|
| 368 |
-
const isRunning = text.includes('Ollama is running');
|
| 369 |
-
|
| 370 |
-
setServiceStatus({
|
| 371 |
-
isRunning,
|
| 372 |
-
lastChecked: new Date(),
|
| 373 |
-
});
|
| 374 |
-
|
| 375 |
-
if (isRunning) {
|
| 376 |
-
// If Ollama is running, fetch models
|
| 377 |
-
fetchOllamaModels();
|
| 378 |
-
}
|
| 379 |
-
|
| 380 |
-
return isRunning;
|
| 381 |
-
} catch (error) {
|
| 382 |
-
console.error('Health check error:', error);
|
| 383 |
-
setServiceStatus({
|
| 384 |
-
isRunning: false,
|
| 385 |
-
lastChecked: new Date(),
|
| 386 |
-
error: error instanceof Error ? error.message : 'Failed to connect to Ollama service',
|
| 387 |
-
});
|
| 388 |
-
|
| 389 |
-
return false;
|
| 390 |
-
}
|
| 391 |
-
};
|
| 392 |
-
|
| 393 |
-
// Update manual installation function
|
| 394 |
-
const handleManualInstall = async (modelString: string) => {
|
| 395 |
-
try {
|
| 396 |
-
setIsInstallingModel(modelString);
|
| 397 |
-
setInstallProgress({ model: modelString, progress: 0, status: 'Starting download...' });
|
| 398 |
-
setManualInstall((prev) => ({ ...prev, isOpen: false }));
|
| 399 |
-
|
| 400 |
-
const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
|
| 401 |
-
method: 'POST',
|
| 402 |
-
headers: {
|
| 403 |
-
'Content-Type': 'application/json',
|
| 404 |
-
},
|
| 405 |
-
body: JSON.stringify({ name: modelString }),
|
| 406 |
-
});
|
| 407 |
-
|
| 408 |
-
if (!response.ok) {
|
| 409 |
-
throw new Error(`Failed to install ${modelString}`);
|
| 410 |
-
}
|
| 411 |
-
|
| 412 |
-
const reader = response.body?.getReader();
|
| 413 |
-
|
| 414 |
-
if (!reader) {
|
| 415 |
-
throw new Error('No response reader available');
|
| 416 |
-
}
|
| 417 |
-
|
| 418 |
-
while (true) {
|
| 419 |
-
const { done, value } = await reader.read();
|
| 420 |
-
|
| 421 |
-
if (done) {
|
| 422 |
-
break;
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
const text = new TextDecoder().decode(value);
|
| 426 |
-
const lines = text.split('\n').filter(Boolean);
|
| 427 |
-
|
| 428 |
-
for (const line of lines) {
|
| 429 |
-
const rawData = JSON.parse(line);
|
| 430 |
-
|
| 431 |
-
if (!isOllamaPullResponse(rawData)) {
|
| 432 |
-
console.error('Invalid response format:', rawData);
|
| 433 |
-
continue;
|
| 434 |
-
}
|
| 435 |
-
|
| 436 |
-
setInstallProgress({
|
| 437 |
-
model: modelString,
|
| 438 |
-
progress: rawData.completed && rawData.total ? (rawData.completed / rawData.total) * 100 : 0,
|
| 439 |
-
status: rawData.status,
|
| 440 |
-
});
|
| 441 |
-
}
|
| 442 |
-
}
|
| 443 |
-
|
| 444 |
-
success(`Successfully installed ${modelString}`);
|
| 445 |
-
await fetchOllamaModels();
|
| 446 |
-
} catch (err) {
|
| 447 |
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
| 448 |
-
console.error(`Error installing ${modelString}:`, errorMessage);
|
| 449 |
-
error(`Failed to install ${modelString}`);
|
| 450 |
-
} finally {
|
| 451 |
-
setIsInstallingModel(null);
|
| 452 |
-
setInstallProgress(null);
|
| 453 |
}
|
| 454 |
};
|
| 455 |
|
| 456 |
-
//
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
return (
|
| 475 |
<div
|
| 476 |
className={classNames(
|
| 477 |
-
'rounded-lg
|
| 478 |
'hover:bg-bolt-elements-background-depth-2',
|
| 479 |
'transition-all duration-200',
|
| 480 |
)}
|
|
|
|
|
|
|
| 481 |
>
|
| 482 |
-
{/* Service Status Indicator - Move to top */}
|
| 483 |
-
<div
|
| 484 |
-
className={classNames(
|
| 485 |
-
'flex items-center gap-2 p-2 rounded-lg',
|
| 486 |
-
serviceStatus.isRunning ? 'bg-green-500/10 text-green-500' : 'bg-red-500/10 text-red-500',
|
| 487 |
-
)}
|
| 488 |
-
>
|
| 489 |
-
<div className={classNames('w-2 h-2 rounded-full', serviceStatus.isRunning ? 'bg-green-500' : 'bg-red-500')} />
|
| 490 |
-
<span className="text-sm">
|
| 491 |
-
{serviceStatus.isRunning ? 'Ollama service is running' : 'Ollama service is not running'}
|
| 492 |
-
</span>
|
| 493 |
-
<span className="text-xs text-bolt-elements-textSecondary ml-2">
|
| 494 |
-
Last checked: {serviceStatus.lastChecked.toLocaleTimeString()}
|
| 495 |
-
</span>
|
| 496 |
-
</div>
|
| 497 |
-
|
| 498 |
<motion.div
|
| 499 |
-
className="space-y-
|
| 500 |
initial={{ opacity: 0, y: 20 }}
|
| 501 |
animate={{ opacity: 1, y: 0 }}
|
| 502 |
transition={{ duration: 0.3 }}
|
| 503 |
>
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
|
|
|
| 507 |
className={classNames(
|
| 508 |
-
'w-
|
| 509 |
-
'bg-
|
| 510 |
-
'text-purple-500',
|
| 511 |
)}
|
|
|
|
| 512 |
>
|
| 513 |
-
<BiChip className="w-
|
| 514 |
-
</div>
|
| 515 |
<div>
|
| 516 |
-
<
|
| 517 |
-
<p className="text-sm text-bolt-elements-textSecondary">
|
| 518 |
-
Configure and update local AI models on your machine
|
| 519 |
-
</p>
|
| 520 |
</div>
|
| 521 |
</div>
|
| 522 |
|
| 523 |
<div className="flex items-center gap-2">
|
| 524 |
-
<span className="text-sm text-bolt-elements-textSecondary">Enable All
|
| 525 |
-
<Switch
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
</div>
|
| 527 |
</div>
|
| 528 |
|
| 529 |
-
|
| 530 |
-
|
|
|
|
|
|
|
| 531 |
<motion.div
|
| 532 |
key={provider.name}
|
| 533 |
className={classNames(
|
| 534 |
-
'bg-bolt-elements-background-depth-2',
|
| 535 |
'hover:bg-bolt-elements-background-depth-3',
|
| 536 |
-
'transition-all duration-200',
|
| 537 |
'relative overflow-hidden group',
|
| 538 |
-
'flex flex-col',
|
| 539 |
-
|
| 540 |
-
// Make Ollama span 2 rows
|
| 541 |
-
provider.name === 'Ollama' ? 'row-span-2' : '',
|
| 542 |
-
|
| 543 |
-
// Place Ollama in the second column
|
| 544 |
-
provider.name === 'Ollama' ? 'col-start-2' : 'col-start-1',
|
| 545 |
)}
|
| 546 |
initial={{ opacity: 0, y: 20 }}
|
| 547 |
animate={{ opacity: 1, y: 0 }}
|
| 548 |
-
|
| 549 |
-
whileHover={{ scale: 1.02 }}
|
| 550 |
>
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
|
| 562 |
-
whileHover={{ scale: 1.05 }}
|
| 563 |
-
whileTap={{ scale: 0.95 }}
|
| 564 |
>
|
| 565 |
-
Configurable
|
| 566 |
-
</motion.span>
|
| 567 |
-
)}
|
| 568 |
-
</div>
|
| 569 |
-
|
| 570 |
-
<div className="flex items-start gap-4 p-4">
|
| 571 |
-
<motion.div
|
| 572 |
-
className={classNames(
|
| 573 |
-
'w-10 h-10 flex items-center justify-center rounded-xl',
|
| 574 |
-
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
|
| 575 |
-
'transition-all duration-200',
|
| 576 |
-
provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
|
| 577 |
-
)}
|
| 578 |
-
whileHover={{ scale: 1.1 }}
|
| 579 |
-
whileTap={{ scale: 0.9 }}
|
| 580 |
-
>
|
| 581 |
-
<div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
|
| 582 |
{React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
|
| 583 |
-
className: 'w-
|
| 584 |
-
'aria-label': `${provider.name}
|
| 585 |
})}
|
| 586 |
-
</div>
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
<div>
|
| 592 |
-
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
|
| 593 |
-
{provider.name}
|
| 594 |
-
</h4>
|
| 595 |
-
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
|
| 596 |
-
{PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
|
| 597 |
-
</p>
|
| 598 |
</div>
|
| 599 |
-
<
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
/>
|
| 603 |
</div>
|
| 604 |
-
|
| 605 |
-
{provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
|
| 606 |
-
<motion.div
|
| 607 |
-
initial={{ opacity: 0, height: 0 }}
|
| 608 |
-
animate={{ opacity: 1, height: 'auto' }}
|
| 609 |
-
exit={{ opacity: 0, height: 0 }}
|
| 610 |
-
transition={{ duration: 0.2 }}
|
| 611 |
-
>
|
| 612 |
-
<div className="flex items-center gap-2 mt-4">
|
| 613 |
-
{editingProvider === provider.name ? (
|
| 614 |
-
<input
|
| 615 |
-
type="text"
|
| 616 |
-
defaultValue={provider.settings.baseUrl}
|
| 617 |
-
placeholder={`Enter ${provider.name} base URL`}
|
| 618 |
-
className={classNames(
|
| 619 |
-
'flex-1 px-3 py-1.5 rounded-lg text-sm',
|
| 620 |
-
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
| 621 |
-
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
| 622 |
-
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
| 623 |
-
'transition-all duration-200',
|
| 624 |
-
)}
|
| 625 |
-
onKeyDown={(e) => {
|
| 626 |
-
if (e.key === 'Enter') {
|
| 627 |
-
handleUpdateBaseUrl(provider, e.currentTarget.value);
|
| 628 |
-
} else if (e.key === 'Escape') {
|
| 629 |
-
setEditingProvider(null);
|
| 630 |
-
}
|
| 631 |
-
}}
|
| 632 |
-
onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
|
| 633 |
-
autoFocus
|
| 634 |
-
/>
|
| 635 |
-
) : (
|
| 636 |
-
<div
|
| 637 |
-
className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
|
| 638 |
-
onClick={() => setEditingProvider(provider.name)}
|
| 639 |
-
>
|
| 640 |
-
<div className="flex items-center gap-2 text-bolt-elements-textSecondary">
|
| 641 |
-
<div className="i-ph:link text-sm" />
|
| 642 |
-
<span className="group-hover/url:text-purple-500 transition-colors">
|
| 643 |
-
{provider.settings.baseUrl || 'Click to set base URL'}
|
| 644 |
-
</span>
|
| 645 |
-
</div>
|
| 646 |
-
</div>
|
| 647 |
-
)}
|
| 648 |
-
|
| 649 |
-
{providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
|
| 650 |
-
<div className="mt-2 text-xs">
|
| 651 |
-
<div className="flex items-center gap-1">
|
| 652 |
-
<div
|
| 653 |
-
className={
|
| 654 |
-
getEnvUrl(provider)
|
| 655 |
-
? 'i-ph:check-circle text-green-500'
|
| 656 |
-
: 'i-ph:warning-circle text-yellow-500'
|
| 657 |
-
}
|
| 658 |
-
/>
|
| 659 |
-
<span className={getEnvUrl(provider) ? 'text-green-500' : 'text-yellow-500'}>
|
| 660 |
-
{getEnvUrl(provider)
|
| 661 |
-
? 'Environment URL set in .env.local'
|
| 662 |
-
: 'Environment URL not set in .env.local'}
|
| 663 |
-
</span>
|
| 664 |
-
</div>
|
| 665 |
-
</div>
|
| 666 |
-
)}
|
| 667 |
-
</div>
|
| 668 |
-
</motion.div>
|
| 669 |
-
)}
|
| 670 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
</div>
|
| 672 |
|
| 673 |
-
{
|
| 674 |
-
|
|
|
|
| 675 |
<div className="flex items-center justify-between">
|
| 676 |
<div className="flex items-center gap-2">
|
| 677 |
<div className="i-ph:cube-duotone text-purple-500" />
|
| 678 |
-
<
|
| 679 |
</div>
|
| 680 |
{isLoadingModels ? (
|
| 681 |
-
<div className="flex items-center gap-2
|
| 682 |
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
|
| 683 |
-
Loading models
|
| 684 |
</div>
|
| 685 |
) : (
|
| 686 |
<span className="text-sm text-bolt-elements-textSecondary">
|
|
@@ -689,226 +498,221 @@ export function LocalProvidersTab() {
|
|
| 689 |
)}
|
| 690 |
</div>
|
| 691 |
|
| 692 |
-
<div className="space-y-
|
| 693 |
-
{
|
| 694 |
-
<div
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
</div>
|
| 707 |
-
|
| 708 |
-
<
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
</span>
|
| 727 |
)}
|
| 728 |
</div>
|
| 729 |
</div>
|
| 730 |
-
<
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
disabled={model.status === 'updating'}
|
| 734 |
-
className={classNames(
|
| 735 |
-
'rounded-md px-4 py-2 text-sm',
|
| 736 |
-
'bg-purple-500 text-white',
|
| 737 |
-
'hover:bg-purple-600',
|
| 738 |
-
'dark:bg-purple-500 dark:hover:bg-purple-600',
|
| 739 |
-
'transition-all duration-200',
|
| 740 |
-
)}
|
| 741 |
-
whileHover={{ scale: 1.02 }}
|
| 742 |
-
whileTap={{ scale: 0.98 }}
|
| 743 |
-
>
|
| 744 |
-
<div className="i-ph:arrows-clockwise" />
|
| 745 |
-
Update
|
| 746 |
-
</motion.button>
|
| 747 |
-
<motion.button
|
| 748 |
-
onClick={() => {
|
| 749 |
-
if (window.confirm(`Are you sure you want to delete ${model.name}?`)) {
|
| 750 |
-
handleDeleteOllamaModel(model.name);
|
| 751 |
-
}
|
| 752 |
-
}}
|
| 753 |
-
disabled={model.status === 'updating'}
|
| 754 |
-
className={classNames(
|
| 755 |
-
'rounded-md px-4 py-2 text-sm',
|
| 756 |
-
'bg-red-500 text-white',
|
| 757 |
-
'hover:bg-red-600',
|
| 758 |
-
'dark:bg-red-500 dark:hover:bg-red-600',
|
| 759 |
-
'transition-all duration-200',
|
| 760 |
-
)}
|
| 761 |
-
whileHover={{ scale: 1.02 }}
|
| 762 |
-
whileTap={{ scale: 0.98 }}
|
| 763 |
-
>
|
| 764 |
-
<div className="i-ph:trash" />
|
| 765 |
-
Delete
|
| 766 |
-
</motion.button>
|
| 767 |
-
</div>
|
| 768 |
</div>
|
| 769 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 770 |
</div>
|
| 771 |
-
</div>
|
| 772 |
-
)}
|
| 773 |
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
</div>
|
| 785 |
</motion.div>
|
|
|
|
|
|
|
|
|
|
| 786 |
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Install New Model</h3>
|
| 793 |
-
<p className="text-sm text-bolt-elements-textSecondary">
|
| 794 |
-
Enter the model name exactly as shown (e.g., deepseek-r1:1.5b)
|
| 795 |
-
</p>
|
| 796 |
-
</div>
|
| 797 |
-
</div>
|
| 798 |
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
</div>
|
| 805 |
-
<div className="space-y-2 text-sm text-bolt-elements-textSecondary">
|
| 806 |
-
<p>
|
| 807 |
-
Browse available models at{' '}
|
| 808 |
-
<a
|
| 809 |
-
href="https://ollama.com/library"
|
| 810 |
-
target="_blank"
|
| 811 |
-
rel="noopener noreferrer"
|
| 812 |
-
className="text-purple-500 hover:underline"
|
| 813 |
-
>
|
| 814 |
-
ollama.com/library
|
| 815 |
-
</a>
|
| 816 |
-
</p>
|
| 817 |
-
<div className="space-y-1">
|
| 818 |
-
<p className="font-medium text-bolt-elements-textPrimary">Popular models:</p>
|
| 819 |
-
<ul className="list-disc list-inside space-y-1 ml-2">
|
| 820 |
-
<li>deepseek-r1:1.5b - DeepSeek's reasoning model</li>
|
| 821 |
-
<li>llama3:8b - Meta's Llama 3 (8B parameters)</li>
|
| 822 |
-
<li>mistral:7b - Mistral's 7B model</li>
|
| 823 |
-
<li>gemma:2b - Google's Gemma model</li>
|
| 824 |
-
<li>qwen2:7b - Alibaba's Qwen2 model</li>
|
| 825 |
-
</ul>
|
| 826 |
-
</div>
|
| 827 |
-
<p className="mt-2">
|
| 828 |
-
<span className="text-yellow-500">Note:</span> Copy the exact model name including the tag (e.g.,
|
| 829 |
-
'deepseek-r1:1.5b') from the library to ensure successful installation.
|
| 830 |
-
</p>
|
| 831 |
-
</div>
|
| 832 |
-
</div>
|
| 833 |
|
| 834 |
-
|
| 835 |
-
<div className="flex-1">
|
| 836 |
-
<input
|
| 837 |
-
type="text"
|
| 838 |
-
className="w-full px-3 py-2 rounded-md bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor text-bolt-elements-textPrimary"
|
| 839 |
-
placeholder="deepseek-r1:1.5b"
|
| 840 |
-
value={manualInstall.modelString}
|
| 841 |
-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
| 842 |
-
setManualInstall((prev) => ({ ...prev, modelString: e.target.value }))
|
| 843 |
-
}
|
| 844 |
-
/>
|
| 845 |
-
</div>
|
| 846 |
-
<motion.button
|
| 847 |
-
onClick={() => handleManualInstall(manualInstall.modelString)}
|
| 848 |
-
disabled={!manualInstall.modelString || !!isInstallingModel}
|
| 849 |
-
className={classNames(
|
| 850 |
-
'rounded-md px-4 py-2 text-sm',
|
| 851 |
-
'bg-purple-500 text-white',
|
| 852 |
-
'hover:bg-purple-600',
|
| 853 |
-
'dark:bg-purple-500 dark:hover:bg-purple-600',
|
| 854 |
-
'transition-all duration-200',
|
| 855 |
-
)}
|
| 856 |
-
whileHover={{ scale: 1.02 }}
|
| 857 |
-
whileTap={{ scale: 0.98 }}
|
| 858 |
-
>
|
| 859 |
-
{isInstallingModel ? (
|
| 860 |
-
<div className="flex items-center justify-center gap-2">
|
| 861 |
-
<div className="i-ph:spinner-gap-bold animate-spin" />
|
| 862 |
-
Installing...
|
| 863 |
-
</div>
|
| 864 |
-
) : (
|
| 865 |
-
<>
|
| 866 |
-
<div className="i-ph:download" />
|
| 867 |
-
Install Model
|
| 868 |
-
</>
|
| 869 |
-
)}
|
| 870 |
-
</motion.button>
|
| 871 |
-
{isInstallingModel && (
|
| 872 |
-
<motion.button
|
| 873 |
-
onClick={() => {
|
| 874 |
-
setIsInstallingModel(null);
|
| 875 |
-
setInstallProgress(null);
|
| 876 |
-
error('Installation cancelled');
|
| 877 |
-
}}
|
| 878 |
-
className={classNames(
|
| 879 |
-
'rounded-md px-4 py-2 text-sm',
|
| 880 |
-
'bg-red-500 text-white',
|
| 881 |
-
'hover:bg-red-600',
|
| 882 |
-
'dark:bg-red-500 dark:hover:bg-red-600',
|
| 883 |
-
'transition-all duration-200',
|
| 884 |
-
)}
|
| 885 |
-
whileHover={{ scale: 1.02 }}
|
| 886 |
-
whileTap={{ scale: 0.98 }}
|
| 887 |
-
>
|
| 888 |
-
<div className="i-ph:x" />
|
| 889 |
-
Cancel
|
| 890 |
-
</motion.button>
|
| 891 |
-
)}
|
| 892 |
-
</div>
|
| 893 |
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
className="h-full bg-purple-500 transition-all duration-200"
|
| 903 |
-
style={{ width: `${installProgress.progress}%` }}
|
| 904 |
-
/>
|
| 905 |
-
</div>
|
| 906 |
-
</div>
|
| 907 |
-
)}
|
| 908 |
-
</div>
|
| 909 |
-
)}
|
| 910 |
-
</div>
|
| 911 |
);
|
| 912 |
}
|
| 913 |
-
|
| 914 |
-
export default LocalProvidersTab;
|
|
|
|
| 4 |
import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
|
| 5 |
import type { IProviderConfig } from '~/types/model';
|
| 6 |
import { logStore } from '~/lib/stores/logs';
|
| 7 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 8 |
import { classNames } from '~/utils/classNames';
|
| 9 |
import { BsRobot } from 'react-icons/bs';
|
| 10 |
import type { IconType } from 'react-icons';
|
|
|
|
| 12 |
import { TbBrandOpenai } from 'react-icons/tb';
|
| 13 |
import { providerBaseUrlEnvKeys } from '~/utils/constants';
|
| 14 |
import { useToast } from '~/components/ui/use-toast';
|
| 15 |
+
import { Progress } from '~/components/ui/Progress';
|
| 16 |
+
import OllamaModelInstaller from './OllamaModelInstaller';
|
| 17 |
|
| 18 |
// Add type for provider names to ensure type safety
|
| 19 |
type ProviderName = 'Ollama' | 'LMStudio' | 'OpenAILike';
|
|
|
|
| 55 |
};
|
| 56 |
}
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
interface OllamaPullResponse {
|
| 59 |
status: string;
|
| 60 |
completed?: number;
|
|
|
|
| 71 |
);
|
| 72 |
};
|
| 73 |
|
| 74 |
+
export default function LocalProvidersTab() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
const { providers, updateProviderSettings } = useSettings();
|
| 76 |
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
|
| 77 |
+
const [categoryEnabled, setCategoryEnabled] = useState(false);
|
|
|
|
| 78 |
const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
|
| 79 |
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
| 80 |
+
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
| 81 |
+
const { toast } = useToast();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
// Effect to filter and sort providers
|
| 84 |
useEffect(() => {
|
|
|
|
| 143 |
setFilteredProviders(sorted);
|
| 144 |
}, [providers, updateProviderSettings]);
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
// Add effect to update category toggle state based on provider states
|
| 147 |
useEffect(() => {
|
| 148 |
const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
|
|
|
|
| 178 |
}
|
| 179 |
};
|
| 180 |
|
| 181 |
+
const updateOllamaModel = async (modelName: string): Promise<boolean> => {
|
| 182 |
try {
|
| 183 |
const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
|
| 184 |
method: 'POST',
|
|
|
|
| 236 |
const updatedData = (await updatedResponse.json()) as { models: OllamaModel[] };
|
| 237 |
const updatedModel = updatedData.models.find((m) => m.name === modelName);
|
| 238 |
|
| 239 |
+
return updatedModel !== undefined;
|
| 240 |
} catch (error) {
|
| 241 |
console.error(`Error updating ${modelName}:`, error);
|
| 242 |
+
return false;
|
| 243 |
}
|
| 244 |
};
|
| 245 |
|
| 246 |
const handleToggleCategory = useCallback(
|
| 247 |
+
async (enabled: boolean) => {
|
|
|
|
| 248 |
filteredProviders.forEach((provider) => {
|
| 249 |
updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
| 250 |
});
|
| 251 |
+
toast(enabled ? 'All local providers enabled' : 'All local providers disabled');
|
| 252 |
},
|
| 253 |
+
[filteredProviders, updateProviderSettings],
|
| 254 |
);
|
| 255 |
|
| 256 |
const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
|
| 257 |
+
updateProviderSettings(provider.name, {
|
| 258 |
+
...provider.settings,
|
| 259 |
+
enabled,
|
| 260 |
+
});
|
| 261 |
|
| 262 |
if (enabled) {
|
| 263 |
logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
|
| 264 |
+
toast(`${provider.name} enabled`);
|
| 265 |
} else {
|
| 266 |
logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
|
| 267 |
+
toast(`${provider.name} disabled`);
|
| 268 |
}
|
| 269 |
};
|
| 270 |
|
| 271 |
+
const handleUpdateBaseUrl = (provider: IProviderConfig, newBaseUrl: string) => {
|
| 272 |
+
updateProviderSettings(provider.name, {
|
| 273 |
+
...provider.settings,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
baseUrl: newBaseUrl,
|
| 275 |
});
|
| 276 |
+
toast(`${provider.name} base URL updated`);
|
| 277 |
setEditingProvider(null);
|
| 278 |
};
|
| 279 |
|
| 280 |
const handleUpdateOllamaModel = async (modelName: string) => {
|
| 281 |
+
const updateSuccess = await updateOllamaModel(modelName);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
if (updateSuccess) {
|
| 284 |
+
toast(`Updated ${modelName}`);
|
| 285 |
} else {
|
| 286 |
+
toast(`Failed to update ${modelName}`);
|
| 287 |
}
|
| 288 |
};
|
| 289 |
|
|
|
|
| 302 |
}
|
| 303 |
|
| 304 |
setOllamaModels((current) => current.filter((m) => m.name !== modelName));
|
| 305 |
+
toast(`Deleted ${modelName}`);
|
| 306 |
} catch (err) {
|
| 307 |
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
| 308 |
console.error(`Error deleting ${modelName}:`, errorMessage);
|
| 309 |
+
toast(`Failed to delete ${modelName}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
}
|
| 311 |
};
|
| 312 |
|
| 313 |
+
// Update model details display
|
| 314 |
+
const ModelDetails = ({ model }: { model: OllamaModel }) => (
|
| 315 |
+
<div className="flex items-center gap-3 text-xs text-bolt-elements-textSecondary">
|
| 316 |
+
<div className="flex items-center gap-1">
|
| 317 |
+
<div className="i-ph:code text-purple-500" />
|
| 318 |
+
<span>{model.digest.substring(0, 7)}</span>
|
| 319 |
+
</div>
|
| 320 |
+
{model.details && (
|
| 321 |
+
<>
|
| 322 |
+
<div className="flex items-center gap-1">
|
| 323 |
+
<div className="i-ph:database text-purple-500" />
|
| 324 |
+
<span>{model.details.parameter_size}</span>
|
| 325 |
+
</div>
|
| 326 |
+
<div className="flex items-center gap-1">
|
| 327 |
+
<div className="i-ph:cube text-purple-500" />
|
| 328 |
+
<span>{model.details.quantization_level}</span>
|
| 329 |
+
</div>
|
| 330 |
+
</>
|
| 331 |
+
)}
|
| 332 |
+
</div>
|
| 333 |
+
);
|
| 334 |
|
| 335 |
+
// Update model actions to not use Tooltip
|
| 336 |
+
const ModelActions = ({
|
| 337 |
+
model,
|
| 338 |
+
onUpdate,
|
| 339 |
+
onDelete,
|
| 340 |
+
}: {
|
| 341 |
+
model: OllamaModel;
|
| 342 |
+
onUpdate: () => void;
|
| 343 |
+
onDelete: () => void;
|
| 344 |
+
}) => (
|
| 345 |
+
<div className="flex items-center gap-2">
|
| 346 |
+
<motion.button
|
| 347 |
+
onClick={onUpdate}
|
| 348 |
+
disabled={model.status === 'updating'}
|
| 349 |
+
className={classNames(
|
| 350 |
+
'rounded-lg p-2',
|
| 351 |
+
'bg-purple-500/10 text-purple-500',
|
| 352 |
+
'hover:bg-purple-500/20',
|
| 353 |
+
'transition-all duration-200',
|
| 354 |
+
{ 'opacity-50 cursor-not-allowed': model.status === 'updating' },
|
| 355 |
+
)}
|
| 356 |
+
whileHover={{ scale: 1.05 }}
|
| 357 |
+
whileTap={{ scale: 0.95 }}
|
| 358 |
+
title="Update model"
|
| 359 |
+
>
|
| 360 |
+
{model.status === 'updating' ? (
|
| 361 |
+
<div className="flex items-center gap-2">
|
| 362 |
+
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
|
| 363 |
+
<span className="text-sm">Updating...</span>
|
| 364 |
+
</div>
|
| 365 |
+
) : (
|
| 366 |
+
<div className="i-ph:arrows-clockwise text-lg" />
|
| 367 |
+
)}
|
| 368 |
+
</motion.button>
|
| 369 |
+
<motion.button
|
| 370 |
+
onClick={onDelete}
|
| 371 |
+
disabled={model.status === 'updating'}
|
| 372 |
+
className={classNames(
|
| 373 |
+
'rounded-lg p-2',
|
| 374 |
+
'bg-red-500/10 text-red-500',
|
| 375 |
+
'hover:bg-red-500/20',
|
| 376 |
+
'transition-all duration-200',
|
| 377 |
+
{ 'opacity-50 cursor-not-allowed': model.status === 'updating' },
|
| 378 |
+
)}
|
| 379 |
+
whileHover={{ scale: 1.05 }}
|
| 380 |
+
whileTap={{ scale: 0.95 }}
|
| 381 |
+
title="Delete model"
|
| 382 |
+
>
|
| 383 |
+
<div className="i-ph:trash text-lg" />
|
| 384 |
+
</motion.button>
|
| 385 |
+
</div>
|
| 386 |
+
);
|
| 387 |
|
| 388 |
return (
|
| 389 |
<div
|
| 390 |
className={classNames(
|
| 391 |
+
'rounded-lg bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
|
| 392 |
'hover:bg-bolt-elements-background-depth-2',
|
| 393 |
'transition-all duration-200',
|
| 394 |
)}
|
| 395 |
+
role="region"
|
| 396 |
+
aria-label="Local Providers Configuration"
|
| 397 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
<motion.div
|
| 399 |
+
className="space-y-6"
|
| 400 |
initial={{ opacity: 0, y: 20 }}
|
| 401 |
animate={{ opacity: 1, y: 0 }}
|
| 402 |
transition={{ duration: 0.3 }}
|
| 403 |
>
|
| 404 |
+
{/* Header section */}
|
| 405 |
+
<div className="flex items-center justify-between gap-4 border-b border-bolt-elements-borderColor pb-4">
|
| 406 |
+
<div className="flex items-center gap-3">
|
| 407 |
+
<motion.div
|
| 408 |
className={classNames(
|
| 409 |
+
'w-10 h-10 flex items-center justify-center rounded-xl',
|
| 410 |
+
'bg-purple-500/10 text-purple-500',
|
|
|
|
| 411 |
)}
|
| 412 |
+
whileHover={{ scale: 1.05 }}
|
| 413 |
>
|
| 414 |
+
<BiChip className="w-6 h-6" />
|
| 415 |
+
</motion.div>
|
| 416 |
<div>
|
| 417 |
+
<h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Local AI Models</h2>
|
| 418 |
+
<p className="text-sm text-bolt-elements-textSecondary">Configure and manage your local AI providers</p>
|
|
|
|
|
|
|
| 419 |
</div>
|
| 420 |
</div>
|
| 421 |
|
| 422 |
<div className="flex items-center gap-2">
|
| 423 |
+
<span className="text-sm text-bolt-elements-textSecondary">Enable All</span>
|
| 424 |
+
<Switch
|
| 425 |
+
checked={categoryEnabled}
|
| 426 |
+
onCheckedChange={handleToggleCategory}
|
| 427 |
+
aria-label="Toggle all local providers"
|
| 428 |
+
/>
|
| 429 |
</div>
|
| 430 |
</div>
|
| 431 |
|
| 432 |
+
{/* Ollama Section */}
|
| 433 |
+
{filteredProviders
|
| 434 |
+
.filter((provider) => provider.name === 'Ollama')
|
| 435 |
+
.map((provider) => (
|
| 436 |
<motion.div
|
| 437 |
key={provider.name}
|
| 438 |
className={classNames(
|
| 439 |
+
'bg-bolt-elements-background-depth-2 rounded-xl',
|
| 440 |
'hover:bg-bolt-elements-background-depth-3',
|
| 441 |
+
'transition-all duration-200 p-5',
|
| 442 |
'relative overflow-hidden group',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
)}
|
| 444 |
initial={{ opacity: 0, y: 20 }}
|
| 445 |
animate={{ opacity: 1, y: 0 }}
|
| 446 |
+
whileHover={{ scale: 1.01 }}
|
|
|
|
| 447 |
>
|
| 448 |
+
{/* Provider Header */}
|
| 449 |
+
<div className="flex items-start justify-between gap-4">
|
| 450 |
+
<div className="flex items-start gap-4">
|
| 451 |
+
<motion.div
|
| 452 |
+
className={classNames(
|
| 453 |
+
'w-12 h-12 flex items-center justify-center rounded-xl',
|
| 454 |
+
'bg-bolt-elements-background-depth-3',
|
| 455 |
+
provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
|
| 456 |
+
)}
|
| 457 |
+
whileHover={{ scale: 1.1, rotate: 5 }}
|
|
|
|
|
|
|
|
|
|
| 458 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
{React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
|
| 460 |
+
className: 'w-7 h-7',
|
| 461 |
+
'aria-label': `${provider.name} icon`,
|
| 462 |
})}
|
| 463 |
+
</motion.div>
|
| 464 |
+
<div>
|
| 465 |
+
<div className="flex items-center gap-2">
|
| 466 |
+
<h3 className="text-md font-semibold text-bolt-elements-textPrimary">{provider.name}</h3>
|
| 467 |
+
<span className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500">Local</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
</div>
|
| 469 |
+
<p className="text-sm text-bolt-elements-textSecondary mt-1">
|
| 470 |
+
{PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
|
| 471 |
+
</p>
|
|
|
|
| 472 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
</div>
|
| 474 |
+
<Switch
|
| 475 |
+
checked={provider.settings.enabled}
|
| 476 |
+
onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
|
| 477 |
+
aria-label={`Toggle ${provider.name} provider`}
|
| 478 |
+
/>
|
| 479 |
</div>
|
| 480 |
|
| 481 |
+
{/* Ollama Models Section */}
|
| 482 |
+
{provider.settings.enabled && (
|
| 483 |
+
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-6 space-y-4">
|
| 484 |
<div className="flex items-center justify-between">
|
| 485 |
<div className="flex items-center gap-2">
|
| 486 |
<div className="i-ph:cube-duotone text-purple-500" />
|
| 487 |
+
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">Installed Models</h4>
|
| 488 |
</div>
|
| 489 |
{isLoadingModels ? (
|
| 490 |
+
<div className="flex items-center gap-2">
|
| 491 |
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
|
| 492 |
+
<span className="text-sm text-bolt-elements-textSecondary">Loading models...</span>
|
| 493 |
</div>
|
| 494 |
) : (
|
| 495 |
<span className="text-sm text-bolt-elements-textSecondary">
|
|
|
|
| 498 |
)}
|
| 499 |
</div>
|
| 500 |
|
| 501 |
+
<div className="space-y-3">
|
| 502 |
+
{isLoadingModels ? (
|
| 503 |
+
<div className="space-y-3">
|
| 504 |
+
{Array.from({ length: 3 }).map((_, i) => (
|
| 505 |
+
<div
|
| 506 |
+
key={i}
|
| 507 |
+
className="h-20 w-full bg-bolt-elements-background-depth-3 rounded-lg animate-pulse"
|
| 508 |
+
/>
|
| 509 |
+
))}
|
| 510 |
+
</div>
|
| 511 |
+
) : ollamaModels.length === 0 ? (
|
| 512 |
+
<div className="text-center py-8 text-bolt-elements-textSecondary">
|
| 513 |
+
<div className="i-ph:cube-transparent text-4xl mx-auto mb-2" />
|
| 514 |
+
<p>No models installed yet</p>
|
| 515 |
+
<p className="text-sm">Install your first model below</p>
|
| 516 |
+
</div>
|
| 517 |
+
) : (
|
| 518 |
+
ollamaModels.map((model) => (
|
| 519 |
+
<motion.div
|
| 520 |
+
key={model.name}
|
| 521 |
+
className={classNames(
|
| 522 |
+
'p-4 rounded-xl',
|
| 523 |
+
'bg-bolt-elements-background-depth-3',
|
| 524 |
+
'hover:bg-bolt-elements-background-depth-4',
|
| 525 |
+
'transition-all duration-200',
|
| 526 |
+
)}
|
| 527 |
+
whileHover={{ scale: 1.01 }}
|
| 528 |
+
>
|
| 529 |
+
<div className="flex items-center justify-between">
|
| 530 |
+
<div className="space-y-2">
|
| 531 |
+
<div className="flex items-center gap-2">
|
| 532 |
+
<h5 className="text-sm font-medium text-bolt-elements-textPrimary">{model.name}</h5>
|
| 533 |
+
<ModelStatusBadge status={model.status} />
|
| 534 |
+
</div>
|
| 535 |
+
<ModelDetails model={model} />
|
| 536 |
+
</div>
|
| 537 |
+
<ModelActions
|
| 538 |
+
model={model}
|
| 539 |
+
onUpdate={() => handleUpdateOllamaModel(model.name)}
|
| 540 |
+
onDelete={() => {
|
| 541 |
+
if (window.confirm(`Are you sure you want to delete ${model.name}?`)) {
|
| 542 |
+
handleDeleteOllamaModel(model.name);
|
| 543 |
+
}
|
| 544 |
+
}}
|
| 545 |
+
/>
|
| 546 |
</div>
|
| 547 |
+
{model.progress && (
|
| 548 |
+
<div className="mt-3">
|
| 549 |
+
<Progress
|
| 550 |
+
value={Math.round((model.progress.current / model.progress.total) * 100)}
|
| 551 |
+
className="h-1"
|
| 552 |
+
/>
|
| 553 |
+
<div className="flex justify-between mt-1 text-xs text-bolt-elements-textSecondary">
|
| 554 |
+
<span>{model.progress.status}</span>
|
| 555 |
+
<span>{Math.round((model.progress.current / model.progress.total) * 100)}%</span>
|
| 556 |
+
</div>
|
| 557 |
+
</div>
|
| 558 |
+
)}
|
| 559 |
+
</motion.div>
|
| 560 |
+
))
|
| 561 |
+
)}
|
| 562 |
+
</div>
|
| 563 |
+
|
| 564 |
+
{/* Model Installation Section */}
|
| 565 |
+
<OllamaModelInstaller onModelInstalled={fetchOllamaModels} />
|
| 566 |
+
</motion.div>
|
| 567 |
+
)}
|
| 568 |
+
</motion.div>
|
| 569 |
+
))}
|
| 570 |
+
|
| 571 |
+
{/* Other Providers Section */}
|
| 572 |
+
<div className="border-t border-bolt-elements-borderColor pt-6 mt-8">
|
| 573 |
+
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary mb-4">Other Local Providers</h3>
|
| 574 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 575 |
+
{filteredProviders
|
| 576 |
+
.filter((provider) => provider.name !== 'Ollama')
|
| 577 |
+
.map((provider, index) => (
|
| 578 |
+
<motion.div
|
| 579 |
+
key={provider.name}
|
| 580 |
+
className={classNames(
|
| 581 |
+
'bg-bolt-elements-background-depth-2 rounded-xl',
|
| 582 |
+
'hover:bg-bolt-elements-background-depth-3',
|
| 583 |
+
'transition-all duration-200 p-5',
|
| 584 |
+
'relative overflow-hidden group',
|
| 585 |
+
)}
|
| 586 |
+
initial={{ opacity: 0, y: 20 }}
|
| 587 |
+
animate={{ opacity: 1, y: 0 }}
|
| 588 |
+
transition={{ delay: index * 0.1 }}
|
| 589 |
+
whileHover={{ scale: 1.01 }}
|
| 590 |
+
>
|
| 591 |
+
{/* Provider Header */}
|
| 592 |
+
<div className="flex items-start justify-between gap-4">
|
| 593 |
+
<div className="flex items-start gap-4">
|
| 594 |
+
<motion.div
|
| 595 |
+
className={classNames(
|
| 596 |
+
'w-12 h-12 flex items-center justify-center rounded-xl',
|
| 597 |
+
'bg-bolt-elements-background-depth-3',
|
| 598 |
+
provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
|
| 599 |
+
)}
|
| 600 |
+
whileHover={{ scale: 1.1, rotate: 5 }}
|
| 601 |
+
>
|
| 602 |
+
{React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
|
| 603 |
+
className: 'w-7 h-7',
|
| 604 |
+
'aria-label': `${provider.name} icon`,
|
| 605 |
+
})}
|
| 606 |
+
</motion.div>
|
| 607 |
+
<div>
|
| 608 |
+
<div className="flex items-center gap-2">
|
| 609 |
+
<h3 className="text-md font-semibold text-bolt-elements-textPrimary">{provider.name}</h3>
|
| 610 |
+
<div className="flex gap-1">
|
| 611 |
+
<span className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500">
|
| 612 |
+
Local
|
| 613 |
+
</span>
|
| 614 |
+
{URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
|
| 615 |
+
<span className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500">
|
| 616 |
+
Configurable
|
| 617 |
</span>
|
| 618 |
)}
|
| 619 |
</div>
|
| 620 |
</div>
|
| 621 |
+
<p className="text-sm text-bolt-elements-textSecondary mt-1">
|
| 622 |
+
{PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
|
| 623 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
</div>
|
| 625 |
+
</div>
|
| 626 |
+
<Switch
|
| 627 |
+
checked={provider.settings.enabled}
|
| 628 |
+
onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
|
| 629 |
+
aria-label={`Toggle ${provider.name} provider`}
|
| 630 |
+
/>
|
| 631 |
</div>
|
|
|
|
|
|
|
| 632 |
|
| 633 |
+
{/* URL Configuration Section */}
|
| 634 |
+
<AnimatePresence>
|
| 635 |
+
{provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
|
| 636 |
+
<motion.div
|
| 637 |
+
initial={{ opacity: 0, height: 0 }}
|
| 638 |
+
animate={{ opacity: 1, height: 'auto' }}
|
| 639 |
+
exit={{ opacity: 0, height: 0 }}
|
| 640 |
+
className="mt-4"
|
| 641 |
+
>
|
| 642 |
+
<div className="flex flex-col gap-2">
|
| 643 |
+
<label className="text-sm text-bolt-elements-textSecondary">API Endpoint</label>
|
| 644 |
+
{editingProvider === provider.name ? (
|
| 645 |
+
<input
|
| 646 |
+
type="text"
|
| 647 |
+
defaultValue={provider.settings.baseUrl}
|
| 648 |
+
placeholder={`Enter ${provider.name} base URL`}
|
| 649 |
+
className={classNames(
|
| 650 |
+
'w-full px-3 py-2 rounded-lg text-sm',
|
| 651 |
+
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
| 652 |
+
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
| 653 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
| 654 |
+
'transition-all duration-200',
|
| 655 |
+
)}
|
| 656 |
+
onKeyDown={(e) => {
|
| 657 |
+
if (e.key === 'Enter') {
|
| 658 |
+
handleUpdateBaseUrl(provider, e.currentTarget.value);
|
| 659 |
+
} else if (e.key === 'Escape') {
|
| 660 |
+
setEditingProvider(null);
|
| 661 |
+
}
|
| 662 |
+
}}
|
| 663 |
+
onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
|
| 664 |
+
autoFocus
|
| 665 |
+
/>
|
| 666 |
+
) : (
|
| 667 |
+
<div
|
| 668 |
+
onClick={() => setEditingProvider(provider.name)}
|
| 669 |
+
className={classNames(
|
| 670 |
+
'w-full px-3 py-2 rounded-lg text-sm cursor-pointer',
|
| 671 |
+
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
| 672 |
+
'hover:border-purple-500/30 hover:bg-bolt-elements-background-depth-4',
|
| 673 |
+
'transition-all duration-200',
|
| 674 |
+
)}
|
| 675 |
+
>
|
| 676 |
+
<div className="flex items-center gap-2 text-bolt-elements-textSecondary">
|
| 677 |
+
<div className="i-ph:link text-sm" />
|
| 678 |
+
<span>{provider.settings.baseUrl || 'Click to set base URL'}</span>
|
| 679 |
+
</div>
|
| 680 |
+
</div>
|
| 681 |
+
)}
|
| 682 |
+
</div>
|
| 683 |
+
</motion.div>
|
| 684 |
+
)}
|
| 685 |
+
</AnimatePresence>
|
| 686 |
+
</motion.div>
|
| 687 |
+
))}
|
| 688 |
+
</div>
|
| 689 |
</div>
|
| 690 |
</motion.div>
|
| 691 |
+
</div>
|
| 692 |
+
);
|
| 693 |
+
}
|
| 694 |
|
| 695 |
+
// Helper component for model status badge
|
| 696 |
+
function ModelStatusBadge({ status }: { status?: string }) {
|
| 697 |
+
if (!status || status === 'idle') {
|
| 698 |
+
return null;
|
| 699 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
|
| 701 |
+
const statusConfig = {
|
| 702 |
+
updating: { bg: 'bg-yellow-500/10', text: 'text-yellow-500', label: 'Updating' },
|
| 703 |
+
updated: { bg: 'bg-green-500/10', text: 'text-green-500', label: 'Updated' },
|
| 704 |
+
error: { bg: 'bg-red-500/10', text: 'text-red-500', label: 'Error' },
|
| 705 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
|
| 707 |
+
const config = statusConfig[status as keyof typeof statusConfig];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
|
| 709 |
+
if (!config) {
|
| 710 |
+
return null;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
return (
|
| 714 |
+
<span className={classNames('px-2 py-0.5 rounded-full text-xs font-medium', config.bg, config.text)}>
|
| 715 |
+
{config.label}
|
| 716 |
+
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
);
|
| 718 |
}
|
|
|
|
|
|
|
@@ -0,0 +1,597 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { motion } from 'framer-motion';
|
| 3 |
+
import { classNames } from '~/utils/classNames';
|
| 4 |
+
import { Progress } from '~/components/ui/Progress';
|
| 5 |
+
import { useToast } from '~/components/ui/use-toast';
|
| 6 |
+
|
| 7 |
+
interface OllamaModelInstallerProps {
|
| 8 |
+
onModelInstalled: () => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
interface InstallProgress {
|
| 12 |
+
status: string;
|
| 13 |
+
progress: number;
|
| 14 |
+
downloadedSize?: string;
|
| 15 |
+
totalSize?: string;
|
| 16 |
+
speed?: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
interface ModelInfo {
|
| 20 |
+
name: string;
|
| 21 |
+
desc: string;
|
| 22 |
+
size: string;
|
| 23 |
+
tags: string[];
|
| 24 |
+
installedVersion?: string;
|
| 25 |
+
latestVersion?: string;
|
| 26 |
+
needsUpdate?: boolean;
|
| 27 |
+
status?: 'idle' | 'installing' | 'updating' | 'updated' | 'error';
|
| 28 |
+
details?: {
|
| 29 |
+
family: string;
|
| 30 |
+
parameter_size: string;
|
| 31 |
+
quantization_level: string;
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const POPULAR_MODELS: ModelInfo[] = [
|
| 36 |
+
{
|
| 37 |
+
name: 'deepseek-coder:6.7b',
|
| 38 |
+
desc: "DeepSeek's code generation model",
|
| 39 |
+
size: '4.1GB',
|
| 40 |
+
tags: ['coding', 'popular'],
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
name: 'llama2:7b',
|
| 44 |
+
desc: "Meta's Llama 2 (7B parameters)",
|
| 45 |
+
size: '3.8GB',
|
| 46 |
+
tags: ['general', 'popular'],
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
name: 'mistral:7b',
|
| 50 |
+
desc: "Mistral's 7B model",
|
| 51 |
+
size: '4.1GB',
|
| 52 |
+
tags: ['general', 'popular'],
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
name: 'gemma:7b',
|
| 56 |
+
desc: "Google's Gemma model",
|
| 57 |
+
size: '4.0GB',
|
| 58 |
+
tags: ['general', 'new'],
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
name: 'codellama:7b',
|
| 62 |
+
desc: "Meta's Code Llama model",
|
| 63 |
+
size: '4.1GB',
|
| 64 |
+
tags: ['coding', 'popular'],
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
name: 'neural-chat:7b',
|
| 68 |
+
desc: "Intel's Neural Chat model",
|
| 69 |
+
size: '4.1GB',
|
| 70 |
+
tags: ['chat', 'popular'],
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
name: 'phi:latest',
|
| 74 |
+
desc: "Microsoft's Phi-2 model",
|
| 75 |
+
size: '2.7GB',
|
| 76 |
+
tags: ['small', 'fast'],
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
name: 'qwen:7b',
|
| 80 |
+
desc: "Alibaba's Qwen model",
|
| 81 |
+
size: '4.1GB',
|
| 82 |
+
tags: ['general'],
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
name: 'solar:10.7b',
|
| 86 |
+
desc: "Upstage's Solar model",
|
| 87 |
+
size: '6.1GB',
|
| 88 |
+
tags: ['large', 'powerful'],
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
name: 'openchat:7b',
|
| 92 |
+
desc: 'Open-source chat model',
|
| 93 |
+
size: '4.1GB',
|
| 94 |
+
tags: ['chat', 'popular'],
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
name: 'dolphin-phi:2.7b',
|
| 98 |
+
desc: 'Lightweight chat model',
|
| 99 |
+
size: '1.6GB',
|
| 100 |
+
tags: ['small', 'fast'],
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
name: 'stable-code:3b',
|
| 104 |
+
desc: 'Lightweight coding model',
|
| 105 |
+
size: '1.8GB',
|
| 106 |
+
tags: ['coding', 'small'],
|
| 107 |
+
},
|
| 108 |
+
];
|
| 109 |
+
|
| 110 |
+
function formatBytes(bytes: number): string {
|
| 111 |
+
if (bytes === 0) {
|
| 112 |
+
return '0 B';
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
const k = 1024;
|
| 116 |
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
| 117 |
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
| 118 |
+
|
| 119 |
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
function formatSpeed(bytesPerSecond: number): string {
|
| 123 |
+
return `${formatBytes(bytesPerSecond)}/s`;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// Add Ollama Icon SVG component
|
| 127 |
+
function OllamaIcon({ className }: { className?: string }) {
|
| 128 |
+
return (
|
| 129 |
+
<svg viewBox="0 0 1024 1024" className={className} fill="currentColor">
|
| 130 |
+
<path d="M684.3 322.2H339.8c-9.5.1-17.7 6.8-19.6 16.1-8.2 41.4-12.4 83.5-12.4 125.7 0 42.2 4.2 84.3 12.4 125.7 1.9 9.3 10.1 16 19.6 16.1h344.5c9.5-.1 17.7-6.8 19.6-16.1 8.2-41.4 12.4-83.5 12.4-125.7 0-42.2-4.2-84.3-12.4-125.7-1.9-9.3-10.1-16-19.6-16.1zM512 640c-176.7 0-320-143.3-320-320S335.3 0 512 0s320 143.3 320 320-143.3 320-320 320z" />
|
| 131 |
+
</svg>
|
| 132 |
+
);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelInstallerProps) {
|
| 136 |
+
const [modelString, setModelString] = useState('');
|
| 137 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 138 |
+
const [isInstalling, setIsInstalling] = useState(false);
|
| 139 |
+
const [isChecking, setIsChecking] = useState(false);
|
| 140 |
+
const [installProgress, setInstallProgress] = useState<InstallProgress | null>(null);
|
| 141 |
+
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
| 142 |
+
const [models, setModels] = useState<ModelInfo[]>(POPULAR_MODELS);
|
| 143 |
+
const { toast } = useToast();
|
| 144 |
+
|
| 145 |
+
// Function to check installed models and their versions
|
| 146 |
+
const checkInstalledModels = async () => {
|
| 147 |
+
try {
|
| 148 |
+
const response = await fetch('http://127.0.0.1:11434/api/tags', {
|
| 149 |
+
method: 'GET',
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
if (!response.ok) {
|
| 153 |
+
throw new Error('Failed to fetch installed models');
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
const data = (await response.json()) as { models: Array<{ name: string; digest: string; latest: string }> };
|
| 157 |
+
const installedModels = data.models || [];
|
| 158 |
+
|
| 159 |
+
// Update models with installed versions
|
| 160 |
+
setModels((prevModels) =>
|
| 161 |
+
prevModels.map((model) => {
|
| 162 |
+
const installed = installedModels.find((m) => m.name.toLowerCase() === model.name.toLowerCase());
|
| 163 |
+
|
| 164 |
+
if (installed) {
|
| 165 |
+
return {
|
| 166 |
+
...model,
|
| 167 |
+
installedVersion: installed.digest.substring(0, 8),
|
| 168 |
+
needsUpdate: installed.digest !== installed.latest,
|
| 169 |
+
latestVersion: installed.latest?.substring(0, 8),
|
| 170 |
+
};
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
return model;
|
| 174 |
+
}),
|
| 175 |
+
);
|
| 176 |
+
} catch (error) {
|
| 177 |
+
console.error('Error checking installed models:', error);
|
| 178 |
+
}
|
| 179 |
+
};
|
| 180 |
+
|
| 181 |
+
// Check installed models on mount and after installation
|
| 182 |
+
useEffect(() => {
|
| 183 |
+
checkInstalledModels();
|
| 184 |
+
}, []);
|
| 185 |
+
|
| 186 |
+
const handleCheckUpdates = async () => {
|
| 187 |
+
setIsChecking(true);
|
| 188 |
+
|
| 189 |
+
try {
|
| 190 |
+
await checkInstalledModels();
|
| 191 |
+
toast('Model versions checked');
|
| 192 |
+
} catch (err) {
|
| 193 |
+
console.error('Failed to check model versions:', err);
|
| 194 |
+
toast('Failed to check model versions');
|
| 195 |
+
} finally {
|
| 196 |
+
setIsChecking(false);
|
| 197 |
+
}
|
| 198 |
+
};
|
| 199 |
+
|
| 200 |
+
const filteredModels = models.filter((model) => {
|
| 201 |
+
const matchesSearch =
|
| 202 |
+
searchQuery === '' ||
|
| 203 |
+
model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
| 204 |
+
model.desc.toLowerCase().includes(searchQuery.toLowerCase());
|
| 205 |
+
const matchesTags = selectedTags.length === 0 || selectedTags.some((tag) => model.tags.includes(tag));
|
| 206 |
+
|
| 207 |
+
return matchesSearch && matchesTags;
|
| 208 |
+
});
|
| 209 |
+
|
| 210 |
+
const handleInstallModel = async (modelToInstall: string) => {
|
| 211 |
+
if (!modelToInstall) {
|
| 212 |
+
return;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
try {
|
| 216 |
+
setIsInstalling(true);
|
| 217 |
+
setInstallProgress({
|
| 218 |
+
status: 'Starting download...',
|
| 219 |
+
progress: 0,
|
| 220 |
+
downloadedSize: '0 B',
|
| 221 |
+
totalSize: 'Calculating...',
|
| 222 |
+
speed: '0 B/s',
|
| 223 |
+
});
|
| 224 |
+
setModelString('');
|
| 225 |
+
setSearchQuery('');
|
| 226 |
+
|
| 227 |
+
const response = await fetch('http://127.0.0.1:11434/api/pull', {
|
| 228 |
+
method: 'POST',
|
| 229 |
+
headers: {
|
| 230 |
+
'Content-Type': 'application/json',
|
| 231 |
+
},
|
| 232 |
+
body: JSON.stringify({ name: modelToInstall }),
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
if (!response.ok) {
|
| 236 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
const reader = response.body?.getReader();
|
| 240 |
+
|
| 241 |
+
if (!reader) {
|
| 242 |
+
throw new Error('Failed to get response reader');
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
let lastTime = Date.now();
|
| 246 |
+
let lastBytes = 0;
|
| 247 |
+
|
| 248 |
+
while (true) {
|
| 249 |
+
const { done, value } = await reader.read();
|
| 250 |
+
|
| 251 |
+
if (done) {
|
| 252 |
+
break;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
const text = new TextDecoder().decode(value);
|
| 256 |
+
const lines = text.split('\n').filter(Boolean);
|
| 257 |
+
|
| 258 |
+
for (const line of lines) {
|
| 259 |
+
try {
|
| 260 |
+
const data = JSON.parse(line);
|
| 261 |
+
|
| 262 |
+
if ('status' in data) {
|
| 263 |
+
const currentTime = Date.now();
|
| 264 |
+
const timeDiff = (currentTime - lastTime) / 1000; // Convert to seconds
|
| 265 |
+
const bytesDiff = (data.completed || 0) - lastBytes;
|
| 266 |
+
const speed = bytesDiff / timeDiff;
|
| 267 |
+
|
| 268 |
+
setInstallProgress({
|
| 269 |
+
status: data.status,
|
| 270 |
+
progress: data.completed && data.total ? (data.completed / data.total) * 100 : 0,
|
| 271 |
+
downloadedSize: formatBytes(data.completed || 0),
|
| 272 |
+
totalSize: data.total ? formatBytes(data.total) : 'Calculating...',
|
| 273 |
+
speed: formatSpeed(speed),
|
| 274 |
+
});
|
| 275 |
+
|
| 276 |
+
lastTime = currentTime;
|
| 277 |
+
lastBytes = data.completed || 0;
|
| 278 |
+
}
|
| 279 |
+
} catch (err) {
|
| 280 |
+
console.error('Error parsing progress:', err);
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
toast('Successfully installed ' + modelToInstall + '. The model list will refresh automatically.');
|
| 286 |
+
|
| 287 |
+
// Ensure we call onModelInstalled after successful installation
|
| 288 |
+
setTimeout(() => {
|
| 289 |
+
onModelInstalled();
|
| 290 |
+
}, 1000);
|
| 291 |
+
} catch (err) {
|
| 292 |
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
| 293 |
+
console.error(`Error installing ${modelToInstall}:`, errorMessage);
|
| 294 |
+
toast(`Failed to install ${modelToInstall}. ${errorMessage}`);
|
| 295 |
+
} finally {
|
| 296 |
+
setIsInstalling(false);
|
| 297 |
+
setInstallProgress(null);
|
| 298 |
+
}
|
| 299 |
+
};
|
| 300 |
+
|
| 301 |
+
const handleUpdateModel = async (modelToUpdate: string) => {
|
| 302 |
+
try {
|
| 303 |
+
setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'updating' } : m)));
|
| 304 |
+
|
| 305 |
+
const response = await fetch('http://127.0.0.1:11434/api/pull', {
|
| 306 |
+
method: 'POST',
|
| 307 |
+
headers: {
|
| 308 |
+
'Content-Type': 'application/json',
|
| 309 |
+
},
|
| 310 |
+
body: JSON.stringify({ name: modelToUpdate }),
|
| 311 |
+
});
|
| 312 |
+
|
| 313 |
+
if (!response.ok) {
|
| 314 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
const reader = response.body?.getReader();
|
| 318 |
+
|
| 319 |
+
if (!reader) {
|
| 320 |
+
throw new Error('Failed to get response reader');
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
let lastTime = Date.now();
|
| 324 |
+
let lastBytes = 0;
|
| 325 |
+
|
| 326 |
+
while (true) {
|
| 327 |
+
const { done, value } = await reader.read();
|
| 328 |
+
|
| 329 |
+
if (done) {
|
| 330 |
+
break;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
const text = new TextDecoder().decode(value);
|
| 334 |
+
const lines = text.split('\n').filter(Boolean);
|
| 335 |
+
|
| 336 |
+
for (const line of lines) {
|
| 337 |
+
try {
|
| 338 |
+
const data = JSON.parse(line);
|
| 339 |
+
|
| 340 |
+
if ('status' in data) {
|
| 341 |
+
const currentTime = Date.now();
|
| 342 |
+
const timeDiff = (currentTime - lastTime) / 1000;
|
| 343 |
+
const bytesDiff = (data.completed || 0) - lastBytes;
|
| 344 |
+
const speed = bytesDiff / timeDiff;
|
| 345 |
+
|
| 346 |
+
setInstallProgress({
|
| 347 |
+
status: data.status,
|
| 348 |
+
progress: data.completed && data.total ? (data.completed / data.total) * 100 : 0,
|
| 349 |
+
downloadedSize: formatBytes(data.completed || 0),
|
| 350 |
+
totalSize: data.total ? formatBytes(data.total) : 'Calculating...',
|
| 351 |
+
speed: formatSpeed(speed),
|
| 352 |
+
});
|
| 353 |
+
|
| 354 |
+
lastTime = currentTime;
|
| 355 |
+
lastBytes = data.completed || 0;
|
| 356 |
+
}
|
| 357 |
+
} catch (err) {
|
| 358 |
+
console.error('Error parsing progress:', err);
|
| 359 |
+
}
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
toast('Successfully updated ' + modelToUpdate);
|
| 364 |
+
|
| 365 |
+
// Refresh model list after update
|
| 366 |
+
await checkInstalledModels();
|
| 367 |
+
} catch (err) {
|
| 368 |
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
| 369 |
+
console.error(`Error updating ${modelToUpdate}:`, errorMessage);
|
| 370 |
+
toast(`Failed to update ${modelToUpdate}. ${errorMessage}`);
|
| 371 |
+
setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'error' } : m)));
|
| 372 |
+
} finally {
|
| 373 |
+
setInstallProgress(null);
|
| 374 |
+
}
|
| 375 |
+
};
|
| 376 |
+
|
| 377 |
+
const allTags = Array.from(new Set(POPULAR_MODELS.flatMap((model) => model.tags)));
|
| 378 |
+
|
| 379 |
+
return (
|
| 380 |
+
<div className="space-y-6">
|
| 381 |
+
<div className="flex items-center justify-between pt-6">
|
| 382 |
+
<div className="flex items-center gap-3">
|
| 383 |
+
<OllamaIcon className="w-8 h-8 text-purple-500" />
|
| 384 |
+
<div>
|
| 385 |
+
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary">Ollama Models</h3>
|
| 386 |
+
<p className="text-sm text-bolt-elements-textSecondary mt-1">Install and manage your Ollama models</p>
|
| 387 |
+
</div>
|
| 388 |
+
</div>
|
| 389 |
+
<motion.button
|
| 390 |
+
onClick={handleCheckUpdates}
|
| 391 |
+
disabled={isChecking}
|
| 392 |
+
className={classNames(
|
| 393 |
+
'px-4 py-2 rounded-lg',
|
| 394 |
+
'bg-purple-500/10 text-purple-500',
|
| 395 |
+
'hover:bg-purple-500/20',
|
| 396 |
+
'transition-all duration-200',
|
| 397 |
+
'flex items-center gap-2',
|
| 398 |
+
)}
|
| 399 |
+
whileHover={{ scale: 1.02 }}
|
| 400 |
+
whileTap={{ scale: 0.98 }}
|
| 401 |
+
>
|
| 402 |
+
{isChecking ? (
|
| 403 |
+
<div className="i-ph:spinner-gap-bold animate-spin" />
|
| 404 |
+
) : (
|
| 405 |
+
<div className="i-ph:arrows-clockwise" />
|
| 406 |
+
)}
|
| 407 |
+
Check Updates
|
| 408 |
+
</motion.button>
|
| 409 |
+
</div>
|
| 410 |
+
|
| 411 |
+
<div className="flex gap-4">
|
| 412 |
+
<div className="flex-1">
|
| 413 |
+
<div className="space-y-1">
|
| 414 |
+
<input
|
| 415 |
+
type="text"
|
| 416 |
+
className={classNames(
|
| 417 |
+
'w-full px-4 py-3 rounded-xl',
|
| 418 |
+
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
| 419 |
+
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
| 420 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
| 421 |
+
'transition-all duration-200',
|
| 422 |
+
)}
|
| 423 |
+
placeholder="Search models or enter custom model name..."
|
| 424 |
+
value={searchQuery || modelString}
|
| 425 |
+
onChange={(e) => {
|
| 426 |
+
const value = e.target.value;
|
| 427 |
+
setSearchQuery(value);
|
| 428 |
+
setModelString(value);
|
| 429 |
+
}}
|
| 430 |
+
disabled={isInstalling}
|
| 431 |
+
/>
|
| 432 |
+
<p className="text-xs text-bolt-elements-textTertiary px-1">
|
| 433 |
+
Browse models at{' '}
|
| 434 |
+
<a
|
| 435 |
+
href="https://ollama.com/library"
|
| 436 |
+
target="_blank"
|
| 437 |
+
rel="noopener noreferrer"
|
| 438 |
+
className="text-purple-500 hover:underline inline-flex items-center gap-0.5"
|
| 439 |
+
>
|
| 440 |
+
ollama.com/library
|
| 441 |
+
<div className="i-ph:arrow-square-out text-[10px]" />
|
| 442 |
+
</a>{' '}
|
| 443 |
+
and copy model names to install
|
| 444 |
+
</p>
|
| 445 |
+
</div>
|
| 446 |
+
</div>
|
| 447 |
+
<motion.button
|
| 448 |
+
onClick={() => handleInstallModel(modelString)}
|
| 449 |
+
disabled={!modelString || isInstalling}
|
| 450 |
+
className={classNames(
|
| 451 |
+
'rounded-xl px-6 py-3',
|
| 452 |
+
'bg-purple-500 text-white',
|
| 453 |
+
'hover:bg-purple-600',
|
| 454 |
+
'transition-all duration-200',
|
| 455 |
+
{ 'opacity-50 cursor-not-allowed': !modelString || isInstalling },
|
| 456 |
+
)}
|
| 457 |
+
whileHover={{ scale: 1.02 }}
|
| 458 |
+
whileTap={{ scale: 0.98 }}
|
| 459 |
+
>
|
| 460 |
+
{isInstalling ? (
|
| 461 |
+
<div className="flex items-center gap-2">
|
| 462 |
+
<div className="i-ph:spinner-gap-bold animate-spin" />
|
| 463 |
+
<span>Installing...</span>
|
| 464 |
+
</div>
|
| 465 |
+
) : (
|
| 466 |
+
<div className="flex items-center gap-2">
|
| 467 |
+
<OllamaIcon className="w-4 h-4" />
|
| 468 |
+
<span>Install Model</span>
|
| 469 |
+
</div>
|
| 470 |
+
)}
|
| 471 |
+
</motion.button>
|
| 472 |
+
</div>
|
| 473 |
+
|
| 474 |
+
<div className="flex flex-wrap gap-2">
|
| 475 |
+
{allTags.map((tag) => (
|
| 476 |
+
<button
|
| 477 |
+
key={tag}
|
| 478 |
+
onClick={() => {
|
| 479 |
+
setSelectedTags((prev) => (prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]));
|
| 480 |
+
}}
|
| 481 |
+
className={classNames(
|
| 482 |
+
'px-3 py-1 rounded-full text-xs font-medium transition-all duration-200',
|
| 483 |
+
selectedTags.includes(tag)
|
| 484 |
+
? 'bg-purple-500 text-white'
|
| 485 |
+
: 'bg-bolt-elements-background-depth-3 text-bolt-elements-textSecondary hover:bg-bolt-elements-background-depth-4',
|
| 486 |
+
)}
|
| 487 |
+
>
|
| 488 |
+
{tag}
|
| 489 |
+
</button>
|
| 490 |
+
))}
|
| 491 |
+
</div>
|
| 492 |
+
|
| 493 |
+
<div className="grid grid-cols-1 gap-2">
|
| 494 |
+
{filteredModels.map((model) => (
|
| 495 |
+
<motion.div
|
| 496 |
+
key={model.name}
|
| 497 |
+
className={classNames(
|
| 498 |
+
'flex items-start gap-2 p-3 rounded-lg',
|
| 499 |
+
'bg-bolt-elements-background-depth-3',
|
| 500 |
+
'hover:bg-bolt-elements-background-depth-4',
|
| 501 |
+
'transition-all duration-200',
|
| 502 |
+
'relative group',
|
| 503 |
+
)}
|
| 504 |
+
>
|
| 505 |
+
<OllamaIcon className="w-5 h-5 text-purple-500 mt-0.5 flex-shrink-0" />
|
| 506 |
+
<div className="flex-1 space-y-1.5">
|
| 507 |
+
<div className="flex items-start justify-between">
|
| 508 |
+
<div>
|
| 509 |
+
<p className="text-bolt-elements-textPrimary font-mono text-sm">{model.name}</p>
|
| 510 |
+
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">{model.desc}</p>
|
| 511 |
+
</div>
|
| 512 |
+
<div className="text-right">
|
| 513 |
+
<span className="text-xs text-bolt-elements-textTertiary">{model.size}</span>
|
| 514 |
+
{model.installedVersion && (
|
| 515 |
+
<div className="mt-0.5 flex flex-col items-end gap-0.5">
|
| 516 |
+
<span className="text-xs text-bolt-elements-textTertiary">v{model.installedVersion}</span>
|
| 517 |
+
{model.needsUpdate && model.latestVersion && (
|
| 518 |
+
<span className="text-xs text-purple-500">v{model.latestVersion} available</span>
|
| 519 |
+
)}
|
| 520 |
+
</div>
|
| 521 |
+
)}
|
| 522 |
+
</div>
|
| 523 |
+
</div>
|
| 524 |
+
<div className="flex items-center justify-between">
|
| 525 |
+
<div className="flex flex-wrap gap-1">
|
| 526 |
+
{model.tags.map((tag) => (
|
| 527 |
+
<span
|
| 528 |
+
key={tag}
|
| 529 |
+
className="px-1.5 py-0.5 rounded-full text-[10px] bg-bolt-elements-background-depth-4 text-bolt-elements-textTertiary"
|
| 530 |
+
>
|
| 531 |
+
{tag}
|
| 532 |
+
</span>
|
| 533 |
+
))}
|
| 534 |
+
</div>
|
| 535 |
+
<div className="flex gap-2">
|
| 536 |
+
{model.installedVersion ? (
|
| 537 |
+
model.needsUpdate ? (
|
| 538 |
+
<motion.button
|
| 539 |
+
onClick={() => handleUpdateModel(model.name)}
|
| 540 |
+
className={classNames(
|
| 541 |
+
'px-2 py-0.5 rounded-lg text-xs',
|
| 542 |
+
'bg-purple-500 text-white',
|
| 543 |
+
'hover:bg-purple-600',
|
| 544 |
+
'transition-all duration-200',
|
| 545 |
+
'flex items-center gap-1',
|
| 546 |
+
)}
|
| 547 |
+
whileHover={{ scale: 1.02 }}
|
| 548 |
+
whileTap={{ scale: 0.98 }}
|
| 549 |
+
>
|
| 550 |
+
<div className="i-ph:arrows-clockwise text-xs" />
|
| 551 |
+
Update
|
| 552 |
+
</motion.button>
|
| 553 |
+
) : (
|
| 554 |
+
<span className="px-2 py-0.5 rounded-lg text-xs text-green-500 bg-green-500/10">Up to date</span>
|
| 555 |
+
)
|
| 556 |
+
) : (
|
| 557 |
+
<motion.button
|
| 558 |
+
onClick={() => handleInstallModel(model.name)}
|
| 559 |
+
className={classNames(
|
| 560 |
+
'px-2 py-0.5 rounded-lg text-xs',
|
| 561 |
+
'bg-purple-500 text-white',
|
| 562 |
+
'hover:bg-purple-600',
|
| 563 |
+
'transition-all duration-200',
|
| 564 |
+
'flex items-center gap-1',
|
| 565 |
+
)}
|
| 566 |
+
whileHover={{ scale: 1.02 }}
|
| 567 |
+
whileTap={{ scale: 0.98 }}
|
| 568 |
+
>
|
| 569 |
+
<div className="i-ph:download text-xs" />
|
| 570 |
+
Install
|
| 571 |
+
</motion.button>
|
| 572 |
+
)}
|
| 573 |
+
</div>
|
| 574 |
+
</div>
|
| 575 |
+
</div>
|
| 576 |
+
</motion.div>
|
| 577 |
+
))}
|
| 578 |
+
</div>
|
| 579 |
+
|
| 580 |
+
{installProgress && (
|
| 581 |
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-2">
|
| 582 |
+
<div className="flex justify-between text-sm">
|
| 583 |
+
<span className="text-bolt-elements-textSecondary">{installProgress.status}</span>
|
| 584 |
+
<div className="flex items-center gap-4">
|
| 585 |
+
<span className="text-bolt-elements-textTertiary">
|
| 586 |
+
{installProgress.downloadedSize} / {installProgress.totalSize}
|
| 587 |
+
</span>
|
| 588 |
+
<span className="text-bolt-elements-textTertiary">{installProgress.speed}</span>
|
| 589 |
+
<span className="text-bolt-elements-textSecondary">{Math.round(installProgress.progress)}%</span>
|
| 590 |
+
</div>
|
| 591 |
+
</div>
|
| 592 |
+
<Progress value={installProgress.progress} className="h-1" />
|
| 593 |
+
</motion.div>
|
| 594 |
+
)}
|
| 595 |
+
</div>
|
| 596 |
+
);
|
| 597 |
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class AmazonBedrockStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class AmazonBedrockStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class AnthropicStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class AnthropicStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class CohereStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class CohereStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class DeepseekStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class DeepseekStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class GoogleStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class GoogleStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class GroqStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class GroqStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class HuggingFaceStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class HuggingFaceStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class HyperbolicStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class HyperbolicStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class MistralStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class MistralStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class OpenAIStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class OpenAIStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class OpenRouterStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class OpenRouterStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class PerplexityStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class PerplexityStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class TogetherStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class TogetherStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import { BaseProviderChecker } from '~/components
|
| 2 |
-
import type { StatusCheckResult } from '~/components
|
| 3 |
|
| 4 |
export class XAIStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
|
|
| 1 |
+
import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
|
| 2 |
+
import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
|
| 3 |
|
| 4 |
export class XAIStatusChecker extends BaseProviderChecker {
|
| 5 |
async checkStatus(): Promise<StatusCheckResult> {
|
|
File without changes
|
|
File without changes
|
|
@@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
|
|
| 4 |
import { classNames } from '~/utils/classNames';
|
| 5 |
import { Switch } from '~/components/ui/Switch';
|
| 6 |
import { themeStore, kTheme } from '~/lib/stores/theme';
|
| 7 |
-
import type { UserProfile } from '~/components
|
| 8 |
import { useStore } from '@nanostores/react';
|
| 9 |
import { shortcutsStore } from '~/lib/stores/settings';
|
| 10 |
|
|
|
|
| 4 |
import { classNames } from '~/utils/classNames';
|
| 5 |
import { Switch } from '~/components/ui/Switch';
|
| 6 |
import { themeStore, kTheme } from '~/lib/stores/theme';
|
| 7 |
+
import type { UserProfile } from '~/components/@settings/core/types';
|
| 8 |
import { useStore } from '@nanostores/react';
|
| 9 |
import { shortcutsStore } from '~/lib/stores/settings';
|
| 10 |
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
import
|
|
|
|
| 2 |
import { classNames } from '~/utils/classNames';
|
| 3 |
import { Line } from 'react-chartjs-2';
|
| 4 |
import {
|
|
@@ -12,6 +13,9 @@ import {
|
|
| 12 |
Legend,
|
| 13 |
} from 'chart.js';
|
| 14 |
import { toast } from 'react-toastify'; // Import toast
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
// Register ChartJS components
|
| 17 |
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
|
@@ -74,12 +78,6 @@ interface SystemMetrics {
|
|
| 74 |
lcp: number;
|
| 75 |
};
|
| 76 |
};
|
| 77 |
-
storage: {
|
| 78 |
-
total: number;
|
| 79 |
-
used: number;
|
| 80 |
-
free: number;
|
| 81 |
-
type: string;
|
| 82 |
-
};
|
| 83 |
health: {
|
| 84 |
score: number;
|
| 85 |
issues: string[];
|
|
@@ -134,37 +132,46 @@ declare global {
|
|
| 134 |
}
|
| 135 |
}
|
| 136 |
|
| 137 |
-
|
| 138 |
-
const BATTERY_THRESHOLD = 20; // Enable energy saver when battery below 20%
|
| 139 |
const UPDATE_INTERVALS = {
|
| 140 |
normal: {
|
| 141 |
-
metrics: 1000, //
|
|
|
|
| 142 |
},
|
| 143 |
energySaver: {
|
| 144 |
-
metrics: 5000, //
|
|
|
|
| 145 |
},
|
| 146 |
};
|
| 147 |
|
| 148 |
-
//
|
| 149 |
-
const
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
};
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
fps: { warning: 30, critical: 15 },
|
| 159 |
-
loadTime: { warning: 3000, critical: 5000 },
|
| 160 |
};
|
| 161 |
|
|
|
|
| 162 |
const POWER_PROFILES: PowerProfile[] = [
|
| 163 |
{
|
| 164 |
name: 'Performance',
|
| 165 |
-
description: 'Maximum performance
|
| 166 |
settings: {
|
| 167 |
-
updateInterval:
|
| 168 |
enableAnimations: true,
|
| 169 |
backgroundProcessing: true,
|
| 170 |
networkThrottling: false,
|
|
@@ -172,7 +179,7 @@ const POWER_PROFILES: PowerProfile[] = [
|
|
| 172 |
},
|
| 173 |
{
|
| 174 |
name: 'Balanced',
|
| 175 |
-
description: '
|
| 176 |
settings: {
|
| 177 |
updateInterval: 2000,
|
| 178 |
enableAnimations: true,
|
|
@@ -181,10 +188,10 @@ const POWER_PROFILES: PowerProfile[] = [
|
|
| 181 |
},
|
| 182 |
},
|
| 183 |
{
|
| 184 |
-
name: '
|
| 185 |
-
description: 'Maximum
|
| 186 |
settings: {
|
| 187 |
-
updateInterval:
|
| 188 |
enableAnimations: false,
|
| 189 |
backgroundProcessing: false,
|
| 190 |
networkThrottling: true,
|
|
@@ -192,50 +199,271 @@ const POWER_PROFILES: PowerProfile[] = [
|
|
| 192 |
},
|
| 193 |
];
|
| 194 |
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
| 207 |
},
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
updatesReduced: 0,
|
| 232 |
timeInSaverMode: 0,
|
| 233 |
estimatedEnergySaved: 0,
|
| 234 |
-
});
|
| 235 |
-
|
| 236 |
-
const saverModeStartTime = useRef<number | null>(null);
|
| 237 |
-
const [selectedProfile, setSelectedProfile] = useState<PowerProfile>(POWER_PROFILES[1]); // Default to Balanced
|
| 238 |
const [alerts, setAlerts] = useState<PerformanceAlert[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
// Handle energy saver mode changes
|
| 241 |
const handleEnergySaverChange = (checked: boolean) => {
|
|
@@ -296,48 +524,6 @@ export default function TaskManagerTab() {
|
|
| 296 |
return () => clearInterval(interval);
|
| 297 |
}, [updateEnergySavings]);
|
| 298 |
|
| 299 |
-
// Get detailed performance metrics
|
| 300 |
-
const getPerformanceMetrics = async (): Promise<Partial<SystemMetrics['performance']>> => {
|
| 301 |
-
try {
|
| 302 |
-
// Get FPS
|
| 303 |
-
const fps = await measureFrameRate();
|
| 304 |
-
|
| 305 |
-
// Get page load metrics
|
| 306 |
-
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
| 307 |
-
const pageLoad = navigation.loadEventEnd - navigation.startTime;
|
| 308 |
-
const domReady = navigation.domContentLoadedEventEnd - navigation.startTime;
|
| 309 |
-
|
| 310 |
-
// Get resource metrics
|
| 311 |
-
const resources = performance.getEntriesByType('resource');
|
| 312 |
-
const resourceMetrics = {
|
| 313 |
-
total: resources.length,
|
| 314 |
-
size: resources.reduce((total, r) => total + (r as any).transferSize || 0, 0),
|
| 315 |
-
loadTime: Math.max(...resources.map((r) => r.duration)),
|
| 316 |
-
};
|
| 317 |
-
|
| 318 |
-
// Get Web Vitals
|
| 319 |
-
const ttfb = navigation.responseStart - navigation.requestStart;
|
| 320 |
-
const paintEntries = performance.getEntriesByType('paint');
|
| 321 |
-
const fcp = paintEntries.find((entry) => entry.name === 'first-contentful-paint')?.startTime || 0;
|
| 322 |
-
const lcpEntry = await getLargestContentfulPaint();
|
| 323 |
-
|
| 324 |
-
return {
|
| 325 |
-
fps,
|
| 326 |
-
pageLoad,
|
| 327 |
-
domReady,
|
| 328 |
-
resources: resourceMetrics,
|
| 329 |
-
timing: {
|
| 330 |
-
ttfb,
|
| 331 |
-
fcp,
|
| 332 |
-
lcp: lcpEntry?.startTime || 0,
|
| 333 |
-
},
|
| 334 |
-
};
|
| 335 |
-
} catch (error) {
|
| 336 |
-
console.error('Failed to get performance metrics:', error);
|
| 337 |
-
return {};
|
| 338 |
-
}
|
| 339 |
-
};
|
| 340 |
-
|
| 341 |
// Measure frame rate
|
| 342 |
const measureFrameRate = async (): Promise<number> => {
|
| 343 |
return new Promise((resolve) => {
|
|
@@ -486,12 +672,6 @@ export default function TaskManagerTab() {
|
|
| 486 |
battery: batteryInfo,
|
| 487 |
network: networkInfo,
|
| 488 |
performance: performanceMetrics as SystemMetrics['performance'],
|
| 489 |
-
storage: {
|
| 490 |
-
total: 0,
|
| 491 |
-
used: 0,
|
| 492 |
-
free: 0,
|
| 493 |
-
type: 'unknown',
|
| 494 |
-
},
|
| 495 |
health: { score: 0, issues: [], suggestions: [] },
|
| 496 |
};
|
| 497 |
|
|
@@ -597,23 +777,6 @@ export default function TaskManagerTab() {
|
|
| 597 |
};
|
| 598 |
}, [energySaverMode]);
|
| 599 |
|
| 600 |
-
// Initial update effect
|
| 601 |
-
useEffect((): (() => void) => {
|
| 602 |
-
// Initial update
|
| 603 |
-
updateMetrics();
|
| 604 |
-
|
| 605 |
-
// Set up intervals for live updates
|
| 606 |
-
const metricsInterval = setInterval(
|
| 607 |
-
updateMetrics,
|
| 608 |
-
energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
|
| 609 |
-
);
|
| 610 |
-
|
| 611 |
-
// Cleanup on unmount
|
| 612 |
-
return () => {
|
| 613 |
-
clearInterval(metricsInterval);
|
| 614 |
-
};
|
| 615 |
-
}, [energySaverMode]); // Re-create intervals when energy saver mode changes
|
| 616 |
-
|
| 617 |
const getUsageColor = (usage: number): string => {
|
| 618 |
if (usage > 80) {
|
| 619 |
return 'text-red-500';
|
|
@@ -761,6 +924,7 @@ export default function TaskManagerTab() {
|
|
| 761 |
onChange={(e) => handleAutoEnergySaverChange(e.target.checked)}
|
| 762 |
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
|
| 763 |
/>
|
|
|
|
| 764 |
<label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
|
| 765 |
Auto Energy Saver
|
| 766 |
</label>
|
|
@@ -774,6 +938,7 @@ export default function TaskManagerTab() {
|
|
| 774 |
disabled={autoEnergySaver}
|
| 775 |
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
|
| 776 |
/>
|
|
|
|
| 777 |
<label
|
| 778 |
htmlFor="energySaver"
|
| 779 |
className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
|
|
@@ -782,24 +947,43 @@ export default function TaskManagerTab() {
|
|
| 782 |
{energySaverMode && <span className="ml-2 text-xs text-bolt-elements-textSecondary">Active</span>}
|
| 783 |
</label>
|
| 784 |
</div>
|
| 785 |
-
<
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 803 |
</div>
|
| 804 |
</div>
|
| 805 |
<div className="text-sm text-bolt-elements-textSecondary">{selectedProfile.description}</div>
|
|
@@ -981,30 +1165,6 @@ export default function TaskManagerTab() {
|
|
| 981 |
</div>
|
| 982 |
)}
|
| 983 |
|
| 984 |
-
{/* Storage Section */}
|
| 985 |
-
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
|
| 986 |
-
<div className="flex items-center justify-between">
|
| 987 |
-
<span className="text-sm text-bolt-elements-textSecondary">Storage</span>
|
| 988 |
-
<span className="text-sm font-medium text-bolt-elements-textPrimary">
|
| 989 |
-
{formatBytes(metrics.storage.used)} / {formatBytes(metrics.storage.total)}
|
| 990 |
-
</span>
|
| 991 |
-
</div>
|
| 992 |
-
<div className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
| 993 |
-
<div
|
| 994 |
-
className={classNames('h-full transition-all duration-300', {
|
| 995 |
-
'bg-green-500': metrics.storage.used / metrics.storage.total < 0.7,
|
| 996 |
-
'bg-yellow-500':
|
| 997 |
-
metrics.storage.used / metrics.storage.total >= 0.7 &&
|
| 998 |
-
metrics.storage.used / metrics.storage.total < 0.9,
|
| 999 |
-
'bg-red-500': metrics.storage.used / metrics.storage.total >= 0.9,
|
| 1000 |
-
})}
|
| 1001 |
-
style={{ width: `${(metrics.storage.used / metrics.storage.total) * 100}%` }}
|
| 1002 |
-
/>
|
| 1003 |
-
</div>
|
| 1004 |
-
<div className="text-xs text-bolt-elements-textSecondary mt-2">Free: {formatBytes(metrics.storage.free)}</div>
|
| 1005 |
-
<div className="text-xs text-bolt-elements-textSecondary">Type: {metrics.storage.type}</div>
|
| 1006 |
-
</div>
|
| 1007 |
-
|
| 1008 |
{/* Performance Alerts */}
|
| 1009 |
{alerts.length > 0 && (
|
| 1010 |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
|
|
@@ -1071,7 +1231,9 @@ export default function TaskManagerTab() {
|
|
| 1071 |
</div>
|
| 1072 |
</div>
|
| 1073 |
);
|
| 1074 |
-
}
|
|
|
|
|
|
|
| 1075 |
|
| 1076 |
// Helper function to format bytes
|
| 1077 |
const formatBytes = (bytes: number): string => {
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import { useEffect, useState, useRef, useCallback } from 'react';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
import { Line } from 'react-chartjs-2';
|
| 5 |
import {
|
|
|
|
| 13 |
Legend,
|
| 14 |
} from 'chart.js';
|
| 15 |
import { toast } from 'react-toastify'; // Import toast
|
| 16 |
+
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
|
| 17 |
+
import { tabConfigurationStore, type TabConfig } from '~/lib/stores/tabConfigurationStore';
|
| 18 |
+
import { useStore } from 'zustand';
|
| 19 |
|
| 20 |
// Register ChartJS components
|
| 21 |
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
|
|
|
| 78 |
lcp: number;
|
| 79 |
};
|
| 80 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
health: {
|
| 82 |
score: number;
|
| 83 |
issues: string[];
|
|
|
|
| 132 |
}
|
| 133 |
}
|
| 134 |
|
| 135 |
+
// Constants for update intervals
|
|
|
|
| 136 |
const UPDATE_INTERVALS = {
|
| 137 |
normal: {
|
| 138 |
+
metrics: 1000, // 1 second
|
| 139 |
+
animation: 16, // ~60fps
|
| 140 |
},
|
| 141 |
energySaver: {
|
| 142 |
+
metrics: 5000, // 5 seconds
|
| 143 |
+
animation: 32, // ~30fps
|
| 144 |
},
|
| 145 |
};
|
| 146 |
|
| 147 |
+
// Constants for performance thresholds
|
| 148 |
+
const PERFORMANCE_THRESHOLDS = {
|
| 149 |
+
cpu: {
|
| 150 |
+
warning: 70,
|
| 151 |
+
critical: 90,
|
| 152 |
+
},
|
| 153 |
+
memory: {
|
| 154 |
+
warning: 80,
|
| 155 |
+
critical: 95,
|
| 156 |
+
},
|
| 157 |
+
fps: {
|
| 158 |
+
warning: 30,
|
| 159 |
+
critical: 15,
|
| 160 |
+
},
|
| 161 |
};
|
| 162 |
|
| 163 |
+
// Constants for energy calculations
|
| 164 |
+
const ENERGY_COSTS = {
|
| 165 |
+
update: 0.1, // mWh per update
|
|
|
|
|
|
|
| 166 |
};
|
| 167 |
|
| 168 |
+
// Default power profiles
|
| 169 |
const POWER_PROFILES: PowerProfile[] = [
|
| 170 |
{
|
| 171 |
name: 'Performance',
|
| 172 |
+
description: 'Maximum performance with frequent updates',
|
| 173 |
settings: {
|
| 174 |
+
updateInterval: UPDATE_INTERVALS.normal.metrics,
|
| 175 |
enableAnimations: true,
|
| 176 |
backgroundProcessing: true,
|
| 177 |
networkThrottling: false,
|
|
|
|
| 179 |
},
|
| 180 |
{
|
| 181 |
name: 'Balanced',
|
| 182 |
+
description: 'Optimal balance between performance and energy efficiency',
|
| 183 |
settings: {
|
| 184 |
updateInterval: 2000,
|
| 185 |
enableAnimations: true,
|
|
|
|
| 188 |
},
|
| 189 |
},
|
| 190 |
{
|
| 191 |
+
name: 'Energy Saver',
|
| 192 |
+
description: 'Maximum energy efficiency with reduced updates',
|
| 193 |
settings: {
|
| 194 |
+
updateInterval: UPDATE_INTERVALS.energySaver.metrics,
|
| 195 |
enableAnimations: false,
|
| 196 |
backgroundProcessing: false,
|
| 197 |
networkThrottling: true,
|
|
|
|
| 199 |
},
|
| 200 |
];
|
| 201 |
|
| 202 |
+
// Default metrics state
|
| 203 |
+
const DEFAULT_METRICS_STATE: SystemMetrics = {
|
| 204 |
+
cpu: {
|
| 205 |
+
usage: 0,
|
| 206 |
+
cores: [],
|
| 207 |
+
},
|
| 208 |
+
memory: {
|
| 209 |
+
used: 0,
|
| 210 |
+
total: 0,
|
| 211 |
+
percentage: 0,
|
| 212 |
+
heap: {
|
| 213 |
+
used: 0,
|
| 214 |
+
total: 0,
|
| 215 |
+
limit: 0,
|
| 216 |
},
|
| 217 |
+
},
|
| 218 |
+
uptime: 0,
|
| 219 |
+
network: {
|
| 220 |
+
downlink: 0,
|
| 221 |
+
latency: 0,
|
| 222 |
+
type: 'unknown',
|
| 223 |
+
bytesReceived: 0,
|
| 224 |
+
bytesSent: 0,
|
| 225 |
+
},
|
| 226 |
+
performance: {
|
| 227 |
+
fps: 0,
|
| 228 |
+
pageLoad: 0,
|
| 229 |
+
domReady: 0,
|
| 230 |
+
resources: {
|
| 231 |
+
total: 0,
|
| 232 |
+
size: 0,
|
| 233 |
+
loadTime: 0,
|
| 234 |
+
},
|
| 235 |
+
timing: {
|
| 236 |
+
ttfb: 0,
|
| 237 |
+
fcp: 0,
|
| 238 |
+
lcp: 0,
|
| 239 |
+
},
|
| 240 |
+
},
|
| 241 |
+
health: {
|
| 242 |
+
score: 0,
|
| 243 |
+
issues: [],
|
| 244 |
+
suggestions: [],
|
| 245 |
+
},
|
| 246 |
+
};
|
| 247 |
+
|
| 248 |
+
// Default metrics history
|
| 249 |
+
const DEFAULT_METRICS_HISTORY: MetricsHistory = {
|
| 250 |
+
timestamps: Array(10).fill(new Date().toLocaleTimeString()),
|
| 251 |
+
cpu: Array(10).fill(0),
|
| 252 |
+
memory: Array(10).fill(0),
|
| 253 |
+
battery: Array(10).fill(0),
|
| 254 |
+
network: Array(10).fill(0),
|
| 255 |
+
};
|
| 256 |
+
|
| 257 |
+
// Battery threshold for auto energy saver mode
|
| 258 |
+
const BATTERY_THRESHOLD = 20; // percentage
|
| 259 |
+
|
| 260 |
+
// Maximum number of history points to keep
|
| 261 |
+
const MAX_HISTORY_POINTS = 10;
|
| 262 |
+
|
| 263 |
+
const TaskManagerTab: React.FC = () => {
|
| 264 |
+
// Initialize metrics state with defaults
|
| 265 |
+
const [metrics, setMetrics] = useState<SystemMetrics>(() => DEFAULT_METRICS_STATE);
|
| 266 |
+
const [metricsHistory, setMetricsHistory] = useState<MetricsHistory>(() => DEFAULT_METRICS_HISTORY);
|
| 267 |
+
const [energySaverMode, setEnergySaverMode] = useState<boolean>(false);
|
| 268 |
+
const [autoEnergySaver, setAutoEnergySaver] = useState<boolean>(false);
|
| 269 |
+
const [energySavings, setEnergySavings] = useState<EnergySavings>(() => ({
|
| 270 |
updatesReduced: 0,
|
| 271 |
timeInSaverMode: 0,
|
| 272 |
estimatedEnergySaved: 0,
|
| 273 |
+
}));
|
| 274 |
+
const [selectedProfile, setSelectedProfile] = useState<PowerProfile>(() => POWER_PROFILES[1]);
|
|
|
|
|
|
|
| 275 |
const [alerts, setAlerts] = useState<PerformanceAlert[]>([]);
|
| 276 |
+
const saverModeStartTime = useRef<number | null>(null);
|
| 277 |
+
|
| 278 |
+
// Get update status and tab configuration
|
| 279 |
+
const { hasUpdate } = useUpdateCheck();
|
| 280 |
+
const tabConfig = useStore(tabConfigurationStore);
|
| 281 |
+
|
| 282 |
+
const resetTabConfiguration = useCallback(() => {
|
| 283 |
+
tabConfig.reset();
|
| 284 |
+
return tabConfig.get();
|
| 285 |
+
}, [tabConfig]);
|
| 286 |
+
|
| 287 |
+
// Effect to handle tab visibility
|
| 288 |
+
useEffect(() => {
|
| 289 |
+
const handleTabVisibility = () => {
|
| 290 |
+
const currentConfig = tabConfig.get();
|
| 291 |
+
const controlledTabs = ['debug', 'update'];
|
| 292 |
+
|
| 293 |
+
// Update visibility based on conditions
|
| 294 |
+
const updatedTabs = currentConfig.userTabs.map((tab: TabConfig) => {
|
| 295 |
+
if (controlledTabs.includes(tab.id)) {
|
| 296 |
+
return {
|
| 297 |
+
...tab,
|
| 298 |
+
visible: tab.id === 'debug' ? metrics.cpu.usage > 80 : hasUpdate,
|
| 299 |
+
};
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
return tab;
|
| 303 |
+
});
|
| 304 |
+
|
| 305 |
+
tabConfig.set({
|
| 306 |
+
...currentConfig,
|
| 307 |
+
userTabs: updatedTabs,
|
| 308 |
+
});
|
| 309 |
+
};
|
| 310 |
+
|
| 311 |
+
const checkInterval = setInterval(handleTabVisibility, 5000);
|
| 312 |
+
|
| 313 |
+
return () => {
|
| 314 |
+
clearInterval(checkInterval);
|
| 315 |
+
};
|
| 316 |
+
}, [metrics.cpu.usage, hasUpdate, tabConfig]);
|
| 317 |
+
|
| 318 |
+
// Effect to handle reset and initialization
|
| 319 |
+
useEffect(() => {
|
| 320 |
+
const resetToDefaults = () => {
|
| 321 |
+
console.log('TaskManagerTab: Resetting to defaults');
|
| 322 |
+
|
| 323 |
+
// Reset metrics and local state
|
| 324 |
+
setMetrics(DEFAULT_METRICS_STATE);
|
| 325 |
+
setMetricsHistory(DEFAULT_METRICS_HISTORY);
|
| 326 |
+
setEnergySaverMode(false);
|
| 327 |
+
setAutoEnergySaver(false);
|
| 328 |
+
setEnergySavings({
|
| 329 |
+
updatesReduced: 0,
|
| 330 |
+
timeInSaverMode: 0,
|
| 331 |
+
estimatedEnergySaved: 0,
|
| 332 |
+
});
|
| 333 |
+
setSelectedProfile(POWER_PROFILES[1]);
|
| 334 |
+
setAlerts([]);
|
| 335 |
+
saverModeStartTime.current = null;
|
| 336 |
+
|
| 337 |
+
// Reset tab configuration to ensure proper visibility
|
| 338 |
+
const defaultConfig = resetTabConfiguration();
|
| 339 |
+
console.log('TaskManagerTab: Reset tab configuration:', defaultConfig);
|
| 340 |
+
};
|
| 341 |
+
|
| 342 |
+
// Listen for both storage changes and custom reset event
|
| 343 |
+
const handleReset = (event: Event | StorageEvent) => {
|
| 344 |
+
if (event instanceof StorageEvent) {
|
| 345 |
+
if (event.key === 'tabConfiguration' && event.newValue === null) {
|
| 346 |
+
resetToDefaults();
|
| 347 |
+
}
|
| 348 |
+
} else if (event instanceof CustomEvent && event.type === 'tabConfigReset') {
|
| 349 |
+
resetToDefaults();
|
| 350 |
+
}
|
| 351 |
+
};
|
| 352 |
+
|
| 353 |
+
// Initial setup
|
| 354 |
+
const initializeTab = async () => {
|
| 355 |
+
try {
|
| 356 |
+
// Load saved preferences
|
| 357 |
+
const savedEnergySaver = localStorage.getItem('energySaverMode');
|
| 358 |
+
const savedAutoSaver = localStorage.getItem('autoEnergySaver');
|
| 359 |
+
const savedProfile = localStorage.getItem('selectedProfile');
|
| 360 |
+
|
| 361 |
+
if (savedEnergySaver) {
|
| 362 |
+
setEnergySaverMode(JSON.parse(savedEnergySaver));
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
if (savedAutoSaver) {
|
| 366 |
+
setAutoEnergySaver(JSON.parse(savedAutoSaver));
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
if (savedProfile) {
|
| 370 |
+
const profile = POWER_PROFILES.find((p) => p.name === savedProfile);
|
| 371 |
+
|
| 372 |
+
if (profile) {
|
| 373 |
+
setSelectedProfile(profile);
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
await updateMetrics();
|
| 378 |
+
} catch (error) {
|
| 379 |
+
console.error('Failed to initialize TaskManagerTab:', error);
|
| 380 |
+
resetToDefaults();
|
| 381 |
+
}
|
| 382 |
+
};
|
| 383 |
+
|
| 384 |
+
window.addEventListener('storage', handleReset);
|
| 385 |
+
window.addEventListener('tabConfigReset', handleReset);
|
| 386 |
+
initializeTab();
|
| 387 |
+
|
| 388 |
+
return () => {
|
| 389 |
+
window.removeEventListener('storage', handleReset);
|
| 390 |
+
window.removeEventListener('tabConfigReset', handleReset);
|
| 391 |
+
};
|
| 392 |
+
}, []);
|
| 393 |
+
|
| 394 |
+
// Get detailed performance metrics
|
| 395 |
+
const getPerformanceMetrics = async (): Promise<Partial<SystemMetrics['performance']>> => {
|
| 396 |
+
try {
|
| 397 |
+
// Get FPS
|
| 398 |
+
const fps = await measureFrameRate();
|
| 399 |
+
|
| 400 |
+
// Get page load metrics
|
| 401 |
+
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
| 402 |
+
const pageLoad = navigation.loadEventEnd - navigation.startTime;
|
| 403 |
+
const domReady = navigation.domContentLoadedEventEnd - navigation.startTime;
|
| 404 |
+
|
| 405 |
+
// Get resource metrics
|
| 406 |
+
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
|
| 407 |
+
const resourceMetrics = {
|
| 408 |
+
total: resources.length,
|
| 409 |
+
size: resources.reduce((total, r) => total + (r.transferSize || 0), 0),
|
| 410 |
+
loadTime: Math.max(0, ...resources.map((r) => r.duration)),
|
| 411 |
+
};
|
| 412 |
+
|
| 413 |
+
// Get Web Vitals
|
| 414 |
+
const ttfb = navigation.responseStart - navigation.requestStart;
|
| 415 |
+
const paintEntries = performance.getEntriesByType('paint');
|
| 416 |
+
const fcp = paintEntries.find((entry) => entry.name === 'first-contentful-paint')?.startTime || 0;
|
| 417 |
+
const lcpEntry = await getLargestContentfulPaint();
|
| 418 |
+
|
| 419 |
+
return {
|
| 420 |
+
fps,
|
| 421 |
+
pageLoad,
|
| 422 |
+
domReady,
|
| 423 |
+
resources: resourceMetrics,
|
| 424 |
+
timing: {
|
| 425 |
+
ttfb,
|
| 426 |
+
fcp,
|
| 427 |
+
lcp: lcpEntry?.startTime || 0,
|
| 428 |
+
},
|
| 429 |
+
};
|
| 430 |
+
} catch (error) {
|
| 431 |
+
console.error('Failed to get performance metrics:', error);
|
| 432 |
+
return {};
|
| 433 |
+
}
|
| 434 |
+
};
|
| 435 |
+
|
| 436 |
+
// Single useEffect for metrics updates
|
| 437 |
+
useEffect(() => {
|
| 438 |
+
let isComponentMounted = true;
|
| 439 |
+
|
| 440 |
+
const updateMetricsWrapper = async () => {
|
| 441 |
+
if (!isComponentMounted) {
|
| 442 |
+
return;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
try {
|
| 446 |
+
await updateMetrics();
|
| 447 |
+
} catch (error) {
|
| 448 |
+
console.error('Failed to update metrics:', error);
|
| 449 |
+
}
|
| 450 |
+
};
|
| 451 |
+
|
| 452 |
+
// Initial update
|
| 453 |
+
updateMetricsWrapper();
|
| 454 |
+
|
| 455 |
+
// Set up interval with immediate assignment
|
| 456 |
+
const metricsInterval = setInterval(
|
| 457 |
+
updateMetricsWrapper,
|
| 458 |
+
energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
|
| 459 |
+
);
|
| 460 |
+
|
| 461 |
+
// Cleanup function
|
| 462 |
+
return () => {
|
| 463 |
+
isComponentMounted = false;
|
| 464 |
+
clearInterval(metricsInterval);
|
| 465 |
+
};
|
| 466 |
+
}, [energySaverMode]); // Only depend on energySaverMode
|
| 467 |
|
| 468 |
// Handle energy saver mode changes
|
| 469 |
const handleEnergySaverChange = (checked: boolean) => {
|
|
|
|
| 524 |
return () => clearInterval(interval);
|
| 525 |
}, [updateEnergySavings]);
|
| 526 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
// Measure frame rate
|
| 528 |
const measureFrameRate = async (): Promise<number> => {
|
| 529 |
return new Promise((resolve) => {
|
|
|
|
| 672 |
battery: batteryInfo,
|
| 673 |
network: networkInfo,
|
| 674 |
performance: performanceMetrics as SystemMetrics['performance'],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
health: { score: 0, issues: [], suggestions: [] },
|
| 676 |
};
|
| 677 |
|
|
|
|
| 777 |
};
|
| 778 |
}, [energySaverMode]);
|
| 779 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 780 |
const getUsageColor = (usage: number): string => {
|
| 781 |
if (usage > 80) {
|
| 782 |
return 'text-red-500';
|
|
|
|
| 924 |
onChange={(e) => handleAutoEnergySaverChange(e.target.checked)}
|
| 925 |
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
|
| 926 |
/>
|
| 927 |
+
<div className="i-ph:gauge-duotone w-4 h-4 text-bolt-elements-textSecondary" />
|
| 928 |
<label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
|
| 929 |
Auto Energy Saver
|
| 930 |
</label>
|
|
|
|
| 938 |
disabled={autoEnergySaver}
|
| 939 |
className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
|
| 940 |
/>
|
| 941 |
+
<div className="i-ph:leaf-duotone w-4 h-4 text-bolt-elements-textSecondary" />
|
| 942 |
<label
|
| 943 |
htmlFor="energySaver"
|
| 944 |
className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
|
|
|
|
| 947 |
{energySaverMode && <span className="ml-2 text-xs text-bolt-elements-textSecondary">Active</span>}
|
| 948 |
</label>
|
| 949 |
</div>
|
| 950 |
+
<div className="relative">
|
| 951 |
+
<select
|
| 952 |
+
value={selectedProfile.name}
|
| 953 |
+
onChange={(e) => {
|
| 954 |
+
const profile = POWER_PROFILES.find((p) => p.name === e.target.value);
|
| 955 |
+
|
| 956 |
+
if (profile) {
|
| 957 |
+
setSelectedProfile(profile);
|
| 958 |
+
toast.success(`Switched to ${profile.name} power profile`);
|
| 959 |
+
}
|
| 960 |
+
}}
|
| 961 |
+
className="pl-8 pr-8 py-1.5 rounded-md bg-bolt-background-secondary dark:bg-[#1E1E1E] border border-bolt-border dark:border-bolt-borderDark text-sm text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark hover:border-bolt-action-primary dark:hover:border-bolt-action-primary focus:outline-none focus:ring-1 focus:ring-bolt-action-primary appearance-none min-w-[160px] cursor-pointer transition-colors duration-150"
|
| 962 |
+
style={{ WebkitAppearance: 'none', MozAppearance: 'none' }}
|
| 963 |
+
>
|
| 964 |
+
{POWER_PROFILES.map((profile) => (
|
| 965 |
+
<option
|
| 966 |
+
key={profile.name}
|
| 967 |
+
value={profile.name}
|
| 968 |
+
className="py-2 px-3 bg-bolt-background-secondary dark:bg-[#1E1E1E] text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark hover:bg-bolt-background-tertiary dark:hover:bg-bolt-backgroundDark-tertiary cursor-pointer"
|
| 969 |
+
>
|
| 970 |
+
{profile.name}
|
| 971 |
+
</option>
|
| 972 |
+
))}
|
| 973 |
+
</select>
|
| 974 |
+
<div className="absolute left-2 top-1/2 -translate-y-1/2 pointer-events-none">
|
| 975 |
+
<div
|
| 976 |
+
className={classNames('w-4 h-4 text-bolt-elements-textSecondary', {
|
| 977 |
+
'i-ph:lightning-fill text-yellow-500': selectedProfile.name === 'Performance',
|
| 978 |
+
'i-ph:scales-fill text-blue-500': selectedProfile.name === 'Balanced',
|
| 979 |
+
'i-ph:leaf-fill text-green-500': selectedProfile.name === 'Energy Saver',
|
| 980 |
+
})}
|
| 981 |
+
/>
|
| 982 |
+
</div>
|
| 983 |
+
<div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
|
| 984 |
+
<div className="i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary opacity-75" />
|
| 985 |
+
</div>
|
| 986 |
+
</div>
|
| 987 |
</div>
|
| 988 |
</div>
|
| 989 |
<div className="text-sm text-bolt-elements-textSecondary">{selectedProfile.description}</div>
|
|
|
|
| 1165 |
</div>
|
| 1166 |
)}
|
| 1167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1168 |
{/* Performance Alerts */}
|
| 1169 |
{alerts.length > 0 && (
|
| 1170 |
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
|
|
|
|
| 1231 |
</div>
|
| 1232 |
</div>
|
| 1233 |
);
|
| 1234 |
+
};
|
| 1235 |
+
|
| 1236 |
+
export default React.memo(TaskManagerTab);
|
| 1237 |
|
| 1238 |
// Helper function to format bytes
|
| 1239 |
const formatBytes = (bytes: number): string => {
|
|
@@ -35,6 +35,10 @@ interface UpdateInfo {
|
|
| 35 |
downloadProgress?: number;
|
| 36 |
installProgress?: number;
|
| 37 |
estimatedTimeRemaining?: number;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
interface UpdateSettings {
|
|
@@ -46,11 +50,8 @@ interface UpdateSettings {
|
|
| 46 |
interface UpdateResponse {
|
| 47 |
success: boolean;
|
| 48 |
error?: string;
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
total: number;
|
| 52 |
-
stage: 'download' | 'install' | 'complete';
|
| 53 |
-
};
|
| 54 |
}
|
| 55 |
|
| 56 |
const categorizeChangelog = (messages: string[]) => {
|
|
@@ -190,62 +191,29 @@ const UpdateTab = () => {
|
|
| 190 |
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
|
| 191 |
}, [updateSettings]);
|
| 192 |
|
| 193 |
-
const handleUpdateProgress = async (response: Response): Promise<void> => {
|
| 194 |
-
const reader = response.body?.getReader();
|
| 195 |
-
|
| 196 |
-
if (!reader) {
|
| 197 |
-
return;
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
const contentLength = +(response.headers.get('Content-Length') ?? 0);
|
| 201 |
-
let receivedLength = 0;
|
| 202 |
-
|
| 203 |
-
while (true) {
|
| 204 |
-
const { done, value } = await reader.read();
|
| 205 |
-
|
| 206 |
-
if (done) {
|
| 207 |
-
break;
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
receivedLength += value.length;
|
| 211 |
-
|
| 212 |
-
const progress = (receivedLength / contentLength) * 100;
|
| 213 |
-
|
| 214 |
-
setUpdateInfo((prev) => (prev ? { ...prev, downloadProgress: progress } : prev));
|
| 215 |
-
}
|
| 216 |
-
};
|
| 217 |
-
|
| 218 |
const checkForUpdates = async () => {
|
| 219 |
console.log('Starting update check...');
|
| 220 |
setIsChecking(true);
|
| 221 |
setError(null);
|
| 222 |
setLastChecked(new Date());
|
| 223 |
|
| 224 |
-
// Add a minimum delay of 2 seconds to show the spinning animation
|
| 225 |
-
const startTime = Date.now();
|
| 226 |
-
|
| 227 |
try {
|
| 228 |
console.log('Fetching update info...');
|
| 229 |
|
| 230 |
-
const githubToken = localStorage.getItem('github_connection');
|
| 231 |
-
const headers: HeadersInit = {};
|
| 232 |
-
|
| 233 |
-
if (githubToken) {
|
| 234 |
-
const { token } = JSON.parse(githubToken);
|
| 235 |
-
headers.Authorization = `Bearer ${token}`;
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
const branchToCheck = isLatestBranch ? 'main' : 'stable';
|
| 239 |
-
const info = await GITHUB_URLS.commitJson(branchToCheck
|
| 240 |
|
| 241 |
-
|
| 242 |
-
const elapsedTime = Date.now() - startTime;
|
| 243 |
|
| 244 |
-
if (
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
-
|
|
|
|
| 249 |
|
| 250 |
if (info.hasUpdate) {
|
| 251 |
const existingLogs = Object.values(logStore.logs.get());
|
|
@@ -267,18 +235,23 @@ const UpdateTab = () => {
|
|
| 267 |
});
|
| 268 |
|
| 269 |
if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
|
| 270 |
-
setUpdateChangelog(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
setShowUpdateDialog(true);
|
| 272 |
}
|
| 273 |
}
|
| 274 |
}
|
| 275 |
} catch (err) {
|
| 276 |
-
console.error('Detailed update check error:', err);
|
| 277 |
-
setError('Failed to check for updates. Please try again later.');
|
| 278 |
console.error('Update check failed:', err);
|
|
|
|
|
|
|
|
|
|
| 279 |
setUpdateFailed(true);
|
| 280 |
} finally {
|
| 281 |
-
console.log('Update check completed');
|
| 282 |
setIsChecking(false);
|
| 283 |
}
|
| 284 |
};
|
|
@@ -292,49 +265,45 @@ const UpdateTab = () => {
|
|
| 292 |
|
| 293 |
const attemptUpdate = async (): Promise<void> => {
|
| 294 |
try {
|
| 295 |
-
const
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
branch: isLatestBranch ? 'main' : 'stable',
|
| 305 |
-
settings: updateSettings,
|
| 306 |
-
}),
|
| 307 |
-
});
|
| 308 |
-
|
| 309 |
-
if (!response.ok) {
|
| 310 |
-
throw new Error('Failed to initiate update');
|
| 311 |
-
}
|
| 312 |
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
-
|
| 316 |
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
toast.success('Update completed successfully!');
|
| 323 |
-
setUpdateFailed(false);
|
| 324 |
|
| 325 |
-
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
-
|
| 329 |
}
|
| 330 |
|
| 331 |
-
|
| 332 |
-
logStore.logInfo('Manual update required', {
|
| 333 |
-
type: 'update',
|
| 334 |
-
message: 'Please download and install the latest version from the GitHub releases page.',
|
| 335 |
-
});
|
| 336 |
-
|
| 337 |
-
return;
|
| 338 |
} catch (err) {
|
| 339 |
currentRetry++;
|
| 340 |
|
|
@@ -349,13 +318,11 @@ const UpdateTab = () => {
|
|
| 349 |
return;
|
| 350 |
}
|
| 351 |
|
| 352 |
-
setError('Failed to
|
| 353 |
console.error('Update failed:', err);
|
| 354 |
logStore.logSystem('Update failed: ' + errorMessage);
|
| 355 |
toast.error('Update failed: ' + errorMessage);
|
| 356 |
setUpdateFailed(true);
|
| 357 |
-
|
| 358 |
-
return;
|
| 359 |
}
|
| 360 |
};
|
| 361 |
|
|
@@ -518,7 +485,19 @@ const UpdateTab = () => {
|
|
| 518 |
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400">
|
| 519 |
<div className="flex items-center gap-2">
|
| 520 |
<div className="i-ph:warning-circle" />
|
| 521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
</div>
|
| 523 |
</div>
|
| 524 |
)}
|
|
@@ -803,7 +782,7 @@ const UpdateTab = () => {
|
|
| 803 |
</DialogDescription>
|
| 804 |
|
| 805 |
<div className="mt-3">
|
| 806 |
-
<h3 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">
|
| 807 |
<div
|
| 808 |
className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[300px] overflow-y-auto"
|
| 809 |
style={{
|
|
@@ -814,7 +793,18 @@ const UpdateTab = () => {
|
|
| 814 |
<div className="text-sm text-bolt-elements-textSecondary space-y-1.5">
|
| 815 |
{updateChangelog.map((log, index) => (
|
| 816 |
<div key={index} className="break-words leading-relaxed">
|
| 817 |
-
{log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
</div>
|
| 819 |
))}
|
| 820 |
</div>
|
|
|
|
| 35 |
downloadProgress?: number;
|
| 36 |
installProgress?: number;
|
| 37 |
estimatedTimeRemaining?: number;
|
| 38 |
+
error?: {
|
| 39 |
+
type: string;
|
| 40 |
+
message: string;
|
| 41 |
+
};
|
| 42 |
}
|
| 43 |
|
| 44 |
interface UpdateSettings {
|
|
|
|
| 50 |
interface UpdateResponse {
|
| 51 |
success: boolean;
|
| 52 |
error?: string;
|
| 53 |
+
message?: string;
|
| 54 |
+
instructions?: string[];
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
|
| 57 |
const categorizeChangelog = (messages: string[]) => {
|
|
|
|
| 191 |
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
|
| 192 |
}, [updateSettings]);
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
const checkForUpdates = async () => {
|
| 195 |
console.log('Starting update check...');
|
| 196 |
setIsChecking(true);
|
| 197 |
setError(null);
|
| 198 |
setLastChecked(new Date());
|
| 199 |
|
|
|
|
|
|
|
|
|
|
| 200 |
try {
|
| 201 |
console.log('Fetching update info...');
|
| 202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
const branchToCheck = isLatestBranch ? 'main' : 'stable';
|
| 204 |
+
const info = await GITHUB_URLS.commitJson(branchToCheck);
|
| 205 |
|
| 206 |
+
setUpdateInfo(info);
|
|
|
|
| 207 |
|
| 208 |
+
if (info.error) {
|
| 209 |
+
setError(info.error.message);
|
| 210 |
+
logStore.logWarning('Update Check Failed', {
|
| 211 |
+
type: 'update',
|
| 212 |
+
message: info.error.message,
|
| 213 |
+
});
|
| 214 |
|
| 215 |
+
return;
|
| 216 |
+
}
|
| 217 |
|
| 218 |
if (info.hasUpdate) {
|
| 219 |
const existingLogs = Object.values(logStore.logs.get());
|
|
|
|
| 235 |
});
|
| 236 |
|
| 237 |
if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
|
| 238 |
+
setUpdateChangelog([
|
| 239 |
+
'New version available.',
|
| 240 |
+
`Compare changes: https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
|
| 241 |
+
'',
|
| 242 |
+
'Click "Update Now" to start the update process.',
|
| 243 |
+
]);
|
| 244 |
setShowUpdateDialog(true);
|
| 245 |
}
|
| 246 |
}
|
| 247 |
}
|
| 248 |
} catch (err) {
|
|
|
|
|
|
|
| 249 |
console.error('Update check failed:', err);
|
| 250 |
+
|
| 251 |
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
| 252 |
+
setError(`Failed to check for updates: ${errorMessage}`);
|
| 253 |
setUpdateFailed(true);
|
| 254 |
} finally {
|
|
|
|
| 255 |
setIsChecking(false);
|
| 256 |
}
|
| 257 |
};
|
|
|
|
| 265 |
|
| 266 |
const attemptUpdate = async (): Promise<void> => {
|
| 267 |
try {
|
| 268 |
+
const response = await fetch('/api/update', {
|
| 269 |
+
method: 'POST',
|
| 270 |
+
headers: {
|
| 271 |
+
'Content-Type': 'application/json',
|
| 272 |
+
},
|
| 273 |
+
body: JSON.stringify({
|
| 274 |
+
branch: isLatestBranch ? 'main' : 'stable',
|
| 275 |
+
}),
|
| 276 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
+
if (!response.ok) {
|
| 279 |
+
const errorData = (await response.json()) as { error: string };
|
| 280 |
+
throw new Error(errorData.error || 'Failed to initiate update');
|
| 281 |
+
}
|
| 282 |
|
| 283 |
+
const result = (await response.json()) as UpdateResponse;
|
| 284 |
|
| 285 |
+
if (result.success) {
|
| 286 |
+
logStore.logSuccess('Update instructions ready', {
|
| 287 |
+
type: 'update',
|
| 288 |
+
message: result.message || 'Update instructions ready',
|
| 289 |
+
});
|
|
|
|
|
|
|
| 290 |
|
| 291 |
+
// Show manual update instructions
|
| 292 |
+
setShowManualInstructions(true);
|
| 293 |
+
setUpdateChangelog(
|
| 294 |
+
result.instructions || [
|
| 295 |
+
'Failed to get update instructions. Please update manually:',
|
| 296 |
+
'1. git pull origin main',
|
| 297 |
+
'2. pnpm install',
|
| 298 |
+
'3. pnpm build',
|
| 299 |
+
'4. Restart the application',
|
| 300 |
+
],
|
| 301 |
+
);
|
| 302 |
|
| 303 |
+
return;
|
| 304 |
}
|
| 305 |
|
| 306 |
+
throw new Error(result.error || 'Update failed');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
} catch (err) {
|
| 308 |
currentRetry++;
|
| 309 |
|
|
|
|
| 318 |
return;
|
| 319 |
}
|
| 320 |
|
| 321 |
+
setError('Failed to get update instructions. Please update manually.');
|
| 322 |
console.error('Update failed:', err);
|
| 323 |
logStore.logSystem('Update failed: ' + errorMessage);
|
| 324 |
toast.error('Update failed: ' + errorMessage);
|
| 325 |
setUpdateFailed(true);
|
|
|
|
|
|
|
| 326 |
}
|
| 327 |
};
|
| 328 |
|
|
|
|
| 485 |
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400">
|
| 486 |
<div className="flex items-center gap-2">
|
| 487 |
<div className="i-ph:warning-circle" />
|
| 488 |
+
<div className="flex flex-col">
|
| 489 |
+
<span className="font-medium">{error}</span>
|
| 490 |
+
{error.includes('rate limit') && (
|
| 491 |
+
<span className="text-sm mt-1">
|
| 492 |
+
Try adding a GitHub token in the connections tab to increase the rate limit.
|
| 493 |
+
</span>
|
| 494 |
+
)}
|
| 495 |
+
{error.includes('authentication') && (
|
| 496 |
+
<span className="text-sm mt-1">
|
| 497 |
+
Please check your GitHub token configuration in the connections tab.
|
| 498 |
+
</span>
|
| 499 |
+
)}
|
| 500 |
+
</div>
|
| 501 |
</div>
|
| 502 |
</div>
|
| 503 |
)}
|
|
|
|
| 782 |
</DialogDescription>
|
| 783 |
|
| 784 |
<div className="mt-3">
|
| 785 |
+
<h3 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Update Information:</h3>
|
| 786 |
<div
|
| 787 |
className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[300px] overflow-y-auto"
|
| 788 |
style={{
|
|
|
|
| 793 |
<div className="text-sm text-bolt-elements-textSecondary space-y-1.5">
|
| 794 |
{updateChangelog.map((log, index) => (
|
| 795 |
<div key={index} className="break-words leading-relaxed">
|
| 796 |
+
{log.startsWith('Compare changes:') ? (
|
| 797 |
+
<a
|
| 798 |
+
href={log.split(': ')[1]}
|
| 799 |
+
target="_blank"
|
| 800 |
+
rel="noopener noreferrer"
|
| 801 |
+
className="text-purple-500 hover:text-purple-600 dark:text-purple-400 dark:hover:text-purple-300"
|
| 802 |
+
>
|
| 803 |
+
View changes on GitHub
|
| 804 |
+
</a>
|
| 805 |
+
) : (
|
| 806 |
+
log
|
| 807 |
+
)}
|
| 808 |
</div>
|
| 809 |
))}
|
| 810 |
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Variants } from 'framer-motion';
|
| 2 |
+
|
| 3 |
+
export const fadeIn: Variants = {
|
| 4 |
+
initial: { opacity: 0 },
|
| 5 |
+
animate: { opacity: 1 },
|
| 6 |
+
exit: { opacity: 0 },
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
export const slideIn: Variants = {
|
| 10 |
+
initial: { opacity: 0, y: 20 },
|
| 11 |
+
animate: { opacity: 1, y: 0 },
|
| 12 |
+
exit: { opacity: 0, y: -20 },
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
export const scaleIn: Variants = {
|
| 16 |
+
initial: { opacity: 0, scale: 0.8 },
|
| 17 |
+
animate: { opacity: 1, scale: 1 },
|
| 18 |
+
exit: { opacity: 0, scale: 0.8 },
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
export const tabAnimation: Variants = {
|
| 22 |
+
initial: { opacity: 0, scale: 0.8, y: 20 },
|
| 23 |
+
animate: { opacity: 1, scale: 1, y: 0 },
|
| 24 |
+
exit: { opacity: 0, scale: 0.8, y: -20 },
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
export const overlayAnimation: Variants = {
|
| 28 |
+
initial: { opacity: 0 },
|
| 29 |
+
animate: { opacity: 1 },
|
| 30 |
+
exit: { opacity: 0 },
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
export const modalAnimation: Variants = {
|
| 34 |
+
initial: { opacity: 0, scale: 0.95, y: 20 },
|
| 35 |
+
animate: { opacity: 1, scale: 1, y: 0 },
|
| 36 |
+
exit: { opacity: 0, scale: 0.95, y: 20 },
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
export const transition = {
|
| 40 |
+
duration: 0.2,
|
| 41 |
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { TabType, TabVisibilityConfig } from '~/components/@settings/core/types';
|
| 2 |
+
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
|
| 3 |
+
|
| 4 |
+
export const getVisibleTabs = (
|
| 5 |
+
tabConfiguration: { userTabs: TabVisibilityConfig[]; developerTabs?: TabVisibilityConfig[] },
|
| 6 |
+
isDeveloperMode: boolean,
|
| 7 |
+
notificationsEnabled: boolean,
|
| 8 |
+
): TabVisibilityConfig[] => {
|
| 9 |
+
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
| 10 |
+
console.warn('Invalid tab configuration, using defaults');
|
| 11 |
+
return DEFAULT_TAB_CONFIG as TabVisibilityConfig[];
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
// In developer mode, show ALL tabs without restrictions
|
| 15 |
+
if (isDeveloperMode) {
|
| 16 |
+
// Combine all unique tabs from both user and developer configurations
|
| 17 |
+
const allTabs = new Set([
|
| 18 |
+
...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
|
| 19 |
+
...tabConfiguration.userTabs.map((tab) => tab.id),
|
| 20 |
+
...(tabConfiguration.developerTabs || []).map((tab) => tab.id),
|
| 21 |
+
'task-manager' as TabType, // Always include task-manager in developer mode
|
| 22 |
+
]);
|
| 23 |
+
|
| 24 |
+
// Create a complete tab list with all tabs visible
|
| 25 |
+
const devTabs = Array.from(allTabs).map((tabId) => {
|
| 26 |
+
// Try to find existing configuration for this tab
|
| 27 |
+
const existingTab =
|
| 28 |
+
tabConfiguration.developerTabs?.find((t) => t.id === tabId) ||
|
| 29 |
+
tabConfiguration.userTabs?.find((t) => t.id === tabId) ||
|
| 30 |
+
DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
|
| 31 |
+
|
| 32 |
+
return {
|
| 33 |
+
id: tabId as TabType,
|
| 34 |
+
visible: true,
|
| 35 |
+
window: 'developer' as const,
|
| 36 |
+
order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
|
| 37 |
+
} as TabVisibilityConfig;
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
return devTabs.sort((a, b) => a.order - b.order);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// In user mode, only show visible user tabs
|
| 44 |
+
return tabConfiguration.userTabs
|
| 45 |
+
.filter((tab) => {
|
| 46 |
+
if (!tab || typeof tab.id !== 'string') {
|
| 47 |
+
console.warn('Invalid tab entry:', tab);
|
| 48 |
+
return false;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Hide notifications tab if notifications are disabled
|
| 52 |
+
if (tab.id === 'notifications' && !notificationsEnabled) {
|
| 53 |
+
return false;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Always show task-manager in user mode if it's configured as visible
|
| 57 |
+
if (tab.id === 'task-manager') {
|
| 58 |
+
return tab.visible;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Only show tabs that are explicitly visible and assigned to the user window
|
| 62 |
+
return tab.visible && tab.window === 'user';
|
| 63 |
+
})
|
| 64 |
+
.sort((a, b) => a.order - b.order);
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
export const reorderTabs = (
|
| 68 |
+
tabs: TabVisibilityConfig[],
|
| 69 |
+
startIndex: number,
|
| 70 |
+
endIndex: number,
|
| 71 |
+
): TabVisibilityConfig[] => {
|
| 72 |
+
const result = Array.from(tabs);
|
| 73 |
+
const [removed] = result.splice(startIndex, 1);
|
| 74 |
+
result.splice(endIndex, 0, removed);
|
| 75 |
+
|
| 76 |
+
// Update order property
|
| 77 |
+
return result.map((tab, index) => ({
|
| 78 |
+
...tab,
|
| 79 |
+
order: index,
|
| 80 |
+
}));
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
export const resetToDefaultConfig = (isDeveloperMode: boolean): TabVisibilityConfig[] => {
|
| 84 |
+
return DEFAULT_TAB_CONFIG.map((tab) => ({
|
| 85 |
+
...tab,
|
| 86 |
+
visible: isDeveloperMode ? true : tab.window === 'user',
|
| 87 |
+
window: isDeveloperMode ? 'developer' : tab.window,
|
| 88 |
+
})) as TabVisibilityConfig[];
|
| 89 |
+
};
|
|
@@ -23,6 +23,7 @@ import type { ProviderInfo } from '~/types/model';
|
|
| 23 |
import { useSearchParams } from '@remix-run/react';
|
| 24 |
import { createSampler } from '~/utils/sampler';
|
| 25 |
import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
|
|
|
|
| 26 |
|
| 27 |
const toastAnimation = cssTransition({
|
| 28 |
enter: 'animated fadeInRight',
|
|
@@ -114,8 +115,8 @@ export const ChatImpl = memo(
|
|
| 114 |
|
| 115 |
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 116 |
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
| 117 |
-
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
| 118 |
-
const [imageDataList, setImageDataList] = useState<string[]>([]);
|
| 119 |
const [searchParams, setSearchParams] = useSearchParams();
|
| 120 |
const [fakeLoading, setFakeLoading] = useState(false);
|
| 121 |
const files = useStore(workbenchStore.files);
|
|
@@ -161,6 +162,11 @@ export const ChatImpl = memo(
|
|
| 161 |
sendExtraMessageFields: true,
|
| 162 |
onError: (e) => {
|
| 163 |
logger.error('Request failed\n\n', e, error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
toast.error(
|
| 165 |
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
|
| 166 |
);
|
|
@@ -171,8 +177,14 @@ export const ChatImpl = memo(
|
|
| 171 |
|
| 172 |
if (usage) {
|
| 173 |
console.log('Token usage:', usage);
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
}
|
| 177 |
|
| 178 |
logger.debug('Finished streaming');
|
|
@@ -231,6 +243,13 @@ export const ChatImpl = memo(
|
|
| 231 |
stop();
|
| 232 |
chatStore.setKey('aborted', true);
|
| 233 |
workbenchStore.abortAllActions();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
};
|
| 235 |
|
| 236 |
useEffect(() => {
|
|
@@ -262,9 +281,9 @@ export const ChatImpl = memo(
|
|
| 262 |
};
|
| 263 |
|
| 264 |
const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
|
| 265 |
-
const
|
| 266 |
|
| 267 |
-
if (!
|
| 268 |
return;
|
| 269 |
}
|
| 270 |
|
|
@@ -280,7 +299,7 @@ export const ChatImpl = memo(
|
|
| 280 |
|
| 281 |
if (autoSelectTemplate) {
|
| 282 |
const { template, title } = await selectStarterTemplate({
|
| 283 |
-
message:
|
| 284 |
model,
|
| 285 |
provider,
|
| 286 |
});
|
|
@@ -302,7 +321,7 @@ export const ChatImpl = memo(
|
|
| 302 |
{
|
| 303 |
id: `${new Date().getTime()}`,
|
| 304 |
role: 'user',
|
| 305 |
-
content:
|
| 306 |
},
|
| 307 |
{
|
| 308 |
id: `${new Date().getTime()}`,
|
|
@@ -332,7 +351,7 @@ export const ChatImpl = memo(
|
|
| 332 |
content: [
|
| 333 |
{
|
| 334 |
type: 'text',
|
| 335 |
-
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${
|
| 336 |
},
|
| 337 |
...imageDataList.map((imageData) => ({
|
| 338 |
type: 'image',
|
|
@@ -356,31 +375,20 @@ export const ChatImpl = memo(
|
|
| 356 |
chatStore.setKey('aborted', false);
|
| 357 |
|
| 358 |
if (fileModifications !== undefined) {
|
| 359 |
-
/**
|
| 360 |
-
* If we have file modifications we append a new user message manually since we have to prefix
|
| 361 |
-
* the user input with the file modifications and we don't want the new user input to appear
|
| 362 |
-
* in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to
|
| 363 |
-
* manually reset the input and we'd have to manually pass in file attachments. However, those
|
| 364 |
-
* aren't relevant here.
|
| 365 |
-
*/
|
| 366 |
append({
|
| 367 |
role: 'user',
|
| 368 |
content: [
|
| 369 |
{
|
| 370 |
type: 'text',
|
| 371 |
-
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${
|
| 372 |
},
|
| 373 |
...imageDataList.map((imageData) => ({
|
| 374 |
type: 'image',
|
| 375 |
image: imageData,
|
| 376 |
})),
|
| 377 |
-
] as any,
|
| 378 |
});
|
| 379 |
|
| 380 |
-
/**
|
| 381 |
-
* After sending a new message we reset all modifications since the model
|
| 382 |
-
* should now be aware of all the changes.
|
| 383 |
-
*/
|
| 384 |
workbenchStore.resetAllFileModifications();
|
| 385 |
} else {
|
| 386 |
append({
|
|
@@ -388,20 +396,19 @@ export const ChatImpl = memo(
|
|
| 388 |
content: [
|
| 389 |
{
|
| 390 |
type: 'text',
|
| 391 |
-
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${
|
| 392 |
},
|
| 393 |
...imageDataList.map((imageData) => ({
|
| 394 |
type: 'image',
|
| 395 |
image: imageData,
|
| 396 |
})),
|
| 397 |
-
] as any,
|
| 398 |
});
|
| 399 |
}
|
| 400 |
|
| 401 |
setInput('');
|
| 402 |
Cookies.remove(PROMPT_COOKIE_KEY);
|
| 403 |
|
| 404 |
-
// Add file cleanup here
|
| 405 |
setUploadedFiles([]);
|
| 406 |
setImageDataList([]);
|
| 407 |
|
|
|
|
| 23 |
import { useSearchParams } from '@remix-run/react';
|
| 24 |
import { createSampler } from '~/utils/sampler';
|
| 25 |
import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
|
| 26 |
+
import { logStore } from '~/lib/stores/logs';
|
| 27 |
|
| 28 |
const toastAnimation = cssTransition({
|
| 29 |
enter: 'animated fadeInRight',
|
|
|
|
| 115 |
|
| 116 |
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 117 |
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
| 118 |
+
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
|
| 119 |
+
const [imageDataList, setImageDataList] = useState<string[]>([]);
|
| 120 |
const [searchParams, setSearchParams] = useSearchParams();
|
| 121 |
const [fakeLoading, setFakeLoading] = useState(false);
|
| 122 |
const files = useStore(workbenchStore.files);
|
|
|
|
| 162 |
sendExtraMessageFields: true,
|
| 163 |
onError: (e) => {
|
| 164 |
logger.error('Request failed\n\n', e, error);
|
| 165 |
+
logStore.logError('Chat request failed', e, {
|
| 166 |
+
component: 'Chat',
|
| 167 |
+
action: 'request',
|
| 168 |
+
error: e.message,
|
| 169 |
+
});
|
| 170 |
toast.error(
|
| 171 |
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
|
| 172 |
);
|
|
|
|
| 177 |
|
| 178 |
if (usage) {
|
| 179 |
console.log('Token usage:', usage);
|
| 180 |
+
logStore.logProvider('Chat response completed', {
|
| 181 |
+
component: 'Chat',
|
| 182 |
+
action: 'response',
|
| 183 |
+
model,
|
| 184 |
+
provider: provider.name,
|
| 185 |
+
usage,
|
| 186 |
+
messageLength: message.content.length,
|
| 187 |
+
});
|
| 188 |
}
|
| 189 |
|
| 190 |
logger.debug('Finished streaming');
|
|
|
|
| 243 |
stop();
|
| 244 |
chatStore.setKey('aborted', true);
|
| 245 |
workbenchStore.abortAllActions();
|
| 246 |
+
|
| 247 |
+
logStore.logProvider('Chat response aborted', {
|
| 248 |
+
component: 'Chat',
|
| 249 |
+
action: 'abort',
|
| 250 |
+
model,
|
| 251 |
+
provider: provider.name,
|
| 252 |
+
});
|
| 253 |
};
|
| 254 |
|
| 255 |
useEffect(() => {
|
|
|
|
| 281 |
};
|
| 282 |
|
| 283 |
const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
|
| 284 |
+
const messageContent = messageInput || input;
|
| 285 |
|
| 286 |
+
if (!messageContent?.trim()) {
|
| 287 |
return;
|
| 288 |
}
|
| 289 |
|
|
|
|
| 299 |
|
| 300 |
if (autoSelectTemplate) {
|
| 301 |
const { template, title } = await selectStarterTemplate({
|
| 302 |
+
message: messageContent,
|
| 303 |
model,
|
| 304 |
provider,
|
| 305 |
});
|
|
|
|
| 321 |
{
|
| 322 |
id: `${new Date().getTime()}`,
|
| 323 |
role: 'user',
|
| 324 |
+
content: messageContent,
|
| 325 |
},
|
| 326 |
{
|
| 327 |
id: `${new Date().getTime()}`,
|
|
|
|
| 351 |
content: [
|
| 352 |
{
|
| 353 |
type: 'text',
|
| 354 |
+
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
|
| 355 |
},
|
| 356 |
...imageDataList.map((imageData) => ({
|
| 357 |
type: 'image',
|
|
|
|
| 375 |
chatStore.setKey('aborted', false);
|
| 376 |
|
| 377 |
if (fileModifications !== undefined) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
append({
|
| 379 |
role: 'user',
|
| 380 |
content: [
|
| 381 |
{
|
| 382 |
type: 'text',
|
| 383 |
+
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
|
| 384 |
},
|
| 385 |
...imageDataList.map((imageData) => ({
|
| 386 |
type: 'image',
|
| 387 |
image: imageData,
|
| 388 |
})),
|
| 389 |
+
] as any,
|
| 390 |
});
|
| 391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
workbenchStore.resetAllFileModifications();
|
| 393 |
} else {
|
| 394 |
append({
|
|
|
|
| 396 |
content: [
|
| 397 |
{
|
| 398 |
type: 'text',
|
| 399 |
+
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
|
| 400 |
},
|
| 401 |
...imageDataList.map((imageData) => ({
|
| 402 |
type: 'image',
|
| 403 |
image: imageData,
|
| 404 |
})),
|
| 405 |
+
] as any,
|
| 406 |
});
|
| 407 |
}
|
| 408 |
|
| 409 |
setInput('');
|
| 410 |
Cookies.remove(PROMPT_COOKIE_KEY);
|
| 411 |
|
|
|
|
| 412 |
setUploadedFiles([]);
|
| 413 |
setImageDataList([]);
|
| 414 |
|
|
@@ -6,7 +6,7 @@ import { generateId } from '~/utils/fileUtils';
|
|
| 6 |
import { useState } from 'react';
|
| 7 |
import { toast } from 'react-toastify';
|
| 8 |
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
|
| 9 |
-
import { RepositorySelectionDialog } from '~/components
|
| 10 |
import { classNames } from '~/utils/classNames';
|
| 11 |
import { Button } from '~/components/ui/Button';
|
| 12 |
import type { IChatMetadata } from '~/lib/persistence/db';
|
|
|
|
| 6 |
import { useState } from 'react';
|
| 7 |
import { toast } from 'react-toastify';
|
| 8 |
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
|
| 9 |
+
import { RepositorySelectionDialog } from '~/components/@settings/tabs/connections/components/RepositorySelectionDialog';
|
| 10 |
import { classNames } from '~/utils/classNames';
|
| 11 |
import { Button } from '~/components/ui/Button';
|
| 12 |
import type { IChatMetadata } from '~/lib/persistence/db';
|