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 { DialogTitle } from '~/components/ui/Dialog';
import { AvatarDropdown } from './AvatarDropdown';
import BackgroundRays from '~/components/ui/BackgroundRays';
// Import all tab components
import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab';
@ -83,7 +84,7 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
};
// 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 = () => (
<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,12 +416,17 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
'rounded-2xl shadow-2xl',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'flex flex-col overflow-hidden',
'relative',
)}
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.2 }}
>
<div className="absolute inset-0 overflow-hidden rounded-2xl">
<BackgroundRays />
</div>
<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">
@ -518,6 +524,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
)}
</motion.div>
</div>
</div>
</motion.div>
</RadixDialog.Content>
</div>

View File

@ -44,6 +44,14 @@ const OPTIONAL_USER_TABS: TabType[] = ['profile', 'settings', 'task-manager', 's
// All available tabs for user mode
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 = () => {
const [searchQuery, setSearchQuery] = useState('');
const tabConfiguration = useStore(tabConfigurationStore);
@ -217,9 +225,12 @@ export const TabManagement = () => {
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
{TAB_LABELS[tab.id]}
</h4>
{BETA_TABS.has(tab.id) && <BetaLabel />}
</div>
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
{tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
</p>

View File

@ -9,6 +9,7 @@ import { ScrollArea } from '~/components/ui/ScrollArea';
import { Badge } from '~/components/ui/Badge';
import { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
import { jsPDF } from 'jspdf';
import { useSettings } from '~/lib/hooks/useSettings';
interface SystemInfo {
os: string;
@ -138,6 +139,11 @@ interface OllamaServiceStatus {
isRunning: boolean;
lastChecked: Date;
error?: string;
models?: Array<{
name: string;
size: string;
quantization: string;
}>;
}
interface ExportFormat {
@ -232,6 +238,8 @@ export default function DebugTab() {
performance: false,
});
const { isLocalModel, providers } = useSettings();
// Subscribe to logStore updates
const logs = useStore(logStore.logs);
const errorLogs = useMemo(() => {
@ -1093,41 +1101,48 @@ export default function DebugTab() {
];
// Add Ollama health check function
const checkOllamaHealth = async () => {
const checkOllamaStatus = useCallback(async () => {
try {
const response = await fetch('http://127.0.0.1:11434/api/version');
const isHealthy = response.ok;
// First check if service is running
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({
isRunning: isHealthy,
isRunning: true,
lastChecked: new Date(),
error: isHealthy ? undefined : 'Ollama service is not responding',
models: modelsData.models,
});
return isHealthy;
} catch {
setOllamaStatus({
isRunning: false,
error: 'Connection failed',
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
const ExportButton = () => {
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 (
<div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
{/* Quick Stats Banner */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* Add Ollama Service Status Card */}
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
{/* Ollama Service Status 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: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={classNames(
'w-2 h-2 rounded-full animate-pulse',
ollamaStatus.isRunning ? 'bg-green-500' : 'bg-red-500',
)}
className={classNames('w-2 h-2 rounded-full animate-pulse', status.bgColor, {
'shadow-lg shadow-green-500/20': status.status === 'Running',
'shadow-lg shadow-red-500/20': status.status === 'Not Running',
})}
/>
<span
className={classNames('text-sm font-medium', ollamaStatus.isRunning ? 'text-green-500' : 'text-red-500')}
>
{ollamaStatus.isRunning ? 'Running' : 'Not Running'}
<span className={classNames('text-sm font-medium flex items-center gap-1.5', status.color)}>
{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" />}
{status.status === 'Disabled' && <div className="i-ph:prohibit-fill w-3.5 h-3.5" />}
{status.status}
</span>
</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()}
</div>
</div>
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
{/* Memory Usage 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:cpu text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
{systemInfo?.memory.percentage}%
</div>
<Progress value={systemInfo?.memory.percentage || 0} className="mt-2" />
<div className="flex items-center gap-2 mt-2">
<span
className={classNames(
'text-2xl font-semibold',
(systemInfo?.memory?.percentage ?? 0) > 80
? 'text-red-500'
: (systemInfo?.memory?.percentage ?? 0) > 60
? 'text-yellow-500'
: 'text-green-500',
)}
>
{systemInfo?.memory?.percentage ?? 0}%
</span>
</div>
<Progress
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 className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
{/* Page Load Time 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:timer text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
{systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) + 's' : '-'}
</div>
<div className="text-xs text-bolt-elements-textSecondary mt-2">
DOM Ready: {systemInfo ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2) + 's' : '-'}
<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="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
{/* Network Speed 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:wifi-high text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
{systemInfo?.network.downlink || '-'} Mbps
</div>
<div className="text-xs text-bolt-elements-textSecondary mt-2">RTT: {systemInfo?.network.rtt || '-'} ms</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>
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
{/* 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 className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLogs.length}</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>

View File

@ -87,19 +87,10 @@ export default function LocalProvidersTab() {
.map(([key, value]) => {
const provider = value as IProviderConfig;
const envKey = providerBaseUrlEnvKeys[key]?.baseUrlKey;
// Get environment URL safely
const envUrl = envKey ? (import.meta.env[envKey] as string | undefined) : undefined;
console.log(`Checking env URL for ${key}:`, {
envKey,
envUrl,
currentBaseUrl: provider.settings.baseUrl,
});
// If there's an environment URL and no base URL set, update it
// Set base URL if provided by environment
if (envUrl && !provider.settings.baseUrl) {
console.log(`Setting base URL for ${key} from env:`, envUrl);
updateProviderSettings(key, {
...provider.settings,
baseUrl: envUrl,
@ -414,7 +405,9 @@ export default function LocalProvidersTab() {
<BiChip className="w-6 h-6" />
</motion.div>
<div>
<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>
</div>
</div>

View File

@ -8,6 +8,8 @@ import { db, chatId } from '~/lib/persistence/useChatHistory';
import { forkChat } from '~/lib/persistence/db';
import { toast } from 'react-toastify';
import WithTooltip from '~/components/ui/Tooltip';
import { useStore } from '@nanostores/react';
import { profileStore } from '~/lib/stores/profile';
interface MessagesProps {
id?: string;
@ -24,6 +26,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
const [isUserInteracting, setIsUserInteracting] = useState(false);
const [lastScrollTop, setLastScrollTop] = useState(0);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
const profile = useStore(profileStore);
// Check if we should auto-scroll based on scroll position
const checkShouldAutoScroll = () => {
@ -166,8 +169,18 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
})}
>
{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="i-ph:user-fill text-xl"></div>
<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">
{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 className="grid grid-col-1 w-full">

View File

@ -12,6 +12,8 @@ import { HistoryItem } from './HistoryItem';
import { binDates } from './date-binning';
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
import { classNames } from '~/utils/classNames';
import { useStore } from '@nanostores/react';
import { profileStore } from '~/lib/stores/profile';
const menuVariants = {
closed: {
@ -65,6 +67,7 @@ export const Menu = () => {
const [open, setOpen] = useState(false);
const [dialogContent, setDialogContent] = useState<DialogContent>(null);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const profile = useStore(profileStore);
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
items: list,
@ -169,7 +172,27 @@ export const Menu = () => {
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 />
<div className="flex-1 flex flex-col h-full w-full overflow-hidden">
<div className="p-4 space-y-3">

View File

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