From f33ba635e84351d380995a54775cf5b92d22b49a Mon Sep 17 00:00:00 2001 From: Stijnus <72551117+Stijnus@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:33:20 +0100 Subject: [PATCH 01/61] V1 : Release of the new Settings Dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🚀 Release v1.0.0 ## What's Changed 🌟 ### 🎨 UI/UX Improvements - **Dark Mode Support** - Implemented comprehensive dark theme across all components - Enhanced contrast and readability in dark mode - Added smooth theme transitions - Optimized dialog overlays and backdrops ### 🛠️ Settings Panel - **Data Management** - Added chat history export/import functionality - Implemented settings backup and restore - Added secure data deletion with confirmations - Added profile customization options - **Provider Management** - Added comprehensive provider configuration - Implemented URL-configurable providers - Added local model support (Ollama, LMStudio) - Added provider health checks - Added provider status indicators - **Ollama Integration** - Added Ollama Model Manager with real-time updates - Implemented model version tracking - Added bulk update capability - Added progress tracking for model updates - Displays model details (parameter size, quantization) - **GitHub Integration** - Added GitHub connection management - Implemented secure token storage - Added connection state persistence - Real-time connection status updates - Proper error handling and user feedback ### 📊 Event Logging - **System Monitoring** - Added real-time event logging system - Implemented log filtering by type (info, warning, error, debug) - Added log export functionality - Added auto-scroll and search capabilities - Enhanced log visualization with color coding ### 💫 Animations & Interactions - Added smooth page transitions - Implemented loading states with spinners - Added micro-interactions for better feedback - Enhanced button hover and active states - Added motion effects for UI elements ### 🔐 Security Features - Secure token storage - Added confirmation dialogs for destructive actions - Implemented data validation - Added file size and type validation - Secure connection management ### ♿️ Accessibility - Improved keyboard navigation - Enhanced screen reader support - Added ARIA labels and descriptions - Implemented focus management - Added proper dialog accessibility ### 🎯 Developer Experience - Added comprehensive debug information - Implemented system status monitoring - Added version control integration - Enhanced error handling and reporting - Added detailed logging system --- ## 🔧 Technical Details - **Frontend Stack** - React 18 with TypeScript - Framer Motion for animations - TailwindCSS for styling - Radix UI for accessible components - **State Management** - Local storage for persistence - React hooks for state - Custom stores for global state - **API Integration** - GitHub API integration - Ollama API integration - Provider API management - Error boundary implementation ## 📝 Notes - Initial release focusing on core functionality and user experience - Enhanced dark mode support across all components - Improved accessibility and keyboard navigation - Added comprehensive logging and debugging tools - Implemented robust error handling and user feedback --- .windsurf/config.json | 39 + .windsurf/rules.json | 103 +++ app/components/settings/Settings.module.scss | 63 -- app/components/settings/SettingsWindow.tsx | 374 +++++++--- .../settings/connections/ConnectionsTab.tsx | 285 +++++--- app/components/settings/data/DataTab.tsx | 670 +++++++++--------- app/components/settings/debug/DebugTab.tsx | 248 ++++--- .../settings/event-logs/EventLogsTab.tsx | 334 +++++++-- .../settings/features/FeaturesTab.tsx | 335 +++++++-- .../settings/profile/ProfileTab.tsx | 399 +++++++++++ .../settings/providers/OllamaModelUpdater.tsx | 295 ++++++++ .../settings/providers/ProvidersTab.tsx | 480 ++++++++++--- app/components/settings/settings.styles.ts | 37 + app/components/settings/settings.types.ts | 53 ++ app/components/ui/Dialog.tsx | 134 ++-- app/components/ui/Separator.tsx | 22 + app/lib/stores/logs.ts | 5 + package.json | 9 +- pnpm-lock.yaml | 171 ++++- uno.config.ts | 68 +- 20 files changed, 3107 insertions(+), 1017 deletions(-) create mode 100644 .windsurf/config.json create mode 100644 .windsurf/rules.json delete mode 100644 app/components/settings/Settings.module.scss create mode 100644 app/components/settings/profile/ProfileTab.tsx create mode 100644 app/components/settings/providers/OllamaModelUpdater.tsx create mode 100644 app/components/settings/settings.styles.ts create mode 100644 app/components/settings/settings.types.ts create mode 100644 app/components/ui/Separator.tsx diff --git a/.windsurf/config.json b/.windsurf/config.json new file mode 100644 index 0000000000..27848d341d --- /dev/null +++ b/.windsurf/config.json @@ -0,0 +1,39 @@ +{ + "enabled": true, + "rulesPath": ".windsurf/rules.json", + "integration": { + "ide": { + "cursor": true, + "vscode": true + }, + "autoApply": true, + "notifications": true, + "autoFix": { + "enabled": true, + "onSave": true, + "formatOnSave": true, + "suggestImports": true, + "suggestComponents": true + }, + "suggestions": { + "inline": true, + "quickFix": true, + "codeActions": true, + "snippets": true + } + }, + "features": { + "codeCompletion": true, + "linting": true, + "formatting": true, + "importValidation": true, + "dependencyChecks": true, + "uiStandardization": true + }, + "hooks": { + "preCommit": true, + "prePush": true, + "onFileCreate": true, + "onImportAdd": true + } +} diff --git a/.windsurf/rules.json b/.windsurf/rules.json new file mode 100644 index 0000000000..a0008b1ef5 --- /dev/null +++ b/.windsurf/rules.json @@ -0,0 +1,103 @@ +{ + "version": "1.0", + "rules": { + "fileTypes": { + "typescript": ["ts", "tsx"], + "javascript": ["js", "jsx", "mjs", "cjs"], + "json": ["json"], + "markdown": ["md"], + "css": ["css"], + "dockerfile": ["Dockerfile"] + }, + "formatting": { + "typescript": { + "indentSize": 2, + "useTabs": false, + "maxLineLength": 100, + "semicolons": true, + "quotes": "single", + "trailingComma": "es5" + }, + "javascript": { + "indentSize": 2, + "useTabs": false, + "maxLineLength": 100, + "semicolons": true, + "quotes": "single", + "trailingComma": "es5" + } + }, + "linting": { + "typescript": { + "noUnusedVariables": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noConsole": "warn" + } + }, + "dependencies": { + "nodeVersion": ">=18.18.0", + "packageManager": "pnpm", + "requiredFiles": ["package.json", "tsconfig.json", ".env.example"] + }, + "git": { + "ignoredPaths": ["node_modules", "build", ".env", ".env.local"], + "protectedBranches": ["main", "master"] + }, + "testing": { + "framework": "vitest", + "coverage": { + "statements": 70, + "branches": 70, + "functions": 70, + "lines": 70 + } + }, + "security": { + "secrets": { + "patterns": ["API_KEY", "SECRET", "PASSWORD", "TOKEN"], + "locations": [".env", ".env.local"] + } + }, + "commands": { + "dev": "pnpm dev", + "build": "pnpm build", + "test": "pnpm test", + "lint": "pnpm lint", + "typecheck": "pnpm typecheck" + }, + "codeQuality": { + "imports": { + "validateImports": true, + "checkPackageAvailability": true, + "requireExactVersions": true, + "preventUnusedImports": true + }, + "fileManagement": { + "preventUnnecessaryFiles": true, + "requireFileJustification": true, + "checkExistingImplementations": true + }, + "dependencies": { + "autoInstallMissing": false, + "validateVersionCompatibility": true, + "checkPackageJson": true + } + }, + "uiStandards": { + "styling": { + "framework": "tailwind", + "preferredIconSets": ["@iconify-json/ph", "@iconify-json/svg-spinners"], + "colorScheme": { + "useSystemPreference": true, + "supportDarkMode": true + }, + "components": { + "preferModern": true, + "accessibility": true, + "responsive": true + } + } + } + } +} diff --git a/app/components/settings/Settings.module.scss b/app/components/settings/Settings.module.scss deleted file mode 100644 index 639cbbc57b..0000000000 --- a/app/components/settings/Settings.module.scss +++ /dev/null @@ -1,63 +0,0 @@ -.settings-tabs { - button { - width: 100%; - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - border-radius: 0.5rem; - text-align: left; - font-size: 0.875rem; - transition: all 0.2s; - margin-bottom: 0.5rem; - - &.active { - background: var(--bolt-elements-button-primary-background); - color: var(--bolt-elements-textPrimary); - } - - &:not(.active) { - background: var(--bolt-elements-bg-depth-3); - color: var(--bolt-elements-textPrimary); - - &:hover { - background: var(--bolt-elements-button-primary-backgroundHover); - } - } - } -} - -.settings-button { - background-color: var(--bolt-elements-button-primary-background); - color: var(--bolt-elements-textPrimary); - border-radius: 0.5rem; - padding: 0.5rem 1rem; - transition: background-color 0.2s; - - &:hover { - background-color: var(--bolt-elements-button-primary-backgroundHover); - } -} - -.settings-danger-area { - background-color: transparent; - color: var(--bolt-elements-textPrimary); - border-radius: 0.5rem; - padding: 1rem; - margin-bottom: 1rem; - border-style: solid; - border-color: var(--bolt-elements-button-danger-backgroundHover); - border-width: thin; - - button { - background-color: var(--bolt-elements-button-danger-background); - color: var(--bolt-elements-button-danger-text); - border-radius: 0.5rem; - padding: 0.5rem 1rem; - transition: background-color 0.2s; - - &:hover { - background-color: var(--bolt-elements-button-danger-backgroundHover); - } - } -} diff --git a/app/components/settings/SettingsWindow.tsx b/app/components/settings/SettingsWindow.tsx index f53d547c0f..6d19c99fd4 100644 --- a/app/components/settings/SettingsWindow.tsx +++ b/app/components/settings/SettingsWindow.tsx @@ -1,10 +1,11 @@ import * as RadixDialog from '@radix-ui/react-dialog'; -import { motion } from 'framer-motion'; -import { useState, type ReactElement } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useState } from 'react'; import { classNames } from '~/utils/classNames'; -import { DialogTitle, dialogVariants, dialogBackdropVariants } from '~/components/ui/Dialog'; -import { IconButton } from '~/components/ui/IconButton'; -import styles from './Settings.module.scss'; +import { DialogTitle } from '~/components/ui/Dialog'; +import type { SettingCategory, TabType } from './settings.types'; +import { categoryLabels, categoryIcons } from './settings.types'; +import ProfileTab from './profile/ProfileTab'; import ProvidersTab from './providers/ProvidersTab'; import { useSettings } from '~/lib/hooks/useSettings'; import FeaturesTab from './features/FeaturesTab'; @@ -18,110 +19,281 @@ interface SettingsProps { onClose: () => void; } -type TabType = 'data' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection'; - export const SettingsWindow = ({ open, onClose }: SettingsProps) => { const { debug, eventLogs } = useSettings(); - const [activeTab, setActiveTab] = useState('data'); - - const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [ - { id: 'data', label: 'Data', icon: 'i-ph:database', component: }, - { id: 'providers', label: 'Providers', icon: 'i-ph:key', component: }, - { id: 'connection', label: 'Connection', icon: 'i-ph:link', component: }, - { id: 'features', label: 'Features', icon: 'i-ph:star', component: }, - ...(debug - ? [ - { - id: 'debug' as TabType, - label: 'Debug Tab', - icon: 'i-ph:bug', - component: , - }, - ] - : []), - ...(eventLogs - ? [ - { - id: 'event-logs' as TabType, - label: 'Event Logs', - icon: 'i-ph:list-bullets', - component: , - }, - ] - : []), - ]; + const [searchQuery, setSearchQuery] = useState(''); + const [activeTab, setActiveTab] = useState(null); + + const settingItems = [ + { + id: 'profile' as const, + label: 'Profile Settings', + icon: 'i-ph:user-circle', + category: 'profile' as const, + description: 'Manage your personal information and preferences', + component: () => , + keywords: ['profile', 'account', 'avatar', 'email', 'name', 'theme', 'notifications'], + }, + + { + id: 'data' as const, + label: 'Data Management', + icon: 'i-ph:database', + category: 'file_sharing' as const, + description: 'Manage your chat history and application data', + component: () => , + keywords: ['data', 'export', 'import', 'backup', 'delete'], + }, + + { + id: 'providers' as const, + label: 'Providers', + icon: 'i-ph:key', + category: 'file_sharing' as const, + description: 'Configure AI providers and API keys', + component: () => , + keywords: ['api', 'keys', 'providers', 'configuration'], + }, + + { + id: 'connection' as const, + label: 'Connection', + icon: 'i-ph:link', + category: 'connectivity' as const, + description: 'Manage network and connection settings', + component: () => , + keywords: ['network', 'connection', 'proxy', 'ssl'], + }, + + { + id: 'features' as const, + label: 'Features', + icon: 'i-ph:star', + category: 'system' as const, + description: 'Configure application features and preferences', + component: () => , + keywords: ['features', 'settings', 'options'], + }, + ] as const; + + const debugItems = debug + ? [ + { + id: 'debug' as const, + label: 'Debug', + icon: 'i-ph:bug', + category: 'system' as const, + description: 'Advanced debugging tools and options', + component: () => , + keywords: ['debug', 'logs', 'developer'], + }, + ] + : []; + + const eventLogItems = eventLogs + ? [ + { + id: 'event-logs' as const, + label: 'Event Logs', + icon: 'i-ph:list-bullets', + category: 'system' as const, + description: 'View system events and application logs', + component: () => , + keywords: ['logs', 'events', 'history'], + }, + ] + : []; + + const allSettingItems = [...settingItems, ...debugItems, ...eventLogItems]; + + const filteredItems = allSettingItems.filter( + (item) => + item.label.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description?.toLowerCase().includes(searchQuery.toLowerCase()) || + item.keywords?.some((keyword) => keyword.toLowerCase().includes(searchQuery.toLowerCase())), + ); + + const groupedItems = filteredItems.reduce( + (acc, item) => { + if (!acc[item.category]) { + acc[item.category] = allSettingItems.filter((i) => i.category === item.category); + } + + return acc; + }, + {} as Record, + ); + + const handleBackToDashboard = () => { + setActiveTab(null); + onClose(); + }; + + const activeTabItem = allSettingItems.find((item) => item.id === activeTab); return ( - - - - - -
-
- - Settings - - {tabs.map((tab) => ( - - ))} - - - - - - +
+
+
+ + Bolt Control Panel + +
+
+
+ setSearchQuery(e.target.value)} + className={classNames( + 'w-full h-10 pl-10 pr-4 rounded-lg text-sm', + 'bg-[#F8F8F8] dark:bg-[#1A1A1A]', + 'border border-[#E5E5E5] dark:border-[#333333]', + 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', + 'focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all', + )} + /> +
+
+
+
+ +
+
+ +
+
+ {(Object.keys(groupedItems) as SettingCategory[]).map((category) => ( +
+
+
+

+ {categoryLabels[category]} +

+
+
+ {groupedItems[category].map((item) => ( + + ))} +
+
+ ))} +
+
+ + )} + + + +
); diff --git a/app/components/settings/connections/ConnectionsTab.tsx b/app/components/settings/connections/ConnectionsTab.tsx index 4b89022e7e..3188374596 100644 --- a/app/components/settings/connections/ConnectionsTab.tsx +++ b/app/components/settings/connections/ConnectionsTab.tsx @@ -1,150 +1,207 @@ import React, { useState, useEffect } from 'react'; -import { toast } from 'react-toastify'; -import Cookies from 'js-cookie'; import { logStore } from '~/lib/stores/logs'; +import { classNames } from '~/utils/classNames'; +import { motion } from 'framer-motion'; +import { toast } from 'react-toastify'; interface GitHubUserResponse { login: string; - id: number; - [key: string]: any; // for other properties we don't explicitly need + avatar_url: string; + html_url: string; } -export default function ConnectionsTab() { - const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || ''); - const [githubToken, setGithubToken] = useState(Cookies.get('githubToken') || ''); - const [isConnected, setIsConnected] = useState(false); - const [isVerifying, setIsVerifying] = useState(false); +interface GitHubConnection { + user: GitHubUserResponse | null; + token: string; +} +export default function ConnectionsTab() { + const [connection, setConnection] = useState({ + user: null, + token: '', + }); + const [isLoading, setIsLoading] = useState(true); + const [isConnecting, setIsConnecting] = useState(false); + + // Load saved connection on mount useEffect(() => { - // Check if credentials exist and verify them - if (githubUsername && githubToken) { - verifyGitHubCredentials(); + const savedConnection = localStorage.getItem('github_connection'); + + if (savedConnection) { + setConnection(JSON.parse(savedConnection)); } - }, []); - const verifyGitHubCredentials = async () => { - setIsVerifying(true); + setIsLoading(false); + }, []); + const fetchGithubUser = async (token: string) => { try { + setIsConnecting(true); + const response = await fetch('https://api.github.com/user', { headers: { - Authorization: `Bearer ${githubToken}`, + Authorization: `Bearer ${token}`, }, }); - if (response.ok) { - const data = (await response.json()) as GitHubUserResponse; - - if (data.login === githubUsername) { - setIsConnected(true); - return true; - } + if (!response.ok) { + throw new Error('Invalid token or unauthorized'); } - setIsConnected(false); + const data = (await response.json()) as GitHubUserResponse; + const newConnection = { user: data, token }; - return false; + // Save connection + localStorage.setItem('github_connection', JSON.stringify(newConnection)); + setConnection(newConnection); + toast.success('Successfully connected to GitHub'); } catch (error) { - console.error('Error verifying GitHub credentials:', error); - setIsConnected(false); - - return false; + logStore.logError('Failed to authenticate with GitHub', { error }); + toast.error('Failed to connect to GitHub'); + setConnection({ user: null, token: '' }); } finally { - setIsVerifying(false); + setIsConnecting(false); } }; - const handleSaveConnection = async () => { - if (!githubUsername || !githubToken) { - toast.error('Please provide both GitHub username and token'); - return; - } - - setIsVerifying(true); - - const isValid = await verifyGitHubCredentials(); - - if (isValid) { - Cookies.set('githubUsername', githubUsername); - Cookies.set('githubToken', githubToken); - logStore.logSystem('GitHub connection settings updated', { - username: githubUsername, - hasToken: !!githubToken, - }); - toast.success('GitHub credentials verified and saved successfully!'); - Cookies.set('git:github.com', JSON.stringify({ username: githubToken, password: 'x-oauth-basic' })); - setIsConnected(true); - } else { - toast.error('Invalid GitHub credentials. Please check your username and token.'); - } + const handleConnect = async (event: React.FormEvent) => { + event.preventDefault(); + await fetchGithubUser(connection.token); }; const handleDisconnect = () => { - Cookies.remove('githubUsername'); - Cookies.remove('githubToken'); - Cookies.remove('git:github.com'); - setGithubUsername(''); - setGithubToken(''); - setIsConnected(false); - logStore.logSystem('GitHub connection removed'); - toast.success('GitHub connection removed successfully!'); + localStorage.removeItem('github_connection'); + setConnection({ user: null, token: '' }); + toast.success('Disconnected from GitHub'); }; - return ( -
-

GitHub Connection

-
-
- - setGithubUsername(e.target.value)} - disabled={isVerifying} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50" - /> -
-
- - setGithubToken(e.target.value)} - disabled={isVerifying} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50" - /> + if (isLoading) { + return ( +
+
+
+ Loading...
-
- {!isConnected ? ( - - ) : ( - - )} - {isConnected && ( - -
- Connected to GitHub - - )} + ); + } + + return ( +
+ {/* Header */} + +
+

Connection Settings

+ +

+ Manage your external service connections and integrations +

+ +
+ {/* GitHub Connection */} + +
+
+
+

GitHub Connection

+
+ +
+
+ + +
+ +
+ + setConnection((prev) => ({ ...prev, token: e.target.value }))} + disabled={isConnecting || !!connection.user} + placeholder="Enter your GitHub token" + className={classNames( + 'w-full px-3 py-2 rounded-lg text-sm', + 'bg-[#F8F8F8] dark:bg-[#1A1A1A]', + 'border border-[#E5E5E5] dark:border-[#333333]', + 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', + 'focus:outline-none focus:ring-1 focus:ring-purple-500', + 'disabled:opacity-50', + )} + /> +
+
+ +
+ {!connection.user ? ( + + ) : ( + + )} + + {connection.user && ( + +
+ Connected to GitHub + + )} +
+
+
); diff --git a/app/components/settings/data/DataTab.tsx b/app/components/settings/data/DataTab.tsx index 9219d01557..9883507052 100644 --- a/app/components/settings/data/DataTab.tsx +++ b/app/components/settings/data/DataTab.tsx @@ -1,388 +1,422 @@ -import React, { useState } from 'react'; -import { useNavigate } from '@remix-run/react'; -import Cookies from 'js-cookie'; +import { useState, useRef } from 'react'; +import { motion } from 'framer-motion'; import { toast } from 'react-toastify'; -import { db, deleteById, getAll, setMessages } from '~/lib/persistence'; -import { logStore } from '~/lib/stores/logs'; -import { classNames } from '~/utils/classNames'; -import type { Message } from 'ai'; - -// List of supported providers that can have API keys -const API_KEY_PROVIDERS = [ - 'Anthropic', - 'OpenAI', - 'Google', - 'Groq', - 'HuggingFace', - 'OpenRouter', - 'Deepseek', - 'Mistral', - 'OpenAILike', - 'Together', - 'xAI', - 'Perplexity', - 'Cohere', - 'AzureOpenAI', - 'AmazonBedrock', -] as const; - -interface ApiKeys { - [key: string]: string; -} +import { DialogRoot, DialogClose, Dialog, DialogTitle } from '~/components/ui/Dialog'; +import { db, getAll } from '~/lib/persistence'; export default function DataTab() { - const navigate = useNavigate(); + const [isDownloadingTemplate, setIsDownloadingTemplate] = useState(false); + const [isImportingKeys, setIsImportingKeys] = useState(false); + const [isResetting, setIsResetting] = useState(false); const [isDeleting, setIsDeleting] = useState(false); - - const downloadAsJson = (data: any, filename: string) => { - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }; + const [showResetInlineConfirm, setShowResetInlineConfirm] = useState(false); + const [showDeleteInlineConfirm, setShowDeleteInlineConfirm] = useState(false); + const fileInputRef = useRef(null); + const apiKeyFileInputRef = useRef(null); const handleExportAllChats = async () => { - if (!db) { - const error = new Error('Database is not available'); - logStore.logError('Failed to export chats - DB unavailable', error); - toast.error('Database is not available'); - - return; - } - try { + if (!db) { + throw new Error('Database not initialized'); + } + + // Get all chats from IndexedDB const allChats = await getAll(db); const exportData = { chats: allChats, exportDate: new Date().toISOString(), }; - downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`); - logStore.logSystem('Chats exported successfully', { count: allChats.length }); + // Download as JSON + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `bolt-chats-${new Date().toISOString()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success('Chats exported successfully'); } catch (error) { - logStore.logError('Failed to export chats', error); + console.error('Export error:', error); toast.error('Failed to export chats'); - console.error(error); } }; - const handleDeleteAllChats = async () => { - const confirmDelete = window.confirm('Are you sure you want to delete all chats? This action cannot be undone.'); - - if (!confirmDelete) { - return; - } - - if (!db) { - const error = new Error('Database is not available'); - logStore.logError('Failed to delete chats - DB unavailable', error); - toast.error('Database is not available'); - - return; - } - + const handleExportSettings = () => { try { - setIsDeleting(true); + const settings = { + userProfile: localStorage.getItem('bolt_user_profile'), + settings: localStorage.getItem('bolt_settings'), + exportDate: new Date().toISOString(), + }; - const allChats = await getAll(db); - await Promise.all(allChats.map((chat) => deleteById(db!, chat.id))); - logStore.logSystem('All chats deleted successfully', { count: allChats.length }); - toast.success('All chats deleted successfully'); - navigate('/', { replace: true }); + const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `bolt-settings-${new Date().toISOString()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + toast.success('Settings exported successfully'); } catch (error) { - logStore.logError('Failed to delete chats', error); - toast.error('Failed to delete chats'); - console.error(error); - } finally { - setIsDeleting(false); + console.error('Export error:', error); + toast.error('Failed to export settings'); } }; - const handleExportSettings = () => { - const settings = { - providers: Cookies.get('providers'), - isDebugEnabled: Cookies.get('isDebugEnabled'), - isEventLogsEnabled: Cookies.get('isEventLogsEnabled'), - isLocalModelsEnabled: Cookies.get('isLocalModelsEnabled'), - promptId: Cookies.get('promptId'), - isLatestBranch: Cookies.get('isLatestBranch'), - commitHash: Cookies.get('commitHash'), - eventLogs: Cookies.get('eventLogs'), - selectedModel: Cookies.get('selectedModel'), - selectedProvider: Cookies.get('selectedProvider'), - githubUsername: Cookies.get('githubUsername'), - githubToken: Cookies.get('githubToken'), - bolt_theme: localStorage.getItem('bolt_theme'), - }; - - downloadAsJson(settings, 'bolt-settings.json'); - toast.success('Settings exported successfully'); - }; - - const handleImportSettings = (event: React.ChangeEvent) => { + const handleImportSettings = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) { return; } - const reader = new FileReader(); - - reader.onload = (e) => { - try { - const settings = JSON.parse(e.target?.result as string); - - Object.entries(settings).forEach(([key, value]) => { - if (key === 'bolt_theme') { - if (value) { - localStorage.setItem(key, value as string); - } - } else if (value) { - Cookies.set(key, value as string); - } - }); - - toast.success('Settings imported successfully. Please refresh the page for changes to take effect.'); - } catch (error) { - toast.error('Failed to import settings. Make sure the file is a valid JSON file.'); - console.error('Failed to import settings:', error); - } - }; - reader.readAsText(file); - event.target.value = ''; - }; + try { + const content = await file.text(); + const settings = JSON.parse(content); - const handleExportApiKeyTemplate = () => { - const template: ApiKeys = {}; - API_KEY_PROVIDERS.forEach((provider) => { - template[`${provider}_API_KEY`] = ''; - }); + if (settings.userProfile) { + localStorage.setItem('bolt_user_profile', settings.userProfile); + } - template.OPENAI_LIKE_API_BASE_URL = ''; - template.LMSTUDIO_API_BASE_URL = ''; - template.OLLAMA_API_BASE_URL = ''; - template.TOGETHER_API_BASE_URL = ''; + if (settings.settings) { + localStorage.setItem('bolt_settings', settings.settings); + } - downloadAsJson(template, 'api-keys-template.json'); - toast.success('API keys template exported successfully'); + window.location.reload(); // Reload to apply settings + toast.success('Settings imported successfully'); + } catch (error) { + console.error('Import error:', error); + toast.error('Failed to import settings'); + } }; - const handleImportApiKeys = (event: React.ChangeEvent) => { + const handleImportAPIKeys = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) { return; } - const reader = new FileReader(); - - reader.onload = (e) => { - try { - const apiKeys = JSON.parse(e.target?.result as string); - let importedCount = 0; - const consolidatedKeys: Record = {}; + setIsImportingKeys(true); - API_KEY_PROVIDERS.forEach((provider) => { - const keyName = `${provider}_API_KEY`; - - if (apiKeys[keyName]) { - consolidatedKeys[provider] = apiKeys[keyName]; - importedCount++; - } - }); - - if (importedCount > 0) { - // Store all API keys in a single cookie as JSON - Cookies.set('apiKeys', JSON.stringify(consolidatedKeys)); - - // Also set individual cookies for backward compatibility - Object.entries(consolidatedKeys).forEach(([provider, key]) => { - Cookies.set(`${provider}_API_KEY`, key); - }); - - toast.success(`Successfully imported ${importedCount} API keys/URLs. Refreshing page to apply changes...`); + try { + const content = await file.text(); + const keys = JSON.parse(content); - // Reload the page after a short delay to allow the toast to be seen - setTimeout(() => { - window.location.reload(); - }, 1500); - } else { - toast.warn('No valid API keys found in the file'); + // Validate and save each key + Object.entries(keys).forEach(([key, value]) => { + if (typeof value !== 'string') { + throw new Error(`Invalid value for key: ${key}`); } - // Set base URLs if they exist - ['OPENAI_LIKE_API_BASE_URL', 'LMSTUDIO_API_BASE_URL', 'OLLAMA_API_BASE_URL', 'TOGETHER_API_BASE_URL'].forEach( - (baseUrl) => { - if (apiKeys[baseUrl]) { - Cookies.set(baseUrl, apiKeys[baseUrl]); - } - }, - ); - } catch (error) { - toast.error('Failed to import API keys. Make sure the file is a valid JSON file.'); - console.error('Failed to import API keys:', error); - } - }; - reader.readAsText(file); - event.target.value = ''; - }; + localStorage.setItem(`bolt_${key.toLowerCase()}`, value); + }); - const processChatData = ( - data: any, - ): Array<{ - id: string; - messages: Message[]; - description: string; - urlId?: string; - }> => { - // Handle Bolt standard format (single chat) - if (data.messages && Array.isArray(data.messages)) { - const chatId = crypto.randomUUID(); - return [ - { - id: chatId, - messages: data.messages, - description: data.description || 'Imported Chat', - urlId: chatId, - }, - ]; - } + toast.success('API keys imported successfully'); + } catch (error) { + console.error('Error importing API keys:', error); + toast.error('Failed to import API keys'); + } finally { + setIsImportingKeys(false); - // Handle Bolt export format (multiple chats) - if (data.chats && Array.isArray(data.chats)) { - return data.chats.map((chat: { id?: string; messages: Message[]; description?: string; urlId?: string }) => ({ - id: chat.id || crypto.randomUUID(), - messages: chat.messages, - description: chat.description || 'Imported Chat', - urlId: chat.urlId, - })); + if (apiKeyFileInputRef.current) { + apiKeyFileInputRef.current.value = ''; + } } - - console.error('No matching format found for:', data); - throw new Error('Unsupported chat format'); }; - const handleImportChats = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json'; - - input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; + const handleDownloadTemplate = () => { + setIsDownloadingTemplate(true); - if (!file || !db) { - toast.error('Something went wrong'); - return; - } + try { + const template = { + Anthropic_API_KEY: '', + OpenAI_API_KEY: '', + Google_API_KEY: '', + Groq_API_KEY: '', + HuggingFace_API_KEY: '', + OpenRouter_API_KEY: '', + Deepseek_API_KEY: '', + Mistral_API_KEY: '', + OpenAILike_API_KEY: '', + Together_API_KEY: '', + xAI_API_KEY: '', + Perplexity_API_KEY: '', + Cohere_API_KEY: '', + AzureOpenAI_API_KEY: '', + OPENAI_LIKE_API_BASE_URL: '', + LMSTUDIO_API_BASE_URL: '', + OLLAMA_API_BASE_URL: '', + TOGETHER_API_BASE_URL: '', + }; - try { - const content = await file.text(); - const data = JSON.parse(content); - const chatsToImport = processChatData(data); + const blob = new Blob([JSON.stringify(template, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'bolt-api-keys-template.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + toast.success('Template downloaded successfully'); + } catch (error) { + console.error('Error downloading template:', error); + toast.error('Failed to download template'); + } finally { + setIsDownloadingTemplate(false); + } + }; - for (const chat of chatsToImport) { - await setMessages(db, chat.id, chat.messages, chat.urlId, chat.description); - } + const handleResetSettings = async () => { + setIsResetting(true); - logStore.logSystem('Chats imported successfully', { count: chatsToImport.length }); - toast.success(`Successfully imported ${chatsToImport.length} chat${chatsToImport.length > 1 ? 's' : ''}`); - window.location.reload(); - } catch (error) { - if (error instanceof Error) { - logStore.logError('Failed to import chats:', error); - toast.error('Failed to import chats: ' + error.message); - } else { - toast.error('Failed to import chats'); - } + try { + // Clear all stored settings + localStorage.removeItem('bolt_user_profile'); + localStorage.removeItem('bolt_settings'); + localStorage.removeItem('bolt_chat_history'); + + // Reload the page to apply reset + window.location.reload(); + toast.success('Settings reset successfully'); + } catch (error) { + console.error('Reset error:', error); + toast.error('Failed to reset settings'); + } finally { + setIsResetting(false); + } + }; - console.error(error); - } - }; + const handleDeleteAllChats = async () => { + setIsDeleting(true); - input.click(); + try { + // Clear chat history + localStorage.removeItem('bolt_chat_history'); + toast.success('Chat history deleted successfully'); + } catch (error) { + console.error('Delete error:', error); + toast.error('Failed to delete chat history'); + } finally { + setIsDeleting(false); + } }; return ( -
-
-

Data Management

-
-
-
-

Chat History

-

Export or delete all your chat history.

-
- - - -
+
+ + {/* Reset Settings Dialog */} + + +
+
+
+ Reset All Settings?
- -
-

Settings Backup

-

- Export your settings to a JSON file or import settings from a previously exported file. -

-
- - -
+ + + {isResetting ? ( +
+ ) : ( +
+ )} + Reset Settings +
- -
-

API Keys Management

-

- Import API keys from a JSON file or download a template to fill in your keys. -

-
-
+
+
+ + {/* Delete Confirmation Dialog */} + + +
+
+
+ Delete All Chats? +
+

+ This will permanently delete all your chat history. This action cannot be undone. +

+
+ + - -
+ + + {isDeleting ? ( +
+ ) : ( +
+ )} + Delete All +
+
+
+ + {/* Chat History Section */} + +
+
+

Chat History

+
+

Export or delete all your chat history.

+
+ +
+ Export All Chats + + setShowDeleteInlineConfirm(true)} + > +
+ Delete All Chats + +
+ + + {/* Settings Backup Section */} + +
+
+

Settings Backup

+
+

+ Export your settings to a JSON file or import settings from a previously exported file. +

+
+ +
+ Export Settings + + fileInputRef.current?.click()} + > +
+ Import Settings + + setShowResetInlineConfirm(true)} + > +
+ Reset Settings + +
+ + + {/* API Keys Management Section */} + +
+
+

API Keys Management

+
+

+ Import API keys from a JSON file or download a template to fill in your keys. +

+
+ + + {isDownloadingTemplate ? ( +
+ ) : ( +
+ )} + Download Template + + apiKeyFileInputRef.current?.click()} + disabled={isImportingKeys} + > + {isImportingKeys ? ( +
+ ) : ( +
+ )} + Import API Keys +
-
+
); } diff --git a/app/components/settings/debug/DebugTab.tsx b/app/components/settings/debug/DebugTab.tsx index aca22e10bd..e8e74e601a 100644 --- a/app/components/settings/debug/DebugTab.tsx +++ b/app/components/settings/debug/DebugTab.tsx @@ -2,6 +2,9 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useSettings } from '~/lib/hooks/useSettings'; import { toast } from 'react-toastify'; import { providerBaseUrlEnvKeys } from '~/utils/constants'; +import { motion } from 'framer-motion'; +import { classNames } from '~/utils/classNames'; +import { settingsStyles } from '~/components/settings/settings.styles'; interface ProviderStatus { name: string; @@ -438,107 +441,182 @@ export default function DebugTab() { }, [activeProviders, systemInfo, isLatestBranch]); return ( -
+
-

Debug Information

+
+
+

Debug Information

+
- - + {isCheckingUpdate ? ( + <> +
+ Checking... + + ) : ( + <> +
+ Check for Updates + + )} +
{updateMessage && ( -
-

{updateMessage}

- {updateMessage.includes('Update available') && ( -
-

To update:

-
    -
  1. - Pull the latest changes:{' '} - git pull upstream main -
  2. -
  3. - Install any new dependencies:{' '} - pnpm install -
  4. -
  5. Restart the application
  6. -
+
+
+
+

{updateMessage}

+ {updateMessage.includes('Update available') && ( +
+

To update:

+
    +
  1. +
    +
    + Pull the latest changes:{' '} + + git pull upstream main + +
    +
  2. +
  3. +
    +
    + Install any new dependencies:{' '} + + pnpm install + +
    +
  4. +
  5. +
    +
    + Restart the application +
    +
  6. +
+
+ )}
- )} -
+
+ )}
-
-

System Information

-
+ +
+
+

System Information

+
+
-

Operating System

+
+
+

Operating System

+

{systemInfo.os}

-

Device Type

+
+
+

Device Type

+

{systemInfo.deviceType}

-

Browser

+
+
+

Browser

+

{systemInfo.browser}

-

Display

+
+
+

Display

+

{systemInfo.screen} ({systemInfo.colorDepth}) @{systemInfo.pixelRatio}x

-

Connection

-

+

+
+

Connection

+
+
- + {systemInfo.online ? 'Online' : 'Offline'} -

-
-
-

Screen Resolution

-

{systemInfo.screen}

+
-

Language

+
+
+

Language

+

{systemInfo.language}

-

Timezone

+
+
+

Timezone

+

{systemInfo.timezone}

-

CPU Cores

+
+
+

CPU Cores

+

{systemInfo.cores}

-
-

Version

+
+
+
+

Version

+

{connitJson.commit.slice(0, 7)} @@ -546,22 +624,31 @@ export default function DebugTab() {

+ + + + +
+
+

Local LLM Status

-
- -
-

Local LLM Status

-
-
+ +
{activeProviders.map((provider) => ( -
+
@@ -575,17 +662,21 @@ export default function DebugTab() {
{provider.enabled ? 'Enabled' : 'Disabled'} {provider.enabled && ( {provider.isRunning ? 'Running' : 'Not Running'} @@ -593,31 +684,28 @@ export default function DebugTab() {
-
- {/* Status Details */} +
- + Last checked: {new Date(provider.lastChecked).toLocaleTimeString()} {provider.responseTime && ( - + Response time: {Math.round(provider.responseTime)}ms )}
- {/* Error Message */} {provider.error && ( -
+
Error: {provider.error}
)} - {/* Connection Info */} {provider.url && ( -
+
Endpoints checked: -
    +
    • {provider.url} (root)
    • {provider.url}/api/health
    • {provider.url}/v1/models
    • @@ -631,8 +719,8 @@ export default function DebugTab() {
      No local LLMs configured
      )}
-
-
+ +
); diff --git a/app/components/settings/event-logs/EventLogsTab.tsx b/app/components/settings/event-logs/EventLogsTab.tsx index 5c1ed44a9e..da092243e1 100644 --- a/app/components/settings/event-logs/EventLogsTab.tsx +++ b/app/components/settings/event-logs/EventLogsTab.tsx @@ -1,22 +1,27 @@ -import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react'; import { useSettings } from '~/lib/hooks/useSettings'; import { toast } from 'react-toastify'; import { Switch } from '~/components/ui/Switch'; import { logStore, type LogEntry } from '~/lib/stores/logs'; import { useStore } from '@nanostores/react'; import { classNames } from '~/utils/classNames'; +import { motion } from 'framer-motion'; +import { settingsStyles } from '~/components/settings/settings.styles'; export default function EventLogsTab() { const {} = useSettings(); const showLogs = useStore(logStore.showLogs); + const logs = useStore(logStore.logs); const [logLevel, setLogLevel] = useState('info'); const [autoScroll, setAutoScroll] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [, forceUpdate] = useState({}); + const logsContainerRef = useRef(null); + const [isScrolledToBottom, setIsScrolledToBottom] = useState(true); const filteredLogs = useMemo(() => { - const logs = logStore.getLogs(); - return logs.filter((log) => { + const allLogs = Object.values(logs); + const filtered = allLogs.filter((log) => { const matchesLevel = !logLevel || log.level === logLevel || logLevel === 'all'; const matchesSearch = !searchQuery || @@ -25,7 +30,9 @@ export default function EventLogsTab() { return matchesLevel && matchesSearch; }); - }, [logLevel, searchQuery]); + + return filtered.reverse(); + }, [logs, logLevel, searchQuery]); // Effect to initialize showLogs useEffect(() => { @@ -37,18 +44,51 @@ export default function EventLogsTab() { logStore.logSystem('Application initialized', { version: process.env.NEXT_PUBLIC_APP_VERSION, environment: process.env.NODE_ENV, + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent, }); // Debug logs for system state logStore.logDebug('System configuration loaded', { runtime: 'Next.js', - features: ['AI Chat', 'Event Logging'], + features: ['AI Chat', 'Event Logging', 'Provider Management', 'Theme Support'], + locale: navigator.language, + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }); + + // Performance metrics + logStore.logSystem('Performance metrics', { + deviceMemory: (navigator as any).deviceMemory || 'unknown', + hardwareConcurrency: navigator.hardwareConcurrency, + connectionType: (navigator as any).connection?.effectiveType || 'unknown', + }); + + // Provider status + logStore.logProvider('Provider status check', { + availableProviders: ['OpenAI', 'Anthropic', 'Mistral', 'Ollama'], + defaultProvider: 'OpenAI', + status: 'operational', + }); + + // Theme and accessibility + logStore.logSystem('User preferences loaded', { + theme: document.documentElement.dataset.theme || 'system', + prefersReducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches, + prefersDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches, }); // Warning logs for potential issues logStore.logWarning('Resource usage threshold approaching', { memoryUsage: '75%', cpuLoad: '60%', + timestamp: new Date().toISOString(), + }); + + // Security checks + logStore.logSystem('Security status', { + httpsEnabled: window.location.protocol === 'https:', + cookiesEnabled: navigator.cookieEnabled, + storageQuota: 'checking...', }); // Error logs with detailed context @@ -56,16 +96,50 @@ export default function EventLogsTab() { endpoint: '/api/chat', retryCount: 3, lastAttempt: new Date().toISOString(), + statusCode: 408, }); + + // Debug logs for development + if (process.env.NODE_ENV === 'development') { + logStore.logDebug('Development mode active', { + debugFlags: true, + mockServices: false, + apiEndpoint: 'local', + }); + } + }, []); + + // Scroll handling + useEffect(() => { + const container = logsContainerRef.current; + + if (!container) { + return undefined; + } + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = container; + const isBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10; + setIsScrolledToBottom(isBottom); + }; + + container.addEventListener('scroll', handleScroll); + + const cleanup = () => { + container.removeEventListener('scroll', handleScroll); + }; + + return cleanup; }, []); + // Auto-scroll effect useEffect(() => { - const container = document.querySelector('.logs-container'); + const container = logsContainerRef.current; - if (container && autoScroll) { - container.scrollTop = container.scrollHeight; + if (container && (autoScroll || isScrolledToBottom)) { + container.scrollTop = 0; } - }, [filteredLogs, autoScroll]); + }, [filteredLogs, autoScroll, isScrolledToBottom]); const handleClearLogs = useCallback(() => { if (confirm('Are you sure you want to clear all logs?')) { @@ -103,33 +177,56 @@ export default function EventLogsTab() { } }, []); + const getLevelIcon = (level: LogEntry['level']): string => { + switch (level) { + case 'info': + return 'i-ph:info'; + case 'warning': + return 'i-ph:warning'; + case 'error': + return 'i-ph:x-circle'; + case 'debug': + return 'i-ph:bug'; + default: + return 'i-ph:circle'; + } + }; + const getLevelColor = (level: LogEntry['level']) => { switch (level) { case 'info': - return 'text-blue-500'; + return 'text-[#1389FD] dark:text-[#1389FD]'; case 'warning': - return 'text-yellow-500'; + return 'text-[#FFDB6C] dark:text-[#FFDB6C]'; case 'error': - return 'text-red-500'; + return 'text-[#EE4744] dark:text-[#EE4744]'; case 'debug': - return 'text-gray-500'; + return 'text-[#77828D] dark:text-[#77828D]'; default: return 'text-bolt-elements-textPrimary'; } }; return ( -
-
+
+
{/* Title and Toggles Row */}
-

Event Logs

+
+
+
+

Event Logs

+

Track system events and debug information

+
+
-
+
+
Show Actions logStore.showLogs.set(checked)} />
-
+
+
Auto-scroll
@@ -137,83 +234,166 @@ export default function EventLogsTab() {
{/* Controls Row */} -
- +
+
+
+ +
+
+
- setSearchQuery(e.target.value)} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" - /> +
+ setSearchQuery(e.target.value)} + className={classNames( + 'w-full pl-9 pr-3 py-2 rounded-lg', + 'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor', + 'text-sm text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', + 'focus:outline-none focus:ring-2 focus:ring-purple-500/30', + 'group-hover:border-purple-500/30', + 'transition-all duration-200', + )} + /> +
+
{showLogs && (
- - +
)}
-
+ {filteredLogs.length === 0 ? ( -
No logs found
- ) : ( - filteredLogs.map((log, index) => ( -
+ + -
- - [{log.level.toUpperCase()}] - - - {new Date(log.timestamp).toLocaleString()} - - {log.message} -
- {log.details && ( -
-                  {JSON.stringify(log.details, null, 2)}
-                
- )} -
- )) + No logs found + +
+ ) : ( +
+ {filteredLogs.map((log, index) => ( + +
+
+
+
+ + {log.level.toUpperCase()} + + + {new Date(log.timestamp).toLocaleString()} + + {log.message} +
+ {log.details && ( + + {JSON.stringify(log.details, null, 2)} + + )} +
+
+ + ))} +
)} -
+
); } diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx index f67ddc8937..93d8d7dd93 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/settings/features/FeaturesTab.tsx @@ -1,7 +1,22 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Switch } from '~/components/ui/Switch'; import { PromptLibrary } from '~/lib/common/prompt-library'; import { useSettings } from '~/lib/hooks/useSettings'; +import { motion, AnimatePresence } from 'framer-motion'; +import { classNames } from '~/utils/classNames'; +import { settingsStyles } from '~/components/settings/settings.styles'; +import { toast } from 'react-toastify'; + +interface FeatureToggle { + id: string; + title: string; + description: string; + icon: string; + enabled: boolean; + beta?: boolean; + experimental?: boolean; + tooltip?: string; +} export default function FeaturesTab() { const { @@ -20,88 +35,266 @@ export default function FeaturesTab() { contextOptimizationEnabled, } = useSettings(); + const [hoveredFeature, setHoveredFeature] = useState(null); + const [expandedFeature, setExpandedFeature] = useState(null); + const handleToggle = (enabled: boolean) => { enableDebugMode(enabled); enableEventLogs(enabled); + toast.success(`Debug features ${enabled ? 'enabled' : 'disabled'}`); + }; + + const features: FeatureToggle[] = [ + { + id: 'debug', + title: 'Debug Features', + description: 'Enable debugging tools and detailed logging', + icon: 'i-ph:bug', + enabled: debug, + experimental: true, + tooltip: 'Access advanced debugging tools and view detailed system logs', + }, + { + id: 'latestBranch', + title: 'Use Main Branch', + description: 'Check for updates against the main branch instead of stable', + icon: 'i-ph:git-branch', + enabled: isLatestBranch, + beta: true, + tooltip: 'Get the latest features and improvements before they are officially released', + }, + { + id: 'autoTemplate', + title: 'Auto Select Code Template', + description: 'Let Bolt select the best starter template for your project', + icon: 'i-ph:magic-wand', + enabled: autoSelectTemplate, + tooltip: 'Automatically choose the most suitable template based on your project type', + }, + { + id: 'contextOptimization', + title: 'Context Optimization', + description: 'Optimize chat context by redacting file contents and using system prompts', + icon: 'i-ph:arrows-in', + enabled: contextOptimizationEnabled, + tooltip: 'Improve AI responses by optimizing the context window and system prompts', + }, + { + id: 'experimentalProviders', + title: 'Experimental Providers', + description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike', + icon: 'i-ph:robot', + enabled: isLocalModel, + experimental: true, + tooltip: 'Try out new AI providers and models in development', + }, + ]; + + const handleToggleFeature = (featureId: string, enabled: boolean) => { + switch (featureId) { + case 'debug': + handleToggle(enabled); + break; + case 'latestBranch': + enableLatestBranch(enabled); + toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`); + break; + case 'autoTemplate': + setAutoSelectTemplate(enabled); + toast.success(`Auto template selection ${enabled ? 'enabled' : 'disabled'}`); + break; + case 'contextOptimization': + enableContextOptimization(enabled); + toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`); + break; + case 'experimentalProviders': + enableLocalModels(enabled); + toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`); + break; + } }; return ( -
-
-

Optional Features

-
-
- Debug Features - -
-
-
- Use Main Branch -

- Check for updates against the main branch instead of stable -

-
- -
-
-
- Auto Select Code Template -

- Let Bolt select the best starter template for your project. -

+
+ +
+
+

Features

+

Customize your Bolt experience

+
+ + + + {features.map((feature, index) => ( + setHoveredFeature(feature.id)} + onHoverEnd={() => setHoveredFeature(null)} + onClick={() => setExpandedFeature(expandedFeature === feature.id ? null : feature.id)} + > + + {hoveredFeature === feature.id && feature.tooltip && ( + + {feature.tooltip} +
+ + )} + + +
+ {feature.beta && ( + + Beta + + )} + {feature.experimental && ( + + Experimental + + )}
- -
-
-
- Use Context Optimization -

- redact file contents form chat and puts the latest file contents on the system prompt -

+ +
+ +
+ + +
+
+
+

+ {feature.title} +

+

{feature.description}

+
+ handleToggleFeature(feature.id, checked)} + /> +
+
- -
-
-
+
+ ))} +
-
-

Experimental Features

-

- Disclaimer: Experimental features may be unstable and are subject to change. -

-
-
- Experimental Providers - -
-

- Enable experimental providers such as Ollama, LMStudio, and OpenAILike. -

-
-
-
- Prompt Library -

- Choose a prompt from the library to use as the system prompt. -

-
- +
+ + +
+
+
+

+ Prompt Library +

+

+ Choose a prompt from the library to use as the system prompt +

+
+ +
+
-
+
); } diff --git a/app/components/settings/profile/ProfileTab.tsx b/app/components/settings/profile/ProfileTab.tsx new file mode 100644 index 0000000000..a3c02bb533 --- /dev/null +++ b/app/components/settings/profile/ProfileTab.tsx @@ -0,0 +1,399 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { AnimatePresence } from 'framer-motion'; +import { toast } from 'react-toastify'; +import { classNames } from '~/utils/classNames'; +import { Switch } from '~/components/ui/Switch'; +import type { UserProfile } from '~/components/settings/settings.types'; +import { themeStore, kTheme } from '~/lib/stores/theme'; +import { motion } from 'framer-motion'; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/gif']; +const MIN_PASSWORD_LENGTH = 8; + +export default function ProfileTab() { + const fileInputRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [currentTimezone, setCurrentTimezone] = useState(''); + const [profile, setProfile] = useState(() => { + const saved = localStorage.getItem('bolt_user_profile'); + return saved + ? JSON.parse(saved) + : { + name: '', + email: '', + theme: 'system', + notifications: true, + language: 'en', + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + password: '', + bio: '', + }; + }); + + useEffect(() => { + setCurrentTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone); + }, []); + + // Apply theme when profile changes + useEffect(() => { + if (profile.theme === 'system') { + // Remove theme override + localStorage.removeItem(kTheme); + + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light'); + } else { + // Set specific theme + localStorage.setItem(kTheme, profile.theme); + document.querySelector('html')?.setAttribute('data-theme', profile.theme); + themeStore.set(profile.theme); + } + }, [profile.theme]); + + const handleAvatarUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (!file) { + return; + } + + if (!ALLOWED_FILE_TYPES.includes(file.type)) { + toast.error('Please upload a valid image file (JPEG, PNG, or GIF)'); + return; + } + + if (file.size > MAX_FILE_SIZE) { + toast.error('File size must be less than 5MB'); + return; + } + + setIsLoading(true); + + try { + const reader = new FileReader(); + + reader.onloadend = () => { + setProfile((prev) => ({ ...prev, avatar: reader.result as string })); + setIsLoading(false); + }; + reader.readAsDataURL(file); + } catch (error) { + console.error('Error uploading avatar:', error); + toast.error('Failed to upload avatar'); + setIsLoading(false); + } + }; + + const handleSave = async () => { + if (!profile.name.trim()) { + toast.error('Name is required'); + return; + } + + if (!profile.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profile.email)) { + toast.error('Please enter a valid email address'); + return; + } + + if (profile.password && profile.password.length < MIN_PASSWORD_LENGTH) { + toast.error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters long`); + return; + } + + setIsLoading(true); + + try { + localStorage.setItem('bolt_user_profile', JSON.stringify(profile)); + toast.success('Profile settings saved successfully'); + } catch (error) { + console.error('Error saving profile:', error); + toast.error('Failed to save profile settings'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {/* Profile Information */} + +
+
+ Personal Information +
+
+ {/* Avatar */} +
+
+ + {isLoading ? ( +
+ ) : profile.avatar ? ( + Profile + ) : ( +
+ )} + +
+ + +
+ + {/* Profile Fields */} +
+
+
+
+
+ setProfile((prev) => ({ ...prev, name: e.target.value }))} + placeholder="Enter your name" + className={classNames( + 'w-full px-3 py-1.5 rounded-lg text-sm', + 'pl-10', + 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]', + 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', + 'focus:outline-none focus:ring-1 focus:ring-purple-500', + )} + /> +
+ +
+
+
+
+ setProfile((prev) => ({ ...prev, email: e.target.value }))} + placeholder="Enter your email" + className={classNames( + 'w-full px-3 py-1.5 rounded-lg text-sm', + 'pl-10', + 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]', + 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', + 'focus:outline-none focus:ring-1 focus:ring-purple-500', + )} + /> +
+ +
+ setProfile((prev) => ({ ...prev, password: e.target.value }))} + placeholder="Enter new password" + className={classNames( + 'w-full px-3 py-1.5 rounded-lg text-sm', + 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]', + 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', + 'focus:outline-none focus:ring-1 focus:ring-purple-500', + )} + /> + +
+
+
+ + + {/* Theme & Language */} + +
+
+ Appearance +
+ +
+
+
+ +
+
+ {(['light', 'dark', 'system'] as const).map((theme) => ( + + ))} +
+
+ +
+
+
+ +
+ +
+ +
+
+
+ +
+
+ + {profile.notifications ? 'Notifications are enabled' : 'Notifications are disabled'} + + setProfile((prev) => ({ ...prev, notifications: checked }))} + /> +
+
+ + + {/* Timezone */} +
+
+
+ Time Settings +
+ +
+
+ +
+
+ + +
+
+
+ + {/* Save Button */} + + + +
+ ); +} diff --git a/app/components/settings/providers/OllamaModelUpdater.tsx b/app/components/settings/providers/OllamaModelUpdater.tsx new file mode 100644 index 0000000000..110ffb4d4c --- /dev/null +++ b/app/components/settings/providers/OllamaModelUpdater.tsx @@ -0,0 +1,295 @@ +import React, { useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import { toast } from 'react-toastify'; +import { classNames } from '~/utils/classNames'; +import { settingsStyles } from '~/components/settings/settings.styles'; +import { DialogTitle, DialogDescription } from '~/components/ui/Dialog'; + +interface OllamaModel { + name: string; + digest: string; + size: number; + modified_at: string; + details?: { + family: string; + parameter_size: string; + quantization_level: string; + }; + status?: 'idle' | 'updating' | 'updated' | 'error' | 'checking'; + error?: string; + newDigest?: string; + progress?: { + current: number; + total: number; + status: string; + }; +} + +interface OllamaTagResponse { + models: Array<{ + name: string; + digest: string; + size: number; + modified_at: string; + details?: { + family: string; + parameter_size: string; + quantization_level: string; + }; + }>; +} + +interface OllamaPullResponse { + status: string; + digest?: string; + total?: number; + completed?: number; +} + +export default function OllamaModelUpdater() { + const [models, setModels] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isBulkUpdating, setIsBulkUpdating] = useState(false); + + useEffect(() => { + fetchModels(); + }, []); + + const fetchModels = async () => { + try { + setIsLoading(true); + + const response = await fetch('http://localhost:11434/api/tags'); + const data = (await response.json()) as OllamaTagResponse; + setModels( + data.models.map((model) => ({ + name: model.name, + digest: model.digest, + size: model.size, + modified_at: model.modified_at, + details: model.details, + status: 'idle' as const, + })), + ); + } catch (error) { + toast.error('Failed to fetch Ollama models'); + console.error('Error fetching models:', error); + } finally { + setIsLoading(false); + } + }; + + const updateModel = async (modelName: string): Promise<{ success: boolean; newDigest?: string }> => { + try { + const response = await fetch('http://localhost:11434/api/pull', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: modelName }), + }); + + if (!response.ok) { + throw new Error(`Failed to update ${modelName}`); + } + + const reader = response.body?.getReader(); + + if (!reader) { + throw new Error('No response reader available'); + } + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + const text = new TextDecoder().decode(value); + const lines = text.split('\n').filter(Boolean); + + for (const line of lines) { + const data = JSON.parse(line) as OllamaPullResponse; + + setModels((current) => + current.map((m) => + m.name === modelName + ? { + ...m, + progress: { + current: data.completed || 0, + total: data.total || 0, + status: data.status, + }, + newDigest: data.digest, + } + : m, + ), + ); + } + } + + setModels((current) => current.map((m) => (m.name === modelName ? { ...m, status: 'checking' } : m))); + + const updatedResponse = await fetch('http://localhost:11434/api/tags'); + const data = (await updatedResponse.json()) as OllamaTagResponse; + const updatedModel = data.models.find((m) => m.name === modelName); + + return { success: true, newDigest: updatedModel?.digest }; + } catch (error) { + console.error(`Error updating ${modelName}:`, error); + return { success: false }; + } + }; + + const handleBulkUpdate = async () => { + setIsBulkUpdating(true); + + for (const model of models) { + setModels((current) => current.map((m) => (m.name === model.name ? { ...m, status: 'updating' } : m))); + + const { success, newDigest } = await updateModel(model.name); + + setModels((current) => + current.map((m) => + m.name === model.name + ? { + ...m, + status: success ? 'updated' : 'error', + error: success ? undefined : 'Update failed', + newDigest, + } + : m, + ), + ); + } + + setIsBulkUpdating(false); + toast.success('Bulk update completed'); + }; + + const handleSingleUpdate = async (modelName: string) => { + setModels((current) => current.map((m) => (m.name === modelName ? { ...m, status: 'updating' } : m))); + + const { success, newDigest } = await updateModel(modelName); + + setModels((current) => + current.map((m) => + m.name === modelName + ? { + ...m, + status: success ? 'updated' : 'error', + error: success ? undefined : 'Update failed', + newDigest, + } + : m, + ), + ); + + if (success) { + toast.success(`Updated ${modelName}`); + } else { + toast.error(`Failed to update ${modelName}`); + } + }; + + if (isLoading) { + return ( +
+
+ Loading models... +
+ ); + } + + return ( +
+
+ Ollama Model Manager + Update your local Ollama models to their latest versions +
+ +
+
+
+ {models.length} models available +
+ + {isBulkUpdating ? ( + <> +
+ Updating All... + + ) : ( + <> +
+ Update All Models + + )} + +
+ +
+ {models.map((model) => ( +
+
+
+
+ {model.name} + {model.status === 'updating' &&
} + {model.status === 'updated' &&
} + {model.status === 'error' &&
} +
+
+ Version: {model.digest.substring(0, 7)} + {model.status === 'updated' && model.newDigest && ( + <> +
+ {model.newDigest.substring(0, 7)} + + )} + {model.progress && ( + + {model.progress.status}{' '} + {model.progress.total > 0 && ( + <>({Math.round((model.progress.current / model.progress.total) * 100)}%) + )} + + )} + {model.details && ( + + ({model.details.parameter_size}, {model.details.quantization_level}) + + )} +
+
+ handleSingleUpdate(model.name)} + disabled={model.status === 'updating'} + className={classNames(settingsStyles.button.base, settingsStyles.button.secondary)} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+ Update + +
+ ))} +
+
+ ); +} diff --git a/app/components/settings/providers/ProvidersTab.tsx b/app/components/settings/providers/ProvidersTab.tsx index 2f790bc8ab..7818fbf536 100644 --- a/app/components/settings/providers/ProvidersTab.tsx +++ b/app/components/settings/providers/ProvidersTab.tsx @@ -1,34 +1,157 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { Switch } from '~/components/ui/Switch'; +import Separator from '~/components/ui/Separator'; import { useSettings } from '~/lib/hooks/useSettings'; import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings'; import type { IProviderConfig } from '~/types/model'; import { logStore } from '~/lib/stores/logs'; - -// Import a default fallback icon +import { motion } from 'framer-motion'; +import { classNames } from '~/utils/classNames'; +import { settingsStyles } from '~/components/settings/settings.styles'; +import { toast } from 'react-toastify'; import { providerBaseUrlEnvKeys } from '~/utils/constants'; +import { SiAmazon, SiOpenai, SiGoogle, SiHuggingface, SiPerplexity } from 'react-icons/si'; +import { BsRobot, BsCloud, BsCodeSquare, BsCpu, BsBox } from 'react-icons/bs'; +import { TbBrandOpenai, TbBrain, TbCloudComputing } from 'react-icons/tb'; +import { BiCodeBlock, BiChip } from 'react-icons/bi'; +import { FaCloud, FaBrain } from 'react-icons/fa'; +import type { IconType } from 'react-icons'; +import OllamaModelUpdater from './OllamaModelUpdater'; +import { DialogRoot, Dialog } from '~/components/ui/Dialog'; + +// Add type for provider names to ensure type safety +type ProviderName = + | 'AmazonBedrock' + | 'Anthropic' + | 'Cohere' + | 'Deepseek' + | 'Google' + | 'Groq' + | 'HuggingFace' + | 'Hyperbolic' + | 'LMStudio' + | 'Mistral' + | 'Ollama' + | 'OpenAI' + | 'OpenAILike' + | 'OpenRouter' + | 'Perplexity' + | 'Together' + | 'XAI'; + +// Update the PROVIDER_ICONS type to use the ProviderName type +const PROVIDER_ICONS: Record = { + AmazonBedrock: SiAmazon, + Anthropic: FaBrain, + Cohere: BiChip, + Deepseek: BiCodeBlock, + Google: SiGoogle, + Groq: BsCpu, + HuggingFace: SiHuggingface, + Hyperbolic: TbCloudComputing, + LMStudio: BsCodeSquare, + Mistral: TbBrain, + Ollama: BsBox, + OpenAI: SiOpenai, + OpenAILike: TbBrandOpenai, + OpenRouter: FaCloud, + Perplexity: SiPerplexity, + Together: BsCloud, + XAI: BsRobot, +}; + +// Update PROVIDER_DESCRIPTIONS to use the same type +const PROVIDER_DESCRIPTIONS: Partial> = { + OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models', + Anthropic: 'Access Claude and other Anthropic models', + Ollama: 'Run open-source models locally on your machine', + LMStudio: 'Local model inference with LM Studio', + OpenAILike: 'Connect to OpenAI-compatible API endpoints', +}; + +// Add these types and helper functions +type ProviderCategory = 'cloud' | 'local'; -const DefaultIcon = '/icons/Default.svg'; // Adjust the path as necessary +interface ProviderGroup { + title: string; + description: string; + icon: string; + providers: IProviderConfig[]; +} + +// Add this type +interface CategoryToggleState { + cloud: boolean; + local: boolean; +} export default function ProvidersTab() { const { providers, updateProviderSettings, isLocalModel } = useSettings(); + const [editingProvider, setEditingProvider] = useState(null); const [filteredProviders, setFilteredProviders] = useState([]); + const [categoryEnabled, setCategoryEnabled] = useState({ + cloud: false, + local: false, + }); + const [showOllamaUpdater, setShowOllamaUpdater] = useState(false); + + // Group providers by category + const groupedProviders = useMemo(() => { + const groups: Record = { + cloud: { + title: 'Cloud Providers', + description: 'AI models hosted on cloud platforms', + icon: 'i-ph:cloud-duotone', + providers: [], + }, + local: { + title: 'Local Providers', + description: 'Run models locally on your machine', + icon: 'i-ph:desktop-duotone', + providers: [], + }, + }; + + filteredProviders.forEach((provider) => { + const category: ProviderCategory = LOCAL_PROVIDERS.includes(provider.name) ? 'local' : 'cloud'; + groups[category].providers.push(provider); + }); - // Load base URLs from cookies - const [searchTerm, setSearchTerm] = useState(''); + return groups; + }, [filteredProviders]); + // Update the toggle handler + const handleToggleCategory = useCallback( + (category: ProviderCategory, enabled: boolean) => { + setCategoryEnabled((prev) => ({ ...prev, [category]: enabled })); + + // Get providers for this category + const categoryProviders = groupedProviders[category].providers; + categoryProviders.forEach((provider) => { + updateProviderSettings(provider.name, { ...provider.settings, enabled }); + }); + + toast.success(enabled ? `All ${category} providers enabled` : `All ${category} providers disabled`); + }, + [groupedProviders, updateProviderSettings], + ); + + // Add effect to update category toggle states based on provider states + useEffect(() => { + const newCategoryState = { + cloud: groupedProviders.cloud.providers.every((p) => p.settings.enabled), + local: groupedProviders.local.providers.every((p) => p.settings.enabled), + }; + setCategoryEnabled(newCategoryState); + }, [groupedProviders]); + + // Effect to filter and sort providers useEffect(() => { let newFilteredProviders: IProviderConfig[] = Object.entries(providers).map(([key, value]) => ({ ...value, name: key, })); - if (searchTerm && searchTerm.length > 0) { - newFilteredProviders = newFilteredProviders.filter((provider) => - provider.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); - } - if (!isLocalModel) { newFilteredProviders = newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name)); } @@ -40,108 +163,245 @@ export default function ProvidersTab() { const urlConfigurable = newFilteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name)); setFilteredProviders([...regular, ...urlConfigurable]); - }, [providers, searchTerm, isLocalModel]); - - const renderProviderCard = (provider: IProviderConfig) => { - const envBaseUrlKey = providerBaseUrlEnvKeys[provider.name].baseUrlKey; - const envBaseUrl = envBaseUrlKey ? import.meta.env[envBaseUrlKey] : undefined; - const isUrlConfigurable = URL_CONFIGURABLE_PROVIDERS.includes(provider.name); - - return ( -
-
-
- { - e.currentTarget.src = DefaultIcon; - }} - alt={`${provider.name} icon`} - className="w-6 h-6 dark:invert" - /> - {provider.name} -
- { - updateProviderSettings(provider.name, { ...provider.settings, enabled }); - - if (enabled) { - logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name }); - } else { - logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name }); - } - }} - /> -
- {isUrlConfigurable && provider.settings.enabled && ( -
- {envBaseUrl && ( - - )} - - { - let newBaseUrl: string | undefined = e.target.value; - - if (newBaseUrl && newBaseUrl.trim().length === 0) { - newBaseUrl = undefined; - } - - updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl }); - logStore.logProvider(`Base URL updated for ${provider.name}`, { - provider: provider.name, - baseUrl: newBaseUrl, - }); - }} - placeholder={`Enter ${provider.name} base URL`} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" - /> -
- )} -
- ); + }, [providers, isLocalModel]); + + const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => { + updateProviderSettings(provider.name, { ...provider.settings, enabled }); + + if (enabled) { + logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name }); + toast.success(`${provider.name} enabled`); + } else { + logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name }); + toast.success(`${provider.name} disabled`); + } }; - const regularProviders = filteredProviders.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name)); - const urlConfigurableProviders = filteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name)); + const handleUpdateBaseUrl = (provider: IProviderConfig, baseUrl: string) => { + let newBaseUrl: string | undefined = baseUrl; + + if (newBaseUrl && newBaseUrl.trim().length === 0) { + newBaseUrl = undefined; + } + + updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl }); + logStore.logProvider(`Base URL updated for ${provider.name}`, { + provider: provider.name, + baseUrl: newBaseUrl, + }); + toast.success(`${provider.name} base URL updated`); + setEditingProvider(null); + }; return ( -
-
- setSearchTerm(e.target.value)} - className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" - /> -
- - {/* Regular Providers Grid */} -
{regularProviders.map(renderProviderCard)}
- - {/* URL Configurable Providers Section */} - {urlConfigurableProviders.length > 0 && ( -
-

Experimental Providers

-

- These providers are experimental and allow you to run AI models locally or connect to your own - infrastructure. They require additional setup but offer more flexibility. -

-
{urlConfigurableProviders.map(renderProviderCard)}
-
- )} +
+ {Object.entries(groupedProviders).map(([category, group]) => ( + +
+
+
+
+
+
+

{group.title}

+

{group.description}

+
+
+ +
+ + Enable All {category === 'cloud' ? 'Cloud' : 'Local'} + + handleToggleCategory(category as ProviderCategory, checked)} + /> +
+
+ +
+ {group.providers.map((provider, index) => ( + +
+ {LOCAL_PROVIDERS.includes(provider.name) && ( + + Local + + )} + {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && ( + + Configurable + + )} +
+ +
+ +
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, { + className: 'w-full h-full', + 'aria-label': `${provider.name} logo`, + })} +
+
+ +
+
+
+

+ {provider.name} +

+

+ {PROVIDER_DESCRIPTIONS[provider.name as keyof typeof PROVIDER_DESCRIPTIONS] || + (URL_CONFIGURABLE_PROVIDERS.includes(provider.name) + ? 'Configure custom endpoint for this provider' + : 'Standard AI provider integration')} +

+
+ handleToggleProvider(provider, checked)} + /> +
+ + {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && ( + +
+ {editingProvider === provider.name ? ( + { + if (e.key === 'Enter') { + handleUpdateBaseUrl(provider, e.currentTarget.value); + } else if (e.key === 'Escape') { + setEditingProvider(null); + } + }} + onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)} + autoFocus + /> + ) : ( +
setEditingProvider(provider.name)} + > +
+
+ + {provider.settings.baseUrl || 'Click to set base URL'} + +
+
+ )} +
+ + {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && ( +
+
+
+ Environment URL set in .env file +
+
+ )} + + )} +
+
+ + + + {provider.name === 'Ollama' && provider.settings.enabled && ( + setShowOllamaUpdater(true)} + className={classNames(settingsStyles.button.base, settingsStyles.button.secondary, 'ml-2')} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+ Update Models + + )} + + + +
+ +
+
+
+ + ))} +
+ + {category === 'cloud' && } +
+ ))}
); } diff --git a/app/components/settings/settings.styles.ts b/app/components/settings/settings.styles.ts new file mode 100644 index 0000000000..ddb9c22668 --- /dev/null +++ b/app/components/settings/settings.styles.ts @@ -0,0 +1,37 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export const settingsStyles = { + // Card styles + card: 'bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A]', + + // Button styles + button: { + base: 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed', + primary: 'bg-purple-500 text-white hover:bg-purple-600', + secondary: + 'bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white', + danger: 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20', + warning: 'bg-yellow-50 text-yellow-600 hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20', + success: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-500/10 dark:hover:bg-green-500/20', + }, + + // Form styles + form: { + label: 'block text-sm text-bolt-elements-textSecondary mb-2', + input: + 'w-full px-3 py-2 rounded-lg text-sm bg-[#F8F8F8] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500', + }, + + // Search container + search: { + input: + 'w-full h-10 pl-10 pr-4 rounded-lg text-sm bg-[#F8F8F8] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all', + }, + + 'loading-spinner': 'i-ph:spinner-gap-bold animate-spin w-4 h-4', +} as const; diff --git a/app/components/settings/settings.types.ts b/app/components/settings/settings.types.ts new file mode 100644 index 0000000000..3037ae820d --- /dev/null +++ b/app/components/settings/settings.types.ts @@ -0,0 +1,53 @@ +import type { ReactNode } from 'react'; + +export type SettingCategory = 'profile' | 'file_sharing' | 'connectivity' | 'system' | 'services' | 'preferences'; +export type TabType = + | 'profile' + | 'data' + | 'providers' + | 'features' + | 'debug' + | 'event-logs' + | 'connection' + | 'preferences'; + +export interface UserProfile { + name: string; + email: string; + avatar?: string; + theme: 'light' | 'dark' | 'system'; + notifications: boolean; + password?: string; + bio?: string; + language: string; + timezone: string; +} + +export interface SettingItem { + id: TabType; + label: string; + icon: string; + category: SettingCategory; + description?: string; + component: () => ReactNode; + badge?: string; + keywords?: string[]; +} + +export const categoryLabels: Record = { + profile: 'Profile & Account', + file_sharing: 'File Sharing', + connectivity: 'Connectivity', + system: 'System', + services: 'Services', + preferences: 'Preferences', +}; + +export const categoryIcons: Record = { + profile: 'i-ph:user-circle', + file_sharing: 'i-ph:folder-simple', + connectivity: 'i-ph:wifi-high', + system: 'i-ph:gear', + services: 'i-ph:cube', + preferences: 'i-ph:sliders', +}; diff --git a/app/components/ui/Dialog.tsx b/app/components/ui/Dialog.tsx index a808c77422..0ea110dba9 100644 --- a/app/components/ui/Dialog.tsx +++ b/app/components/ui/Dialog.tsx @@ -7,60 +7,23 @@ import { IconButton } from './IconButton'; export { Close as DialogClose, Root as DialogRoot } from '@radix-ui/react-dialog'; -const transition = { - duration: 0.15, - ease: cubicEasingFn, -}; - -export const dialogBackdropVariants = { - closed: { - opacity: 0, - transition, - }, - open: { - opacity: 1, - transition, - }, -} satisfies Variants; - -export const dialogVariants = { - closed: { - x: '-50%', - y: '-40%', - scale: 0.96, - opacity: 0, - transition, - }, - open: { - x: '-50%', - y: '-50%', - scale: 1, - opacity: 1, - transition, - }, -} satisfies Variants; - interface DialogButtonProps { type: 'primary' | 'secondary' | 'danger'; children: ReactNode; - onClick?: (event: React.UIEvent) => void; + onClick?: (event: React.MouseEvent) => void; + disabled?: boolean; } -export const DialogButton = memo(({ type, children, onClick }: DialogButtonProps) => { +export const DialogButton = memo(({ type, children, onClick, disabled }: DialogButtonProps) => { return ( @@ -70,10 +33,7 @@ export const DialogButton = memo(({ type, children, onClick }: DialogButtonProps export const DialogTitle = memo(({ className, children, ...props }: RadixDialog.DialogTitleProps) => { return ( {children} @@ -84,7 +44,7 @@ export const DialogTitle = memo(({ className, children, ...props }: RadixDialog. export const DialogDescription = memo(({ className, children, ...props }: RadixDialog.DialogDescriptionProps) => { return ( {children} @@ -92,29 +52,72 @@ export const DialogDescription = memo(({ className, children, ...props }: RadixD ); }); +const transition = { + duration: 0.15, + ease: cubicEasingFn, +}; + +export const dialogBackdropVariants = { + closed: { + opacity: 0, + transition, + }, + open: { + opacity: 1, + transition, + }, +} satisfies Variants; + +export const dialogVariants = { + closed: { + x: '-50%', + y: '-40%', + scale: 0.96, + opacity: 0, + transition, + }, + open: { + x: '-50%', + y: '-50%', + scale: 1, + opacity: 1, + transition, + }, +} satisfies Variants; + interface DialogProps { - children: ReactNode | ReactNode[]; + children: ReactNode; className?: string; - onBackdrop?: (event: React.UIEvent) => void; - onClose?: (event: React.UIEvent) => void; + showCloseButton?: boolean; + onClose?: () => void; + onBackdrop?: () => void; } -export const Dialog = memo(({ className, children, onBackdrop, onClose }: DialogProps) => { +export const Dialog = memo(({ children, className, showCloseButton = true, onClose, onBackdrop }: DialogProps) => { return ( - + - {children} - - - +
+ {children} + {showCloseButton && ( + + + + )} +
diff --git a/app/components/ui/Separator.tsx b/app/components/ui/Separator.tsx new file mode 100644 index 0000000000..8ea43a5ec0 --- /dev/null +++ b/app/components/ui/Separator.tsx @@ -0,0 +1,22 @@ +import * as SeparatorPrimitive from '@radix-ui/react-separator'; +import { classNames } from '~/utils/classNames'; + +interface SeparatorProps { + className?: string; + orientation?: 'horizontal' | 'vertical'; +} + +export const Separator = ({ className, orientation = 'horizontal' }: SeparatorProps) => { + return ( + + ); +}; + +export default Separator; diff --git a/app/lib/stores/logs.ts b/app/lib/stores/logs.ts index 4b306a5e47..1af2506ae9 100644 --- a/app/lib/stores/logs.ts +++ b/app/lib/stores/logs.ts @@ -24,6 +24,11 @@ class LogStore { this._loadLogs(); } + // Expose the logs store for subscription + get logs() { + return this._logs; + } + private _loadLogs() { const savedLogs = Cookies.get('eventLogs'); diff --git a/package.json b/package.json index 5788c0030d..a259ab5a97 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,12 @@ "node": ">=18.18.0" }, "dependencies": { + "@ai-sdk/amazon-bedrock": "1.0.6", "@ai-sdk/anthropic": "^0.0.39", "@ai-sdk/cohere": "^1.0.3", "@ai-sdk/google": "^0.0.52", "@ai-sdk/mistral": "^0.0.43", "@ai-sdk/openai": "^0.0.66", - "@ai-sdk/amazon-bedrock": "1.0.6", "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", "@codemirror/lang-cpp": "^6.0.2", @@ -52,7 +52,7 @@ "@codemirror/search": "^6.5.8", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.35.0", - "@iconify-json/ph": "^1.2.1", + "@headlessui/react": "^2.2.0", "@iconify-json/svg-spinners": "^1.2.1", "@lezer/highlight": "^1.2.1", "@nanostores/react": "^0.7.3", @@ -76,6 +76,7 @@ "@xterm/xterm": "^5.5.0", "ai": "^4.0.13", "chalk": "^5.4.1", + "clsx": "^2.1.1", "date-fns": "^3.6.0", "diff": "^5.2.0", "dotenv": "^16.4.7", @@ -93,6 +94,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-hotkeys-hook": "^4.6.1", + "react-icons": "^5.4.0", "react-markdown": "^9.0.1", "react-resizable-panels": "^2.1.7", "react-toastify": "^10.0.6", @@ -102,11 +104,14 @@ "remix-island": "^0.2.0", "remix-utils": "^7.7.0", "shiki": "^1.24.0", + "tailwind-merge": "^2.6.0", "unist-util-visit": "^5.0.0" }, "devDependencies": { "@blitz/eslint-plugin": "0.1.0", "@cloudflare/workers-types": "^4.20241127.0", + "@iconify-json/ph": "^1.2.1", + "@iconify/types": "^2.0.0", "@remix-run/dev": "^2.15.0", "@types/diff": "^5.2.3", "@types/dom-speech-recognition": "^0.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f625a94cb3..c4d52d1d50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,9 +77,9 @@ importers: '@codemirror/view': specifier: ^6.35.0 version: 6.35.0 - '@iconify-json/ph': - specifier: ^1.2.1 - version: 1.2.1 + '@headlessui/react': + specifier: ^2.2.0 + version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@iconify-json/svg-spinners': specifier: ^1.2.1 version: 1.2.1 @@ -149,6 +149,9 @@ importers: chalk: specifier: ^5.4.1 version: 5.4.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 date-fns: specifier: ^3.6.0 version: 3.6.0 @@ -200,6 +203,9 @@ importers: react-hotkeys-hook: specifier: ^4.6.1 version: 4.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-icons: + specifier: ^5.4.0 + version: 5.4.0(react@18.3.1) react-markdown: specifier: ^9.0.1 version: 9.0.1(@types/react@18.3.12)(react@18.3.1) @@ -227,6 +233,9 @@ importers: shiki: specifier: ^1.24.0 version: 1.24.0 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 unist-util-visit: specifier: ^5.0.0 version: 5.0.0 @@ -237,6 +246,12 @@ importers: '@cloudflare/workers-types': specifier: ^4.20241127.0 version: 4.20241127.0 + '@iconify-json/ph': + specifier: ^1.2.1 + version: 1.2.1 + '@iconify/types': + specifier: ^2.0.0 + version: 2.0.0 '@remix-run/dev': specifier: ^2.15.0 version: 2.15.0(@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@types/node@22.10.1)(sass-embedded@1.81.0)(typescript@5.7.2)(vite@5.4.11(@types/node@22.10.1)(sass-embedded@1.81.0))(wrangler@3.91.0(@cloudflare/workers-types@4.20241127.0)) @@ -1437,9 +1452,22 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/utils@0.2.8': resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + '@headlessui/react@2.2.0': + resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1997,6 +2025,40 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@react-aria/focus@3.19.1': + resolution: {integrity: sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.23.0': + resolution: {integrity: sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/ssr@3.9.7': + resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/utils@3.27.0': + resolution: {integrity: sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/utils@3.10.5': + resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.27.0': + resolution: {integrity: sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@remix-run/cloudflare-pages@2.15.0': resolution: {integrity: sha512-3FjiON0BmEH3fwGdmP6eEf9TL5BejCt9LOMnszefDGdwY7kgXCodJNr8TAYseor6m7LlC4xgSkgkgj/YRIZTGA==} engines: {node: '>=18.0.0'} @@ -2398,6 +2460,18 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tanstack/react-virtual@3.11.2': + resolution: {integrity: sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.11.2': + resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==} + '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -4965,6 +5039,11 @@ packages: react: '>=16.8.1' react-dom: '>=16.8.1' + react-icons@5.4.0: + resolution: {integrity: sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==} + peerDependencies: + react: '*' + react-markdown@9.0.1: resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==} peerDependencies: @@ -5559,6 +5638,12 @@ packages: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} @@ -7372,8 +7457,25 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.2.0 + '@floating-ui/utils@0.2.8': {} + '@headlessui/react@2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.19.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': 3.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -7980,6 +8082,49 @@ snapshots: '@radix-ui/rect@1.1.0': {} + '@react-aria/focus@3.19.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/interactions': 3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.27.0(react@18.3.1) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/interactions@3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.7(react@18.3.1) + '@react-aria/utils': 3.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.27.0(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/ssr@3.9.7(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.15 + react: 18.3.1 + + '@react-aria/utils@3.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.7(react@18.3.1) + '@react-stately/utils': 3.10.5(react@18.3.1) + '@react-types/shared': 3.27.0(react@18.3.1) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-stately/utils@3.10.5(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.15 + react: 18.3.1 + + '@react-types/shared@3.27.0(react@18.3.1)': + dependencies: + react: 18.3.1 + '@remix-run/cloudflare-pages@2.15.0(@cloudflare/workers-types@4.20241127.0)(typescript@5.7.2)': dependencies: '@cloudflare/workers-types': 4.20241127.0 @@ -8543,6 +8688,18 @@ snapshots: - supports-color - typescript + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tanstack/react-virtual@3.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.11.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/virtual-core@3.11.2': {} + '@types/acorn@4.0.6': dependencies: '@types/estree': 1.0.6 @@ -11823,6 +11980,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-icons@5.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-markdown@9.0.1(@types/react@18.3.12)(react@18.3.1): dependencies: '@types/hast': 3.0.4 @@ -12456,6 +12617,10 @@ snapshots: '@pkgr/core': 0.1.1 tslib: 2.8.1 + tabbable@6.2.0: {} + + tailwind-merge@2.6.0: {} + tar-fs@2.1.1: dependencies: chownr: 1.1.4 diff --git a/uno.config.ts b/uno.config.ts index d8ac5a98a4..27743f45b7 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -1,23 +1,43 @@ import { globSync } from 'fast-glob'; import fs from 'node:fs/promises'; -import { basename } from 'node:path'; +import { basename, join } from 'node:path'; import { defineConfig, presetIcons, presetUno, transformerDirectives } from 'unocss'; +import type { IconifyJSON } from '@iconify/types'; -const iconPaths = globSync('./icons/*.svg'); +// Debug: Log the current working directory and icon paths +console.log('CWD:', process.cwd()); + +const iconPaths = globSync(join(process.cwd(), 'public/icons/*.svg')); +console.log('Found icons:', iconPaths); const collectionName = 'bolt'; -const customIconCollection = iconPaths.reduce( - (acc, iconPath) => { - const [iconName] = basename(iconPath).split('.'); +const customIconCollection = { + [collectionName]: iconPaths.reduce( + (acc, iconPath) => { + const [iconName] = basename(iconPath).split('.'); - acc[collectionName] ??= {}; - acc[collectionName][iconName] = async () => fs.readFile(iconPath, 'utf8'); + acc[iconName] = async () => { + try { + const content = await fs.readFile(iconPath, 'utf8'); + return content + .replace(/fill="[^"]*"/g, '') + .replace(/fill='[^']*'/g, '') + .replace(/width="[^"]*"/g, '') + .replace(/height="[^"]*"/g, '') + .replace(/viewBox="[^"]*"/g, 'viewBox="0 0 24 24"') + .replace(/]*)>/, ''); + } catch (error) { + console.error(`Error loading icon ${iconName}:`, error); + return ''; + } + }; - return acc; - }, - {} as Record Promise>>, -); + return acc; + }, + {} as Record Promise>, + ), +}; const BASE_COLORS = { white: '#FFFFFF', @@ -98,9 +118,7 @@ const COLOR_PRIMITIVES = { }; export default defineConfig({ - safelist: [ - ...Object.keys(customIconCollection[collectionName]||{}).map(x=>`i-bolt:${x}`) - ], + safelist: [...Object.keys(customIconCollection[collectionName] || {}).map((x) => `i-bolt:${x}`)], shortcuts: { 'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]', 'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier', @@ -242,9 +260,27 @@ export default defineConfig({ presetIcons({ warn: true, collections: { - ...customIconCollection, + bolt: customIconCollection.bolt, + ph: async () => { + const icons = await import('@iconify-json/ph/icons.json'); + return icons.default as IconifyJSON; + }, + }, + extraProperties: { + display: 'inline-block', + 'vertical-align': 'middle', + width: '24px', + height: '24px', + }, + customizations: { + customize(props) { + return { + ...props, + width: '24px', + height: '24px', + }; + }, }, - unit: 'em', }), ], }); From 8f3f37ae7eabe46d38382a4bd16ff9c736de015a Mon Sep 17 00:00:00 2001 From: Stijnus <72551117+Stijnus@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:55:07 +0100 Subject: [PATCH 02/61] fix --- .windsurf/config.json | 39 ---------------- .windsurf/rules.json | 103 ------------------------------------------ 2 files changed, 142 deletions(-) delete mode 100644 .windsurf/config.json delete mode 100644 .windsurf/rules.json diff --git a/.windsurf/config.json b/.windsurf/config.json deleted file mode 100644 index 27848d341d..0000000000 --- a/.windsurf/config.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "enabled": true, - "rulesPath": ".windsurf/rules.json", - "integration": { - "ide": { - "cursor": true, - "vscode": true - }, - "autoApply": true, - "notifications": true, - "autoFix": { - "enabled": true, - "onSave": true, - "formatOnSave": true, - "suggestImports": true, - "suggestComponents": true - }, - "suggestions": { - "inline": true, - "quickFix": true, - "codeActions": true, - "snippets": true - } - }, - "features": { - "codeCompletion": true, - "linting": true, - "formatting": true, - "importValidation": true, - "dependencyChecks": true, - "uiStandardization": true - }, - "hooks": { - "preCommit": true, - "prePush": true, - "onFileCreate": true, - "onImportAdd": true - } -} diff --git a/.windsurf/rules.json b/.windsurf/rules.json deleted file mode 100644 index a0008b1ef5..0000000000 --- a/.windsurf/rules.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "version": "1.0", - "rules": { - "fileTypes": { - "typescript": ["ts", "tsx"], - "javascript": ["js", "jsx", "mjs", "cjs"], - "json": ["json"], - "markdown": ["md"], - "css": ["css"], - "dockerfile": ["Dockerfile"] - }, - "formatting": { - "typescript": { - "indentSize": 2, - "useTabs": false, - "maxLineLength": 100, - "semicolons": true, - "quotes": "single", - "trailingComma": "es5" - }, - "javascript": { - "indentSize": 2, - "useTabs": false, - "maxLineLength": 100, - "semicolons": true, - "quotes": "single", - "trailingComma": "es5" - } - }, - "linting": { - "typescript": { - "noUnusedVariables": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noConsole": "warn" - } - }, - "dependencies": { - "nodeVersion": ">=18.18.0", - "packageManager": "pnpm", - "requiredFiles": ["package.json", "tsconfig.json", ".env.example"] - }, - "git": { - "ignoredPaths": ["node_modules", "build", ".env", ".env.local"], - "protectedBranches": ["main", "master"] - }, - "testing": { - "framework": "vitest", - "coverage": { - "statements": 70, - "branches": 70, - "functions": 70, - "lines": 70 - } - }, - "security": { - "secrets": { - "patterns": ["API_KEY", "SECRET", "PASSWORD", "TOKEN"], - "locations": [".env", ".env.local"] - } - }, - "commands": { - "dev": "pnpm dev", - "build": "pnpm build", - "test": "pnpm test", - "lint": "pnpm lint", - "typecheck": "pnpm typecheck" - }, - "codeQuality": { - "imports": { - "validateImports": true, - "checkPackageAvailability": true, - "requireExactVersions": true, - "preventUnusedImports": true - }, - "fileManagement": { - "preventUnnecessaryFiles": true, - "requireFileJustification": true, - "checkExistingImplementations": true - }, - "dependencies": { - "autoInstallMissing": false, - "validateVersionCompatibility": true, - "checkPackageJson": true - } - }, - "uiStandards": { - "styling": { - "framework": "tailwind", - "preferredIconSets": ["@iconify-json/ph", "@iconify-json/svg-spinners"], - "colorScheme": { - "useSystemPreference": true, - "supportDarkMode": true - }, - "components": { - "preferModern": true, - "accessibility": true, - "responsive": true - } - } - } - } -} From 436a8e54bf67bbfb010e513ca320a2bdcf0c2e26 Mon Sep 17 00:00:00 2001 From: Stijnus <72551117+Stijnus@users.noreply.github.com> Date: Mon, 20 Jan 2025 09:53:15 +0100 Subject: [PATCH 03/61] ui refactor --- app/components/settings/SettingsWindow.tsx | 300 ---- app/components/settings/data/DataTab.tsx | 16 +- app/components/settings/debug/DebugTab.tsx | 1408 +++++++++-------- .../settings/developer/DeveloperWindow.tsx | 378 +++++ .../settings/developer/TabManagement.tsx | 315 ++++ .../settings/event-logs/EventLogsTab.tsx | 732 +++++---- .../settings/features/FeaturesTab.tsx | 83 +- .../notifications/NotificationsTab.tsx | 143 ++ .../settings/profile/ProfileTab.tsx | 391 ++--- .../settings/providers/ProvidersTab.tsx | 46 +- app/components/settings/settings.styles.ts | 16 +- app/components/settings/settings.types.ts | 65 +- .../settings/settings/SettingsTab.tsx | 215 +++ .../settings/shared/DraggableTabList.tsx | 158 ++ app/components/settings/shared/TabTile.tsx | 230 +++ app/components/settings/update/UpdateTab.tsx | 217 +++ .../settings/user/ProfileHeader.tsx | 25 + app/components/settings/user/UsersWindow.tsx | 385 +++++ app/components/sidebar/Menu.client.tsx | 4 +- app/components/ui/Dropdown.tsx | 63 + app/components/workbench/Preview.tsx | 160 +- app/lib/api/connection.ts | 18 + app/lib/api/debug.ts | 65 + app/lib/api/features.ts | 35 + app/lib/api/notifications.ts | 40 + app/lib/api/updates.ts | 28 + app/lib/hooks/index.ts | 5 + app/lib/hooks/useConnectionStatus.ts | 61 + app/lib/hooks/useDebugStatus.ts | 89 ++ app/lib/hooks/useFeatures.ts | 72 + app/lib/hooks/useNotifications.ts | 77 + app/lib/hooks/useSettings.ts | 222 +++ app/lib/hooks/useSettings.tsx | 229 --- app/lib/hooks/useUpdateCheck.ts | 58 + app/lib/modules/llm/providers/github.ts | 3 +- app/lib/stores/logs.ts | 83 +- app/lib/stores/settings.ts | 52 +- app/root.tsx | 6 +- app/utils/localStorage.ts | 17 + package.json | 2 + pnpm-lock.yaml | 83 + 41 files changed, 4690 insertions(+), 1905 deletions(-) delete mode 100644 app/components/settings/SettingsWindow.tsx create mode 100644 app/components/settings/developer/DeveloperWindow.tsx create mode 100644 app/components/settings/developer/TabManagement.tsx create mode 100644 app/components/settings/notifications/NotificationsTab.tsx create mode 100644 app/components/settings/settings/SettingsTab.tsx create mode 100644 app/components/settings/shared/DraggableTabList.tsx create mode 100644 app/components/settings/shared/TabTile.tsx create mode 100644 app/components/settings/update/UpdateTab.tsx create mode 100644 app/components/settings/user/ProfileHeader.tsx create mode 100644 app/components/settings/user/UsersWindow.tsx create mode 100644 app/components/ui/Dropdown.tsx create mode 100644 app/lib/api/connection.ts create mode 100644 app/lib/api/debug.ts create mode 100644 app/lib/api/features.ts create mode 100644 app/lib/api/notifications.ts create mode 100644 app/lib/api/updates.ts create mode 100644 app/lib/hooks/useConnectionStatus.ts create mode 100644 app/lib/hooks/useDebugStatus.ts create mode 100644 app/lib/hooks/useFeatures.ts create mode 100644 app/lib/hooks/useNotifications.ts create mode 100644 app/lib/hooks/useSettings.ts delete mode 100644 app/lib/hooks/useSettings.tsx create mode 100644 app/lib/hooks/useUpdateCheck.ts create mode 100644 app/utils/localStorage.ts diff --git a/app/components/settings/SettingsWindow.tsx b/app/components/settings/SettingsWindow.tsx deleted file mode 100644 index 6d19c99fd4..0000000000 --- a/app/components/settings/SettingsWindow.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import * as RadixDialog from '@radix-ui/react-dialog'; -import { motion, AnimatePresence } from 'framer-motion'; -import { useState } from 'react'; -import { classNames } from '~/utils/classNames'; -import { DialogTitle } from '~/components/ui/Dialog'; -import type { SettingCategory, TabType } from './settings.types'; -import { categoryLabels, categoryIcons } from './settings.types'; -import ProfileTab from './profile/ProfileTab'; -import ProvidersTab from './providers/ProvidersTab'; -import { useSettings } from '~/lib/hooks/useSettings'; -import FeaturesTab from './features/FeaturesTab'; -import DebugTab from './debug/DebugTab'; -import EventLogsTab from './event-logs/EventLogsTab'; -import ConnectionsTab from './connections/ConnectionsTab'; -import DataTab from './data/DataTab'; - -interface SettingsProps { - open: boolean; - onClose: () => void; -} - -export const SettingsWindow = ({ open, onClose }: SettingsProps) => { - const { debug, eventLogs } = useSettings(); - const [searchQuery, setSearchQuery] = useState(''); - const [activeTab, setActiveTab] = useState(null); - - const settingItems = [ - { - id: 'profile' as const, - label: 'Profile Settings', - icon: 'i-ph:user-circle', - category: 'profile' as const, - description: 'Manage your personal information and preferences', - component: () => , - keywords: ['profile', 'account', 'avatar', 'email', 'name', 'theme', 'notifications'], - }, - - { - id: 'data' as const, - label: 'Data Management', - icon: 'i-ph:database', - category: 'file_sharing' as const, - description: 'Manage your chat history and application data', - component: () => , - keywords: ['data', 'export', 'import', 'backup', 'delete'], - }, - - { - id: 'providers' as const, - label: 'Providers', - icon: 'i-ph:key', - category: 'file_sharing' as const, - description: 'Configure AI providers and API keys', - component: () => , - keywords: ['api', 'keys', 'providers', 'configuration'], - }, - - { - id: 'connection' as const, - label: 'Connection', - icon: 'i-ph:link', - category: 'connectivity' as const, - description: 'Manage network and connection settings', - component: () => , - keywords: ['network', 'connection', 'proxy', 'ssl'], - }, - - { - id: 'features' as const, - label: 'Features', - icon: 'i-ph:star', - category: 'system' as const, - description: 'Configure application features and preferences', - component: () => , - keywords: ['features', 'settings', 'options'], - }, - ] as const; - - const debugItems = debug - ? [ - { - id: 'debug' as const, - label: 'Debug', - icon: 'i-ph:bug', - category: 'system' as const, - description: 'Advanced debugging tools and options', - component: () => , - keywords: ['debug', 'logs', 'developer'], - }, - ] - : []; - - const eventLogItems = eventLogs - ? [ - { - id: 'event-logs' as const, - label: 'Event Logs', - icon: 'i-ph:list-bullets', - category: 'system' as const, - description: 'View system events and application logs', - component: () => , - keywords: ['logs', 'events', 'history'], - }, - ] - : []; - - const allSettingItems = [...settingItems, ...debugItems, ...eventLogItems]; - - const filteredItems = allSettingItems.filter( - (item) => - item.label.toLowerCase().includes(searchQuery.toLowerCase()) || - item.description?.toLowerCase().includes(searchQuery.toLowerCase()) || - item.keywords?.some((keyword) => keyword.toLowerCase().includes(searchQuery.toLowerCase())), - ); - - const groupedItems = filteredItems.reduce( - (acc, item) => { - if (!acc[item.category]) { - acc[item.category] = allSettingItems.filter((i) => i.category === item.category); - } - - return acc; - }, - {} as Record, - ); - - const handleBackToDashboard = () => { - setActiveTab(null); - onClose(); - }; - - const activeTabItem = allSettingItems.find((item) => item.id === activeTab); - - return ( - - -
- - - - - - - {activeTab ? ( - -
-
- - -
|
- - {activeTabItem && ( -
-
-
-

- {activeTabItem.label} -

-

{activeTabItem.description}

-
-
- )} -
- - -
-
- {allSettingItems.find((item) => item.id === activeTab)?.component()} -
- - ) : ( - -
-
-
- - Bolt Control Panel - -
-
-
- setSearchQuery(e.target.value)} - className={classNames( - 'w-full h-10 pl-10 pr-4 rounded-lg text-sm', - 'bg-[#F8F8F8] dark:bg-[#1A1A1A]', - 'border border-[#E5E5E5] dark:border-[#333333]', - 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', - 'focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all', - )} - /> -
-
-
-
- -
-
- -
-
- {(Object.keys(groupedItems) as SettingCategory[]).map((category) => ( -
-
-
-

- {categoryLabels[category]} -

-
-
- {groupedItems[category].map((item) => ( - - ))} -
-
- ))} -
-
- - )} - - - -
- - - ); -}; diff --git a/app/components/settings/data/DataTab.tsx b/app/components/settings/data/DataTab.tsx index 9883507052..08ec75aa93 100644 --- a/app/components/settings/data/DataTab.tsx +++ b/app/components/settings/data/DataTab.tsx @@ -185,11 +185,15 @@ export default function DataTab() { localStorage.removeItem('bolt_settings'); localStorage.removeItem('bolt_chat_history'); - // Reload the page to apply reset + // Close the dialog first + setShowResetInlineConfirm(false); + + // Then reload and show success message window.location.reload(); toast.success('Settings reset successfully'); } catch (error) { console.error('Reset error:', error); + setShowResetInlineConfirm(false); toast.error('Failed to reset settings'); } finally { setIsResetting(false); @@ -202,9 +206,15 @@ export default function DataTab() { try { // Clear chat history localStorage.removeItem('bolt_chat_history'); + + // Close the dialog first + setShowDeleteInlineConfirm(false); + + // Then show the success message toast.success('Chat history deleted successfully'); } catch (error) { console.error('Delete error:', error); + setShowDeleteInlineConfirm(false); toast.error('Failed to delete chat history'); } finally { setIsDeleting(false); @@ -216,7 +226,7 @@ export default function DataTab() { {/* Reset Settings Dialog */} - +
@@ -252,7 +262,7 @@ export default function DataTab() { {/* Delete Confirmation Dialog */} - +
diff --git a/app/components/settings/debug/DebugTab.tsx b/app/components/settings/debug/DebugTab.tsx index e8e74e601a..330e2cb400 100644 --- a/app/components/settings/debug/DebugTab.tsx +++ b/app/components/settings/debug/DebugTab.tsx @@ -1,727 +1,869 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useSettings } from '~/lib/hooks/useSettings'; +import React, { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; -import { providerBaseUrlEnvKeys } from '~/utils/constants'; -import { motion } from 'framer-motion'; import { classNames } from '~/utils/classNames'; -import { settingsStyles } from '~/components/settings/settings.styles'; +import { logStore } from '~/lib/stores/logs'; interface ProviderStatus { + id: string; name: string; - enabled: boolean; - isLocal: boolean; - isRunning: boolean | null; + status: 'online' | 'offline' | 'error'; error?: string; - lastChecked: Date; - responseTime?: number; - url: string | null; } interface SystemInfo { os: string; - browser: string; - screen: string; - language: string; - timezone: string; - memory: string; - cores: number; - deviceType: string; - colorDepth: string; - pixelRatio: number; - online: boolean; - cookiesEnabled: boolean; - doNotTrack: boolean; -} - -interface IProviderConfig { - name: string; - settings: { - enabled: boolean; - baseUrl?: string; + arch: string; + platform: string; + cpus: string; + memory: { + total: string; + free: string; + used: string; + percentage: number; + }; + node: string; + browser: { + name: string; + version: string; + language: string; + userAgent: string; + cookiesEnabled: boolean; + online: boolean; + platform: string; + cores: number; + }; + screen: { + width: number; + height: number; + colorDepth: number; + pixelRatio: number; + }; + time: { + timezone: string; + offset: number; + locale: string; + }; + performance: { + memory: { + jsHeapSizeLimit: number; + totalJSHeapSize: number; + usedJSHeapSize: number; + usagePercentage: number; + }; + timing: { + loadTime: number; + domReadyTime: number; + readyStart: number; + redirectTime: number; + appcacheTime: number; + unloadEventTime: number; + lookupDomainTime: number; + connectTime: number; + requestTime: number; + initDomTreeTime: number; + loadEventTime: number; + }; + navigation: { + type: number; + redirectCount: number; + }; + }; + network: { + downlink: number; + effectiveType: string; + rtt: number; + saveData: boolean; + type: string; + }; + battery?: { + charging: boolean; + chargingTime: number; + dischargingTime: number; + level: number; + }; + storage: { + quota: number; + usage: number; + persistent: boolean; + temporary: boolean; }; } -interface CommitData { - commit: string; - version?: string; -} - -const connitJson: CommitData = { - commit: __COMMIT_HASH, - version: __APP_VERSION, -}; - -const LOCAL_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike']; +export default function DebugTab() { + const [providerStatuses, setProviderStatuses] = useState([]); + const [systemInfo, setSystemInfo] = useState(null); + const [loading, setLoading] = useState({ + systemInfo: false, + providers: false, + performance: false, + errors: false, + }); + const [errorLog, setErrorLog] = useState<{ + errors: any[]; + lastCheck: string | null; + }>({ + errors: [], + lastCheck: null, + }); + + // Fetch initial data + useEffect(() => { + checkProviderStatus(); + getSystemInfo(); + }, []); -const versionHash = connitJson.commit; -const versionTag = connitJson.version; + // Set up error listeners when component mounts + useEffect(() => { + const errors: any[] = []; + + const handleError = (event: ErrorEvent) => { + errors.push({ + type: 'error', + message: event.message, + filename: event.filename, + lineNumber: event.lineno, + columnNumber: event.colno, + error: event.error, + timestamp: new Date().toISOString(), + }); + }; -const GITHUB_URLS = { - original: 'https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/main', - fork: 'https://api.github.com/repos/Stijnus/bolt.new-any-llm/commits/main', - commitJson: async (branch: string) => { - try { - const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`); - const data: { sha: string } = await response.json(); + const handleRejection = (event: PromiseRejectionEvent) => { + errors.push({ + type: 'unhandledRejection', + reason: event.reason, + timestamp: new Date().toISOString(), + }); + }; - const packageJsonResp = await fetch( - `https://raw.githubusercontent.com/stackblitz-labs/bolt.diy/${branch}/package.json`, - ); - const packageJson: { version: string } = await packageJsonResp.json(); + window.addEventListener('error', handleError); + window.addEventListener('unhandledrejection', handleRejection); - return { - commit: data.sha.slice(0, 7), - version: packageJson.version, - }; - } catch (error) { - console.log('Failed to fetch local commit info:', error); - throw new Error('Failed to fetch local commit info'); - } - }, -}; + return () => { + window.removeEventListener('error', handleError); + window.removeEventListener('unhandledrejection', handleRejection); + }; + }, []); -function getSystemInfo(): SystemInfo { - const formatBytes = (bytes: number): string => { - if (bytes === 0) { - return '0 Bytes'; - } + const checkProviderStatus = async () => { + try { + setLoading((prev) => ({ ...prev, providers: true })); - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); + // Fetch real provider statuses + const providers: ProviderStatus[] = []; - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - }; + // Check OpenAI status + try { + const openaiResponse = await fetch('/api/providers/openai/status'); + providers.push({ + id: 'openai', + name: 'OpenAI', + status: openaiResponse.ok ? 'online' : 'error', + error: !openaiResponse.ok ? 'API Error' : undefined, + }); + } catch { + providers.push({ id: 'openai', name: 'OpenAI', status: 'offline' }); + } - const getBrowserInfo = (): string => { - const ua = navigator.userAgent; - let browser = 'Unknown'; - - if (ua.includes('Firefox/')) { - browser = 'Firefox'; - } else if (ua.includes('Chrome/')) { - if (ua.includes('Edg/')) { - browser = 'Edge'; - } else if (ua.includes('OPR/')) { - browser = 'Opera'; - } else { - browser = 'Chrome'; + // Check Anthropic status + try { + const anthropicResponse = await fetch('/api/providers/anthropic/status'); + providers.push({ + id: 'anthropic', + name: 'Anthropic', + status: anthropicResponse.ok ? 'online' : 'error', + error: !anthropicResponse.ok ? 'API Error' : undefined, + }); + } catch { + providers.push({ id: 'anthropic', name: 'Anthropic', status: 'offline' }); } - } else if (ua.includes('Safari/')) { - if (!ua.includes('Chrome')) { - browser = 'Safari'; + + // Check Local Models status + try { + const localResponse = await fetch('/api/providers/local/status'); + providers.push({ + id: 'local', + name: 'Local Models', + status: localResponse.ok ? 'online' : 'error', + error: !localResponse.ok ? 'API Error' : undefined, + }); + } catch { + providers.push({ id: 'local', name: 'Local Models', status: 'offline' }); } - } - // Extract version number - const match = ua.match(new RegExp(`${browser}\\/([\\d.]+)`)); - const version = match ? ` ${match[1]}` : ''; + // Check Ollama status + try { + const ollamaResponse = await fetch('/api/providers/ollama/status'); + providers.push({ + id: 'ollama', + name: 'Ollama', + status: ollamaResponse.ok ? 'online' : 'error', + error: !ollamaResponse.ok ? 'API Error' : undefined, + }); + } catch { + providers.push({ id: 'ollama', name: 'Ollama', status: 'offline' }); + } - return `${browser}${version}`; + setProviderStatuses(providers); + toast.success('Provider status updated'); + } catch (error) { + toast.error('Failed to check provider status'); + console.error('Failed to check provider status:', error); + } finally { + setLoading((prev) => ({ ...prev, providers: false })); + } }; - const getOperatingSystem = (): string => { - const ua = navigator.userAgent; - const platform = navigator.platform; - - if (ua.includes('Win')) { - return 'Windows'; - } + const getSystemInfo = async () => { + try { + setLoading((prev) => ({ ...prev, systemInfo: true })); + + // Get browser info + const ua = navigator.userAgent; + const browserName = ua.includes('Firefox') + ? 'Firefox' + : ua.includes('Chrome') + ? 'Chrome' + : ua.includes('Safari') + ? 'Safari' + : ua.includes('Edge') + ? 'Edge' + : 'Unknown'; + const browserVersion = ua.match(/(Firefox|Chrome|Safari|Edge)\/([0-9.]+)/)?.[2] || 'Unknown'; + + // Get performance metrics + const memory = (performance as any).memory || {}; + const timing = performance.timing; + const navigation = performance.navigation; + const connection = (navigator as any).connection; + + // Get battery info + let batteryInfo; - if (ua.includes('Mac')) { - if (ua.includes('iPhone') || ua.includes('iPad')) { - return 'iOS'; + try { + const battery = await (navigator as any).getBattery(); + batteryInfo = { + charging: battery.charging, + chargingTime: battery.chargingTime, + dischargingTime: battery.dischargingTime, + level: battery.level * 100, + }; + } catch { + console.log('Battery API not supported'); } - return 'macOS'; - } + // Get storage info + let storageInfo = { + quota: 0, + usage: 0, + persistent: false, + temporary: false, + }; - if (ua.includes('Linux')) { - return 'Linux'; - } + try { + const storage = await navigator.storage.estimate(); + const persistent = await navigator.storage.persist(); + storageInfo = { + quota: storage.quota || 0, + usage: storage.usage || 0, + persistent, + temporary: !persistent, + }; + } catch { + console.log('Storage API not supported'); + } - if (ua.includes('Android')) { - return 'Android'; - } + // Get memory info from browser performance API + const performanceMemory = (performance as any).memory || {}; + const totalMemory = performanceMemory.jsHeapSizeLimit || 0; + const usedMemory = performanceMemory.usedJSHeapSize || 0; + const freeMemory = totalMemory - usedMemory; + const memoryPercentage = totalMemory ? (usedMemory / totalMemory) * 100 : 0; + + const systemInfo: SystemInfo = { + os: navigator.platform, + arch: navigator.userAgent.includes('x64') ? 'x64' : navigator.userAgent.includes('arm') ? 'arm' : 'unknown', + platform: navigator.platform, + cpus: navigator.hardwareConcurrency + ' cores', + memory: { + total: formatBytes(totalMemory), + free: formatBytes(freeMemory), + used: formatBytes(usedMemory), + percentage: Math.round(memoryPercentage), + }, + node: 'browser', + browser: { + name: browserName, + version: browserVersion, + language: navigator.language, + userAgent: navigator.userAgent, + cookiesEnabled: navigator.cookieEnabled, + online: navigator.onLine, + platform: navigator.platform, + cores: navigator.hardwareConcurrency, + }, + screen: { + width: window.screen.width, + height: window.screen.height, + colorDepth: window.screen.colorDepth, + pixelRatio: window.devicePixelRatio, + }, + time: { + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + offset: new Date().getTimezoneOffset(), + locale: navigator.language, + }, + performance: { + memory: { + jsHeapSizeLimit: memory.jsHeapSizeLimit || 0, + totalJSHeapSize: memory.totalJSHeapSize || 0, + usedJSHeapSize: memory.usedJSHeapSize || 0, + usagePercentage: memory.totalJSHeapSize ? (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100 : 0, + }, + timing: { + loadTime: timing.loadEventEnd - timing.navigationStart, + domReadyTime: timing.domContentLoadedEventEnd - timing.navigationStart, + readyStart: timing.fetchStart - timing.navigationStart, + redirectTime: timing.redirectEnd - timing.redirectStart, + appcacheTime: timing.domainLookupStart - timing.fetchStart, + unloadEventTime: timing.unloadEventEnd - timing.unloadEventStart, + lookupDomainTime: timing.domainLookupEnd - timing.domainLookupStart, + connectTime: timing.connectEnd - timing.connectStart, + requestTime: timing.responseEnd - timing.requestStart, + initDomTreeTime: timing.domInteractive - timing.responseEnd, + loadEventTime: timing.loadEventEnd - timing.loadEventStart, + }, + navigation: { + type: navigation.type, + redirectCount: navigation.redirectCount, + }, + }, + network: { + downlink: connection?.downlink || 0, + effectiveType: connection?.effectiveType || 'unknown', + rtt: connection?.rtt || 0, + saveData: connection?.saveData || false, + type: connection?.type || 'unknown', + }, + battery: batteryInfo, + storage: storageInfo, + }; - return platform || 'Unknown'; + setSystemInfo(systemInfo); + toast.success('System information updated'); + } catch (error) { + toast.error('Failed to get system information'); + console.error('Failed to get system information:', error); + } finally { + setLoading((prev) => ({ ...prev, systemInfo: false })); + } }; - const getDeviceType = (): string => { - const ua = navigator.userAgent; + // Helper function to format bytes to human readable format + const formatBytes = (bytes: number) => { + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; - if (ua.includes('Mobile')) { - return 'Mobile'; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; } - if (ua.includes('Tablet')) { - return 'Tablet'; - } - - return 'Desktop'; + return `${Math.round(size)} ${units[unitIndex]}`; }; - // Get more detailed memory info if available - const getMemoryInfo = (): string => { - if ('memory' in performance) { - const memory = (performance as any).memory; - return `${formatBytes(memory.jsHeapSizeLimit)} (Used: ${formatBytes(memory.usedJSHeapSize)})`; + const handleLogSystemInfo = () => { + if (!systemInfo) { + return; } - return 'Not available'; + logStore.logSystem('System Information', { + os: systemInfo.os, + arch: systemInfo.arch, + cpus: systemInfo.cpus, + memory: systemInfo.memory, + node: systemInfo.node, + }); + toast.success('System information logged'); }; - return { - os: getOperatingSystem(), - browser: getBrowserInfo(), - screen: `${window.screen.width}x${window.screen.height}`, - language: navigator.language, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - memory: getMemoryInfo(), - cores: navigator.hardwareConcurrency || 0, - deviceType: getDeviceType(), - - // Add new fields - colorDepth: `${window.screen.colorDepth}-bit`, - pixelRatio: window.devicePixelRatio, - online: navigator.onLine, - cookiesEnabled: navigator.cookieEnabled, - doNotTrack: navigator.doNotTrack === '1', - }; -} + const handleLogPerformance = () => { + try { + setLoading((prev) => ({ ...prev, performance: true })); -const checkProviderStatus = async (url: string | null, providerName: string): Promise => { - if (!url) { - console.log(`[Debug] No URL provided for ${providerName}`); - return { - name: providerName, - enabled: false, - isLocal: true, - isRunning: false, - error: 'No URL configured', - lastChecked: new Date(), - url: null, - }; - } + // Get performance metrics using modern Performance API + const performanceEntries = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; + const memory = (performance as any).memory; - console.log(`[Debug] Checking status for ${providerName} at ${url}`); + // Calculate timing metrics + const timingMetrics = { + loadTime: performanceEntries.loadEventEnd - performanceEntries.startTime, + domReadyTime: performanceEntries.domContentLoadedEventEnd - performanceEntries.startTime, + fetchTime: performanceEntries.responseEnd - performanceEntries.fetchStart, + redirectTime: performanceEntries.redirectEnd - performanceEntries.redirectStart, + dnsTime: performanceEntries.domainLookupEnd - performanceEntries.domainLookupStart, + tcpTime: performanceEntries.connectEnd - performanceEntries.connectStart, + ttfb: performanceEntries.responseStart - performanceEntries.requestStart, + processingTime: performanceEntries.loadEventEnd - performanceEntries.responseEnd, + }; - const startTime = performance.now(); + // Get resource timing data + const resourceEntries = performance.getEntriesByType('resource'); + const resourceStats = { + totalResources: resourceEntries.length, + totalSize: resourceEntries.reduce((total, entry) => total + (entry as any).transferSize || 0, 0), + totalTime: Math.max(...resourceEntries.map((entry) => entry.duration)), + }; - try { - if (providerName.toLowerCase() === 'ollama') { - // Special check for Ollama root endpoint - try { - console.log(`[Debug] Checking Ollama root endpoint: ${url}`); + // Get memory metrics + const memoryMetrics = memory + ? { + jsHeapSizeLimit: memory.jsHeapSizeLimit, + totalJSHeapSize: memory.totalJSHeapSize, + usedJSHeapSize: memory.usedJSHeapSize, + heapUtilization: (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100, + } + : null; - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout + // Get frame rate metrics + let fps = 0; - const response = await fetch(url, { - signal: controller.signal, - headers: { - Accept: 'text/plain,application/json', - }, - }); - clearTimeout(timeoutId); - - const text = await response.text(); - console.log(`[Debug] Ollama root response:`, text); - - if (text.includes('Ollama is running')) { - console.log(`[Debug] Ollama running confirmed via root endpoint`); - return { - name: providerName, - enabled: false, - isLocal: true, - isRunning: true, - lastChecked: new Date(), - responseTime: performance.now() - startTime, - url, - }; - } - } catch (error) { - console.log(`[Debug] Ollama root check failed:`, error); - - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - - if (errorMessage.includes('aborted')) { - return { - name: providerName, - enabled: false, - isLocal: true, - isRunning: false, - error: 'Connection timeout', - lastChecked: new Date(), - responseTime: performance.now() - startTime, - url, - }; - } - } - } + if ('requestAnimationFrame' in window) { + const times: number[] = []; - // Try different endpoints based on provider - const checkUrls = [`${url}/api/health`, url.endsWith('v1') ? `${url}/models` : `${url}/v1/models`]; - console.log(`[Debug] Checking additional endpoints:`, checkUrls); - - const results = await Promise.all( - checkUrls.map(async (checkUrl) => { - try { - console.log(`[Debug] Trying endpoint: ${checkUrl}`); - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); - - const response = await fetch(checkUrl, { - signal: controller.signal, - headers: { - Accept: 'application/json', - }, - }); - clearTimeout(timeoutId); - - const ok = response.ok; - console.log(`[Debug] Endpoint ${checkUrl} response:`, ok); - - if (ok) { - try { - const data = await response.json(); - console.log(`[Debug] Endpoint ${checkUrl} data:`, data); - } catch { - console.log(`[Debug] Could not parse JSON from ${checkUrl}`); - } + function calculateFPS(now: number) { + times.push(now); + + if (times.length > 10) { + const fps = Math.round((1000 * 10) / (now - times[0])); + times.shift(); + + return fps; } - return ok; - } catch (error) { - console.log(`[Debug] Endpoint ${checkUrl} failed:`, error); - return false; + requestAnimationFrame(calculateFPS); + + return 0; } - }), - ); - - const isRunning = results.some((result) => result); - console.log(`[Debug] Final status for ${providerName}:`, isRunning); - - return { - name: providerName, - enabled: false, - isLocal: true, - isRunning, - lastChecked: new Date(), - responseTime: performance.now() - startTime, - url, - }; - } catch (error) { - console.log(`[Debug] Provider check failed for ${providerName}:`, error); - return { - name: providerName, - enabled: false, - isLocal: true, - isRunning: false, - error: error instanceof Error ? error.message : 'Unknown error', - lastChecked: new Date(), - responseTime: performance.now() - startTime, - url, - }; - } -}; -export default function DebugTab() { - const { providers, isLatestBranch } = useSettings(); - const [activeProviders, setActiveProviders] = useState([]); - const [updateMessage, setUpdateMessage] = useState(''); - const [systemInfo] = useState(getSystemInfo()); - const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); - - const updateProviderStatuses = async () => { - if (!providers) { - return; - } + fps = calculateFPS(performance.now()); + } - try { - const entries = Object.entries(providers) as [string, IProviderConfig][]; - const statuses = await Promise.all( - entries - .filter(([, provider]) => LOCAL_PROVIDERS.includes(provider.name)) - .map(async ([, provider]) => { - const envVarName = - providerBaseUrlEnvKeys[provider.name].baseUrlKey || `REACT_APP_${provider.name.toUpperCase()}_URL`; - - // Access environment variables through import.meta.env - let settingsUrl = provider.settings.baseUrl; - - if (settingsUrl && settingsUrl.trim().length === 0) { - settingsUrl = undefined; - } - - const url = settingsUrl || import.meta.env[envVarName] || null; // Ensure baseUrl is used - console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`); - - const status = await checkProviderStatus(url, provider.name); - - return { - ...status, - enabled: provider.settings.enabled ?? false, - }; - }), - ); - - setActiveProviders(statuses); + // Log all performance metrics + logStore.logSystem('Performance Metrics', { + timing: timingMetrics, + resources: resourceStats, + memory: memoryMetrics, + fps, + timestamp: new Date().toISOString(), + navigationEntry: { + type: performanceEntries.type, + redirectCount: performanceEntries.redirectCount, + }, + }); + + toast.success('Performance metrics logged'); } catch (error) { - console.error('[Debug] Failed to update provider statuses:', error); + toast.error('Failed to log performance metrics'); + console.error('Failed to log performance metrics:', error); + } finally { + setLoading((prev) => ({ ...prev, performance: false })); } }; - useEffect(() => { - updateProviderStatuses(); - - const interval = setInterval(updateProviderStatuses, 30000); - - return () => clearInterval(interval); - }, [providers]); - - const handleCheckForUpdate = useCallback(async () => { - if (isCheckingUpdate) { - return; - } - + const handleCheckErrors = () => { try { - setIsCheckingUpdate(true); - setUpdateMessage('Checking for updates...'); - - const branchToCheck = isLatestBranch ? 'main' : 'stable'; - console.log(`[Debug] Checking for updates against ${branchToCheck} branch`); - - const latestCommitResp = await GITHUB_URLS.commitJson(branchToCheck); - - const remoteCommitHash = latestCommitResp.commit; - const currentCommitHash = versionHash; - - if (remoteCommitHash !== currentCommitHash) { - setUpdateMessage( - `Update available from ${branchToCheck} branch!\n` + - `Current: ${currentCommitHash.slice(0, 7)}\n` + - `Latest: ${remoteCommitHash.slice(0, 7)}`, - ); + setLoading((prev) => ({ ...prev, errors: true })); + + // Get any errors from the performance entries + const resourceErrors = performance + .getEntriesByType('resource') + .filter((entry) => { + const failedEntry = entry as PerformanceResourceTiming; + return failedEntry.responseEnd - failedEntry.startTime === 0; + }) + .map((entry) => ({ + type: 'networkError', + resource: entry.name, + timestamp: new Date().toISOString(), + })); + + // Combine collected errors with resource errors + const allErrors = [...errorLog.errors, ...resourceErrors]; + + if (allErrors.length > 0) { + logStore.logError('JavaScript Errors Found', { + errors: allErrors, + timestamp: new Date().toISOString(), + }); + toast.error(`Found ${allErrors.length} error(s)`); } else { - setUpdateMessage(`You are on the latest version from the ${branchToCheck} branch`); + toast.success('No errors found'); } + + // Update error log + setErrorLog({ + errors: allErrors, + lastCheck: new Date().toISOString(), + }); } catch (error) { - setUpdateMessage('Failed to check for updates'); - console.error('[Debug] Failed to check for updates:', error); + toast.error('Failed to check for errors'); + console.error('Failed to check for errors:', error); } finally { - setIsCheckingUpdate(false); + setLoading((prev) => ({ ...prev, errors: false })); } - }, [isCheckingUpdate, isLatestBranch]); - - const handleCopyToClipboard = useCallback(() => { - const debugInfo = { - System: systemInfo, - Providers: activeProviders.map((provider) => ({ - name: provider.name, - enabled: provider.enabled, - isLocal: provider.isLocal, - running: provider.isRunning, - error: provider.error, - lastChecked: provider.lastChecked, - responseTime: provider.responseTime, - url: provider.url, - })), - Version: { - hash: versionHash.slice(0, 7), - branch: isLatestBranch ? 'main' : 'stable', - }, - Timestamp: new Date().toISOString(), - }; - - navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => { - toast.success('Debug information copied to clipboard!'); - }); - }, [activeProviders, systemInfo, isLatestBranch]); + }; return (
-
-
-
-

Debug Information

-
-
- -
- Copy Debug Info - - - {isCheckingUpdate ? ( - <> -
- Checking... - - ) : ( - <> -
- Check for Updates - - )} - -
-
- - {updateMessage && ( - -
-
+
+
+
+

System Information

+
+
+ +
- - )} - -
- -
-
-

System Information

+ > +
+ Refresh +
- -
-
-
-
-

Operating System

-
-

{systemInfo.os}

+
+ {systemInfo ? ( +
+
+
+
+ OS: + {systemInfo.os}
-
-
-
-

Device Type

-
-

{systemInfo.deviceType}

+
+
+ Platform: + {systemInfo.platform}
-
-
-
-

Browser

-
-

{systemInfo.browser}

+
+
+ Architecture: + {systemInfo.arch}
-
-
-
-

Display

-
-

- {systemInfo.screen} ({systemInfo.colorDepth}) @{systemInfo.pixelRatio}x -

+
+
+ CPU Cores: + {systemInfo.cpus}
-
-
-
-

Connection

-
-
- - - {systemInfo.online ? 'Online' : 'Offline'} +
+
+ Node Version: + {systemInfo.node} +
+
+
+ Network Type: + + {systemInfo.network.type} ({systemInfo.network.effectiveType}) + +
+
+
+ Network Speed: + + {systemInfo.network.downlink}Mbps (RTT: {systemInfo.network.rtt}ms) + +
+ {systemInfo.battery && ( +
+
+ Battery: + + {systemInfo.battery.level.toFixed(1)}% {systemInfo.battery.charging ? '(Charging)' : ''}
+ )} +
+
+ Storage: + + {(systemInfo.storage.usage / (1024 * 1024 * 1024)).toFixed(2)}GB /{' '} + {(systemInfo.storage.quota / (1024 * 1024 * 1024)).toFixed(2)}GB +
-
-
-
-

Language

-
-

{systemInfo.language}

+
+
+
+
+ Memory Usage: + + {systemInfo.memory.used} / {systemInfo.memory.total} ({systemInfo.memory.percentage}%) +
-
-
-
-

Timezone

-
-

{systemInfo.timezone}

+
+
+ Browser: + + {systemInfo.browser.name} {systemInfo.browser.version} +
-
-
-
-

CPU Cores

-
-

{systemInfo.cores}

+
+
+ Screen: + + {systemInfo.screen.width}x{systemInfo.screen.height} ({systemInfo.screen.pixelRatio}x) + +
+
+
+ Timezone: + {systemInfo.time.timezone} +
+
+
+ Language: + {systemInfo.browser.language} +
+
+
+ JS Heap: + + {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '} + {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB ( + {systemInfo.performance.memory.usagePercentage.toFixed(1)}%) + +
+
+
+ Page Load: + + {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s + +
+
+
+ DOM Ready: + + {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s +
-
-
-
-

Version

+
+ ) : ( +
Loading system information...
+ )} +
+ + {/* Provider Status */} +
+
+
+
+

Provider Status

+
+ +
+
+ {providerStatuses.map((provider) => ( +
+
+
+ {provider.name}
-

- {connitJson.commit.slice(0, 7)} - - (v{versionTag || '0.0.1'}) - {isLatestBranch ? 'nightly' : 'stable'} + {provider.status} +

+ ))} +
+
+ + {/* Performance Metrics */} +
+
+
+
+

Performance Metrics

+
+ +
+ {systemInfo && ( +
+
+
+ Page Load Time: + + {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s + +
+
+ DOM Ready Time: + + {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s + +
+
+ Request Time: + + {(systemInfo.performance.timing.requestTime / 1000).toFixed(2)}s -

+
+
+ Redirect Time: + + {(systemInfo.performance.timing.redirectTime / 1000).toFixed(2)}s + +
+
+
+
+ JS Heap Usage: + + {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '} + {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB + +
+
+ Heap Utilization: + + {systemInfo.performance.memory.usagePercentage.toFixed(1)}% + +
+
+ Navigation Type: + + {systemInfo.performance.navigation.type === 0 + ? 'Navigate' + : systemInfo.performance.navigation.type === 1 + ? 'Reload' + : systemInfo.performance.navigation.type === 2 + ? 'Back/Forward' + : 'Other'} + +
+
+ Redirects: + + {systemInfo.performance.navigation.redirectCount} + +
- - - - -
-
-

Local LLM Status

- -
- {activeProviders.map((provider) => ( -
-
-
-
-
-
-
-

{provider.name}

- {provider.url && ( -

- {provider.url} -

- )} -
-
-
- - {provider.enabled ? 'Enabled' : 'Disabled'} - - {provider.enabled && ( - - {provider.isRunning ? 'Running' : 'Not Running'} - - )} -
-
+ )} +
-
-
- - Last checked: {new Date(provider.lastChecked).toLocaleTimeString()} - - {provider.responseTime && ( - - Response time: {Math.round(provider.responseTime)}ms - - )} -
- - {provider.error && ( -
- Error: {provider.error} -
- )} - - {provider.url && ( -
- Endpoints checked: -
    -
  • {provider.url} (root)
  • -
  • {provider.url}/api/health
  • -
  • {provider.url}/v1/models
  • -
-
- )} + {/* Error Check */} +
+
+
+
+

Error Check

+
+ +
+
+
+ Checks for: +
    +
  • Unhandled JavaScript errors
  • +
  • Unhandled Promise rejections
  • +
  • Runtime exceptions
  • +
  • Network errors
  • +
+
+
+ Last Check: + + {loading.errors + ? 'Checking...' + : errorLog.lastCheck + ? `Last checked ${new Date(errorLog.lastCheck).toLocaleString()} (${errorLog.errors.length} errors found)` + : 'Click to check for errors'} + +
+ {errorLog.errors.length > 0 && ( +
+
Recent Errors:
+
+ {errorLog.errors.slice(0, 3).map((error, index) => ( +
+ {error.type === 'error' && `${error.message} (${error.filename}:${error.lineNumber})`} + {error.type === 'unhandledRejection' && `Unhandled Promise Rejection: ${error.reason}`} + {error.type === 'networkError' && `Network Error: Failed to load ${error.resource}`}
-
- ))} - {activeProviders.length === 0 && ( -
No local LLMs configured
- )} + ))} + {errorLog.errors.length > 3 && ( +
+ And {errorLog.errors.length - 3} more errors... +
+ )} +
- - -
+ )} +
+
); } diff --git a/app/components/settings/developer/DeveloperWindow.tsx b/app/components/settings/developer/DeveloperWindow.tsx new file mode 100644 index 0000000000..d0467bbf7b --- /dev/null +++ b/app/components/settings/developer/DeveloperWindow.tsx @@ -0,0 +1,378 @@ +import * as RadixDialog from '@radix-ui/react-dialog'; +import { motion } from 'framer-motion'; +import { useState } from 'react'; +import { classNames } from '~/utils/classNames'; +import { TabManagement } from './TabManagement'; +import { TabTile } from '~/components/settings/shared/TabTile'; +import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types'; +import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings'; +import { useStore } from '@nanostores/react'; +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import DebugTab from '~/components/settings/debug/DebugTab'; +import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab'; +import UpdateTab from '~/components/settings/update/UpdateTab'; +import { ProvidersTab } from '~/components/settings/providers/ProvidersTab'; +import DataTab from '~/components/settings/data/DataTab'; +import FeaturesTab from '~/components/settings/features/FeaturesTab'; +import NotificationsTab from '~/components/settings/notifications/NotificationsTab'; +import SettingsTab from '~/components/settings/settings/SettingsTab'; +import ProfileTab from '~/components/settings/profile/ProfileTab'; +import ConnectionsTab from '~/components/settings/connections/ConnectionsTab'; +import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks'; + +interface DraggableTabTileProps { + tab: TabVisibilityConfig; + index: number; + moveTab: (dragIndex: number, hoverIndex: number) => void; + onClick: () => void; + isActive: boolean; + hasUpdate: boolean; + statusMessage: string; + description: string; + isLoading?: boolean; +} + +const TAB_DESCRIPTIONS: Record = { + profile: 'Manage your profile and account settings', + settings: 'Configure application preferences', + notifications: 'View and manage your notifications', + features: 'Explore new and upcoming features', + data: 'Manage your data and storage', + providers: 'Configure AI providers and models', + connection: 'Check connection status and settings', + debug: 'Debug tools and system information', + 'event-logs': 'View system events and logs', + update: 'Check for updates and release notes', +}; + +const DraggableTabTile = ({ + tab, + index, + moveTab, + onClick, + isActive, + hasUpdate, + statusMessage, + description, + isLoading, +}: DraggableTabTileProps) => { + const [{ isDragging }, drag] = useDrag({ + type: 'tab', + item: { index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + + const [, drop] = useDrop({ + accept: 'tab', + hover: (item: { index: number }) => { + if (item.index === index) { + return; + } + + moveTab(item.index, index); + item.index = index; + }, + }); + + return ( +
drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}> + +
+ ); +}; + +interface DeveloperWindowProps { + open: boolean; + onClose: () => void; +} + +export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => { + const tabConfiguration = useStore(tabConfigurationStore); + const [activeTab, setActiveTab] = useState(null); + const [showTabManagement, setShowTabManagement] = useState(false); + const [loadingTab, setLoadingTab] = useState(null); + + // Status hooks + const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck(); + const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures(); + const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications(); + const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus(); + const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus(); + + const handleBack = () => { + if (showTabManagement) { + setShowTabManagement(false); + } else if (activeTab) { + setActiveTab(null); + } + }; + + // Only show tabs that are assigned to the developer window AND are visible + const visibleDeveloperTabs = tabConfiguration.developerTabs + .filter((tab: TabVisibilityConfig) => tab.window === 'developer' && tab.visible) + .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0)); + + const moveTab = (dragIndex: number, hoverIndex: number) => { + const draggedTab = visibleDeveloperTabs[dragIndex]; + const targetTab = visibleDeveloperTabs[hoverIndex]; + + // Update the order of the dragged and target tabs + const updatedDraggedTab = { ...draggedTab, order: targetTab.order }; + const updatedTargetTab = { ...targetTab, order: draggedTab.order }; + + // Update both tabs in the store + updateTabConfiguration(updatedDraggedTab); + updateTabConfiguration(updatedTargetTab); + }; + + const handleTabClick = async (tabId: TabType) => { + setLoadingTab(tabId); + setActiveTab(tabId); + + // Acknowledge the status based on tab type + switch (tabId) { + case 'update': + await acknowledgeUpdate(); + break; + case 'features': + await acknowledgeAllFeatures(); + break; + case 'notifications': + await markAllAsRead(); + break; + case 'connection': + acknowledgeIssue(); + break; + case 'debug': + await acknowledgeAllIssues(); + break; + } + + // Simulate loading time (remove this in production) + await new Promise((resolve) => setTimeout(resolve, 1000)); + setLoadingTab(null); + }; + + const getTabComponent = () => { + switch (activeTab) { + case 'profile': + return ; + case 'settings': + return ; + case 'notifications': + return ; + case 'features': + return ; + case 'data': + return ; + case 'providers': + return ; + case 'connection': + return ; + case 'debug': + return ; + case 'event-logs': + return ; + case 'update': + return ; + default: + return null; + } + }; + + const getTabUpdateStatus = (tabId: TabType): boolean => { + switch (tabId) { + case 'update': + return hasUpdate; + case 'features': + return hasNewFeatures; + case 'notifications': + return hasUnreadNotifications; + case 'connection': + return hasConnectionIssues; + case 'debug': + return hasActiveWarnings; + default: + return false; + } + }; + + const getStatusMessage = (tabId: TabType): string => { + switch (tabId) { + case 'update': + return `New update available (v${currentVersion})`; + case 'features': + return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`; + case 'notifications': + return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`; + case 'connection': + return currentIssue === 'disconnected' + ? 'Connection lost' + : currentIssue === 'high-latency' + ? 'High latency detected' + : 'Connection issues detected'; + case 'debug': { + const warnings = activeIssues.filter((i) => i.type === 'warning').length; + const errors = activeIssues.filter((i) => i.type === 'error').length; + + return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`; + } + default: + return ''; + } + }; + + return ( + + + +
+ + + + + + {/* Header */} +
+
+ {(activeTab || showTabManagement) && ( + +
+ + )} +
+ +

+ {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Dashboard'} +

+
+
+
+ {!showTabManagement && !activeTab && ( + setShowTabManagement(true)} + className={classNames( + 'px-3 py-1.5 rounded-lg text-sm', + 'bg-purple-500/10 text-purple-500', + 'hover:bg-purple-500/20', + 'transition-colors duration-200', + )} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + Manage Tabs + + )} + +
+ +
+
+ + {/* Content */} +
+ + {showTabManagement ? ( + + ) : activeTab ? ( + getTabComponent() + ) : ( +
+ {visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => ( + handleTabClick(tab.id)} + isActive={activeTab === tab.id} + hasUpdate={getTabUpdateStatus(tab.id)} + statusMessage={getStatusMessage(tab.id)} + description={TAB_DESCRIPTIONS[tab.id]} + isLoading={loadingTab === tab.id} + /> + ))} +
+ )} +
+
+ + +
+ + + + ); +}; diff --git a/app/components/settings/developer/TabManagement.tsx b/app/components/settings/developer/TabManagement.tsx new file mode 100644 index 0000000000..3ec23a4c51 --- /dev/null +++ b/app/components/settings/developer/TabManagement.tsx @@ -0,0 +1,315 @@ +import { motion } from 'framer-motion'; +import { useState } from 'react'; +import { classNames } from '~/utils/classNames'; +import { tabConfigurationStore, updateTabConfiguration, resetTabConfiguration } from '~/lib/stores/settings'; +import { useStore } from '@nanostores/react'; +import { TAB_LABELS, type TabType, type TabVisibilityConfig } from '~/components/settings/settings.types'; +import { toast } from 'react-toastify'; + +// Define icons for each tab type +const TAB_ICONS: Record = { + profile: 'i-ph:user-circle-fill', + settings: 'i-ph:gear-six-fill', + notifications: 'i-ph:bell-fill', + features: 'i-ph:sparkle-fill', + data: 'i-ph:database-fill', + providers: 'i-ph:robot-fill', + connection: 'i-ph:plug-fill', + debug: 'i-ph:bug-fill', + 'event-logs': 'i-ph:list-bullets-fill', + update: 'i-ph:arrow-clockwise-fill', +}; + +interface TabGroupProps { + title: string; + description?: string; + tabs: TabVisibilityConfig[]; + onVisibilityChange: (tabId: TabType, enabled: boolean) => void; + targetWindow: 'user' | 'developer'; + standardTabs: TabType[]; +} + +const TabGroup = ({ title, description, tabs, onVisibilityChange, targetWindow }: TabGroupProps) => { + // Split tabs into visible and hidden + const visibleTabs = tabs.filter((tab) => tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0)); + const hiddenTabs = tabs.filter((tab) => !tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0)); + + return ( +
+
+

+ + {title} +

+ {description &&

{description}

} +
+ +
+ + {visibleTabs.map((tab) => ( + +
+
+ + {TAB_LABELS[tab.id]} + + {tab.id === 'profile' && targetWindow === 'user' && ( + + Standard + + )} +
+
+ {targetWindow === 'user' ? ( +