Avatar Fix , control pannel UI fix

This commit is contained in:
Stijnus 2025-02-03 01:04:23 +01:00
parent f3468d495d
commit f091409f7e
7 changed files with 392 additions and 164 deletions

View File

@ -22,6 +22,7 @@ import type { TabType, TabVisibilityConfig, Profile } from './types';
import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants'; import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
import { DialogTitle } from '~/components/ui/Dialog'; import { DialogTitle } from '~/components/ui/Dialog';
import { AvatarDropdown } from './AvatarDropdown'; import { AvatarDropdown } from './AvatarDropdown';
import BackgroundRays from '~/components/ui/BackgroundRays';
// Import all tab components // Import all tab components
import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab'; import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab';
@ -83,7 +84,7 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
}; };
// Beta status for experimental features // Beta status for experimental features
const BETA_TABS = new Set<TabType>(['task-manager', 'service-status']); const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']);
const BetaLabel = () => ( const BetaLabel = () => (
<div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20"> <div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20">
@ -415,108 +416,114 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
'rounded-2xl shadow-2xl', 'rounded-2xl shadow-2xl',
'border border-[#E5E5E5] dark:border-[#1A1A1A]', 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'flex flex-col overflow-hidden', 'flex flex-col overflow-hidden',
'relative',
)} )}
initial={{ opacity: 0, scale: 0.95, y: 20 }} initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }} animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }} exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
{/* Header */} <div className="absolute inset-0 overflow-hidden rounded-2xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700"> <BackgroundRays />
<div className="flex items-center space-x-4"> </div>
{(activeTab || showTabManagement) && ( <div className="relative z-10 flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-4">
{(activeTab || showTabManagement) && (
<button
onClick={handleBack}
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
>
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</button>
)}
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
{showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
</DialogTitle>
</div>
<div className="flex items-center gap-6">
{/* Mode Toggle */}
<div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
<AnimatedSwitch
id="developer-mode"
checked={developerMode}
onCheckedChange={handleDeveloperModeChange}
label={developerMode ? 'Developer Mode' : 'User Mode'}
/>
</div>
{/* Avatar and Dropdown */}
<div className="border-l border-gray-200 dark:border-gray-800 pl-6">
<AvatarDropdown onSelectTab={handleTabClick} />
</div>
{/* Close Button */}
<button <button
onClick={handleBack} onClick={onClose}
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200" className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
> >
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" /> <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</button> </button>
</div>
</div>
{/* Content */}
<div
className={classNames(
'flex-1',
'overflow-y-auto',
'hover:overflow-y-auto',
'scrollbar scrollbar-w-2',
'scrollbar-track-transparent',
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
'will-change-scroll',
'touch-auto',
)} )}
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
{showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
</DialogTitle>
</div>
<div className="flex items-center gap-6">
{/* Mode Toggle */}
<div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
<AnimatedSwitch
id="developer-mode"
checked={developerMode}
onCheckedChange={handleDeveloperModeChange}
label={developerMode ? 'Developer Mode' : 'User Mode'}
/>
</div>
{/* Avatar and Dropdown */}
<div className="border-l border-gray-200 dark:border-gray-800 pl-6">
<AvatarDropdown onSelectTab={handleTabClick} />
</div>
{/* Close Button */}
<button
onClick={onClose}
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
>
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
</button>
</div>
</div>
{/* Content */}
<div
className={classNames(
'flex-1',
'overflow-y-auto',
'hover:overflow-y-auto',
'scrollbar scrollbar-w-2',
'scrollbar-track-transparent',
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
'will-change-scroll',
'touch-auto',
)}
>
<motion.div
key={activeTab || 'home'}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="p-6"
> >
{showTabManagement ? ( <motion.div
<TabManagement /> key={activeTab || 'home'}
) : activeTab ? ( initial={{ opacity: 0 }}
getTabComponent(activeTab) animate={{ opacity: 1 }}
) : ( exit={{ opacity: 0 }}
<motion.div transition={{ duration: 0.2 }}
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative" className="p-6"
variants={gridLayoutVariants} >
initial="hidden" {showTabManagement ? (
animate="visible" <TabManagement />
> ) : activeTab ? (
<AnimatePresence mode="popLayout"> getTabComponent(activeTab)
{(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => ( ) : (
<motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]"> <motion.div
<TabTile className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative"
tab={tab} variants={gridLayoutVariants}
onClick={() => handleTabClick(tab.id as TabType)} initial="hidden"
isActive={activeTab === tab.id} animate="visible"
hasUpdate={getTabUpdateStatus(tab.id)} >
statusMessage={getStatusMessage(tab.id)} <AnimatePresence mode="popLayout">
description={TAB_DESCRIPTIONS[tab.id]} {(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
isLoading={loadingTab === tab.id} <motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]">
className="h-full relative" <TabTile
> tab={tab}
{BETA_TABS.has(tab.id) && <BetaLabel />} onClick={() => handleTabClick(tab.id as TabType)}
</TabTile> isActive={activeTab === tab.id}
</motion.div> hasUpdate={getTabUpdateStatus(tab.id)}
))} statusMessage={getStatusMessage(tab.id)}
</AnimatePresence> description={TAB_DESCRIPTIONS[tab.id]}
</motion.div> isLoading={loadingTab === tab.id}
)} className="h-full relative"
</motion.div> >
{BETA_TABS.has(tab.id) && <BetaLabel />}
</TabTile>
</motion.div>
))}
</AnimatePresence>
</motion.div>
)}
</motion.div>
</div>
</div> </div>
</motion.div> </motion.div>
</RadixDialog.Content> </RadixDialog.Content>

View File

@ -44,6 +44,14 @@ const OPTIONAL_USER_TABS: TabType[] = ['profile', 'settings', 'task-manager', 's
// All available tabs for user mode // All available tabs for user mode
const ALL_USER_TABS = [...DEFAULT_USER_TABS, ...OPTIONAL_USER_TABS]; const ALL_USER_TABS = [...DEFAULT_USER_TABS, ...OPTIONAL_USER_TABS];
// Define which tabs are beta
const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']);
// Beta label component
const BetaLabel = () => (
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-purple-500/10 text-purple-500 font-medium">BETA</span>
);
export const TabManagement = () => { export const TabManagement = () => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const tabConfiguration = useStore(tabConfigurationStore); const tabConfiguration = useStore(tabConfigurationStore);
@ -217,9 +225,12 @@ export const TabManagement = () => {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div> <div>
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors"> <div className="flex items-center gap-2">
{TAB_LABELS[tab.id]} <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
</h4> {TAB_LABELS[tab.id]}
</h4>
{BETA_TABS.has(tab.id) && <BetaLabel />}
</div>
<p className="text-xs text-bolt-elements-textSecondary mt-0.5"> <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
{tab.visible ? 'Visible in user mode' : 'Hidden in user mode'} {tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
</p> </p>

View File

@ -9,6 +9,7 @@ import { ScrollArea } from '~/components/ui/ScrollArea';
import { Badge } from '~/components/ui/Badge'; import { Badge } from '~/components/ui/Badge';
import { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; import { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
import { jsPDF } from 'jspdf'; import { jsPDF } from 'jspdf';
import { useSettings } from '~/lib/hooks/useSettings';
interface SystemInfo { interface SystemInfo {
os: string; os: string;
@ -138,6 +139,11 @@ interface OllamaServiceStatus {
isRunning: boolean; isRunning: boolean;
lastChecked: Date; lastChecked: Date;
error?: string; error?: string;
models?: Array<{
name: string;
size: string;
quantization: string;
}>;
} }
interface ExportFormat { interface ExportFormat {
@ -232,6 +238,8 @@ export default function DebugTab() {
performance: false, performance: false,
}); });
const { isLocalModel, providers } = useSettings();
// Subscribe to logStore updates // Subscribe to logStore updates
const logs = useStore(logStore.logs); const logs = useStore(logStore.logs);
const errorLogs = useMemo(() => { const errorLogs = useMemo(() => {
@ -1093,41 +1101,48 @@ export default function DebugTab() {
]; ];
// Add Ollama health check function // Add Ollama health check function
const checkOllamaHealth = async () => { const checkOllamaStatus = useCallback(async () => {
try { try {
const response = await fetch('http://127.0.0.1:11434/api/version'); // First check if service is running
const isHealthy = response.ok; const versionResponse = await fetch('http://127.0.0.1:11434/api/version');
if (!versionResponse.ok) {
throw new Error('Service not running');
}
// Then fetch installed models
const modelsResponse = await fetch('http://127.0.0.1:11434/api/tags');
const modelsData = (await modelsResponse.json()) as {
models: Array<{ name: string; size: string; quantization: string }>;
};
setOllamaStatus({ setOllamaStatus({
isRunning: isHealthy, isRunning: true,
lastChecked: new Date(), lastChecked: new Date(),
error: isHealthy ? undefined : 'Ollama service is not responding', models: modelsData.models,
}); });
return isHealthy;
} catch { } catch {
setOllamaStatus({ setOllamaStatus({
isRunning: false, isRunning: false,
error: 'Connection failed',
lastChecked: new Date(), lastChecked: new Date(),
error: 'Failed to connect to Ollama service', models: undefined,
}); });
return false;
} }
};
// Add Ollama health check effect
useEffect(() => {
const checkHealth = async () => {
await checkOllamaHealth();
};
checkHealth();
const interval = setInterval(checkHealth, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, []); }, []);
// Monitor isLocalModel changes and check status periodically
useEffect(() => {
// Check immediately when isLocalModel changes
checkOllamaStatus();
// Set up periodic checks every 10 seconds
const intervalId = setInterval(checkOllamaStatus, 10000);
return () => clearInterval(intervalId);
}, [isLocalModel, checkOllamaStatus]);
// Replace the existing export button with this new component // Replace the existing export button with this new component
const ExportButton = () => { const ExportButton = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -1199,60 +1214,225 @@ export default function DebugTab() {
); );
}; };
// Add helper function to get Ollama status text and color
const getOllamaStatus = () => {
const ollamaProvider = providers?.Ollama;
const isOllamaEnabled = ollamaProvider?.settings?.enabled;
if (!isLocalModel) {
return {
status: 'Disabled',
color: 'text-red-500',
bgColor: 'bg-red-500',
message: 'Local models are disabled in settings',
};
}
if (!isOllamaEnabled) {
return {
status: 'Disabled',
color: 'text-red-500',
bgColor: 'bg-red-500',
message: 'Ollama provider is disabled in settings',
};
}
if (!ollamaStatus.isRunning) {
return {
status: 'Not Running',
color: 'text-red-500',
bgColor: 'bg-red-500',
message: ollamaStatus.error || 'Ollama service is not running',
};
}
const modelCount = ollamaStatus.models?.length ?? 0;
return {
status: 'Running',
color: 'text-green-500',
bgColor: 'bg-green-500',
message: `Ollama service is running with ${modelCount} installed models (Provider: Enabled)`,
};
};
// Add type for status result
type StatusResult = {
status: string;
color: string;
bgColor: string;
message: string;
};
const status = getOllamaStatus() as StatusResult;
return ( return (
<div className="flex flex-col gap-6 max-w-7xl mx-auto p-4"> <div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
{/* Quick Stats Banner */} {/* Quick Stats Banner */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* Add Ollama Service Status Card */} {/* Ollama Service Status Card */}
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"> <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
<div className="text-sm text-bolt-elements-textSecondary">Ollama Service</div> <div className="flex items-center gap-2">
<div className="i-ph:robot text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Ollama Service</div>
</div>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<div <div
className={classNames( className={classNames('w-2 h-2 rounded-full animate-pulse', status.bgColor, {
'w-2 h-2 rounded-full animate-pulse', 'shadow-lg shadow-green-500/20': status.status === 'Running',
ollamaStatus.isRunning ? 'bg-green-500' : 'bg-red-500', 'shadow-lg shadow-red-500/20': status.status === 'Not Running',
)} })}
/> />
<span <span className={classNames('text-sm font-medium flex items-center gap-1.5', status.color)}>
className={classNames('text-sm font-medium', ollamaStatus.isRunning ? 'text-green-500' : 'text-red-500')} {status.status === 'Running' && <div className="i-ph:check-circle-fill w-3.5 h-3.5" />}
> {status.status === 'Not Running' && <div className="i-ph:x-circle-fill w-3.5 h-3.5" />}
{ollamaStatus.isRunning ? 'Running' : 'Not Running'} {status.status === 'Disabled' && <div className="i-ph:prohibit-fill w-3.5 h-3.5" />}
{status.status}
</span> </span>
</div> </div>
<div className="text-xs text-bolt-elements-textSecondary mt-2"> <div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div
className={classNames('w-3.5 h-3.5', {
'i-ph:info text-green-500': status.status === 'Running',
'i-ph:warning text-red-500': status.status === 'Not Running' || status.status === 'Disabled',
})}
/>
{status.message}
</div>
{ollamaStatus.models && ollamaStatus.models.length > 0 && (
<div className="mt-3 space-y-1 border-t border-[#E5E5E5] dark:border-[#1A1A1A] pt-2">
<div className="text-xs font-medium text-bolt-elements-textSecondary flex items-center gap-1.5">
<div className="i-ph:cube-duotone w-3.5 h-3.5 text-purple-500" />
Installed Models
</div>
{ollamaStatus.models.map((model) => (
<div key={model.name} className="text-xs text-bolt-elements-textSecondary flex items-center gap-2 pl-5">
<div className="i-ph:cube w-3 h-3 text-purple-500/70" />
<span className="font-mono">{model.name}</span>
<span className="text-bolt-elements-textTertiary">
({Math.round(parseInt(model.size) / 1024 / 1024)}MB, {model.quantization})
</span>
</div>
))}
</div>
)}
<div className="text-xs text-bolt-elements-textTertiary mt-3 flex items-center gap-1.5">
<div className="i-ph:clock w-3 h-3" />
Last checked: {ollamaStatus.lastChecked.toLocaleTimeString()} Last checked: {ollamaStatus.lastChecked.toLocaleTimeString()}
</div> </div>
</div> </div>
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"> {/* Memory Usage Card */}
<div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div> <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1"> <div className="flex items-center gap-2">
{systemInfo?.memory.percentage}% <div className="i-ph:cpu text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
</div> </div>
<Progress value={systemInfo?.memory.percentage || 0} className="mt-2" /> <div className="flex items-center gap-2 mt-2">
</div> <span
className={classNames(
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"> 'text-2xl font-semibold',
<div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div> (systemInfo?.memory?.percentage ?? 0) > 80
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1"> ? 'text-red-500'
{systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) + 's' : '-'} : (systemInfo?.memory?.percentage ?? 0) > 60
? 'text-yellow-500'
: 'text-green-500',
)}
>
{systemInfo?.memory?.percentage ?? 0}%
</span>
</div> </div>
<div className="text-xs text-bolt-elements-textSecondary mt-2"> <Progress
DOM Ready: {systemInfo ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2) + 's' : '-'} value={systemInfo?.memory?.percentage ?? 0}
className={classNames(
'mt-2',
(systemInfo?.memory?.percentage ?? 0) > 80
? '[&>div]:bg-red-500'
: (systemInfo?.memory?.percentage ?? 0) > 60
? '[&>div]:bg-yellow-500'
: '[&>div]:bg-green-500',
)}
/>
<div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div className="i-ph:info w-3.5 h-3.5 text-purple-500" />
Used: {systemInfo?.memory.used ?? '0 GB'} / {systemInfo?.memory.total ?? '0 GB'}
</div> </div>
</div> </div>
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"> {/* Page Load Time Card */}
<div className="text-sm text-bolt-elements-textSecondary">Network Speed</div> <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1"> <div className="flex items-center gap-2">
{systemInfo?.network.downlink || '-'} Mbps <div className="i-ph:timer text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
</div>
<div className="flex items-center gap-2 mt-2">
<span
className={classNames(
'text-2xl font-semibold',
(systemInfo?.performance.timing.loadTime ?? 0) > 2000
? 'text-red-500'
: (systemInfo?.performance.timing.loadTime ?? 0) > 1000
? 'text-yellow-500'
: 'text-green-500',
)}
>
{systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) : '-'}s
</span>
</div>
<div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div className="i-ph:code w-3.5 h-3.5 text-purple-500" />
DOM Ready: {systemInfo ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2) : '-'}s
</div> </div>
<div className="text-xs text-bolt-elements-textSecondary mt-2">RTT: {systemInfo?.network.rtt || '-'} ms</div>
</div> </div>
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"> {/* Network Speed Card */}
<div className="text-sm text-bolt-elements-textSecondary">Errors</div> <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLogs.length}</div> <div className="flex items-center gap-2">
<div className="i-ph:wifi-high text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
</div>
<div className="flex items-center gap-2 mt-2">
<span
className={classNames(
'text-2xl font-semibold',
(systemInfo?.network.downlink ?? 0) < 5
? 'text-red-500'
: (systemInfo?.network.downlink ?? 0) < 10
? 'text-yellow-500'
: 'text-green-500',
)}
>
{systemInfo?.network.downlink ?? '-'} Mbps
</span>
</div>
<div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div className="i-ph:activity w-3.5 h-3.5 text-purple-500" />
RTT: {systemInfo?.network.rtt ?? '-'} ms
</div>
</div>
{/* Errors Card */}
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200">
<div className="flex items-center gap-2">
<div className="i-ph:warning-octagon text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Errors</div>
</div>
<div className="flex items-center gap-2 mt-2">
<span
className={classNames('text-2xl font-semibold', errorLogs.length > 0 ? 'text-red-500' : 'text-green-500')}
>
{errorLogs.length}
</span>
</div>
<div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div
className={classNames(
'w-3.5 h-3.5',
errorLogs.length > 0 ? 'i-ph:warning text-red-500' : 'i-ph:check-circle text-green-500',
)}
/>
{errorLogs.length > 0 ? 'Errors detected' : 'No errors detected'}
</div>
</div> </div>
</div> </div>

View File

@ -87,19 +87,10 @@ export default function LocalProvidersTab() {
.map(([key, value]) => { .map(([key, value]) => {
const provider = value as IProviderConfig; const provider = value as IProviderConfig;
const envKey = providerBaseUrlEnvKeys[key]?.baseUrlKey; const envKey = providerBaseUrlEnvKeys[key]?.baseUrlKey;
// Get environment URL safely
const envUrl = envKey ? (import.meta.env[envKey] as string | undefined) : undefined; const envUrl = envKey ? (import.meta.env[envKey] as string | undefined) : undefined;
console.log(`Checking env URL for ${key}:`, { // Set base URL if provided by environment
envKey,
envUrl,
currentBaseUrl: provider.settings.baseUrl,
});
// If there's an environment URL and no base URL set, update it
if (envUrl && !provider.settings.baseUrl) { if (envUrl && !provider.settings.baseUrl) {
console.log(`Setting base URL for ${key} from env:`, envUrl);
updateProviderSettings(key, { updateProviderSettings(key, {
...provider.settings, ...provider.settings,
baseUrl: envUrl, baseUrl: envUrl,
@ -414,7 +405,9 @@ export default function LocalProvidersTab() {
<BiChip className="w-6 h-6" /> <BiChip className="w-6 h-6" />
</motion.div> </motion.div>
<div> <div>
<h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Local AI Models</h2> <div className="flex items-center gap-2">
<h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Local AI Models</h2>
</div>
<p className="text-sm text-bolt-elements-textSecondary">Configure and manage your local AI providers</p> <p className="text-sm text-bolt-elements-textSecondary">Configure and manage your local AI providers</p>
</div> </div>
</div> </div>

View File

@ -8,6 +8,8 @@ import { db, chatId } from '~/lib/persistence/useChatHistory';
import { forkChat } from '~/lib/persistence/db'; import { forkChat } from '~/lib/persistence/db';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import WithTooltip from '~/components/ui/Tooltip'; import WithTooltip from '~/components/ui/Tooltip';
import { useStore } from '@nanostores/react';
import { profileStore } from '~/lib/stores/profile';
interface MessagesProps { interface MessagesProps {
id?: string; id?: string;
@ -24,6 +26,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
const [isUserInteracting, setIsUserInteracting] = useState(false); const [isUserInteracting, setIsUserInteracting] = useState(false);
const [lastScrollTop, setLastScrollTop] = useState(0); const [lastScrollTop, setLastScrollTop] = useState(0);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
const profile = useStore(profileStore);
// Check if we should auto-scroll based on scroll position // Check if we should auto-scroll based on scroll position
const checkShouldAutoScroll = () => { const checkShouldAutoScroll = () => {
@ -166,8 +169,18 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
})} })}
> >
{isUserMessage && ( {isUserMessage && (
<div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start"> <div className="flex items-center justify-center w-[40px] h-[40px] overflow-hidden bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-500 rounded-full shrink-0 self-start">
<div className="i-ph:user-fill text-xl"></div> {profile?.avatar ? (
<img
src={profile.avatar}
alt={profile?.username || 'User'}
className="w-full h-full object-cover"
loading="eager"
decoding="sync"
/>
) : (
<div className="i-ph:user-fill text-2xl" />
)}
</div> </div>
)} )}
<div className="grid grid-col-1 w-full"> <div className="grid grid-col-1 w-full">

View File

@ -12,6 +12,8 @@ import { HistoryItem } from './HistoryItem';
import { binDates } from './date-binning'; import { binDates } from './date-binning';
import { useSearchFilter } from '~/lib/hooks/useSearchFilter'; import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
import { classNames } from '~/utils/classNames'; import { classNames } from '~/utils/classNames';
import { useStore } from '@nanostores/react';
import { profileStore } from '~/lib/stores/profile';
const menuVariants = { const menuVariants = {
closed: { closed: {
@ -65,6 +67,7 @@ export const Menu = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [dialogContent, setDialogContent] = useState<DialogContent>(null); const [dialogContent, setDialogContent] = useState<DialogContent>(null);
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const profile = useStore(profileStore);
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({ const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
items: list, items: list,
@ -169,7 +172,27 @@ export const Menu = () => {
isSettingsOpen ? 'z-40' : 'z-sidebar', isSettingsOpen ? 'z-40' : 'z-sidebar',
)} )}
> >
<div className="h-12 flex items-center px-4 border-b border-gray-100 dark:border-gray-800/50 bg-gray-50/50 dark:bg-gray-900/50"></div> <div className="h-12 flex items-center justify-between px-4 border-b border-gray-100 dark:border-gray-800/50 bg-gray-50/50 dark:bg-gray-900/50">
<div className="text-gray-900 dark:text-white font-medium"></div>
<div className="flex items-center gap-3">
<span className="font-medium text-sm text-gray-900 dark:text-white truncate">
{profile?.username || 'Guest User'}
</span>
<div className="flex items-center justify-center w-[32px] h-[32px] overflow-hidden bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-500 rounded-full shrink-0">
{profile?.avatar ? (
<img
src={profile.avatar}
alt={profile?.username || 'User'}
className="w-full h-full object-cover"
loading="eager"
decoding="sync"
/>
) : (
<div className="i-ph:user-fill text-lg" />
)}
</div>
</div>
</div>
<CurrentDateTime /> <CurrentDateTime />
<div className="flex-1 flex flex-col h-full w-full overflow-hidden"> <div className="flex-1 flex flex-col h-full w-full overflow-hidden">
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">

View File

@ -90,7 +90,8 @@ const getInitialProviderSettings = (): ProviderSetting => {
initialSettings[provider.name] = { initialSettings[provider.name] = {
...provider, ...provider,
settings: { settings: {
enabled: true, // Local providers should be disabled by default
enabled: !LOCAL_PROVIDERS.includes(provider.name),
}, },
}; };
}); });