Stijnus commited on
Commit
0765bc3
·
1 Parent(s): f32016c

add install ollama models , fixes

Browse files
app/components/chat/Chat.client.tsx CHANGED
@@ -251,31 +251,67 @@ export const ChatImpl = memo(
251
  const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
252
  const _input = messageInput || input;
253
 
254
- if (_input.length === 0 || isLoading) {
255
  return;
256
  }
257
 
258
- /**
259
- * @note (delm) Usually saving files shouldn't take long but it may take longer if there
260
- * many unsaved files. In that case we need to block user input and show an indicator
261
- * of some kind so the user is aware that something is happening. But I consider the
262
- * happy case to be no unsaved files and I would expect users to save their changes
263
- * before they send another message.
264
- */
265
- await workbenchStore.saveAllFiles();
266
-
267
- if (error != null) {
268
- setMessages(messages.slice(0, -1));
269
  }
270
 
271
- const fileModifications = workbenchStore.getFileModifcations();
272
-
273
- chatStore.setKey('aborted', false);
274
-
275
  runAnimation();
276
 
277
- if (!chatStarted && _input && autoSelectTemplate) {
278
  setFakeLoading(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  setMessages([
280
  {
281
  id: `${new Date().getTime()}`,
@@ -289,102 +325,22 @@ export const ChatImpl = memo(
289
  type: 'image',
290
  image: imageData,
291
  })),
292
- ] as any, // Type assertion to bypass compiler check
293
  },
294
  ]);
 
 
295
 
296
- // reload();
297
-
298
- const { template, title } = await selectStarterTemplate({
299
- message: _input,
300
- model,
301
- provider,
302
- });
303
-
304
- if (template !== 'blank') {
305
- const temResp = await getTemplates(template, title).catch((e) => {
306
- if (e.message.includes('rate limit')) {
307
- toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template');
308
- } else {
309
- toast.warning('Failed to import starter template\n Continuing with blank template');
310
- }
311
-
312
- return null;
313
- });
314
-
315
- if (temResp) {
316
- const { assistantMessage, userMessage } = temResp;
317
-
318
- setMessages([
319
- {
320
- id: `${new Date().getTime()}`,
321
- role: 'user',
322
- content: _input,
323
-
324
- // annotations: ['hidden'],
325
- },
326
- {
327
- id: `${new Date().getTime()}`,
328
- role: 'assistant',
329
- content: assistantMessage,
330
- },
331
- {
332
- id: `${new Date().getTime()}`,
333
- role: 'user',
334
- content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`,
335
- annotations: ['hidden'],
336
- },
337
- ]);
338
-
339
- reload();
340
- setFakeLoading(false);
341
 
342
- return;
343
- } else {
344
- setMessages([
345
- {
346
- id: `${new Date().getTime()}`,
347
- role: 'user',
348
- content: [
349
- {
350
- type: 'text',
351
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
352
- },
353
- ...imageDataList.map((imageData) => ({
354
- type: 'image',
355
- image: imageData,
356
- })),
357
- ] as any, // Type assertion to bypass compiler check
358
- },
359
- ]);
360
- reload();
361
- setFakeLoading(false);
362
 
363
- return;
364
- }
365
- } else {
366
- setMessages([
367
- {
368
- id: `${new Date().getTime()}`,
369
- role: 'user',
370
- content: [
371
- {
372
- type: 'text',
373
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
374
- },
375
- ...imageDataList.map((imageData) => ({
376
- type: 'image',
377
- image: imageData,
378
- })),
379
- ] as any, // Type assertion to bypass compiler check
380
- },
381
- ]);
382
- reload();
383
- setFakeLoading(false);
384
 
385
- return;
386
- }
387
- }
388
 
389
  if (fileModifications !== undefined) {
390
  /**
 
251
  const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
252
  const _input = messageInput || input;
253
 
254
+ if (!_input) {
255
  return;
256
  }
257
 
258
+ if (isLoading) {
259
+ abort();
260
+ return;
 
 
 
 
 
 
 
 
261
  }
262
 
 
 
 
 
263
  runAnimation();
264
 
265
+ if (!chatStarted) {
266
  setFakeLoading(true);
267
+
268
+ if (autoSelectTemplate) {
269
+ const { template, title } = await selectStarterTemplate({
270
+ message: _input,
271
+ model,
272
+ provider,
273
+ });
274
+
275
+ if (template !== 'blank') {
276
+ const temResp = await getTemplates(template, title).catch((e) => {
277
+ if (e.message.includes('rate limit')) {
278
+ toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template');
279
+ } else {
280
+ toast.warning('Failed to import starter template\n Continuing with blank template');
281
+ }
282
+
283
+ return null;
284
+ });
285
+
286
+ if (temResp) {
287
+ const { assistantMessage, userMessage } = temResp;
288
+ setMessages([
289
+ {
290
+ id: `${new Date().getTime()}`,
291
+ role: 'user',
292
+ content: _input,
293
+ },
294
+ {
295
+ id: `${new Date().getTime()}`,
296
+ role: 'assistant',
297
+ content: assistantMessage,
298
+ },
299
+ {
300
+ id: `${new Date().getTime()}`,
301
+ role: 'user',
302
+ content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`,
303
+ annotations: ['hidden'],
304
+ },
305
+ ]);
306
+ reload();
307
+ setFakeLoading(false);
308
+
309
+ return;
310
+ }
311
+ }
312
+ }
313
+
314
+ // If autoSelectTemplate is disabled or template selection failed, proceed with normal message
315
  setMessages([
316
  {
317
  id: `${new Date().getTime()}`,
 
325
  type: 'image',
326
  image: imageData,
327
  })),
328
+ ] as any,
329
  },
330
  ]);
331
+ reload();
332
+ setFakeLoading(false);
333
 
334
+ return;
335
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
+ if (error != null) {
338
+ setMessages(messages.slice(0, -1));
339
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
+ const fileModifications = workbenchStore.getFileModifcations();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
+ chatStore.setKey('aborted', false);
 
 
344
 
345
  if (fileModifications !== undefined) {
346
  /**
app/components/settings/developer/DeveloperWindow.tsx CHANGED
@@ -385,242 +385,247 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
385
  }, [open]);
386
 
387
  return (
388
- <DndProvider backend={HTML5Backend}>
389
- <RadixDialog.Root open={open}>
390
- <RadixDialog.Portal>
391
- <div className="fixed inset-0 flex items-center justify-center z-[100]">
392
- <RadixDialog.Overlay className="fixed inset-0">
393
- <motion.div
394
- className="absolute inset-0 bg-black/50 backdrop-blur-sm"
395
- initial={{ opacity: 0 }}
396
- animate={{ opacity: 1 }}
397
- exit={{ opacity: 0 }}
398
- transition={{ duration: 0.2 }}
399
- />
400
- </RadixDialog.Overlay>
401
-
402
- <RadixDialog.Content
403
- aria-describedby={undefined}
404
- onEscapeKeyDown={onClose}
405
- onPointerDownOutside={onClose}
406
- className="relative z-[101]"
407
  >
408
- <motion.div
409
- className={classNames(
410
- 'w-[1200px] h-[90vh]',
411
- 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
412
- 'rounded-2xl shadow-2xl',
413
- 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
414
- 'flex flex-col overflow-hidden',
415
- )}
416
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
417
- animate={{ opacity: developerMode ? 1 : 0, scale: developerMode ? 1 : 0.95, y: developerMode ? 0 : 20 }}
418
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
419
- transition={{ duration: 0.2 }}
420
- >
421
- {/* Header */}
422
- <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
423
- <div className="flex items-center space-x-4">
424
- {activeTab || showTabManagement ? (
425
- <button
426
- onClick={handleBack}
427
- className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
428
- >
429
- <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
430
- </button>
431
- ) : (
432
- <motion.div
433
- className="i-ph:lightning-fill w-5 h-5 text-purple-500"
434
- initial={{ rotate: -10 }}
435
- animate={{ rotate: 10 }}
436
- transition={{
437
- repeat: Infinity,
438
- repeatType: 'reverse',
439
- duration: 2,
440
- ease: 'easeInOut',
441
- }}
442
- />
443
- )}
444
- <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
445
- {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Settings'}
446
- </DialogTitle>
447
  </div>
448
-
449
- <div className="flex items-center space-x-4">
450
- {!activeTab && !showTabManagement && (
451
- <motion.button
452
- onClick={() => setShowTabManagement(true)}
453
- className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
454
- whileHover={{ scale: 1.05 }}
455
- whileTap={{ scale: 0.95 }}
456
- >
457
- <div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
458
- <span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
459
- Manage Tabs
460
- </span>
461
- </motion.button>
462
  )}
463
-
464
- <div className="flex items-center gap-2">
465
- <Switch
466
- checked={developerMode}
467
- onCheckedChange={handleDeveloperModeChange}
468
- className="data-[state=checked]:bg-purple-500"
469
- aria-label="Toggle developer mode"
470
- />
471
- <label className="text-sm text-gray-500 dark:text-gray-400">Switch to User Mode</label>
472
- </div>
473
-
474
- <div className="relative">
475
- <DropdownMenu.Root>
476
- <DropdownMenu.Trigger asChild>
477
- <button className="flex items-center justify-center w-8 h-8 rounded-full overflow-hidden hover:ring-2 ring-gray-300 dark:ring-gray-600 transition-all">
478
- {profile.avatar ? (
479
- <img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
480
- ) : (
481
- <div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
482
- <svg
483
- className="w-5 h-5 text-gray-500 dark:text-gray-400"
484
- fill="none"
485
- stroke="currentColor"
486
- viewBox="0 0 24 24"
487
- >
488
- <path
489
- strokeLinecap="round"
490
- strokeLinejoin="round"
491
- strokeWidth={2}
492
- d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
493
- />
494
- </svg>
495
- </div>
496
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  </button>
498
- </DropdownMenu.Trigger>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
- <DropdownMenu.Portal>
501
- <DropdownMenu.Content
502
- className="min-w-[220px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-[200]"
503
- sideOffset={5}
504
- align="end"
 
 
505
  >
506
- <DropdownMenu.Item
507
- className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
508
- onSelect={() => handleTabClick('profile')}
509
- >
510
- <div className="mr-3 flex h-5 w-5 items-center justify-center">
511
- <div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
512
- </div>
513
- <span className="group-hover:text-purple-500 transition-colors">Profile</span>
514
- </DropdownMenu.Item>
515
-
516
- <DropdownMenu.Item
517
- className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
518
- onSelect={() => handleTabClick('settings')}
519
- >
520
- <div className="mr-3 flex h-5 w-5 items-center justify-center">
521
- <div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
522
- </div>
523
- <span className="group-hover:text-purple-500 transition-colors">Settings</span>
524
- </DropdownMenu.Item>
525
-
526
- {profile.notifications && (
527
- <>
528
- <DropdownMenu.Item
529
- className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
530
- onSelect={() => handleTabClick('notifications')}
531
- >
532
- <div className="mr-3 flex h-5 w-5 items-center justify-center">
533
- <div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
534
- </div>
535
- <span className="group-hover:text-purple-500 transition-colors">
536
- Notifications
537
- {hasUnreadNotifications && (
538
- <span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
539
- {unreadNotifications.length}
540
- </span>
541
- )}
542
- </span>
543
- </DropdownMenu.Item>
544
-
545
- <DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
546
- </>
547
- )}
548
- <DropdownMenu.Item
549
- className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
550
- onSelect={() => handleTabClick('task-manager')}
551
- >
552
- <div className="mr-3 flex h-5 w-5 items-center justify-center">
553
- <div className="i-ph:activity w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
554
- </div>
555
- <span className="group-hover:text-purple-500 transition-colors">Task Manager</span>
556
- </DropdownMenu.Item>
557
- <DropdownMenu.Item
558
- className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
559
- onSelect={onClose}
560
- >
561
- <div className="mr-3 flex h-5 w-5 items-center justify-center">
562
- <div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
563
- </div>
564
- <span className="group-hover:text-purple-500 transition-colors">Close</span>
565
- </DropdownMenu.Item>
566
- </DropdownMenu.Content>
567
- </DropdownMenu.Portal>
568
- </DropdownMenu.Root>
569
  </div>
570
 
571
- <button
572
- onClick={onClose}
573
- className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
 
 
 
 
 
 
 
 
 
 
574
  >
575
- <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
576
- </button>
577
- </div>
578
- </div>
579
-
580
- {/* Content */}
581
- <div
582
- className={classNames(
583
- 'flex-1',
584
- 'overflow-y-auto',
585
- 'hover:overflow-y-auto',
586
- 'scrollbar scrollbar-w-2',
587
- 'scrollbar-track-transparent',
588
- 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
589
- 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
590
- 'will-change-scroll',
591
- 'touch-auto',
592
- )}
593
- >
594
- <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="p-6">
595
- {showTabManagement ? (
596
- <TabManagement />
597
- ) : activeTab ? (
598
- getTabComponent()
599
- ) : (
600
- <div className="grid grid-cols-4 gap-4">
601
- {visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
602
- <DraggableTabTile
603
- key={tab.id}
604
- tab={tab}
605
- index={index}
606
- moveTab={moveTab}
607
- onClick={() => handleTabClick(tab.id)}
608
- isActive={activeTab === tab.id}
609
- hasUpdate={getTabUpdateStatus(tab.id)}
610
- statusMessage={getStatusMessage(tab.id)}
611
- description={TAB_DESCRIPTIONS[tab.id]}
612
- isLoading={loadingTab === tab.id}
613
- />
614
- ))}
615
- </div>
616
- )}
617
  </motion.div>
618
- </div>
619
- </motion.div>
620
- </RadixDialog.Content>
621
- </div>
622
- </RadixDialog.Portal>
623
- </RadixDialog.Root>
624
- </DndProvider>
625
  );
626
  };
 
385
  }, [open]);
386
 
387
  return (
388
+ <>
389
+ <DropdownMenu.Root>
390
+ <DropdownMenu.Portal>
391
+ <DropdownMenu.Content
392
+ className="min-w-[220px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-[200] animate-in fade-in-0 zoom-in-95"
393
+ sideOffset={5}
394
+ align="end"
395
+ >
396
+ <DropdownMenu.Item
397
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
398
+ onSelect={() => handleTabClick('profile')}
 
 
 
 
 
 
 
 
399
  >
400
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
401
+ <div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
402
+ </div>
403
+ <span className="group-hover:text-purple-500 transition-colors">Profile</span>
404
+ </DropdownMenu.Item>
405
+
406
+ <DropdownMenu.Item
407
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
408
+ onSelect={() => handleTabClick('settings')}
409
+ >
410
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
411
+ <div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
412
+ </div>
413
+ <span className="group-hover:text-purple-500 transition-colors">Settings</span>
414
+ </DropdownMenu.Item>
415
+
416
+ {profile.notifications && (
417
+ <>
418
+ <DropdownMenu.Item
419
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
420
+ onSelect={() => handleTabClick('notifications')}
421
+ >
422
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
423
+ <div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  </div>
425
+ <span className="group-hover:text-purple-500 transition-colors">
426
+ Notifications
427
+ {hasUnreadNotifications && (
428
+ <span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
429
+ {unreadNotifications.length}
430
+ </span>
 
 
 
 
 
 
 
 
431
  )}
432
+ </span>
433
+ </DropdownMenu.Item>
434
+
435
+ <DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
436
+ </>
437
+ )}
438
+ <DropdownMenu.Item
439
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
440
+ onSelect={() => handleTabClick('task-manager')}
441
+ >
442
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
443
+ <div className="i-ph:activity w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
444
+ </div>
445
+ <span className="group-hover:text-purple-500 transition-colors">Task Manager</span>
446
+ </DropdownMenu.Item>
447
+ <DropdownMenu.Item
448
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
449
+ onSelect={onClose}
450
+ >
451
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
452
+ <div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
453
+ </div>
454
+ <span className="group-hover:text-purple-500 transition-colors">Close</span>
455
+ </DropdownMenu.Item>
456
+ </DropdownMenu.Content>
457
+ </DropdownMenu.Portal>
458
+ <DndProvider backend={HTML5Backend}>
459
+ <RadixDialog.Root open={open}>
460
+ <RadixDialog.Portal>
461
+ <div className="fixed inset-0 flex items-center justify-center z-[100]">
462
+ <RadixDialog.Overlay className="fixed inset-0">
463
+ <motion.div
464
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
465
+ initial={{ opacity: 0 }}
466
+ animate={{ opacity: 1 }}
467
+ exit={{ opacity: 0 }}
468
+ transition={{ duration: 0.2 }}
469
+ />
470
+ </RadixDialog.Overlay>
471
+
472
+ <RadixDialog.Content
473
+ aria-describedby={undefined}
474
+ onEscapeKeyDown={onClose}
475
+ onPointerDownOutside={onClose}
476
+ className="relative z-[101]"
477
+ >
478
+ <motion.div
479
+ className={classNames(
480
+ 'w-[1200px] h-[90vh]',
481
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
482
+ 'rounded-2xl shadow-2xl',
483
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
484
+ 'flex flex-col overflow-hidden',
485
+ )}
486
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
487
+ animate={{
488
+ opacity: developerMode ? 1 : 0,
489
+ scale: developerMode ? 1 : 0.95,
490
+ y: developerMode ? 0 : 20,
491
+ }}
492
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
493
+ transition={{ duration: 0.2 }}
494
+ >
495
+ {/* Header */}
496
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
497
+ <div className="flex items-center space-x-4">
498
+ {activeTab || showTabManagement ? (
499
+ <button
500
+ onClick={handleBack}
501
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
502
+ >
503
+ <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
504
  </button>
505
+ ) : (
506
+ <motion.div
507
+ className="i-ph:lightning-fill w-5 h-5 text-purple-500"
508
+ initial={{ rotate: -10 }}
509
+ animate={{ rotate: 10 }}
510
+ transition={{
511
+ repeat: Infinity,
512
+ repeatType: 'reverse',
513
+ duration: 2,
514
+ ease: 'easeInOut',
515
+ }}
516
+ />
517
+ )}
518
+ <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
519
+ {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Settings'}
520
+ </DialogTitle>
521
+ </div>
522
 
523
+ <div className="flex items-center space-x-4">
524
+ {!activeTab && !showTabManagement && (
525
+ <motion.button
526
+ onClick={() => setShowTabManagement(true)}
527
+ className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
528
+ whileHover={{ scale: 1.05 }}
529
+ whileTap={{ scale: 0.95 }}
530
  >
531
+ <div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
532
+ <span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
533
+ Manage Tabs
534
+ </span>
535
+ </motion.button>
536
+ )}
537
+
538
+ <div className="flex items-center gap-2">
539
+ <Switch
540
+ checked={developerMode}
541
+ onCheckedChange={handleDeveloperModeChange}
542
+ className="data-[state=checked]:bg-purple-500"
543
+ aria-label="Toggle developer mode"
544
+ />
545
+ <label className="text-sm text-gray-500 dark:text-gray-400">Switch to User Mode</label>
546
+ </div>
547
+
548
+ <div className="relative">
549
+ <DropdownMenu.Trigger asChild>
550
+ <button className="flex items-center justify-center w-8 h-8 rounded-full overflow-hidden hover:ring-2 ring-gray-300 dark:ring-gray-600 transition-all">
551
+ {profile.avatar ? (
552
+ <img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
553
+ ) : (
554
+ <div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
555
+ <svg
556
+ className="w-5 h-5 text-gray-500 dark:text-gray-400"
557
+ fill="none"
558
+ stroke="currentColor"
559
+ viewBox="0 0 24 24"
560
+ >
561
+ <path
562
+ strokeLinecap="round"
563
+ strokeLinejoin="round"
564
+ strokeWidth={2}
565
+ d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
566
+ />
567
+ </svg>
568
+ </div>
569
+ )}
570
+ </button>
571
+ </DropdownMenu.Trigger>
572
+ </div>
573
+
574
+ <button
575
+ onClick={onClose}
576
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
577
+ >
578
+ <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
579
+ </button>
580
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
581
  </div>
582
 
583
+ {/* Content */}
584
+ <div
585
+ className={classNames(
586
+ 'flex-1',
587
+ 'overflow-y-auto',
588
+ 'hover:overflow-y-auto',
589
+ 'scrollbar scrollbar-w-2',
590
+ 'scrollbar-track-transparent',
591
+ 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
592
+ 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
593
+ 'will-change-scroll',
594
+ 'touch-auto',
595
+ )}
596
  >
597
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="p-6">
598
+ {showTabManagement ? (
599
+ <TabManagement />
600
+ ) : activeTab ? (
601
+ getTabComponent()
602
+ ) : (
603
+ <div className="grid grid-cols-4 gap-4">
604
+ {visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
605
+ <DraggableTabTile
606
+ key={tab.id}
607
+ tab={tab}
608
+ index={index}
609
+ moveTab={moveTab}
610
+ onClick={() => handleTabClick(tab.id)}
611
+ isActive={activeTab === tab.id}
612
+ hasUpdate={getTabUpdateStatus(tab.id)}
613
+ statusMessage={getStatusMessage(tab.id)}
614
+ description={TAB_DESCRIPTIONS[tab.id]}
615
+ isLoading={loadingTab === tab.id}
616
+ />
617
+ ))}
618
+ </div>
619
+ )}
620
+ </motion.div>
621
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  </motion.div>
623
+ </RadixDialog.Content>
624
+ </div>
625
+ </RadixDialog.Portal>
626
+ </RadixDialog.Root>
627
+ </DndProvider>
628
+ </DropdownMenu.Root>
629
+ </>
630
  );
631
  };
app/components/settings/features/FeaturesTab.tsx CHANGED
@@ -6,13 +6,14 @@ import { classNames } from '~/utils/classNames';
6
  import { toast } from 'react-toastify';
7
  import { PromptLibrary } from '~/lib/common/prompt-library';
8
  import {
9
- isEventLogsEnabled,
 
 
10
  isLocalModelsEnabled,
11
- latestBranchStore as latestBranchAtom,
12
  promptStore as promptAtom,
13
- autoSelectStarterTemplate as autoSelectTemplateAtom,
14
- enableContextOptimizationStore as contextOptimizationAtom,
15
  } from '~/lib/stores/settings';
 
16
 
17
  interface FeatureToggle {
18
  id: string;
@@ -115,14 +116,6 @@ const FeatureSection = memo(
115
  export default function FeaturesTab() {
116
  const { autoSelectTemplate, isLatestBranch, contextOptimizationEnabled, eventLogs, isLocalModel } = useSettings();
117
 
118
- // Setup store setters
119
- const setEventLogs = (value: boolean) => isEventLogsEnabled.set(value);
120
- const setLocalModels = (value: boolean) => isLocalModelsEnabled.set(value);
121
- const setLatestBranch = (value: boolean) => latestBranchAtom.set(value);
122
- const setPromptId = (value: string) => promptAtom.set(value);
123
- const setAutoSelectTemplate = (value: boolean) => autoSelectTemplateAtom.set(value);
124
- const setContextOptimization = (value: boolean) => contextOptimizationAtom.set(value);
125
-
126
  const getLocalStorageBoolean = (key: string, defaultValue: boolean): boolean => {
127
  const value = localStorage.getItem(key);
128
 
@@ -137,7 +130,6 @@ export default function FeaturesTab() {
137
  }
138
  };
139
 
140
- // Initialize state with proper type handling
141
  const autoSelectTemplateState = getLocalStorageBoolean('autoSelectTemplate', autoSelectTemplate);
142
  const enableLatestBranchState = getLocalStorageBoolean('enableLatestBranch', isLatestBranch);
143
  const contextOptimizationState = getLocalStorageBoolean('contextOptimization', contextOptimizationEnabled);
@@ -155,7 +147,6 @@ export default function FeaturesTab() {
155
  const [promptIdLocal, setPromptIdLocal] = useState(promptIdState);
156
 
157
  useEffect(() => {
158
- // Update localStorage
159
  localStorage.setItem('autoSelectTemplate', JSON.stringify(autoSelectTemplateLocal));
160
  localStorage.setItem('enableLatestBranch', JSON.stringify(enableLatestBranchLocal));
161
  localStorage.setItem('contextOptimization', JSON.stringify(contextOptimizationLocal));
@@ -164,13 +155,12 @@ export default function FeaturesTab() {
164
  localStorage.setItem('promptLibrary', JSON.stringify(promptLibraryLocal));
165
  localStorage.setItem('promptId', promptIdLocal);
166
 
167
- // Update global state
168
- setEventLogs(eventLogsLocal);
169
- setLocalModels(experimentalProvidersLocal);
170
- setLatestBranch(enableLatestBranchLocal);
171
- setPromptId(promptIdLocal);
172
- setAutoSelectTemplate(autoSelectTemplateLocal);
173
- setContextOptimization(contextOptimizationLocal);
174
  }, [
175
  autoSelectTemplateLocal,
176
  enableLatestBranchLocal,
@@ -182,27 +172,34 @@ export default function FeaturesTab() {
182
  ]);
183
 
184
  const handleToggleFeature = (featureId: string, enabled: boolean) => {
 
 
185
  switch (featureId) {
186
  case 'latestBranch':
187
  setEnableLatestBranchLocal(enabled);
 
188
  toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
189
  break;
190
- case 'autoTemplate':
191
  setAutoSelectTemplateLocal(enabled);
 
192
  toast.success(`Auto template selection ${enabled ? 'enabled' : 'disabled'}`);
193
  break;
194
  case 'contextOptimization':
195
  setContextOptimizationLocal(enabled);
 
196
  toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
197
  break;
 
 
 
 
 
198
  case 'eventLogs':
199
  setEventLogsLocal(enabled);
 
200
  toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
201
  break;
202
- case 'experimentalProviders':
203
- setExperimentalProvidersLocal(enabled);
204
- toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
205
- break;
206
  case 'promptLibrary':
207
  setPromptLibraryLocal(enabled);
208
  toast.success(`Prompt Library ${enabled ? 'enabled' : 'disabled'}`);
@@ -213,7 +210,7 @@ export default function FeaturesTab() {
213
  const features: Record<'stable' | 'beta' | 'experimental', FeatureToggle[]> = {
214
  stable: [
215
  {
216
- id: 'autoTemplate',
217
  title: 'Auto Select Code Template',
218
  description: 'Let Bolt select the best starter template for your project',
219
  icon: 'i-ph:magic-wand',
@@ -245,20 +242,10 @@ export default function FeaturesTab() {
245
  tooltip: 'Enable or disable the prompt library',
246
  },
247
  ],
248
- beta: [
249
- {
250
- id: 'latestBranch',
251
- title: 'Use Main Branch',
252
- description: 'Check for updates against the main branch instead of stable',
253
- icon: 'i-ph:git-branch',
254
- enabled: enableLatestBranchLocal,
255
- beta: true,
256
- tooltip: 'Get the latest features and improvements before they are officially released',
257
- },
258
- ],
259
  experimental: [
260
  {
261
- id: 'experimentalProviders',
262
  title: 'Experimental Providers',
263
  description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
264
  icon: 'i-ph:robot',
 
6
  import { toast } from 'react-toastify';
7
  import { PromptLibrary } from '~/lib/common/prompt-library';
8
  import {
9
+ latestBranchStore,
10
+ autoSelectStarterTemplate,
11
+ enableContextOptimizationStore,
12
  isLocalModelsEnabled,
13
+ isEventLogsEnabled,
14
  promptStore as promptAtom,
 
 
15
  } from '~/lib/stores/settings';
16
+ import { logStore } from '~/lib/stores/logs';
17
 
18
  interface FeatureToggle {
19
  id: string;
 
116
  export default function FeaturesTab() {
117
  const { autoSelectTemplate, isLatestBranch, contextOptimizationEnabled, eventLogs, isLocalModel } = useSettings();
118
 
 
 
 
 
 
 
 
 
119
  const getLocalStorageBoolean = (key: string, defaultValue: boolean): boolean => {
120
  const value = localStorage.getItem(key);
121
 
 
130
  }
131
  };
132
 
 
133
  const autoSelectTemplateState = getLocalStorageBoolean('autoSelectTemplate', autoSelectTemplate);
134
  const enableLatestBranchState = getLocalStorageBoolean('enableLatestBranch', isLatestBranch);
135
  const contextOptimizationState = getLocalStorageBoolean('contextOptimization', contextOptimizationEnabled);
 
147
  const [promptIdLocal, setPromptIdLocal] = useState(promptIdState);
148
 
149
  useEffect(() => {
 
150
  localStorage.setItem('autoSelectTemplate', JSON.stringify(autoSelectTemplateLocal));
151
  localStorage.setItem('enableLatestBranch', JSON.stringify(enableLatestBranchLocal));
152
  localStorage.setItem('contextOptimization', JSON.stringify(contextOptimizationLocal));
 
155
  localStorage.setItem('promptLibrary', JSON.stringify(promptLibraryLocal));
156
  localStorage.setItem('promptId', promptIdLocal);
157
 
158
+ autoSelectStarterTemplate.set(autoSelectTemplateLocal);
159
+ latestBranchStore.set(enableLatestBranchLocal);
160
+ enableContextOptimizationStore.set(contextOptimizationLocal);
161
+ isEventLogsEnabled.set(eventLogsLocal);
162
+ isLocalModelsEnabled.set(experimentalProvidersLocal);
163
+ promptAtom.set(promptIdLocal);
 
164
  }, [
165
  autoSelectTemplateLocal,
166
  enableLatestBranchLocal,
 
172
  ]);
173
 
174
  const handleToggleFeature = (featureId: string, enabled: boolean) => {
175
+ logStore.logFeatureToggle(featureId, enabled);
176
+
177
  switch (featureId) {
178
  case 'latestBranch':
179
  setEnableLatestBranchLocal(enabled);
180
+ latestBranchStore.set(enabled);
181
  toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
182
  break;
183
+ case 'autoSelectTemplate':
184
  setAutoSelectTemplateLocal(enabled);
185
+ autoSelectStarterTemplate.set(enabled);
186
  toast.success(`Auto template selection ${enabled ? 'enabled' : 'disabled'}`);
187
  break;
188
  case 'contextOptimization':
189
  setContextOptimizationLocal(enabled);
190
+ enableContextOptimizationStore.set(enabled);
191
  toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
192
  break;
193
+ case 'localModels':
194
+ setExperimentalProvidersLocal(enabled);
195
+ isLocalModelsEnabled.set(enabled);
196
+ toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
197
+ break;
198
  case 'eventLogs':
199
  setEventLogsLocal(enabled);
200
+ isEventLogsEnabled.set(enabled);
201
  toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
202
  break;
 
 
 
 
203
  case 'promptLibrary':
204
  setPromptLibraryLocal(enabled);
205
  toast.success(`Prompt Library ${enabled ? 'enabled' : 'disabled'}`);
 
210
  const features: Record<'stable' | 'beta' | 'experimental', FeatureToggle[]> = {
211
  stable: [
212
  {
213
+ id: 'autoSelectTemplate',
214
  title: 'Auto Select Code Template',
215
  description: 'Let Bolt select the best starter template for your project',
216
  icon: 'i-ph:magic-wand',
 
242
  tooltip: 'Enable or disable the prompt library',
243
  },
244
  ],
245
+ beta: [],
 
 
 
 
 
 
 
 
 
 
246
  experimental: [
247
  {
248
+ id: 'localModels',
249
  title: 'Experimental Providers',
250
  description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
251
  icon: 'i-ph:robot',
app/components/settings/notifications/NotificationsTab.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { logStore } from '~/lib/stores/logs';
4
  import { useStore } from '@nanostores/react';
@@ -21,14 +21,47 @@ const NotificationsTab = () => {
21
  const [filter, setFilter] = useState<FilterType>('all');
22
  const logs = useStore(logStore.logs);
23
 
 
 
 
 
 
 
 
 
 
24
  const handleClearNotifications = () => {
 
 
 
 
 
 
 
25
  logStore.clearLogs();
26
  };
27
 
28
  const handleUpdateAction = (updateUrl: string) => {
 
 
 
 
 
 
29
  window.open(updateUrl, '_blank');
30
  };
31
 
 
 
 
 
 
 
 
 
 
 
 
32
  const filteredLogs = Object.values(logs)
33
  .filter((log) => {
34
  if (filter === 'all') {
@@ -172,7 +205,7 @@ const NotificationsTab = () => {
172
  <DropdownMenu.Item
173
  key={option.id}
174
  className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
175
- onClick={() => setFilter(option.id)}
176
  >
177
  <div className="mr-3 flex h-5 w-5 items-center justify-center">
178
  <div
 
1
+ import React, { useState, useEffect } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { logStore } from '~/lib/stores/logs';
4
  import { useStore } from '@nanostores/react';
 
21
  const [filter, setFilter] = useState<FilterType>('all');
22
  const logs = useStore(logStore.logs);
23
 
24
+ useEffect(() => {
25
+ const startTime = performance.now();
26
+
27
+ return () => {
28
+ const duration = performance.now() - startTime;
29
+ logStore.logPerformanceMetric('NotificationsTab', 'mount-duration', duration);
30
+ };
31
+ }, []);
32
+
33
  const handleClearNotifications = () => {
34
+ const count = Object.keys(logs).length;
35
+ logStore.logInfo('Cleared notifications', {
36
+ type: 'notification_clear',
37
+ message: `Cleared ${count} notifications`,
38
+ clearedCount: count,
39
+ component: 'notifications',
40
+ });
41
  logStore.clearLogs();
42
  };
43
 
44
  const handleUpdateAction = (updateUrl: string) => {
45
+ logStore.logInfo('Update link clicked', {
46
+ type: 'update_click',
47
+ message: 'User clicked update link',
48
+ updateUrl,
49
+ component: 'notifications',
50
+ });
51
  window.open(updateUrl, '_blank');
52
  };
53
 
54
+ const handleFilterChange = (newFilter: FilterType) => {
55
+ logStore.logInfo('Notification filter changed', {
56
+ type: 'filter_change',
57
+ message: `Filter changed to ${newFilter}`,
58
+ previousFilter: filter,
59
+ newFilter,
60
+ component: 'notifications',
61
+ });
62
+ setFilter(newFilter);
63
+ };
64
+
65
  const filteredLogs = Object.values(logs)
66
  .filter((log) => {
67
  if (filter === 'all') {
 
205
  <DropdownMenu.Item
206
  key={option.id}
207
  className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
208
+ onClick={() => handleFilterChange(option.id)}
209
  >
210
  <div className="mr-3 flex h-5 w-5 items-center justify-center">
211
  <div
app/components/settings/providers/LocalProvidersTab.tsx CHANGED
@@ -7,20 +7,20 @@ import { logStore } from '~/lib/stores/logs';
7
  import { motion } from 'framer-motion';
8
  import { classNames } from '~/utils/classNames';
9
  import { settingsStyles } from '~/components/settings/settings.styles';
10
- import { toast } from 'react-toastify';
11
- import { BsBox, BsCodeSquare, BsRobot } from 'react-icons/bs';
12
  import type { IconType } from 'react-icons';
13
  import { BiChip } from 'react-icons/bi';
14
  import { TbBrandOpenai } from 'react-icons/tb';
15
  import { providerBaseUrlEnvKeys } from '~/utils/constants';
 
16
 
17
  // Add type for provider names to ensure type safety
18
  type ProviderName = 'Ollama' | 'LMStudio' | 'OpenAILike';
19
 
20
  // Update the PROVIDER_ICONS type to use the ProviderName type
21
  const PROVIDER_ICONS: Record<ProviderName, IconType> = {
22
- Ollama: BsBox,
23
- LMStudio: BsCodeSquare,
24
  OpenAILike: TbBrandOpenai,
25
  };
26
 
@@ -31,6 +31,9 @@ const PROVIDER_DESCRIPTIONS: Record<ProviderName, string> = {
31
  OpenAILike: 'Connect to OpenAI-compatible API endpoints',
32
  };
33
 
 
 
 
34
  interface OllamaModel {
35
  name: string;
36
  digest: string;
@@ -51,17 +54,59 @@ interface OllamaModel {
51
  };
52
  }
53
 
54
- const LocalProvidersTab = () => {
55
- const settings = useSettings();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
57
  const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
58
  const [editingProvider, setEditingProvider] = useState<string | null>(null);
59
  const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
60
  const [isLoadingModels, setIsLoadingModels] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  // Effect to filter and sort providers
63
  useEffect(() => {
64
- const newFilteredProviders = Object.entries(settings.providers || {})
65
  .filter(([key]) => [...LOCAL_PROVIDERS, 'OpenAILike'].includes(key))
66
  .map(([key, value]) => {
67
  const provider = value as IProviderConfig;
@@ -79,7 +124,7 @@ const LocalProvidersTab = () => {
79
  // If there's an environment URL and no base URL set, update it
80
  if (envUrl && !provider.settings.baseUrl) {
81
  console.log(`Setting base URL for ${key} from env:`, envUrl);
82
- settings.updateProviderSettings(key, {
83
  ...provider.settings,
84
  baseUrl: envUrl,
85
  });
@@ -120,7 +165,7 @@ const LocalProvidersTab = () => {
120
  return a.name.localeCompare(b.name);
121
  });
122
  setFilteredProviders(sorted);
123
- }, [settings.providers]);
124
 
125
  // Helper function to safely get environment URL
126
  const getEnvUrl = (provider: IProviderConfig): string | undefined => {
@@ -165,7 +210,7 @@ const LocalProvidersTab = () => {
165
 
166
  const updateOllamaModel = async (modelName: string): Promise<{ success: boolean; newDigest?: string }> => {
167
  try {
168
- const response = await fetch('http://127.0.0.1:11434/api/pull', {
169
  method: 'POST',
170
  headers: { 'Content-Type': 'application/json' },
171
  body: JSON.stringify({ name: modelName }),
@@ -192,12 +237,12 @@ const LocalProvidersTab = () => {
192
  const lines = text.split('\n').filter(Boolean);
193
 
194
  for (const line of lines) {
195
- const data = JSON.parse(line) as {
196
- status: string;
197
- completed?: number;
198
- total?: number;
199
- digest?: string;
200
- };
201
 
202
  setOllamaModels((current) =>
203
  current.map((m) =>
@@ -205,11 +250,11 @@ const LocalProvidersTab = () => {
205
  ? {
206
  ...m,
207
  progress: {
208
- current: data.completed || 0,
209
- total: data.total || 0,
210
- status: data.status,
211
  },
212
- newDigest: data.digest,
213
  }
214
  : m,
215
  ),
@@ -232,22 +277,22 @@ const LocalProvidersTab = () => {
232
  (enabled: boolean) => {
233
  setCategoryEnabled(enabled);
234
  filteredProviders.forEach((provider) => {
235
- settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
236
  });
237
- toast.success(enabled ? 'All local providers enabled' : 'All local providers disabled');
238
  },
239
- [filteredProviders, settings],
240
  );
241
 
242
  const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
243
- settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
244
 
245
  if (enabled) {
246
  logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
247
- toast.success(`${provider.name} enabled`);
248
  } else {
249
  logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
250
- toast.success(`${provider.name} disabled`);
251
  }
252
  };
253
 
@@ -258,42 +303,193 @@ const LocalProvidersTab = () => {
258
  newBaseUrl = undefined;
259
  }
260
 
261
- settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
262
  logStore.logProvider(`Base URL updated for ${provider.name}`, {
263
  provider: provider.name,
264
  baseUrl: newBaseUrl,
265
  });
266
- toast.success(`${provider.name} base URL updated`);
267
  setEditingProvider(null);
268
  };
269
 
270
  const handleUpdateOllamaModel = async (modelName: string) => {
271
  setOllamaModels((current) => current.map((m) => (m.name === modelName ? { ...m, status: 'updating' } : m)));
272
 
273
- const { success, newDigest } = await updateOllamaModel(modelName);
274
 
275
  setOllamaModels((current) =>
276
  current.map((m) =>
277
  m.name === modelName
278
  ? {
279
  ...m,
280
- status: success ? 'updated' : 'error',
281
- error: success ? undefined : 'Update failed',
282
  newDigest,
283
  }
284
  : m,
285
  ),
286
  );
287
 
288
- if (success) {
289
- toast.success(`Updated ${modelName}`);
290
  } else {
291
- toast.error(`Failed to update ${modelName}`);
292
  }
293
  };
294
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  return (
296
  <div className="space-y-6">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  <motion.div
298
  className="space-y-4"
299
  initial={{ opacity: 0, y: 20 }}
@@ -527,21 +723,40 @@ const LocalProvidersTab = () => {
527
  )}
528
  </div>
529
  </div>
530
- <motion.button
531
- onClick={() => handleUpdateOllamaModel(model.name)}
532
- disabled={model.status === 'updating'}
533
- className={classNames(
534
- settingsStyles.button.base,
535
- settingsStyles.button.secondary,
536
- 'hover:bg-purple-500/10 hover:text-purple-500',
537
- 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
538
- )}
539
- whileHover={{ scale: 1.02 }}
540
- whileTap={{ scale: 0.98 }}
541
- >
542
- <div className="i-ph:arrows-clockwise" />
543
- Update
544
- </motion.button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
  </div>
546
  ))}
547
  </div>
@@ -560,8 +775,130 @@ const LocalProvidersTab = () => {
560
  ))}
561
  </div>
562
  </motion.div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  </div>
564
  );
565
- };
566
 
567
  export default LocalProvidersTab;
 
7
  import { motion } from 'framer-motion';
8
  import { classNames } from '~/utils/classNames';
9
  import { settingsStyles } from '~/components/settings/settings.styles';
10
+ import { BsRobot } from 'react-icons/bs';
 
11
  import type { IconType } from 'react-icons';
12
  import { BiChip } from 'react-icons/bi';
13
  import { TbBrandOpenai } from 'react-icons/tb';
14
  import { providerBaseUrlEnvKeys } from '~/utils/constants';
15
+ import { useToast } from '~/components/ui/use-toast';
16
 
17
  // Add type for provider names to ensure type safety
18
  type ProviderName = 'Ollama' | 'LMStudio' | 'OpenAILike';
19
 
20
  // Update the PROVIDER_ICONS type to use the ProviderName type
21
  const PROVIDER_ICONS: Record<ProviderName, IconType> = {
22
+ Ollama: BsRobot,
23
+ LMStudio: BsRobot,
24
  OpenAILike: TbBrandOpenai,
25
  };
26
 
 
31
  OpenAILike: 'Connect to OpenAI-compatible API endpoints',
32
  };
33
 
34
+ // Add a constant for the Ollama API base URL
35
+ const OLLAMA_API_URL = 'http://127.0.0.1:11434';
36
+
37
  interface OllamaModel {
38
  name: string;
39
  digest: string;
 
54
  };
55
  }
56
 
57
+ interface OllamaServiceStatus {
58
+ isRunning: boolean;
59
+ lastChecked: Date;
60
+ error?: string;
61
+ }
62
+
63
+ interface OllamaPullResponse {
64
+ status: string;
65
+ completed?: number;
66
+ total?: number;
67
+ digest?: string;
68
+ }
69
+
70
+ const isOllamaPullResponse = (data: unknown): data is OllamaPullResponse => {
71
+ return (
72
+ typeof data === 'object' &&
73
+ data !== null &&
74
+ 'status' in data &&
75
+ typeof (data as OllamaPullResponse).status === 'string'
76
+ );
77
+ };
78
+
79
+ interface ManualInstallState {
80
+ isOpen: boolean;
81
+ modelString: string;
82
+ }
83
+
84
+ export function LocalProvidersTab() {
85
+ const { success, error } = useToast();
86
+ const { providers, updateProviderSettings } = useSettings();
87
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
88
  const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
89
  const [editingProvider, setEditingProvider] = useState<string | null>(null);
90
  const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
91
  const [isLoadingModels, setIsLoadingModels] = useState(false);
92
+ const [serviceStatus, setServiceStatus] = useState<OllamaServiceStatus>({
93
+ isRunning: false,
94
+ lastChecked: new Date(),
95
+ });
96
+ const [isInstallingModel, setIsInstallingModel] = useState<string | null>(null);
97
+ const [installProgress, setInstallProgress] = useState<{
98
+ model: string;
99
+ progress: number;
100
+ status: string;
101
+ } | null>(null);
102
+ const [manualInstall, setManualInstall] = useState<ManualInstallState>({
103
+ isOpen: false,
104
+ modelString: '',
105
+ });
106
 
107
  // Effect to filter and sort providers
108
  useEffect(() => {
109
+ const newFilteredProviders = Object.entries(providers || {})
110
  .filter(([key]) => [...LOCAL_PROVIDERS, 'OpenAILike'].includes(key))
111
  .map(([key, value]) => {
112
  const provider = value as IProviderConfig;
 
124
  // If there's an environment URL and no base URL set, update it
125
  if (envUrl && !provider.settings.baseUrl) {
126
  console.log(`Setting base URL for ${key} from env:`, envUrl);
127
+ updateProviderSettings(key, {
128
  ...provider.settings,
129
  baseUrl: envUrl,
130
  });
 
165
  return a.name.localeCompare(b.name);
166
  });
167
  setFilteredProviders(sorted);
168
+ }, [providers, updateProviderSettings]);
169
 
170
  // Helper function to safely get environment URL
171
  const getEnvUrl = (provider: IProviderConfig): string | undefined => {
 
210
 
211
  const updateOllamaModel = async (modelName: string): Promise<{ success: boolean; newDigest?: string }> => {
212
  try {
213
+ const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
214
  method: 'POST',
215
  headers: { 'Content-Type': 'application/json' },
216
  body: JSON.stringify({ name: modelName }),
 
237
  const lines = text.split('\n').filter(Boolean);
238
 
239
  for (const line of lines) {
240
+ const rawData = JSON.parse(line);
241
+
242
+ if (!isOllamaPullResponse(rawData)) {
243
+ console.error('Invalid response format:', rawData);
244
+ continue;
245
+ }
246
 
247
  setOllamaModels((current) =>
248
  current.map((m) =>
 
250
  ? {
251
  ...m,
252
  progress: {
253
+ current: rawData.completed || 0,
254
+ total: rawData.total || 0,
255
+ status: rawData.status,
256
  },
257
+ newDigest: rawData.digest,
258
  }
259
  : m,
260
  ),
 
277
  (enabled: boolean) => {
278
  setCategoryEnabled(enabled);
279
  filteredProviders.forEach((provider) => {
280
+ updateProviderSettings(provider.name, { ...provider.settings, enabled });
281
  });
282
+ success(enabled ? 'All local providers enabled' : 'All local providers disabled');
283
  },
284
+ [filteredProviders, updateProviderSettings, success],
285
  );
286
 
287
  const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
288
+ updateProviderSettings(provider.name, { ...provider.settings, enabled });
289
 
290
  if (enabled) {
291
  logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
292
+ success(`${provider.name} enabled`);
293
  } else {
294
  logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
295
+ success(`${provider.name} disabled`);
296
  }
297
  };
298
 
 
303
  newBaseUrl = undefined;
304
  }
305
 
306
+ updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
307
  logStore.logProvider(`Base URL updated for ${provider.name}`, {
308
  provider: provider.name,
309
  baseUrl: newBaseUrl,
310
  });
311
+ success(`${provider.name} base URL updated`);
312
  setEditingProvider(null);
313
  };
314
 
315
  const handleUpdateOllamaModel = async (modelName: string) => {
316
  setOllamaModels((current) => current.map((m) => (m.name === modelName ? { ...m, status: 'updating' } : m)));
317
 
318
+ const { success: updateSuccess, newDigest } = await updateOllamaModel(modelName);
319
 
320
  setOllamaModels((current) =>
321
  current.map((m) =>
322
  m.name === modelName
323
  ? {
324
  ...m,
325
+ status: updateSuccess ? 'updated' : 'error',
326
+ error: updateSuccess ? undefined : 'Update failed',
327
  newDigest,
328
  }
329
  : m,
330
  ),
331
  );
332
 
333
+ if (updateSuccess) {
334
+ success(`Updated ${modelName}`);
335
  } else {
336
+ error(`Failed to update ${modelName}`);
337
  }
338
  };
339
 
340
+ const handleDeleteOllamaModel = async (modelName: string) => {
341
+ try {
342
+ const response = await fetch(`${OLLAMA_API_URL}/api/delete`, {
343
+ method: 'DELETE',
344
+ headers: {
345
+ 'Content-Type': 'application/json',
346
+ },
347
+ body: JSON.stringify({ name: modelName }),
348
+ });
349
+
350
+ if (!response.ok) {
351
+ throw new Error(`Failed to delete ${modelName}`);
352
+ }
353
+
354
+ setOllamaModels((current) => current.filter((m) => m.name !== modelName));
355
+ success(`Deleted ${modelName}`);
356
+ } catch (err) {
357
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
358
+ console.error(`Error deleting ${modelName}:`, errorMessage);
359
+ error(`Failed to delete ${modelName}`);
360
+ }
361
+ };
362
+
363
+ // Health check function
364
+ const checkOllamaHealth = async () => {
365
+ try {
366
+ // Use the root endpoint instead of /api/health
367
+ const response = await fetch(OLLAMA_API_URL);
368
+ const text = await response.text();
369
+ const isRunning = text.includes('Ollama is running');
370
+
371
+ setServiceStatus({
372
+ isRunning,
373
+ lastChecked: new Date(),
374
+ });
375
+
376
+ if (isRunning) {
377
+ // If Ollama is running, fetch models
378
+ fetchOllamaModels();
379
+ }
380
+
381
+ return isRunning;
382
+ } catch (error) {
383
+ console.error('Health check error:', error);
384
+ setServiceStatus({
385
+ isRunning: false,
386
+ lastChecked: new Date(),
387
+ error: error instanceof Error ? error.message : 'Failed to connect to Ollama service',
388
+ });
389
+
390
+ return false;
391
+ }
392
+ };
393
+
394
+ // Update manual installation function
395
+ const handleManualInstall = async (modelString: string) => {
396
+ try {
397
+ setIsInstallingModel(modelString);
398
+ setInstallProgress({ model: modelString, progress: 0, status: 'Starting download...' });
399
+ setManualInstall((prev) => ({ ...prev, isOpen: false }));
400
+
401
+ const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
402
+ method: 'POST',
403
+ headers: {
404
+ 'Content-Type': 'application/json',
405
+ },
406
+ body: JSON.stringify({ name: modelString }),
407
+ });
408
+
409
+ if (!response.ok) {
410
+ throw new Error(`Failed to install ${modelString}`);
411
+ }
412
+
413
+ const reader = response.body?.getReader();
414
+
415
+ if (!reader) {
416
+ throw new Error('No response reader available');
417
+ }
418
+
419
+ while (true) {
420
+ const { done, value } = await reader.read();
421
+
422
+ if (done) {
423
+ break;
424
+ }
425
+
426
+ const text = new TextDecoder().decode(value);
427
+ const lines = text.split('\n').filter(Boolean);
428
+
429
+ for (const line of lines) {
430
+ const rawData = JSON.parse(line);
431
+
432
+ if (!isOllamaPullResponse(rawData)) {
433
+ console.error('Invalid response format:', rawData);
434
+ continue;
435
+ }
436
+
437
+ setInstallProgress({
438
+ model: modelString,
439
+ progress: rawData.completed && rawData.total ? (rawData.completed / rawData.total) * 100 : 0,
440
+ status: rawData.status,
441
+ });
442
+ }
443
+ }
444
+
445
+ success(`Successfully installed ${modelString}`);
446
+ await fetchOllamaModels();
447
+ } catch (err) {
448
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
449
+ console.error(`Error installing ${modelString}:`, errorMessage);
450
+ error(`Failed to install ${modelString}`);
451
+ } finally {
452
+ setIsInstallingModel(null);
453
+ setInstallProgress(null);
454
+ }
455
+ };
456
+
457
+ // Add health check effect
458
+ useEffect(() => {
459
+ const checkHealth = async () => {
460
+ const isHealthy = await checkOllamaHealth();
461
+
462
+ if (!isHealthy) {
463
+ error('Ollama service is not running. Please start the Ollama service.');
464
+ }
465
+ };
466
+
467
+ checkHealth();
468
+
469
+ const interval = setInterval(checkHealth, 50000);
470
+
471
+ // Check every 30 seconds
472
+ return () => clearInterval(interval);
473
+ }, []);
474
+
475
  return (
476
  <div className="space-y-6">
477
+ {/* Service Status Indicator - Move to top */}
478
+ <div
479
+ className={classNames(
480
+ 'flex items-center gap-2 p-2 rounded-lg',
481
+ serviceStatus.isRunning ? 'bg-green-500/10 text-green-500' : 'bg-red-500/10 text-red-500',
482
+ )}
483
+ >
484
+ <div className={classNames('w-2 h-2 rounded-full', serviceStatus.isRunning ? 'bg-green-500' : 'bg-red-500')} />
485
+ <span className="text-sm">
486
+ {serviceStatus.isRunning ? 'Ollama service is running' : 'Ollama service is not running'}
487
+ </span>
488
+ <span className="text-xs text-bolt-elements-textSecondary ml-2">
489
+ Last checked: {serviceStatus.lastChecked.toLocaleTimeString()}
490
+ </span>
491
+ </div>
492
+
493
  <motion.div
494
  className="space-y-4"
495
  initial={{ opacity: 0, y: 20 }}
 
723
  )}
724
  </div>
725
  </div>
726
+ <div className="flex items-center gap-2">
727
+ <motion.button
728
+ onClick={() => handleUpdateOllamaModel(model.name)}
729
+ disabled={model.status === 'updating'}
730
+ className={classNames(
731
+ settingsStyles.button.base,
732
+ settingsStyles.button.secondary,
733
+ 'hover:bg-purple-500/10 hover:text-purple-500',
734
+ )}
735
+ whileHover={{ scale: 1.02 }}
736
+ whileTap={{ scale: 0.98 }}
737
+ >
738
+ <div className="i-ph:arrows-clockwise" />
739
+ Update
740
+ </motion.button>
741
+ <motion.button
742
+ onClick={() => {
743
+ if (window.confirm(`Are you sure you want to delete ${model.name}?`)) {
744
+ handleDeleteOllamaModel(model.name);
745
+ }
746
+ }}
747
+ disabled={model.status === 'updating'}
748
+ className={classNames(
749
+ settingsStyles.button.base,
750
+ settingsStyles.button.secondary,
751
+ 'hover:bg-red-500/10 hover:text-red-500',
752
+ )}
753
+ whileHover={{ scale: 1.02 }}
754
+ whileTap={{ scale: 0.98 }}
755
+ >
756
+ <div className="i-ph:trash" />
757
+ Delete
758
+ </motion.button>
759
+ </div>
760
  </div>
761
  ))}
762
  </div>
 
775
  ))}
776
  </div>
777
  </motion.div>
778
+
779
+ {/* Manual Installation Section */}
780
+ {serviceStatus.isRunning && (
781
+ <div className="mt-8 space-y-4">
782
+ <div className="flex items-center justify-between">
783
+ <div>
784
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Install New Model</h3>
785
+ <p className="text-sm text-bolt-elements-textSecondary">
786
+ Enter the model name exactly as shown (e.g., deepseek-r1:1.5b)
787
+ </p>
788
+ </div>
789
+ </div>
790
+
791
+ {/* Model Information Section */}
792
+ <div className="p-4 rounded-lg bg-bolt-elements-background-depth-2 space-y-3">
793
+ <div className="flex items-center gap-2 text-bolt-elements-textPrimary">
794
+ <div className="i-ph:info text-purple-500" />
795
+ <span className="font-medium">Where to find models?</span>
796
+ </div>
797
+ <div className="space-y-2 text-sm text-bolt-elements-textSecondary">
798
+ <p>
799
+ Browse available models at{' '}
800
+ <a
801
+ href="https://ollama.com/library"
802
+ target="_blank"
803
+ rel="noopener noreferrer"
804
+ className="text-purple-500 hover:underline"
805
+ >
806
+ ollama.com/library
807
+ </a>
808
+ </p>
809
+ <div className="space-y-1">
810
+ <p className="font-medium text-bolt-elements-textPrimary">Popular models:</p>
811
+ <ul className="list-disc list-inside space-y-1 ml-2">
812
+ <li>deepseek-r1:1.5b - DeepSeek's reasoning model</li>
813
+ <li>llama3:8b - Meta's Llama 3 (8B parameters)</li>
814
+ <li>mistral:7b - Mistral's 7B model</li>
815
+ <li>gemma:2b - Google's Gemma model</li>
816
+ <li>qwen2:7b - Alibaba's Qwen2 model</li>
817
+ </ul>
818
+ </div>
819
+ <p className="mt-2">
820
+ <span className="text-yellow-500">Note:</span> Copy the exact model name including the tag (e.g.,
821
+ 'deepseek-r1:1.5b') from the library to ensure successful installation.
822
+ </p>
823
+ </div>
824
+ </div>
825
+
826
+ <div className="flex gap-4">
827
+ <div className="flex-1">
828
+ <input
829
+ type="text"
830
+ className="w-full px-3 py-2 rounded-md bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor text-bolt-elements-textPrimary"
831
+ placeholder="deepseek-r1:1.5b"
832
+ value={manualInstall.modelString}
833
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
834
+ setManualInstall((prev) => ({ ...prev, modelString: e.target.value }))
835
+ }
836
+ />
837
+ </div>
838
+ <motion.button
839
+ onClick={() => handleManualInstall(manualInstall.modelString)}
840
+ disabled={!manualInstall.modelString || !!isInstallingModel}
841
+ className={classNames(
842
+ settingsStyles.button.base,
843
+ settingsStyles.button.primary,
844
+ 'hover:bg-purple-500/10 hover:text-purple-500',
845
+ 'min-w-[120px] justify-center',
846
+ )}
847
+ whileHover={{ scale: 1.02 }}
848
+ whileTap={{ scale: 0.98 }}
849
+ >
850
+ {isInstallingModel ? (
851
+ <div className="flex items-center justify-center gap-2">
852
+ <div className="i-ph:spinner-gap-bold animate-spin" />
853
+ Installing...
854
+ </div>
855
+ ) : (
856
+ <>
857
+ <div className="i-ph:download" />
858
+ Install Model
859
+ </>
860
+ )}
861
+ </motion.button>
862
+ {isInstallingModel && (
863
+ <motion.button
864
+ onClick={() => {
865
+ setIsInstallingModel(null);
866
+ setInstallProgress(null);
867
+ error('Installation cancelled');
868
+ }}
869
+ className={classNames(
870
+ settingsStyles.button.base,
871
+ settingsStyles.button.secondary,
872
+ 'hover:bg-red-500/10 hover:text-red-500',
873
+ 'min-w-[100px] justify-center',
874
+ )}
875
+ whileHover={{ scale: 1.02 }}
876
+ whileTap={{ scale: 0.98 }}
877
+ >
878
+ <div className="i-ph:x" />
879
+ Cancel
880
+ </motion.button>
881
+ )}
882
+ </div>
883
+
884
+ {installProgress && (
885
+ <div className="mt-2 space-y-2">
886
+ <div className="flex items-center justify-between text-sm text-bolt-elements-textSecondary">
887
+ <span>{installProgress.status}</span>
888
+ <span>{Math.round(installProgress.progress)}%</span>
889
+ </div>
890
+ <div className="w-full h-2 bg-bolt-elements-background-depth-3 rounded-full overflow-hidden">
891
+ <div
892
+ className="h-full bg-purple-500 transition-all duration-200"
893
+ style={{ width: `${installProgress.progress}%` }}
894
+ />
895
+ </div>
896
+ </div>
897
+ )}
898
+ </div>
899
+ )}
900
  </div>
901
  );
902
+ }
903
 
904
  export default LocalProvidersTab;
app/components/settings/settings/SettingsTab.tsx CHANGED
@@ -229,19 +229,42 @@ export default function SettingsTab() {
229
 
230
  <div className="space-y-2">
231
  {Object.entries(useStore(shortcutsStore)).map(([name, shortcut]) => (
232
- <div key={name} className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A]">
 
 
 
233
  <span className="text-sm text-bolt-elements-textPrimary capitalize">
234
  {name.replace(/([A-Z])/g, ' $1').toLowerCase()}
235
  </span>
236
  <div className="flex items-center gap-1">
237
  {shortcut.ctrlOrMetaKey && (
238
- <kbd className="kdb">{navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}</kbd>
 
 
239
  )}
240
- {shortcut.ctrlKey && <kbd className="kdb">Ctrl</kbd>}
241
- {shortcut.metaKey && <kbd className="kdb">⌘</kbd>}
242
- {shortcut.shiftKey && <kbd className="kdb">⇧</kbd>}
243
- {shortcut.altKey && <kbd className="kdb">⌥</kbd>}
244
- <kbd className="kdb">{shortcut.key.toUpperCase()}</kbd>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  </div>
246
  </div>
247
  ))}
 
229
 
230
  <div className="space-y-2">
231
  {Object.entries(useStore(shortcutsStore)).map(([name, shortcut]) => (
232
+ <div
233
+ key={name}
234
+ className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A] hover:bg-purple-50 dark:hover:bg-purple-500/10 transition-colors"
235
+ >
236
  <span className="text-sm text-bolt-elements-textPrimary capitalize">
237
  {name.replace(/([A-Z])/g, ' $1').toLowerCase()}
238
  </span>
239
  <div className="flex items-center gap-1">
240
  {shortcut.ctrlOrMetaKey && (
241
+ <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">
242
+ {navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}
243
+ </kbd>
244
  )}
245
+ {shortcut.ctrlKey && (
246
+ <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">
247
+ Ctrl
248
+ </kbd>
249
+ )}
250
+ {shortcut.metaKey && (
251
+ <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">
252
+
253
+ </kbd>
254
+ )}
255
+ {shortcut.altKey && (
256
+ <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">
257
+ {navigator.platform.includes('Mac') ? '⌥' : 'Alt'}
258
+ </kbd>
259
+ )}
260
+ {shortcut.shiftKey && (
261
+ <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">
262
+
263
+ </kbd>
264
+ )}
265
+ <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">
266
+ {shortcut.key.toUpperCase()}
267
+ </kbd>
268
  </div>
269
  </div>
270
  ))}
app/components/settings/task-manager/TaskManagerTab.tsx CHANGED
@@ -309,7 +309,7 @@ export default function TaskManagerTab() {
309
  try {
310
  setLoading((prev) => ({ ...prev, metrics: true }));
311
 
312
- // Get memory info
313
  const memory = performance.memory || {
314
  jsHeapSizeLimit: 0,
315
  totalJSHeapSize: 0,
@@ -319,6 +319,9 @@ export default function TaskManagerTab() {
319
  const usedMem = memory.usedJSHeapSize / (1024 * 1024);
320
  const memPercentage = (usedMem / totalMem) * 100;
321
 
 
 
 
322
  // Get battery info
323
  let batteryInfo: SystemMetrics['battery'] | undefined;
324
 
@@ -333,7 +336,7 @@ export default function TaskManagerTab() {
333
  console.log('Battery API not available');
334
  }
335
 
336
- // Get network info
337
  const connection =
338
  (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection;
339
  const networkInfo = {
@@ -343,13 +346,13 @@ export default function TaskManagerTab() {
343
  };
344
 
345
  const newMetrics = {
346
- cpu: Math.random() * 100,
347
  memory: {
348
  used: Math.round(usedMem),
349
  total: Math.round(totalMem),
350
  percentage: Math.round(memPercentage),
351
  },
352
- activeProcesses: document.querySelectorAll('[data-process]').length,
353
  uptime: performance.now() / 1000,
354
  battery: batteryInfo,
355
  network: networkInfo,
@@ -375,60 +378,111 @@ export default function TaskManagerTab() {
375
  }
376
  };
377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  const updateProcesses = async () => {
379
  try {
380
  setLoading((prev) => ({ ...prev, processes: true }));
381
 
382
- // Enhanced process monitoring
383
- const mockProcesses: ProcessInfo[] = [
384
- {
385
- name: 'Ollama Model Updates',
 
 
 
 
 
 
 
 
386
  type: 'Network',
387
- cpuUsage: Math.random() * 5,
388
- memoryUsage: Math.random() * 50,
389
- status: 'idle',
390
- lastUpdate: new Date().toISOString(),
391
- impact: 'high',
392
- },
393
- {
394
- name: 'UI Animations',
395
- type: 'Animation',
396
- cpuUsage: Math.random() * 3,
397
- memoryUsage: Math.random() * 30,
398
- status: 'idle',
399
  lastUpdate: new Date().toISOString(),
400
- impact: 'medium',
401
- },
402
- {
403
- name: 'Background Sync',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  type: 'Background',
405
- cpuUsage: Math.random() * 2,
406
- memoryUsage: Math.random() * 20,
407
- status: 'idle',
408
  lastUpdate: new Date().toISOString(),
409
  impact: 'low',
410
- },
411
- {
412
- name: 'IndexedDB Operations',
413
- type: 'Storage',
414
- cpuUsage: Math.random() * 1,
415
- memoryUsage: Math.random() * 15,
416
- status: 'idle',
417
- lastUpdate: new Date().toISOString(),
418
- impact: 'low',
419
- },
420
- {
421
- name: 'WebSocket Connection',
422
- type: 'Network',
423
- cpuUsage: Math.random() * 2,
424
- memoryUsage: Math.random() * 10,
425
- status: 'idle',
426
- lastUpdate: new Date().toISOString(),
427
- impact: 'medium',
428
- },
429
- ];
430
 
431
- setProcesses(mockProcesses);
432
  } catch (error) {
433
  console.error('Failed to update process list:', error);
434
  } finally {
 
309
  try {
310
  setLoading((prev) => ({ ...prev, metrics: true }));
311
 
312
+ // Get memory info using Performance API
313
  const memory = performance.memory || {
314
  jsHeapSizeLimit: 0,
315
  totalJSHeapSize: 0,
 
319
  const usedMem = memory.usedJSHeapSize / (1024 * 1024);
320
  const memPercentage = (usedMem / totalMem) * 100;
321
 
322
+ // Get CPU usage using Performance API
323
+ const cpuUsage = await getCPUUsage();
324
+
325
  // Get battery info
326
  let batteryInfo: SystemMetrics['battery'] | undefined;
327
 
 
336
  console.log('Battery API not available');
337
  }
338
 
339
+ // Get network info using Network Information API
340
  const connection =
341
  (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection;
342
  const networkInfo = {
 
346
  };
347
 
348
  const newMetrics = {
349
+ cpu: cpuUsage,
350
  memory: {
351
  used: Math.round(usedMem),
352
  total: Math.round(totalMem),
353
  percentage: Math.round(memPercentage),
354
  },
355
+ activeProcesses: await getActiveProcessCount(),
356
  uptime: performance.now() / 1000,
357
  battery: batteryInfo,
358
  network: networkInfo,
 
378
  }
379
  };
380
 
381
+ // Get real CPU usage using Performance API
382
+ const getCPUUsage = async (): Promise<number> => {
383
+ try {
384
+ const t0 = performance.now();
385
+ const startEntries = performance.getEntriesByType('measure');
386
+
387
+ // Wait a short time to measure CPU usage
388
+ await new Promise((resolve) => setTimeout(resolve, 100));
389
+
390
+ const t1 = performance.now();
391
+ const endEntries = performance.getEntriesByType('measure');
392
+
393
+ // Calculate CPU usage based on the number of performance entries
394
+ const entriesPerMs = (endEntries.length - startEntries.length) / (t1 - t0);
395
+
396
+ // Normalize to percentage (0-100)
397
+ return Math.min(100, entriesPerMs * 1000);
398
+ } catch (error) {
399
+ console.error('Failed to get CPU usage:', error);
400
+ return 0;
401
+ }
402
+ };
403
+
404
+ // Get real active process count
405
+ const getActiveProcessCount = async (): Promise<number> => {
406
+ try {
407
+ // Count active network connections
408
+ const networkCount = (navigator as any)?.connections?.length || 0;
409
+
410
+ // Count active service workers
411
+ const swCount = (await navigator.serviceWorker?.getRegistrations().then((regs) => regs.length)) || 0;
412
+
413
+ // Count active animations
414
+ const animationCount = document.getAnimations().length;
415
+
416
+ // Count active fetch requests
417
+ const fetchCount = performance
418
+ .getEntriesByType('resource')
419
+ .filter(
420
+ (entry) => (entry as PerformanceResourceTiming).initiatorType === 'fetch' && entry.duration === 0,
421
+ ).length;
422
+
423
+ return networkCount + swCount + animationCount + fetchCount;
424
+ } catch (error) {
425
+ console.error('Failed to get active process count:', error);
426
+ return 0;
427
+ }
428
+ };
429
+
430
  const updateProcesses = async () => {
431
  try {
432
  setLoading((prev) => ({ ...prev, processes: true }));
433
 
434
+ // Get real process information
435
+ const processes: ProcessInfo[] = [];
436
+
437
+ // Add network processes
438
+ const networkEntries = performance
439
+ .getEntriesByType('resource')
440
+ .filter((entry) => (entry as PerformanceResourceTiming).initiatorType === 'fetch' && entry.duration === 0)
441
+ .slice(-5); // Get last 5 active requests
442
+
443
+ networkEntries.forEach((entry) => {
444
+ processes.push({
445
+ name: `Network Request: ${new URL((entry as PerformanceResourceTiming).name).pathname}`,
446
  type: 'Network',
447
+ cpuUsage: entry.duration > 0 ? entry.duration / 100 : 0,
448
+ memoryUsage: (entry as PerformanceResourceTiming).encodedBodySize / (1024 * 1024), // Convert to MB
449
+ status: entry.duration === 0 ? 'active' : 'idle',
 
 
 
 
 
 
 
 
 
450
  lastUpdate: new Date().toISOString(),
451
+ impact: entry.duration > 1000 ? 'high' : entry.duration > 500 ? 'medium' : 'low',
452
+ });
453
+ });
454
+
455
+ // Add animation processes
456
+ document
457
+ .getAnimations()
458
+ .slice(0, 5)
459
+ .forEach((animation) => {
460
+ processes.push({
461
+ name: `Animation: ${animation.id || 'Unnamed'}`,
462
+ type: 'Animation',
463
+ cpuUsage: animation.playState === 'running' ? 2 : 0,
464
+ memoryUsage: 1, // Approximate memory usage
465
+ status: animation.playState === 'running' ? 'active' : 'idle',
466
+ lastUpdate: new Date().toISOString(),
467
+ impact: 'low',
468
+ });
469
+ });
470
+
471
+ // Add service worker processes
472
+ const serviceWorkers = (await navigator.serviceWorker?.getRegistrations()) || [];
473
+ serviceWorkers.forEach((sw) => {
474
+ processes.push({
475
+ name: `Service Worker: ${sw.scope}`,
476
  type: 'Background',
477
+ cpuUsage: sw.active ? 1 : 0,
478
+ memoryUsage: 5, // Approximate memory usage
479
+ status: sw.active ? 'active' : 'idle',
480
  lastUpdate: new Date().toISOString(),
481
  impact: 'low',
482
+ });
483
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
 
485
+ setProcesses(processes);
486
  } catch (error) {
487
  console.error('Failed to update process list:', error);
488
  } finally {
app/components/settings/user/UsersWindow.tsx CHANGED
@@ -379,156 +379,6 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
379
  }
380
  };
381
 
382
- const renderHeader = () => (
383
- <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
384
- <div className="flex items-center space-x-4">
385
- {activeTab ? (
386
- <button
387
- onClick={handleBack}
388
- className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
389
- >
390
- <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
391
- </button>
392
- ) : (
393
- <motion.div
394
- className="i-ph:lightning-fill w-5 h-5 text-purple-500"
395
- initial={{ rotate: -10 }}
396
- animate={{ rotate: 10 }}
397
- transition={{
398
- repeat: Infinity,
399
- repeatType: 'reverse',
400
- duration: 2,
401
- ease: 'easeInOut',
402
- }}
403
- />
404
- )}
405
- <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
406
- {activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
407
- </DialogTitle>
408
- </div>
409
-
410
- <div className="flex items-center space-x-4">
411
- <div className="flex items-center gap-2">
412
- <Switch
413
- checked={developerMode}
414
- onCheckedChange={handleDeveloperModeChange}
415
- className="data-[state=checked]:bg-purple-500"
416
- aria-label="Toggle developer mode"
417
- />
418
- <label className="text-sm text-gray-500 dark:text-gray-400">Switch to Developer Mode</label>
419
- </div>
420
-
421
- <DropdownMenu.Root>
422
- <DropdownMenu.Trigger asChild>
423
- <button className="flex items-center justify-center w-8 h-8 rounded-full overflow-hidden hover:ring-2 ring-gray-300 dark:ring-gray-600 transition-all">
424
- {profile.avatar ? (
425
- <img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
426
- ) : (
427
- <div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
428
- <svg
429
- className="w-5 h-5 text-gray-500 dark:text-gray-400"
430
- fill="none"
431
- stroke="currentColor"
432
- viewBox="0 0 24 24"
433
- >
434
- <path
435
- strokeLinecap="round"
436
- strokeLinejoin="round"
437
- strokeWidth={2}
438
- d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
439
- />
440
- </svg>
441
- </div>
442
- )}
443
- </button>
444
- </DropdownMenu.Trigger>
445
-
446
- <DropdownMenu.Portal>
447
- <DropdownMenu.Content
448
- className="min-w-[220px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-50 animate-in fade-in-0 zoom-in-95"
449
- sideOffset={5}
450
- align="end"
451
- >
452
- <DropdownMenu.Item
453
- className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
454
- onSelect={() => handleTabClick('profile')}
455
- >
456
- <div className="mr-3 flex h-5 w-5 items-center justify-center">
457
- <div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
458
- </div>
459
- <span className="group-hover:text-purple-500 transition-colors">Profile</span>
460
- </DropdownMenu.Item>
461
-
462
- <DropdownMenu.Item
463
- className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
464
- onSelect={() => handleTabClick('settings')}
465
- >
466
- <div className="mr-3 flex h-5 w-5 items-center justify-center">
467
- <div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
468
- </div>
469
- <span className="group-hover:text-purple-500 transition-colors">Settings</span>
470
- </DropdownMenu.Item>
471
-
472
- {profile.notifications && (
473
- <>
474
- <DropdownMenu.Item
475
- className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
476
- onSelect={() => handleTabClick('notifications')}
477
- >
478
- <div className="mr-3 flex h-5 w-5 items-center justify-center">
479
- <div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
480
- </div>
481
- <span className="group-hover:text-purple-500 transition-colors">
482
- Notifications
483
- {hasUnreadNotifications && (
484
- <span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
485
- {unreadNotifications.length}
486
- </span>
487
- )}
488
- </span>
489
- </DropdownMenu.Item>
490
-
491
- <DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
492
- </>
493
- )}
494
-
495
- <DropdownMenu.Item
496
- className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
497
- onSelect={onClose}
498
- >
499
- <div className="mr-3 flex h-5 w-5 items-center justify-center">
500
- <div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
501
- </div>
502
- <span className="group-hover:text-purple-500 transition-colors">Close</span>
503
- </DropdownMenu.Item>
504
- </DropdownMenu.Content>
505
- </DropdownMenu.Portal>
506
- </DropdownMenu.Root>
507
-
508
- <button
509
- onClick={onClose}
510
- className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
511
- >
512
- <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
513
- </button>
514
- </div>
515
- </div>
516
- );
517
-
518
- // Trap focus when window is open
519
- useEffect(() => {
520
- if (open) {
521
- // Prevent background scrolling
522
- document.body.style.overflow = 'hidden';
523
- } else {
524
- document.body.style.overflow = 'unset';
525
- }
526
-
527
- return () => {
528
- document.body.style.overflow = 'unset';
529
- };
530
- }, [open]);
531
-
532
  return (
533
  <>
534
  <DeveloperWindow open={showDeveloperWindow} onClose={handleDeveloperWindowClose} />
@@ -567,7 +417,139 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
567
  transition={{ duration: 0.2 }}
568
  >
569
  {/* Header */}
570
- {renderHeader()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
  {/* Content */}
573
  <div
 
379
  }
380
  };
381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  return (
383
  <>
384
  <DeveloperWindow open={showDeveloperWindow} onClose={handleDeveloperWindowClose} />
 
417
  transition={{ duration: 0.2 }}
418
  >
419
  {/* Header */}
420
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
421
+ <div className="flex items-center space-x-4">
422
+ {activeTab ? (
423
+ <button
424
+ onClick={handleBack}
425
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
426
+ >
427
+ <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
428
+ </button>
429
+ ) : (
430
+ <motion.div
431
+ className="i-ph:lightning-fill w-5 h-5 text-purple-500"
432
+ initial={{ rotate: -10 }}
433
+ animate={{ rotate: 10 }}
434
+ transition={{
435
+ repeat: Infinity,
436
+ repeatType: 'reverse',
437
+ duration: 2,
438
+ ease: 'easeInOut',
439
+ }}
440
+ />
441
+ )}
442
+ <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
443
+ {activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
444
+ </DialogTitle>
445
+ </div>
446
+
447
+ <div className="flex items-center space-x-4">
448
+ <div className="flex items-center gap-2">
449
+ <Switch
450
+ checked={developerMode}
451
+ onCheckedChange={handleDeveloperModeChange}
452
+ className="data-[state=checked]:bg-purple-500"
453
+ aria-label="Toggle developer mode"
454
+ />
455
+ <label className="text-sm text-gray-500 dark:text-gray-400">Switch to Developer Mode</label>
456
+ </div>
457
+
458
+ <DropdownMenu.Root>
459
+ <DropdownMenu.Trigger asChild>
460
+ <button className="flex items-center justify-center w-8 h-8 rounded-full overflow-hidden hover:ring-2 ring-gray-300 dark:ring-gray-600 transition-all">
461
+ {profile.avatar ? (
462
+ <img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
463
+ ) : (
464
+ <div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
465
+ <svg
466
+ className="w-5 h-5 text-gray-500 dark:text-gray-400"
467
+ fill="none"
468
+ stroke="currentColor"
469
+ viewBox="0 0 24 24"
470
+ >
471
+ <path
472
+ strokeLinecap="round"
473
+ strokeLinejoin="round"
474
+ strokeWidth={2}
475
+ d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
476
+ />
477
+ </svg>
478
+ </div>
479
+ )}
480
+ </button>
481
+ </DropdownMenu.Trigger>
482
+
483
+ <DropdownMenu.Portal>
484
+ <DropdownMenu.Content
485
+ className="min-w-[220px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-[200] animate-in fade-in-0 zoom-in-95"
486
+ sideOffset={5}
487
+ align="end"
488
+ >
489
+ <DropdownMenu.Item
490
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
491
+ onSelect={() => handleTabClick('profile')}
492
+ >
493
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
494
+ <div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
495
+ </div>
496
+ <span className="group-hover:text-purple-500 transition-colors">Profile</span>
497
+ </DropdownMenu.Item>
498
+
499
+ <DropdownMenu.Item
500
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
501
+ onSelect={() => handleTabClick('settings')}
502
+ >
503
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
504
+ <div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
505
+ </div>
506
+ <span className="group-hover:text-purple-500 transition-colors">Settings</span>
507
+ </DropdownMenu.Item>
508
+
509
+ {profile.notifications && (
510
+ <>
511
+ <DropdownMenu.Item
512
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
513
+ onSelect={() => handleTabClick('notifications')}
514
+ >
515
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
516
+ <div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
517
+ </div>
518
+ <span className="group-hover:text-purple-500 transition-colors">
519
+ Notifications
520
+ {hasUnreadNotifications && (
521
+ <span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
522
+ {unreadNotifications.length}
523
+ </span>
524
+ )}
525
+ </span>
526
+ </DropdownMenu.Item>
527
+
528
+ <DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
529
+ </>
530
+ )}
531
+
532
+ <DropdownMenu.Item
533
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
534
+ onSelect={onClose}
535
+ >
536
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
537
+ <div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
538
+ </div>
539
+ <span className="group-hover:text-purple-500 transition-colors">Close</span>
540
+ </DropdownMenu.Item>
541
+ </DropdownMenu.Content>
542
+ </DropdownMenu.Portal>
543
+ </DropdownMenu.Root>
544
+
545
+ <button
546
+ onClick={onClose}
547
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
548
+ >
549
+ <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
550
+ </button>
551
+ </div>
552
+ </div>
553
 
554
  {/* Content */}
555
  <div
app/components/ui/Card.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '~/lib/utils';
3
+
4
+ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
5
+
6
+ const Card = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
7
+ return (
8
+ <div
9
+ ref={ref}
10
+ className={cn(
11
+ 'rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary shadow-sm',
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ });
18
+ Card.displayName = 'Card';
19
+
20
+ const CardHeader = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
21
+ return <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />;
22
+ });
23
+ CardHeader.displayName = 'CardHeader';
24
+
25
+ const CardTitle = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
26
+ ({ className, ...props }, ref) => {
27
+ return <h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />;
28
+ },
29
+ );
30
+ CardTitle.displayName = 'CardTitle';
31
+
32
+ const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
33
+ ({ className, ...props }, ref) => {
34
+ return <p ref={ref} className={cn('text-sm text-bolt-elements-textSecondary', className)} {...props} />;
35
+ },
36
+ );
37
+ CardDescription.displayName = 'CardDescription';
38
+
39
+ const CardContent = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
40
+ return <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />;
41
+ });
42
+ CardContent.displayName = 'CardContent';
43
+
44
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent };
app/components/ui/Input.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '~/lib/utils';
3
+
4
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
5
+
6
+ const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ 'flex h-10 w-full rounded-md border border-bolt-elements-borderColor bg-bolt-elements-background-depth-1 px-3 py-2 text-sm',
12
+ 'ring-offset-bolt-elements-background-depth-1 file:border-0 file:bg-transparent file:text-sm file:font-medium',
13
+ 'placeholder:text-bolt-elements-textTertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500/30',
14
+ 'focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
15
+ className,
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ );
21
+ });
22
+
23
+ Input.displayName = 'Input';
24
+
25
+ export { Input };
app/components/ui/Label.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '~/lib/utils';
3
+
4
+ export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
5
+
6
+ const Label = forwardRef<HTMLLabelElement, LabelProps>(({ className, ...props }, ref) => {
7
+ return (
8
+ <label
9
+ ref={ref}
10
+ className={cn(
11
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
12
+ 'text-bolt-elements-textPrimary',
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ );
18
+ });
19
+
20
+ Label.displayName = 'Label';
21
+
22
+ export { Label };
app/components/ui/Tabs.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as TabsPrimitive from '@radix-ui/react-tabs';
2
+ import { forwardRef } from 'react';
3
+ import { cn } from '~/lib/utils';
4
+
5
+ const Tabs = TabsPrimitive.Root;
6
+
7
+ const TabsList = forwardRef<
8
+ React.ElementRef<typeof TabsPrimitive.List>,
9
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
10
+ >(({ className, ...props }, ref) => (
11
+ <TabsPrimitive.List
12
+ ref={ref}
13
+ className={cn(
14
+ 'inline-flex h-10 items-center justify-center rounded-md bg-bolt-elements-background-depth-2 p-1',
15
+ 'text-bolt-elements-textSecondary',
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ ));
21
+ TabsList.displayName = TabsPrimitive.List.displayName;
22
+
23
+ const TabsTrigger = forwardRef<
24
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
26
+ >(({ className, ...props }, ref) => (
27
+ <TabsPrimitive.Trigger
28
+ ref={ref}
29
+ className={cn(
30
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-bolt-elements-background-depth-1',
31
+ 'transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500/30 focus-visible:ring-offset-2',
32
+ 'disabled:pointer-events-none disabled:opacity-50',
33
+ 'data-[state=active]:bg-bolt-elements-background-depth-1 data-[state=active]:text-bolt-elements-textPrimary data-[state=active]:shadow-sm',
34
+ className,
35
+ )}
36
+ {...props}
37
+ />
38
+ ));
39
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
40
+
41
+ const TabsContent = forwardRef<
42
+ React.ElementRef<typeof TabsPrimitive.Content>,
43
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
44
+ >(({ className, ...props }, ref) => (
45
+ <TabsPrimitive.Content
46
+ ref={ref}
47
+ className={cn(
48
+ 'mt-2 ring-offset-bolt-elements-background-depth-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500/30 focus-visible:ring-offset-2',
49
+ className,
50
+ )}
51
+ {...props}
52
+ />
53
+ ));
54
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
55
+
56
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
app/components/ui/use-toast.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback } from 'react';
2
+ import { toast as toastify } from 'react-toastify';
3
+
4
+ interface ToastOptions {
5
+ type?: 'success' | 'error' | 'info' | 'warning';
6
+ duration?: number;
7
+ }
8
+
9
+ export function useToast() {
10
+ const toast = useCallback((message: string, options: ToastOptions = {}) => {
11
+ const { type = 'info', duration = 3000 } = options;
12
+
13
+ toastify[type](message, {
14
+ position: 'bottom-right',
15
+ autoClose: duration,
16
+ hideProgressBar: false,
17
+ closeOnClick: true,
18
+ pauseOnHover: true,
19
+ draggable: true,
20
+ progress: undefined,
21
+ theme: 'dark',
22
+ });
23
+ }, []);
24
+
25
+ const success = useCallback(
26
+ (message: string, options: Omit<ToastOptions, 'type'> = {}) => {
27
+ toast(message, { ...options, type: 'success' });
28
+ },
29
+ [toast],
30
+ );
31
+
32
+ const error = useCallback(
33
+ (message: string, options: Omit<ToastOptions, 'type'> = {}) => {
34
+ toast(message, { ...options, type: 'error' });
35
+ },
36
+ [toast],
37
+ );
38
+
39
+ return { toast, success, error };
40
+ }
app/lib/.server/llm/select-context.ts CHANGED
@@ -126,7 +126,7 @@ export async function selectContext(props: {
126
  ---
127
  ${filePaths.map((path) => `- ${path}`).join('\n')}
128
  ---
129
-
130
  You have following code loaded in the context buffer that you can refer to:
131
 
132
  CURRENT CONTEXT BUFFER
@@ -137,14 +137,14 @@ export async function selectContext(props: {
137
  Now, you are given a task. You need to select the files that are relevant to the task from the list of files above.
138
 
139
  RESPONSE FORMAT:
140
- your response shoudl be in following format:
141
  ---
142
  <updateContextBuffer>
143
  <includeFile path="path/to/file"/>
144
  <excludeFile path="path/to/file"/>
145
  </updateContextBuffer>
146
  ---
147
- * Your should start with <updateContextBuffer> and end with </updateContextBuffer>.
148
  * You can include multiple <includeFile> and <excludeFile> tags in the response.
149
  * You should not include any other text in the response.
150
  * You should not include any file that is not in the list of files above.
 
126
  ---
127
  ${filePaths.map((path) => `- ${path}`).join('\n')}
128
  ---
129
+
130
  You have following code loaded in the context buffer that you can refer to:
131
 
132
  CURRENT CONTEXT BUFFER
 
137
  Now, you are given a task. You need to select the files that are relevant to the task from the list of files above.
138
 
139
  RESPONSE FORMAT:
140
+ your response should be in following format:
141
  ---
142
  <updateContextBuffer>
143
  <includeFile path="path/to/file"/>
144
  <excludeFile path="path/to/file"/>
145
  </updateContextBuffer>
146
  ---
147
+ * Your should start with <updateContextBuffer> and end with </updateContextBuffer>.
148
  * You can include multiple <includeFile> and <excludeFile> tags in the response.
149
  * You should not include any other text in the response.
150
  * You should not include any file that is not in the list of files above.
app/lib/hooks/useLocalProviders.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useState } from 'react';
2
+ import type { IProviderConfig } from '~/types/model';
3
+
4
+ export interface UseLocalProvidersReturn {
5
+ localProviders: IProviderConfig[];
6
+ refreshLocalProviders: () => void;
7
+ }
8
+
9
+ export function useLocalProviders(): UseLocalProvidersReturn {
10
+ const [localProviders, setLocalProviders] = useState<IProviderConfig[]>([]);
11
+
12
+ const refreshLocalProviders = useCallback(() => {
13
+ /*
14
+ * Refresh logic for local providers
15
+ * This would typically involve checking the status of Ollama and LMStudio
16
+ * For now, we'll just return an empty array
17
+ */
18
+ setLocalProviders([]);
19
+ }, []);
20
+
21
+ return {
22
+ localProviders,
23
+ refreshLocalProviders,
24
+ };
25
+ }
app/lib/stores/logs.ts CHANGED
@@ -10,24 +10,43 @@ export interface LogEntry {
10
  level: 'info' | 'warning' | 'error' | 'debug';
11
  message: string;
12
  details?: Record<string, any>;
13
- category: 'system' | 'provider' | 'user' | 'error' | 'api' | 'auth' | 'database' | 'network' | 'performance';
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  subCategory?: string;
15
  duration?: number;
16
  statusCode?: number;
17
  source?: string;
18
  stack?: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
21
  const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
22
 
23
  class LogStore {
24
- logInfo(message: string, details: { type: string; message: string }) {
25
- return this.addLog(message, 'info', 'system', details);
26
- }
27
-
28
- logSuccess(message: string, details: { type: string; message: string }) {
29
- return this.addLog(message, 'info', 'system', { ...details, success: true });
30
- }
31
  private _logs = map<Record<string, LogEntry>>({});
32
  showLogs = atom(true);
33
  private _readLogs = new Set<string>();
@@ -106,13 +125,13 @@ class LogStore {
106
  }
107
  }
108
 
109
- addLog(
 
110
  message: string,
111
- level: LogEntry['level'] = 'info',
112
- category: LogEntry['category'] = 'system',
113
  details?: Record<string, any>,
114
- statusCode?: number,
115
- duration?: number,
116
  ) {
117
  const id = this._generateId();
118
  const entry: LogEntry = {
@@ -122,8 +141,7 @@ class LogStore {
122
  message,
123
  details,
124
  category,
125
- statusCode,
126
- duration,
127
  };
128
 
129
  this._logs.setKey(id, entry);
@@ -133,19 +151,40 @@ class LogStore {
133
  return id;
134
  }
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  // System events
137
  logSystem(message: string, details?: Record<string, any>) {
138
- return this.addLog(message, 'info', 'system', details);
139
  }
140
 
141
  // Provider events
142
  logProvider(message: string, details?: Record<string, any>) {
143
- return this.addLog(message, 'info', 'provider', details);
144
  }
145
 
146
  // User actions
147
  logUserAction(message: string, details?: Record<string, any>) {
148
- return this.addLog(message, 'info', 'user', details);
149
  }
150
 
151
  // API Connection Logging
@@ -153,7 +192,7 @@ class LogStore {
153
  const message = `${method} ${endpoint} - ${statusCode} (${duration}ms)`;
154
  const level = statusCode >= 400 ? 'error' : statusCode >= 300 ? 'warning' : 'info';
155
 
156
- return this.addLog(message, level, 'api', {
157
  ...details,
158
  endpoint,
159
  method,
@@ -172,7 +211,7 @@ class LogStore {
172
  const message = `Auth ${action} - ${success ? 'Success' : 'Failed'}`;
173
  const level = success ? 'info' : 'error';
174
 
175
- return this.addLog(message, level, 'auth', {
176
  ...details,
177
  action,
178
  success,
@@ -185,7 +224,7 @@ class LogStore {
185
  const message = `Network ${status}`;
186
  const level = status === 'offline' ? 'error' : status === 'reconnecting' ? 'warning' : 'info';
187
 
188
- return this.addLog(message, level, 'network', {
189
  ...details,
190
  status,
191
  timestamp: new Date().toISOString(),
@@ -197,7 +236,7 @@ class LogStore {
197
  const message = `DB ${operation} - ${success ? 'Success' : 'Failed'} (${duration}ms)`;
198
  const level = success ? 'info' : 'error';
199
 
200
- return this.addLog(message, level, 'database', {
201
  ...details,
202
  operation,
203
  success,
@@ -218,17 +257,17 @@ class LogStore {
218
  }
219
  : { error, ...details };
220
 
221
- return this.addLog(message, 'error', 'error', errorDetails);
222
  }
223
 
224
  // Warning events
225
  logWarning(message: string, details?: Record<string, any>) {
226
- return this.addLog(message, 'warning', 'system', details);
227
  }
228
 
229
  // Debug events
230
  logDebug(message: string, details?: Record<string, any>) {
231
- return this.addLog(message, 'debug', 'system', details);
232
  }
233
 
234
  clearLogs() {
@@ -269,66 +308,79 @@ class LogStore {
269
  this._saveReadLogs();
270
  }
271
 
272
- // Network request logging
273
- logNetworkRequest(
274
  method: string,
275
- url: string,
276
  statusCode: number,
277
  duration: number,
278
  requestData?: any,
279
  responseData?: any,
280
  ) {
281
- this.addLog(
282
- `${method} ${url}`,
283
  statusCode >= 400 ? 'error' : 'info',
284
- 'network',
285
  {
286
  method,
287
- url,
288
  statusCode,
289
  duration,
290
  request: requestData,
291
  response: responseData,
292
  },
293
- statusCode,
294
- duration,
 
 
295
  );
296
  }
297
 
298
- // Authentication events
299
- logAuthEvent(event: string, success: boolean, details?: Record<string, any>) {
300
- this.addLog(`Auth ${event} ${success ? 'succeeded' : 'failed'}`, success ? 'info' : 'error', 'auth', details);
301
- }
302
-
303
- // API interactions
304
- logApiCall(
305
- endpoint: string,
306
  method: string,
 
307
  statusCode: number,
308
  duration: number,
309
  requestData?: any,
310
  responseData?: any,
311
  ) {
312
- this.addLog(
313
- `API ${method} ${endpoint}`,
314
  statusCode >= 400 ? 'error' : 'info',
315
- 'api',
316
  {
317
- endpoint,
318
  method,
 
319
  statusCode,
320
  duration,
321
  request: requestData,
322
  response: responseData,
323
  },
324
- statusCode,
325
- duration,
 
 
326
  );
327
  }
328
 
329
- // Performance monitoring
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  logPerformance(operation: string, duration: number, details?: Record<string, any>) {
331
- this.addLog(
332
  `Performance: ${operation}`,
333
  duration > 1000 ? 'warning' : 'info',
334
  'performance',
@@ -337,18 +389,29 @@ class LogStore {
337
  duration,
338
  ...details,
339
  },
340
- undefined,
341
- duration,
 
 
342
  );
343
  }
344
 
345
- // Error logging with stack trace
346
  logErrorWithStack(error: Error, category: LogEntry['category'] = 'error', details?: Record<string, any>) {
347
- this.addLog(error.message, 'error', category, {
348
- ...details,
349
- name: error.name,
350
- stack: error.stack,
351
- });
 
 
 
 
 
 
 
 
 
352
  }
353
 
354
  // Refresh logs (useful for real-time updates)
@@ -356,6 +419,101 @@ class LogStore {
356
  const currentLogs = this._logs.get();
357
  this._logs.set({ ...currentLogs });
358
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  }
360
 
361
  export const logStore = new LogStore();
 
10
  level: 'info' | 'warning' | 'error' | 'debug';
11
  message: string;
12
  details?: Record<string, any>;
13
+ category:
14
+ | 'system'
15
+ | 'provider'
16
+ | 'user'
17
+ | 'error'
18
+ | 'api'
19
+ | 'auth'
20
+ | 'database'
21
+ | 'network'
22
+ | 'performance'
23
+ | 'settings'
24
+ | 'task'
25
+ | 'update'
26
+ | 'feature';
27
  subCategory?: string;
28
  duration?: number;
29
  statusCode?: number;
30
  source?: string;
31
  stack?: string;
32
+ metadata?: {
33
+ component?: string;
34
+ action?: string;
35
+ userId?: string;
36
+ sessionId?: string;
37
+ previousValue?: any;
38
+ newValue?: any;
39
+ };
40
+ }
41
+
42
+ interface LogDetails extends Record<string, any> {
43
+ type: string;
44
+ message: string;
45
  }
46
 
47
  const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
48
 
49
  class LogStore {
 
 
 
 
 
 
 
50
  private _logs = map<Record<string, LogEntry>>({});
51
  showLogs = atom(true);
52
  private _readLogs = new Set<string>();
 
125
  }
126
  }
127
 
128
+ // Base log method for general logging
129
+ private _addLog(
130
  message: string,
131
+ level: LogEntry['level'],
132
+ category: LogEntry['category'],
133
  details?: Record<string, any>,
134
+ metadata?: LogEntry['metadata'],
 
135
  ) {
136
  const id = this._generateId();
137
  const entry: LogEntry = {
 
141
  message,
142
  details,
143
  category,
144
+ metadata,
 
145
  };
146
 
147
  this._logs.setKey(id, entry);
 
151
  return id;
152
  }
153
 
154
+ // Specialized method for API logging
155
+ private _addApiLog(
156
+ message: string,
157
+ method: string,
158
+ url: string,
159
+ details: {
160
+ method: string;
161
+ url: string;
162
+ statusCode: number;
163
+ duration: number;
164
+ request: any;
165
+ response: any;
166
+ },
167
+ ) {
168
+ const statusCode = details.statusCode;
169
+ return this._addLog(message, statusCode >= 400 ? 'error' : 'info', 'api', details, {
170
+ component: 'api',
171
+ action: method,
172
+ });
173
+ }
174
+
175
  // System events
176
  logSystem(message: string, details?: Record<string, any>) {
177
+ return this._addLog(message, 'info', 'system', details);
178
  }
179
 
180
  // Provider events
181
  logProvider(message: string, details?: Record<string, any>) {
182
+ return this._addLog(message, 'info', 'provider', details);
183
  }
184
 
185
  // User actions
186
  logUserAction(message: string, details?: Record<string, any>) {
187
+ return this._addLog(message, 'info', 'user', details);
188
  }
189
 
190
  // API Connection Logging
 
192
  const message = `${method} ${endpoint} - ${statusCode} (${duration}ms)`;
193
  const level = statusCode >= 400 ? 'error' : statusCode >= 300 ? 'warning' : 'info';
194
 
195
+ return this._addLog(message, level, 'api', {
196
  ...details,
197
  endpoint,
198
  method,
 
211
  const message = `Auth ${action} - ${success ? 'Success' : 'Failed'}`;
212
  const level = success ? 'info' : 'error';
213
 
214
+ return this._addLog(message, level, 'auth', {
215
  ...details,
216
  action,
217
  success,
 
224
  const message = `Network ${status}`;
225
  const level = status === 'offline' ? 'error' : status === 'reconnecting' ? 'warning' : 'info';
226
 
227
+ return this._addLog(message, level, 'network', {
228
  ...details,
229
  status,
230
  timestamp: new Date().toISOString(),
 
236
  const message = `DB ${operation} - ${success ? 'Success' : 'Failed'} (${duration}ms)`;
237
  const level = success ? 'info' : 'error';
238
 
239
+ return this._addLog(message, level, 'database', {
240
  ...details,
241
  operation,
242
  success,
 
257
  }
258
  : { error, ...details };
259
 
260
+ return this._addLog(message, 'error', 'error', errorDetails);
261
  }
262
 
263
  // Warning events
264
  logWarning(message: string, details?: Record<string, any>) {
265
+ return this._addLog(message, 'warning', 'system', details);
266
  }
267
 
268
  // Debug events
269
  logDebug(message: string, details?: Record<string, any>) {
270
+ return this._addLog(message, 'debug', 'system', details);
271
  }
272
 
273
  clearLogs() {
 
308
  this._saveReadLogs();
309
  }
310
 
311
+ // API interactions
312
+ logApiCall(
313
  method: string,
314
+ endpoint: string,
315
  statusCode: number,
316
  duration: number,
317
  requestData?: any,
318
  responseData?: any,
319
  ) {
320
+ return this._addLog(
321
+ `API ${method} ${endpoint}`,
322
  statusCode >= 400 ? 'error' : 'info',
323
+ 'api',
324
  {
325
  method,
326
+ endpoint,
327
  statusCode,
328
  duration,
329
  request: requestData,
330
  response: responseData,
331
  },
332
+ {
333
+ component: 'api',
334
+ action: method,
335
+ },
336
  );
337
  }
338
 
339
+ // Network operations
340
+ logNetworkRequest(
 
 
 
 
 
 
341
  method: string,
342
+ url: string,
343
  statusCode: number,
344
  duration: number,
345
  requestData?: any,
346
  responseData?: any,
347
  ) {
348
+ return this._addLog(
349
+ `${method} ${url}`,
350
  statusCode >= 400 ? 'error' : 'info',
351
+ 'network',
352
  {
 
353
  method,
354
+ url,
355
  statusCode,
356
  duration,
357
  request: requestData,
358
  response: responseData,
359
  },
360
+ {
361
+ component: 'network',
362
+ action: method,
363
+ },
364
  );
365
  }
366
 
367
+ // Authentication events
368
+ logAuthEvent(event: string, success: boolean, details?: Record<string, any>) {
369
+ return this._addLog(
370
+ `Auth ${event} ${success ? 'succeeded' : 'failed'}`,
371
+ success ? 'info' : 'error',
372
+ 'auth',
373
+ details,
374
+ {
375
+ component: 'auth',
376
+ action: event,
377
+ },
378
+ );
379
+ }
380
+
381
+ // Performance tracking
382
  logPerformance(operation: string, duration: number, details?: Record<string, any>) {
383
+ return this._addLog(
384
  `Performance: ${operation}`,
385
  duration > 1000 ? 'warning' : 'info',
386
  'performance',
 
389
  duration,
390
  ...details,
391
  },
392
+ {
393
+ component: 'performance',
394
+ action: 'metric',
395
+ },
396
  );
397
  }
398
 
399
+ // Error handling
400
  logErrorWithStack(error: Error, category: LogEntry['category'] = 'error', details?: Record<string, any>) {
401
+ return this._addLog(
402
+ error.message,
403
+ 'error',
404
+ category,
405
+ {
406
+ ...details,
407
+ name: error.name,
408
+ stack: error.stack,
409
+ },
410
+ {
411
+ component: category,
412
+ action: 'error',
413
+ },
414
+ );
415
  }
416
 
417
  // Refresh logs (useful for real-time updates)
 
419
  const currentLogs = this._logs.get();
420
  this._logs.set({ ...currentLogs });
421
  }
422
+
423
+ // Enhanced logging methods
424
+ logInfo(message: string, details: LogDetails) {
425
+ return this._addLog(message, 'info', 'system', details);
426
+ }
427
+
428
+ logSuccess(message: string, details: LogDetails) {
429
+ return this._addLog(message, 'info', 'system', { ...details, success: true });
430
+ }
431
+
432
+ logApiRequest(
433
+ method: string,
434
+ url: string,
435
+ details: {
436
+ method: string;
437
+ url: string;
438
+ statusCode: number;
439
+ duration: number;
440
+ request: any;
441
+ response: any;
442
+ },
443
+ ) {
444
+ return this._addApiLog(`API ${method} ${url}`, method, url, details);
445
+ }
446
+
447
+ logSettingsChange(component: string, setting: string, oldValue: any, newValue: any) {
448
+ return this._addLog(
449
+ `Settings changed in ${component}: ${setting}`,
450
+ 'info',
451
+ 'settings',
452
+ {
453
+ setting,
454
+ previousValue: oldValue,
455
+ newValue,
456
+ },
457
+ {
458
+ component,
459
+ action: 'settings_change',
460
+ previousValue: oldValue,
461
+ newValue,
462
+ },
463
+ );
464
+ }
465
+
466
+ logFeatureToggle(featureId: string, enabled: boolean) {
467
+ return this._addLog(
468
+ `Feature ${featureId} ${enabled ? 'enabled' : 'disabled'}`,
469
+ 'info',
470
+ 'feature',
471
+ { featureId, enabled },
472
+ {
473
+ component: 'features',
474
+ action: 'feature_toggle',
475
+ },
476
+ );
477
+ }
478
+
479
+ logTaskOperation(taskId: string, operation: string, status: string, details?: any) {
480
+ return this._addLog(
481
+ `Task ${taskId}: ${operation} - ${status}`,
482
+ 'info',
483
+ 'task',
484
+ { taskId, operation, status, ...details },
485
+ {
486
+ component: 'task-manager',
487
+ action: 'task_operation',
488
+ },
489
+ );
490
+ }
491
+
492
+ logProviderAction(provider: string, action: string, success: boolean, details?: any) {
493
+ return this._addLog(
494
+ `Provider ${provider}: ${action} - ${success ? 'Success' : 'Failed'}`,
495
+ success ? 'info' : 'error',
496
+ 'provider',
497
+ { provider, action, success, ...details },
498
+ {
499
+ component: 'providers',
500
+ action: 'provider_action',
501
+ },
502
+ );
503
+ }
504
+
505
+ logPerformanceMetric(component: string, operation: string, duration: number, details?: any) {
506
+ return this._addLog(
507
+ `Performance: ${component} - ${operation} took ${duration}ms`,
508
+ duration > 1000 ? 'warning' : 'info',
509
+ 'performance',
510
+ { component, operation, duration, ...details },
511
+ {
512
+ component,
513
+ action: 'performance_metric',
514
+ },
515
+ );
516
+ }
517
  }
518
 
519
  export const logStore = new LogStore();
app/lib/stores/settings.ts CHANGED
@@ -32,24 +32,25 @@ export type ProviderSetting = Record<string, IProviderConfig>;
32
 
33
  export const shortcutsStore = map<Shortcuts>({
34
  toggleTerminal: {
35
- key: 'j',
36
  ctrlOrMetaKey: true,
37
  action: () => workbenchStore.toggleTerminal(),
38
  },
39
  toggleTheme: {
40
- key: 't',
41
  ctrlOrMetaKey: true,
42
- shiftKey: true,
43
  action: () => toggleTheme(),
44
  },
45
  toggleChat: {
46
- key: '/',
47
  ctrlOrMetaKey: true,
48
  action: () => chatStore.setKey('showChat', !chatStore.get().showChat),
49
  },
50
  toggleSettings: {
51
- key: ',',
52
  ctrlOrMetaKey: true,
 
53
  action: () => {
54
  // This will be connected to the settings panel toggle
55
  document.dispatchEvent(new CustomEvent('toggle-settings'));
 
32
 
33
  export const shortcutsStore = map<Shortcuts>({
34
  toggleTerminal: {
35
+ key: '`',
36
  ctrlOrMetaKey: true,
37
  action: () => workbenchStore.toggleTerminal(),
38
  },
39
  toggleTheme: {
40
+ key: 'd',
41
  ctrlOrMetaKey: true,
42
+ altKey: true,
43
  action: () => toggleTheme(),
44
  },
45
  toggleChat: {
46
+ key: 'k',
47
  ctrlOrMetaKey: true,
48
  action: () => chatStore.setKey('showChat', !chatStore.get().showChat),
49
  },
50
  toggleSettings: {
51
+ key: 's',
52
  ctrlOrMetaKey: true,
53
+ altKey: true,
54
  action: () => {
55
  // This will be connected to the settings panel toggle
56
  document.dispatchEvent(new CustomEvent('toggle-settings'));
package.json CHANGED
@@ -63,13 +63,14 @@
63
  "@phosphor-icons/react": "^2.1.7",
64
  "@radix-ui/react-collapsible": "^1.0.3",
65
  "@radix-ui/react-context-menu": "^2.2.2",
66
- "@radix-ui/react-dialog": "^1.1.2",
67
  "@radix-ui/react-dropdown-menu": "^2.1.2",
68
  "@radix-ui/react-popover": "^1.1.5",
69
  "@radix-ui/react-progress": "^1.0.3",
70
  "@radix-ui/react-scroll-area": "^1.2.2",
71
  "@radix-ui/react-separator": "^1.1.0",
72
  "@radix-ui/react-switch": "^1.1.1",
 
73
  "@radix-ui/react-tooltip": "^1.1.4",
74
  "@remix-run/cloudflare": "^2.15.2",
75
  "@remix-run/cloudflare-pages": "^2.15.2",
 
63
  "@phosphor-icons/react": "^2.1.7",
64
  "@radix-ui/react-collapsible": "^1.0.3",
65
  "@radix-ui/react-context-menu": "^2.2.2",
66
+ "@radix-ui/react-dialog": "^1.1.5",
67
  "@radix-ui/react-dropdown-menu": "^2.1.2",
68
  "@radix-ui/react-popover": "^1.1.5",
69
  "@radix-ui/react-progress": "^1.0.3",
70
  "@radix-ui/react-scroll-area": "^1.2.2",
71
  "@radix-ui/react-separator": "^1.1.0",
72
  "@radix-ui/react-switch": "^1.1.1",
73
+ "@radix-ui/react-tabs": "^1.1.2",
74
  "@radix-ui/react-tooltip": "^1.1.4",
75
  "@remix-run/cloudflare": "^2.15.2",
76
  "@remix-run/cloudflare-pages": "^2.15.2",
pnpm-lock.yaml CHANGED
@@ -111,7 +111,7 @@ importers:
111
  specifier: ^2.2.2
112
  version: 2.2.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
113
  '@radix-ui/react-dialog':
114
- specifier: ^1.1.2
115
  version: 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
116
  '@radix-ui/react-dropdown-menu':
117
  specifier: ^2.1.2
@@ -131,6 +131,9 @@ importers:
131
  '@radix-ui/react-switch':
132
  specifier: ^1.1.1
133
  version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
 
 
 
134
  '@radix-ui/react-tooltip':
135
  specifier: ^1.1.4
136
  version: 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -2164,6 +2167,19 @@ packages:
2164
  '@types/react-dom':
2165
  optional: true
2166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2167
  '@radix-ui/react-tooltip@1.1.7':
2168
  resolution: {integrity: sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==}
2169
  peerDependencies:
@@ -8494,6 +8510,22 @@ snapshots:
8494
  '@types/react': 18.3.18
8495
  '@types/react-dom': 18.3.5(@types/react@18.3.18)
8496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8497
  '@radix-ui/react-tooltip@1.1.7(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
8498
  dependencies:
8499
  '@radix-ui/primitive': 1.1.1
 
111
  specifier: ^2.2.2
112
  version: 2.2.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
113
  '@radix-ui/react-dialog':
114
+ specifier: ^1.1.5
115
  version: 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
116
  '@radix-ui/react-dropdown-menu':
117
  specifier: ^2.1.2
 
131
  '@radix-ui/react-switch':
132
  specifier: ^1.1.1
133
  version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
134
+ '@radix-ui/react-tabs':
135
+ specifier: ^1.1.2
136
+ version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
137
  '@radix-ui/react-tooltip':
138
  specifier: ^1.1.4
139
  version: 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
 
2167
  '@types/react-dom':
2168
  optional: true
2169
 
2170
+ '@radix-ui/react-tabs@1.1.2':
2171
+ resolution: {integrity: sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==}
2172
+ peerDependencies:
2173
+ '@types/react': '*'
2174
+ '@types/react-dom': '*'
2175
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
2176
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
2177
+ peerDependenciesMeta:
2178
+ '@types/react':
2179
+ optional: true
2180
+ '@types/react-dom':
2181
+ optional: true
2182
+
2183
  '@radix-ui/react-tooltip@1.1.7':
2184
  resolution: {integrity: sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==}
2185
  peerDependencies:
 
8510
  '@types/react': 18.3.18
8511
  '@types/react-dom': 18.3.5(@types/react@18.3.18)
8512
 
8513
+ '@radix-ui/react-tabs@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
8514
+ dependencies:
8515
+ '@radix-ui/primitive': 1.1.1
8516
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
8517
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1)
8518
+ '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1)
8519
+ '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
8520
+ '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
8521
+ '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
8522
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1)
8523
+ react: 18.3.1
8524
+ react-dom: 18.3.1(react@18.3.1)
8525
+ optionalDependencies:
8526
+ '@types/react': 18.3.18
8527
+ '@types/react-dom': 18.3.5(@types/react@18.3.18)
8528
+
8529
  '@radix-ui/react-tooltip@1.1.7(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
8530
  dependencies:
8531
  '@radix-ui/primitive': 1.1.1