mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-03-09 21:50:36 +00:00
Avatar Fix , control pannel UI fix
This commit is contained in:
parent
f3468d495d
commit
f091409f7e
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user