mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
feat: add log level control to user settings
- Add logLevel field to UserProfile interface - Update logger to support 'none' level and remove production restrictions - Add log level selector in settings UI with immediate effect - Remove unused preview synchronization code - Update @webcontainer/api dependency version
This commit is contained in:
@@ -25,6 +25,7 @@ export interface UserProfile {
|
||||
bio?: string;
|
||||
language: string;
|
||||
timezone: string;
|
||||
logLevel?: 'none' | 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
||||
}
|
||||
|
||||
export interface SettingItem {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { classNames } from '~/shared/utils/classNames';
|
||||
import { Switch } from '~/shared/components/ui/Switch';
|
||||
import type { UserProfile } from '~/settings/core/types';
|
||||
import { isMac } from '~/shared/utils/os';
|
||||
import { logger, type DebugLevel } from '~/shared/utils/logger';
|
||||
|
||||
// Helper to get modifier key symbols/text
|
||||
const getModifierSymbol = (modifier: string): string => {
|
||||
@@ -24,13 +25,21 @@ export default function SettingsTab() {
|
||||
const [currentTimezone, setCurrentTimezone] = useState('');
|
||||
const [settings, setSettings] = useState<UserProfile>(() => {
|
||||
const saved = localStorage.getItem('bolt_user_profile');
|
||||
return saved
|
||||
const profile = saved
|
||||
? JSON.parse(saved)
|
||||
: {
|
||||
notifications: true,
|
||||
language: 'en',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
logLevel: 'info' as DebugLevel,
|
||||
};
|
||||
|
||||
// Initialize logger with saved preference
|
||||
if (profile.logLevel) {
|
||||
logger.setLevel(profile.logLevel);
|
||||
}
|
||||
|
||||
return profile;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,9 +58,16 @@ export default function SettingsTab() {
|
||||
notifications: settings.notifications,
|
||||
language: settings.language,
|
||||
timezone: settings.timezone,
|
||||
logLevel: settings.logLevel,
|
||||
};
|
||||
|
||||
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
|
||||
|
||||
// Update logger level when logLevel changes
|
||||
if (settings.logLevel) {
|
||||
logger.setLevel(settings.logLevel);
|
||||
}
|
||||
|
||||
toast.success('Settings updated');
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
@@ -139,6 +155,50 @@ export default function SettingsTab() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="i-ph:bug-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
||||
<label className="block text-sm text-bolt-elements-textSecondary">Logging Level</label>
|
||||
</div>
|
||||
<select
|
||||
value={settings.logLevel || 'info'}
|
||||
onChange={(e) => {
|
||||
const newLogLevel = e.target.value as DebugLevel;
|
||||
setSettings((prev) => ({ ...prev, logLevel: newLogLevel }));
|
||||
|
||||
// Update logger immediately
|
||||
logger.setLevel(newLogLevel);
|
||||
|
||||
// Update localStorage immediately
|
||||
const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
|
||||
const updatedProfile = {
|
||||
...existingProfile,
|
||||
logLevel: newLogLevel,
|
||||
};
|
||||
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
|
||||
|
||||
toast.success(`Logging level set to ${newLogLevel}`);
|
||||
}}
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg text-sm',
|
||||
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
'transition-all duration-200',
|
||||
)}
|
||||
>
|
||||
<option value="none">None - Disable all logging</option>
|
||||
<option value="error">Error - Only critical errors</option>
|
||||
<option value="warn">Warning - Errors and warnings</option>
|
||||
<option value="info">Info - General information (default)</option>
|
||||
<option value="debug">Debug - Detailed debugging info</option>
|
||||
<option value="trace">Trace - Very detailed tracing</option>
|
||||
</select>
|
||||
<p className="text-xs text-bolt-elements-textSecondary mt-1">
|
||||
Controls the verbosity of application logs in the browser console
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Timezone */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
||||
export type DebugLevel = 'none' | 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
||||
import { Chalk } from 'chalk';
|
||||
|
||||
const chalk = new Chalk({ level: 3 });
|
||||
@@ -14,7 +14,7 @@ interface Logger {
|
||||
setLevel: (level: DebugLevel) => void;
|
||||
}
|
||||
|
||||
let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info';
|
||||
let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'none' : 'info';
|
||||
|
||||
export const logger: Logger = {
|
||||
trace: (...messages: any[]) => log('trace', undefined, messages),
|
||||
@@ -37,15 +37,16 @@ export function createScopedLogger(scope: string): Logger {
|
||||
}
|
||||
|
||||
function setLevel(level: DebugLevel) {
|
||||
if ((level === 'trace' || level === 'debug') && import.meta.env.PROD) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentLevel = level;
|
||||
}
|
||||
|
||||
function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
|
||||
const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error'];
|
||||
// If logging is completely disabled, return early
|
||||
if (currentLevel === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
const levelOrder: DebugLevel[] = ['none', 'trace', 'debug', 'info', 'warn', 'error'];
|
||||
|
||||
if (levelOrder.indexOf(level) < levelOrder.indexOf(currentLevel)) {
|
||||
return;
|
||||
@@ -98,6 +99,9 @@ function getLabelStyles(color: string, textColor: string) {
|
||||
|
||||
function getColorForLevel(level: DebugLevel): string {
|
||||
switch (level) {
|
||||
case 'none': {
|
||||
return '#000000';
|
||||
}
|
||||
case 'trace':
|
||||
case 'debug': {
|
||||
return '#77828D';
|
||||
|
||||
@@ -24,7 +24,6 @@ import { Preview } from './Preview';
|
||||
import useViewport from '~/shared/hooks';
|
||||
import { PushToGitHubDialog } from '~/settings/tabs/connections/components/PushToGitHubDialog';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { usePreviewStore } from '~/shared/workbench/stores/previews';
|
||||
import { chatStore } from '~/chat/stores/chat';
|
||||
import type { ElementInfo } from './ui/Inspector';
|
||||
|
||||
@@ -327,16 +326,9 @@ export const Workbench = memo(
|
||||
}, []);
|
||||
|
||||
const onFileSave = useCallback(() => {
|
||||
workbenchStore
|
||||
.saveCurrentDocument()
|
||||
.then(() => {
|
||||
// Explicitly refresh all previews after a file save
|
||||
const previewStore = usePreviewStore();
|
||||
previewStore.refreshAllPreviews();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Failed to update file content');
|
||||
});
|
||||
workbenchStore.saveCurrentDocument().catch(() => {
|
||||
toast.error('Failed to update file content');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onFileReset = useCallback(() => {
|
||||
|
||||
@@ -14,182 +14,28 @@ export interface PreviewInfo {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
// Create a broadcast channel for preview updates
|
||||
const PREVIEW_CHANNEL = 'preview-updates';
|
||||
|
||||
export class PreviewsStore {
|
||||
#availablePreviews = new Map<number, PreviewInfo>();
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
#broadcastChannel: BroadcastChannel;
|
||||
#lastUpdate = new Map<string, number>();
|
||||
#watchedFiles = new Set<string>();
|
||||
#refreshTimeouts = new Map<string, NodeJS.Timeout>();
|
||||
#REFRESH_DELAY = 300;
|
||||
#storageChannel: BroadcastChannel;
|
||||
|
||||
previews = atom<PreviewInfo[]>([]);
|
||||
|
||||
constructor(webcontainerPromise: Promise<WebContainer>) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
this.#broadcastChannel = new BroadcastChannel(PREVIEW_CHANNEL);
|
||||
this.#storageChannel = new BroadcastChannel('storage-sync-channel');
|
||||
|
||||
// Listen for preview updates from other tabs
|
||||
this.#broadcastChannel.onmessage = (event) => {
|
||||
const { type, previewId } = event.data;
|
||||
|
||||
if (type === 'file-change') {
|
||||
const timestamp = event.data.timestamp;
|
||||
const lastUpdate = this.#lastUpdate.get(previewId) || 0;
|
||||
|
||||
if (timestamp > lastUpdate) {
|
||||
this.#lastUpdate.set(previewId, timestamp);
|
||||
this.refreshPreview(previewId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for storage sync messages
|
||||
this.#storageChannel.onmessage = (event) => {
|
||||
const { storage, source } = event.data;
|
||||
|
||||
if (storage && source !== this._getTabId()) {
|
||||
this._syncStorage(storage);
|
||||
}
|
||||
};
|
||||
|
||||
// Override localStorage setItem to catch all changes
|
||||
if (typeof window !== 'undefined') {
|
||||
const originalSetItem = localStorage.setItem;
|
||||
|
||||
localStorage.setItem = (...args) => {
|
||||
originalSetItem.apply(localStorage, args);
|
||||
this._broadcastStorageSync();
|
||||
};
|
||||
}
|
||||
|
||||
this.#init();
|
||||
}
|
||||
|
||||
// Generate a unique ID for this tab
|
||||
private _getTabId(): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (!window._tabId) {
|
||||
window._tabId = Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
return window._tabId;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sync storage data between tabs
|
||||
private _syncStorage(storage: Record<string, string>) {
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.entries(storage).forEach(([key, value]) => {
|
||||
try {
|
||||
const originalSetItem = Object.getPrototypeOf(localStorage).setItem;
|
||||
originalSetItem.call(localStorage, key, value);
|
||||
} catch (error) {
|
||||
console.error('[Preview] Error syncing storage:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Force a refresh after syncing storage
|
||||
const previews = this.previews.get();
|
||||
previews.forEach((preview) => {
|
||||
const previewId = this.getPreviewId(preview.baseUrl);
|
||||
|
||||
if (previewId) {
|
||||
this.refreshPreview(previewId);
|
||||
}
|
||||
});
|
||||
|
||||
// Reload the page content
|
||||
if (typeof window !== 'undefined' && window.location) {
|
||||
const iframe = document.querySelector('iframe');
|
||||
|
||||
if (iframe) {
|
||||
iframe.src = iframe.src;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast storage state to other tabs
|
||||
private _broadcastStorageSync() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const storage: Record<string, string> = {};
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
|
||||
if (key) {
|
||||
storage[key] = localStorage.getItem(key) || '';
|
||||
}
|
||||
}
|
||||
|
||||
this.#storageChannel.postMessage({
|
||||
type: 'storage-sync',
|
||||
storage,
|
||||
source: this._getTabId(),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async #init() {
|
||||
const webcontainer = await this.#webcontainer;
|
||||
|
||||
// Listen for server ready events
|
||||
webcontainer.on('server-ready', (port, url) => {
|
||||
console.log('[Preview] Server ready on port:', port, url);
|
||||
this.broadcastUpdate(url);
|
||||
|
||||
// Initial storage sync when preview is ready
|
||||
this._broadcastStorageSync();
|
||||
});
|
||||
|
||||
try {
|
||||
// Watch for file changes
|
||||
webcontainer.internal.watchPaths(
|
||||
{
|
||||
// Only watch specific file types that affect the preview
|
||||
include: ['**/*.html', '**/*.css', '**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/*.json'],
|
||||
exclude: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/coverage/**'],
|
||||
},
|
||||
async (_events) => {
|
||||
const previews = this.previews.get();
|
||||
|
||||
for (const preview of previews) {
|
||||
const previewId = this.getPreviewId(preview.baseUrl);
|
||||
|
||||
if (previewId) {
|
||||
this.broadcastFileChange(previewId);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Watch for DOM changes that might affect storage
|
||||
if (typeof window !== 'undefined') {
|
||||
const observer = new MutationObserver((_mutations) => {
|
||||
// Broadcast storage changes when DOM changes
|
||||
this._broadcastStorageSync();
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Preview] Error setting up watchers:', error);
|
||||
}
|
||||
|
||||
// Listen for port events
|
||||
webcontainer.on('port', (port, type, url) => {
|
||||
let previewInfo = this.#availablePreviews.get(port);
|
||||
@@ -213,10 +59,6 @@ export class PreviewsStore {
|
||||
previewInfo.baseUrl = url;
|
||||
|
||||
this.previews.set([...previews]);
|
||||
|
||||
if (type === 'open') {
|
||||
this.broadcastUpdate(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,46 +68,6 @@ export class PreviewsStore {
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Broadcast state change to all tabs
|
||||
broadcastStateChange(previewId: string) {
|
||||
const timestamp = Date.now();
|
||||
this.#lastUpdate.set(previewId, timestamp);
|
||||
|
||||
this.#broadcastChannel.postMessage({
|
||||
type: 'state-change',
|
||||
previewId,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast file change to all tabs
|
||||
broadcastFileChange(previewId: string) {
|
||||
const timestamp = Date.now();
|
||||
this.#lastUpdate.set(previewId, timestamp);
|
||||
|
||||
this.#broadcastChannel.postMessage({
|
||||
type: 'file-change',
|
||||
previewId,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast update to all tabs
|
||||
broadcastUpdate(url: string) {
|
||||
const previewId = this.getPreviewId(url);
|
||||
|
||||
if (previewId) {
|
||||
const timestamp = Date.now();
|
||||
this.#lastUpdate.set(previewId, timestamp);
|
||||
|
||||
this.#broadcastChannel.postMessage({
|
||||
type: 'file-change',
|
||||
previewId,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method to refresh a specific preview
|
||||
refreshPreview(previewId: string) {
|
||||
// Clear any pending refresh for this preview
|
||||
@@ -295,18 +97,6 @@ export class PreviewsStore {
|
||||
|
||||
this.#refreshTimeouts.set(previewId, timeout);
|
||||
}
|
||||
|
||||
refreshAllPreviews() {
|
||||
const previews = this.previews.get();
|
||||
|
||||
for (const preview of previews) {
|
||||
const previewId = this.getPreviewId(preview.baseUrl);
|
||||
|
||||
if (previewId) {
|
||||
this.broadcastFileChange(previewId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
|
||||
@@ -579,10 +579,6 @@ export class WorkbenchStore {
|
||||
|
||||
this.#editorStore.updateFile(fullPath, data.action.content);
|
||||
|
||||
if (!isStreaming && data.action.content) {
|
||||
await this.saveFile(fullPath);
|
||||
}
|
||||
|
||||
if (!isStreaming) {
|
||||
await artifact.runner.runAction(data);
|
||||
this.resetAllFileModifications();
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.6",
|
||||
"@unocss/reset": "^0.61.9",
|
||||
"@webcontainer/api": "1.6.1-internal.1",
|
||||
"@webcontainer/api": "1.6.4-internal.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -171,8 +171,8 @@ importers:
|
||||
specifier: ^0.61.9
|
||||
version: 0.61.9
|
||||
'@webcontainer/api':
|
||||
specifier: 1.6.1-internal.1
|
||||
version: 1.6.1-internal.1
|
||||
specifier: 1.6.4-internal.2
|
||||
version: 1.6.4-internal.2
|
||||
'@xterm/addon-fit':
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0(@xterm/xterm@5.5.0)
|
||||
@@ -3515,8 +3515,8 @@ packages:
|
||||
'@web3-storage/multipart-parser@1.0.0':
|
||||
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
|
||||
|
||||
'@webcontainer/api@1.6.1-internal.1':
|
||||
resolution: {integrity: sha512-3kIlLzJonzKlPzONuKudMWHICqwkwyXSsIoUHt4vJwjAr8/qFjlMVxq8ggHS6Yck9JBh1JllDWfCiC95ipSsxw==}
|
||||
'@webcontainer/api@1.6.4-internal.2':
|
||||
resolution: {integrity: sha512-ATzYHu8TyuNXOZ+U+Wh1LLLzWY2gbBINmEXnU9lNEMN7E9IA+EEdpsYca1Or0v8rm2cdxrIARuwbm+ffJdrV3w==}
|
||||
|
||||
'@xmldom/xmldom@0.8.10':
|
||||
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
|
||||
@@ -11658,7 +11658,7 @@ snapshots:
|
||||
|
||||
'@web3-storage/multipart-parser@1.0.0': {}
|
||||
|
||||
'@webcontainer/api@1.6.1-internal.1': {}
|
||||
'@webcontainer/api@1.6.4-internal.2': {}
|
||||
|
||||
'@xmldom/xmldom@0.8.10': {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user