Stijnus
commited on
Commit
·
0e60d9c
1
Parent(s):
6e89710
UI bug fixes
Browse files- app/components/@settings/core/ControlPanel.tsx +24 -3
- app/components/@settings/shared/components/TabManagement.tsx +10 -1
- app/components/@settings/tabs/debug/DebugTab.tsx +6 -3
- app/components/@settings/tabs/profile/ProfileTab.tsx +14 -7
- app/components/@settings/tabs/providers/local/OllamaModelInstaller.tsx +9 -4
- app/components/@settings/tabs/settings/SettingsTab.tsx +20 -58
- app/components/chat/BaseChat.tsx +2 -1
- app/lib/stores/settings.ts +39 -26
- app/utils/debounce.ts +9 -13
app/components/@settings/core/ControlPanel.tsx
CHANGED
|
@@ -263,6 +263,27 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 263 |
},
|
| 264 |
};
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
// Handlers
|
| 267 |
const handleBack = () => {
|
| 268 |
if (showTabManagement) {
|
|
@@ -405,8 +426,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 405 |
|
| 406 |
<RadixDialog.Content
|
| 407 |
aria-describedby={undefined}
|
| 408 |
-
onEscapeKeyDown={
|
| 409 |
-
onPointerDownOutside={
|
| 410 |
className="relative z-[101]"
|
| 411 |
>
|
| 412 |
<motion.div
|
|
@@ -461,7 +482,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
|
| 461 |
|
| 462 |
{/* Close Button */}
|
| 463 |
<button
|
| 464 |
-
onClick={
|
| 465 |
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"
|
| 466 |
>
|
| 467 |
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
|
|
|
| 263 |
},
|
| 264 |
};
|
| 265 |
|
| 266 |
+
// Reset to default view when modal opens/closes
|
| 267 |
+
useEffect(() => {
|
| 268 |
+
if (!open) {
|
| 269 |
+
// Reset when closing
|
| 270 |
+
setActiveTab(null);
|
| 271 |
+
setLoadingTab(null);
|
| 272 |
+
setShowTabManagement(false);
|
| 273 |
+
} else {
|
| 274 |
+
// When opening, set to null to show the main view
|
| 275 |
+
setActiveTab(null);
|
| 276 |
+
}
|
| 277 |
+
}, [open]);
|
| 278 |
+
|
| 279 |
+
// Handle closing
|
| 280 |
+
const handleClose = () => {
|
| 281 |
+
setActiveTab(null);
|
| 282 |
+
setLoadingTab(null);
|
| 283 |
+
setShowTabManagement(false);
|
| 284 |
+
onClose();
|
| 285 |
+
};
|
| 286 |
+
|
| 287 |
// Handlers
|
| 288 |
const handleBack = () => {
|
| 289 |
if (showTabManagement) {
|
|
|
|
| 426 |
|
| 427 |
<RadixDialog.Content
|
| 428 |
aria-describedby={undefined}
|
| 429 |
+
onEscapeKeyDown={handleClose}
|
| 430 |
+
onPointerDownOutside={handleClose}
|
| 431 |
className="relative z-[101]"
|
| 432 |
>
|
| 433 |
<motion.div
|
|
|
|
| 482 |
|
| 483 |
{/* Close Button */}
|
| 484 |
<button
|
| 485 |
+
onClick={handleClose}
|
| 486 |
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"
|
| 487 |
>
|
| 488 |
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
app/components/@settings/shared/components/TabManagement.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { useState } from 'react';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
import { useStore } from '@nanostores/react';
|
| 4 |
import { Switch } from '~/components/ui/Switch';
|
|
@@ -8,6 +8,7 @@ import { TAB_LABELS } from '~/components/@settings/core/constants';
|
|
| 8 |
import type { TabType } from '~/components/@settings/core/types';
|
| 9 |
import { toast } from 'react-toastify';
|
| 10 |
import { TbLayoutGrid } from 'react-icons/tb';
|
|
|
|
| 11 |
|
| 12 |
// Define tab icons mapping
|
| 13 |
const TAB_ICONS: Record<TabType, string> = {
|
|
@@ -55,6 +56,7 @@ const BetaLabel = () => (
|
|
| 55 |
export const TabManagement = () => {
|
| 56 |
const [searchQuery, setSearchQuery] = useState('');
|
| 57 |
const tabConfiguration = useStore(tabConfigurationStore);
|
|
|
|
| 58 |
|
| 59 |
const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => {
|
| 60 |
// Get current tab configuration
|
|
@@ -126,6 +128,13 @@ export const TabManagement = () => {
|
|
| 126 |
// Filter tabs based on search query
|
| 127 |
const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()));
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
return (
|
| 130 |
<div className="space-y-6">
|
| 131 |
<motion.div
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
import { useStore } from '@nanostores/react';
|
| 4 |
import { Switch } from '~/components/ui/Switch';
|
|
|
|
| 8 |
import type { TabType } from '~/components/@settings/core/types';
|
| 9 |
import { toast } from 'react-toastify';
|
| 10 |
import { TbLayoutGrid } from 'react-icons/tb';
|
| 11 |
+
import { useSettingsStore } from '~/lib/stores/settings';
|
| 12 |
|
| 13 |
// Define tab icons mapping
|
| 14 |
const TAB_ICONS: Record<TabType, string> = {
|
|
|
|
| 56 |
export const TabManagement = () => {
|
| 57 |
const [searchQuery, setSearchQuery] = useState('');
|
| 58 |
const tabConfiguration = useStore(tabConfigurationStore);
|
| 59 |
+
const { setSelectedTab } = useSettingsStore();
|
| 60 |
|
| 61 |
const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => {
|
| 62 |
// Get current tab configuration
|
|
|
|
| 128 |
// Filter tabs based on search query
|
| 129 |
const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()));
|
| 130 |
|
| 131 |
+
useEffect(() => {
|
| 132 |
+
// Reset to first tab when component unmounts
|
| 133 |
+
return () => {
|
| 134 |
+
setSelectedTab('user'); // Reset to user tab when unmounting
|
| 135 |
+
};
|
| 136 |
+
}, [setSelectedTab]);
|
| 137 |
+
|
| 138 |
return (
|
| 139 |
<div className="space-y-6">
|
| 140 |
<motion.div
|
app/components/@settings/tabs/debug/DebugTab.tsx
CHANGED
|
@@ -1103,15 +1103,18 @@ export default function DebugTab() {
|
|
| 1103 |
// Add Ollama health check function
|
| 1104 |
const checkOllamaStatus = useCallback(async () => {
|
| 1105 |
try {
|
|
|
|
|
|
|
|
|
|
| 1106 |
// First check if service is running
|
| 1107 |
-
const versionResponse = await fetch(
|
| 1108 |
|
| 1109 |
if (!versionResponse.ok) {
|
| 1110 |
throw new Error('Service not running');
|
| 1111 |
}
|
| 1112 |
|
| 1113 |
// Then fetch installed models
|
| 1114 |
-
const modelsResponse = await fetch(
|
| 1115 |
|
| 1116 |
const modelsData = (await modelsResponse.json()) as {
|
| 1117 |
models: Array<{ name: string; size: string; quantization: string }>;
|
|
@@ -1130,7 +1133,7 @@ export default function DebugTab() {
|
|
| 1130 |
models: undefined,
|
| 1131 |
});
|
| 1132 |
}
|
| 1133 |
-
}, []);
|
| 1134 |
|
| 1135 |
// Monitor isLocalModel changes and check status periodically
|
| 1136 |
useEffect(() => {
|
|
|
|
| 1103 |
// Add Ollama health check function
|
| 1104 |
const checkOllamaStatus = useCallback(async () => {
|
| 1105 |
try {
|
| 1106 |
+
const ollamaProvider = providers?.Ollama;
|
| 1107 |
+
const baseUrl = ollamaProvider?.settings?.baseUrl || 'http://127.0.0.1:11434';
|
| 1108 |
+
|
| 1109 |
// First check if service is running
|
| 1110 |
+
const versionResponse = await fetch(`${baseUrl}/api/version`);
|
| 1111 |
|
| 1112 |
if (!versionResponse.ok) {
|
| 1113 |
throw new Error('Service not running');
|
| 1114 |
}
|
| 1115 |
|
| 1116 |
// Then fetch installed models
|
| 1117 |
+
const modelsResponse = await fetch(`${baseUrl}/api/tags`);
|
| 1118 |
|
| 1119 |
const modelsData = (await modelsResponse.json()) as {
|
| 1120 |
models: Array<{ name: string; size: string; quantization: string }>;
|
|
|
|
| 1133 |
models: undefined,
|
| 1134 |
});
|
| 1135 |
}
|
| 1136 |
+
}, [providers]);
|
| 1137 |
|
| 1138 |
// Monitor isLocalModel changes and check status periodically
|
| 1139 |
useEffect(() => {
|
app/components/@settings/tabs/profile/ProfileTab.tsx
CHANGED
|
@@ -1,13 +1,23 @@
|
|
| 1 |
-
import { useState } from 'react';
|
| 2 |
import { useStore } from '@nanostores/react';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
import { profileStore, updateProfile } from '~/lib/stores/profile';
|
| 5 |
import { toast } from 'react-toastify';
|
|
|
|
| 6 |
|
| 7 |
export default function ProfileTab() {
|
| 8 |
const profile = useStore(profileStore);
|
| 9 |
const [isUploading, setIsUploading] = useState(false);
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 12 |
const file = e.target.files?.[0];
|
| 13 |
|
|
@@ -42,14 +52,11 @@ export default function ProfileTab() {
|
|
| 42 |
};
|
| 43 |
|
| 44 |
const handleProfileUpdate = (field: 'username' | 'bio', value: string) => {
|
|
|
|
| 45 |
updateProfile({ [field]: value });
|
| 46 |
|
| 47 |
-
//
|
| 48 |
-
|
| 49 |
-
toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`);
|
| 50 |
-
}, 1000);
|
| 51 |
-
|
| 52 |
-
return () => clearTimeout(debounceToast);
|
| 53 |
};
|
| 54 |
|
| 55 |
return (
|
|
|
|
| 1 |
+
import { useState, useCallback } from 'react';
|
| 2 |
import { useStore } from '@nanostores/react';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
import { profileStore, updateProfile } from '~/lib/stores/profile';
|
| 5 |
import { toast } from 'react-toastify';
|
| 6 |
+
import { debounce } from '~/utils/debounce';
|
| 7 |
|
| 8 |
export default function ProfileTab() {
|
| 9 |
const profile = useStore(profileStore);
|
| 10 |
const [isUploading, setIsUploading] = useState(false);
|
| 11 |
|
| 12 |
+
// Create debounced update functions
|
| 13 |
+
const debouncedUpdate = useCallback(
|
| 14 |
+
debounce((field: 'username' | 'bio', value: string) => {
|
| 15 |
+
updateProfile({ [field]: value });
|
| 16 |
+
toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`);
|
| 17 |
+
}, 1000),
|
| 18 |
+
[],
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 22 |
const file = e.target.files?.[0];
|
| 23 |
|
|
|
|
| 52 |
};
|
| 53 |
|
| 54 |
const handleProfileUpdate = (field: 'username' | 'bio', value: string) => {
|
| 55 |
+
// Update the store immediately for UI responsiveness
|
| 56 |
updateProfile({ [field]: value });
|
| 57 |
|
| 58 |
+
// Debounce the toast notification
|
| 59 |
+
debouncedUpdate(field, value);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
};
|
| 61 |
|
| 62 |
return (
|
app/components/@settings/tabs/providers/local/OllamaModelInstaller.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion';
|
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
import { Progress } from '~/components/ui/Progress';
|
| 5 |
import { useToast } from '~/components/ui/use-toast';
|
|
|
|
| 6 |
|
| 7 |
interface OllamaModelInstallerProps {
|
| 8 |
onModelInstalled: () => void;
|
|
@@ -141,11 +142,15 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
|
|
| 141 |
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
| 142 |
const [models, setModels] = useState<ModelInfo[]>(POPULAR_MODELS);
|
| 143 |
const { toast } = useToast();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
// Function to check installed models and their versions
|
| 146 |
const checkInstalledModels = async () => {
|
| 147 |
try {
|
| 148 |
-
const response = await fetch(
|
| 149 |
method: 'GET',
|
| 150 |
});
|
| 151 |
|
|
@@ -181,7 +186,7 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
|
|
| 181 |
// Check installed models on mount and after installation
|
| 182 |
useEffect(() => {
|
| 183 |
checkInstalledModels();
|
| 184 |
-
}, []);
|
| 185 |
|
| 186 |
const handleCheckUpdates = async () => {
|
| 187 |
setIsChecking(true);
|
|
@@ -224,7 +229,7 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
|
|
| 224 |
setModelString('');
|
| 225 |
setSearchQuery('');
|
| 226 |
|
| 227 |
-
const response = await fetch(
|
| 228 |
method: 'POST',
|
| 229 |
headers: {
|
| 230 |
'Content-Type': 'application/json',
|
|
@@ -302,7 +307,7 @@ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelIn
|
|
| 302 |
try {
|
| 303 |
setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'updating' } : m)));
|
| 304 |
|
| 305 |
-
const response = await fetch(
|
| 306 |
method: 'POST',
|
| 307 |
headers: {
|
| 308 |
'Content-Type': 'application/json',
|
|
|
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
import { Progress } from '~/components/ui/Progress';
|
| 5 |
import { useToast } from '~/components/ui/use-toast';
|
| 6 |
+
import { useSettings } from '~/lib/hooks/useSettings';
|
| 7 |
|
| 8 |
interface OllamaModelInstallerProps {
|
| 9 |
onModelInstalled: () => void;
|
|
|
|
| 142 |
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
| 143 |
const [models, setModels] = useState<ModelInfo[]>(POPULAR_MODELS);
|
| 144 |
const { toast } = useToast();
|
| 145 |
+
const { providers } = useSettings();
|
| 146 |
+
|
| 147 |
+
// Get base URL from provider settings
|
| 148 |
+
const baseUrl = providers?.Ollama?.settings?.baseUrl || 'http://127.0.0.1:11434';
|
| 149 |
|
| 150 |
// Function to check installed models and their versions
|
| 151 |
const checkInstalledModels = async () => {
|
| 152 |
try {
|
| 153 |
+
const response = await fetch(`${baseUrl}/api/tags`, {
|
| 154 |
method: 'GET',
|
| 155 |
});
|
| 156 |
|
|
|
|
| 186 |
// Check installed models on mount and after installation
|
| 187 |
useEffect(() => {
|
| 188 |
checkInstalledModels();
|
| 189 |
+
}, [baseUrl]);
|
| 190 |
|
| 191 |
const handleCheckUpdates = async () => {
|
| 192 |
setIsChecking(true);
|
|
|
|
| 229 |
setModelString('');
|
| 230 |
setSearchQuery('');
|
| 231 |
|
| 232 |
+
const response = await fetch(`${baseUrl}/api/pull`, {
|
| 233 |
method: 'POST',
|
| 234 |
headers: {
|
| 235 |
'Content-Type': 'application/json',
|
|
|
|
| 307 |
try {
|
| 308 |
setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'updating' } : m)));
|
| 309 |
|
| 310 |
+
const response = await fetch(`${baseUrl}/api/pull`, {
|
| 311 |
method: 'POST',
|
| 312 |
headers: {
|
| 313 |
'Content-Type': 'application/json',
|
app/components/@settings/tabs/settings/SettingsTab.tsx
CHANGED
|
@@ -4,19 +4,8 @@ import { toast } from 'react-toastify';
|
|
| 4 |
import { classNames } from '~/utils/classNames';
|
| 5 |
import { Switch } from '~/components/ui/Switch';
|
| 6 |
import type { UserProfile } from '~/components/@settings/core/types';
|
| 7 |
-
import { useStore } from '@nanostores/react';
|
| 8 |
-
import { shortcutsStore } from '~/lib/stores/settings';
|
| 9 |
import { isMac } from '~/utils/os';
|
| 10 |
|
| 11 |
-
// Helper to format shortcut key display
|
| 12 |
-
const formatShortcutKey = (key: string) => {
|
| 13 |
-
if (key === '`') {
|
| 14 |
-
return '`';
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
return key.toUpperCase();
|
| 18 |
-
};
|
| 19 |
-
|
| 20 |
// Helper to get modifier key symbols/text
|
| 21 |
const getModifierSymbol = (modifier: string): string => {
|
| 22 |
switch (modifier) {
|
|
@@ -24,8 +13,6 @@ const getModifierSymbol = (modifier: string): string => {
|
|
| 24 |
return isMac ? '⌘' : 'Win';
|
| 25 |
case 'alt':
|
| 26 |
return isMac ? '⌥' : 'Alt';
|
| 27 |
-
case 'ctrl':
|
| 28 |
-
return isMac ? '⌃' : 'Ctrl';
|
| 29 |
case 'shift':
|
| 30 |
return '⇧';
|
| 31 |
default:
|
|
@@ -188,7 +175,7 @@ export default function SettingsTab() {
|
|
| 188 |
</div>
|
| 189 |
</motion.div>
|
| 190 |
|
| 191 |
-
{/* Keyboard Shortcuts */}
|
| 192 |
<motion.div
|
| 193 |
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
|
| 194 |
initial={{ opacity: 0, y: 20 }}
|
|
@@ -201,51 +188,26 @@ export default function SettingsTab() {
|
|
| 201 |
</div>
|
| 202 |
|
| 203 |
<div className="space-y-2">
|
| 204 |
-
|
| 205 |
-
<div
|
| 206 |
-
|
| 207 |
-
className="
|
| 208 |
-
>
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
)}
|
| 223 |
-
{shortcut.ctrlKey && (
|
| 224 |
-
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
| 225 |
-
{getModifierSymbol('ctrl')}
|
| 226 |
-
</kbd>
|
| 227 |
-
)}
|
| 228 |
-
{shortcut.metaKey && (
|
| 229 |
-
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
| 230 |
-
{getModifierSymbol('meta')}
|
| 231 |
-
</kbd>
|
| 232 |
-
)}
|
| 233 |
-
{shortcut.altKey && (
|
| 234 |
-
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
| 235 |
-
{getModifierSymbol('alt')}
|
| 236 |
-
</kbd>
|
| 237 |
-
)}
|
| 238 |
-
{shortcut.shiftKey && (
|
| 239 |
-
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
| 240 |
-
{getModifierSymbol('shift')}
|
| 241 |
-
</kbd>
|
| 242 |
-
)}
|
| 243 |
-
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
| 244 |
-
{formatShortcutKey(shortcut.key)}
|
| 245 |
-
</kbd>
|
| 246 |
-
</div>
|
| 247 |
</div>
|
| 248 |
-
|
| 249 |
</div>
|
| 250 |
</motion.div>
|
| 251 |
</div>
|
|
|
|
| 4 |
import { classNames } from '~/utils/classNames';
|
| 5 |
import { Switch } from '~/components/ui/Switch';
|
| 6 |
import type { UserProfile } from '~/components/@settings/core/types';
|
|
|
|
|
|
|
| 7 |
import { isMac } from '~/utils/os';
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
// Helper to get modifier key symbols/text
|
| 10 |
const getModifierSymbol = (modifier: string): string => {
|
| 11 |
switch (modifier) {
|
|
|
|
| 13 |
return isMac ? '⌘' : 'Win';
|
| 14 |
case 'alt':
|
| 15 |
return isMac ? '⌥' : 'Alt';
|
|
|
|
|
|
|
| 16 |
case 'shift':
|
| 17 |
return '⇧';
|
| 18 |
default:
|
|
|
|
| 175 |
</div>
|
| 176 |
</motion.div>
|
| 177 |
|
| 178 |
+
{/* Simplified Keyboard Shortcuts */}
|
| 179 |
<motion.div
|
| 180 |
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
|
| 181 |
initial={{ opacity: 0, y: 20 }}
|
|
|
|
| 188 |
</div>
|
| 189 |
|
| 190 |
<div className="space-y-2">
|
| 191 |
+
<div className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A]">
|
| 192 |
+
<div className="flex flex-col">
|
| 193 |
+
<span className="text-sm text-bolt-elements-textPrimary">Toggle Theme</span>
|
| 194 |
+
<span className="text-xs text-bolt-elements-textSecondary">Switch between light and dark mode</span>
|
| 195 |
+
</div>
|
| 196 |
+
<div className="flex items-center gap-1">
|
| 197 |
+
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
| 198 |
+
{getModifierSymbol('meta')}
|
| 199 |
+
</kbd>
|
| 200 |
+
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
| 201 |
+
{getModifierSymbol('alt')}
|
| 202 |
+
</kbd>
|
| 203 |
+
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
| 204 |
+
{getModifierSymbol('shift')}
|
| 205 |
+
</kbd>
|
| 206 |
+
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
| 207 |
+
D
|
| 208 |
+
</kbd>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
</div>
|
| 210 |
+
</div>
|
| 211 |
</div>
|
| 212 |
</motion.div>
|
| 213 |
</div>
|
app/components/chat/BaseChat.tsx
CHANGED
|
@@ -34,6 +34,7 @@ import ChatAlert from './ChatAlert';
|
|
| 34 |
import type { ModelInfo } from '~/lib/modules/llm/types';
|
| 35 |
import ProgressCompilation from './ProgressCompilation';
|
| 36 |
import type { ProgressAnnotation } from '~/types/context';
|
|
|
|
| 37 |
|
| 38 |
const TEXTAREA_MIN_HEIGHT = 76;
|
| 39 |
|
|
@@ -404,7 +405,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
| 404 |
apiKeys={apiKeys}
|
| 405 |
modelLoading={isModelLoading}
|
| 406 |
/>
|
| 407 |
-
{(providerList || []).length > 0 && provider && (
|
| 408 |
<APIKeyManager
|
| 409 |
provider={provider}
|
| 410 |
apiKey={apiKeys[provider.name] || ''}
|
|
|
|
| 34 |
import type { ModelInfo } from '~/lib/modules/llm/types';
|
| 35 |
import ProgressCompilation from './ProgressCompilation';
|
| 36 |
import type { ProgressAnnotation } from '~/types/context';
|
| 37 |
+
import { LOCAL_PROVIDERS } from '~/lib/stores/settings';
|
| 38 |
|
| 39 |
const TEXTAREA_MIN_HEIGHT = 76;
|
| 40 |
|
|
|
|
| 405 |
apiKeys={apiKeys}
|
| 406 |
modelLoading={isModelLoading}
|
| 407 |
/>
|
| 408 |
+
{(providerList || []).length > 0 && provider && !LOCAL_PROVIDERS.includes(provider.name) && (
|
| 409 |
<APIKeyManager
|
| 410 |
provider={provider}
|
| 411 |
apiKey={apiKeys[provider.name] || ''}
|
app/lib/stores/settings.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import { atom, map } from 'nanostores';
|
| 2 |
-
import { workbenchStore } from './workbench';
|
| 3 |
import { PROVIDER_LIST } from '~/utils/constants';
|
| 4 |
import type { IProviderConfig } from '~/types/model';
|
| 5 |
import type {
|
|
@@ -11,7 +10,7 @@ import type {
|
|
| 11 |
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
|
| 12 |
import Cookies from 'js-cookie';
|
| 13 |
import { toggleTheme } from './theme';
|
| 14 |
-
import {
|
| 15 |
|
| 16 |
export interface Shortcut {
|
| 17 |
key: string;
|
|
@@ -26,10 +25,8 @@ export interface Shortcut {
|
|
| 26 |
}
|
| 27 |
|
| 28 |
export interface Shortcuts {
|
| 29 |
-
toggleTerminal: Shortcut;
|
| 30 |
toggleTheme: Shortcut;
|
| 31 |
-
|
| 32 |
-
toggleSettings: Shortcut;
|
| 33 |
}
|
| 34 |
|
| 35 |
export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
|
|
@@ -37,15 +34,8 @@ export const LOCAL_PROVIDERS = ['OpenAILike', 'LMStudio', 'Ollama'];
|
|
| 37 |
|
| 38 |
export type ProviderSetting = Record<string, IProviderConfig>;
|
| 39 |
|
| 40 |
-
//
|
| 41 |
export const shortcutsStore = map<Shortcuts>({
|
| 42 |
-
toggleTerminal: {
|
| 43 |
-
key: '`',
|
| 44 |
-
ctrlOrMetaKey: true,
|
| 45 |
-
action: () => workbenchStore.toggleTerminal(),
|
| 46 |
-
description: 'Toggle terminal',
|
| 47 |
-
isPreventDefault: true,
|
| 48 |
-
},
|
| 49 |
toggleTheme: {
|
| 50 |
key: 'd',
|
| 51 |
metaKey: true,
|
|
@@ -55,22 +45,13 @@ export const shortcutsStore = map<Shortcuts>({
|
|
| 55 |
description: 'Toggle theme',
|
| 56 |
isPreventDefault: true,
|
| 57 |
},
|
| 58 |
-
|
| 59 |
-
key: '
|
| 60 |
-
ctrlOrMetaKey: true,
|
| 61 |
-
altKey: true, // Added alt key to make it more unique
|
| 62 |
-
action: () => chatStore.setKey('showChat', !chatStore.get().showChat),
|
| 63 |
-
description: 'Toggle chat',
|
| 64 |
-
isPreventDefault: true,
|
| 65 |
-
},
|
| 66 |
-
toggleSettings: {
|
| 67 |
-
key: 's',
|
| 68 |
ctrlOrMetaKey: true,
|
| 69 |
-
altKey: true,
|
| 70 |
action: () => {
|
| 71 |
-
|
| 72 |
},
|
| 73 |
-
description: 'Toggle
|
| 74 |
isPreventDefault: true,
|
| 75 |
},
|
| 76 |
});
|
|
@@ -319,3 +300,35 @@ export const setDeveloperMode = (value: boolean) => {
|
|
| 319 |
localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value));
|
| 320 |
}
|
| 321 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { atom, map } from 'nanostores';
|
|
|
|
| 2 |
import { PROVIDER_LIST } from '~/utils/constants';
|
| 3 |
import type { IProviderConfig } from '~/types/model';
|
| 4 |
import type {
|
|
|
|
| 10 |
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
|
| 11 |
import Cookies from 'js-cookie';
|
| 12 |
import { toggleTheme } from './theme';
|
| 13 |
+
import { create } from 'zustand';
|
| 14 |
|
| 15 |
export interface Shortcut {
|
| 16 |
key: string;
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
export interface Shortcuts {
|
|
|
|
| 28 |
toggleTheme: Shortcut;
|
| 29 |
+
toggleTerminal: Shortcut;
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
|
|
|
|
| 34 |
|
| 35 |
export type ProviderSetting = Record<string, IProviderConfig>;
|
| 36 |
|
| 37 |
+
// Simplified shortcuts store with only theme toggle
|
| 38 |
export const shortcutsStore = map<Shortcuts>({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
toggleTheme: {
|
| 40 |
key: 'd',
|
| 41 |
metaKey: true,
|
|
|
|
| 45 |
description: 'Toggle theme',
|
| 46 |
isPreventDefault: true,
|
| 47 |
},
|
| 48 |
+
toggleTerminal: {
|
| 49 |
+
key: '`',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
ctrlOrMetaKey: true,
|
|
|
|
| 51 |
action: () => {
|
| 52 |
+
// This will be handled by the terminal component
|
| 53 |
},
|
| 54 |
+
description: 'Toggle terminal',
|
| 55 |
isPreventDefault: true,
|
| 56 |
},
|
| 57 |
});
|
|
|
|
| 300 |
localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value));
|
| 301 |
}
|
| 302 |
};
|
| 303 |
+
|
| 304 |
+
// First, let's define the SettingsStore interface
|
| 305 |
+
interface SettingsStore {
|
| 306 |
+
isOpen: boolean;
|
| 307 |
+
selectedTab: string;
|
| 308 |
+
openSettings: () => void;
|
| 309 |
+
closeSettings: () => void;
|
| 310 |
+
setSelectedTab: (tab: string) => void;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
export const useSettingsStore = create<SettingsStore>((set) => ({
|
| 314 |
+
isOpen: false,
|
| 315 |
+
selectedTab: 'user', // Default tab
|
| 316 |
+
|
| 317 |
+
openSettings: () => {
|
| 318 |
+
set({
|
| 319 |
+
isOpen: true,
|
| 320 |
+
selectedTab: 'user', // Always open to user tab
|
| 321 |
+
});
|
| 322 |
+
},
|
| 323 |
+
|
| 324 |
+
closeSettings: () => {
|
| 325 |
+
set({
|
| 326 |
+
isOpen: false,
|
| 327 |
+
selectedTab: 'user', // Reset to user tab when closing
|
| 328 |
+
});
|
| 329 |
+
},
|
| 330 |
+
|
| 331 |
+
setSelectedTab: (tab: string) => {
|
| 332 |
+
set({ selectedTab: tab });
|
| 333 |
+
},
|
| 334 |
+
}));
|
app/utils/debounce.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
| 1 |
-
export function debounce<
|
| 2 |
-
|
| 3 |
-
return fn;
|
| 4 |
-
}
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
clearTimeout(timer);
|
| 12 |
-
|
| 13 |
-
timer = window.setTimeout(() => {
|
| 14 |
-
fn.apply(context, args);
|
| 15 |
-
}, delay);
|
| 16 |
};
|
| 17 |
}
|
|
|
|
| 1 |
+
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
|
| 2 |
+
let timeout: NodeJS.Timeout;
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
return function executedFunction(...args: Parameters<T>) {
|
| 5 |
+
const later = () => {
|
| 6 |
+
clearTimeout(timeout);
|
| 7 |
+
func(...args);
|
| 8 |
+
};
|
| 9 |
|
| 10 |
+
clearTimeout(timeout);
|
| 11 |
+
timeout = setTimeout(later, wait);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
};
|
| 13 |
}
|