ui refactor

This commit is contained in:
Stijnus
2025-01-20 09:53:15 +01:00
parent 9230ef3b55
commit 436a8e54bf
41 changed files with 4749 additions and 1964 deletions

18
app/lib/api/connection.ts Normal file
View File

@@ -0,0 +1,18 @@
export interface ConnectionStatus {
connected: boolean;
latency: number;
lastChecked: string;
}
export const checkConnection = async (): Promise<ConnectionStatus> => {
/*
* TODO: Implement actual connection check logic
* This is a mock implementation
*/
const connected = Math.random() > 0.1; // 90% chance of being connected
return {
connected,
latency: connected ? Math.floor(Math.random() * 1500) : 0, // Random latency between 0-1500ms
lastChecked: new Date().toISOString(),
};
};

65
app/lib/api/debug.ts Normal file
View File

@@ -0,0 +1,65 @@
export interface DebugWarning {
id: string;
message: string;
timestamp: string;
code: string;
}
export interface DebugError {
id: string;
message: string;
timestamp: string;
stack?: string;
}
export interface DebugStatus {
warnings: DebugWarning[];
errors: DebugError[];
lastChecked: string;
}
export interface DebugIssue {
id: string;
type: 'warning' | 'error';
message: string;
}
export const getDebugStatus = async (): Promise<DebugStatus> => {
/*
* TODO: Implement actual debug status logic
* This is a mock implementation
*/
return {
warnings: [
{
id: 'warn-1',
message: 'High memory usage detected',
timestamp: new Date().toISOString(),
code: 'MEM_HIGH',
},
],
errors: [
{
id: 'err-1',
message: 'Failed to connect to database',
timestamp: new Date().toISOString(),
stack: 'Error: Connection timeout',
},
],
lastChecked: new Date().toISOString(),
};
};
export const acknowledgeWarning = async (warningId: string): Promise<void> => {
/*
* TODO: Implement actual warning acknowledgment logic
*/
console.log(`Acknowledging warning ${warningId}`);
};
export const acknowledgeError = async (errorId: string): Promise<void> => {
/*
* TODO: Implement actual error acknowledgment logic
*/
console.log(`Acknowledging error ${errorId}`);
};

35
app/lib/api/features.ts Normal file
View File

@@ -0,0 +1,35 @@
export interface Feature {
id: string;
name: string;
description: string;
viewed: boolean;
releaseDate: string;
}
export const getFeatureFlags = async (): Promise<Feature[]> => {
/*
* TODO: Implement actual feature flags logic
* This is a mock implementation
*/
return [
{
id: 'feature-1',
name: 'Dark Mode',
description: 'Enable dark mode for better night viewing',
viewed: true,
releaseDate: '2024-03-15',
},
{
id: 'feature-2',
name: 'Tab Management',
description: 'Customize your tab layout',
viewed: false,
releaseDate: '2024-03-20',
},
];
};
export const markFeatureViewed = async (featureId: string): Promise<void> => {
/* TODO: Implement actual feature viewed logic */
console.log(`Marking feature ${featureId} as viewed`);
};

View File

@@ -0,0 +1,40 @@
export interface Notification {
id: string;
title: string;
message: string;
type: 'info' | 'warning' | 'error' | 'success';
read: boolean;
timestamp: string;
}
export const getNotifications = async (): Promise<Notification[]> => {
/*
* TODO: Implement actual notifications logic
* This is a mock implementation
*/
return [
{
id: 'notif-1',
title: 'Welcome to Bolt',
message: 'Get started by exploring the features',
type: 'info',
read: true,
timestamp: new Date().toISOString(),
},
{
id: 'notif-2',
title: 'New Update Available',
message: 'Version 1.0.1 is now available',
type: 'info',
read: false,
timestamp: new Date().toISOString(),
},
];
};
export const markNotificationRead = async (notificationId: string): Promise<void> => {
/*
* TODO: Implement actual notification read logic
*/
console.log(`Marking notification ${notificationId} as read`);
};

28
app/lib/api/updates.ts Normal file
View File

@@ -0,0 +1,28 @@
export interface UpdateCheckResult {
available: boolean;
version: string;
releaseNotes?: string;
}
export const checkForUpdates = async (): Promise<UpdateCheckResult> => {
/*
* TODO: Implement actual update check logic
* This is a mock implementation
*/
return {
available: Math.random() > 0.7, // 30% chance of update
version: '1.0.1',
releaseNotes: 'Bug fixes and performance improvements',
};
};
export const acknowledgeUpdate = async (version: string): Promise<void> => {
/*
* TODO: Implement actual update acknowledgment logic
* This is a mock implementation that would typically:
* 1. Store the acknowledged version in a persistent store
* 2. Update the UI state
* 3. Potentially send analytics
*/
console.log(`Acknowledging update version ${version}`);
};

View File

@@ -4,3 +4,8 @@ export * from './useShortcuts';
export * from './useSnapScroll';
export * from './useEditChatDescription';
export { default } from './useViewport';
export { useUpdateCheck } from './useUpdateCheck';
export { useFeatures } from './useFeatures';
export { useNotifications } from './useNotifications';
export { useConnectionStatus } from './useConnectionStatus';
export { useDebugStatus } from './useDebugStatus';

View File

@@ -0,0 +1,61 @@
import { useState, useEffect } from 'react';
import { checkConnection } from '~/lib/api/connection';
const ACKNOWLEDGED_CONNECTION_ISSUE_KEY = 'bolt_acknowledged_connection_issue';
type ConnectionIssueType = 'disconnected' | 'high-latency' | null;
const getAcknowledgedIssue = (): string | null => {
try {
return localStorage.getItem(ACKNOWLEDGED_CONNECTION_ISSUE_KEY);
} catch {
return null;
}
};
export const useConnectionStatus = () => {
const [hasConnectionIssues, setHasConnectionIssues] = useState(false);
const [currentIssue, setCurrentIssue] = useState<ConnectionIssueType>(null);
const [acknowledgedIssue, setAcknowledgedIssue] = useState<string | null>(() => getAcknowledgedIssue());
const checkStatus = async () => {
try {
const status = await checkConnection();
const issue = !status.connected ? 'disconnected' : status.latency > 1000 ? 'high-latency' : null;
setCurrentIssue(issue);
// Only show issues if they're new or different from the acknowledged one
setHasConnectionIssues(issue !== null && issue !== acknowledgedIssue);
} catch (error) {
console.error('Failed to check connection:', error);
// Show connection issues if we can't even check the status
setCurrentIssue('disconnected');
setHasConnectionIssues(true);
}
};
useEffect(() => {
// Check immediately and then every 10 seconds
checkStatus();
const interval = setInterval(checkStatus, 10 * 1000);
return () => clearInterval(interval);
}, [acknowledgedIssue]);
const acknowledgeIssue = () => {
setAcknowledgedIssue(currentIssue);
setAcknowledgedIssue(currentIssue);
setHasConnectionIssues(false);
};
const resetAcknowledgment = () => {
setAcknowledgedIssue(null);
setAcknowledgedIssue(null);
checkStatus();
};
return { hasConnectionIssues, currentIssue, acknowledgeIssue, resetAcknowledgment };
};

View File

@@ -0,0 +1,89 @@
import { useState, useEffect } from 'react';
import { getDebugStatus, acknowledgeWarning, acknowledgeError, type DebugIssue } from '~/lib/api/debug';
const ACKNOWLEDGED_DEBUG_ISSUES_KEY = 'bolt_acknowledged_debug_issues';
const getAcknowledgedIssues = (): string[] => {
try {
const stored = localStorage.getItem(ACKNOWLEDGED_DEBUG_ISSUES_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
};
const setAcknowledgedIssues = (issueIds: string[]) => {
try {
localStorage.setItem(ACKNOWLEDGED_DEBUG_ISSUES_KEY, JSON.stringify(issueIds));
} catch (error) {
console.error('Failed to persist acknowledged debug issues:', error);
}
};
export const useDebugStatus = () => {
const [hasActiveWarnings, setHasActiveWarnings] = useState(false);
const [activeIssues, setActiveIssues] = useState<DebugIssue[]>([]);
const [acknowledgedIssueIds, setAcknowledgedIssueIds] = useState<string[]>(() => getAcknowledgedIssues());
const checkDebugStatus = async () => {
try {
const status = await getDebugStatus();
const issues: DebugIssue[] = [
...status.warnings.map((w) => ({ ...w, type: 'warning' as const })),
...status.errors.map((e) => ({ ...e, type: 'error' as const })),
].filter((issue) => !acknowledgedIssueIds.includes(issue.id));
setActiveIssues(issues);
setHasActiveWarnings(issues.length > 0);
} catch (error) {
console.error('Failed to check debug status:', error);
}
};
useEffect(() => {
// Check immediately and then every 5 seconds
checkDebugStatus();
const interval = setInterval(checkDebugStatus, 5 * 1000);
return () => clearInterval(interval);
}, [acknowledgedIssueIds]);
const acknowledgeIssue = async (issue: DebugIssue) => {
try {
if (issue.type === 'warning') {
await acknowledgeWarning(issue.id);
} else {
await acknowledgeError(issue.id);
}
const newAcknowledgedIds = [...acknowledgedIssueIds, issue.id];
setAcknowledgedIssueIds(newAcknowledgedIds);
setAcknowledgedIssues(newAcknowledgedIds);
setActiveIssues((prev) => prev.filter((i) => i.id !== issue.id));
setHasActiveWarnings(activeIssues.length > 1);
} catch (error) {
console.error('Failed to acknowledge issue:', error);
}
};
const acknowledgeAllIssues = async () => {
try {
await Promise.all(
activeIssues.map((issue) =>
issue.type === 'warning' ? acknowledgeWarning(issue.id) : acknowledgeError(issue.id),
),
);
const newAcknowledgedIds = [...acknowledgedIssueIds, ...activeIssues.map((i) => i.id)];
setAcknowledgedIssueIds(newAcknowledgedIds);
setAcknowledgedIssues(newAcknowledgedIds);
setActiveIssues([]);
setHasActiveWarnings(false);
} catch (error) {
console.error('Failed to acknowledge all issues:', error);
}
};
return { hasActiveWarnings, activeIssues, acknowledgeIssue, acknowledgeAllIssues };
};

View File

@@ -0,0 +1,72 @@
import { useState, useEffect } from 'react';
import { getFeatureFlags, markFeatureViewed, type Feature } from '~/lib/api/features';
const VIEWED_FEATURES_KEY = 'bolt_viewed_features';
const getViewedFeatures = (): string[] => {
try {
const stored = localStorage.getItem(VIEWED_FEATURES_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
};
const setViewedFeatures = (featureIds: string[]) => {
try {
localStorage.setItem(VIEWED_FEATURES_KEY, JSON.stringify(featureIds));
} catch (error) {
console.error('Failed to persist viewed features:', error);
}
};
export const useFeatures = () => {
const [hasNewFeatures, setHasNewFeatures] = useState(false);
const [unviewedFeatures, setUnviewedFeatures] = useState<Feature[]>([]);
const [viewedFeatureIds, setViewedFeatureIds] = useState<string[]>(() => getViewedFeatures());
useEffect(() => {
const checkNewFeatures = async () => {
try {
const features = await getFeatureFlags();
const unviewed = features.filter((feature) => !viewedFeatureIds.includes(feature.id));
setUnviewedFeatures(unviewed);
setHasNewFeatures(unviewed.length > 0);
} catch (error) {
console.error('Failed to check for new features:', error);
}
};
checkNewFeatures();
}, [viewedFeatureIds]);
const acknowledgeFeature = async (featureId: string) => {
try {
await markFeatureViewed(featureId);
const newViewedIds = [...viewedFeatureIds, featureId];
setViewedFeatureIds(newViewedIds);
setViewedFeatures(newViewedIds);
setUnviewedFeatures((prev) => prev.filter((feature) => feature.id !== featureId));
setHasNewFeatures(unviewedFeatures.length > 1);
} catch (error) {
console.error('Failed to acknowledge feature:', error);
}
};
const acknowledgeAllFeatures = async () => {
try {
await Promise.all(unviewedFeatures.map((feature) => markFeatureViewed(feature.id)));
const newViewedIds = [...viewedFeatureIds, ...unviewedFeatures.map((f) => f.id)];
setViewedFeatureIds(newViewedIds);
setViewedFeatures(newViewedIds);
setUnviewedFeatures([]);
setHasNewFeatures(false);
} catch (error) {
console.error('Failed to acknowledge all features:', error);
}
};
return { hasNewFeatures, unviewedFeatures, acknowledgeFeature, acknowledgeAllFeatures };
};

View File

@@ -0,0 +1,77 @@
import { useState, useEffect } from 'react';
import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications';
const READ_NOTIFICATIONS_KEY = 'bolt_read_notifications';
const getReadNotifications = (): string[] => {
try {
const stored = localStorage.getItem(READ_NOTIFICATIONS_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
};
const setReadNotifications = (notificationIds: string[]) => {
try {
localStorage.setItem(READ_NOTIFICATIONS_KEY, JSON.stringify(notificationIds));
} catch (error) {
console.error('Failed to persist read notifications:', error);
}
};
export const useNotifications = () => {
const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
const [unreadNotifications, setUnreadNotifications] = useState<Notification[]>([]);
const [readNotificationIds, setReadNotificationIds] = useState<string[]>(() => getReadNotifications());
const checkNotifications = async () => {
try {
const notifications = await getNotifications();
const unread = notifications.filter((n) => !readNotificationIds.includes(n.id));
setUnreadNotifications(unread);
setHasUnreadNotifications(unread.length > 0);
} catch (error) {
console.error('Failed to check notifications:', error);
}
};
useEffect(() => {
// Check immediately and then every minute
checkNotifications();
const interval = setInterval(checkNotifications, 60 * 1000);
return () => clearInterval(interval);
}, [readNotificationIds]);
const markAsRead = async (notificationId: string) => {
try {
await markNotificationRead(notificationId);
const newReadIds = [...readNotificationIds, notificationId];
setReadNotificationIds(newReadIds);
setReadNotifications(newReadIds);
setUnreadNotifications((prev) => prev.filter((n) => n.id !== notificationId));
setHasUnreadNotifications(unreadNotifications.length > 1);
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
};
const markAllAsRead = async () => {
try {
await Promise.all(unreadNotifications.map((n) => markNotificationRead(n.id)));
const newReadIds = [...readNotificationIds, ...unreadNotifications.map((n) => n.id)];
setReadNotificationIds(newReadIds);
setReadNotifications(newReadIds);
setUnreadNotifications([]);
setHasUnreadNotifications(false);
} catch (error) {
console.error('Failed to mark all notifications as read:', error);
}
};
return { hasUnreadNotifications, unreadNotifications, markAsRead, markAllAsRead };
};

View File

@@ -0,0 +1,222 @@
import { useStore } from '@nanostores/react';
import {
isDebugMode,
isEventLogsEnabled,
isLocalModelsEnabled,
LOCAL_PROVIDERS,
promptStore,
providersStore,
latestBranchStore,
autoSelectStarterTemplate,
enableContextOptimizationStore,
tabConfigurationStore,
updateTabConfiguration as updateTabConfig,
resetTabConfiguration as resetTabConfig,
} from '~/lib/stores/settings';
import { useCallback, useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import type { IProviderSetting, ProviderInfo, IProviderConfig } from '~/types/model';
import type { TabWindowConfig, TabVisibilityConfig } from '~/components/settings/settings.types';
import { logStore } from '~/lib/stores/logs';
import { getLocalStorage, setLocalStorage } from '~/utils/localStorage';
export interface Settings {
theme: 'light' | 'dark' | 'system';
language: string;
notifications: boolean;
eventLogs: boolean;
timezone: string;
tabConfiguration: TabWindowConfig;
}
export interface UseSettingsReturn {
// Theme and UI settings
setTheme: (theme: Settings['theme']) => void;
setLanguage: (language: string) => void;
setNotifications: (enabled: boolean) => void;
setEventLogs: (enabled: boolean) => void;
setTimezone: (timezone: string) => void;
settings: Settings;
// Provider settings
providers: Record<string, IProviderConfig>;
activeProviders: ProviderInfo[];
updateProviderSettings: (provider: string, config: IProviderSetting) => void;
isLocalModel: boolean;
enableLocalModels: (enabled: boolean) => void;
// Debug and development settings
debug: boolean;
enableDebugMode: (enabled: boolean) => void;
eventLogs: boolean;
promptId: string;
setPromptId: (promptId: string) => void;
isLatestBranch: boolean;
enableLatestBranch: (enabled: boolean) => void;
autoSelectTemplate: boolean;
setAutoSelectTemplate: (enabled: boolean) => void;
contextOptimizationEnabled: boolean;
enableContextOptimization: (enabled: boolean) => void;
// Tab configuration
tabConfiguration: TabWindowConfig;
updateTabConfiguration: (config: TabVisibilityConfig) => void;
resetTabConfiguration: () => void;
}
export function useSettings(): UseSettingsReturn {
const providers = useStore(providersStore) as Record<string, IProviderConfig>;
const debug = useStore(isDebugMode);
const eventLogs = useStore(isEventLogsEnabled);
const promptId = useStore(promptStore);
const isLocalModel = useStore(isLocalModelsEnabled);
const isLatestBranch = useStore(latestBranchStore);
const autoSelectTemplate = useStore(autoSelectStarterTemplate);
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
const tabConfiguration = useStore(tabConfigurationStore);
const [settings, setSettings] = useState<Settings>(() => {
const storedSettings = getLocalStorage('settings');
return {
theme: storedSettings?.theme || 'system',
language: storedSettings?.language || 'en',
notifications: storedSettings?.notifications ?? true,
eventLogs: storedSettings?.eventLogs ?? true,
timezone: storedSettings?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
tabConfiguration,
};
});
// writing values to cookies on change
useEffect(() => {
const providers = providersStore.get();
const providerSetting: Record<string, IProviderSetting> = {};
Object.keys(providers).forEach((provider) => {
providerSetting[provider] = providers[provider].settings;
});
Cookies.set('providers', JSON.stringify(providerSetting));
}, [providers]);
useEffect(() => {
let active = Object.entries(providers)
.filter(([_key, provider]) => provider.settings.enabled)
.map(([_k, p]) => p);
if (!isLocalModel) {
active = active.filter((p) => !LOCAL_PROVIDERS.includes(p.name));
}
setActiveProviders(active);
}, [providers, isLocalModel]);
const saveSettings = useCallback((newSettings: Partial<Settings>) => {
setSettings((prev) => {
const updated = { ...prev, ...newSettings };
setLocalStorage('settings', updated);
return updated;
});
}, []);
const updateProviderSettings = useCallback((provider: string, config: IProviderSetting) => {
providersStore.setKey(provider, { settings: config } as IProviderConfig);
}, []);
const enableDebugMode = useCallback((enabled: boolean) => {
isDebugMode.set(enabled);
logStore.logSystem(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isDebugEnabled', String(enabled));
}, []);
const setEventLogs = useCallback((enabled: boolean) => {
isEventLogsEnabled.set(enabled);
logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isEventLogsEnabled', String(enabled));
}, []);
const enableLocalModels = useCallback((enabled: boolean) => {
isLocalModelsEnabled.set(enabled);
logStore.logSystem(`Local models ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isLocalModelsEnabled', String(enabled));
}, []);
const setPromptId = useCallback((promptId: string) => {
promptStore.set(promptId);
Cookies.set('promptId', promptId);
}, []);
const enableLatestBranch = useCallback((enabled: boolean) => {
latestBranchStore.set(enabled);
logStore.logSystem(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isLatestBranch', String(enabled));
}, []);
const setAutoSelectTemplate = useCallback((enabled: boolean) => {
autoSelectStarterTemplate.set(enabled);
logStore.logSystem(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('autoSelectTemplate', String(enabled));
}, []);
const enableContextOptimization = useCallback((enabled: boolean) => {
enableContextOptimizationStore.set(enabled);
logStore.logSystem(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('contextOptimizationEnabled', String(enabled));
}, []);
const setTheme = useCallback(
(theme: Settings['theme']) => {
saveSettings({ theme });
},
[saveSettings],
);
const setLanguage = useCallback(
(language: string) => {
saveSettings({ language });
},
[saveSettings],
);
const setNotifications = useCallback(
(enabled: boolean) => {
saveSettings({ notifications: enabled });
},
[saveSettings],
);
const setTimezone = useCallback(
(timezone: string) => {
saveSettings({ timezone });
},
[saveSettings],
);
return {
...settings,
providers,
activeProviders,
updateProviderSettings,
isLocalModel,
enableLocalModels,
debug,
enableDebugMode,
eventLogs,
setEventLogs,
promptId,
setPromptId,
isLatestBranch,
enableLatestBranch,
autoSelectTemplate,
setAutoSelectTemplate,
contextOptimizationEnabled,
enableContextOptimization,
setTheme,
setLanguage,
setNotifications,
setTimezone,
settings,
tabConfiguration,
updateTabConfiguration: updateTabConfig,
resetTabConfiguration: resetTabConfig,
};
}

View File

@@ -1,229 +0,0 @@
import { useStore } from '@nanostores/react';
import {
isDebugMode,
isEventLogsEnabled,
isLocalModelsEnabled,
LOCAL_PROVIDERS,
promptStore,
providersStore,
latestBranchStore,
autoSelectStarterTemplate,
enableContextOptimizationStore,
} from '~/lib/stores/settings';
import { useCallback, useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import type { IProviderSetting, ProviderInfo } from '~/types/model';
import { logStore } from '~/lib/stores/logs'; // assuming logStore is imported from this location
interface CommitData {
commit: string;
version?: string;
}
const versionData: CommitData = {
commit: __COMMIT_HASH,
version: __APP_VERSION,
};
export function useSettings() {
const providers = useStore(providersStore);
const debug = useStore(isDebugMode);
const eventLogs = useStore(isEventLogsEnabled);
const promptId = useStore(promptStore);
const isLocalModel = useStore(isLocalModelsEnabled);
const isLatestBranch = useStore(latestBranchStore);
const autoSelectTemplate = useStore(autoSelectStarterTemplate);
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
// Function to check if we're on stable version
const checkIsStableVersion = async () => {
try {
const response = await fetch(
`https://api.github.com/repos/stackblitz-labs/bolt.diy/git/refs/tags/v${versionData.version}`,
);
const data: { object: { sha: string } } = await response.json();
return versionData.commit.slice(0, 7) === data.object.sha.slice(0, 7);
} catch (error) {
console.warn('Error checking stable version:', error);
return false;
}
};
// reading values from cookies on mount
useEffect(() => {
const savedProviders = Cookies.get('providers');
if (savedProviders) {
try {
const parsedProviders: Record<string, IProviderSetting> = JSON.parse(savedProviders);
Object.keys(providers).forEach((provider) => {
const currentProviderSettings = parsedProviders[provider];
if (currentProviderSettings) {
providersStore.setKey(provider, {
...providers[provider],
settings: {
...currentProviderSettings,
enabled: currentProviderSettings.enabled ?? true,
},
});
}
});
} catch (error) {
console.error('Failed to parse providers from cookies:', error);
}
}
// load debug mode from cookies
const savedDebugMode = Cookies.get('isDebugEnabled');
if (savedDebugMode) {
isDebugMode.set(savedDebugMode === 'true');
}
// load event logs from cookies
const savedEventLogs = Cookies.get('isEventLogsEnabled');
if (savedEventLogs) {
isEventLogsEnabled.set(savedEventLogs === 'true');
}
// load local models from cookies
const savedLocalModels = Cookies.get('isLocalModelsEnabled');
if (savedLocalModels) {
isLocalModelsEnabled.set(savedLocalModels === 'true');
}
const promptId = Cookies.get('promptId');
if (promptId) {
promptStore.set(promptId);
}
// load latest branch setting from cookies or determine based on version
const savedLatestBranch = Cookies.get('isLatestBranch');
let checkCommit = Cookies.get('commitHash');
if (checkCommit === undefined) {
checkCommit = versionData.commit;
}
if (savedLatestBranch === undefined || checkCommit !== versionData.commit) {
// If setting hasn't been set by user, check version
checkIsStableVersion().then((isStable) => {
const shouldUseLatest = !isStable;
latestBranchStore.set(shouldUseLatest);
Cookies.set('isLatestBranch', String(shouldUseLatest));
Cookies.set('commitHash', String(versionData.commit));
});
} else {
latestBranchStore.set(savedLatestBranch === 'true');
}
const autoSelectTemplate = Cookies.get('autoSelectTemplate');
if (autoSelectTemplate) {
autoSelectStarterTemplate.set(autoSelectTemplate === 'true');
}
const savedContextOptimizationEnabled = Cookies.get('contextOptimizationEnabled');
if (savedContextOptimizationEnabled) {
enableContextOptimizationStore.set(savedContextOptimizationEnabled === 'true');
}
}, []);
// writing values to cookies on change
useEffect(() => {
const providers = providersStore.get();
const providerSetting: Record<string, IProviderSetting> = {};
Object.keys(providers).forEach((provider) => {
providerSetting[provider] = providers[provider].settings;
});
Cookies.set('providers', JSON.stringify(providerSetting));
}, [providers]);
useEffect(() => {
let active = Object.entries(providers)
.filter(([_key, provider]) => provider.settings.enabled)
.map(([_k, p]) => p);
if (!isLocalModel) {
active = active.filter((p) => !LOCAL_PROVIDERS.includes(p.name));
}
setActiveProviders(active);
}, [providers, isLocalModel]);
// helper function to update settings
const updateProviderSettings = useCallback(
(provider: string, config: IProviderSetting) => {
const settings = providers[provider].settings;
providersStore.setKey(provider, { ...providers[provider], settings: { ...settings, ...config } });
},
[providers],
);
const enableDebugMode = useCallback((enabled: boolean) => {
isDebugMode.set(enabled);
logStore.logSystem(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isDebugEnabled', String(enabled));
}, []);
const enableEventLogs = useCallback((enabled: boolean) => {
isEventLogsEnabled.set(enabled);
logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isEventLogsEnabled', String(enabled));
}, []);
const enableLocalModels = useCallback((enabled: boolean) => {
isLocalModelsEnabled.set(enabled);
logStore.logSystem(`Local models ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isLocalModelsEnabled', String(enabled));
}, []);
const setPromptId = useCallback((promptId: string) => {
promptStore.set(promptId);
Cookies.set('promptId', promptId);
}, []);
const enableLatestBranch = useCallback((enabled: boolean) => {
latestBranchStore.set(enabled);
logStore.logSystem(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isLatestBranch', String(enabled));
}, []);
const setAutoSelectTemplate = useCallback((enabled: boolean) => {
autoSelectStarterTemplate.set(enabled);
logStore.logSystem(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('autoSelectTemplate', String(enabled));
}, []);
const enableContextOptimization = useCallback((enabled: boolean) => {
enableContextOptimizationStore.set(enabled);
logStore.logSystem(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('contextOptimizationEnabled', String(enabled));
}, []);
return {
providers,
activeProviders,
updateProviderSettings,
debug,
enableDebugMode,
eventLogs,
enableEventLogs,
isLocalModel,
enableLocalModels,
promptId,
setPromptId,
isLatestBranch,
enableLatestBranch,
autoSelectTemplate,
setAutoSelectTemplate,
contextOptimizationEnabled,
enableContextOptimization,
};
}

View File

@@ -0,0 +1,58 @@
import { useState, useEffect } from 'react';
import { checkForUpdates, acknowledgeUpdate } from '~/lib/api/updates';
const LAST_ACKNOWLEDGED_VERSION_KEY = 'bolt_last_acknowledged_version';
export const useUpdateCheck = () => {
const [hasUpdate, setHasUpdate] = useState(false);
const [currentVersion, setCurrentVersion] = useState<string>('');
const [lastAcknowledgedVersion, setLastAcknowledgedVersion] = useState<string | null>(() => {
try {
return localStorage.getItem(LAST_ACKNOWLEDGED_VERSION_KEY);
} catch {
return null;
}
});
useEffect(() => {
const checkUpdate = async () => {
try {
const { available, version } = await checkForUpdates();
setCurrentVersion(version);
// Only show update if it's a new version and hasn't been acknowledged
setHasUpdate(available && version !== lastAcknowledgedVersion);
} catch (error) {
console.error('Failed to check for updates:', error);
}
};
// Check immediately and then every 30 minutes
checkUpdate();
const interval = setInterval(checkUpdate, 30 * 60 * 1000);
return () => clearInterval(interval);
}, [lastAcknowledgedVersion]);
const handleAcknowledgeUpdate = async () => {
try {
const { version } = await checkForUpdates();
await acknowledgeUpdate(version);
// Store in localStorage
try {
localStorage.setItem(LAST_ACKNOWLEDGED_VERSION_KEY, version);
} catch (error) {
console.error('Failed to persist acknowledged version:', error);
}
setLastAcknowledgedVersion(version);
setHasUpdate(false);
} catch (error) {
console.error('Failed to acknowledge update:', error);
}
};
return { hasUpdate, currentVersion, acknowledgeUpdate: handleAcknowledgeUpdate };
};

View File

@@ -11,7 +11,8 @@ export default class GithubProvider extends BaseProvider {
config = {
apiTokenKey: 'GITHUB_API_KEY',
};
// find more in https://github.com/marketplace?type=models
// find more in https://github.com/marketplace?type=models
staticModels: ModelInfo[] = [
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
{ name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },

View File

@@ -10,7 +10,10 @@ export interface LogEntry {
level: 'info' | 'warning' | 'error' | 'debug';
message: string;
details?: Record<string, any>;
category: 'system' | 'provider' | 'user' | 'error';
category: 'system' | 'provider' | 'user' | 'error' | 'api' | 'auth' | 'database' | 'network';
subCategory?: string;
duration?: number;
statusCode?: number;
}
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
@@ -101,18 +104,76 @@ class LogStore {
return this.addLog(message, 'info', 'user', details);
}
// API Connection Logging
logAPIRequest(endpoint: string, method: string, duration: number, statusCode: number, details?: Record<string, any>) {
const message = `${method} ${endpoint} - ${statusCode} (${duration}ms)`;
const level = statusCode >= 400 ? 'error' : statusCode >= 300 ? 'warning' : 'info';
return this.addLog(message, level, 'api', {
...details,
endpoint,
method,
duration,
statusCode,
timestamp: new Date().toISOString(),
});
}
// Authentication Logging
logAuth(
action: 'login' | 'logout' | 'token_refresh' | 'key_validation',
success: boolean,
details?: Record<string, any>,
) {
const message = `Auth ${action} - ${success ? 'Success' : 'Failed'}`;
const level = success ? 'info' : 'error';
return this.addLog(message, level, 'auth', {
...details,
action,
success,
timestamp: new Date().toISOString(),
});
}
// Network Status Logging
logNetworkStatus(status: 'online' | 'offline' | 'reconnecting' | 'connected', details?: Record<string, any>) {
const message = `Network ${status}`;
const level = status === 'offline' ? 'error' : status === 'reconnecting' ? 'warning' : 'info';
return this.addLog(message, level, 'network', {
...details,
status,
timestamp: new Date().toISOString(),
});
}
// Database Operations Logging
logDatabase(operation: string, success: boolean, duration: number, details?: Record<string, any>) {
const message = `DB ${operation} - ${success ? 'Success' : 'Failed'} (${duration}ms)`;
const level = success ? 'info' : 'error';
return this.addLog(message, level, 'database', {
...details,
operation,
success,
duration,
timestamp: new Date().toISOString(),
});
}
// Error events
logError(message: string, error?: Error | unknown, details?: Record<string, any>) {
const errorDetails = {
...(details || {}),
error:
error instanceof Error
? {
message: error.message,
stack: error.stack,
}
: error,
};
const errorDetails =
error instanceof Error
? {
name: error.name,
message: error.message,
stack: error.stack,
...details,
}
: { error, ...details };
return this.addLog(message, 'error', 'error', errorDetails);
}

View File

@@ -2,6 +2,9 @@ import { atom, map } from 'nanostores';
import { workbenchStore } from './workbench';
import { PROVIDER_LIST } from '~/utils/constants';
import type { IProviderConfig } from '~/types/model';
import type { TabVisibilityConfig, TabWindowConfig } from '~/components/settings/settings.types';
import { DEFAULT_TAB_CONFIG } from '~/components/settings/settings.types';
import Cookies from 'js-cookie';
export interface Shortcut {
key: string;
@@ -46,7 +49,9 @@ export const providersStore = map<ProviderSetting>(initialProviderSettings);
export const isDebugMode = atom(false);
export const isEventLogsEnabled = atom(false);
// Initialize event logs from cookie or default to false
const savedEventLogs = Cookies.get('isEventLogsEnabled');
export const isEventLogsEnabled = atom(savedEventLogs === 'true');
export const isLocalModelsEnabled = atom(true);
@@ -56,3 +61,48 @@ export const latestBranchStore = atom(false);
export const autoSelectStarterTemplate = atom(false);
export const enableContextOptimizationStore = atom(false);
// Initialize tab configuration from cookie or default
const savedTabConfig = Cookies.get('tabConfiguration');
const initialTabConfig: TabWindowConfig = savedTabConfig
? JSON.parse(savedTabConfig)
: {
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
};
export const tabConfigurationStore = map<TabWindowConfig>(initialTabConfig);
// Helper function to update tab configuration
export const updateTabConfiguration = (config: TabVisibilityConfig) => {
const currentConfig = tabConfigurationStore.get();
const isUserTab = config.window === 'user';
const targetArray = isUserTab ? 'userTabs' : 'developerTabs';
// Only update the tab in its respective window
const updatedTabs = currentConfig[targetArray].map((tab) => (tab.id === config.id ? { ...config } : tab));
// If tab doesn't exist in this window yet, add it
if (!updatedTabs.find((tab) => tab.id === config.id)) {
updatedTabs.push(config);
}
// Create new config, only updating the target window's tabs
const newConfig: TabWindowConfig = {
...currentConfig,
[targetArray]: updatedTabs,
};
tabConfigurationStore.set(newConfig);
Cookies.set('tabConfiguration', JSON.stringify(newConfig));
};
// Helper function to reset tab configuration
export const resetTabConfiguration = () => {
const defaultConfig: TabWindowConfig = {
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
};
tabConfigurationStore.set(defaultConfig);
Cookies.set('tabConfiguration', JSON.stringify(defaultConfig));
};