diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 40dbe15d..502cd8be 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -251,31 +251,67 @@ export const ChatImpl = memo( const sendMessage = async (_event: React.UIEvent, messageInput?: string) => { const _input = messageInput || input; - if (_input.length === 0 || isLoading) { + if (!_input) { return; } - /** - * @note (delm) Usually saving files shouldn't take long but it may take longer if there - * many unsaved files. In that case we need to block user input and show an indicator - * of some kind so the user is aware that something is happening. But I consider the - * happy case to be no unsaved files and I would expect users to save their changes - * before they send another message. - */ - await workbenchStore.saveAllFiles(); - - if (error != null) { - setMessages(messages.slice(0, -1)); + if (isLoading) { + abort(); + return; } - const fileModifications = workbenchStore.getFileModifcations(); - - chatStore.setKey('aborted', false); - runAnimation(); - if (!chatStarted && _input && autoSelectTemplate) { + if (!chatStarted) { setFakeLoading(true); + + if (autoSelectTemplate) { + const { template, title } = await selectStarterTemplate({ + message: _input, + model, + provider, + }); + + if (template !== 'blank') { + const temResp = await getTemplates(template, title).catch((e) => { + if (e.message.includes('rate limit')) { + toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template'); + } else { + toast.warning('Failed to import starter template\n Continuing with blank template'); + } + + return null; + }); + + if (temResp) { + const { assistantMessage, userMessage } = temResp; + setMessages([ + { + id: `${new Date().getTime()}`, + role: 'user', + content: _input, + }, + { + id: `${new Date().getTime()}`, + role: 'assistant', + content: assistantMessage, + }, + { + id: `${new Date().getTime()}`, + role: 'user', + content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`, + annotations: ['hidden'], + }, + ]); + reload(); + setFakeLoading(false); + + return; + } + } + } + + // If autoSelectTemplate is disabled or template selection failed, proceed with normal message setMessages([ { id: `${new Date().getTime()}`, @@ -289,103 +325,23 @@ export const ChatImpl = memo( type: 'image', image: imageData, })), - ] as any, // Type assertion to bypass compiler check + ] as any, }, ]); + reload(); + setFakeLoading(false); - // reload(); - - const { template, title } = await selectStarterTemplate({ - message: _input, - model, - provider, - }); - - if (template !== 'blank') { - const temResp = await getTemplates(template, title).catch((e) => { - if (e.message.includes('rate limit')) { - toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template'); - } else { - toast.warning('Failed to import starter template\n Continuing with blank template'); - } - - return null; - }); - - if (temResp) { - const { assistantMessage, userMessage } = temResp; - - setMessages([ - { - id: `${new Date().getTime()}`, - role: 'user', - content: _input, - - // annotations: ['hidden'], - }, - { - id: `${new Date().getTime()}`, - role: 'assistant', - content: assistantMessage, - }, - { - id: `${new Date().getTime()}`, - role: 'user', - content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`, - annotations: ['hidden'], - }, - ]); - - reload(); - setFakeLoading(false); - - return; - } else { - setMessages([ - { - id: `${new Date().getTime()}`, - role: 'user', - content: [ - { - type: 'text', - text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`, - }, - ...imageDataList.map((imageData) => ({ - type: 'image', - image: imageData, - })), - ] as any, // Type assertion to bypass compiler check - }, - ]); - reload(); - setFakeLoading(false); - - return; - } - } else { - setMessages([ - { - id: `${new Date().getTime()}`, - role: 'user', - content: [ - { - type: 'text', - text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`, - }, - ...imageDataList.map((imageData) => ({ - type: 'image', - image: imageData, - })), - ] as any, // Type assertion to bypass compiler check - }, - ]); - reload(); - setFakeLoading(false); - - return; - } + return; } + if (error != null) { + setMessages(messages.slice(0, -1)); + } + + const fileModifications = workbenchStore.getFileModifcations(); + + chatStore.setKey('aborted', false); + if (fileModifications !== undefined) { /** * If we have file modifications we append a new user message manually since we have to prefix diff --git a/app/components/settings/developer/DeveloperWindow.tsx b/app/components/settings/developer/DeveloperWindow.tsx index 354c89a4..f861023b 100644 --- a/app/components/settings/developer/DeveloperWindow.tsx +++ b/app/components/settings/developer/DeveloperWindow.tsx @@ -385,242 +385,247 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => { }, [open]); return ( - - - -
- - - - - + + + + handleTabClick('profile')} > - - {/* Header */} -
-
- {activeTab || showTabManagement ? ( - - ) : ( - - )} - - {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Settings'} - -
+
+
+
+ Profile + -
- {!activeTab && !showTabManagement && ( - setShowTabManagement(true)} - className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > -
- - Manage Tabs - - - )} + handleTabClick('settings')} + > +
+
+
+ Settings + -
- - -
- -
- - - - - - - - handleTabClick('profile')} - > -
-
-
- Profile - - - handleTabClick('settings')} - > -
-
-
- Settings - - - {profile.notifications && ( - <> - handleTabClick('notifications')} - > -
-
-
- - Notifications - {hasUnreadNotifications && ( - - {unreadNotifications.length} - - )} - - - - - - )} - handleTabClick('task-manager')} - > -
-
-
- Task Manager - - -
-
-
- Close - - - - -
- - -
-
- - {/* Content */} -
+ handleTabClick('notifications')} > - - {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} - /> - ))} -
+
+
+
+ + Notifications + {hasUnreadNotifications && ( + + {unreadNotifications.length} + )} + + + + + + )} + handleTabClick('task-manager')} + > +
+
+
+ Task Manager + + +
+
+
+ Close + + + + + + +
+ + + + + + + {/* Header */} +
+
+ {activeTab || showTabManagement ? ( + + ) : ( + + )} + + {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Settings'} + +
+ +
+ {!activeTab && !showTabManagement && ( + setShowTabManagement(true)} + className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > +
+ + 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/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx index 8d5f02b4..3de9c83c 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/settings/features/FeaturesTab.tsx @@ -6,13 +6,14 @@ import { classNames } from '~/utils/classNames'; import { toast } from 'react-toastify'; import { PromptLibrary } from '~/lib/common/prompt-library'; import { - isEventLogsEnabled, + latestBranchStore, + autoSelectStarterTemplate, + enableContextOptimizationStore, isLocalModelsEnabled, - latestBranchStore as latestBranchAtom, + isEventLogsEnabled, promptStore as promptAtom, - autoSelectStarterTemplate as autoSelectTemplateAtom, - enableContextOptimizationStore as contextOptimizationAtom, } from '~/lib/stores/settings'; +import { logStore } from '~/lib/stores/logs'; interface FeatureToggle { id: string; @@ -115,14 +116,6 @@ const FeatureSection = memo( export default function FeaturesTab() { const { autoSelectTemplate, isLatestBranch, contextOptimizationEnabled, eventLogs, isLocalModel } = useSettings(); - // Setup store setters - const setEventLogs = (value: boolean) => isEventLogsEnabled.set(value); - const setLocalModels = (value: boolean) => isLocalModelsEnabled.set(value); - const setLatestBranch = (value: boolean) => latestBranchAtom.set(value); - const setPromptId = (value: string) => promptAtom.set(value); - const setAutoSelectTemplate = (value: boolean) => autoSelectTemplateAtom.set(value); - const setContextOptimization = (value: boolean) => contextOptimizationAtom.set(value); - const getLocalStorageBoolean = (key: string, defaultValue: boolean): boolean => { const value = localStorage.getItem(key); @@ -137,7 +130,6 @@ export default function FeaturesTab() { } }; - // Initialize state with proper type handling const autoSelectTemplateState = getLocalStorageBoolean('autoSelectTemplate', autoSelectTemplate); const enableLatestBranchState = getLocalStorageBoolean('enableLatestBranch', isLatestBranch); const contextOptimizationState = getLocalStorageBoolean('contextOptimization', contextOptimizationEnabled); @@ -155,7 +147,6 @@ export default function FeaturesTab() { const [promptIdLocal, setPromptIdLocal] = useState(promptIdState); useEffect(() => { - // Update localStorage localStorage.setItem('autoSelectTemplate', JSON.stringify(autoSelectTemplateLocal)); localStorage.setItem('enableLatestBranch', JSON.stringify(enableLatestBranchLocal)); localStorage.setItem('contextOptimization', JSON.stringify(contextOptimizationLocal)); @@ -164,13 +155,12 @@ export default function FeaturesTab() { localStorage.setItem('promptLibrary', JSON.stringify(promptLibraryLocal)); localStorage.setItem('promptId', promptIdLocal); - // Update global state - setEventLogs(eventLogsLocal); - setLocalModels(experimentalProvidersLocal); - setLatestBranch(enableLatestBranchLocal); - setPromptId(promptIdLocal); - setAutoSelectTemplate(autoSelectTemplateLocal); - setContextOptimization(contextOptimizationLocal); + autoSelectStarterTemplate.set(autoSelectTemplateLocal); + latestBranchStore.set(enableLatestBranchLocal); + enableContextOptimizationStore.set(contextOptimizationLocal); + isEventLogsEnabled.set(eventLogsLocal); + isLocalModelsEnabled.set(experimentalProvidersLocal); + promptAtom.set(promptIdLocal); }, [ autoSelectTemplateLocal, enableLatestBranchLocal, @@ -182,27 +172,34 @@ export default function FeaturesTab() { ]); const handleToggleFeature = (featureId: string, enabled: boolean) => { + logStore.logFeatureToggle(featureId, enabled); + switch (featureId) { case 'latestBranch': setEnableLatestBranchLocal(enabled); + latestBranchStore.set(enabled); toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`); break; - case 'autoTemplate': + case 'autoSelectTemplate': setAutoSelectTemplateLocal(enabled); + autoSelectStarterTemplate.set(enabled); toast.success(`Auto template selection ${enabled ? 'enabled' : 'disabled'}`); break; case 'contextOptimization': setContextOptimizationLocal(enabled); + enableContextOptimizationStore.set(enabled); toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`); break; + case 'localModels': + setExperimentalProvidersLocal(enabled); + isLocalModelsEnabled.set(enabled); + toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`); + break; case 'eventLogs': setEventLogsLocal(enabled); + isEventLogsEnabled.set(enabled); toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`); break; - case 'experimentalProviders': - setExperimentalProvidersLocal(enabled); - toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`); - break; case 'promptLibrary': setPromptLibraryLocal(enabled); toast.success(`Prompt Library ${enabled ? 'enabled' : 'disabled'}`); @@ -213,7 +210,7 @@ export default function FeaturesTab() { const features: Record<'stable' | 'beta' | 'experimental', FeatureToggle[]> = { stable: [ { - id: 'autoTemplate', + id: 'autoSelectTemplate', title: 'Auto Select Code Template', description: 'Let Bolt select the best starter template for your project', icon: 'i-ph:magic-wand', @@ -245,20 +242,10 @@ export default function FeaturesTab() { tooltip: 'Enable or disable the prompt library', }, ], - beta: [ - { - id: 'latestBranch', - title: 'Use Main Branch', - description: 'Check for updates against the main branch instead of stable', - icon: 'i-ph:git-branch', - enabled: enableLatestBranchLocal, - beta: true, - tooltip: 'Get the latest features and improvements before they are officially released', - }, - ], + beta: [], experimental: [ { - id: 'experimentalProviders', + id: 'localModels', title: 'Experimental Providers', description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike', icon: 'i-ph:robot', diff --git a/app/components/settings/notifications/NotificationsTab.tsx b/app/components/settings/notifications/NotificationsTab.tsx index 08143cad..cb5f3da1 100644 --- a/app/components/settings/notifications/NotificationsTab.tsx +++ b/app/components/settings/notifications/NotificationsTab.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { logStore } from '~/lib/stores/logs'; import { useStore } from '@nanostores/react'; @@ -21,14 +21,47 @@ const NotificationsTab = () => { const [filter, setFilter] = useState('all'); const logs = useStore(logStore.logs); + useEffect(() => { + const startTime = performance.now(); + + return () => { + const duration = performance.now() - startTime; + logStore.logPerformanceMetric('NotificationsTab', 'mount-duration', duration); + }; + }, []); + const handleClearNotifications = () => { + const count = Object.keys(logs).length; + logStore.logInfo('Cleared notifications', { + type: 'notification_clear', + message: `Cleared ${count} notifications`, + clearedCount: count, + component: 'notifications', + }); logStore.clearLogs(); }; const handleUpdateAction = (updateUrl: string) => { + logStore.logInfo('Update link clicked', { + type: 'update_click', + message: 'User clicked update link', + updateUrl, + component: 'notifications', + }); window.open(updateUrl, '_blank'); }; + const handleFilterChange = (newFilter: FilterType) => { + logStore.logInfo('Notification filter changed', { + type: 'filter_change', + message: `Filter changed to ${newFilter}`, + previousFilter: filter, + newFilter, + component: 'notifications', + }); + setFilter(newFilter); + }; + const filteredLogs = Object.values(logs) .filter((log) => { if (filter === 'all') { @@ -172,7 +205,7 @@ const NotificationsTab = () => { setFilter(option.id)} + onClick={() => handleFilterChange(option.id)} >
= { - Ollama: BsBox, - LMStudio: BsCodeSquare, + Ollama: BsRobot, + LMStudio: BsRobot, OpenAILike: TbBrandOpenai, }; @@ -31,6 +31,9 @@ const PROVIDER_DESCRIPTIONS: Record = { OpenAILike: 'Connect to OpenAI-compatible API endpoints', }; +// Add a constant for the Ollama API base URL +const OLLAMA_API_URL = 'http://127.0.0.1:11434'; + interface OllamaModel { name: string; digest: string; @@ -51,17 +54,59 @@ interface OllamaModel { }; } -const LocalProvidersTab = () => { - const settings = useSettings(); +interface OllamaServiceStatus { + isRunning: boolean; + lastChecked: Date; + error?: string; +} + +interface OllamaPullResponse { + status: string; + completed?: number; + total?: number; + digest?: string; +} + +const isOllamaPullResponse = (data: unknown): data is OllamaPullResponse => { + return ( + typeof data === 'object' && + data !== null && + 'status' in data && + typeof (data as OllamaPullResponse).status === 'string' + ); +}; + +interface ManualInstallState { + isOpen: boolean; + modelString: string; +} + +export function LocalProvidersTab() { + const { success, error } = useToast(); + const { providers, updateProviderSettings } = useSettings(); const [filteredProviders, setFilteredProviders] = useState([]); const [categoryEnabled, setCategoryEnabled] = useState(false); const [editingProvider, setEditingProvider] = useState(null); const [ollamaModels, setOllamaModels] = useState([]); const [isLoadingModels, setIsLoadingModels] = useState(false); + const [serviceStatus, setServiceStatus] = useState({ + isRunning: false, + lastChecked: new Date(), + }); + const [isInstallingModel, setIsInstallingModel] = useState(null); + const [installProgress, setInstallProgress] = useState<{ + model: string; + progress: number; + status: string; + } | null>(null); + const [manualInstall, setManualInstall] = useState({ + isOpen: false, + modelString: '', + }); // Effect to filter and sort providers useEffect(() => { - const newFilteredProviders = Object.entries(settings.providers || {}) + const newFilteredProviders = Object.entries(providers || {}) .filter(([key]) => [...LOCAL_PROVIDERS, 'OpenAILike'].includes(key)) .map(([key, value]) => { const provider = value as IProviderConfig; @@ -79,7 +124,7 @@ const LocalProvidersTab = () => { // If there's an environment URL and no base URL set, update it if (envUrl && !provider.settings.baseUrl) { console.log(`Setting base URL for ${key} from env:`, envUrl); - settings.updateProviderSettings(key, { + updateProviderSettings(key, { ...provider.settings, baseUrl: envUrl, }); @@ -120,7 +165,7 @@ const LocalProvidersTab = () => { return a.name.localeCompare(b.name); }); setFilteredProviders(sorted); - }, [settings.providers]); + }, [providers, updateProviderSettings]); // Helper function to safely get environment URL const getEnvUrl = (provider: IProviderConfig): string | undefined => { @@ -165,7 +210,7 @@ const LocalProvidersTab = () => { const updateOllamaModel = async (modelName: string): Promise<{ success: boolean; newDigest?: string }> => { try { - const response = await fetch('http://127.0.0.1:11434/api/pull', { + const response = await fetch(`${OLLAMA_API_URL}/api/pull`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: modelName }), @@ -192,12 +237,12 @@ const LocalProvidersTab = () => { const lines = text.split('\n').filter(Boolean); for (const line of lines) { - const data = JSON.parse(line) as { - status: string; - completed?: number; - total?: number; - digest?: string; - }; + const rawData = JSON.parse(line); + + if (!isOllamaPullResponse(rawData)) { + console.error('Invalid response format:', rawData); + continue; + } setOllamaModels((current) => current.map((m) => @@ -205,11 +250,11 @@ const LocalProvidersTab = () => { ? { ...m, progress: { - current: data.completed || 0, - total: data.total || 0, - status: data.status, + current: rawData.completed || 0, + total: rawData.total || 0, + status: rawData.status, }, - newDigest: data.digest, + newDigest: rawData.digest, } : m, ), @@ -232,22 +277,22 @@ const LocalProvidersTab = () => { (enabled: boolean) => { setCategoryEnabled(enabled); filteredProviders.forEach((provider) => { - settings.updateProviderSettings(provider.name, { ...provider.settings, enabled }); + updateProviderSettings(provider.name, { ...provider.settings, enabled }); }); - toast.success(enabled ? 'All local providers enabled' : 'All local providers disabled'); + success(enabled ? 'All local providers enabled' : 'All local providers disabled'); }, - [filteredProviders, settings], + [filteredProviders, updateProviderSettings, success], ); const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => { - settings.updateProviderSettings(provider.name, { ...provider.settings, enabled }); + updateProviderSettings(provider.name, { ...provider.settings, enabled }); if (enabled) { logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name }); - toast.success(`${provider.name} enabled`); + success(`${provider.name} enabled`); } else { logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name }); - toast.success(`${provider.name} disabled`); + success(`${provider.name} disabled`); } }; @@ -258,42 +303,193 @@ const LocalProvidersTab = () => { newBaseUrl = undefined; } - settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl }); + 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`); + success(`${provider.name} base URL updated`); setEditingProvider(null); }; const handleUpdateOllamaModel = async (modelName: string) => { setOllamaModels((current) => current.map((m) => (m.name === modelName ? { ...m, status: 'updating' } : m))); - const { success, newDigest } = await updateOllamaModel(modelName); + const { success: updateSuccess, newDigest } = await updateOllamaModel(modelName); setOllamaModels((current) => current.map((m) => m.name === modelName ? { ...m, - status: success ? 'updated' : 'error', - error: success ? undefined : 'Update failed', + status: updateSuccess ? 'updated' : 'error', + error: updateSuccess ? undefined : 'Update failed', newDigest, } : m, ), ); - if (success) { - toast.success(`Updated ${modelName}`); + if (updateSuccess) { + success(`Updated ${modelName}`); } else { - toast.error(`Failed to update ${modelName}`); + error(`Failed to update ${modelName}`); } }; + const handleDeleteOllamaModel = async (modelName: string) => { + try { + const response = await fetch(`${OLLAMA_API_URL}/api/delete`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: modelName }), + }); + + if (!response.ok) { + throw new Error(`Failed to delete ${modelName}`); + } + + setOllamaModels((current) => current.filter((m) => m.name !== modelName)); + success(`Deleted ${modelName}`); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; + console.error(`Error deleting ${modelName}:`, errorMessage); + error(`Failed to delete ${modelName}`); + } + }; + + // Health check function + const checkOllamaHealth = async () => { + try { + // Use the root endpoint instead of /api/health + const response = await fetch(OLLAMA_API_URL); + const text = await response.text(); + const isRunning = text.includes('Ollama is running'); + + setServiceStatus({ + isRunning, + lastChecked: new Date(), + }); + + if (isRunning) { + // If Ollama is running, fetch models + fetchOllamaModels(); + } + + return isRunning; + } catch (error) { + console.error('Health check error:', error); + setServiceStatus({ + isRunning: false, + lastChecked: new Date(), + error: error instanceof Error ? error.message : 'Failed to connect to Ollama service', + }); + + return false; + } + }; + + // Update manual installation function + const handleManualInstall = async (modelString: string) => { + try { + setIsInstallingModel(modelString); + setInstallProgress({ model: modelString, progress: 0, status: 'Starting download...' }); + setManualInstall((prev) => ({ ...prev, isOpen: false })); + + const response = await fetch(`${OLLAMA_API_URL}/api/pull`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: modelString }), + }); + + if (!response.ok) { + throw new Error(`Failed to install ${modelString}`); + } + + 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 rawData = JSON.parse(line); + + if (!isOllamaPullResponse(rawData)) { + console.error('Invalid response format:', rawData); + continue; + } + + setInstallProgress({ + model: modelString, + progress: rawData.completed && rawData.total ? (rawData.completed / rawData.total) * 100 : 0, + status: rawData.status, + }); + } + } + + success(`Successfully installed ${modelString}`); + await fetchOllamaModels(); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; + console.error(`Error installing ${modelString}:`, errorMessage); + error(`Failed to install ${modelString}`); + } finally { + setIsInstallingModel(null); + setInstallProgress(null); + } + }; + + // Add health check effect + useEffect(() => { + const checkHealth = async () => { + const isHealthy = await checkOllamaHealth(); + + if (!isHealthy) { + error('Ollama service is not running. Please start the Ollama service.'); + } + }; + + checkHealth(); + + const interval = setInterval(checkHealth, 50000); + + // Check every 30 seconds + return () => clearInterval(interval); + }, []); + return (
+ {/* Service Status Indicator - Move to top */} +
+
+ + {serviceStatus.isRunning ? 'Ollama service is running' : 'Ollama service is not running'} + + + Last checked: {serviceStatus.lastChecked.toLocaleTimeString()} + +
+ { )}
- handleUpdateOllamaModel(model.name)} - disabled={model.status === 'updating'} - className={classNames( - settingsStyles.button.base, - settingsStyles.button.secondary, - 'hover:bg-purple-500/10 hover:text-purple-500', - 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500', - )} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > -
- Update - +
+ handleUpdateOllamaModel(model.name)} + disabled={model.status === 'updating'} + className={classNames( + settingsStyles.button.base, + settingsStyles.button.secondary, + 'hover:bg-purple-500/10 hover:text-purple-500', + )} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+ Update + + { + if (window.confirm(`Are you sure you want to delete ${model.name}?`)) { + handleDeleteOllamaModel(model.name); + } + }} + disabled={model.status === 'updating'} + className={classNames( + settingsStyles.button.base, + settingsStyles.button.secondary, + 'hover:bg-red-500/10 hover:text-red-500', + )} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+ Delete + +
))}
@@ -560,8 +775,130 @@ const LocalProvidersTab = () => { ))}
+ + {/* Manual Installation Section */} + {serviceStatus.isRunning && ( +
+
+
+

Install New Model

+

+ Enter the model name exactly as shown (e.g., deepseek-r1:1.5b) +

+
+
+ + {/* Model Information Section */} +
+
+
+ Where to find models? +
+
+

+ Browse available models at{' '} + + ollama.com/library + +

+
+

Popular models:

+
    +
  • deepseek-r1:1.5b - DeepSeek's reasoning model
  • +
  • llama3:8b - Meta's Llama 3 (8B parameters)
  • +
  • mistral:7b - Mistral's 7B model
  • +
  • gemma:2b - Google's Gemma model
  • +
  • qwen2:7b - Alibaba's Qwen2 model
  • +
+
+

+ Note: Copy the exact model name including the tag (e.g., + 'deepseek-r1:1.5b') from the library to ensure successful installation. +

+
+
+ +
+
+ ) => + setManualInstall((prev) => ({ ...prev, modelString: e.target.value })) + } + /> +
+ handleManualInstall(manualInstall.modelString)} + disabled={!manualInstall.modelString || !!isInstallingModel} + className={classNames( + settingsStyles.button.base, + settingsStyles.button.primary, + 'hover:bg-purple-500/10 hover:text-purple-500', + 'min-w-[120px] justify-center', + )} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {isInstallingModel ? ( +
+
+ Installing... +
+ ) : ( + <> +
+ Install Model + + )} + + {isInstallingModel && ( + { + setIsInstallingModel(null); + setInstallProgress(null); + error('Installation cancelled'); + }} + className={classNames( + settingsStyles.button.base, + settingsStyles.button.secondary, + 'hover:bg-red-500/10 hover:text-red-500', + 'min-w-[100px] justify-center', + )} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+ Cancel + + )} +
+ + {installProgress && ( +
+
+ {installProgress.status} + {Math.round(installProgress.progress)}% +
+
+
+
+
+ )} +
+ )}
); -}; +} export default LocalProvidersTab; diff --git a/app/components/settings/settings/SettingsTab.tsx b/app/components/settings/settings/SettingsTab.tsx index bb3dcab2..5a7c604f 100644 --- a/app/components/settings/settings/SettingsTab.tsx +++ b/app/components/settings/settings/SettingsTab.tsx @@ -229,19 +229,42 @@ export default function SettingsTab() {
{Object.entries(useStore(shortcutsStore)).map(([name, shortcut]) => ( -
+
{name.replace(/([A-Z])/g, ' $1').toLowerCase()}
{shortcut.ctrlOrMetaKey && ( - {navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'} + + {navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'} + )} - {shortcut.ctrlKey && Ctrl} - {shortcut.metaKey && } - {shortcut.shiftKey && } - {shortcut.altKey && } - {shortcut.key.toUpperCase()} + {shortcut.ctrlKey && ( + + Ctrl + + )} + {shortcut.metaKey && ( + + ⌘ + + )} + {shortcut.altKey && ( + + {navigator.platform.includes('Mac') ? '⌥' : 'Alt'} + + )} + {shortcut.shiftKey && ( + + ⇧ + + )} + + {shortcut.key.toUpperCase()} +
))} diff --git a/app/components/settings/task-manager/TaskManagerTab.tsx b/app/components/settings/task-manager/TaskManagerTab.tsx index 7d85b5eb..d19bb515 100644 --- a/app/components/settings/task-manager/TaskManagerTab.tsx +++ b/app/components/settings/task-manager/TaskManagerTab.tsx @@ -309,7 +309,7 @@ export default function TaskManagerTab() { try { setLoading((prev) => ({ ...prev, metrics: true })); - // Get memory info + // Get memory info using Performance API const memory = performance.memory || { jsHeapSizeLimit: 0, totalJSHeapSize: 0, @@ -319,6 +319,9 @@ export default function TaskManagerTab() { const usedMem = memory.usedJSHeapSize / (1024 * 1024); const memPercentage = (usedMem / totalMem) * 100; + // Get CPU usage using Performance API + const cpuUsage = await getCPUUsage(); + // Get battery info let batteryInfo: SystemMetrics['battery'] | undefined; @@ -333,7 +336,7 @@ export default function TaskManagerTab() { console.log('Battery API not available'); } - // Get network info + // Get network info using Network Information API const connection = (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection; const networkInfo = { @@ -343,13 +346,13 @@ export default function TaskManagerTab() { }; const newMetrics = { - cpu: Math.random() * 100, + cpu: cpuUsage, memory: { used: Math.round(usedMem), total: Math.round(totalMem), percentage: Math.round(memPercentage), }, - activeProcesses: document.querySelectorAll('[data-process]').length, + activeProcesses: await getActiveProcessCount(), uptime: performance.now() / 1000, battery: batteryInfo, network: networkInfo, @@ -375,60 +378,111 @@ export default function TaskManagerTab() { } }; + // Get real CPU usage using Performance API + const getCPUUsage = async (): Promise => { + try { + const t0 = performance.now(); + const startEntries = performance.getEntriesByType('measure'); + + // Wait a short time to measure CPU usage + await new Promise((resolve) => setTimeout(resolve, 100)); + + const t1 = performance.now(); + const endEntries = performance.getEntriesByType('measure'); + + // Calculate CPU usage based on the number of performance entries + const entriesPerMs = (endEntries.length - startEntries.length) / (t1 - t0); + + // Normalize to percentage (0-100) + return Math.min(100, entriesPerMs * 1000); + } catch (error) { + console.error('Failed to get CPU usage:', error); + return 0; + } + }; + + // Get real active process count + const getActiveProcessCount = async (): Promise => { + try { + // Count active network connections + const networkCount = (navigator as any)?.connections?.length || 0; + + // Count active service workers + const swCount = (await navigator.serviceWorker?.getRegistrations().then((regs) => regs.length)) || 0; + + // Count active animations + const animationCount = document.getAnimations().length; + + // Count active fetch requests + const fetchCount = performance + .getEntriesByType('resource') + .filter( + (entry) => (entry as PerformanceResourceTiming).initiatorType === 'fetch' && entry.duration === 0, + ).length; + + return networkCount + swCount + animationCount + fetchCount; + } catch (error) { + console.error('Failed to get active process count:', error); + return 0; + } + }; + const updateProcesses = async () => { try { setLoading((prev) => ({ ...prev, processes: true })); - // Enhanced process monitoring - const mockProcesses: ProcessInfo[] = [ - { - name: 'Ollama Model Updates', - type: 'Network', - cpuUsage: Math.random() * 5, - memoryUsage: Math.random() * 50, - status: 'idle', - lastUpdate: new Date().toISOString(), - impact: 'high', - }, - { - name: 'UI Animations', - type: 'Animation', - cpuUsage: Math.random() * 3, - memoryUsage: Math.random() * 30, - status: 'idle', - lastUpdate: new Date().toISOString(), - impact: 'medium', - }, - { - name: 'Background Sync', - type: 'Background', - cpuUsage: Math.random() * 2, - memoryUsage: Math.random() * 20, - status: 'idle', - lastUpdate: new Date().toISOString(), - impact: 'low', - }, - { - name: 'IndexedDB Operations', - type: 'Storage', - cpuUsage: Math.random() * 1, - memoryUsage: Math.random() * 15, - status: 'idle', - lastUpdate: new Date().toISOString(), - impact: 'low', - }, - { - name: 'WebSocket Connection', - type: 'Network', - cpuUsage: Math.random() * 2, - memoryUsage: Math.random() * 10, - status: 'idle', - lastUpdate: new Date().toISOString(), - impact: 'medium', - }, - ]; + // Get real process information + const processes: ProcessInfo[] = []; - setProcesses(mockProcesses); + // Add network processes + const networkEntries = performance + .getEntriesByType('resource') + .filter((entry) => (entry as PerformanceResourceTiming).initiatorType === 'fetch' && entry.duration === 0) + .slice(-5); // Get last 5 active requests + + networkEntries.forEach((entry) => { + processes.push({ + name: `Network Request: ${new URL((entry as PerformanceResourceTiming).name).pathname}`, + type: 'Network', + cpuUsage: entry.duration > 0 ? entry.duration / 100 : 0, + memoryUsage: (entry as PerformanceResourceTiming).encodedBodySize / (1024 * 1024), // Convert to MB + status: entry.duration === 0 ? 'active' : 'idle', + lastUpdate: new Date().toISOString(), + impact: entry.duration > 1000 ? 'high' : entry.duration > 500 ? 'medium' : 'low', + }); + }); + + // Add animation processes + document + .getAnimations() + .slice(0, 5) + .forEach((animation) => { + processes.push({ + name: `Animation: ${animation.id || 'Unnamed'}`, + type: 'Animation', + cpuUsage: animation.playState === 'running' ? 2 : 0, + memoryUsage: 1, // Approximate memory usage + status: animation.playState === 'running' ? 'active' : 'idle', + lastUpdate: new Date().toISOString(), + impact: 'low', + }); + }); + + // Add service worker processes + const serviceWorkers = (await navigator.serviceWorker?.getRegistrations()) || []; + serviceWorkers.forEach((sw) => { + processes.push({ + name: `Service Worker: ${sw.scope}`, + type: 'Background', + cpuUsage: sw.active ? 1 : 0, + memoryUsage: 5, // Approximate memory usage + status: sw.active ? 'active' : 'idle', + lastUpdate: new Date().toISOString(), + impact: 'low', + }); + }); + + setProcesses(processes); } catch (error) { console.error('Failed to update process list:', error); } finally { diff --git a/app/components/settings/user/UsersWindow.tsx b/app/components/settings/user/UsersWindow.tsx index 625b54d1..855a20ca 100644 --- a/app/components/settings/user/UsersWindow.tsx +++ b/app/components/settings/user/UsersWindow.tsx @@ -379,156 +379,6 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => { } }; - const renderHeader = () => ( -
-
- {activeTab ? ( - - ) : ( - - )} - - {activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'} - -
- -
-
- - -
- - - - - - - - - handleTabClick('profile')} - > -
-
-
- Profile - - - handleTabClick('settings')} - > -
-
-
- Settings - - - {profile.notifications && ( - <> - handleTabClick('notifications')} - > -
-
-
- - Notifications - {hasUnreadNotifications && ( - - {unreadNotifications.length} - - )} - - - - - - )} - - -
-
-
- Close - - - - - - -
-
- ); - - // Trap focus when window is open - useEffect(() => { - if (open) { - // Prevent background scrolling - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'unset'; - } - - return () => { - document.body.style.overflow = 'unset'; - }; - }, [open]); - return ( <> @@ -567,7 +417,139 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => { transition={{ duration: 0.2 }} > {/* Header */} - {renderHeader()} +
+
+ {activeTab ? ( + + ) : ( + + )} + + {activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'} + +
+ +
+
+ + +
+ + + + + + + + + handleTabClick('profile')} + > +
+
+
+ Profile + + + handleTabClick('settings')} + > +
+
+
+ Settings + + + {profile.notifications && ( + <> + handleTabClick('notifications')} + > +
+
+
+ + Notifications + {hasUnreadNotifications && ( + + {unreadNotifications.length} + + )} + + + + + + )} + + +
+
+
+ Close + + + + + + +
+
{/* Content */}
{} + +const Card = forwardRef(({ className, ...props }, ref) => { + return ( +
+ ); +}); +Card.displayName = 'Card'; + +const CardHeader = forwardRef(({ className, ...props }, ref) => { + return
; +}); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = forwardRef>( + ({ className, ...props }, ref) => { + return

; + }, +); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = forwardRef>( + ({ className, ...props }, ref) => { + return

; + }, +); +CardDescription.displayName = 'CardDescription'; + +const CardContent = forwardRef(({ className, ...props }, ref) => { + return

; +}); +CardContent.displayName = 'CardContent'; + +export { Card, CardHeader, CardTitle, CardDescription, CardContent }; diff --git a/app/components/ui/Input.tsx b/app/components/ui/Input.tsx new file mode 100644 index 00000000..35fcda70 --- /dev/null +++ b/app/components/ui/Input.tsx @@ -0,0 +1,25 @@ +import { forwardRef } from 'react'; +import { cn } from '~/lib/utils'; + +export interface InputProps extends React.InputHTMLAttributes {} + +const Input = forwardRef(({ className, type, ...props }, ref) => { + return ( + + ); +}); + +Input.displayName = 'Input'; + +export { Input }; diff --git a/app/components/ui/Label.tsx b/app/components/ui/Label.tsx new file mode 100644 index 00000000..cd7618bf --- /dev/null +++ b/app/components/ui/Label.tsx @@ -0,0 +1,22 @@ +import { forwardRef } from 'react'; +import { cn } from '~/lib/utils'; + +export interface LabelProps extends React.LabelHTMLAttributes {} + +const Label = forwardRef(({ className, ...props }, ref) => { + return ( +