Stijnus commited on
Commit
d1d23d8
·
1 Parent(s): d9a380f

fixes feedback from thecodacus

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .cursorrules +1 -0
  2. .gitignore +2 -0
  3. app/components/chat/GitCloneButton.tsx +2 -2
  4. app/components/chat/ImportFolderButton.tsx +3 -2
  5. app/components/chat/StarterTemplates.tsx +10 -1
  6. app/components/chat/chatExportAndImport/ImportButtons.tsx +3 -3
  7. app/components/settings/connections/components/ConnectionForm.tsx +1 -1
  8. app/components/settings/connections/components/PushToGitHubDialog.tsx +1 -1
  9. app/components/settings/connections/components/RepositorySelectionDialog.tsx +10 -9
  10. app/components/settings/debug/DebugTab.tsx +331 -302
  11. app/components/settings/developer/DeveloperWindow.tsx +4 -7
  12. app/components/settings/developer/TabManagement.tsx +11 -1
  13. app/components/settings/event-logs/EventLogsTab.tsx +19 -9
  14. app/components/settings/profile/ProfileTab.tsx +11 -5
  15. app/components/settings/providers/CloudProvidersTab.tsx +1 -2
  16. app/components/settings/providers/LocalProvidersTab.tsx +27 -17
  17. app/components/settings/providers/OllamaModelUpdater.tsx +115 -90
  18. app/components/settings/providers/service-status/ServiceStatusTab.tsx +135 -0
  19. app/components/settings/providers/service-status/provider-factory.ts +56 -62
  20. app/components/settings/providers/service-status/providers/amazon-bedrock.ts +76 -0
  21. app/components/settings/providers/service-status/providers/anthropic.ts +80 -0
  22. app/components/settings/providers/service-status/providers/cohere.ts +91 -0
  23. app/components/settings/providers/service-status/providers/deepseek.ts +40 -0
  24. app/components/settings/providers/service-status/providers/google.ts +77 -0
  25. app/components/settings/providers/service-status/providers/groq.ts +72 -0
  26. app/components/settings/providers/service-status/providers/huggingface.ts +98 -0
  27. app/components/settings/providers/service-status/providers/hyperbolic.ts +40 -0
  28. app/components/settings/providers/service-status/providers/mistral.ts +76 -0
  29. app/components/settings/providers/service-status/providers/openrouter.ts +91 -0
  30. app/components/settings/providers/service-status/providers/perplexity.ts +91 -0
  31. app/components/settings/providers/service-status/providers/together.ts +91 -0
  32. app/components/settings/providers/service-status/providers/xai.ts +40 -0
  33. app/components/settings/providers/service-status/types.ts +5 -8
  34. app/components/settings/settings.styles.ts +0 -43
  35. app/components/settings/settings.types.ts +7 -7
  36. app/components/settings/settings/SettingsTab.tsx +3 -5
  37. app/components/settings/task-manager/TaskManagerTab.tsx +163 -559
  38. app/components/settings/user/UsersWindow.tsx +0 -2
  39. app/components/ui/Badge.tsx +11 -12
  40. app/components/ui/Button.tsx +2 -2
  41. app/components/ui/Card.tsx +18 -7
  42. app/components/ui/Input.tsx +3 -6
  43. app/components/ui/Label.tsx +17 -19
  44. app/components/ui/Progress.tsx +12 -12
  45. app/components/ui/ScrollArea.tsx +8 -6
  46. app/components/ui/Tabs.tsx +11 -15
  47. app/lib/hooks/useSettings.ts +1 -1
  48. app/lib/modules/llm/providers/github.ts +53 -0
  49. app/lib/modules/llm/registry.ts +2 -0
  50. app/lib/persistence/index.ts +1 -0
.cursorrules CHANGED
@@ -154,3 +154,4 @@ bolt.diy (previously oTToDev) is an open-source AI-powered full-stack web develo
154
  - Don't use white background for dark mode
155
  - Don't use white text on white background for dark mode
156
  - Match the style of the existing codebase
 
 
154
  - Don't use white background for dark mode
155
  - Don't use white text on white background for dark mode
156
  - Match the style of the existing codebase
157
+ - Use consistent naming conventions for components and variables
.gitignore CHANGED
@@ -42,3 +42,5 @@ site
42
  app/commit.json
43
  changelogUI.md
44
  docs/instructions/Roadmap.md
 
 
 
42
  app/commit.json
43
  changelogUI.md
44
  docs/instructions/Roadmap.md
45
+ .cursorrules
46
+ .cursorrules
app/components/chat/GitCloneButton.tsx CHANGED
@@ -7,7 +7,7 @@ import { useState } from 'react';
7
  import { toast } from 'react-toastify';
8
  import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
9
  import { RepositorySelectionDialog } from '~/components/settings/connections/components/RepositorySelectionDialog';
10
- import { cn } from '~/lib/utils';
11
  import { Button } from '~/components/ui/Button';
12
  import type { IChatMetadata } from '~/lib/persistence/db';
13
 
@@ -158,7 +158,7 @@ ${escapeBoltTags(file.content)}
158
  title="Clone a Git Repo"
159
  variant="outline"
160
  size="lg"
161
- className={cn(
162
  'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
163
  'text-bolt-elements-textPrimary dark:text-white',
164
  'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
 
7
  import { toast } from 'react-toastify';
8
  import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
9
  import { RepositorySelectionDialog } from '~/components/settings/connections/components/RepositorySelectionDialog';
10
+ import { classNames } from '~/utils/classNames';
11
  import { Button } from '~/components/ui/Button';
12
  import type { IChatMetadata } from '~/lib/persistence/db';
13
 
 
158
  title="Clone a Git Repo"
159
  variant="outline"
160
  size="lg"
161
+ className={classNames(
162
  'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
163
  'text-bolt-elements-textPrimary dark:text-white',
164
  'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
app/components/chat/ImportFolderButton.tsx CHANGED
@@ -5,7 +5,7 @@ import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
5
  import { createChatFromFolder } from '~/utils/folderImport';
6
  import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
7
  import { Button } from '~/components/ui/Button';
8
- import { cn } from '~/lib/utils';
9
 
10
  interface ImportFolderButtonProps {
11
  className?: string;
@@ -119,9 +119,10 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
119
  const input = document.getElementById('folder-import');
120
  input?.click();
121
  }}
 
122
  variant="outline"
123
  size="lg"
124
- className={cn(
125
  'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
126
  'text-bolt-elements-textPrimary dark:text-white',
127
  'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
 
5
  import { createChatFromFolder } from '~/utils/folderImport';
6
  import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
7
  import { Button } from '~/components/ui/Button';
8
+ import { classNames } from '~/utils/classNames';
9
 
10
  interface ImportFolderButtonProps {
11
  className?: string;
 
119
  const input = document.getElementById('folder-import');
120
  input?.click();
121
  }}
122
+ title="Import Folder"
123
  variant="outline"
124
  size="lg"
125
+ className={classNames(
126
  'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
127
  'text-bolt-elements-textPrimary dark:text-white',
128
  'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
app/components/chat/StarterTemplates.tsx CHANGED
@@ -11,15 +11,24 @@ const FrameworkLink: React.FC<FrameworkLinkProps> = ({ template }) => (
11
  href={`/git?url=https://github.com/${template.githubRepo}.git`}
12
  data-state="closed"
13
  data-discover="true"
14
- className="items-center justify-center "
15
  >
16
  <div
17
  className={`inline-block ${template.icon} w-8 h-8 text-4xl transition-theme opacity-25 hover:opacity-100 hover:text-purple-500 dark:text-white dark:opacity-50 dark:hover:opacity-100 dark:hover:text-purple-400 transition-all`}
 
18
  />
19
  </a>
20
  );
21
 
22
  const StarterTemplates: React.FC = () => {
 
 
 
 
 
 
 
 
23
  return (
24
  <div className="flex flex-col items-center gap-4">
25
  <span className="text-sm text-gray-500">or start a blank app with your favorite stack</span>
 
11
  href={`/git?url=https://github.com/${template.githubRepo}.git`}
12
  data-state="closed"
13
  data-discover="true"
14
+ className="items-center justify-center"
15
  >
16
  <div
17
  className={`inline-block ${template.icon} w-8 h-8 text-4xl transition-theme opacity-25 hover:opacity-100 hover:text-purple-500 dark:text-white dark:opacity-50 dark:hover:opacity-100 dark:hover:text-purple-400 transition-all`}
18
+ title={template.label}
19
  />
20
  </a>
21
  );
22
 
23
  const StarterTemplates: React.FC = () => {
24
+ // Debug: Log available templates and their icons
25
+ React.useEffect(() => {
26
+ console.log(
27
+ 'Available templates:',
28
+ STARTER_TEMPLATES.map((t) => ({ name: t.name, icon: t.icon })),
29
+ );
30
+ }, []);
31
+
32
  return (
33
  <div className="flex flex-col items-center gap-4">
34
  <span className="text-sm text-gray-500">or start a blank app with your favorite stack</span>
app/components/chat/chatExportAndImport/ImportButtons.tsx CHANGED
@@ -2,7 +2,7 @@ import type { Message } from 'ai';
2
  import { toast } from 'react-toastify';
3
  import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
4
  import { Button } from '~/components/ui/Button';
5
- import { cn } from '~/lib/utils';
6
 
7
  type ChatData = {
8
  messages?: Message[]; // Standard Bolt format
@@ -66,7 +66,7 @@ export function ImportButtons(importChat: ((description: string, messages: Messa
66
  }}
67
  variant="outline"
68
  size="lg"
69
- className={cn(
70
  'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
71
  'text-bolt-elements-textPrimary dark:text-white',
72
  'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
@@ -80,7 +80,7 @@ export function ImportButtons(importChat: ((description: string, messages: Messa
80
  </Button>
81
  <ImportFolderButton
82
  importChat={importChat}
83
- className={cn(
84
  'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
85
  'text-bolt-elements-textPrimary dark:text-white',
86
  'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
 
2
  import { toast } from 'react-toastify';
3
  import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
4
  import { Button } from '~/components/ui/Button';
5
+ import { classNames } from '~/utils/classNames';
6
 
7
  type ChatData = {
8
  messages?: Message[]; // Standard Bolt format
 
66
  }}
67
  variant="outline"
68
  size="lg"
69
+ className={classNames(
70
  'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
71
  'text-bolt-elements-textPrimary dark:text-white',
72
  'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
 
80
  </Button>
81
  <ImportFolderButton
82
  importChat={importChat}
83
+ className={classNames(
84
  'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
85
  'text-bolt-elements-textPrimary dark:text-white',
86
  'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
app/components/settings/connections/components/ConnectionForm.tsx CHANGED
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
2
  import { classNames } from '~/utils/classNames';
3
  import type { GitHubAuthState } from '~/components/settings/connections/types/GitHub';
4
  import Cookies from 'js-cookie';
5
- import { getLocalStorage } from '~/utils/localStorage';
6
 
7
  const GITHUB_TOKEN_KEY = 'github_token';
8
 
 
2
  import { classNames } from '~/utils/classNames';
3
  import type { GitHubAuthState } from '~/components/settings/connections/types/GitHub';
4
  import Cookies from 'js-cookie';
5
+ import { getLocalStorage } from '~/lib/persistence';
6
 
7
  const GITHUB_TOKEN_KEY = 'github_token';
8
 
app/components/settings/connections/components/PushToGitHubDialog.tsx CHANGED
@@ -2,7 +2,7 @@ import * as Dialog from '@radix-ui/react-dialog';
2
  import { useState, useEffect } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { motion } from 'framer-motion';
5
- import { getLocalStorage } from '~/utils/localStorage';
6
  import { classNames } from '~/utils/classNames';
7
  import type { GitHubUserResponse } from '~/types/GitHub';
8
  import { logStore } from '~/lib/stores/logs';
 
2
  import { useState, useEffect } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { motion } from 'framer-motion';
5
+ import { getLocalStorage } from '~/lib/persistence';
6
  import { classNames } from '~/utils/classNames';
7
  import type { GitHubUserResponse } from '~/types/GitHub';
8
  import { logStore } from '~/lib/stores/logs';
app/components/settings/connections/components/RepositorySelectionDialog.tsx CHANGED
@@ -2,11 +2,11 @@ import type { GitHubRepoInfo, GitHubContent, RepositoryStats } from '~/types/Git
2
  import { useState, useEffect } from 'react';
3
  import { toast } from 'react-toastify';
4
  import * as Dialog from '@radix-ui/react-dialog';
5
- import { cn } from '~/lib/utils';
6
- import { getLocalStorage } from '~/utils/localStorage';
7
- import { classNames as utilsClassNames } from '~/utils/classNames';
8
  import { motion } from 'framer-motion';
9
  import { formatSize } from '~/utils/formatSize';
 
10
 
11
  interface GitHubTreeResponse {
12
  tree: Array<{
@@ -445,7 +445,7 @@ export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: Reposit
445
  </Dialog.Title>
446
  <Dialog.Close
447
  onClick={handleClose}
448
- className={cn(
449
  'p-2 rounded-lg transition-all duration-200 ease-in-out',
450
  'text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary',
451
  'dark:text-bolt-elements-textTertiary-dark dark:hover:text-bolt-elements-textPrimary-dark',
@@ -476,12 +476,13 @@ export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: Reposit
476
 
477
  {activeTab === 'url' ? (
478
  <div className="space-y-4">
479
- <input
480
- type="text"
481
- placeholder="Enter GitHub repository URL..."
482
  value={customUrl}
483
  onChange={(e) => setCustomUrl(e.target.value)}
484
- className="w-full px-4 py-2 rounded-lg bg-[#F5F5F5] dark:bg-[#252525] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary"
 
 
485
  />
486
  <button
487
  onClick={handleImport}
@@ -610,7 +611,7 @@ function TabButton({ active, onClick, children }: { active: boolean; onClick: ()
610
  return (
611
  <button
612
  onClick={onClick}
613
- className={utilsClassNames(
614
  'px-4 py-2 h-10 rounded-lg transition-all duration-200 flex items-center gap-2 min-w-[120px] justify-center',
615
  active
616
  ? 'bg-purple-500 text-white hover:bg-purple-600'
 
2
  import { useState, useEffect } from 'react';
3
  import { toast } from 'react-toastify';
4
  import * as Dialog from '@radix-ui/react-dialog';
5
+ import { classNames } from '~/utils/classNames';
6
+ import { getLocalStorage } from '~/lib/persistence';
 
7
  import { motion } from 'framer-motion';
8
  import { formatSize } from '~/utils/formatSize';
9
+ import { Input } from '~/components/ui/Input';
10
 
11
  interface GitHubTreeResponse {
12
  tree: Array<{
 
445
  </Dialog.Title>
446
  <Dialog.Close
447
  onClick={handleClose}
448
+ className={classNames(
449
  'p-2 rounded-lg transition-all duration-200 ease-in-out',
450
  'text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary',
451
  'dark:text-bolt-elements-textTertiary-dark dark:hover:text-bolt-elements-textPrimary-dark',
 
476
 
477
  {activeTab === 'url' ? (
478
  <div className="space-y-4">
479
+ <Input
480
+ placeholder="Enter repository URL"
 
481
  value={customUrl}
482
  onChange={(e) => setCustomUrl(e.target.value)}
483
+ className={classNames('w-full', {
484
+ 'border-red-500': false,
485
+ })}
486
  />
487
  <button
488
  onClick={handleImport}
 
611
  return (
612
  <button
613
  onClick={onClick}
614
+ className={classNames(
615
  'px-4 py-2 h-10 rounded-lg transition-all duration-200 flex items-center gap-2 min-w-[120px] justify-center',
616
  active
617
  ? 'bg-purple-500 text-white hover:bg-purple-600'
app/components/settings/debug/DebugTab.tsx CHANGED
@@ -1,13 +1,12 @@
1
- import React, { useEffect, useState } from 'react';
2
  import { toast } from 'react-toastify';
3
  import { classNames } from '~/utils/classNames';
4
- import { logStore } from '~/lib/stores/logs';
5
- import type { LogEntry } from '~/lib/stores/logs';
6
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/Collapsible';
7
  import { Progress } from '~/components/ui/Progress';
8
  import { ScrollArea } from '~/components/ui/ScrollArea';
9
  import { Badge } from '~/components/ui/Badge';
10
- import { cn } from '~/lib/utils';
11
 
12
  interface SystemInfo {
13
  os: string;
@@ -88,125 +87,134 @@ interface SystemInfo {
88
  };
89
  }
90
 
91
- interface WebAppInfo {
92
- // Local WebApp Info
93
- name: string;
94
- version: string;
95
- description: string;
96
- license: string;
97
- nodeVersion: string;
98
- dependencies: { [key: string]: string };
99
- devDependencies: { [key: string]: string };
100
-
101
- // Build Info
102
- buildTime?: string;
103
- buildNumber?: string;
104
- environment?: string;
105
 
106
- // Git Info
107
- gitInfo?: {
 
108
  branch: string;
109
- commit: string;
110
  commitTime: string;
111
  author: string;
 
112
  remoteUrl: string;
 
113
  };
114
-
115
- // GitHub Repository Info
116
- repoInfo?: {
117
- name: string;
118
- fullName: string;
119
- description: string;
120
- stars: number;
121
- forks: number;
122
- openIssues: number;
123
- defaultBranch: string;
124
- lastUpdate: string;
125
- owner: {
126
- login: string;
127
- avatarUrl: string;
128
- };
129
  };
 
130
  }
131
 
132
- // Add interface for GitHub API response
133
- interface GitHubRepoResponse {
134
  name: string;
135
- full_name: string;
136
- description: string | null;
137
- stargazers_count: number;
138
- forks_count: number;
139
- open_issues_count: number;
140
- default_branch: string;
141
- updated_at: string;
142
- owner: {
143
- login: string;
144
- avatar_url: string;
 
 
 
145
  };
 
146
  }
147
 
148
- // Add interface for Git info response
149
- interface GitInfo {
150
- branch: string;
151
- commit: string;
152
- commitTime: string;
153
- author: string;
154
- remoteUrl: string;
155
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
  export default function DebugTab() {
158
  const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
159
  const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
160
  const [loading, setLoading] = useState({
161
  systemInfo: false,
162
- performance: false,
163
- errors: false,
164
  webAppInfo: false,
 
 
165
  });
166
- const [errorLog, setErrorLog] = useState<{
167
- errors: any[];
168
- lastCheck: string | null;
169
- }>({
170
- errors: [],
171
- lastCheck: null,
172
- });
173
-
174
- // Add section collapse state
175
  const [openSections, setOpenSections] = useState({
176
- system: true,
177
- performance: true,
178
- webapp: true,
179
- errors: true,
180
  });
181
 
182
- // Fetch initial data
183
- useEffect(() => {
184
- getSystemInfo();
185
- getWebAppInfo();
186
- }, []);
 
 
187
 
188
  // Set up error listeners when component mounts
189
  useEffect(() => {
190
- const errors: any[] = [];
191
-
192
  const handleError = (event: ErrorEvent) => {
193
- errors.push({
194
- type: 'error',
195
- message: event.message,
196
  filename: event.filename,
197
  lineNumber: event.lineno,
198
  columnNumber: event.colno,
199
- error: event.error,
200
- timestamp: new Date().toISOString(),
201
  });
202
  };
203
 
204
  const handleRejection = (event: PromiseRejectionEvent) => {
205
- errors.push({
206
- type: 'unhandledRejection',
207
- reason: event.reason,
208
- timestamp: new Date().toISOString(),
209
- });
210
  };
211
 
212
  window.addEventListener('error', handleError);
@@ -218,6 +226,66 @@ export default function DebugTab() {
218
  };
219
  }, []);
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  const getSystemInfo = async () => {
222
  try {
223
  setLoading((prev) => ({ ...prev, systemInfo: true }));
@@ -367,67 +435,32 @@ export default function DebugTab() {
367
  try {
368
  setLoading((prev) => ({ ...prev, webAppInfo: true }));
369
 
370
- // Fetch local app info
371
- const appInfoResponse = await fetch('/api/system/app-info');
 
 
372
 
373
- if (!appInfoResponse.ok) {
374
  throw new Error('Failed to fetch webapp info');
375
  }
376
 
377
- const appData = (await appInfoResponse.json()) as Record<string, unknown>;
378
-
379
- // Fetch git info
380
- const gitInfoResponse = await fetch('/api/system/git-info');
381
- let gitInfo: GitInfo | undefined;
382
-
383
- if (gitInfoResponse.ok) {
384
- gitInfo = (await gitInfoResponse.json()) as GitInfo;
385
- }
386
-
387
- // Fetch GitHub repository info
388
- const repoInfoResponse = await fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy');
389
- let repoInfo: WebAppInfo['repoInfo'] | undefined;
390
-
391
- if (repoInfoResponse.ok) {
392
- const repoData = (await repoInfoResponse.json()) as GitHubRepoResponse;
393
- repoInfo = {
394
- name: repoData.name,
395
- fullName: repoData.full_name,
396
- description: repoData.description ?? '',
397
- stars: repoData.stargazers_count,
398
- forks: repoData.forks_count,
399
- openIssues: repoData.open_issues_count,
400
- defaultBranch: repoData.default_branch,
401
- lastUpdate: repoData.updated_at,
402
- owner: {
403
- login: repoData.owner.login,
404
- avatarUrl: repoData.owner.avatar_url,
405
- },
406
- };
407
- }
408
-
409
- // Get build info from environment variables or config
410
- const buildInfo = {
411
- buildTime: process.env.NEXT_PUBLIC_BUILD_TIME || new Date().toISOString(),
412
- buildNumber: process.env.NEXT_PUBLIC_BUILD_NUMBER || 'development',
413
- environment: process.env.NEXT_PUBLIC_ENV || 'development',
414
- };
415
 
416
  setWebAppInfo({
417
- name: appData.name as string,
418
- version: appData.version as string,
419
- description: appData.description as string,
420
- license: appData.license as string,
421
- nodeVersion: appData.nodeVersion as string,
422
- dependencies: appData.dependencies as Record<string, string>,
423
- devDependencies: appData.devDependencies as Record<string, string>,
424
- ...buildInfo,
425
- gitInfo,
426
- repoInfo,
427
  });
 
 
 
 
428
  } catch (error) {
429
  console.error('Failed to fetch webapp info:', error);
430
  toast.error('Failed to fetch webapp information');
 
 
 
431
  } finally {
432
  setLoading((prev) => ({ ...prev, webAppInfo: false }));
433
  }
@@ -536,28 +569,12 @@ export default function DebugTab() {
536
  setLoading((prev) => ({ ...prev, errors: true }));
537
 
538
  // Get errors from log store
539
- const storedErrors = logStore.getLogs().filter((log: LogEntry) => log.level === 'error');
540
-
541
- // Combine with runtime errors
542
- const allErrors = [
543
- ...errorLog.errors,
544
- ...storedErrors.map((error) => ({
545
- type: 'stored',
546
- message: error.message,
547
- timestamp: error.timestamp,
548
- details: error.details || {},
549
- })),
550
- ];
551
-
552
- setErrorLog({
553
- errors: allErrors,
554
- lastCheck: new Date().toISOString(),
555
- });
556
 
557
- if (allErrors.length === 0) {
558
  toast.success('No errors found');
559
  } else {
560
- toast.warning(`Found ${allErrors.length} error(s)`);
561
  }
562
  } catch (error) {
563
  toast.error('Failed to check errors');
@@ -573,7 +590,7 @@ export default function DebugTab() {
573
  timestamp: new Date().toISOString(),
574
  system: systemInfo,
575
  webApp: webAppInfo,
576
- errors: errorLog.errors,
577
  performance: {
578
  memory: (performance as any).memory || {},
579
  timing: performance.timing,
@@ -629,10 +646,7 @@ export default function DebugTab() {
629
 
630
  <div className="p-4 rounded-xl bg-gradient-to-br from-red-500/10 to-red-500/5 border border-red-500/20">
631
  <div className="text-sm text-bolt-elements-textSecondary">Errors</div>
632
- <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLog.errors.length}</div>
633
- <div className="text-xs text-bolt-elements-textSecondary mt-2">
634
- Last Check: {errorLog.lastCheck ? new Date(errorLog.lastCheck).toLocaleTimeString() : 'Never'}
635
- </div>
636
  </div>
637
  </div>
638
 
@@ -746,7 +760,7 @@ export default function DebugTab() {
746
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
747
  </div>
748
  <div
749
- className={cn(
750
  'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
751
  openSections.system ? 'rotate-180' : '',
752
  )}
@@ -893,7 +907,7 @@ export default function DebugTab() {
893
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
894
  </div>
895
  <div
896
- className={cn(
897
  'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
898
  openSections.performance ? 'rotate-180' : '',
899
  )}
@@ -973,7 +987,7 @@ export default function DebugTab() {
973
  {/* WebApp Information */}
974
  <Collapsible
975
  open={openSections.webapp}
976
- onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, webapp: open }))}
977
  className="w-full"
978
  >
979
  <CollapsibleTrigger className="w-full">
@@ -981,9 +995,10 @@ export default function DebugTab() {
981
  <div className="flex items-center gap-3">
982
  <div className="i-ph:info text-blue-500 w-5 h-5" />
983
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
 
984
  </div>
985
  <div
986
- className={cn(
987
  'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
988
  openSections.webapp ? 'rotate-180' : '',
989
  )}
@@ -993,142 +1008,154 @@ export default function DebugTab() {
993
 
994
  <CollapsibleContent>
995
  <div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
996
- {webAppInfo ? (
997
- <div className="grid grid-cols-2 gap-4">
998
- <div className="space-y-2">
999
- <div className="text-sm flex items-center gap-2">
1000
- <div className="i-ph:app-window text-bolt-elements-textSecondary w-4 h-4" />
1001
- <span className="text-bolt-elements-textSecondary">Name: </span>
1002
- <span className="text-bolt-elements-textPrimary">{webAppInfo.name}</span>
1003
- </div>
1004
- <div className="text-sm flex items-center gap-2">
1005
- <div className="i-ph:tag text-bolt-elements-textSecondary w-4 h-4" />
1006
- <span className="text-bolt-elements-textSecondary">Version: </span>
1007
- <span className="text-bolt-elements-textPrimary">{webAppInfo.version}</span>
1008
- </div>
1009
- <div className="text-sm flex items-center gap-2">
1010
- <div className="i-ph:file-text text-bolt-elements-textSecondary w-4 h-4" />
1011
- <span className="text-bolt-elements-textSecondary">Description: </span>
1012
- <span className="text-bolt-elements-textPrimary">{webAppInfo.description}</span>
1013
- </div>
1014
- <div className="text-sm flex items-center gap-2">
1015
- <div className="i-ph:certificate text-bolt-elements-textSecondary w-4 h-4" />
1016
- <span className="text-bolt-elements-textSecondary">License: </span>
1017
- <span className="text-bolt-elements-textPrimary">{webAppInfo.license}</span>
1018
- </div>
1019
- <div className="text-sm flex items-center gap-2">
1020
- <div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
1021
- <span className="text-bolt-elements-textSecondary">Node Version: </span>
1022
- <span className="text-bolt-elements-textPrimary">{webAppInfo.nodeVersion}</span>
1023
- </div>
1024
- {webAppInfo.buildTime && (
1025
  <div className="text-sm flex items-center gap-2">
1026
- <div className="i-ph:calendar text-bolt-elements-textSecondary w-4 h-4" />
1027
- <span className="text-bolt-elements-textSecondary">Build Time: </span>
1028
- <span className="text-bolt-elements-textPrimary">{webAppInfo.buildTime}</span>
1029
  </div>
1030
- )}
1031
- {webAppInfo.buildNumber && (
1032
  <div className="text-sm flex items-center gap-2">
1033
- <div className="i-ph:hash text-bolt-elements-textSecondary w-4 h-4" />
1034
- <span className="text-bolt-elements-textSecondary">Build Number: </span>
1035
- <span className="text-bolt-elements-textPrimary">{webAppInfo.buildNumber}</span>
 
 
 
 
 
1036
  </div>
1037
- )}
1038
- {webAppInfo.environment && (
1039
  <div className="text-sm flex items-center gap-2">
1040
  <div className="i-ph:cloud text-bolt-elements-textSecondary w-4 h-4" />
1041
- <span className="text-bolt-elements-textSecondary">Environment: </span>
1042
  <span className="text-bolt-elements-textPrimary">{webAppInfo.environment}</span>
1043
  </div>
1044
- )}
 
 
 
 
 
1045
  </div>
1046
- <div className="space-y-2">
1047
- <div className="text-sm">
1048
- <div className="flex items-center gap-2 mb-2">
1049
- <div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
1050
- <span className="text-bolt-elements-textSecondary">Key Dependencies:</span>
 
 
 
1051
  </div>
1052
- <div className="pl-6 space-y-1">
1053
- {Object.entries(webAppInfo.dependencies)
1054
- .filter(([key]) => ['react', '@remix-run/react', 'next', 'typescript'].includes(key))
1055
- .map(([key, version]) => (
1056
- <div key={key} className="text-xs text-bolt-elements-textPrimary">
1057
- {key}: {version}
1058
- </div>
1059
- ))}
1060
  </div>
1061
- </div>
1062
- {webAppInfo.gitInfo && (
1063
- <div className="text-sm">
1064
- <div className="flex items-center gap-2 mb-2">
1065
- <div className="i-ph:git-branch text-bolt-elements-textSecondary w-4 h-4" />
1066
- <span className="text-bolt-elements-textSecondary">Git Info:</span>
1067
- </div>
1068
- <div className="pl-6 space-y-1">
1069
- <div className="text-xs text-bolt-elements-textPrimary">
1070
- Branch: {webAppInfo.gitInfo.branch}
1071
- </div>
1072
- <div className="text-xs text-bolt-elements-textPrimary">
1073
- Commit: {webAppInfo.gitInfo.commit}
1074
- </div>
1075
- <div className="text-xs text-bolt-elements-textPrimary">
1076
- Commit Time: {webAppInfo.gitInfo.commitTime}
1077
- </div>
1078
- <div className="text-xs text-bolt-elements-textPrimary">
1079
- Author: {webAppInfo.gitInfo.author}
1080
- </div>
1081
- <div className="text-xs text-bolt-elements-textPrimary">
1082
- Remote URL: {webAppInfo.gitInfo.remoteUrl}
1083
- </div>
1084
- </div>
1085
  </div>
1086
- )}
1087
- {webAppInfo.repoInfo && (
1088
- <div className="text-sm">
1089
- <div className="flex items-center gap-2 mb-2">
1090
- <div className="i-ph:github text-bolt-elements-textSecondary w-4 h-4" />
1091
- <span className="text-bolt-elements-textSecondary">GitHub Repository:</span>
1092
- </div>
1093
- <div className="pl-6 space-y-3">
1094
- <div className="flex items-center gap-3">
1095
- <img
1096
- src={webAppInfo.repoInfo.owner.avatarUrl}
1097
- alt={`${webAppInfo.repoInfo.owner.login}'s avatar`}
1098
- className="w-8 h-8 rounded-full border border-[#E5E5E5] dark:border-[#1A1A1A]"
1099
- />
1100
- <div className="space-y-0.5">
1101
- <div className="text-xs text-bolt-elements-textPrimary font-medium">
1102
- Owner: {webAppInfo.repoInfo.owner.login}
 
 
 
 
 
 
 
 
 
 
 
 
 
1103
  </div>
1104
- <div className="text-xs text-bolt-elements-textSecondary">
1105
- Last Update: {new Date(webAppInfo.repoInfo.lastUpdate).toLocaleDateString()}
 
 
 
1106
  </div>
1107
  </div>
1108
  </div>
1109
 
1110
- <div className="grid grid-cols-3 gap-2 mt-2">
1111
- <div className="flex items-center gap-1 text-xs text-bolt-elements-textSecondary">
1112
- <div className="i-ph:star text-yellow-500 w-4 h-4" />
1113
- {webAppInfo.repoInfo.stars.toLocaleString()} stars
1114
- </div>
1115
- <div className="flex items-center gap-1 text-xs text-bolt-elements-textSecondary">
1116
- <div className="i-ph:git-fork text-blue-500 w-4 h-4" />
1117
- {webAppInfo.repoInfo.forks.toLocaleString()} forks
1118
- </div>
1119
- <div className="flex items-center gap-1 text-xs text-bolt-elements-textSecondary">
1120
- <div className="i-ph:warning-circle text-red-500 w-4 h-4" />
1121
- {webAppInfo.repoInfo.openIssues.toLocaleString()} issues
 
 
 
 
 
 
 
 
 
 
 
 
1122
  </div>
1123
- </div>
1124
- </div>
1125
- </div>
1126
- )}
1127
  </div>
1128
  </div>
1129
- ) : (
1130
- <div className="text-sm text-bolt-elements-textSecondary">
1131
- {loading.webAppInfo ? 'Loading webapp information...' : 'No webapp information available'}
 
 
 
 
 
 
 
 
1132
  </div>
1133
  )}
1134
  </div>
@@ -1138,7 +1165,7 @@ export default function DebugTab() {
1138
  {/* Error Check */}
1139
  <Collapsible
1140
  open={openSections.errors}
1141
- onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, errors: open }))}
1142
  className="w-full"
1143
  >
1144
  <CollapsibleTrigger className="w-full">
@@ -1146,14 +1173,14 @@ export default function DebugTab() {
1146
  <div className="flex items-center gap-3">
1147
  <div className="i-ph:warning text-red-500 w-5 h-5" />
1148
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
1149
- {errorLog.errors.length > 0 && (
1150
  <Badge variant="destructive" className="ml-2">
1151
- {errorLog.errors.length} Errors
1152
  </Badge>
1153
  )}
1154
  </div>
1155
  <div
1156
- className={cn(
1157
  'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
1158
  openSections.errors ? 'rotate-180' : '',
1159
  )}
@@ -1175,31 +1202,33 @@ export default function DebugTab() {
1175
  </ul>
1176
  </div>
1177
  <div className="text-sm">
1178
- <span className="text-bolt-elements-textSecondary">Last Check: </span>
1179
  <span className="text-bolt-elements-textPrimary">
1180
  {loading.errors
1181
  ? 'Checking...'
1182
- : errorLog.lastCheck
1183
- ? `Last checked ${new Date(errorLog.lastCheck).toLocaleString()} (${errorLog.errors.length} errors found)`
1184
- : 'Click to check for errors'}
1185
  </span>
1186
  </div>
1187
- {errorLog.errors.length > 0 && (
1188
  <div className="mt-4">
1189
  <div className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Recent Errors:</div>
1190
  <div className="space-y-2">
1191
- {errorLog.errors.slice(0, 3).map((error, index) => (
1192
- <div key={index} className="text-sm text-red-500 dark:text-red-400">
1193
- {error.type === 'error' && `${error.message} (${error.filename}:${error.lineNumber})`}
1194
- {error.type === 'unhandledRejection' && `Unhandled Promise Rejection: ${error.reason}`}
1195
- {error.type === 'networkError' && `Network Error: Failed to load ${error.resource}`}
 
 
 
 
 
 
 
1196
  </div>
1197
  ))}
1198
- {errorLog.errors.length > 3 && (
1199
- <div className="text-sm text-bolt-elements-textSecondary">
1200
- And {errorLog.errors.length - 3} more errors...
1201
- </div>
1202
- )}
1203
  </div>
1204
  </div>
1205
  )}
 
1
+ import React, { useEffect, useState, useMemo } from 'react';
2
  import { toast } from 'react-toastify';
3
  import { classNames } from '~/utils/classNames';
4
+ import { logStore, type LogEntry } from '~/lib/stores/logs';
5
+ import { useStore } from '@nanostores/react';
6
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/Collapsible';
7
  import { Progress } from '~/components/ui/Progress';
8
  import { ScrollArea } from '~/components/ui/ScrollArea';
9
  import { Badge } from '~/components/ui/Badge';
 
10
 
11
  interface SystemInfo {
12
  os: string;
 
87
  };
88
  }
89
 
90
+ interface GitHubRepoInfo {
91
+ fullName: string;
92
+ defaultBranch: string;
93
+ stars: number;
94
+ forks: number;
95
+ openIssues?: number;
96
+ }
 
 
 
 
 
 
 
97
 
98
+ interface GitInfo {
99
+ local: {
100
+ commitHash: string;
101
  branch: string;
 
102
  commitTime: string;
103
  author: string;
104
+ email: string;
105
  remoteUrl: string;
106
+ repoName: string;
107
  };
108
+ github?: {
109
+ currentRepo: GitHubRepoInfo;
110
+ upstream?: GitHubRepoInfo;
 
 
 
 
 
 
 
 
 
 
 
 
111
  };
112
+ isForked?: boolean;
113
  }
114
 
115
+ interface WebAppInfo {
 
116
  name: string;
117
+ version: string;
118
+ description: string;
119
+ license: string;
120
+ environment: string;
121
+ timestamp: string;
122
+ runtimeInfo: {
123
+ nodeVersion: string;
124
+ };
125
+ dependencies: {
126
+ production: Array<{ name: string; version: string; type: string }>;
127
+ development: Array<{ name: string; version: string; type: string }>;
128
+ peer: Array<{ name: string; version: string; type: string }>;
129
+ optional: Array<{ name: string; version: string; type: string }>;
130
  };
131
+ gitInfo: GitInfo;
132
  }
133
 
134
+ const DependencySection = ({
135
+ title,
136
+ deps,
137
+ }: {
138
+ title: string;
139
+ deps: Array<{ name: string; version: string; type: string }>;
140
+ }) => {
141
+ const [isOpen, setIsOpen] = useState(false);
142
+
143
+ if (deps.length === 0) {
144
+ return null;
145
+ }
146
+
147
+ return (
148
+ <Collapsible open={isOpen} onOpenChange={setIsOpen}>
149
+ <CollapsibleTrigger className="flex w-full items-center justify-between p-4 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg">
150
+ <div className="flex items-center gap-3">
151
+ <div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
152
+ <span className="text-base text-bolt-elements-textPrimary">
153
+ {title} Dependencies ({deps.length})
154
+ </span>
155
+ </div>
156
+ <div className="flex items-center gap-2">
157
+ <span className="text-sm text-bolt-elements-textSecondary">{isOpen ? 'Hide' : 'Show'}</span>
158
+ <div
159
+ className={classNames(
160
+ 'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
161
+ isOpen ? 'rotate-180' : '',
162
+ )}
163
+ />
164
+ </div>
165
+ </CollapsibleTrigger>
166
+ <CollapsibleContent>
167
+ <ScrollArea className="h-[200px] w-full p-4">
168
+ <div className="space-y-2 pl-7">
169
+ {deps.map((dep) => (
170
+ <div key={dep.name} className="flex items-center justify-between text-sm">
171
+ <span className="text-bolt-elements-textPrimary">{dep.name}</span>
172
+ <span className="text-bolt-elements-textSecondary">{dep.version}</span>
173
+ </div>
174
+ ))}
175
+ </div>
176
+ </ScrollArea>
177
+ </CollapsibleContent>
178
+ </Collapsible>
179
+ );
180
+ };
181
 
182
  export default function DebugTab() {
183
  const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
184
  const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
185
  const [loading, setLoading] = useState({
186
  systemInfo: false,
 
 
187
  webAppInfo: false,
188
+ errors: false,
189
+ performance: false,
190
  });
 
 
 
 
 
 
 
 
 
191
  const [openSections, setOpenSections] = useState({
192
+ system: false,
193
+ webapp: false,
194
+ errors: false,
195
+ performance: false,
196
  });
197
 
198
+ // Subscribe to logStore updates
199
+ const logs = useStore(logStore.logs);
200
+ const errorLogs = useMemo(() => {
201
+ return Object.values(logs).filter(
202
+ (log): log is LogEntry => typeof log === 'object' && log !== null && 'level' in log && log.level === 'error',
203
+ );
204
+ }, [logs]);
205
 
206
  // Set up error listeners when component mounts
207
  useEffect(() => {
 
 
208
  const handleError = (event: ErrorEvent) => {
209
+ logStore.logError(event.message, event.error, {
 
 
210
  filename: event.filename,
211
  lineNumber: event.lineno,
212
  columnNumber: event.colno,
 
 
213
  });
214
  };
215
 
216
  const handleRejection = (event: PromiseRejectionEvent) => {
217
+ logStore.logError('Unhandled Promise Rejection', event.reason);
 
 
 
 
218
  };
219
 
220
  window.addEventListener('error', handleError);
 
226
  };
227
  }, []);
228
 
229
+ // Check for errors when the errors section is opened
230
+ useEffect(() => {
231
+ if (openSections.errors) {
232
+ checkErrors();
233
+ }
234
+ }, [openSections.errors]);
235
+
236
+ // Load initial data when component mounts
237
+ useEffect(() => {
238
+ const loadInitialData = async () => {
239
+ await Promise.all([getSystemInfo(), getWebAppInfo()]);
240
+ };
241
+
242
+ loadInitialData();
243
+ }, []);
244
+
245
+ // Refresh data when sections are opened
246
+ useEffect(() => {
247
+ if (openSections.system) {
248
+ getSystemInfo();
249
+ }
250
+
251
+ if (openSections.webapp) {
252
+ getWebAppInfo();
253
+ }
254
+ }, [openSections.system, openSections.webapp]);
255
+
256
+ // Add periodic refresh of git info
257
+ useEffect(() => {
258
+ if (!openSections.webapp) {
259
+ return undefined;
260
+ }
261
+
262
+ const interval = setInterval(async () => {
263
+ try {
264
+ const response = await fetch('/api/system/git-info');
265
+ const updatedGitInfo = (await response.json()) as GitInfo;
266
+
267
+ setWebAppInfo((prev) => {
268
+ if (!prev) {
269
+ return null;
270
+ }
271
+
272
+ return {
273
+ ...prev,
274
+ gitInfo: updatedGitInfo,
275
+ };
276
+ });
277
+ } catch (error) {
278
+ console.error('Failed to refresh git info:', error);
279
+ }
280
+ }, 5000);
281
+
282
+ const cleanup = () => {
283
+ clearInterval(interval);
284
+ };
285
+
286
+ return cleanup;
287
+ }, [openSections.webapp]);
288
+
289
  const getSystemInfo = async () => {
290
  try {
291
  setLoading((prev) => ({ ...prev, systemInfo: true }));
 
435
  try {
436
  setLoading((prev) => ({ ...prev, webAppInfo: true }));
437
 
438
+ const [appResponse, gitResponse] = await Promise.all([
439
+ fetch('/api/system/app-info'),
440
+ fetch('/api/system/git-info'),
441
+ ]);
442
 
443
+ if (!appResponse.ok || !gitResponse.ok) {
444
  throw new Error('Failed to fetch webapp info');
445
  }
446
 
447
+ const appData = (await appResponse.json()) as Omit<WebAppInfo, 'gitInfo'>;
448
+ const gitData = (await gitResponse.json()) as GitInfo;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
 
450
  setWebAppInfo({
451
+ ...appData,
452
+ gitInfo: gitData,
 
 
 
 
 
 
 
 
453
  });
454
+
455
+ toast.success('WebApp information updated');
456
+
457
+ return true;
458
  } catch (error) {
459
  console.error('Failed to fetch webapp info:', error);
460
  toast.error('Failed to fetch webapp information');
461
+ setWebAppInfo(null);
462
+
463
+ return false;
464
  } finally {
465
  setLoading((prev) => ({ ...prev, webAppInfo: false }));
466
  }
 
569
  setLoading((prev) => ({ ...prev, errors: true }));
570
 
571
  // Get errors from log store
572
+ const storedErrors = errorLogs;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
 
574
+ if (storedErrors.length === 0) {
575
  toast.success('No errors found');
576
  } else {
577
+ toast.warning(`Found ${storedErrors.length} error(s)`);
578
  }
579
  } catch (error) {
580
  toast.error('Failed to check errors');
 
590
  timestamp: new Date().toISOString(),
591
  system: systemInfo,
592
  webApp: webAppInfo,
593
+ errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
594
  performance: {
595
  memory: (performance as any).memory || {},
596
  timing: performance.timing,
 
646
 
647
  <div className="p-4 rounded-xl bg-gradient-to-br from-red-500/10 to-red-500/5 border border-red-500/20">
648
  <div className="text-sm text-bolt-elements-textSecondary">Errors</div>
649
+ <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLogs.length}</div>
 
 
 
650
  </div>
651
  </div>
652
 
 
760
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
761
  </div>
762
  <div
763
+ className={classNames(
764
  'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
765
  openSections.system ? 'rotate-180' : '',
766
  )}
 
907
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
908
  </div>
909
  <div
910
+ className={classNames(
911
  'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
912
  openSections.performance ? 'rotate-180' : '',
913
  )}
 
987
  {/* WebApp Information */}
988
  <Collapsible
989
  open={openSections.webapp}
990
+ onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, webapp: open }))}
991
  className="w-full"
992
  >
993
  <CollapsibleTrigger className="w-full">
 
995
  <div className="flex items-center gap-3">
996
  <div className="i-ph:info text-blue-500 w-5 h-5" />
997
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
998
+ {loading.webAppInfo && <span className="loading loading-spinner loading-sm" />}
999
  </div>
1000
  <div
1001
+ className={classNames(
1002
  'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
1003
  openSections.webapp ? 'rotate-180' : '',
1004
  )}
 
1008
 
1009
  <CollapsibleContent>
1010
  <div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
1011
+ {loading.webAppInfo ? (
1012
+ <div className="flex items-center justify-center p-8">
1013
+ <span className="loading loading-spinner loading-lg" />
1014
+ </div>
1015
+ ) : !webAppInfo ? (
1016
+ <div className="flex flex-col items-center justify-center p-8 text-bolt-elements-textSecondary">
1017
+ <div className="i-ph:warning-circle w-8 h-8 mb-2" />
1018
+ <p>Failed to load WebApp information</p>
1019
+ <button
1020
+ onClick={() => getWebAppInfo()}
1021
+ className="mt-4 px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
1022
+ >
1023
+ Retry
1024
+ </button>
1025
+ </div>
1026
+ ) : (
1027
+ <div className="grid grid-cols-2 gap-6">
1028
+ <div>
1029
+ <h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Basic Information</h3>
1030
+ <div className="space-y-3">
 
 
 
 
 
 
 
 
 
1031
  <div className="text-sm flex items-center gap-2">
1032
+ <div className="i-ph:app-window text-bolt-elements-textSecondary w-4 h-4" />
1033
+ <span className="text-bolt-elements-textSecondary">Name:</span>
1034
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.name}</span>
1035
  </div>
 
 
1036
  <div className="text-sm flex items-center gap-2">
1037
+ <div className="i-ph:tag text-bolt-elements-textSecondary w-4 h-4" />
1038
+ <span className="text-bolt-elements-textSecondary">Version:</span>
1039
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.version}</span>
1040
+ </div>
1041
+ <div className="text-sm flex items-center gap-2">
1042
+ <div className="i-ph:certificate text-bolt-elements-textSecondary w-4 h-4" />
1043
+ <span className="text-bolt-elements-textSecondary">License:</span>
1044
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.license}</span>
1045
  </div>
 
 
1046
  <div className="text-sm flex items-center gap-2">
1047
  <div className="i-ph:cloud text-bolt-elements-textSecondary w-4 h-4" />
1048
+ <span className="text-bolt-elements-textSecondary">Environment:</span>
1049
  <span className="text-bolt-elements-textPrimary">{webAppInfo.environment}</span>
1050
  </div>
1051
+ <div className="text-sm flex items-center gap-2">
1052
+ <div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
1053
+ <span className="text-bolt-elements-textSecondary">Node Version:</span>
1054
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.runtimeInfo.nodeVersion}</span>
1055
+ </div>
1056
+ </div>
1057
  </div>
1058
+
1059
+ <div>
1060
+ <h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Git Information</h3>
1061
+ <div className="space-y-3">
1062
+ <div className="text-sm flex items-center gap-2">
1063
+ <div className="i-ph:git-branch text-bolt-elements-textSecondary w-4 h-4" />
1064
+ <span className="text-bolt-elements-textSecondary">Branch:</span>
1065
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.branch}</span>
1066
  </div>
1067
+ <div className="text-sm flex items-center gap-2">
1068
+ <div className="i-ph:git-commit text-bolt-elements-textSecondary w-4 h-4" />
1069
+ <span className="text-bolt-elements-textSecondary">Commit:</span>
1070
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitHash}</span>
 
 
 
 
1071
  </div>
1072
+ <div className="text-sm flex items-center gap-2">
1073
+ <div className="i-ph:user text-bolt-elements-textSecondary w-4 h-4" />
1074
+ <span className="text-bolt-elements-textSecondary">Author:</span>
1075
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.author}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1076
  </div>
1077
+ <div className="text-sm flex items-center gap-2">
1078
+ <div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
1079
+ <span className="text-bolt-elements-textSecondary">Commit Time:</span>
1080
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitTime}</span>
1081
+ </div>
1082
+
1083
+ {webAppInfo.gitInfo.github && (
1084
+ <>
1085
+ <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
1086
+ <div className="text-sm flex items-center gap-2">
1087
+ <div className="i-ph:git-fork text-bolt-elements-textSecondary w-4 h-4" />
1088
+ <span className="text-bolt-elements-textSecondary">Repository:</span>
1089
+ <span className="text-bolt-elements-textPrimary">
1090
+ {webAppInfo.gitInfo.github.currentRepo.fullName}
1091
+ {webAppInfo.gitInfo.isForked && ' (fork)'}
1092
+ </span>
1093
+ </div>
1094
+
1095
+ <div className="mt-2 flex items-center gap-4 text-sm">
1096
+ <div className="flex items-center gap-1">
1097
+ <div className="i-ph:star text-yellow-500 w-4 h-4" />
1098
+ <span className="text-bolt-elements-textSecondary">
1099
+ {webAppInfo.gitInfo.github.currentRepo.stars}
1100
+ </span>
1101
+ </div>
1102
+ <div className="flex items-center gap-1">
1103
+ <div className="i-ph:git-fork text-blue-500 w-4 h-4" />
1104
+ <span className="text-bolt-elements-textSecondary">
1105
+ {webAppInfo.gitInfo.github.currentRepo.forks}
1106
+ </span>
1107
  </div>
1108
+ <div className="flex items-center gap-1">
1109
+ <div className="i-ph:warning-circle text-red-500 w-4 h-4" />
1110
+ <span className="text-bolt-elements-textSecondary">
1111
+ {webAppInfo.gitInfo.github.currentRepo.openIssues}
1112
+ </span>
1113
  </div>
1114
  </div>
1115
  </div>
1116
 
1117
+ {webAppInfo.gitInfo.github.upstream && (
1118
+ <div className="mt-2">
1119
+ <div className="text-sm flex items-center gap-2">
1120
+ <div className="i-ph:git-fork text-bolt-elements-textSecondary w-4 h-4" />
1121
+ <span className="text-bolt-elements-textSecondary">Upstream:</span>
1122
+ <span className="text-bolt-elements-textPrimary">
1123
+ {webAppInfo.gitInfo.github.upstream.fullName}
1124
+ </span>
1125
+ </div>
1126
+
1127
+ <div className="mt-2 flex items-center gap-4 text-sm">
1128
+ <div className="flex items-center gap-1">
1129
+ <div className="i-ph:star text-yellow-500 w-4 h-4" />
1130
+ <span className="text-bolt-elements-textSecondary">
1131
+ {webAppInfo.gitInfo.github.upstream.stars}
1132
+ </span>
1133
+ </div>
1134
+ <div className="flex items-center gap-1">
1135
+ <div className="i-ph:git-fork text-blue-500 w-4 h-4" />
1136
+ <span className="text-bolt-elements-textSecondary">
1137
+ {webAppInfo.gitInfo.github.upstream.forks}
1138
+ </span>
1139
+ </div>
1140
+ </div>
1141
  </div>
1142
+ )}
1143
+ </>
1144
+ )}
1145
+ </div>
1146
  </div>
1147
  </div>
1148
+ )}
1149
+
1150
+ {webAppInfo && (
1151
+ <div className="mt-6">
1152
+ <h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Dependencies</h3>
1153
+ <div className="space-y-2 bg-gray-50 dark:bg-[#1A1A1A] rounded-lg">
1154
+ <DependencySection title="Production" deps={webAppInfo.dependencies.production} />
1155
+ <DependencySection title="Development" deps={webAppInfo.dependencies.development} />
1156
+ <DependencySection title="Peer" deps={webAppInfo.dependencies.peer} />
1157
+ <DependencySection title="Optional" deps={webAppInfo.dependencies.optional} />
1158
+ </div>
1159
  </div>
1160
  )}
1161
  </div>
 
1165
  {/* Error Check */}
1166
  <Collapsible
1167
  open={openSections.errors}
1168
+ onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, errors: open }))}
1169
  className="w-full"
1170
  >
1171
  <CollapsibleTrigger className="w-full">
 
1173
  <div className="flex items-center gap-3">
1174
  <div className="i-ph:warning text-red-500 w-5 h-5" />
1175
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
1176
+ {errorLogs.length > 0 && (
1177
  <Badge variant="destructive" className="ml-2">
1178
+ {errorLogs.length} Errors
1179
  </Badge>
1180
  )}
1181
  </div>
1182
  <div
1183
+ className={classNames(
1184
  'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
1185
  openSections.errors ? 'rotate-180' : '',
1186
  )}
 
1202
  </ul>
1203
  </div>
1204
  <div className="text-sm">
1205
+ <span className="text-bolt-elements-textSecondary">Status: </span>
1206
  <span className="text-bolt-elements-textPrimary">
1207
  {loading.errors
1208
  ? 'Checking...'
1209
+ : errorLogs.length > 0
1210
+ ? `${errorLogs.length} errors found`
1211
+ : 'No errors found'}
1212
  </span>
1213
  </div>
1214
+ {errorLogs.length > 0 && (
1215
  <div className="mt-4">
1216
  <div className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Recent Errors:</div>
1217
  <div className="space-y-2">
1218
+ {errorLogs.map((error) => (
1219
+ <div key={error.id} className="text-sm text-red-500 dark:text-red-400 p-2 rounded bg-red-500/5">
1220
+ <div className="font-medium">{error.message}</div>
1221
+ {error.source && (
1222
+ <div className="text-xs mt-1 text-red-400">
1223
+ Source: {error.source}
1224
+ {error.details?.lineNumber && `:${error.details.lineNumber}`}
1225
+ </div>
1226
+ )}
1227
+ {error.stack && (
1228
+ <div className="text-xs mt-1 text-red-400 font-mono whitespace-pre-wrap">{error.stack}</div>
1229
+ )}
1230
  </div>
1231
  ))}
 
 
 
 
 
1232
  </div>
1233
  </div>
1234
  )}
app/components/settings/developer/DeveloperWindow.tsx CHANGED
@@ -30,6 +30,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
30
  import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
31
  import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
32
  import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
 
33
  import { Switch } from '~/components/ui/Switch';
34
 
35
  interface DraggableTabTileProps {
@@ -57,7 +58,7 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
57
  'event-logs': 'View application event logs',
58
  update: 'Check for updates',
59
  'task-manager': 'Manage running tasks',
60
- 'service-status': 'View service health and status',
61
  };
62
 
63
  const DraggableTabTile = ({
@@ -207,8 +208,6 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
207
 
208
  // Only show tabs that are assigned to the developer window AND are visible
209
  const visibleDeveloperTabs = useMemo(() => {
210
- console.log('Filtering developer tabs with configuration:', tabConfiguration);
211
-
212
  if (!tabConfiguration?.developerTabs || !Array.isArray(tabConfiguration.developerTabs)) {
213
  console.warn('Invalid tab configuration, using empty array');
214
  return [];
@@ -223,7 +222,6 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
223
 
224
  // Hide notifications tab if notifications are disabled
225
  if (tab.id === 'notifications' && !profile.notifications) {
226
- console.log('Hiding notifications tab due to disabled notifications');
227
  return false;
228
  }
229
 
@@ -235,7 +233,6 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
235
 
236
  // Only show tabs that are explicitly visible and assigned to the developer window
237
  const isVisible = tab.visible && tab.window === 'developer';
238
- console.log(`Tab ${tab.id} visibility:`, isVisible);
239
 
240
  return isVisible;
241
  })
@@ -247,8 +244,6 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
247
  });
248
  }, [tabConfiguration, profile.notifications]);
249
 
250
- console.log('Filtered visible developer tabs:', visibleDeveloperTabs);
251
-
252
  const moveTab = (dragIndex: number, hoverIndex: number) => {
253
  const draggedTab = visibleDeveloperTabs[dragIndex];
254
  const targetTab = visibleDeveloperTabs[hoverIndex];
@@ -324,6 +319,8 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
324
  return <UpdateTab />;
325
  case 'task-manager':
326
  return <TaskManagerTab />;
 
 
327
  default:
328
  return null;
329
  }
 
30
  import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
31
  import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
32
  import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
33
+ import ServiceStatusTab from '~/components/settings/providers/ServiceStatusTab';
34
  import { Switch } from '~/components/ui/Switch';
35
 
36
  interface DraggableTabTileProps {
 
58
  'event-logs': 'View application event logs',
59
  update: 'Check for updates',
60
  'task-manager': 'Manage running tasks',
61
+ 'service-status': 'Monitor provider service health and status',
62
  };
63
 
64
  const DraggableTabTile = ({
 
208
 
209
  // Only show tabs that are assigned to the developer window AND are visible
210
  const visibleDeveloperTabs = useMemo(() => {
 
 
211
  if (!tabConfiguration?.developerTabs || !Array.isArray(tabConfiguration.developerTabs)) {
212
  console.warn('Invalid tab configuration, using empty array');
213
  return [];
 
222
 
223
  // Hide notifications tab if notifications are disabled
224
  if (tab.id === 'notifications' && !profile.notifications) {
 
225
  return false;
226
  }
227
 
 
233
 
234
  // Only show tabs that are explicitly visible and assigned to the developer window
235
  const isVisible = tab.visible && tab.window === 'developer';
 
236
 
237
  return isVisible;
238
  })
 
244
  });
245
  }, [tabConfiguration, profile.notifications]);
246
 
 
 
247
  const moveTab = (dragIndex: number, hoverIndex: number) => {
248
  const draggedTab = visibleDeveloperTabs[dragIndex];
249
  const targetTab = visibleDeveloperTabs[hoverIndex];
 
319
  return <UpdateTab />;
320
  case 'task-manager':
321
  return <TaskManagerTab />;
322
+ case 'service-status':
323
+ return <ServiceStatusTab />;
324
  default:
325
  return null;
326
  }
app/components/settings/developer/TabManagement.tsx CHANGED
@@ -177,7 +177,15 @@ export const TabManagement = () => {
177
  const [searchQuery, setSearchQuery] = useState('');
178
 
179
  // Define standard (visible by default) tabs for each window
180
- const standardUserTabs: TabType[] = ['features', 'data', 'local-providers', 'cloud-providers', 'connection', 'debug'];
 
 
 
 
 
 
 
 
181
  const standardDeveloperTabs: TabType[] = [
182
  'profile',
183
  'settings',
@@ -190,6 +198,8 @@ export const TabManagement = () => {
190
  'debug',
191
  'event-logs',
192
  'update',
 
 
193
  ];
194
 
195
  const handleVisibilityChange = (tabId: TabType, enabled: boolean, targetWindow: 'user' | 'developer') => {
 
177
  const [searchQuery, setSearchQuery] = useState('');
178
 
179
  // Define standard (visible by default) tabs for each window
180
+ const standardUserTabs: TabType[] = [
181
+ 'features',
182
+ 'data',
183
+ 'local-providers',
184
+ 'cloud-providers',
185
+ 'connection',
186
+ 'debug',
187
+ 'service-status',
188
+ ];
189
  const standardDeveloperTabs: TabType[] = [
190
  'profile',
191
  'settings',
 
198
  'debug',
199
  'event-logs',
200
  'update',
201
+ 'task-manager',
202
+ 'service-status',
203
  ];
204
 
205
  const handleVisibilityChange = (tabId: TabType, enabled: boolean, targetWindow: 'user' | 'developer') => {
app/components/settings/event-logs/EventLogsTab.tsx CHANGED
@@ -5,7 +5,6 @@ import { logStore, type LogEntry } from '~/lib/stores/logs';
5
  import { useStore } from '@nanostores/react';
6
  import { classNames } from '~/utils/classNames';
7
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
8
- import { settingsStyles } from '~/components/settings/settings.styles';
9
 
10
  interface SelectOption {
11
  value: string;
@@ -231,7 +230,13 @@ export function EventLogsTab() {
231
  const selectedCategoryOption = logCategoryOptions.find((opt) => opt.value === selectedCategory);
232
 
233
  return (
234
- <div className="flex flex-col h-full gap-4 p-6">
 
 
 
 
 
 
235
  {/* Header */}
236
  <div className="flex items-center justify-between">
237
  <div className="flex items-center gap-3">
@@ -245,10 +250,11 @@ export function EventLogsTab() {
245
  <button
246
  onClick={handleRefresh}
247
  className={classNames(
248
- settingsStyles.button.base,
249
- settingsStyles.button.secondary,
250
- 'hover:bg-purple-500/10 hover:text-purple-500',
251
- 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
 
252
  )}
253
  >
254
  <div className="i-ph:arrows-clockwise text-lg" />
@@ -292,7 +298,13 @@ export function EventLogsTab() {
292
 
293
  <motion.button
294
  onClick={handleExportLogs}
295
- className="flex items-center gap-2 px-4 py-1.5 rounded-lg text-sm bg-purple-500 hover:bg-purple-600 text-white transition-colors"
 
 
 
 
 
 
296
  whileHover={{ scale: 1.02 }}
297
  whileTap={{ scale: 0.98 }}
298
  >
@@ -309,7 +321,6 @@ export function EventLogsTab() {
309
  <DropdownMenu.Trigger asChild>
310
  <button
311
  className={classNames(
312
- 'flex items-center gap-2',
313
  'rounded-lg px-3 py-1.5',
314
  'text-sm text-gray-900 dark:text-white',
315
  'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
@@ -360,7 +371,6 @@ export function EventLogsTab() {
360
  <DropdownMenu.Trigger asChild>
361
  <button
362
  className={classNames(
363
- 'flex items-center gap-2',
364
  'rounded-lg px-3 py-1.5',
365
  'text-sm text-gray-900 dark:text-white',
366
  'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
 
5
  import { useStore } from '@nanostores/react';
6
  import { classNames } from '~/utils/classNames';
7
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
 
8
 
9
  interface SelectOption {
10
  value: string;
 
230
  const selectedCategoryOption = logCategoryOptions.find((opt) => opt.value === selectedCategory);
231
 
232
  return (
233
+ <div
234
+ className={classNames(
235
+ 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
236
+ 'hover:bg-bolt-elements-background-depth-2',
237
+ 'transition-all duration-200',
238
+ )}
239
+ >
240
  {/* Header */}
241
  <div className="flex items-center justify-between">
242
  <div className="flex items-center gap-3">
 
250
  <button
251
  onClick={handleRefresh}
252
  className={classNames(
253
+ 'rounded-md px-4 py-2 text-sm',
254
+ 'bg-purple-500 text-white',
255
+ 'hover:bg-purple-600',
256
+ 'dark:bg-purple-500 dark:hover:bg-purple-600',
257
+ 'transition-all duration-200',
258
  )}
259
  >
260
  <div className="i-ph:arrows-clockwise text-lg" />
 
298
 
299
  <motion.button
300
  onClick={handleExportLogs}
301
+ className={classNames(
302
+ 'rounded-md px-4 py-2 text-sm',
303
+ 'bg-purple-500 text-white',
304
+ 'hover:bg-purple-600',
305
+ 'dark:bg-purple-500 dark:hover:bg-purple-600',
306
+ 'transition-all duration-200',
307
+ )}
308
  whileHover={{ scale: 1.02 }}
309
  whileTap={{ scale: 0.98 }}
310
  >
 
321
  <DropdownMenu.Trigger asChild>
322
  <button
323
  className={classNames(
 
324
  'rounded-lg px-3 py-1.5',
325
  'text-sm text-gray-900 dark:text-white',
326
  'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
 
371
  <DropdownMenu.Trigger asChild>
372
  <button
373
  className={classNames(
 
374
  'rounded-lg px-3 py-1.5',
375
  'text-sm text-gray-900 dark:text-white',
376
  'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
app/components/settings/profile/ProfileTab.tsx CHANGED
@@ -4,7 +4,6 @@ import { toast } from 'react-toastify';
4
  import { classNames } from '~/utils/classNames';
5
  import type { UserProfile } from '~/components/settings/settings.types';
6
  import { motion } from 'framer-motion';
7
- import { settingsStyles } from '~/components/settings/settings.styles';
8
 
9
  const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
10
  const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
@@ -112,7 +111,13 @@ export default function ProfileTab() {
112
  };
113
 
114
  return (
115
- <div className="space-y-4">
 
 
 
 
 
 
116
  {/* Profile Information */}
117
  <motion.div
118
  className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none"
@@ -249,10 +254,11 @@ export default function ProfileTab() {
249
  onClick={handleSave}
250
  disabled={isLoading}
251
  className={classNames(
252
- settingsStyles.button.base,
253
- settingsStyles.button.primary,
254
  'hover:bg-purple-600',
255
- 'disabled:opacity-50 disabled:cursor-not-allowed',
 
256
  )}
257
  >
258
  {isLoading ? (
 
4
  import { classNames } from '~/utils/classNames';
5
  import type { UserProfile } from '~/components/settings/settings.types';
6
  import { motion } from 'framer-motion';
 
7
 
8
  const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
9
  const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
 
111
  };
112
 
113
  return (
114
+ <div
115
+ className={classNames(
116
+ 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
117
+ 'hover:bg-bolt-elements-background-depth-2',
118
+ 'transition-all duration-200',
119
+ )}
120
+ >
121
  {/* Profile Information */}
122
  <motion.div
123
  className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none"
 
254
  onClick={handleSave}
255
  disabled={isLoading}
256
  className={classNames(
257
+ 'rounded-md px-4 py-2 text-sm',
258
+ 'bg-purple-500 text-white',
259
  'hover:bg-purple-600',
260
+ 'dark:bg-purple-500 dark:hover:bg-purple-600',
261
+ 'transition-all duration-200',
262
  )}
263
  >
264
  {isLoading ? (
app/components/settings/providers/CloudProvidersTab.tsx CHANGED
@@ -6,7 +6,6 @@ import type { IProviderConfig } from '~/types/model';
6
  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 { providerBaseUrlEnvKeys } from '~/utils/constants';
12
  import { SiAmazon, SiGoogle, SiHuggingface, SiPerplexity, SiOpenai } from 'react-icons/si';
@@ -168,7 +167,7 @@ const CloudProvidersTab = () => {
168
  <motion.div
169
  key={provider.name}
170
  className={classNames(
171
- settingsStyles.card,
172
  'bg-bolt-elements-background-depth-2',
173
  'hover:bg-bolt-elements-background-depth-3',
174
  'transition-all duration-200',
 
6
  import { logStore } from '~/lib/stores/logs';
7
  import { motion } from 'framer-motion';
8
  import { classNames } from '~/utils/classNames';
 
9
  import { toast } from 'react-toastify';
10
  import { providerBaseUrlEnvKeys } from '~/utils/constants';
11
  import { SiAmazon, SiGoogle, SiHuggingface, SiPerplexity, SiOpenai } from 'react-icons/si';
 
167
  <motion.div
168
  key={provider.name}
169
  className={classNames(
170
+ 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm',
171
  'bg-bolt-elements-background-depth-2',
172
  'hover:bg-bolt-elements-background-depth-3',
173
  'transition-all duration-200',
app/components/settings/providers/LocalProvidersTab.tsx CHANGED
@@ -6,7 +6,6 @@ import type { IProviderConfig } from '~/types/model';
6
  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 { BsRobot } from 'react-icons/bs';
11
  import type { IconType } from 'react-icons';
12
  import { BiChip } from 'react-icons/bi';
@@ -473,7 +472,13 @@ export function LocalProvidersTab() {
473
  }, []);
474
 
475
  return (
476
- <div className="space-y-6">
 
 
 
 
 
 
477
  {/* Service Status Indicator - Move to top */}
478
  <div
479
  className={classNames(
@@ -526,7 +531,6 @@ export function LocalProvidersTab() {
526
  <motion.div
527
  key={provider.name}
528
  className={classNames(
529
- settingsStyles.card,
530
  'bg-bolt-elements-background-depth-2',
531
  'hover:bg-bolt-elements-background-depth-3',
532
  'transition-all duration-200',
@@ -728,9 +732,11 @@ export function LocalProvidersTab() {
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 }}
@@ -746,9 +752,11 @@ export function LocalProvidersTab() {
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 }}
@@ -839,10 +847,11 @@ export function LocalProvidersTab() {
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 }}
@@ -867,10 +876,11 @@ export function LocalProvidersTab() {
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 }}
 
6
  import { logStore } from '~/lib/stores/logs';
7
  import { motion } from 'framer-motion';
8
  import { classNames } from '~/utils/classNames';
 
9
  import { BsRobot } from 'react-icons/bs';
10
  import type { IconType } from 'react-icons';
11
  import { BiChip } from 'react-icons/bi';
 
472
  }, []);
473
 
474
  return (
475
+ <div
476
+ className={classNames(
477
+ 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
478
+ 'hover:bg-bolt-elements-background-depth-2',
479
+ 'transition-all duration-200',
480
+ )}
481
+ >
482
  {/* Service Status Indicator - Move to top */}
483
  <div
484
  className={classNames(
 
531
  <motion.div
532
  key={provider.name}
533
  className={classNames(
 
534
  'bg-bolt-elements-background-depth-2',
535
  'hover:bg-bolt-elements-background-depth-3',
536
  'transition-all duration-200',
 
732
  onClick={() => handleUpdateOllamaModel(model.name)}
733
  disabled={model.status === 'updating'}
734
  className={classNames(
735
+ 'rounded-md px-4 py-2 text-sm',
736
+ 'bg-purple-500 text-white',
737
+ 'hover:bg-purple-600',
738
+ 'dark:bg-purple-500 dark:hover:bg-purple-600',
739
+ 'transition-all duration-200',
740
  )}
741
  whileHover={{ scale: 1.02 }}
742
  whileTap={{ scale: 0.98 }}
 
752
  }}
753
  disabled={model.status === 'updating'}
754
  className={classNames(
755
+ 'rounded-md px-4 py-2 text-sm',
756
+ 'bg-red-500 text-white',
757
+ 'hover:bg-red-600',
758
+ 'dark:bg-red-500 dark:hover:bg-red-600',
759
+ 'transition-all duration-200',
760
  )}
761
  whileHover={{ scale: 1.02 }}
762
  whileTap={{ scale: 0.98 }}
 
847
  onClick={() => handleManualInstall(manualInstall.modelString)}
848
  disabled={!manualInstall.modelString || !!isInstallingModel}
849
  className={classNames(
850
+ 'rounded-md px-4 py-2 text-sm',
851
+ 'bg-purple-500 text-white',
852
+ 'hover:bg-purple-600',
853
+ 'dark:bg-purple-500 dark:hover:bg-purple-600',
854
+ 'transition-all duration-200',
855
  )}
856
  whileHover={{ scale: 1.02 }}
857
  whileTap={{ scale: 0.98 }}
 
876
  error('Installation cancelled');
877
  }}
878
  className={classNames(
879
+ 'rounded-md px-4 py-2 text-sm',
880
+ 'bg-red-500 text-white',
881
+ 'hover:bg-red-600',
882
+ 'dark:bg-red-500 dark:hover:bg-red-600',
883
+ 'transition-all duration-200',
884
  )}
885
  whileHover={{ scale: 1.02 }}
886
  whileTap={{ scale: 0.98 }}
app/components/settings/providers/OllamaModelUpdater.tsx CHANGED
@@ -2,8 +2,6 @@ import React, { useEffect, useState } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { toast } from 'react-toastify';
4
  import { classNames } from '~/utils/classNames';
5
- import { settingsStyles } from '~/components/settings/settings.styles';
6
- import { DialogTitle, DialogDescription } from '~/components/ui/Dialog';
7
 
8
  interface OllamaModel {
9
  name: string;
@@ -197,108 +195,135 @@ export default function OllamaModelUpdater() {
197
  if (isLoading) {
198
  return (
199
  <div className="flex items-center justify-center p-4">
200
- <div className={settingsStyles['loading-spinner']} />
 
 
 
 
 
201
  <span className="ml-2 text-bolt-elements-textSecondary">Loading models...</span>
202
  </div>
203
  );
204
  }
205
 
206
  return (
207
- <div className="space-y-4">
208
- <div className="space-y-2">
209
- <DialogTitle>Ollama Model Manager</DialogTitle>
210
- <DialogDescription>Update your local Ollama models to their latest versions</DialogDescription>
211
- </div>
212
-
213
- <div className="flex items-center justify-between">
214
- <div className="flex items-center gap-2">
215
- <div className="i-ph:arrows-clockwise text-purple-500" />
216
- <span className="text-sm text-bolt-elements-textPrimary">{models.length} models available</span>
 
217
  </div>
218
- <motion.button
219
- onClick={handleBulkUpdate}
220
- disabled={isBulkUpdating}
221
- className={classNames(
222
- settingsStyles.button.base,
223
- settingsStyles.button.primary,
224
- 'hover:bg-purple-500/10 hover:text-purple-500',
225
- 'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
226
- )}
227
- whileHover={{ scale: 1.02 }}
228
- whileTap={{ scale: 0.98 }}
229
- >
230
- {isBulkUpdating ? (
231
- <>
232
- <div className={settingsStyles['loading-spinner']} />
233
- Updating All...
234
- </>
235
- ) : (
236
- <>
237
- <div className="i-ph:arrows-clockwise" />
238
- Update All Models
239
- </>
240
- )}
241
- </motion.button>
242
- </div>
243
 
244
- <div className="space-y-2">
245
- {models.map((model) => (
246
- <div
247
- key={model.name}
 
 
 
 
248
  className={classNames(
249
- 'flex items-center justify-between p-3 rounded-lg',
250
- 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
251
- 'border border-[#E5E5E5] dark:border-[#333333]',
 
 
252
  )}
 
 
253
  >
254
- <div className="flex flex-col gap-1">
255
- <div className="flex items-center gap-2">
256
- <div className="i-ph:cube text-purple-500" />
257
- <span className="text-sm text-bolt-elements-textPrimary">{model.name}</span>
258
- {model.status === 'updating' && <div className={settingsStyles['loading-spinner']} />}
259
- {model.status === 'updated' && <div className="i-ph:check-circle text-green-500" />}
260
- {model.status === 'error' && <div className="i-ph:x-circle text-red-500" />}
261
- </div>
262
- <div className="flex items-center gap-2 text-xs text-bolt-elements-textSecondary">
263
- <span>Version: {model.digest.substring(0, 7)}</span>
264
- {model.status === 'updated' && model.newDigest && (
265
- <>
266
- <div className="i-ph:arrow-right w-3 h-3" />
267
- <span className="text-green-500">{model.newDigest.substring(0, 7)}</span>
268
- </>
269
- )}
270
- {model.progress && (
271
- <span className="ml-2">
272
- {model.progress.status}{' '}
273
- {model.progress.total > 0 && (
274
- <>({Math.round((model.progress.current / model.progress.total) * 100)}%)</>
275
- )}
276
- </span>
277
- )}
278
- {model.details && (
279
- <span className="ml-2">
280
- ({model.details.parameter_size}, {model.details.quantization_level})
281
- </span>
282
- )}
283
- </div>
284
- </div>
285
- <motion.button
286
- onClick={() => handleSingleUpdate(model.name)}
287
- disabled={model.status === 'updating'}
288
  className={classNames(
289
- settingsStyles.button.base,
290
- settingsStyles.button.secondary,
291
- 'hover:bg-purple-500/10 hover:text-purple-500',
292
- 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
293
  )}
294
- whileHover={{ scale: 1.02 }}
295
- whileTap={{ scale: 0.98 }}
296
  >
297
- <div className="i-ph:arrows-clockwise" />
298
- Update
299
- </motion.button>
300
- </div>
301
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  </div>
303
  </div>
304
  );
 
2
  import { motion } from 'framer-motion';
3
  import { toast } from 'react-toastify';
4
  import { classNames } from '~/utils/classNames';
 
 
5
 
6
  interface OllamaModel {
7
  name: string;
 
195
  if (isLoading) {
196
  return (
197
  <div className="flex items-center justify-center p-4">
198
+ <div
199
+ className={classNames(
200
+ 'rounded-full border-4 border-t-4 border-b-4 border-purple-500',
201
+ 'h-16 w-16 animate-spin',
202
+ )}
203
+ />
204
  <span className="ml-2 text-bolt-elements-textSecondary">Loading models...</span>
205
  </div>
206
  );
207
  }
208
 
209
  return (
210
+ <div
211
+ className={classNames(
212
+ 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
213
+ 'hover:bg-bolt-elements-background-depth-2',
214
+ 'transition-all duration-200',
215
+ )}
216
+ >
217
+ <div className="space-y-4">
218
+ <div className="space-y-2">
219
+ <h2 className="text-2xl font-bold">Ollama Model Manager</h2>
220
+ <p>Update your local Ollama models to their latest versions</p>
221
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
+ <div className="flex items-center justify-between">
224
+ <div className="flex items-center gap-2">
225
+ <div className="i-ph:arrows-clockwise text-purple-500" />
226
+ <span className="text-sm text-bolt-elements-textPrimary">{models.length} models available</span>
227
+ </div>
228
+ <motion.button
229
+ onClick={handleBulkUpdate}
230
+ disabled={isBulkUpdating}
231
  className={classNames(
232
+ 'rounded-md px-4 py-2 text-sm',
233
+ 'bg-purple-500 text-white',
234
+ 'hover:bg-purple-600',
235
+ 'dark:bg-purple-500 dark:hover:bg-purple-600',
236
+ 'transition-all duration-200',
237
  )}
238
+ whileHover={{ scale: 1.02 }}
239
+ whileTap={{ scale: 0.98 }}
240
  >
241
+ {isBulkUpdating ? (
242
+ <>
243
+ <div
244
+ className={classNames(
245
+ 'rounded-full border-4 border-t-4 border-b-4 border-purple-500',
246
+ 'h-4 w-4 animate-spin mr-2',
247
+ )}
248
+ />
249
+ Updating All...
250
+ </>
251
+ ) : (
252
+ <>
253
+ <div className="i-ph:arrows-clockwise" />
254
+ Update All Models
255
+ </>
256
+ )}
257
+ </motion.button>
258
+ </div>
259
+
260
+ <div className="space-y-2">
261
+ {models.map((model) => (
262
+ <div
263
+ key={model.name}
 
 
 
 
 
 
 
 
 
 
 
264
  className={classNames(
265
+ 'flex items-center justify-between p-3 rounded-lg',
266
+ 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
267
+ 'border border-[#E5E5E5] dark:border-[#333333]',
 
268
  )}
 
 
269
  >
270
+ <div className="flex flex-col gap-1">
271
+ <div className="flex items-center gap-2">
272
+ <div className="i-ph:cube text-purple-500" />
273
+ <span className="text-sm text-bolt-elements-textPrimary">{model.name}</span>
274
+ {model.status === 'updating' && (
275
+ <div
276
+ className={classNames(
277
+ 'rounded-full border-4 border-t-4 border-b-4 border-purple-500',
278
+ 'h-4 w-4 animate-spin',
279
+ )}
280
+ />
281
+ )}
282
+ {model.status === 'updated' && <div className="i-ph:check-circle text-green-500" />}
283
+ {model.status === 'error' && <div className="i-ph:x-circle text-red-500" />}
284
+ </div>
285
+ <div className="flex items-center gap-2 text-xs text-bolt-elements-textSecondary">
286
+ <span>Version: {model.digest.substring(0, 7)}</span>
287
+ {model.status === 'updated' && model.newDigest && (
288
+ <>
289
+ <div className="i-ph:arrow-right w-3 h-3" />
290
+ <span className="text-green-500">{model.newDigest.substring(0, 7)}</span>
291
+ </>
292
+ )}
293
+ {model.progress && (
294
+ <span className="ml-2">
295
+ {model.progress.status}{' '}
296
+ {model.progress.total > 0 && (
297
+ <>({Math.round((model.progress.current / model.progress.total) * 100)}%)</>
298
+ )}
299
+ </span>
300
+ )}
301
+ {model.details && (
302
+ <span className="ml-2">
303
+ ({model.details.parameter_size}, {model.details.quantization_level})
304
+ </span>
305
+ )}
306
+ </div>
307
+ </div>
308
+ <motion.button
309
+ onClick={() => handleSingleUpdate(model.name)}
310
+ disabled={model.status === 'updating'}
311
+ className={classNames(
312
+ 'rounded-md px-4 py-2 text-sm',
313
+ 'bg-purple-500 text-white',
314
+ 'hover:bg-purple-600',
315
+ 'dark:bg-purple-500 dark:hover:bg-purple-600',
316
+ 'transition-all duration-200',
317
+ )}
318
+ whileHover={{ scale: 1.02 }}
319
+ whileTap={{ scale: 0.98 }}
320
+ >
321
+ <div className="i-ph:arrows-clockwise" />
322
+ Update
323
+ </motion.button>
324
+ </div>
325
+ ))}
326
+ </div>
327
  </div>
328
  </div>
329
  );
app/components/settings/providers/service-status/ServiceStatusTab.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import type { ServiceStatus } from './types';
3
+ import { ProviderStatusCheckerFactory } from './provider-factory';
4
+
5
+ export default function ServiceStatusTab() {
6
+ const [serviceStatuses, setServiceStatuses] = useState<ServiceStatus[]>([]);
7
+ const [loading, setLoading] = useState(true);
8
+ const [error, setError] = useState<string | null>(null);
9
+
10
+ useEffect(() => {
11
+ const checkAllProviders = async () => {
12
+ try {
13
+ setLoading(true);
14
+ setError(null);
15
+
16
+ const providers = ProviderStatusCheckerFactory.getProviderNames();
17
+ const statuses: ServiceStatus[] = [];
18
+
19
+ for (const provider of providers) {
20
+ try {
21
+ const checker = ProviderStatusCheckerFactory.getChecker(provider);
22
+ const result = await checker.checkStatus();
23
+
24
+ statuses.push({
25
+ provider,
26
+ ...result,
27
+ lastChecked: new Date().toISOString(),
28
+ });
29
+ } catch (err) {
30
+ console.error(`Error checking ${provider} status:`, err);
31
+ statuses.push({
32
+ provider,
33
+ status: 'degraded',
34
+ message: 'Unable to check service status',
35
+ incidents: ['Error checking service status'],
36
+ lastChecked: new Date().toISOString(),
37
+ });
38
+ }
39
+ }
40
+
41
+ setServiceStatuses(statuses);
42
+ } catch (err) {
43
+ console.error('Error checking provider statuses:', err);
44
+ setError('Failed to check service statuses');
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ checkAllProviders();
51
+
52
+ // Set up periodic checks every 5 minutes
53
+ const interval = setInterval(checkAllProviders, 5 * 60 * 1000);
54
+
55
+ return () => clearInterval(interval);
56
+ }, []);
57
+
58
+ const getStatusColor = (status: ServiceStatus['status']) => {
59
+ switch (status) {
60
+ case 'operational':
61
+ return 'text-green-500 dark:text-green-400';
62
+ case 'degraded':
63
+ return 'text-yellow-500 dark:text-yellow-400';
64
+ case 'down':
65
+ return 'text-red-500 dark:text-red-400';
66
+ default:
67
+ return 'text-gray-500 dark:text-gray-400';
68
+ }
69
+ };
70
+
71
+ const getStatusIcon = (status: ServiceStatus['status']) => {
72
+ switch (status) {
73
+ case 'operational':
74
+ return 'i-ph:check-circle';
75
+ case 'degraded':
76
+ return 'i-ph:warning';
77
+ case 'down':
78
+ return 'i-ph:x-circle';
79
+ default:
80
+ return 'i-ph:question';
81
+ }
82
+ };
83
+
84
+ if (loading) {
85
+ return (
86
+ <div className="flex items-center justify-center h-full">
87
+ <div className="animate-spin i-ph:circle-notch w-8 h-8 text-purple-500" />
88
+ </div>
89
+ );
90
+ }
91
+
92
+ if (error) {
93
+ return (
94
+ <div className="flex flex-col items-center justify-center h-full text-red-500 dark:text-red-400">
95
+ <div className="i-ph:warning w-8 h-8 mb-2" />
96
+ <p>{error}</p>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ return (
102
+ <div className="space-y-6">
103
+ <div className="grid grid-cols-1 gap-4">
104
+ {serviceStatuses.map((service) => (
105
+ <div
106
+ key={service.provider}
107
+ className="p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
108
+ >
109
+ <div className="flex items-center justify-between mb-2">
110
+ <h3 className="text-lg font-semibold text-gray-900 dark:text-white">{service.provider}</h3>
111
+ <div className={`flex items-center ${getStatusColor(service.status)}`}>
112
+ <div className={`${getStatusIcon(service.status)} w-5 h-5 mr-2`} />
113
+ <span className="capitalize">{service.status}</span>
114
+ </div>
115
+ </div>
116
+ <p className="text-gray-600 dark:text-gray-300 mb-2">{service.message}</p>
117
+ {service.incidents && service.incidents.length > 0 && (
118
+ <div className="mt-2">
119
+ <h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Recent Incidents:</h4>
120
+ <ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
121
+ {service.incidents.map((incident, index) => (
122
+ <li key={index}>{incident}</li>
123
+ ))}
124
+ </ul>
125
+ </div>
126
+ )}
127
+ <div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
128
+ Last checked: {new Date(service.lastChecked).toLocaleString()}
129
+ </div>
130
+ </div>
131
+ ))}
132
+ </div>
133
+ </div>
134
+ );
135
+ }
app/components/settings/providers/service-status/provider-factory.ts CHANGED
@@ -1,122 +1,91 @@
1
  import type { ProviderName, ProviderConfig, StatusCheckResult } from './types';
2
- import { OpenAIStatusChecker } from './providers/openai';
3
  import { BaseProviderChecker } from './base-provider';
4
 
5
- // Import other provider implementations as they are created
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  export class ProviderStatusCheckerFactory {
8
  private static _providerConfigs: Record<ProviderName, ProviderConfig> = {
9
- OpenAI: {
10
- statusUrl: 'https://status.openai.com/',
11
- apiUrl: 'https://api.openai.com/v1/models',
12
- headers: {
13
- Authorization: 'Bearer $OPENAI_API_KEY',
14
- },
15
- testModel: 'gpt-3.5-turbo',
16
- },
17
- Anthropic: {
18
- statusUrl: 'https://status.anthropic.com/',
19
- apiUrl: 'https://api.anthropic.com/v1/messages',
20
- headers: {
21
- 'x-api-key': '$ANTHROPIC_API_KEY',
22
- 'anthropic-version': '2024-02-29',
23
- },
24
- testModel: 'claude-3-sonnet-20240229',
25
- },
26
  AmazonBedrock: {
27
  statusUrl: 'https://health.aws.amazon.com/health/status',
28
  apiUrl: 'https://bedrock.us-east-1.amazonaws.com/models',
29
- headers: {
30
- Authorization: 'Bearer $AWS_BEDROCK_CONFIG',
31
- },
32
  testModel: 'anthropic.claude-3-sonnet-20240229-v1:0',
33
  },
34
  Cohere: {
35
  statusUrl: 'https://status.cohere.com/',
36
  apiUrl: 'https://api.cohere.ai/v1/models',
37
- headers: {
38
- Authorization: 'Bearer $COHERE_API_KEY',
39
- },
40
  testModel: 'command',
41
  },
42
  Deepseek: {
43
  statusUrl: 'https://status.deepseek.com/',
44
  apiUrl: 'https://api.deepseek.com/v1/models',
45
- headers: {
46
- Authorization: 'Bearer $DEEPSEEK_API_KEY',
47
- },
48
  testModel: 'deepseek-chat',
49
  },
50
  Google: {
51
  statusUrl: 'https://status.cloud.google.com/',
52
  apiUrl: 'https://generativelanguage.googleapis.com/v1/models',
53
- headers: {
54
- 'x-goog-api-key': '$GOOGLE_API_KEY',
55
- },
56
  testModel: 'gemini-pro',
57
  },
58
  Groq: {
59
  statusUrl: 'https://groqstatus.com/',
60
  apiUrl: 'https://api.groq.com/v1/models',
61
- headers: {
62
- Authorization: 'Bearer $GROQ_API_KEY',
63
- },
64
  testModel: 'mixtral-8x7b-32768',
65
  },
66
  HuggingFace: {
67
  statusUrl: 'https://status.huggingface.co/',
68
  apiUrl: 'https://api-inference.huggingface.co/models',
69
- headers: {
70
- Authorization: 'Bearer $HUGGINGFACE_API_KEY',
71
- },
72
  testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
73
  },
74
  Hyperbolic: {
75
  statusUrl: 'https://status.hyperbolic.ai/',
76
  apiUrl: 'https://api.hyperbolic.ai/v1/models',
77
- headers: {
78
- Authorization: 'Bearer $HYPERBOLIC_API_KEY',
79
- },
80
  testModel: 'hyperbolic-1',
81
  },
82
  Mistral: {
83
  statusUrl: 'https://status.mistral.ai/',
84
  apiUrl: 'https://api.mistral.ai/v1/models',
85
- headers: {
86
- Authorization: 'Bearer $MISTRAL_API_KEY',
87
- },
88
  testModel: 'mistral-tiny',
89
  },
90
  OpenRouter: {
91
  statusUrl: 'https://status.openrouter.ai/',
92
  apiUrl: 'https://openrouter.ai/api/v1/models',
93
- headers: {
94
- Authorization: 'Bearer $OPEN_ROUTER_API_KEY',
95
- },
96
  testModel: 'anthropic/claude-3-sonnet',
97
  },
98
  Perplexity: {
99
  statusUrl: 'https://status.perplexity.com/',
100
  apiUrl: 'https://api.perplexity.ai/v1/models',
101
- headers: {
102
- Authorization: 'Bearer $PERPLEXITY_API_KEY',
103
- },
104
  testModel: 'pplx-7b-chat',
105
  },
106
  Together: {
107
  statusUrl: 'https://status.together.ai/',
108
  apiUrl: 'https://api.together.xyz/v1/models',
109
- headers: {
110
- Authorization: 'Bearer $TOGETHER_API_KEY',
111
- },
112
  testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
113
  },
114
  XAI: {
115
  statusUrl: 'https://status.x.ai/',
116
  apiUrl: 'https://api.x.ai/v1/models',
117
- headers: {
118
- Authorization: 'Bearer $XAI_API_KEY',
119
- },
120
  testModel: 'grok-1',
121
  },
122
  };
@@ -128,12 +97,31 @@ export class ProviderStatusCheckerFactory {
128
  throw new Error(`No configuration found for provider: ${provider}`);
129
  }
130
 
131
- // Return specific provider implementation or fallback to base implementation
132
  switch (provider) {
133
- case 'OpenAI':
134
- return new OpenAIStatusChecker(config);
135
-
136
- // Add other provider implementations as they are created
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  default:
138
  return new (class extends BaseProviderChecker {
139
  async checkStatus(): Promise<StatusCheckResult> {
@@ -154,7 +142,13 @@ export class ProviderStatusCheckerFactory {
154
  return Object.keys(this._providerConfigs) as ProviderName[];
155
  }
156
 
157
- static getProviderConfig(provider: ProviderName): ProviderConfig | undefined {
158
- return this._providerConfigs[provider];
 
 
 
 
 
 
159
  }
160
  }
 
1
  import type { ProviderName, ProviderConfig, StatusCheckResult } from './types';
 
2
  import { BaseProviderChecker } from './base-provider';
3
 
4
+ import { AmazonBedrockStatusChecker } from './providers/amazon-bedrock';
5
+ import { CohereStatusChecker } from './providers/cohere';
6
+ import { DeepseekStatusChecker } from './providers/deepseek';
7
+ import { GoogleStatusChecker } from './providers/google';
8
+ import { GroqStatusChecker } from './providers/groq';
9
+ import { HuggingFaceStatusChecker } from './providers/huggingface';
10
+ import { HyperbolicStatusChecker } from './providers/hyperbolic';
11
+ import { MistralStatusChecker } from './providers/mistral';
12
+ import { OpenRouterStatusChecker } from './providers/openrouter';
13
+ import { PerplexityStatusChecker } from './providers/perplexity';
14
+ import { TogetherStatusChecker } from './providers/together';
15
+ import { XAIStatusChecker } from './providers/xai';
16
 
17
  export class ProviderStatusCheckerFactory {
18
  private static _providerConfigs: Record<ProviderName, ProviderConfig> = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  AmazonBedrock: {
20
  statusUrl: 'https://health.aws.amazon.com/health/status',
21
  apiUrl: 'https://bedrock.us-east-1.amazonaws.com/models',
22
+ headers: {},
 
 
23
  testModel: 'anthropic.claude-3-sonnet-20240229-v1:0',
24
  },
25
  Cohere: {
26
  statusUrl: 'https://status.cohere.com/',
27
  apiUrl: 'https://api.cohere.ai/v1/models',
28
+ headers: {},
 
 
29
  testModel: 'command',
30
  },
31
  Deepseek: {
32
  statusUrl: 'https://status.deepseek.com/',
33
  apiUrl: 'https://api.deepseek.com/v1/models',
34
+ headers: {},
 
 
35
  testModel: 'deepseek-chat',
36
  },
37
  Google: {
38
  statusUrl: 'https://status.cloud.google.com/',
39
  apiUrl: 'https://generativelanguage.googleapis.com/v1/models',
40
+ headers: {},
 
 
41
  testModel: 'gemini-pro',
42
  },
43
  Groq: {
44
  statusUrl: 'https://groqstatus.com/',
45
  apiUrl: 'https://api.groq.com/v1/models',
46
+ headers: {},
 
 
47
  testModel: 'mixtral-8x7b-32768',
48
  },
49
  HuggingFace: {
50
  statusUrl: 'https://status.huggingface.co/',
51
  apiUrl: 'https://api-inference.huggingface.co/models',
52
+ headers: {},
 
 
53
  testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
54
  },
55
  Hyperbolic: {
56
  statusUrl: 'https://status.hyperbolic.ai/',
57
  apiUrl: 'https://api.hyperbolic.ai/v1/models',
58
+ headers: {},
 
 
59
  testModel: 'hyperbolic-1',
60
  },
61
  Mistral: {
62
  statusUrl: 'https://status.mistral.ai/',
63
  apiUrl: 'https://api.mistral.ai/v1/models',
64
+ headers: {},
 
 
65
  testModel: 'mistral-tiny',
66
  },
67
  OpenRouter: {
68
  statusUrl: 'https://status.openrouter.ai/',
69
  apiUrl: 'https://openrouter.ai/api/v1/models',
70
+ headers: {},
 
 
71
  testModel: 'anthropic/claude-3-sonnet',
72
  },
73
  Perplexity: {
74
  statusUrl: 'https://status.perplexity.com/',
75
  apiUrl: 'https://api.perplexity.ai/v1/models',
76
+ headers: {},
 
 
77
  testModel: 'pplx-7b-chat',
78
  },
79
  Together: {
80
  statusUrl: 'https://status.together.ai/',
81
  apiUrl: 'https://api.together.xyz/v1/models',
82
+ headers: {},
 
 
83
  testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
84
  },
85
  XAI: {
86
  statusUrl: 'https://status.x.ai/',
87
  apiUrl: 'https://api.x.ai/v1/models',
88
+ headers: {},
 
 
89
  testModel: 'grok-1',
90
  },
91
  };
 
97
  throw new Error(`No configuration found for provider: ${provider}`);
98
  }
99
 
 
100
  switch (provider) {
101
+ case 'AmazonBedrock':
102
+ return new AmazonBedrockStatusChecker(config);
103
+ case 'Cohere':
104
+ return new CohereStatusChecker(config);
105
+ case 'Deepseek':
106
+ return new DeepseekStatusChecker(config);
107
+ case 'Google':
108
+ return new GoogleStatusChecker(config);
109
+ case 'Groq':
110
+ return new GroqStatusChecker(config);
111
+ case 'HuggingFace':
112
+ return new HuggingFaceStatusChecker(config);
113
+ case 'Hyperbolic':
114
+ return new HyperbolicStatusChecker(config);
115
+ case 'Mistral':
116
+ return new MistralStatusChecker(config);
117
+ case 'OpenRouter':
118
+ return new OpenRouterStatusChecker(config);
119
+ case 'Perplexity':
120
+ return new PerplexityStatusChecker(config);
121
+ case 'Together':
122
+ return new TogetherStatusChecker(config);
123
+ case 'XAI':
124
+ return new XAIStatusChecker(config);
125
  default:
126
  return new (class extends BaseProviderChecker {
127
  async checkStatus(): Promise<StatusCheckResult> {
 
142
  return Object.keys(this._providerConfigs) as ProviderName[];
143
  }
144
 
145
+ static getProviderConfig(provider: ProviderName): ProviderConfig {
146
+ const config = this._providerConfigs[provider];
147
+
148
+ if (!config) {
149
+ throw new Error(`Unknown provider: ${provider}`);
150
+ }
151
+
152
+ return config;
153
  }
154
  }
app/components/settings/providers/service-status/providers/amazon-bedrock.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class AmazonBedrockStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check AWS health status page
8
+ const statusPageResponse = await fetch('https://health.aws.amazon.com/health/status');
9
+ const text = await statusPageResponse.text();
10
+
11
+ // Check for Bedrock and general AWS status
12
+ const hasBedrockIssues =
13
+ text.includes('Amazon Bedrock') &&
14
+ (text.includes('Service is experiencing elevated error rates') ||
15
+ text.includes('Service disruption') ||
16
+ text.includes('Degraded Service'));
17
+
18
+ const hasGeneralIssues = text.includes('Service disruption') || text.includes('Multiple services affected');
19
+
20
+ // Extract incidents
21
+ const incidents: string[] = [];
22
+ const incidentMatches = text.matchAll(/(\d{4}-\d{2}-\d{2})\s+(.*?)\s+Impact:(.*?)(?=\n|$)/g);
23
+
24
+ for (const match of incidentMatches) {
25
+ const [, date, title, impact] = match;
26
+
27
+ if (title.includes('Bedrock') || title.includes('AWS')) {
28
+ incidents.push(`${date}: ${title.trim()} - Impact: ${impact.trim()}`);
29
+ }
30
+ }
31
+
32
+ let status: StatusCheckResult['status'] = 'operational';
33
+ let message = 'All services operational';
34
+
35
+ if (hasBedrockIssues) {
36
+ status = 'degraded';
37
+ message = 'Amazon Bedrock service issues reported';
38
+ } else if (hasGeneralIssues) {
39
+ status = 'degraded';
40
+ message = 'AWS experiencing general issues';
41
+ }
42
+
43
+ // If status page check fails, fallback to endpoint check
44
+ if (!statusPageResponse.ok) {
45
+ const endpointStatus = await this.checkEndpoint('https://health.aws.amazon.com/health/status');
46
+ const apiEndpoint = 'https://bedrock.us-east-1.amazonaws.com/models';
47
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
48
+
49
+ return {
50
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
51
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
52
+ incidents: ['Note: Limited status information due to CORS restrictions'],
53
+ };
54
+ }
55
+
56
+ return {
57
+ status,
58
+ message,
59
+ incidents: incidents.slice(0, 5),
60
+ };
61
+ } catch (error) {
62
+ console.error('Error checking Amazon Bedrock status:', error);
63
+
64
+ // Fallback to basic endpoint check
65
+ const endpointStatus = await this.checkEndpoint('https://health.aws.amazon.com/health/status');
66
+ const apiEndpoint = 'https://bedrock.us-east-1.amazonaws.com/models';
67
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
68
+
69
+ return {
70
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
71
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
72
+ incidents: ['Note: Limited status information due to CORS restrictions'],
73
+ };
74
+ }
75
+ }
76
+ }
app/components/settings/providers/service-status/providers/anthropic.ts ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class AnthropicStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://status.anthropic.com/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ // Check for specific Anthropic status indicators
12
+ const isOperational = text.includes('All Systems Operational');
13
+ const hasDegradedPerformance = text.includes('Degraded Performance');
14
+ const hasPartialOutage = text.includes('Partial Outage');
15
+ const hasMajorOutage = text.includes('Major Outage');
16
+
17
+ // Extract incidents
18
+ const incidents: string[] = [];
19
+ const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
20
+
21
+ if (incidentSection) {
22
+ const incidentLines = incidentSection[1]
23
+ .split('\n')
24
+ .map((line) => line.trim())
25
+ .filter((line) => line && line.includes('202')); // Only get dated incidents
26
+
27
+ incidents.push(...incidentLines.slice(0, 5));
28
+ }
29
+
30
+ let status: StatusCheckResult['status'] = 'operational';
31
+ let message = 'All systems operational';
32
+
33
+ if (hasMajorOutage) {
34
+ status = 'down';
35
+ message = 'Major service outage';
36
+ } else if (hasPartialOutage) {
37
+ status = 'down';
38
+ message = 'Partial service outage';
39
+ } else if (hasDegradedPerformance) {
40
+ status = 'degraded';
41
+ message = 'Service experiencing degraded performance';
42
+ } else if (!isOperational) {
43
+ status = 'degraded';
44
+ message = 'Service status unknown';
45
+ }
46
+
47
+ // If status page check fails, fallback to endpoint check
48
+ if (!statusPageResponse.ok) {
49
+ const endpointStatus = await this.checkEndpoint('https://status.anthropic.com/');
50
+ const apiEndpoint = 'https://api.anthropic.com/v1/messages';
51
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
52
+
53
+ return {
54
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
55
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
56
+ incidents: ['Note: Limited status information due to CORS restrictions'],
57
+ };
58
+ }
59
+
60
+ return {
61
+ status,
62
+ message,
63
+ incidents,
64
+ };
65
+ } catch (error) {
66
+ console.error('Error checking Anthropic status:', error);
67
+
68
+ // Fallback to basic endpoint check
69
+ const endpointStatus = await this.checkEndpoint('https://status.anthropic.com/');
70
+ const apiEndpoint = 'https://api.anthropic.com/v1/messages';
71
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
72
+
73
+ return {
74
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
75
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
76
+ incidents: ['Note: Limited status information due to CORS restrictions'],
77
+ };
78
+ }
79
+ }
80
+ }
app/components/settings/providers/service-status/providers/cohere.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class CohereStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://status.cohere.com/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ // Check for specific Cohere status indicators
12
+ const isOperational = text.includes('All Systems Operational');
13
+ const hasIncidents = text.includes('Active Incidents');
14
+ const hasDegradation = text.includes('Degraded Performance');
15
+ const hasOutage = text.includes('Service Outage');
16
+
17
+ // Extract incidents
18
+ const incidents: string[] = [];
19
+ const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
20
+
21
+ if (incidentSection) {
22
+ const incidentLines = incidentSection[1]
23
+ .split('\n')
24
+ .map((line) => line.trim())
25
+ .filter((line) => line && line.includes('202')); // Only get dated incidents
26
+
27
+ incidents.push(...incidentLines.slice(0, 5));
28
+ }
29
+
30
+ // Check specific services
31
+ const services = {
32
+ api: {
33
+ operational: text.includes('API Service') && text.includes('Operational'),
34
+ degraded: text.includes('API Service') && text.includes('Degraded Performance'),
35
+ outage: text.includes('API Service') && text.includes('Service Outage'),
36
+ },
37
+ generation: {
38
+ operational: text.includes('Generation Service') && text.includes('Operational'),
39
+ degraded: text.includes('Generation Service') && text.includes('Degraded Performance'),
40
+ outage: text.includes('Generation Service') && text.includes('Service Outage'),
41
+ },
42
+ };
43
+
44
+ let status: StatusCheckResult['status'] = 'operational';
45
+ let message = 'All systems operational';
46
+
47
+ if (services.api.outage || services.generation.outage || hasOutage) {
48
+ status = 'down';
49
+ message = 'Service outage detected';
50
+ } else if (services.api.degraded || services.generation.degraded || hasDegradation || hasIncidents) {
51
+ status = 'degraded';
52
+ message = 'Service experiencing issues';
53
+ } else if (!isOperational) {
54
+ status = 'degraded';
55
+ message = 'Service status unknown';
56
+ }
57
+
58
+ // If status page check fails, fallback to endpoint check
59
+ if (!statusPageResponse.ok) {
60
+ const endpointStatus = await this.checkEndpoint('https://status.cohere.com/');
61
+ const apiEndpoint = 'https://api.cohere.ai/v1/models';
62
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
63
+
64
+ return {
65
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
66
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
67
+ incidents: ['Note: Limited status information due to CORS restrictions'],
68
+ };
69
+ }
70
+
71
+ return {
72
+ status,
73
+ message,
74
+ incidents,
75
+ };
76
+ } catch (error) {
77
+ console.error('Error checking Cohere status:', error);
78
+
79
+ // Fallback to basic endpoint check
80
+ const endpointStatus = await this.checkEndpoint('https://status.cohere.com/');
81
+ const apiEndpoint = 'https://api.cohere.ai/v1/models';
82
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
83
+
84
+ return {
85
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
86
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
87
+ incidents: ['Note: Limited status information due to CORS restrictions'],
88
+ };
89
+ }
90
+ }
91
+ }
app/components/settings/providers/service-status/providers/deepseek.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class DeepseekStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ /*
8
+ * Check status page - Note: Deepseek doesn't have a public status page yet
9
+ * so we'll check their API endpoint directly
10
+ */
11
+ const apiEndpoint = 'https://api.deepseek.com/v1/models';
12
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
13
+
14
+ // Check their website as a secondary indicator
15
+ const websiteStatus = await this.checkEndpoint('https://deepseek.com');
16
+
17
+ let status: StatusCheckResult['status'] = 'operational';
18
+ let message = 'All systems operational';
19
+
20
+ if (apiStatus !== 'reachable' || websiteStatus !== 'reachable') {
21
+ status = apiStatus !== 'reachable' ? 'down' : 'degraded';
22
+ message = apiStatus !== 'reachable' ? 'API appears to be down' : 'Service may be experiencing issues';
23
+ }
24
+
25
+ return {
26
+ status,
27
+ message,
28
+ incidents: [], // No public incident tracking available yet
29
+ };
30
+ } catch (error) {
31
+ console.error('Error checking Deepseek status:', error);
32
+
33
+ return {
34
+ status: 'degraded',
35
+ message: 'Unable to determine service status',
36
+ incidents: ['Note: Limited status information available'],
37
+ };
38
+ }
39
+ }
40
+ }
app/components/settings/providers/service-status/providers/google.ts ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class GoogleStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://status.cloud.google.com/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ // Check for Vertex AI and general cloud status
12
+ const hasVertexAIIssues =
13
+ text.includes('Vertex AI') &&
14
+ (text.includes('Incident') ||
15
+ text.includes('Disruption') ||
16
+ text.includes('Outage') ||
17
+ text.includes('degraded'));
18
+
19
+ const hasGeneralIssues = text.includes('Major Incidents') || text.includes('Service Disruption');
20
+
21
+ // Extract incidents
22
+ const incidents: string[] = [];
23
+ const incidentMatches = text.matchAll(/(\d{4}-\d{2}-\d{2})\s+(.*?)\s+Impact:(.*?)(?=\n|$)/g);
24
+
25
+ for (const match of incidentMatches) {
26
+ const [, date, title, impact] = match;
27
+
28
+ if (title.includes('Vertex AI') || title.includes('Cloud')) {
29
+ incidents.push(`${date}: ${title.trim()} - Impact: ${impact.trim()}`);
30
+ }
31
+ }
32
+
33
+ let status: StatusCheckResult['status'] = 'operational';
34
+ let message = 'All services operational';
35
+
36
+ if (hasVertexAIIssues) {
37
+ status = 'degraded';
38
+ message = 'Vertex AI service issues reported';
39
+ } else if (hasGeneralIssues) {
40
+ status = 'degraded';
41
+ message = 'Google Cloud experiencing issues';
42
+ }
43
+
44
+ // If status page check fails, fallback to endpoint check
45
+ if (!statusPageResponse.ok) {
46
+ const endpointStatus = await this.checkEndpoint('https://status.cloud.google.com/');
47
+ const apiEndpoint = 'https://generativelanguage.googleapis.com/v1/models';
48
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
49
+
50
+ return {
51
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
52
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
53
+ incidents: ['Note: Limited status information due to CORS restrictions'],
54
+ };
55
+ }
56
+
57
+ return {
58
+ status,
59
+ message,
60
+ incidents: incidents.slice(0, 5),
61
+ };
62
+ } catch (error) {
63
+ console.error('Error checking Google status:', error);
64
+
65
+ // Fallback to basic endpoint check
66
+ const endpointStatus = await this.checkEndpoint('https://status.cloud.google.com/');
67
+ const apiEndpoint = 'https://generativelanguage.googleapis.com/v1/models';
68
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
69
+
70
+ return {
71
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
72
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
73
+ incidents: ['Note: Limited status information due to CORS restrictions'],
74
+ };
75
+ }
76
+ }
77
+ }
app/components/settings/providers/service-status/providers/groq.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class GroqStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://groqstatus.com/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ const isOperational = text.includes('All Systems Operational');
12
+ const hasIncidents = text.includes('Active Incidents');
13
+ const hasDegradation = text.includes('Degraded Performance');
14
+ const hasOutage = text.includes('Service Outage');
15
+
16
+ // Extract incidents
17
+ const incidents: string[] = [];
18
+ const incidentMatches = text.matchAll(/(\d{4}-\d{2}-\d{2})\s+(.*?)\s+Status:(.*?)(?=\n|$)/g);
19
+
20
+ for (const match of incidentMatches) {
21
+ const [, date, title, status] = match;
22
+ incidents.push(`${date}: ${title.trim()} - ${status.trim()}`);
23
+ }
24
+
25
+ let status: StatusCheckResult['status'] = 'operational';
26
+ let message = 'All systems operational';
27
+
28
+ if (hasOutage) {
29
+ status = 'down';
30
+ message = 'Service outage detected';
31
+ } else if (hasDegradation || hasIncidents) {
32
+ status = 'degraded';
33
+ message = 'Service experiencing issues';
34
+ } else if (!isOperational) {
35
+ status = 'degraded';
36
+ message = 'Service status unknown';
37
+ }
38
+
39
+ // If status page check fails, fallback to endpoint check
40
+ if (!statusPageResponse.ok) {
41
+ const endpointStatus = await this.checkEndpoint('https://groqstatus.com/');
42
+ const apiEndpoint = 'https://api.groq.com/v1/models';
43
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
44
+
45
+ return {
46
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
47
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
48
+ incidents: ['Note: Limited status information due to CORS restrictions'],
49
+ };
50
+ }
51
+
52
+ return {
53
+ status,
54
+ message,
55
+ incidents: incidents.slice(0, 5),
56
+ };
57
+ } catch (error) {
58
+ console.error('Error checking Groq status:', error);
59
+
60
+ // Fallback to basic endpoint check
61
+ const endpointStatus = await this.checkEndpoint('https://groqstatus.com/');
62
+ const apiEndpoint = 'https://api.groq.com/v1/models';
63
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
64
+
65
+ return {
66
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
67
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
68
+ incidents: ['Note: Limited status information due to CORS restrictions'],
69
+ };
70
+ }
71
+ }
72
+ }
app/components/settings/providers/service-status/providers/huggingface.ts ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class HuggingFaceStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://status.huggingface.co/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ // Check for "All services are online" message
12
+ const allServicesOnline = text.includes('All services are online');
13
+
14
+ // Get last update time
15
+ const lastUpdateMatch = text.match(/Last updated on (.*?)(EST|PST|GMT)/);
16
+ const lastUpdate = lastUpdateMatch ? `${lastUpdateMatch[1]}${lastUpdateMatch[2]}` : '';
17
+
18
+ // Check individual services and their uptime percentages
19
+ const services = {
20
+ 'Huggingface Hub': {
21
+ operational: text.includes('Huggingface Hub') && text.includes('Operational'),
22
+ uptime: text.match(/Huggingface Hub[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
23
+ },
24
+ 'Git Hosting and Serving': {
25
+ operational: text.includes('Git Hosting and Serving') && text.includes('Operational'),
26
+ uptime: text.match(/Git Hosting and Serving[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
27
+ },
28
+ 'Inference API': {
29
+ operational: text.includes('Inference API') && text.includes('Operational'),
30
+ uptime: text.match(/Inference API[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
31
+ },
32
+ 'HF Endpoints': {
33
+ operational: text.includes('HF Endpoints') && text.includes('Operational'),
34
+ uptime: text.match(/HF Endpoints[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
35
+ },
36
+ Spaces: {
37
+ operational: text.includes('Spaces') && text.includes('Operational'),
38
+ uptime: text.match(/Spaces[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
39
+ },
40
+ };
41
+
42
+ // Create service status messages with uptime
43
+ const serviceMessages = Object.entries(services).map(([name, info]) => {
44
+ if (info.uptime) {
45
+ return `${name}: ${info.uptime}% uptime`;
46
+ }
47
+
48
+ return `${name}: ${info.operational ? 'Operational' : 'Issues detected'}`;
49
+ });
50
+
51
+ // Determine overall status
52
+ let status: StatusCheckResult['status'] = 'operational';
53
+ let message = allServicesOnline
54
+ ? `All services are online (Last updated on ${lastUpdate})`
55
+ : 'Checking individual services';
56
+
57
+ // Only mark as degraded if we explicitly detect issues
58
+ const hasIssues = Object.values(services).some((service) => !service.operational);
59
+
60
+ if (hasIssues) {
61
+ status = 'degraded';
62
+ message = `Service issues detected (Last updated on ${lastUpdate})`;
63
+ }
64
+
65
+ // If status page check fails, fallback to endpoint check
66
+ if (!statusPageResponse.ok) {
67
+ const endpointStatus = await this.checkEndpoint('https://status.huggingface.co/');
68
+ const apiEndpoint = 'https://api-inference.huggingface.co/models';
69
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
70
+
71
+ return {
72
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
73
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
74
+ incidents: ['Note: Limited status information due to CORS restrictions'],
75
+ };
76
+ }
77
+
78
+ return {
79
+ status,
80
+ message,
81
+ incidents: serviceMessages,
82
+ };
83
+ } catch (error) {
84
+ console.error('Error checking HuggingFace status:', error);
85
+
86
+ // Fallback to basic endpoint check
87
+ const endpointStatus = await this.checkEndpoint('https://status.huggingface.co/');
88
+ const apiEndpoint = 'https://api-inference.huggingface.co/models';
89
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
90
+
91
+ return {
92
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
93
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
94
+ incidents: ['Note: Limited status information due to CORS restrictions'],
95
+ };
96
+ }
97
+ }
98
+ }
app/components/settings/providers/service-status/providers/hyperbolic.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class HyperbolicStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ /*
8
+ * Check API endpoint directly since Hyperbolic is a newer provider
9
+ * and may not have a public status page yet
10
+ */
11
+ const apiEndpoint = 'https://api.hyperbolic.ai/v1/models';
12
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
13
+
14
+ // Check their website as a secondary indicator
15
+ const websiteStatus = await this.checkEndpoint('https://hyperbolic.ai');
16
+
17
+ let status: StatusCheckResult['status'] = 'operational';
18
+ let message = 'All systems operational';
19
+
20
+ if (apiStatus !== 'reachable' || websiteStatus !== 'reachable') {
21
+ status = apiStatus !== 'reachable' ? 'down' : 'degraded';
22
+ message = apiStatus !== 'reachable' ? 'API appears to be down' : 'Service may be experiencing issues';
23
+ }
24
+
25
+ return {
26
+ status,
27
+ message,
28
+ incidents: [], // No public incident tracking available yet
29
+ };
30
+ } catch (error) {
31
+ console.error('Error checking Hyperbolic status:', error);
32
+
33
+ return {
34
+ status: 'degraded',
35
+ message: 'Unable to determine service status',
36
+ incidents: ['Note: Limited status information available'],
37
+ };
38
+ }
39
+ }
40
+ }
app/components/settings/providers/service-status/providers/mistral.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class MistralStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://status.mistral.ai/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ const isOperational = text.includes('All Systems Operational');
12
+ const hasIncidents = text.includes('Active Incidents');
13
+ const hasDegradation = text.includes('Degraded Performance');
14
+ const hasOutage = text.includes('Service Outage');
15
+
16
+ // Extract incidents
17
+ const incidents: string[] = [];
18
+ const incidentSection = text.match(/Recent Events(.*?)(?=\n\n)/s);
19
+
20
+ if (incidentSection) {
21
+ const incidentLines = incidentSection[1]
22
+ .split('\n')
23
+ .map((line) => line.trim())
24
+ .filter((line) => line && !line.includes('No incidents'));
25
+
26
+ incidents.push(...incidentLines.slice(0, 5));
27
+ }
28
+
29
+ let status: StatusCheckResult['status'] = 'operational';
30
+ let message = 'All systems operational';
31
+
32
+ if (hasOutage) {
33
+ status = 'down';
34
+ message = 'Service outage detected';
35
+ } else if (hasDegradation || hasIncidents) {
36
+ status = 'degraded';
37
+ message = 'Service experiencing issues';
38
+ } else if (!isOperational) {
39
+ status = 'degraded';
40
+ message = 'Service status unknown';
41
+ }
42
+
43
+ // If status page check fails, fallback to endpoint check
44
+ if (!statusPageResponse.ok) {
45
+ const endpointStatus = await this.checkEndpoint('https://status.mistral.ai/');
46
+ const apiEndpoint = 'https://api.mistral.ai/v1/models';
47
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
48
+
49
+ return {
50
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
51
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
52
+ incidents: ['Note: Limited status information due to CORS restrictions'],
53
+ };
54
+ }
55
+
56
+ return {
57
+ status,
58
+ message,
59
+ incidents,
60
+ };
61
+ } catch (error) {
62
+ console.error('Error checking Mistral status:', error);
63
+
64
+ // Fallback to basic endpoint check
65
+ const endpointStatus = await this.checkEndpoint('https://status.mistral.ai/');
66
+ const apiEndpoint = 'https://api.mistral.ai/v1/models';
67
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
68
+
69
+ return {
70
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
71
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
72
+ incidents: ['Note: Limited status information due to CORS restrictions'],
73
+ };
74
+ }
75
+ }
76
+ }
app/components/settings/providers/service-status/providers/openrouter.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class OpenRouterStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://status.openrouter.ai/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ // Check for specific OpenRouter status indicators
12
+ const isOperational = text.includes('All Systems Operational');
13
+ const hasIncidents = text.includes('Active Incidents');
14
+ const hasDegradation = text.includes('Degraded Performance');
15
+ const hasOutage = text.includes('Service Outage');
16
+
17
+ // Extract incidents
18
+ const incidents: string[] = [];
19
+ const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
20
+
21
+ if (incidentSection) {
22
+ const incidentLines = incidentSection[1]
23
+ .split('\n')
24
+ .map((line) => line.trim())
25
+ .filter((line) => line && line.includes('202')); // Only get dated incidents
26
+
27
+ incidents.push(...incidentLines.slice(0, 5));
28
+ }
29
+
30
+ // Check specific services
31
+ const services = {
32
+ api: {
33
+ operational: text.includes('API Service') && text.includes('Operational'),
34
+ degraded: text.includes('API Service') && text.includes('Degraded Performance'),
35
+ outage: text.includes('API Service') && text.includes('Service Outage'),
36
+ },
37
+ routing: {
38
+ operational: text.includes('Routing Service') && text.includes('Operational'),
39
+ degraded: text.includes('Routing Service') && text.includes('Degraded Performance'),
40
+ outage: text.includes('Routing Service') && text.includes('Service Outage'),
41
+ },
42
+ };
43
+
44
+ let status: StatusCheckResult['status'] = 'operational';
45
+ let message = 'All systems operational';
46
+
47
+ if (services.api.outage || services.routing.outage || hasOutage) {
48
+ status = 'down';
49
+ message = 'Service outage detected';
50
+ } else if (services.api.degraded || services.routing.degraded || hasDegradation || hasIncidents) {
51
+ status = 'degraded';
52
+ message = 'Service experiencing issues';
53
+ } else if (!isOperational) {
54
+ status = 'degraded';
55
+ message = 'Service status unknown';
56
+ }
57
+
58
+ // If status page check fails, fallback to endpoint check
59
+ if (!statusPageResponse.ok) {
60
+ const endpointStatus = await this.checkEndpoint('https://status.openrouter.ai/');
61
+ const apiEndpoint = 'https://openrouter.ai/api/v1/models';
62
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
63
+
64
+ return {
65
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
66
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
67
+ incidents: ['Note: Limited status information due to CORS restrictions'],
68
+ };
69
+ }
70
+
71
+ return {
72
+ status,
73
+ message,
74
+ incidents,
75
+ };
76
+ } catch (error) {
77
+ console.error('Error checking OpenRouter status:', error);
78
+
79
+ // Fallback to basic endpoint check
80
+ const endpointStatus = await this.checkEndpoint('https://status.openrouter.ai/');
81
+ const apiEndpoint = 'https://openrouter.ai/api/v1/models';
82
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
83
+
84
+ return {
85
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
86
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
87
+ incidents: ['Note: Limited status information due to CORS restrictions'],
88
+ };
89
+ }
90
+ }
91
+ }
app/components/settings/providers/service-status/providers/perplexity.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class PerplexityStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://status.perplexity.ai/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ // Check for specific Perplexity status indicators
12
+ const isOperational = text.includes('All Systems Operational');
13
+ const hasIncidents = text.includes('Active Incidents');
14
+ const hasDegradation = text.includes('Degraded Performance');
15
+ const hasOutage = text.includes('Service Outage');
16
+
17
+ // Extract incidents
18
+ const incidents: string[] = [];
19
+ const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
20
+
21
+ if (incidentSection) {
22
+ const incidentLines = incidentSection[1]
23
+ .split('\n')
24
+ .map((line) => line.trim())
25
+ .filter((line) => line && line.includes('202')); // Only get dated incidents
26
+
27
+ incidents.push(...incidentLines.slice(0, 5));
28
+ }
29
+
30
+ // Check specific services
31
+ const services = {
32
+ api: {
33
+ operational: text.includes('API Service') && text.includes('Operational'),
34
+ degraded: text.includes('API Service') && text.includes('Degraded Performance'),
35
+ outage: text.includes('API Service') && text.includes('Service Outage'),
36
+ },
37
+ inference: {
38
+ operational: text.includes('Inference Service') && text.includes('Operational'),
39
+ degraded: text.includes('Inference Service') && text.includes('Degraded Performance'),
40
+ outage: text.includes('Inference Service') && text.includes('Service Outage'),
41
+ },
42
+ };
43
+
44
+ let status: StatusCheckResult['status'] = 'operational';
45
+ let message = 'All systems operational';
46
+
47
+ if (services.api.outage || services.inference.outage || hasOutage) {
48
+ status = 'down';
49
+ message = 'Service outage detected';
50
+ } else if (services.api.degraded || services.inference.degraded || hasDegradation || hasIncidents) {
51
+ status = 'degraded';
52
+ message = 'Service experiencing issues';
53
+ } else if (!isOperational) {
54
+ status = 'degraded';
55
+ message = 'Service status unknown';
56
+ }
57
+
58
+ // If status page check fails, fallback to endpoint check
59
+ if (!statusPageResponse.ok) {
60
+ const endpointStatus = await this.checkEndpoint('https://status.perplexity.ai/');
61
+ const apiEndpoint = 'https://api.perplexity.ai/v1/models';
62
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
63
+
64
+ return {
65
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
66
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
67
+ incidents: ['Note: Limited status information due to CORS restrictions'],
68
+ };
69
+ }
70
+
71
+ return {
72
+ status,
73
+ message,
74
+ incidents,
75
+ };
76
+ } catch (error) {
77
+ console.error('Error checking Perplexity status:', error);
78
+
79
+ // Fallback to basic endpoint check
80
+ const endpointStatus = await this.checkEndpoint('https://status.perplexity.ai/');
81
+ const apiEndpoint = 'https://api.perplexity.ai/v1/models';
82
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
83
+
84
+ return {
85
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
86
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
87
+ incidents: ['Note: Limited status information due to CORS restrictions'],
88
+ };
89
+ }
90
+ }
91
+ }
app/components/settings/providers/service-status/providers/together.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class TogetherStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://status.together.ai/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ // Check for specific Together status indicators
12
+ const isOperational = text.includes('All Systems Operational');
13
+ const hasIncidents = text.includes('Active Incidents');
14
+ const hasDegradation = text.includes('Degraded Performance');
15
+ const hasOutage = text.includes('Service Outage');
16
+
17
+ // Extract incidents
18
+ const incidents: string[] = [];
19
+ const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
20
+
21
+ if (incidentSection) {
22
+ const incidentLines = incidentSection[1]
23
+ .split('\n')
24
+ .map((line) => line.trim())
25
+ .filter((line) => line && line.includes('202')); // Only get dated incidents
26
+
27
+ incidents.push(...incidentLines.slice(0, 5));
28
+ }
29
+
30
+ // Check specific services
31
+ const services = {
32
+ api: {
33
+ operational: text.includes('API Service') && text.includes('Operational'),
34
+ degraded: text.includes('API Service') && text.includes('Degraded Performance'),
35
+ outage: text.includes('API Service') && text.includes('Service Outage'),
36
+ },
37
+ inference: {
38
+ operational: text.includes('Inference Service') && text.includes('Operational'),
39
+ degraded: text.includes('Inference Service') && text.includes('Degraded Performance'),
40
+ outage: text.includes('Inference Service') && text.includes('Service Outage'),
41
+ },
42
+ };
43
+
44
+ let status: StatusCheckResult['status'] = 'operational';
45
+ let message = 'All systems operational';
46
+
47
+ if (services.api.outage || services.inference.outage || hasOutage) {
48
+ status = 'down';
49
+ message = 'Service outage detected';
50
+ } else if (services.api.degraded || services.inference.degraded || hasDegradation || hasIncidents) {
51
+ status = 'degraded';
52
+ message = 'Service experiencing issues';
53
+ } else if (!isOperational) {
54
+ status = 'degraded';
55
+ message = 'Service status unknown';
56
+ }
57
+
58
+ // If status page check fails, fallback to endpoint check
59
+ if (!statusPageResponse.ok) {
60
+ const endpointStatus = await this.checkEndpoint('https://status.together.ai/');
61
+ const apiEndpoint = 'https://api.together.ai/v1/models';
62
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
63
+
64
+ return {
65
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
66
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
67
+ incidents: ['Note: Limited status information due to CORS restrictions'],
68
+ };
69
+ }
70
+
71
+ return {
72
+ status,
73
+ message,
74
+ incidents,
75
+ };
76
+ } catch (error) {
77
+ console.error('Error checking Together status:', error);
78
+
79
+ // Fallback to basic endpoint check
80
+ const endpointStatus = await this.checkEndpoint('https://status.together.ai/');
81
+ const apiEndpoint = 'https://api.together.ai/v1/models';
82
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
83
+
84
+ return {
85
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
86
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
87
+ incidents: ['Note: Limited status information due to CORS restrictions'],
88
+ };
89
+ }
90
+ }
91
+ }
app/components/settings/providers/service-status/providers/xai.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class XAIStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ /*
8
+ * Check API endpoint directly since XAI is a newer provider
9
+ * and may not have a public status page yet
10
+ */
11
+ const apiEndpoint = 'https://api.xai.com/v1/models';
12
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
13
+
14
+ // Check their website as a secondary indicator
15
+ const websiteStatus = await this.checkEndpoint('https://x.ai');
16
+
17
+ let status: StatusCheckResult['status'] = 'operational';
18
+ let message = 'All systems operational';
19
+
20
+ if (apiStatus !== 'reachable' || websiteStatus !== 'reachable') {
21
+ status = apiStatus !== 'reachable' ? 'down' : 'degraded';
22
+ message = apiStatus !== 'reachable' ? 'API appears to be down' : 'Service may be experiencing issues';
23
+ }
24
+
25
+ return {
26
+ status,
27
+ message,
28
+ incidents: [], // No public incident tracking available yet
29
+ };
30
+ } catch (error) {
31
+ console.error('Error checking XAI status:', error);
32
+
33
+ return {
34
+ status: 'degraded',
35
+ message: 'Unable to determine service status',
36
+ incidents: ['Note: Limited status information available'],
37
+ };
38
+ }
39
+ }
40
+ }
app/components/settings/providers/service-status/types.ts CHANGED
@@ -2,7 +2,6 @@ import type { IconType } from 'react-icons';
2
 
3
  export type ProviderName =
4
  | 'AmazonBedrock'
5
- | 'Anthropic'
6
  | 'Cohere'
7
  | 'Deepseek'
8
  | 'Google'
@@ -10,7 +9,6 @@ export type ProviderName =
10
  | 'HuggingFace'
11
  | 'Hyperbolic'
12
  | 'Mistral'
13
- | 'OpenAI'
14
  | 'OpenRouter'
15
  | 'Perplexity'
16
  | 'Together'
@@ -27,12 +25,12 @@ export type ServiceStatus = {
27
  incidents?: string[];
28
  };
29
 
30
- export type ProviderConfig = {
31
  statusUrl: string;
32
  apiUrl: string;
33
  headers: Record<string, string>;
34
  testModel: string;
35
- };
36
 
37
  export type ApiResponse = {
38
  error?: {
@@ -51,8 +49,7 @@ export type ApiResponse = {
51
  };
52
 
53
  export type StatusCheckResult = {
54
- status: ServiceStatus['status'];
55
- message?: string;
56
- incidents?: string[];
57
- responseTime?: number;
58
  };
 
2
 
3
  export type ProviderName =
4
  | 'AmazonBedrock'
 
5
  | 'Cohere'
6
  | 'Deepseek'
7
  | 'Google'
 
9
  | 'HuggingFace'
10
  | 'Hyperbolic'
11
  | 'Mistral'
 
12
  | 'OpenRouter'
13
  | 'Perplexity'
14
  | 'Together'
 
25
  incidents?: string[];
26
  };
27
 
28
+ export interface ProviderConfig {
29
  statusUrl: string;
30
  apiUrl: string;
31
  headers: Record<string, string>;
32
  testModel: string;
33
+ }
34
 
35
  export type ApiResponse = {
36
  error?: {
 
49
  };
50
 
51
  export type StatusCheckResult = {
52
+ status: 'operational' | 'degraded' | 'down';
53
+ message: string;
54
+ incidents: string[];
 
55
  };
app/components/settings/settings.styles.ts DELETED
@@ -1,43 +0,0 @@
1
- import { type ClassValue, clsx } from 'clsx';
2
- import { twMerge } from 'tailwind-merge';
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs));
6
- }
7
-
8
- export const settingsStyles = {
9
- // Card styles
10
- card: 'bg-bolt-elements-background dark:bg-bolt-elements-backgroundDark rounded-lg p-6 border border-bolt-elements-border dark:border-bolt-elements-borderDark',
11
-
12
- // Button styles
13
- button: {
14
- base: 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
15
- primary: 'bg-purple-500 text-white hover:bg-purple-600',
16
- secondary:
17
- 'bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondaryDark hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimaryDark',
18
- danger: 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20',
19
- warning: 'bg-yellow-50 text-yellow-600 hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
20
- success: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-500/10 dark:hover:bg-green-500/20',
21
- },
22
-
23
- // Form styles
24
- form: {
25
- label: 'block text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondaryDark mb-2',
26
- input:
27
- 'w-full px-3 py-2 rounded-lg text-sm bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark border border-bolt-elements-border dark:border-bolt-elements-borderDark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500',
28
- },
29
-
30
- // Search container
31
- search: {
32
- input:
33
- 'w-full h-10 pl-10 pr-4 rounded-lg text-sm bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark border border-bolt-elements-border dark:border-bolt-elements-borderDark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all',
34
- },
35
-
36
- // Scroll container styles
37
- scroll: {
38
- container: 'overflow-y-auto overscroll-y-contain',
39
- content: 'min-h-full',
40
- },
41
-
42
- 'loading-spinner': 'i-ph:spinner-gap-bold animate-spin w-4 h-4',
43
- } as const;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/components/settings/settings.types.ts CHANGED
@@ -97,13 +97,13 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
97
  { id: 'features', visible: true, window: 'developer', order: 3 },
98
  { id: 'data', visible: true, window: 'developer', order: 4 },
99
  { id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
100
- { id: 'service-status', visible: true, window: 'developer', order: 6 },
101
- { id: 'local-providers', visible: true, window: 'developer', order: 7 },
102
- { id: 'connection', visible: true, window: 'developer', order: 8 },
103
- { id: 'debug', visible: true, window: 'developer', order: 9 },
104
- { id: 'event-logs', visible: true, window: 'developer', order: 10 },
105
- { id: 'update', visible: true, window: 'developer', order: 11 },
106
- { id: 'task-manager', visible: true, window: 'developer', order: 12 },
107
  ];
108
 
109
  export const categoryLabels: Record<SettingCategory, string> = {
 
97
  { id: 'features', visible: true, window: 'developer', order: 3 },
98
  { id: 'data', visible: true, window: 'developer', order: 4 },
99
  { id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
100
+ { id: 'local-providers', visible: true, window: 'developer', order: 6 },
101
+ { id: 'connection', visible: true, window: 'developer', order: 7 },
102
+ { id: 'debug', visible: true, window: 'developer', order: 8 },
103
+ { id: 'event-logs', visible: true, window: 'developer', order: 9 },
104
+ { id: 'update', visible: true, window: 'developer', order: 10 },
105
+ { id: 'task-manager', visible: true, window: 'developer', order: 11 },
106
+ { id: 'service-status', visible: true, window: 'developer', order: 12 },
107
  ];
108
 
109
  export const categoryLabels: Record<SettingCategory, string> = {
app/components/settings/settings/SettingsTab.tsx CHANGED
@@ -5,7 +5,6 @@ import { classNames } from '~/utils/classNames';
5
  import { Switch } from '~/components/ui/Switch';
6
  import { themeStore, kTheme } from '~/lib/stores/theme';
7
  import type { UserProfile } from '~/components/settings/settings.types';
8
- import { settingsStyles } from '~/components/settings/settings.styles';
9
  import { useStore } from '@nanostores/react';
10
  import { shortcutsStore } from '~/lib/stores/settings';
11
 
@@ -97,11 +96,10 @@ export default function SettingsTab() {
97
  }
98
  }}
99
  className={classNames(
100
- settingsStyles.button.base,
101
- settings.theme === theme ? settingsStyles.button.primary : settingsStyles.button.secondary,
102
  settings.theme === theme
103
- ? 'dark:bg-purple-500 dark:text-white dark:hover:bg-purple-600 dark:hover:text-white'
104
- : 'hover:bg-purple-500/10 hover:text-purple-500 dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
105
  )}
106
  >
107
  <div
 
5
  import { Switch } from '~/components/ui/Switch';
6
  import { themeStore, kTheme } from '~/lib/stores/theme';
7
  import type { UserProfile } from '~/components/settings/settings.types';
 
8
  import { useStore } from '@nanostores/react';
9
  import { shortcutsStore } from '~/lib/stores/settings';
10
 
 
96
  }
97
  }}
98
  className={classNames(
99
+ 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
 
100
  settings.theme === theme
101
+ ? 'bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-500 dark:text-white dark:hover:bg-purple-600'
102
+ : 'bg-bolt-elements-hover dark:bg-[#1A1A1A] text-bolt-elements-textSecondary hover:bg-purple-500/10 hover:text-purple-500 dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
103
  )}
104
  >
105
  <div
app/components/settings/task-manager/TaskManagerTab.tsx CHANGED
@@ -23,19 +23,6 @@ interface BatteryManager extends EventTarget {
23
  level: number;
24
  }
25
 
26
- type ProcessStatus = 'active' | 'idle' | 'suspended';
27
- type ProcessImpact = 'high' | 'medium' | 'low';
28
-
29
- interface ProcessInfo {
30
- name: string;
31
- type: 'API' | 'Animation' | 'Background' | 'Network' | 'Storage';
32
- cpuUsage: number;
33
- memoryUsage: number;
34
- status: ProcessStatus;
35
- lastUpdate: string;
36
- impact: ProcessImpact;
37
- }
38
-
39
  interface SystemMetrics {
40
  cpu: number;
41
  memory: {
@@ -43,7 +30,6 @@ interface SystemMetrics {
43
  total: number;
44
  percentage: number;
45
  };
46
- activeProcesses: number;
47
  uptime: number;
48
  battery?: {
49
  level: number;
@@ -89,11 +75,9 @@ const BATTERY_THRESHOLD = 20; // Enable energy saver when battery below 20%
89
  const UPDATE_INTERVALS = {
90
  normal: {
91
  metrics: 1000, // 1s
92
- processes: 2000, // 2s
93
  },
94
  energySaver: {
95
  metrics: 5000, // 5s
96
- processes: 10000, // 10s
97
  },
98
  };
99
 
@@ -105,11 +89,9 @@ const ENERGY_COSTS = {
105
  };
106
 
107
  export default function TaskManagerTab() {
108
- const [processes, setProcesses] = useState<ProcessInfo[]>([]);
109
  const [metrics, setMetrics] = useState<SystemMetrics>({
110
  cpu: 0,
111
  memory: { used: 0, total: 0, percentage: 0 },
112
- activeProcesses: 0,
113
  uptime: 0,
114
  network: { downlink: 0, latency: 0, type: 'unknown' },
115
  });
@@ -120,10 +102,6 @@ export default function TaskManagerTab() {
120
  battery: [],
121
  network: [],
122
  });
123
- const [loading, setLoading] = useState({
124
- metrics: false,
125
- processes: false,
126
- });
127
  const [energySaverMode, setEnergySaverMode] = useState<boolean>(() => {
128
  // Initialize from localStorage, default to false
129
  const saved = localStorage.getItem('energySaverMode');
@@ -144,8 +122,6 @@ export default function TaskManagerTab() {
144
 
145
  const saverModeStartTime = useRef<number | null>(null);
146
 
147
- const [performanceObserver, setPerformanceObserver] = useState<PerformanceObserver | null>(null);
148
-
149
  // Handle energy saver mode changes
150
  const handleEnergySaverChange = (checked: boolean) => {
151
  setEnergySaverMode(checked);
@@ -166,54 +142,6 @@ export default function TaskManagerTab() {
166
  }
167
  };
168
 
169
- // Add this helper function at the top level of the component
170
- function isNetworkRequest(entry: PerformanceEntry): boolean {
171
- const resourceTiming = entry as PerformanceResourceTiming;
172
- return resourceTiming.initiatorType === 'fetch' && entry.duration === 0;
173
- }
174
-
175
- // Update getActiveProcessCount
176
- const getActiveProcessCount = async (): Promise<number> => {
177
- try {
178
- const networkCount = (navigator as any)?.connections?.length || 0;
179
- const swCount = (await navigator.serviceWorker?.getRegistrations().then((regs) => regs.length)) || 0;
180
- const animationCount = document.getAnimations().length;
181
- const fetchCount = performance.getEntriesByType('resource').filter(isNetworkRequest).length;
182
-
183
- return networkCount + swCount + animationCount + fetchCount;
184
- } catch (error) {
185
- console.error('Failed to get active process count:', error);
186
- return 0;
187
- }
188
- };
189
-
190
- // Update process cleanup
191
- const cleanupOldProcesses = useCallback(() => {
192
- const MAX_PROCESS_AGE = 30000; // 30 seconds
193
-
194
- setProcesses((currentProcesses) => {
195
- const now = Date.now();
196
- return currentProcesses.filter((process) => {
197
- const processTime = new Date(process.lastUpdate).getTime();
198
- const age = now - processTime;
199
-
200
- /*
201
- * Keep processes that are:
202
- * 1. Less than MAX_PROCESS_AGE old, or
203
- * 2. Currently active, or
204
- * 3. Service workers (they're managed separately)
205
- */
206
- return age < MAX_PROCESS_AGE || process.status === 'active' || process.type === 'Background';
207
- });
208
- });
209
- }, []);
210
-
211
- // Add cleanup interval
212
- useEffect(() => {
213
- const interval = setInterval(cleanupOldProcesses, 5000);
214
- return () => clearInterval(interval);
215
- }, [cleanupOldProcesses]);
216
-
217
  // Update energy savings calculation
218
  const updateEnergySavings = useCallback(() => {
219
  if (!energySaverMode) {
@@ -237,8 +165,7 @@ export default function TaskManagerTab() {
237
  const saverUpdatesPerMinute = 60 / (UPDATE_INTERVALS.energySaver.metrics / 1000);
238
  const updatesReduced = Math.floor((normalUpdatesPerMinute - saverUpdatesPerMinute) * (timeInSaverMode / 60));
239
 
240
- const processCount = processes.length;
241
- const energyPerUpdate = ENERGY_COSTS.update + processCount * ENERGY_COSTS.rendering;
242
  const energySaved = (updatesReduced * energyPerUpdate) / 3600;
243
 
244
  setEnergySavings({
@@ -246,7 +173,7 @@ export default function TaskManagerTab() {
246
  timeInSaverMode,
247
  estimatedEnergySaved: energySaved,
248
  });
249
- }, [energySaverMode, processes.length]);
250
 
251
  // Add interval for energy savings updates
252
  useEffect(() => {
@@ -254,153 +181,9 @@ export default function TaskManagerTab() {
254
  return () => clearInterval(interval);
255
  }, [updateEnergySavings]);
256
 
257
- // Improve process monitoring by adding unique IDs and timestamps
258
- const createProcess = (
259
- name: string,
260
- type: ProcessInfo['type'],
261
- cpuUsage: number,
262
- memoryUsage: number,
263
- status: ProcessStatus,
264
- impact: ProcessImpact,
265
- ): ProcessInfo => ({
266
- name,
267
- type,
268
- cpuUsage,
269
- memoryUsage,
270
- status,
271
- lastUpdate: new Date().toISOString(),
272
- impact,
273
- });
274
-
275
- // Update animation monitoring to track changes better
276
- const updateAnimations = useCallback(() => {
277
- const animations = document.getAnimations();
278
-
279
- setProcesses((currentProcesses) => {
280
- const nonAnimationProcesses = currentProcesses.filter((p) => !p.name.startsWith('Animation:'));
281
- const newAnimations = animations
282
- .slice(0, 5)
283
- .map((animation) =>
284
- createProcess(
285
- `Animation: ${animation.id || 'Unnamed'}`,
286
- 'Animation',
287
- animation.playState === 'running' ? 2 : 0,
288
- 1,
289
- animation.playState === 'running' ? 'active' : 'idle',
290
- 'low',
291
- ),
292
- );
293
-
294
- return [...nonAnimationProcesses, ...newAnimations];
295
- });
296
- }, []);
297
-
298
- // Add animation monitoring interval
299
- useEffect(() => {
300
- const interval = setInterval(updateAnimations, energySaverMode ? 5000 : 1000);
301
- return () => clearInterval(interval);
302
- }, [updateAnimations, energySaverMode]);
303
-
304
- useEffect((): (() => void) | undefined => {
305
- if (!autoEnergySaver) {
306
- // If auto mode is disabled, clear any forced energy saver state
307
- setEnergySaverMode(false);
308
- return undefined;
309
- }
310
-
311
- const checkBatteryStatus = async () => {
312
- try {
313
- const battery = await navigator.getBattery();
314
- const shouldEnableSaver = !battery.charging && battery.level * 100 <= BATTERY_THRESHOLD;
315
- setEnergySaverMode(shouldEnableSaver);
316
- } catch {
317
- console.log('Battery API not available');
318
- }
319
- };
320
-
321
- checkBatteryStatus();
322
-
323
- const batteryCheckInterval = setInterval(checkBatteryStatus, 60000);
324
-
325
- return () => clearInterval(batteryCheckInterval);
326
- }, [autoEnergySaver]);
327
-
328
- const getUsageColor = (usage: number): string => {
329
- if (usage > 80) {
330
- return 'text-red-500';
331
- }
332
-
333
- if (usage > 50) {
334
- return 'text-yellow-500';
335
- }
336
-
337
- return 'text-gray-500';
338
- };
339
-
340
- const getImpactColor = (impact: ProcessImpact): string => {
341
- if (impact === 'high') {
342
- return 'text-red-500';
343
- }
344
-
345
- if (impact === 'medium') {
346
- return 'text-yellow-500';
347
- }
348
-
349
- return 'text-gray-500';
350
- };
351
-
352
- const renderUsageGraph = (data: number[], label: string, color: string) => {
353
- const chartData = {
354
- labels: metricsHistory.timestamps,
355
- datasets: [
356
- {
357
- label,
358
- data,
359
- borderColor: color,
360
- fill: false,
361
- tension: 0.4,
362
- },
363
- ],
364
- };
365
-
366
- const options = {
367
- responsive: true,
368
- maintainAspectRatio: false,
369
- scales: {
370
- y: {
371
- beginAtZero: true,
372
- max: 100,
373
- grid: {
374
- color: 'rgba(255, 255, 255, 0.1)',
375
- },
376
- },
377
- x: {
378
- grid: {
379
- display: false,
380
- },
381
- },
382
- },
383
- plugins: {
384
- legend: {
385
- display: false,
386
- },
387
- },
388
- animation: {
389
- duration: 0,
390
- } as const,
391
- };
392
-
393
- return (
394
- <div className="h-32">
395
- <Line data={chartData} options={options} />
396
- </div>
397
- );
398
- };
399
-
400
  const updateMetrics = async () => {
401
  try {
402
- setLoading((prev) => ({ ...prev, metrics: true }));
403
-
404
  // Get memory info using Performance API
405
  const memory = performance.memory || {
406
  jsHeapSizeLimit: 0,
@@ -444,7 +227,6 @@ export default function TaskManagerTab() {
444
  total: Math.round(totalMem),
445
  percentage: Math.round(memPercentage),
446
  },
447
- activeProcesses: await getActiveProcessCount(),
448
  uptime: performance.now() / 1000,
449
  battery: batteryInfo,
450
  network: networkInfo,
@@ -465,8 +247,6 @@ export default function TaskManagerTab() {
465
  });
466
  } catch (error: unknown) {
467
  console.error('Failed to update system metrics:', error);
468
- } finally {
469
- setLoading((prev) => ({ ...prev, metrics: false }));
470
  }
471
  };
472
 
@@ -530,206 +310,129 @@ export default function TaskManagerTab() {
530
  return () => connection.removeEventListener('change', updateNetworkInfo);
531
  }, []);
532
 
533
- // Add this effect for live process monitoring
534
  useEffect(() => {
535
- // Clean up previous observer if exists
536
- performanceObserver?.disconnect();
537
-
538
- // Create new performance observer for network requests
539
- const observer = new PerformanceObserver((list) => {
540
- const entries = list.getEntries();
541
- const newNetworkEntries = entries
542
- .filter((entry: PerformanceEntry): boolean => {
543
- const resourceTiming = entry as PerformanceResourceTiming;
544
- return entry.entryType === 'resource' && resourceTiming.initiatorType === 'fetch';
545
- })
546
- .slice(-5);
547
-
548
- if (newNetworkEntries.length > 0) {
549
- setProcesses((currentProcesses) => {
550
- // Remove old network processes
551
- const filteredProcesses = currentProcesses.filter((p) => !p.name.startsWith('Network Request:'));
552
-
553
- // Add new network processes
554
- const newProcesses = newNetworkEntries.map((entry) => ({
555
- name: `Network Request: ${new URL((entry as PerformanceResourceTiming).name).pathname}`,
556
- type: 'Network' as const,
557
- cpuUsage: entry.duration > 0 ? entry.duration / 100 : 0,
558
- memoryUsage: (entry as PerformanceResourceTiming).encodedBodySize / (1024 * 1024),
559
- status: (entry.duration === 0 ? 'active' : 'idle') as ProcessStatus,
560
- lastUpdate: new Date().toISOString(),
561
- impact: (entry.duration > 1000 ? 'high' : entry.duration > 500 ? 'medium' : 'low') as ProcessImpact,
562
- })) as ProcessInfo[];
563
-
564
- return [...filteredProcesses, ...newProcesses];
565
- });
566
- }
567
- });
568
-
569
- // Start observing resource timing entries
570
- observer.observe({ entryTypes: ['resource'] });
571
- setPerformanceObserver(observer);
572
-
573
- // Set up animation observer
574
- const animationObserver = new MutationObserver(() => {
575
- const animations = document.getAnimations();
576
-
577
- setProcesses((currentProcesses) => {
578
- // Remove old animation processes
579
- const filteredProcesses = currentProcesses.filter((p) => !p.name.startsWith('Animation:'));
580
-
581
- // Add current animations
582
- const animationProcesses = animations.slice(0, 5).map((animation) => ({
583
- name: `Animation: ${animation.id || 'Unnamed'}`,
584
- type: 'Animation' as const,
585
- cpuUsage: animation.playState === 'running' ? 2 : 0,
586
- memoryUsage: 1,
587
- status: (animation.playState === 'running' ? 'active' : 'idle') as ProcessStatus,
588
- lastUpdate: new Date().toISOString(),
589
- impact: 'low' as ProcessImpact,
590
- })) as ProcessInfo[];
591
-
592
- return [...filteredProcesses, ...animationProcesses];
593
- });
594
- });
595
-
596
- // Observe DOM changes that might trigger animations
597
- animationObserver.observe(document.body, {
598
- childList: true,
599
- subtree: true,
600
- attributes: true,
601
- });
602
-
603
- // Set up service worker observer
604
- const checkServiceWorkers = async () => {
605
- const serviceWorkers = (await navigator.serviceWorker?.getRegistrations()) || [];
606
-
607
- setProcesses((currentProcesses) => {
608
- // Remove old service worker processes
609
- const filteredProcesses = currentProcesses.filter((p) => !p.name.startsWith('Service Worker:'));
610
-
611
- // Add current service workers
612
- const swProcesses = serviceWorkers.map((sw) => ({
613
- name: `Service Worker: ${sw.scope}`,
614
- type: 'Background' as const,
615
- cpuUsage: sw.active ? 1 : 0,
616
- memoryUsage: 5,
617
- status: (sw.active ? 'active' : 'idle') as ProcessStatus,
618
- lastUpdate: new Date().toISOString(),
619
- impact: 'low' as ProcessImpact,
620
- })) as ProcessInfo[];
621
-
622
- return [...filteredProcesses, ...swProcesses];
623
- });
624
- };
625
-
626
- // Check service workers periodically
627
- const swInterval = setInterval(checkServiceWorkers, 5000);
628
 
629
- // Clean up
630
  return () => {
631
- performanceObserver?.disconnect();
632
- animationObserver.disconnect();
633
- clearInterval(swInterval);
634
  };
635
- }, []);
636
-
637
- // Update the updateProcesses function
638
- const updateProcesses = async () => {
639
- try {
640
- setLoading((prev) => ({ ...prev, processes: true }));
641
-
642
- // Get initial process information
643
- const processes: ProcessInfo[] = [];
644
-
645
- // Add initial network processes
646
- const networkEntries = performance
647
- .getEntriesByType('resource')
648
- .filter((entry: PerformanceEntry): boolean => {
649
- const resourceTiming = entry as PerformanceResourceTiming;
650
- return entry.entryType === 'resource' && resourceTiming.initiatorType === 'fetch';
651
- })
652
- .slice(-5);
653
-
654
- networkEntries.forEach((entry) => {
655
- processes.push({
656
- name: `Network Request: ${new URL((entry as PerformanceResourceTiming).name).pathname}`,
657
- type: 'Network',
658
- cpuUsage: entry.duration > 0 ? entry.duration / 100 : 0,
659
- memoryUsage: (entry as PerformanceResourceTiming).encodedBodySize / (1024 * 1024),
660
- status: (entry.duration === 0 ? 'active' : 'idle') as ProcessStatus,
661
- lastUpdate: new Date().toISOString(),
662
- impact: (entry.duration > 1000 ? 'high' : entry.duration > 500 ? 'medium' : 'low') as ProcessImpact,
663
- });
664
- });
665
-
666
- // Add initial animations
667
- document
668
- .getAnimations()
669
- .slice(0, 5)
670
- .forEach((animation) => {
671
- processes.push({
672
- name: `Animation: ${animation.id || 'Unnamed'}`,
673
- type: 'Animation',
674
- cpuUsage: animation.playState === 'running' ? 2 : 0,
675
- memoryUsage: 1,
676
- status: (animation.playState === 'running' ? 'active' : 'idle') as ProcessStatus,
677
- lastUpdate: new Date().toISOString(),
678
- impact: 'low' as ProcessImpact,
679
- });
680
- });
681
-
682
- // Add initial service workers
683
- const serviceWorkers = (await navigator.serviceWorker?.getRegistrations()) || [];
684
- serviceWorkers.forEach((sw) => {
685
- processes.push({
686
- name: `Service Worker: ${sw.scope}`,
687
- type: 'Background',
688
- cpuUsage: sw.active ? 1 : 0,
689
- memoryUsage: 5,
690
- status: (sw.active ? 'active' : 'idle') as ProcessStatus,
691
- lastUpdate: new Date().toISOString(),
692
- impact: 'low' as ProcessImpact,
693
- });
694
- });
695
-
696
- setProcesses(processes);
697
- } catch (error) {
698
- console.error('Failed to update process list:', error);
699
- } finally {
700
- setLoading((prev) => ({ ...prev, processes: false }));
701
- }
702
- };
703
 
704
  // Initial update effect
705
  useEffect((): (() => void) => {
706
  // Initial update
707
  updateMetrics();
708
- updateProcesses();
709
 
710
  // Set up intervals for live updates
711
  const metricsInterval = setInterval(
712
  updateMetrics,
713
  energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
714
  );
715
- const processesInterval = setInterval(
716
- updateProcesses,
717
- energySaverMode ? UPDATE_INTERVALS.energySaver.processes : UPDATE_INTERVALS.normal.processes,
718
- );
719
 
720
  // Cleanup on unmount
721
  return () => {
722
  clearInterval(metricsInterval);
723
- clearInterval(processesInterval);
724
  };
725
  }, [energySaverMode]); // Re-create intervals when energy saver mode changes
726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
  return (
728
- <div className="space-y-6">
729
- {/* System Overview */}
730
- <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
731
- <div className="flex justify-between items-center mb-4">
732
- <h2 className="text-lg font-medium text-bolt-elements-textPrimary">System Overview</h2>
733
  <div className="flex items-center gap-4">
734
  <div className="flex items-center gap-2">
735
  <input
@@ -763,185 +466,86 @@ export default function TaskManagerTab() {
763
  </div>
764
  </div>
765
 
766
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
767
- <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
768
- <div className="flex items-center gap-2 mb-2">
769
- <div className="i-ph:cpu text-gray-500 dark:text-gray-400 w-4 h-4" />
770
  <span className="text-sm text-bolt-elements-textSecondary">CPU Usage</span>
 
 
 
771
  </div>
772
- <p className={classNames('text-lg font-medium', getUsageColor(metrics.cpu))}>{Math.round(metrics.cpu)}%</p>
773
  {renderUsageGraph(metricsHistory.cpu, 'CPU', '#9333ea')}
774
  </div>
775
 
776
- <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
777
- <div className="flex items-center gap-2 mb-2">
778
- <div className="i-ph:database text-gray-500 dark:text-gray-400 w-4 h-4" />
779
  <span className="text-sm text-bolt-elements-textSecondary">Memory Usage</span>
 
 
 
780
  </div>
781
- <p className={classNames('text-lg font-medium', getUsageColor(metrics.memory.percentage))}>
782
- {metrics.memory.used}MB / {metrics.memory.total}MB
783
- </p>
784
  {renderUsageGraph(metricsHistory.memory, 'Memory', '#2563eb')}
785
  </div>
786
 
787
- <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
788
- <div className="flex items-center gap-2 mb-2">
789
- <div className="i-ph:battery text-gray-500 dark:text-gray-400 w-4 h-4" />
790
- <span className="text-sm text-bolt-elements-textSecondary">Battery</span>
791
- </div>
792
- {metrics.battery ? (
793
- <div>
794
- <p className="text-lg font-medium text-bolt-elements-textPrimary">
795
- {Math.round(metrics.battery.level)}%
796
- {metrics.battery.charging && (
797
- <span className="ml-2 text-bolt-elements-textSecondary">
798
- <div className="i-ph:lightning-fill w-4 h-4 inline-block" />
799
- </span>
800
- )}
801
- </p>
802
- {metrics.battery.timeRemaining && metrics.battery.timeRemaining !== Infinity && (
803
- <p className="text-xs text-bolt-elements-textSecondary mt-1">
804
- {metrics.battery.charging ? 'Full in: ' : 'Remaining: '}
805
- {Math.round(metrics.battery.timeRemaining / 60)}m
806
- </p>
807
- )}
808
- {renderUsageGraph(metricsHistory.battery, 'Battery', '#22c55e')}
809
  </div>
810
- ) : (
811
- <p className="text-sm text-bolt-elements-textSecondary">Not available</p>
812
- )}
813
- </div>
814
 
815
- <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
816
- <div className="flex items-center gap-2 mb-2">
817
- <div className="i-ph:wifi text-gray-500 dark:text-gray-400 w-4 h-4" />
818
  <span className="text-sm text-bolt-elements-textSecondary">Network</span>
 
 
 
819
  </div>
820
- <p className="text-lg font-medium text-bolt-elements-textPrimary">{metrics.network.downlink} Mbps</p>
821
- <p className="text-xs text-bolt-elements-textSecondary mt-1">Latency: {metrics.network.latency}ms</p>
822
  {renderUsageGraph(metricsHistory.network, 'Network', '#f59e0b')}
823
  </div>
824
  </div>
825
- </div>
826
-
827
- {/* Process List */}
828
- <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
829
- <div className="flex items-center justify-between mb-4">
830
- <div className="flex items-center gap-3">
831
- <div className="i-ph:list-bullets text-purple-500 w-5 h-5" />
832
- <h3 className="text-base font-medium text-bolt-elements-textPrimary">Active Processes</h3>
833
- </div>
834
- <button
835
- onClick={updateProcesses}
836
- className={classNames(
837
- 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
838
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
839
- 'hover:bg-purple-500/10 hover:text-purple-500',
840
- 'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
841
- 'text-bolt-elements-textPrimary',
842
- 'transition-colors duration-200',
843
- { 'opacity-50 cursor-not-allowed': loading.processes },
844
- )}
845
- disabled={loading.processes}
846
- >
847
- <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.processes ? 'animate-spin' : '')} />
848
- Refresh
849
- </button>
850
- </div>
851
-
852
- <div className="overflow-x-auto">
853
- <table className="w-full">
854
- <thead>
855
- <tr className="border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
856
- <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Process</th>
857
- <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Type</th>
858
- <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">CPU</th>
859
- <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Memory</th>
860
- <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Status</th>
861
- <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Impact</th>
862
- <th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">
863
- Last Update
864
- </th>
865
- </tr>
866
- </thead>
867
- <tbody>
868
- {processes.map((process, index) => (
869
- <tr
870
- key={index}
871
- data-process={process.name}
872
- className="border-b border-[#E5E5E5] dark:border-[#1A1A1A] last:border-0"
873
- >
874
- <td className="py-3 px-4">
875
- <div className="flex items-center gap-2">
876
- <div className="i-ph:cube text-gray-500 dark:text-gray-400 w-4 h-4" />
877
- <span className="text-sm text-bolt-elements-textPrimary">{process.name}</span>
878
- </div>
879
- </td>
880
- <td className="py-3 px-4">
881
- <span className="text-sm text-bolt-elements-textSecondary">{process.type}</span>
882
- </td>
883
- <td className="py-3 px-4">
884
- <span className={classNames('text-sm', getUsageColor(process.cpuUsage))}>
885
- {process.cpuUsage.toFixed(1)}%
886
- </span>
887
- </td>
888
- <td className="py-3 px-4">
889
- <span className={classNames('text-sm', getUsageColor(process.memoryUsage))}>
890
- {process.memoryUsage.toFixed(1)} MB
891
- </span>
892
- </td>
893
- <td className="py-3 px-4">
894
- <span className={classNames('text-sm text-bolt-elements-textSecondary capitalize')}>
895
- {process.status}
896
- </span>
897
- </td>
898
- <td className="py-3 px-4">
899
- <span className={classNames('text-sm', getImpactColor(process.impact))}>{process.impact}</span>
900
- </td>
901
- <td className="py-3 px-4">
902
- <span className="text-sm text-bolt-elements-textSecondary">
903
- {new Date(process.lastUpdate).toLocaleTimeString()}
904
- </span>
905
- </td>
906
- </tr>
907
- ))}
908
- </tbody>
909
- </table>
910
- </div>
911
- </div>
912
-
913
- {/* Energy Savings */}
914
- <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
915
- <h3 className="text-base font-medium text-bolt-elements-textPrimary">Energy Savings</h3>
916
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
917
- <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
918
- <div className="flex items-center gap-2 mb-2">
919
- <div className="i-ph:clock text-gray-500 dark:text-gray-400 w-4 h-4" />
920
- <span className="text-sm text-bolt-elements-textSecondary">Time in Saver Mode</span>
921
- </div>
922
- <p className="text-lg font-medium text-bolt-elements-textPrimary">
923
- {Math.floor(energySavings.timeInSaverMode / 60)}m {Math.floor(energySavings.timeInSaverMode % 60)}s
924
- </p>
925
- </div>
926
-
927
- <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
928
- <div className="flex items-center gap-2 mb-2">
929
- <div className="i-ph:chart-line text-gray-500 dark:text-gray-400 w-4 h-4" />
930
- <span className="text-sm text-bolt-elements-textSecondary">Updates Reduced</span>
931
- </div>
932
- <p className="text-lg font-medium text-bolt-elements-textPrimary">{energySavings.updatesReduced}</p>
933
- </div>
934
 
935
- <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
936
- <div className="flex items-center gap-2 mb-2">
937
- <div className="i-ph:battery text-gray-500 dark:text-gray-400 w-4 h-4" />
938
- <span className="text-sm text-bolt-elements-textSecondary">Estimated Energy Saved</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
939
  </div>
940
- <p className="text-lg font-medium text-bolt-elements-textPrimary">
941
- {energySavings.estimatedEnergySaved.toFixed(2)} mWh
942
- </p>
943
  </div>
944
- </div>
945
  </div>
946
  </div>
947
  );
 
23
  level: number;
24
  }
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  interface SystemMetrics {
27
  cpu: number;
28
  memory: {
 
30
  total: number;
31
  percentage: number;
32
  };
 
33
  uptime: number;
34
  battery?: {
35
  level: number;
 
75
  const UPDATE_INTERVALS = {
76
  normal: {
77
  metrics: 1000, // 1s
 
78
  },
79
  energySaver: {
80
  metrics: 5000, // 5s
 
81
  },
82
  };
83
 
 
89
  };
90
 
91
  export default function TaskManagerTab() {
 
92
  const [metrics, setMetrics] = useState<SystemMetrics>({
93
  cpu: 0,
94
  memory: { used: 0, total: 0, percentage: 0 },
 
95
  uptime: 0,
96
  network: { downlink: 0, latency: 0, type: 'unknown' },
97
  });
 
102
  battery: [],
103
  network: [],
104
  });
 
 
 
 
105
  const [energySaverMode, setEnergySaverMode] = useState<boolean>(() => {
106
  // Initialize from localStorage, default to false
107
  const saved = localStorage.getItem('energySaverMode');
 
122
 
123
  const saverModeStartTime = useRef<number | null>(null);
124
 
 
 
125
  // Handle energy saver mode changes
126
  const handleEnergySaverChange = (checked: boolean) => {
127
  setEnergySaverMode(checked);
 
142
  }
143
  };
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  // Update energy savings calculation
146
  const updateEnergySavings = useCallback(() => {
147
  if (!energySaverMode) {
 
165
  const saverUpdatesPerMinute = 60 / (UPDATE_INTERVALS.energySaver.metrics / 1000);
166
  const updatesReduced = Math.floor((normalUpdatesPerMinute - saverUpdatesPerMinute) * (timeInSaverMode / 60));
167
 
168
+ const energyPerUpdate = ENERGY_COSTS.update;
 
169
  const energySaved = (updatesReduced * energyPerUpdate) / 3600;
170
 
171
  setEnergySavings({
 
173
  timeInSaverMode,
174
  estimatedEnergySaved: energySaved,
175
  });
176
+ }, [energySaverMode]);
177
 
178
  // Add interval for energy savings updates
179
  useEffect(() => {
 
181
  return () => clearInterval(interval);
182
  }, [updateEnergySavings]);
183
 
184
+ // Update metrics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  const updateMetrics = async () => {
186
  try {
 
 
187
  // Get memory info using Performance API
188
  const memory = performance.memory || {
189
  jsHeapSizeLimit: 0,
 
227
  total: Math.round(totalMem),
228
  percentage: Math.round(memPercentage),
229
  },
 
230
  uptime: performance.now() / 1000,
231
  battery: batteryInfo,
232
  network: networkInfo,
 
247
  });
248
  } catch (error: unknown) {
249
  console.error('Failed to update system metrics:', error);
 
 
250
  }
251
  };
252
 
 
310
  return () => connection.removeEventListener('change', updateNetworkInfo);
311
  }, []);
312
 
313
+ // Remove all animation and process monitoring
314
  useEffect(() => {
315
+ const metricsInterval = setInterval(
316
+ () => {
317
+ if (!energySaverMode) {
318
+ updateMetrics();
319
+ }
320
+ },
321
+ energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
322
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
 
324
  return () => {
325
+ clearInterval(metricsInterval);
 
 
326
  };
327
+ }, [energySaverMode]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
  // Initial update effect
330
  useEffect((): (() => void) => {
331
  // Initial update
332
  updateMetrics();
 
333
 
334
  // Set up intervals for live updates
335
  const metricsInterval = setInterval(
336
  updateMetrics,
337
  energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
338
  );
 
 
 
 
339
 
340
  // Cleanup on unmount
341
  return () => {
342
  clearInterval(metricsInterval);
 
343
  };
344
  }, [energySaverMode]); // Re-create intervals when energy saver mode changes
345
 
346
+ const getUsageColor = (usage: number): string => {
347
+ if (usage > 80) {
348
+ return 'text-red-500';
349
+ }
350
+
351
+ if (usage > 50) {
352
+ return 'text-yellow-500';
353
+ }
354
+
355
+ return 'text-gray-500';
356
+ };
357
+
358
+ const renderUsageGraph = (data: number[], label: string, color: string) => {
359
+ const chartData = {
360
+ labels: metricsHistory.timestamps,
361
+ datasets: [
362
+ {
363
+ label,
364
+ data,
365
+ borderColor: color,
366
+ fill: false,
367
+ tension: 0.4,
368
+ },
369
+ ],
370
+ };
371
+
372
+ const options = {
373
+ responsive: true,
374
+ maintainAspectRatio: false,
375
+ scales: {
376
+ y: {
377
+ beginAtZero: true,
378
+ max: 100,
379
+ grid: {
380
+ color: 'rgba(255, 255, 255, 0.1)',
381
+ },
382
+ },
383
+ x: {
384
+ grid: {
385
+ display: false,
386
+ },
387
+ },
388
+ },
389
+ plugins: {
390
+ legend: {
391
+ display: false,
392
+ },
393
+ },
394
+ animation: {
395
+ duration: 0,
396
+ } as const,
397
+ };
398
+
399
+ return (
400
+ <div className="h-32">
401
+ <Line data={chartData} options={options} />
402
+ </div>
403
+ );
404
+ };
405
+
406
+ useEffect((): (() => void) | undefined => {
407
+ if (!autoEnergySaver) {
408
+ // If auto mode is disabled, clear any forced energy saver state
409
+ setEnergySaverMode(false);
410
+ return undefined;
411
+ }
412
+
413
+ const checkBatteryStatus = async () => {
414
+ try {
415
+ const battery = await navigator.getBattery();
416
+ const shouldEnableSaver = !battery.charging && battery.level * 100 <= BATTERY_THRESHOLD;
417
+ setEnergySaverMode(shouldEnableSaver);
418
+ } catch {
419
+ console.log('Battery API not available');
420
+ }
421
+ };
422
+
423
+ checkBatteryStatus();
424
+
425
+ const batteryCheckInterval = setInterval(checkBatteryStatus, 60000);
426
+
427
+ return () => clearInterval(batteryCheckInterval);
428
+ }, [autoEnergySaver]);
429
+
430
  return (
431
+ <div className="flex flex-col gap-6">
432
+ {/* System Metrics */}
433
+ <div className="flex flex-col gap-4">
434
+ <div className="flex items-center justify-between">
435
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">System Metrics</h3>
436
  <div className="flex items-center gap-4">
437
  <div className="flex items-center gap-2">
438
  <input
 
466
  </div>
467
  </div>
468
 
469
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
470
+ {/* CPU Usage */}
471
+ <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
472
+ <div className="flex items-center justify-between">
473
  <span className="text-sm text-bolt-elements-textSecondary">CPU Usage</span>
474
+ <span className={classNames('text-sm font-medium', getUsageColor(metrics.cpu))}>
475
+ {Math.round(metrics.cpu)}%
476
+ </span>
477
  </div>
 
478
  {renderUsageGraph(metricsHistory.cpu, 'CPU', '#9333ea')}
479
  </div>
480
 
481
+ {/* Memory Usage */}
482
+ <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
483
+ <div className="flex items-center justify-between">
484
  <span className="text-sm text-bolt-elements-textSecondary">Memory Usage</span>
485
+ <span className={classNames('text-sm font-medium', getUsageColor(metrics.memory.percentage))}>
486
+ {Math.round(metrics.memory.percentage)}%
487
+ </span>
488
  </div>
 
 
 
489
  {renderUsageGraph(metricsHistory.memory, 'Memory', '#2563eb')}
490
  </div>
491
 
492
+ {/* Battery */}
493
+ {metrics.battery && (
494
+ <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
495
+ <div className="flex items-center justify-between">
496
+ <span className="text-sm text-bolt-elements-textSecondary">Battery</span>
497
+ <div className="flex items-center gap-2">
498
+ {metrics.battery.charging && <div className="i-ph:lightning-fill w-4 h-4 text-bolt-action-primary" />}
499
+ <span
500
+ className={classNames(
501
+ 'text-sm font-medium',
502
+ metrics.battery.level > 20 ? 'text-bolt-elements-textPrimary' : 'text-red-500',
503
+ )}
504
+ >
505
+ {Math.round(metrics.battery.level)}%
506
+ </span>
507
+ </div>
 
 
 
 
 
 
508
  </div>
509
+ {renderUsageGraph(metricsHistory.battery, 'Battery', '#22c55e')}
510
+ </div>
511
+ )}
 
512
 
513
+ {/* Network */}
514
+ <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
515
+ <div className="flex items-center justify-between">
516
  <span className="text-sm text-bolt-elements-textSecondary">Network</span>
517
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">
518
+ {metrics.network.downlink.toFixed(1)} Mbps
519
+ </span>
520
  </div>
 
 
521
  {renderUsageGraph(metricsHistory.network, 'Network', '#f59e0b')}
522
  </div>
523
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
 
525
+ {/* Energy Savings */}
526
+ {energySaverMode && (
527
+ <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
528
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary">Energy Savings</h4>
529
+ <div className="grid grid-cols-3 gap-4">
530
+ <div>
531
+ <span className="text-sm text-bolt-elements-textSecondary">Updates Reduced</span>
532
+ <p className="text-lg font-medium text-bolt-elements-textPrimary">{energySavings.updatesReduced}</p>
533
+ </div>
534
+ <div>
535
+ <span className="text-sm text-bolt-elements-textSecondary">Time in Saver Mode</span>
536
+ <p className="text-lg font-medium text-bolt-elements-textPrimary">
537
+ {Math.floor(energySavings.timeInSaverMode / 60)}m {Math.floor(energySavings.timeInSaverMode % 60)}s
538
+ </p>
539
+ </div>
540
+ <div>
541
+ <span className="text-sm text-bolt-elements-textSecondary">Energy Saved</span>
542
+ <p className="text-lg font-medium text-bolt-elements-textPrimary">
543
+ {energySavings.estimatedEnergySaved.toFixed(2)} mWh
544
+ </p>
545
+ </div>
546
  </div>
 
 
 
547
  </div>
548
+ )}
549
  </div>
550
  </div>
551
  );
app/components/settings/user/UsersWindow.tsx CHANGED
@@ -263,8 +263,6 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
263
  });
264
  }, [tabConfiguration, profile.notifications]);
265
 
266
- console.log('Filtered visible user tabs:', visibleUserTabs);
267
-
268
  const moveTab = (dragIndex: number, hoverIndex: number) => {
269
  const draggedTab = visibleUserTabs[dragIndex];
270
  const targetTab = visibleUserTabs[hoverIndex];
 
263
  });
264
  }, [tabConfiguration, profile.notifications]);
265
 
 
 
266
  const moveTab = (dragIndex: number, hoverIndex: number) => {
267
  const draggedTab = visibleUserTabs[dragIndex];
268
  const targetTab = visibleUserTabs[hoverIndex];
app/components/ui/Badge.tsx CHANGED
@@ -2,17 +2,19 @@
2
 
3
  import * as React from 'react';
4
  import { cva, type VariantProps } from 'class-variance-authority';
5
- import { cn } from '~/lib/utils';
6
 
7
  const badgeVariants = cva(
8
- 'inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
9
  {
10
  variants: {
11
  variant: {
12
- default: 'border-transparent bg-primary text-primary-foreground',
13
- secondary: 'border-transparent bg-secondary text-secondary-foreground',
14
- destructive: 'border-transparent bg-red-500/10 text-red-500 dark:bg-red-900/30',
15
- outline: 'text-foreground',
 
 
16
  },
17
  },
18
  defaultVariants: {
@@ -21,13 +23,10 @@ const badgeVariants = cva(
21
  },
22
  );
23
 
24
- interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
25
- variant?: 'default' | 'secondary' | 'destructive' | 'outline';
26
- }
27
 
28
- function Badge({ className, variant = 'default', ...props }: BadgeProps) {
29
- return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
30
  }
31
 
32
  export { Badge, badgeVariants };
33
- export type { BadgeProps };
 
2
 
3
  import * as React from 'react';
4
  import { cva, type VariantProps } from 'class-variance-authority';
5
+ import { classNames } from '~/utils/classNames';
6
 
7
  const badgeVariants = cva(
8
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-bolt-elements-ring focus:ring-offset-2',
9
  {
10
  variants: {
11
  variant: {
12
+ default:
13
+ 'border-transparent bg-bolt-elements-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background/80',
14
+ secondary:
15
+ 'border-transparent bg-bolt-elements-background text-bolt-elements-textSecondary hover:bg-bolt-elements-background/80',
16
+ destructive: 'border-transparent bg-red-500/10 text-red-500 hover:bg-red-500/20',
17
+ outline: 'text-bolt-elements-textPrimary',
18
  },
19
  },
20
  defaultVariants: {
 
23
  },
24
  );
25
 
26
+ export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
 
 
27
 
28
+ function Badge({ className, variant, ...props }: BadgeProps) {
29
+ return <div className={classNames(badgeVariants({ variant }), className)} {...props} />;
30
  }
31
 
32
  export { Badge, badgeVariants };
 
app/components/ui/Button.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import * as React from 'react';
2
  import { cva, type VariantProps } from 'class-variance-authority';
3
- import { cn } from '~/lib/utils';
4
 
5
  const buttonVariants = cva(
6
  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-bolt-elements-borderColor disabled:pointer-events-none disabled:opacity-50',
@@ -38,7 +38,7 @@ export interface ButtonProps
38
 
39
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
  ({ className, variant, size, _asChild = false, ...props }, ref) => {
41
- return <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
42
  },
43
  );
44
  Button.displayName = 'Button';
 
1
  import * as React from 'react';
2
  import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { classNames } from '~/utils/classNames';
4
 
5
  const buttonVariants = cva(
6
  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-bolt-elements-borderColor disabled:pointer-events-none disabled:opacity-50',
 
38
 
39
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
  ({ className, variant, size, _asChild = false, ...props }, ref) => {
41
+ return <button className={classNames(buttonVariants({ variant, size }), className)} ref={ref} {...props} />;
42
  },
43
  );
44
  Button.displayName = 'Button';
app/components/ui/Card.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { forwardRef } from 'react';
2
- import { cn } from '~/lib/utils';
3
 
4
  export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
5
 
@@ -7,7 +7,7 @@ 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
  )}
@@ -18,27 +18,38 @@ const Card = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref
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 };
 
 
 
 
 
 
1
  import { forwardRef } from 'react';
2
+ import { classNames } from '~/utils/classNames';
3
 
4
  export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
5
 
 
7
  return (
8
  <div
9
  ref={ref}
10
+ className={classNames(
11
  'rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary shadow-sm',
12
  className,
13
  )}
 
18
  Card.displayName = 'Card';
19
 
20
  const CardHeader = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
21
+ return <div ref={ref} className={classNames('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 (
28
+ <h3
29
+ ref={ref}
30
+ className={classNames('text-2xl font-semibold leading-none tracking-tight', className)}
31
+ {...props}
32
+ />
33
+ );
34
  },
35
  );
36
  CardTitle.displayName = 'CardTitle';
37
 
38
  const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
39
  ({ className, ...props }, ref) => {
40
+ return <p ref={ref} className={classNames('text-sm text-bolt-elements-textSecondary', className)} {...props} />;
41
  },
42
  );
43
  CardDescription.displayName = 'CardDescription';
44
 
45
  const CardContent = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
46
+ return <div ref={ref} className={classNames('p-6 pt-0', className)} {...props} />;
47
  });
48
  CardContent.displayName = 'CardContent';
49
 
50
+ const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
51
+ <div ref={ref} className={classNames('flex items-center p-6 pt-0', className)} {...props} />
52
+ ));
53
+ CardFooter.displayName = 'CardFooter';
54
+
55
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
app/components/ui/Input.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { forwardRef } from 'react';
2
- import { cn } from '~/lib/utils';
3
 
4
  export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
5
 
@@ -7,11 +7,8 @@ const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type, ...pr
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}
 
1
  import { forwardRef } from 'react';
2
+ import { classNames } from '~/utils/classNames';
3
 
4
  export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
5
 
 
7
  return (
8
  <input
9
  type={type}
10
+ className={classNames(
11
+ 'flex h-10 w-full rounded-md border border-bolt-elements-border bg-bolt-elements-background px-3 py-2 text-sm ring-offset-bolt-elements-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-bolt-elements-textSecondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bolt-elements-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
 
 
 
12
  className,
13
  )}
14
  ref={ref}
app/components/ui/Label.tsx CHANGED
@@ -1,22 +1,20 @@
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 };
 
1
+ import * as React from 'react';
2
+ import * as LabelPrimitive from '@radix-ui/react-label';
3
+ import { classNames } from '~/utils/classNames';
4
 
5
+ const Label = React.forwardRef<
6
+ React.ElementRef<typeof LabelPrimitive.Root>,
7
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
8
+ >(({ className, ...props }, ref) => (
9
+ <LabelPrimitive.Root
10
+ ref={ref}
11
+ className={classNames(
12
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ ));
18
+ Label.displayName = LabelPrimitive.Root.displayName;
 
 
 
19
 
20
  export { Label };
app/components/ui/Progress.tsx CHANGED
@@ -1,22 +1,22 @@
1
  import * as React from 'react';
2
- import * as ProgressPrimitive from '@radix-ui/react-progress';
3
- import { cn } from '~/lib/utils';
4
 
5
- const Progress = React.forwardRef<
6
- React.ElementRef<typeof ProgressPrimitive.Root>,
7
- React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
8
- >(({ className, value, ...props }, ref) => (
9
- <ProgressPrimitive.Root
 
10
  ref={ref}
11
- className={cn('relative h-2 w-full overflow-hidden rounded-full bg-bolt-elements-background', className)}
12
  {...props}
13
  >
14
- <ProgressPrimitive.Indicator
15
- className="h-full w-full flex-1 bg-purple-500 transition-all"
16
  style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
17
  />
18
- </ProgressPrimitive.Root>
19
  ));
20
- Progress.displayName = ProgressPrimitive.Root.displayName;
21
 
22
  export { Progress };
 
1
  import * as React from 'react';
2
+ import { classNames } from '~/utils/classNames';
 
3
 
4
+ interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ value?: number;
6
+ }
7
+
8
+ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(({ className, value, ...props }, ref) => (
9
+ <div
10
  ref={ref}
11
+ className={classNames('relative h-2 w-full overflow-hidden rounded-full bg-bolt-elements-background', className)}
12
  {...props}
13
  >
14
+ <div
15
+ className="h-full w-full flex-1 bg-bolt-elements-textPrimary transition-all"
16
  style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
17
  />
18
+ </div>
19
  ));
20
+ Progress.displayName = 'Progress';
21
 
22
  export { Progress };
app/components/ui/ScrollArea.tsx CHANGED
@@ -2,13 +2,13 @@
2
 
3
  import * as React from 'react';
4
  import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
5
- import { cn } from '~/lib/utils';
6
 
7
  const ScrollArea = React.forwardRef<
8
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
9
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
10
  >(({ className, children, ...props }, ref) => (
11
- <ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
12
  <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
13
  <ScrollBar />
14
  <ScrollAreaPrimitive.Corner />
@@ -23,15 +23,17 @@ const ScrollBar = React.forwardRef<
23
  <ScrollAreaPrimitive.ScrollAreaScrollbar
24
  ref={ref}
25
  orientation={orientation}
26
- className={cn(
27
  'flex touch-none select-none transition-colors',
28
- orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
29
- orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
 
 
30
  className,
31
  )}
32
  {...props}
33
  >
34
- <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
35
  </ScrollAreaPrimitive.ScrollAreaScrollbar>
36
  ));
37
  ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
 
2
 
3
  import * as React from 'react';
4
  import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
5
+ import { classNames } from '~/utils/classNames';
6
 
7
  const ScrollArea = React.forwardRef<
8
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
9
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
10
  >(({ className, children, ...props }, ref) => (
11
+ <ScrollAreaPrimitive.Root ref={ref} className={classNames('relative overflow-hidden', className)} {...props}>
12
  <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
13
  <ScrollBar />
14
  <ScrollAreaPrimitive.Corner />
 
23
  <ScrollAreaPrimitive.ScrollAreaScrollbar
24
  ref={ref}
25
  orientation={orientation}
26
+ className={classNames(
27
  'flex touch-none select-none transition-colors',
28
+ {
29
+ 'h-full w-2.5 border-l border-l-transparent p-[1px]': orientation === 'vertical',
30
+ 'h-2.5 flex-col border-t border-t-transparent p-[1px]': orientation === 'horizontal',
31
+ },
32
  className,
33
  )}
34
  {...props}
35
  >
36
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-bolt-elements-border" />
37
  </ScrollAreaPrimitive.ScrollAreaScrollbar>
38
  ));
39
  ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
app/components/ui/Tabs.tsx CHANGED
@@ -1,18 +1,17 @@
 
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}
@@ -20,17 +19,14 @@ const TabsList = forwardRef<
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}
@@ -38,14 +34,14 @@ const TabsTrigger = forwardRef<
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}
 
1
+ import * as React from 'react';
2
  import * as TabsPrimitive from '@radix-ui/react-tabs';
3
+ import { classNames } from '~/utils/classNames';
 
4
 
5
  const Tabs = TabsPrimitive.Root;
6
 
7
+ const TabsList = React.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={classNames(
14
+ 'inline-flex h-10 items-center justify-center rounded-md bg-bolt-elements-background p-1 text-bolt-elements-textSecondary',
 
15
  className,
16
  )}
17
  {...props}
 
19
  ));
20
  TabsList.displayName = TabsPrimitive.List.displayName;
21
 
22
+ const TabsTrigger = React.forwardRef<
23
  React.ElementRef<typeof TabsPrimitive.Trigger>,
24
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
25
  >(({ className, ...props }, ref) => (
26
  <TabsPrimitive.Trigger
27
  ref={ref}
28
+ className={classNames(
29
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-bolt-elements-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bolt-elements-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-bolt-elements-background data-[state=active]:text-bolt-elements-textPrimary data-[state=active]:shadow-sm',
 
 
 
30
  className,
31
  )}
32
  {...props}
 
34
  ));
35
  TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
36
 
37
+ const TabsContent = React.forwardRef<
38
  React.ElementRef<typeof TabsPrimitive.Content>,
39
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
40
  >(({ className, ...props }, ref) => (
41
  <TabsPrimitive.Content
42
  ref={ref}
43
+ className={classNames(
44
+ 'mt-2 ring-offset-bolt-elements-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bolt-elements-ring focus-visible:ring-offset-2',
45
  className,
46
  )}
47
  {...props}
app/lib/hooks/useSettings.ts CHANGED
@@ -18,7 +18,7 @@ import Cookies from 'js-cookie';
18
  import type { IProviderSetting, ProviderInfo, IProviderConfig } from '~/types/model';
19
  import type { TabWindowConfig, TabVisibilityConfig } from '~/components/settings/settings.types';
20
  import { logStore } from '~/lib/stores/logs';
21
- import { getLocalStorage, setLocalStorage } from '~/utils/localStorage';
22
 
23
  export interface Settings {
24
  theme: 'light' | 'dark' | 'system';
 
18
  import type { IProviderSetting, ProviderInfo, IProviderConfig } from '~/types/model';
19
  import type { TabWindowConfig, TabVisibilityConfig } from '~/components/settings/settings.types';
20
  import { logStore } from '~/lib/stores/logs';
21
+ import { getLocalStorage, setLocalStorage } from '~/lib/persistence';
22
 
23
  export interface Settings {
24
  theme: 'light' | 'dark' | 'system';
app/lib/modules/llm/providers/github.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProvider } from '~/lib/modules/llm/base-provider';
2
+ import type { ModelInfo } from '~/lib/modules/llm/types';
3
+ import type { IProviderSetting } from '~/types/model';
4
+ import type { LanguageModelV1 } from 'ai';
5
+ import { createOpenAI } from '@ai-sdk/openai';
6
+
7
+ export default class GithubProvider extends BaseProvider {
8
+ name = 'Github';
9
+ getApiKeyLink = 'https://github.com/settings/personal-access-tokens';
10
+
11
+ config = {
12
+ apiTokenKey: 'GITHUB_API_KEY',
13
+ };
14
+
15
+ // find more in https://github.com/marketplace?type=models
16
+ staticModels: ModelInfo[] = [
17
+ { name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
18
+ { name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
19
+ { name: 'o1-mini', label: 'o1-mini', provider: 'Github', maxTokenAllowed: 8000 },
20
+ { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'Github', maxTokenAllowed: 8000 },
21
+ { name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
22
+ { name: 'gpt-4', label: 'GPT-4', provider: 'Github', maxTokenAllowed: 8000 },
23
+ { name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
24
+ ];
25
+
26
+ getModelInstance(options: {
27
+ model: string;
28
+ serverEnv: Env;
29
+ apiKeys?: Record<string, string>;
30
+ providerSettings?: Record<string, IProviderSetting>;
31
+ }): LanguageModelV1 {
32
+ const { model, serverEnv, apiKeys, providerSettings } = options;
33
+
34
+ const { apiKey } = this.getProviderBaseUrlAndKey({
35
+ apiKeys,
36
+ providerSettings: providerSettings?.[this.name],
37
+ serverEnv: serverEnv as any,
38
+ defaultBaseUrlKey: '',
39
+ defaultApiTokenKey: 'GITHUB_API_KEY',
40
+ });
41
+
42
+ if (!apiKey) {
43
+ throw new Error(`Missing API key for ${this.name} provider`);
44
+ }
45
+
46
+ const openai = createOpenAI({
47
+ baseURL: 'https://models.inference.ai.azure.com',
48
+ apiKey,
49
+ });
50
+
51
+ return openai(model);
52
+ }
53
+ }
app/lib/modules/llm/registry.ts CHANGED
@@ -15,6 +15,7 @@ import TogetherProvider from './providers/together';
15
  import XAIProvider from './providers/xai';
16
  import HyperbolicProvider from './providers/hyperbolic';
17
  import AmazonBedrockProvider from './providers/amazon-bedrock';
 
18
 
19
  export {
20
  AnthropicProvider,
@@ -34,4 +35,5 @@ export {
34
  TogetherProvider,
35
  LMStudioProvider,
36
  AmazonBedrockProvider,
 
37
  };
 
15
  import XAIProvider from './providers/xai';
16
  import HyperbolicProvider from './providers/hyperbolic';
17
  import AmazonBedrockProvider from './providers/amazon-bedrock';
18
+ import GithubProvider from './providers/github';
19
 
20
  export {
21
  AnthropicProvider,
 
35
  TogetherProvider,
36
  LMStudioProvider,
37
  AmazonBedrockProvider,
38
+ GithubProvider,
39
  };
app/lib/persistence/index.ts CHANGED
@@ -1,2 +1,3 @@
 
1
  export * from './db';
2
  export * from './useChatHistory';
 
1
+ export * from './localStorage';
2
  export * from './db';
3
  export * from './useChatHistory';