samifalouti1 commited on
Commit
55d48a7
·
0 Parent(s):

Fresh start without binaries

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .depcheckrc.json +28 -0
  2. .dockerignore +26 -0
  3. .editorconfig +13 -0
  4. .env.example +221 -0
  5. .env.production +143 -0
  6. .github/CODEOWNERS +30 -0
  7. .github/ISSUE_TEMPLATE/bug_report.yml +73 -0
  8. .github/ISSUE_TEMPLATE/config.yml +8 -0
  9. .github/actions/setup-and-build/action.yaml +32 -0
  10. .github/scripts/generate-changelog.sh +261 -0
  11. .github/workflows/ci.yaml +90 -0
  12. .github/workflows/docker.yaml +67 -0
  13. .github/workflows/docs.yaml +35 -0
  14. .github/workflows/electron.yml +98 -0
  15. .github/workflows/pr-release-validation.yaml +125 -0
  16. .github/workflows/preview.yaml +196 -0
  17. .github/workflows/quality.yaml +181 -0
  18. .github/workflows/security.yaml +121 -0
  19. .github/workflows/semantic-pr.yaml +32 -0
  20. .github/workflows/stale.yml +25 -0
  21. .github/workflows/test-workflows.yaml +247 -0
  22. .github/workflows/update-stable.yml +127 -0
  23. .gitignore +48 -0
  24. .husky/pre-commit +32 -0
  25. .lighthouserc.json +20 -0
  26. .prettierignore +2 -0
  27. .prettierrc +8 -0
  28. Dockerfile +103 -0
  29. LICENSE +21 -0
  30. app/components/@settings/core/AvatarDropdown.tsx +175 -0
  31. app/components/@settings/core/ControlPanel.tsx +345 -0
  32. app/components/@settings/core/constants.tsx +108 -0
  33. app/components/@settings/core/types.ts +114 -0
  34. app/components/@settings/index.ts +12 -0
  35. app/components/@settings/shared/components/TabTile.tsx +151 -0
  36. app/components/@settings/shared/service-integration/ConnectionForm.tsx +193 -0
  37. app/components/@settings/shared/service-integration/ConnectionTestIndicator.tsx +60 -0
  38. app/components/@settings/shared/service-integration/ErrorState.tsx +102 -0
  39. app/components/@settings/shared/service-integration/LoadingState.tsx +94 -0
  40. app/components/@settings/shared/service-integration/ServiceHeader.tsx +72 -0
  41. app/components/@settings/shared/service-integration/index.ts +6 -0
  42. app/components/@settings/tabs/data/DataTab.tsx +721 -0
  43. app/components/@settings/tabs/data/DataVisualization.tsx +384 -0
  44. app/components/@settings/tabs/event-logs/EventLogsTab.tsx +1013 -0
  45. app/components/@settings/tabs/features/FeaturesTab.tsx +295 -0
  46. app/components/@settings/tabs/github/GitHubTab.tsx +281 -0
  47. app/components/@settings/tabs/github/components/GitHubAuthDialog.tsx +173 -0
  48. app/components/@settings/tabs/github/components/GitHubCacheManager.tsx +367 -0
  49. app/components/@settings/tabs/github/components/GitHubConnection.tsx +233 -0
  50. app/components/@settings/tabs/github/components/GitHubErrorBoundary.tsx +105 -0
.depcheckrc.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "ignoreMatches": [
3
+ "@types/*",
4
+ "eslint-*",
5
+ "prettier*",
6
+ "husky",
7
+ "rimraf",
8
+ "vitest",
9
+ "vite",
10
+ "typescript",
11
+ "wrangler",
12
+ "electron*"
13
+ ],
14
+ "ignoreDirs": [
15
+ "dist",
16
+ "build",
17
+ "node_modules",
18
+ ".git"
19
+ ],
20
+ "skipMissing": false,
21
+ "ignorePatterns": [
22
+ "*.d.ts",
23
+ "*.test.ts",
24
+ "*.test.tsx",
25
+ "*.spec.ts",
26
+ "*.spec.tsx"
27
+ ]
28
+ }
.dockerignore ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore Git and GitHub files
2
+ .git
3
+ .github/
4
+
5
+ # Ignore Husky configuration files
6
+ .husky/
7
+
8
+ # Ignore documentation and metadata files
9
+ CONTRIBUTING.md
10
+ LICENSE
11
+ README.md
12
+
13
+ # Ignore environment examples and sensitive info
14
+ .env
15
+ *.local
16
+ *.example
17
+
18
+ # Ignore node modules, logs and cache files
19
+ **/*.log
20
+ **/node_modules
21
+ **/dist
22
+ **/build
23
+ **/.cache
24
+ logs
25
+ dist-ssr
26
+ .DS_Store
.editorconfig ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ end_of_line = lf
6
+ charset = utf-8
7
+ trim_trailing_whitespace = true
8
+ insert_final_newline = true
9
+ max_line_length = 120
10
+ indent_size = 2
11
+
12
+ [*.md]
13
+ trim_trailing_whitespace = false
.env.example ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ======================================
2
+ # Environment Variables for Bolt.diy
3
+ # ======================================
4
+ # Copy this file to .env.local and fill in your API keys
5
+ # See README.md for setup instructions
6
+
7
+ # ======================================
8
+ # AI PROVIDER API KEYS
9
+ # ======================================
10
+
11
+ # Anthropic Claude
12
+ # Get your API key from: https://console.anthropic.com/
13
+ ANTHROPIC_API_KEY=your_anthropic_api_key_here
14
+
15
+ # Cerebras (High-performance inference)
16
+ # Get your API key from: https://cloud.cerebras.ai/settings
17
+ CEREBRAS_API_KEY=your_cerebras_api_key_here
18
+
19
+ # Fireworks AI (Fast inference with FireAttention engine)
20
+ # Get your API key from: https://fireworks.ai/api-keys
21
+ FIREWORKS_API_KEY=your_fireworks_api_key_here
22
+
23
+ # OpenAI GPT models
24
+ # Get your API key from: https://platform.openai.com/api-keys
25
+ OPENAI_API_KEY=your_openai_api_key_here
26
+
27
+ # GitHub Models (OpenAI models hosted by GitHub)
28
+ # Get your Personal Access Token from: https://github.com/settings/tokens
29
+ # - Select "Fine-grained tokens"
30
+ # - Set repository access to "All repositories"
31
+ # - Enable "GitHub Models" permission
32
+ GITHUB_API_KEY=github_pat_your_personal_access_token_here
33
+
34
+ # Perplexity AI (Search-augmented models)
35
+ # Get your API key from: https://www.perplexity.ai/settings/api
36
+ PERPLEXITY_API_KEY=your_perplexity_api_key_here
37
+
38
+ # DeepSeek
39
+ # Get your API key from: https://platform.deepseek.com/api_keys
40
+ DEEPSEEK_API_KEY=your_deepseek_api_key_here
41
+
42
+ # Google Gemini
43
+ # Get your API key from: https://makersuite.google.com/app/apikey
44
+ GOOGLE_GENERATIVE_AI_API_KEY=your_google_gemini_api_key_here
45
+
46
+ # Cohere
47
+ # Get your API key from: https://dashboard.cohere.ai/api-keys
48
+ COHERE_API_KEY=your_cohere_api_key_here
49
+
50
+ # Groq (Fast inference)
51
+ # Get your API key from: https://console.groq.com/keys
52
+ GROQ_API_KEY=your_groq_api_key_here
53
+
54
+ # Mistral
55
+ # Get your API key from: https://console.mistral.ai/api-keys/
56
+ MISTRAL_API_KEY=your_mistral_api_key_here
57
+
58
+ # Together AI
59
+ # Get your API key from: https://api.together.xyz/settings/api-keys
60
+ TOGETHER_API_KEY=your_together_api_key_here
61
+
62
+ # X.AI (Elon Musk's company)
63
+ # Get your API key from: https://console.x.ai/
64
+ XAI_API_KEY=your_xai_api_key_here
65
+
66
+ # Moonshot AI (Kimi models)
67
+ # Get your API key from: https://platform.moonshot.ai/console/api-keys
68
+ MOONSHOT_API_KEY=your_moonshot_api_key_here
69
+
70
+ # Z.AI (GLM models with JWT authentication)
71
+ # Get your API key from: https://open.bigmodel.cn/usercenter/apikeys
72
+ ZAI_API_KEY=your_zai_api_key_here
73
+
74
+ # Hugging Face
75
+ # Get your API key from: https://huggingface.co/settings/tokens
76
+ HuggingFace_API_KEY=your_huggingface_api_key_here
77
+
78
+ # Hyperbolic
79
+ # Get your API key from: https://app.hyperbolic.xyz/settings
80
+ HYPERBOLIC_API_KEY=your_hyperbolic_api_key_here
81
+
82
+ # OpenRouter (Meta routing for multiple providers)
83
+ # Get your API key from: https://openrouter.ai/keys
84
+ OPEN_ROUTER_API_KEY=your_openrouter_api_key_here
85
+
86
+ # ======================================
87
+ # CUSTOM PROVIDER BASE URLS (Optional)
88
+ # ======================================
89
+
90
+ # Ollama (Local models)
91
+ # DON'T USE http://localhost:11434 due to IPv6 issues
92
+ # USE: http://127.0.0.1:11434
93
+ OLLAMA_API_BASE_URL=http://127.0.0.1:11434
94
+
95
+ # OpenAI-like API (Compatible providers)
96
+ OPENAI_LIKE_API_BASE_URL=your_openai_like_base_url_here
97
+ OPENAI_LIKE_API_KEY=your_openai_like_api_key_here
98
+
99
+ # Together AI Base URL
100
+ TOGETHER_API_BASE_URL=your_together_base_url_here
101
+
102
+ # Hyperbolic Base URL
103
+ HYPERBOLIC_API_BASE_URL=https://api.hyperbolic.xyz/v1/chat/completions
104
+
105
+ # LMStudio (Local models)
106
+ # Make sure to enable CORS in LMStudio
107
+ # DON'T USE http://localhost:1234 due to IPv6 issues
108
+ # USE: http://127.0.0.1:1234
109
+ LMSTUDIO_API_BASE_URL=http://127.0.0.1:1234
110
+
111
+ # ======================================
112
+ # CLOUD SERVICES CONFIGURATION
113
+ # ======================================
114
+
115
+ # AWS Bedrock Configuration (JSON format)
116
+ # Get your credentials from: https://console.aws.amazon.com/iam/home
117
+ # Example: {"region": "us-east-1", "accessKeyId": "yourAccessKeyId", "secretAccessKey": "yourSecretAccessKey"}
118
+ AWS_BEDROCK_CONFIG=your_aws_bedrock_config_json_here
119
+
120
+ # ======================================
121
+ # GITHUB INTEGRATION
122
+ # ======================================
123
+
124
+ # GitHub Personal Access Token
125
+ # Get from: https://github.com/settings/tokens
126
+ # Used for importing/cloning repositories and accessing private repos
127
+ VITE_GITHUB_ACCESS_TOKEN=your_github_personal_access_token_here
128
+
129
+ # GitHub Token Type ('classic' or 'fine-grained')
130
+ VITE_GITHUB_TOKEN_TYPE=classic
131
+
132
+ # ======================================
133
+ # GITLAB INTEGRATION
134
+ # ======================================
135
+
136
+ # GitLab Personal Access Token
137
+ # Get your GitLab Personal Access Token here:
138
+ # https://gitlab.com/-/profile/personal_access_tokens
139
+ #
140
+ # This token is used for:
141
+ # 1. Importing/cloning GitLab repositories
142
+ # 2. Accessing private projects
143
+ # 3. Creating/updating branches
144
+ # 4. Creating/updating commits and pushing code
145
+ # 5. Creating new GitLab projects via the API
146
+ #
147
+ # Make sure your token has the following scopes:
148
+ # - api (for full API access including project creation and commits)
149
+ # - read_repository (to clone/import repositories)
150
+ # - write_repository (to push commits and update branches)
151
+ VITE_GITLAB_ACCESS_TOKEN=your_gitlab_personal_access_token_here
152
+
153
+ # Set the GitLab instance URL (e.g., https://gitlab.com or your self-hosted domain)
154
+ VITE_GITLAB_URL=https://gitlab.com
155
+
156
+ # GitLab token type should be 'personal-access-token'
157
+ VITE_GITLAB_TOKEN_TYPE=personal-access-token
158
+
159
+ # ======================================
160
+ # VERCEL INTEGRATION
161
+ # ======================================
162
+
163
+ # Vercel Access Token
164
+ # Get your access token from: https://vercel.com/account/tokens
165
+ # This token is used for:
166
+ # 1. Deploying projects to Vercel
167
+ # 2. Managing Vercel projects and deployments
168
+ # 3. Accessing project analytics and logs
169
+ VITE_VERCEL_ACCESS_TOKEN=your_vercel_access_token_here
170
+
171
+ # ======================================
172
+ # NETLIFY INTEGRATION
173
+ # ======================================
174
+
175
+ # Netlify Access Token
176
+ # Get your access token from: https://app.netlify.com/user/applications
177
+ # This token is used for:
178
+ # 1. Deploying sites to Netlify
179
+ # 2. Managing Netlify sites and deployments
180
+ # 3. Accessing build logs and analytics
181
+ VITE_NETLIFY_ACCESS_TOKEN=your_netlify_access_token_here
182
+
183
+ # ======================================
184
+ # SUPABASE INTEGRATION
185
+ # ======================================
186
+
187
+ # Supabase Project Configuration
188
+ # Get your project details from: https://supabase.com/dashboard
189
+ # Select your project → Settings → API
190
+ VITE_SUPABASE_URL=your_supabase_project_url_here
191
+ VITE_SUPABASE_ANON_KEY=your_supabase_anon_key_here
192
+
193
+ # Supabase Access Token (for management operations)
194
+ # Generate from: https://supabase.com/dashboard/account/tokens
195
+ VITE_SUPABASE_ACCESS_TOKEN=your_supabase_access_token_here
196
+
197
+ # ======================================
198
+ # DEVELOPMENT SETTINGS
199
+ # ======================================
200
+
201
+ # Development Mode
202
+ NODE_ENV=development
203
+
204
+ # Application Port (optional, defaults to 5173 for development)
205
+ PORT=5173
206
+
207
+ # Logging Level (debug, info, warn, error)
208
+ VITE_LOG_LEVEL=debug
209
+
210
+ # Default Context Window Size (for local models)
211
+ DEFAULT_NUM_CTX=32768
212
+
213
+ # ======================================
214
+ # SETUP INSTRUCTIONS
215
+ # ======================================
216
+ # 1. Copy this file to .env.local: cp .env.example .env.local
217
+ # 2. Fill in the API keys for the services you want to use
218
+ # 3. All service integration keys use VITE_ prefix for auto-connection
219
+ # 4. Restart your development server: pnpm run dev
220
+ # 5. Services will auto-connect on startup if tokens are provided
221
+ # 6. Go to Settings > Service tabs to manage connections manually if needed
.env.production ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rename this file to .env once you have filled in the below environment variables!
2
+
3
+ # Get your GROQ API Key here -
4
+ # https://console.groq.com/keys
5
+ # You only need this environment variable set if you want to use Groq models
6
+ GROQ_API_KEY=
7
+ SOOL_API_KEY=sk_live_d9b93ba6c3d1e146f7f31b8f1c802df959755ec4b5ea6d4e
8
+
9
+ # Get your HuggingFace API Key here -
10
+ # https://huggingface.co/settings/tokens
11
+ # You only need this environment variable set if you want to use HuggingFace models
12
+ HuggingFace_API_KEY=
13
+
14
+ # Get your Open AI API Key by following these instructions -
15
+ # https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
16
+ # You only need this environment variable set if you want to use GPT models
17
+ OPENAI_API_KEY=
18
+
19
+ # Get your Anthropic API Key in your account settings -
20
+ # https://console.anthropic.com/settings/keys
21
+ # You only need this environment variable set if you want to use Claude models
22
+ ANTHROPIC_API_KEY=
23
+
24
+ # Get your OpenRouter API Key in your account settings -
25
+ # https://openrouter.ai/settings/keys
26
+ # You only need this environment variable set if you want to use OpenRouter models
27
+ OPEN_ROUTER_API_KEY=
28
+
29
+ # Get your Google Generative AI API Key by following these instructions -
30
+ # https://console.cloud.google.com/apis/credentials
31
+ # You only need this environment variable set if you want to use Google Generative AI models
32
+ GOOGLE_GENERATIVE_AI_API_KEY=
33
+
34
+ # You only need this environment variable set if you want to use oLLAMA models
35
+ # DONT USE http://localhost:11434 due to IPV6 issues
36
+ # USE EXAMPLE http://127.0.0.1:11434
37
+ OLLAMA_API_BASE_URL=
38
+
39
+ # You only need this environment variable set if you want to use OpenAI Like models
40
+ OPENAI_LIKE_API_BASE_URL=
41
+
42
+ # You only need this environment variable set if you want to use Together AI models
43
+ TOGETHER_API_BASE_URL=
44
+
45
+ # You only need this environment variable set if you want to use DeepSeek models through their API
46
+ DEEPSEEK_API_KEY=
47
+
48
+ # Get your OpenAI Like API Key
49
+ OPENAI_LIKE_API_KEY=
50
+
51
+ # Get your Together API Key
52
+ TOGETHER_API_KEY=
53
+
54
+ # You only need this environment variable set if you want to use Hyperbolic models
55
+ HYPERBOLIC_API_KEY=
56
+ HYPERBOLIC_API_BASE_URL=
57
+
58
+ # Get your Mistral API Key by following these instructions -
59
+ # https://console.mistral.ai/api-keys/
60
+ # You only need this environment variable set if you want to use Mistral models
61
+ MISTRAL_API_KEY=
62
+
63
+ # Get the Cohere Api key by following these instructions -
64
+ # https://dashboard.cohere.com/api-keys
65
+ # You only need this environment variable set if you want to use Cohere models
66
+ COHERE_API_KEY=
67
+
68
+ # Get LMStudio Base URL from LM Studio Developer Console
69
+ # Make sure to enable CORS
70
+ # DONT USE http://localhost:1234 due to IPV6 issues
71
+ # Example: http://127.0.0.1:1234
72
+ LMSTUDIO_API_BASE_URL=
73
+
74
+ # Get your xAI API key
75
+ # https://x.ai/api
76
+ # You only need this environment variable set if you want to use xAI models
77
+ XAI_API_KEY=
78
+
79
+ # Get your Perplexity API Key here -
80
+ # https://www.perplexity.ai/settings/api
81
+ # You only need this environment variable set if you want to use Perplexity models
82
+ PERPLEXITY_API_KEY=
83
+
84
+ # Get your AWS configuration
85
+ # https://console.aws.amazon.com/iam/home
86
+ AWS_BEDROCK_CONFIG=
87
+
88
+ # Include this environment variable if you want more logging for debugging locally
89
+ VITE_LOG_LEVEL=
90
+
91
+ # Get your GitHub Personal Access Token here -
92
+ # https://github.com/settings/tokens
93
+ # This token is used for:
94
+ # 1. Importing/cloning GitHub repositories without rate limiting
95
+ # 2. Accessing private repositories
96
+ # 3. Automatic GitHub authentication (no need to manually connect in the UI)
97
+ #
98
+ # For classic tokens, ensure it has these scopes: repo, read:org, read:user
99
+ # For fine-grained tokens, ensure it has Repository and Organization access
100
+ VITE_GITHUB_ACCESS_TOKEN=
101
+
102
+ # Specify the type of GitHub token you're using
103
+ # Can be 'classic' or 'fine-grained'
104
+ # Classic tokens are recommended for broader access
105
+ VITE_GITHUB_TOKEN_TYPE=
106
+
107
+ # ======================================
108
+ # SERVICE INTEGRATIONS
109
+ # ======================================
110
+
111
+ # GitLab Personal Access Token
112
+ # Get your GitLab Personal Access Token here:
113
+ # https://gitlab.com/-/profile/personal_access_tokens
114
+ # Required scopes: api, read_repository, write_repository
115
+ VITE_GITLAB_ACCESS_TOKEN=
116
+
117
+ # GitLab instance URL (e.g., https://gitlab.com or your self-hosted domain)
118
+ VITE_GITLAB_URL=https://gitlab.com
119
+
120
+ # GitLab token type
121
+ VITE_GITLAB_TOKEN_TYPE=personal-access-token
122
+
123
+ # Vercel Access Token
124
+ # Get your access token from: https://vercel.com/account/tokens
125
+ VITE_VERCEL_ACCESS_TOKEN=
126
+
127
+ # Netlify Access Token
128
+ # Get your access token from: https://app.netlify.com/user/applications
129
+ VITE_NETLIFY_ACCESS_TOKEN=
130
+
131
+ # Supabase Configuration
132
+ # Get your project details from: https://supabase.com/dashboard
133
+ VITE_SUPABASE_URL=
134
+ VITE_SUPABASE_ANON_KEY=
135
+ VITE_SUPABASE_ACCESS_TOKEN=
136
+
137
+ # Example Context Values for qwen2.5-coder:32b
138
+ #
139
+ # DEFAULT_NUM_CTX=32768 # Consumes 36GB of VRAM
140
+ # DEFAULT_NUM_CTX=24576 # Consumes 32GB of VRAM
141
+ # DEFAULT_NUM_CTX=12288 # Consumes 26GB of VRAM
142
+ # DEFAULT_NUM_CTX=6144 # Consumes 24GB of VRAM
143
+ DEFAULT_NUM_CTX=
.github/CODEOWNERS ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code Owners for bolt.diy
2
+ # These users/teams will automatically be requested for review when files are modified
3
+
4
+ # Global ownership - repository maintainers
5
+ * @stackblitz-labs/bolt-maintainers
6
+
7
+ # GitHub workflows and CI/CD configuration - require maintainer review
8
+ /.github/ @stackblitz-labs/bolt-maintainers
9
+ /package.json @stackblitz-labs/bolt-maintainers
10
+ /pnpm-lock.yaml @stackblitz-labs/bolt-maintainers
11
+
12
+ # Security-sensitive configurations - require maintainer review
13
+ /.env* @stackblitz-labs/bolt-maintainers
14
+ /wrangler.toml @stackblitz-labs/bolt-maintainers
15
+ /Dockerfile @stackblitz-labs/bolt-maintainers
16
+ /docker-compose.yaml @stackblitz-labs/bolt-maintainers
17
+
18
+ # Core application architecture - require maintainer review
19
+ /app/lib/.server/ @stackblitz-labs/bolt-maintainers
20
+ /app/routes/api.* @stackblitz-labs/bolt-maintainers
21
+
22
+ # Build and deployment configuration - require maintainer review
23
+ /vite*.config.ts @stackblitz-labs/bolt-maintainers
24
+ /tsconfig.json @stackblitz-labs/bolt-maintainers
25
+ /uno.config.ts @stackblitz-labs/bolt-maintainers
26
+ /eslint.config.mjs @stackblitz-labs/bolt-maintainers
27
+
28
+ # Documentation (optional review)
29
+ /*.md
30
+ /docs/
.github/ISSUE_TEMPLATE/bug_report.yml ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 'Bug report'
2
+ description: Create a report to help us improve
3
+ body:
4
+ - type: markdown
5
+ attributes:
6
+ value: |
7
+ Thank you for reporting an issue :pray:.
8
+
9
+ This issue tracker is for bugs and issues found with [Bolt.diy](https://bolt.diy).
10
+ If you experience issues related to WebContainer, please file an issue in the official [StackBlitz WebContainer repo](https://github.com/stackblitz/webcontainer-core).
11
+
12
+ The more information you fill in, the better we can help you.
13
+ - type: textarea
14
+ id: description
15
+ attributes:
16
+ label: Describe the bug
17
+ description: Provide a clear and concise description of what you're running into.
18
+ validations:
19
+ required: true
20
+ - type: input
21
+ id: link
22
+ attributes:
23
+ label: Link to the Bolt URL that caused the error
24
+ description: Please do not delete it after reporting!
25
+ validations:
26
+ required: true
27
+ - type: textarea
28
+ id: steps
29
+ attributes:
30
+ label: Steps to reproduce
31
+ description: Describe the steps we have to take to reproduce the behavior.
32
+ placeholder: |
33
+ 1. Go to '...'
34
+ 2. Click on '....'
35
+ 3. Scroll down to '....'
36
+ 4. See error
37
+ validations:
38
+ required: true
39
+ - type: textarea
40
+ id: expected
41
+ attributes:
42
+ label: Expected behavior
43
+ description: Provide a clear and concise description of what you expected to happen.
44
+ validations:
45
+ required: true
46
+ - type: textarea
47
+ id: screenshots
48
+ attributes:
49
+ label: Screen Recording / Screenshot
50
+ description: If applicable, **please include a screen recording** (preferably) or screenshot showcasing the issue. This will assist us in resolving your issue <u>quickly</u>.
51
+ - type: textarea
52
+ id: platform
53
+ attributes:
54
+ label: Platform
55
+ value: |
56
+ - OS: [e.g. macOS, Windows, Linux]
57
+ - Browser: [e.g. Chrome, Safari, Firefox]
58
+ - Version: [e.g. 91.1]
59
+ - type: input
60
+ id: provider
61
+ attributes:
62
+ label: Provider Used
63
+ description: Tell us the provider you are using.
64
+ - type: input
65
+ id: model
66
+ attributes:
67
+ label: Model Used
68
+ description: Tell us the model you are using.
69
+ - type: textarea
70
+ id: additional
71
+ attributes:
72
+ label: Additional context
73
+ description: Add any other context about the problem here.
.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Bolt.new related issues
4
+ url: https://github.com/stackblitz/bolt.new/issues/new/choose
5
+ about: Report issues related to Bolt.new (not Bolt.diy)
6
+ - name: Chat
7
+ url: https://thinktank.ottomator.ai
8
+ about: Ask questions and discuss with other Bolt.diy users.
.github/actions/setup-and-build/action.yaml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Setup and Build
2
+ description: Generic setup action
3
+ inputs:
4
+ pnpm-version:
5
+ required: false
6
+ type: string
7
+ default: '9.14.4'
8
+ node-version:
9
+ required: false
10
+ type: string
11
+ default: '20.18.0'
12
+
13
+ runs:
14
+ using: composite
15
+
16
+ steps:
17
+ - uses: pnpm/action-setup@v4
18
+ with:
19
+ version: ${{ inputs.pnpm-version }}
20
+ run_install: false
21
+
22
+ - name: Set Node.js version to ${{ inputs.node-version }}
23
+ uses: actions/setup-node@v4
24
+ with:
25
+ node-version: ${{ inputs.node-version }}
26
+ cache: pnpm
27
+
28
+ - name: Install dependencies and build project
29
+ shell: bash
30
+ run: |
31
+ pnpm install
32
+ pnpm run build
.github/scripts/generate-changelog.sh ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ # Ensure we're running in bash
4
+ if [ -z "$BASH_VERSION" ]; then
5
+ echo "This script requires bash. Please run with: bash $0" >&2
6
+ exit 1
7
+ fi
8
+
9
+ # Ensure we're using bash 4.0 or later for associative arrays
10
+ if ((BASH_VERSINFO[0] < 4)); then
11
+ echo "This script requires bash version 4 or later" >&2
12
+ echo "Current bash version: $BASH_VERSION" >&2
13
+ exit 1
14
+ fi
15
+
16
+ # Set default values for required environment variables if not in GitHub Actions
17
+ if [ -z "$GITHUB_ACTIONS" ]; then
18
+ : "${GITHUB_SERVER_URL:=https://github.com}"
19
+ : "${GITHUB_REPOSITORY:=stackblitz-labs/bolt.diy}"
20
+ : "${GITHUB_OUTPUT:=/tmp/github_output}"
21
+ touch "$GITHUB_OUTPUT"
22
+
23
+ # Running locally
24
+ echo "Running locally - checking for upstream remote..."
25
+ MAIN_REMOTE="origin"
26
+ if git remote -v | grep -q "upstream"; then
27
+ MAIN_REMOTE="upstream"
28
+ fi
29
+ MAIN_BRANCH="main" # or "master" depending on your repository
30
+
31
+ # Ensure we have latest tags
32
+ git fetch ${MAIN_REMOTE} --tags
33
+
34
+ # Use the remote reference for git log
35
+ GITLOG_REF="${MAIN_REMOTE}/${MAIN_BRANCH}"
36
+ else
37
+ # Running in GitHub Actions
38
+ GITLOG_REF="HEAD"
39
+ fi
40
+
41
+ # Get the latest tag
42
+ LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
43
+
44
+ # Start changelog file
45
+ echo "# 🚀 Release v${NEW_VERSION}" > changelog.md
46
+ echo "" >> changelog.md
47
+ echo "## What's Changed 🌟" >> changelog.md
48
+ echo "" >> changelog.md
49
+
50
+ if [ -z "$LATEST_TAG" ]; then
51
+ echo "### 🎉 First Release" >> changelog.md
52
+ echo "" >> changelog.md
53
+ echo "Exciting times! This marks our first release. Thanks to everyone who contributed! 🙌" >> changelog.md
54
+ echo "" >> changelog.md
55
+ COMPARE_BASE="$(git rev-list --max-parents=0 HEAD)"
56
+ else
57
+ echo "### 🔄 Changes since $LATEST_TAG" >> changelog.md
58
+ echo "" >> changelog.md
59
+ COMPARE_BASE="$LATEST_TAG"
60
+ fi
61
+
62
+ # Function to extract conventional commit type and associated emoji
63
+ get_commit_type() {
64
+ local msg="$1"
65
+ if [[ $msg =~ ^feat(\(.+\))?:|^feature(\(.+\))?: ]]; then echo "✨ Features"
66
+ elif [[ $msg =~ ^fix(\(.+\))?: ]]; then echo "🐛 Bug Fixes"
67
+ elif [[ $msg =~ ^docs(\(.+\))?: ]]; then echo "📚 Documentation"
68
+ elif [[ $msg =~ ^style(\(.+\))?: ]]; then echo "💎 Styles"
69
+ elif [[ $msg =~ ^refactor(\(.+\))?: ]]; then echo "♻️ Code Refactoring"
70
+ elif [[ $msg =~ ^perf(\(.+\))?: ]]; then echo "⚡ Performance Improvements"
71
+ elif [[ $msg =~ ^test(\(.+\))?: ]]; then echo "🧪 Tests"
72
+ elif [[ $msg =~ ^build(\(.+\))?: ]]; then echo "🛠️ Build System"
73
+ elif [[ $msg =~ ^ci(\(.+\))?: ]]; then echo "⚙️ CI"
74
+ elif [[ $msg =~ ^chore(\(.+\))?: ]]; then echo "" # Skip chore commits
75
+ else echo "🔍 Other Changes" # Default category with emoji
76
+ fi
77
+ }
78
+
79
+ # Initialize associative arrays
80
+ declare -A CATEGORIES
81
+ declare -A COMMITS_BY_CATEGORY
82
+ declare -A ALL_AUTHORS
83
+ declare -A NEW_CONTRIBUTORS
84
+
85
+ # Get all historical authors before the compare base
86
+ while IFS= read -r author; do
87
+ ALL_AUTHORS["$author"]=1
88
+ done < <(git log "${COMPARE_BASE}" --pretty=format:"%ae" | sort -u)
89
+
90
+ # Process all commits since last tag
91
+ while IFS= read -r commit_line; do
92
+ if [[ ! $commit_line =~ ^[a-f0-9]+\| ]]; then
93
+ echo "WARNING: Skipping invalid commit line format: $commit_line" >&2
94
+ continue
95
+ fi
96
+
97
+ HASH=$(echo "$commit_line" | cut -d'|' -f1)
98
+ COMMIT_MSG=$(echo "$commit_line" | cut -d'|' -f2)
99
+ BODY=$(echo "$commit_line" | cut -d'|' -f3)
100
+ # Skip if hash doesn't match the expected format
101
+ if [[ ! $HASH =~ ^[a-f0-9]{40}$ ]]; then
102
+ continue
103
+ fi
104
+
105
+ HASH=$(echo "$commit_line" | cut -d'|' -f1)
106
+ COMMIT_MSG=$(echo "$commit_line" | cut -d'|' -f2)
107
+ BODY=$(echo "$commit_line" | cut -d'|' -f3)
108
+
109
+
110
+ # Validate hash format
111
+ if [[ ! $HASH =~ ^[a-f0-9]{40}$ ]]; then
112
+ echo "WARNING: Invalid commit hash format: $HASH" >&2
113
+ continue
114
+ fi
115
+
116
+ # Check if it's a merge commit
117
+ if [[ $COMMIT_MSG =~ Merge\ pull\ request\ #([0-9]+) ]]; then
118
+ # echo "Processing as merge commit" >&2
119
+ PR_NUM="${BASH_REMATCH[1]}"
120
+
121
+ # Extract the PR title from the merge commit body
122
+ PR_TITLE=$(echo "$BODY" | grep -v "^Merge pull request" | head -n 1)
123
+
124
+ # Only process if it follows conventional commit format
125
+ CATEGORY=$(get_commit_type "$PR_TITLE")
126
+
127
+ if [ -n "$CATEGORY" ]; then # Only process if it's a conventional commit
128
+ # Get PR author's GitHub username
129
+ GITHUB_USERNAME=$(gh pr view "$PR_NUM" --json author --jq '.author.login')
130
+
131
+ if [ -n "$GITHUB_USERNAME" ]; then
132
+ # Check if this is a first-time contributor
133
+ AUTHOR_EMAIL=$(git show -s --format='%ae' "$HASH")
134
+ if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then
135
+ NEW_CONTRIBUTORS["$GITHUB_USERNAME"]=1
136
+ ALL_AUTHORS["$AUTHOR_EMAIL"]=1
137
+ fi
138
+
139
+ CATEGORIES["$CATEGORY"]=1
140
+ COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by @$GITHUB_USERNAME"$'\n'
141
+ else
142
+ COMMITS_BY_CATEGORY["$CATEGORY"]+="* ${PR_TITLE#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n'
143
+ fi
144
+ fi
145
+ # Check if it's a squash merge by looking for (#NUMBER) pattern
146
+ elif [[ $COMMIT_MSG =~ \(#([0-9]+)\) ]]; then
147
+ # echo "Processing as squash commit" >&2
148
+ PR_NUM="${BASH_REMATCH[1]}"
149
+
150
+ # Only process if it follows conventional commit format
151
+ CATEGORY=$(get_commit_type "$COMMIT_MSG")
152
+
153
+ if [ -n "$CATEGORY" ]; then # Only process if it's a conventional commit
154
+ # Get PR author's GitHub username
155
+ GITHUB_USERNAME=$(gh pr view "$PR_NUM" --json author --jq '.author.login')
156
+
157
+ if [ -n "$GITHUB_USERNAME" ]; then
158
+ # Check if this is a first-time contributor
159
+ AUTHOR_EMAIL=$(git show -s --format='%ae' "$HASH")
160
+ if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then
161
+ NEW_CONTRIBUTORS["$GITHUB_USERNAME"]=1
162
+ ALL_AUTHORS["$AUTHOR_EMAIL"]=1
163
+ fi
164
+
165
+ CATEGORIES["$CATEGORY"]=1
166
+ COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix
167
+ COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
168
+ COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM)) by @$GITHUB_USERNAME"$'\n'
169
+ else
170
+ COMMIT_TITLE=${COMMIT_MSG%% (#*} # Remove the PR number suffix
171
+ COMMIT_TITLE=${COMMIT_TITLE#*: } # Remove the type prefix
172
+ COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n'
173
+ fi
174
+ fi
175
+
176
+ else
177
+ # echo "Processing as regular commit" >&2
178
+ # Process conventional commits without PR numbers
179
+ CATEGORY=$(get_commit_type "$COMMIT_MSG")
180
+
181
+ if [ -n "$CATEGORY" ]; then # Only process if it's a conventional commit
182
+ # Get commit author info
183
+ AUTHOR_EMAIL=$(git show -s --format='%ae' "$HASH")
184
+
185
+ # Try to get GitHub username using gh api
186
+ if [ -n "$GITHUB_ACTIONS" ] || command -v gh >/dev/null 2>&1; then
187
+ GITHUB_USERNAME=$(gh api "/repos/${GITHUB_REPOSITORY}/commits/${HASH}" --jq '.author.login' 2>/dev/null)
188
+ fi
189
+
190
+ if [ -n "$GITHUB_USERNAME" ]; then
191
+ # If we got GitHub username, use it
192
+ if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then
193
+ NEW_CONTRIBUTORS["$GITHUB_USERNAME"]=1
194
+ ALL_AUTHORS["$AUTHOR_EMAIL"]=1
195
+ fi
196
+
197
+ CATEGORIES["$CATEGORY"]=1
198
+ COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix
199
+ COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE (${HASH:0:7}) by @$GITHUB_USERNAME"$'\n'
200
+ else
201
+ # Fallback to git author name if no GitHub username found
202
+ AUTHOR_NAME=$(git show -s --format='%an' "$HASH")
203
+
204
+ if [ -z "${ALL_AUTHORS[$AUTHOR_EMAIL]}" ]; then
205
+ NEW_CONTRIBUTORS["$AUTHOR_NAME"]=1
206
+ ALL_AUTHORS["$AUTHOR_EMAIL"]=1
207
+ fi
208
+
209
+ CATEGORIES["$CATEGORY"]=1
210
+ COMMIT_TITLE=${COMMIT_MSG#*: } # Remove the type prefix
211
+ COMMITS_BY_CATEGORY["$CATEGORY"]+="* $COMMIT_TITLE (${HASH:0:7}) by $AUTHOR_NAME"$'\n'
212
+ fi
213
+ fi
214
+ fi
215
+
216
+ done < <(git log "${COMPARE_BASE}..${GITLOG_REF}" --pretty=format:"%H|%s|%b" --reverse --first-parent)
217
+
218
+ # Write categorized commits to changelog with their emojis
219
+ for category in "✨ Features" "🐛 Bug Fixes" "📚 Documentation" "💎 Styles" "♻️ Code Refactoring" "⚡ Performance Improvements" "🧪 Tests" "🛠️ Build System" "⚙️ CI" "🔍 Other Changes"; do
220
+ if [ -n "${COMMITS_BY_CATEGORY[$category]}" ]; then
221
+ echo "### $category" >> changelog.md
222
+ echo "" >> changelog.md
223
+ echo "${COMMITS_BY_CATEGORY[$category]}" >> changelog.md
224
+ echo "" >> changelog.md
225
+ fi
226
+ done
227
+
228
+ # Add first-time contributors section if there are any
229
+ if [ ${#NEW_CONTRIBUTORS[@]} -gt 0 ]; then
230
+ echo "## ✨ First-time Contributors" >> changelog.md
231
+ echo "" >> changelog.md
232
+ echo "A huge thank you to our amazing new contributors! Your first contribution marks the start of an exciting journey! 🌟" >> changelog.md
233
+ echo "" >> changelog.md
234
+ # Use readarray to sort the keys
235
+ readarray -t sorted_contributors < <(printf '%s\n' "${!NEW_CONTRIBUTORS[@]}" | sort)
236
+ for github_username in "${sorted_contributors[@]}"; do
237
+ echo "* 🌟 [@$github_username](https://github.com/$github_username)" >> changelog.md
238
+ done
239
+ echo "" >> changelog.md
240
+ fi
241
+
242
+ # Add compare link if not first release
243
+ if [ -n "$LATEST_TAG" ]; then
244
+ echo "## 📈 Stats" >> changelog.md
245
+ echo "" >> changelog.md
246
+ echo "**Full Changelog**: [\`$LATEST_TAG..v${NEW_VERSION}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/$LATEST_TAG...v${NEW_VERSION})" >> changelog.md
247
+ fi
248
+
249
+ # Output the changelog content
250
+ CHANGELOG_CONTENT=$(cat changelog.md)
251
+ {
252
+ echo "content<<EOF"
253
+ echo "$CHANGELOG_CONTENT"
254
+ echo "EOF"
255
+ } >> "$GITHUB_OUTPUT"
256
+
257
+ # Also print to stdout for local testing
258
+ echo "Generated changelog:"
259
+ echo "==================="
260
+ cat changelog.md
261
+ echo "==================="
.github/workflows/ci.yaml ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI/CD
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ # Cancel in-progress runs on the same branch/PR
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ test:
16
+ name: Test
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 30
19
+
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Setup and Build
25
+ uses: ./.github/actions/setup-and-build
26
+
27
+ - name: Cache TypeScript compilation
28
+ uses: actions/cache@v4
29
+ with:
30
+ path: |
31
+ .tsbuildinfo
32
+ node_modules/.cache
33
+ key: ${{ runner.os }}-typescript-${{ hashFiles('**/tsconfig.json', 'app/**/*.ts', 'app/**/*.tsx') }}
34
+ restore-keys: |
35
+ ${{ runner.os }}-typescript-
36
+
37
+ - name: Run type check
38
+ run: pnpm run typecheck
39
+
40
+ - name: Cache ESLint
41
+ uses: actions/cache@v4
42
+ with:
43
+ path: node_modules/.cache/eslint
44
+ key: ${{ runner.os }}-eslint-${{ hashFiles('.eslintrc*', 'app/**/*.ts', 'app/**/*.tsx') }}
45
+ restore-keys: |
46
+ ${{ runner.os }}-eslint-
47
+
48
+ - name: Run ESLint
49
+ run: pnpm run lint
50
+
51
+ - name: Run tests
52
+ run: pnpm run test
53
+
54
+ - name: Upload test coverage
55
+ uses: actions/upload-artifact@v4
56
+ if: always()
57
+ with:
58
+ name: coverage-report
59
+ path: coverage/
60
+ retention-days: 7
61
+
62
+ docker-validation:
63
+ name: Docker Build Validation
64
+ runs-on: ubuntu-latest
65
+ timeout-minutes: 15
66
+
67
+ steps:
68
+ - name: Checkout
69
+ uses: actions/checkout@v4
70
+
71
+ - name: Set up Docker Buildx
72
+ uses: docker/setup-buildx-action@v3
73
+
74
+ - name: Validate Docker production build
75
+ run: |
76
+ echo "🐳 Testing Docker production target..."
77
+ docker build --target bolt-ai-production . --no-cache --progress=plain
78
+ echo "✅ Production target builds successfully"
79
+
80
+ - name: Validate Docker development build
81
+ run: |
82
+ echo "🐳 Testing Docker development target..."
83
+ docker build --target development . --no-cache --progress=plain
84
+ echo "✅ Development target builds successfully"
85
+
86
+ - name: Validate docker-compose configuration
87
+ run: |
88
+ echo "🐳 Validating docker-compose configuration..."
89
+ docker compose config --quiet
90
+ echo "✅ docker-compose configuration is valid"
.github/workflows/docker.yaml ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [main, stable]
6
+ tags: ['v*', '*.*.*']
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ packages: write
15
+ contents: read
16
+ id-token: write
17
+
18
+ env:
19
+ REGISTRY: ghcr.io
20
+
21
+ jobs:
22
+ docker-build-publish:
23
+ runs-on: ubuntu-latest
24
+ # timeout-minutes: 30
25
+ steps:
26
+ - name: Checkout code
27
+ uses: actions/checkout@v4
28
+
29
+ - name: Set lowercase image name
30
+ id: image
31
+ run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
32
+
33
+ - name: Set up Docker Buildx
34
+ uses: docker/setup-buildx-action@v3
35
+
36
+ - name: Log in to GitHub Container Registry
37
+ uses: docker/login-action@v3
38
+ with:
39
+ registry: ${{ env.REGISTRY }}
40
+ username: ${{ github.actor }}
41
+ password: ${{ secrets.GITHUB_TOKEN }}
42
+
43
+ - name: Extract metadata for Docker image
44
+ id: meta
45
+ uses: docker/metadata-action@v4
46
+ with:
47
+ images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
48
+ tags: |
49
+ type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
50
+ type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' }}
51
+ type=ref,event=tag
52
+ type=sha,format=short
53
+ type=raw,value=${{ github.ref_name }},enable=${{ startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/stable' }}
54
+
55
+ - name: Build and push Docker image
56
+ uses: docker/build-push-action@v6
57
+ with:
58
+ context: .
59
+ platforms: linux/amd64,linux/arm64
60
+ target: bolt-ai-production
61
+ push: true
62
+ tags: ${{ steps.meta.outputs.tags }}
63
+ labels: ${{ steps.meta.outputs.labels }}
64
+
65
+
66
+ - name: Check manifest
67
+ run: docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:${{ steps.meta.outputs.version }}
.github/workflows/docs.yaml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docs CI/CD
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - 'docs/**' # This will only trigger the workflow when files in docs directory change
9
+ permissions:
10
+ contents: write
11
+ jobs:
12
+ build_docs:
13
+ runs-on: ubuntu-latest
14
+ defaults:
15
+ run:
16
+ working-directory: ./docs
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Configure Git Credentials
20
+ run: |
21
+ git config user.name github-actions[bot]
22
+ git config user.email 41898282+github-actions[bot]@users.noreply.github.com
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: 3.x
26
+ - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
27
+ - uses: actions/cache@v4
28
+ with:
29
+ key: mkdocs-material-${{ env.cache_id }}
30
+ path: .cache
31
+ restore-keys: |
32
+ mkdocs-material-
33
+
34
+ - run: pip install mkdocs-material
35
+ - run: mkdocs gh-deploy --force
.github/workflows/electron.yml ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Electron Build and Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ tag:
7
+ description: 'Tag for the release (e.g., v1.0.0). Leave empty if not applicable.'
8
+ required: false
9
+ push:
10
+ branches:
11
+ - electron
12
+ tags:
13
+ - 'v*'
14
+
15
+ permissions:
16
+ contents: write
17
+
18
+ jobs:
19
+ build:
20
+ runs-on: ${{ matrix.os }}
21
+
22
+ strategy:
23
+ matrix:
24
+ os: [ubuntu-latest, windows-latest, macos-latest] # Use unsigned macOS builds for now
25
+ node-version: [20.18.0]
26
+ fail-fast: false
27
+
28
+ steps:
29
+ - name: Check out Git repository
30
+ uses: actions/checkout@v4
31
+
32
+ - name: Install Node.js
33
+ uses: actions/setup-node@v4
34
+ with:
35
+ node-version: ${{ matrix.node-version }}
36
+
37
+ - name: Install pnpm
38
+ uses: pnpm/action-setup@v2
39
+ with:
40
+ version: 9.14.4
41
+ run_install: false
42
+
43
+ - name: Get pnpm store directory
44
+ shell: bash
45
+ run: |
46
+ echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
47
+
48
+ - name: Setup pnpm cache
49
+ uses: actions/cache@v4
50
+ with:
51
+ path: ${{ env.STORE_PATH }}
52
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
53
+ restore-keys: |
54
+ ${{ runner.os }}-pnpm-store-
55
+
56
+ - name: Install dependencies
57
+ run: pnpm install
58
+
59
+ # Install Linux dependencies
60
+ - name: Install Linux dependencies
61
+ if: matrix.os == 'ubuntu-latest'
62
+ run: |
63
+ sudo apt-get update
64
+ sudo apt-get install -y rpm
65
+
66
+ # Build
67
+ - name: Build Electron app
68
+ env:
69
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70
+ NODE_OPTIONS: "--max_old_space_size=4096"
71
+ run: |
72
+ if [ "$RUNNER_OS" == "Windows" ]; then
73
+ pnpm run electron:build:win
74
+ elif [ "$RUNNER_OS" == "macOS" ]; then
75
+ pnpm run electron:build:mac
76
+ else
77
+ pnpm run electron:build:linux
78
+ fi
79
+ shell: bash
80
+
81
+ # Create Release
82
+ - name: Create Release
83
+ uses: softprops/action-gh-release@v2
84
+ with:
85
+ # Use the workflow_dispatch input tag if available, else use the Git ref name.
86
+ tag_name: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }}
87
+ # Only branch pushes remain drafts. For workflow_dispatch and tag pushes the release is published.
88
+ draft: ${{ github.event_name != 'workflow_dispatch' && github.ref_type == 'branch' }}
89
+ # For tag pushes, name the release as "Release <tagname>", otherwise "Electron Release".
90
+ name: ${{ (github.event_name == 'push' && github.ref_type == 'tag') && format('Release {0}', github.ref_name) || 'Electron Release' }}
91
+ files: |
92
+ dist/*.exe
93
+ dist/*.dmg
94
+ dist/*.deb
95
+ dist/*.AppImage
96
+ dist/*.zip
97
+ env:
98
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
.github/workflows/pr-release-validation.yaml ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: PR Validation
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened, labeled, unlabeled]
6
+ branches:
7
+ - main
8
+
9
+ permissions:
10
+ contents: read
11
+ pull-requests: write
12
+ checks: write
13
+
14
+ jobs:
15
+ quality-gates:
16
+ name: Quality Gates
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Wait for CI checks
24
+ uses: lewagon/wait-on-check-action@v1.3.1
25
+ with:
26
+ ref: ${{ github.event.pull_request.head.sha }}
27
+ check-name: 'Test'
28
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
29
+ wait-interval: 10
30
+
31
+ - name: Check required status checks
32
+ uses: actions/github-script@v7
33
+ continue-on-error: true
34
+ with:
35
+ script: |
36
+ const { data: checks } = await github.rest.checks.listForRef({
37
+ owner: context.repo.owner,
38
+ repo: context.repo.repo,
39
+ ref: context.payload.pull_request.head.sha
40
+ });
41
+
42
+ const requiredChecks = ['Test', 'CodeQL Analysis'];
43
+ const optionalChecks = ['Quality Analysis', 'Deploy Preview'];
44
+ const failedChecks = [];
45
+ const passedChecks = [];
46
+
47
+ // Check required workflows
48
+ for (const checkName of requiredChecks) {
49
+ const check = checks.check_runs.find(c => c.name === checkName);
50
+ if (check && check.conclusion === 'success') {
51
+ passedChecks.push(checkName);
52
+ } else {
53
+ failedChecks.push(checkName);
54
+ }
55
+ }
56
+
57
+ // Report optional checks
58
+ for (const checkName of optionalChecks) {
59
+ const check = checks.check_runs.find(c => c.name === checkName);
60
+ if (check && check.conclusion === 'success') {
61
+ passedChecks.push(`${checkName} (optional)`);
62
+ }
63
+ }
64
+
65
+ console.log(`✅ Passed checks: ${passedChecks.join(', ')}`);
66
+
67
+ if (failedChecks.length > 0) {
68
+ console.log(`❌ Failed required checks: ${failedChecks.join(', ')}`);
69
+ core.setFailed(`Required checks failed: ${failedChecks.join(', ')}`);
70
+ } else {
71
+ console.log(`✅ All required checks passed!`);
72
+ }
73
+
74
+ validate-release:
75
+ name: Release Validation
76
+ runs-on: ubuntu-latest
77
+ needs: quality-gates
78
+
79
+ steps:
80
+ - name: Checkout
81
+ uses: actions/checkout@v4
82
+
83
+ - name: Validate PR Labels
84
+ run: |
85
+ if [[ "${{ contains(github.event.pull_request.labels.*.name, 'stable-release') }}" == "true" ]]; then
86
+ echo "✓ PR has stable-release label"
87
+
88
+ # Check version bump labels
89
+ if [[ "${{ contains(github.event.pull_request.labels.*.name, 'major') }}" == "true" ]]; then
90
+ echo "✓ Major version bump requested"
91
+ elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'minor') }}" == "true" ]]; then
92
+ echo "✓ Minor version bump requested"
93
+ else
94
+ echo "✓ Patch version bump will be applied"
95
+ fi
96
+ else
97
+ echo "This PR doesn't have the stable-release label. No release will be created."
98
+ fi
99
+
100
+ - name: Check breaking changes
101
+ if: contains(github.event.pull_request.labels.*.name, 'major')
102
+ run: |
103
+ echo "⚠️ This PR contains breaking changes and will trigger a major release."
104
+
105
+ - name: Validate changelog entry
106
+ if: contains(github.event.pull_request.labels.*.name, 'stable-release')
107
+ run: |
108
+ if ! grep -q "${{ github.event.pull_request.number }}" CHANGES.md; then
109
+ echo "❌ No changelog entry found for PR #${{ github.event.pull_request.number }}"
110
+ echo "Please add an entry to CHANGES.md"
111
+ exit 1
112
+ else
113
+ echo "✓ Changelog entry found"
114
+ fi
115
+
116
+ security-review:
117
+ name: Security Review Required
118
+ runs-on: ubuntu-latest
119
+ if: contains(github.event.pull_request.labels.*.name, 'security')
120
+
121
+ steps:
122
+ - name: Check security label
123
+ run: |
124
+ echo "🔒 This PR has security implications and requires additional review"
125
+ echo "Ensure a security team member has approved this PR before merging"
.github/workflows/preview.yaml ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Preview Deployment
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened, closed]
6
+ branches: [main]
7
+
8
+ # Cancel in-progress runs on the same PR
9
+ concurrency:
10
+ group: preview-${{ github.event.pull_request.number }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+ pull-requests: write
16
+ deployments: write
17
+
18
+ jobs:
19
+ deploy-preview:
20
+ name: Deploy Preview
21
+ runs-on: ubuntu-latest
22
+ if: github.event.action != 'closed'
23
+
24
+ steps:
25
+ - name: Check if preview deployment is configured
26
+ id: check-secrets
27
+ run: |
28
+ if [[ -n "${{ secrets.CLOUDFLARE_API_TOKEN }}" && -n "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" ]]; then
29
+ echo "configured=true" >> $GITHUB_OUTPUT
30
+ else
31
+ echo "configured=false" >> $GITHUB_OUTPUT
32
+ fi
33
+
34
+ - name: Checkout
35
+ if: steps.check-secrets.outputs.configured == 'true'
36
+ uses: actions/checkout@v4
37
+
38
+ - name: Setup and Build
39
+ if: steps.check-secrets.outputs.configured == 'true'
40
+ uses: ./.github/actions/setup-and-build
41
+
42
+ - name: Build for production
43
+ if: steps.check-secrets.outputs.configured == 'true'
44
+ run: pnpm run build
45
+ env:
46
+ NODE_ENV: production
47
+
48
+ - name: Deploy to Cloudflare Pages
49
+ if: steps.check-secrets.outputs.configured == 'true'
50
+ id: deploy
51
+ uses: cloudflare/pages-action@v1
52
+ with:
53
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
54
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
55
+ projectName: bolt-diy-preview
56
+ directory: build/client
57
+ gitHubToken: ${{ secrets.GITHUB_TOKEN }}
58
+
59
+ - name: Preview deployment not configured
60
+ if: steps.check-secrets.outputs.configured == 'false'
61
+ run: |
62
+ echo "✅ Preview deployment is not configured for this repository"
63
+ echo "To enable preview deployments, add the following secrets:"
64
+ echo "- CLOUDFLARE_API_TOKEN"
65
+ echo "- CLOUDFLARE_ACCOUNT_ID"
66
+ echo "This is optional and the workflow will pass without it."
67
+ echo "url=https://preview-not-configured.example.com" >> $GITHUB_OUTPUT
68
+
69
+ - name: Add preview URL comment to PR
70
+ uses: actions/github-script@v7
71
+ continue-on-error: true
72
+ with:
73
+ script: |
74
+ const { data: comments } = await github.rest.issues.listComments({
75
+ owner: context.repo.owner,
76
+ repo: context.repo.repo,
77
+ issue_number: context.issue.number,
78
+ });
79
+
80
+ const previewComment = comments.find(comment =>
81
+ comment.body.includes('🚀 Preview deployment')
82
+ );
83
+
84
+ const isConfigured = '${{ steps.check-secrets.outputs.configured }}' === 'true';
85
+ const deployUrl = '${{ steps.deploy.outputs.url }}' || 'https://preview-not-configured.example.com';
86
+
87
+ let commentBody;
88
+ if (isConfigured) {
89
+ commentBody = `🚀 Preview deployment is ready!
90
+
91
+ | Name | Link |
92
+ |------|------|
93
+ | Latest commit | ${{ github.sha }} |
94
+ | Preview URL | ${deployUrl} |
95
+
96
+ Built with ❤️ by [bolt.diy](https://bolt.diy)
97
+ `;
98
+ } else {
99
+ commentBody = `ℹ️ Preview deployment not configured
100
+
101
+ | Name | Info |
102
+ |------|------|
103
+ | Latest commit | ${{ github.sha }} |
104
+ | Status | Preview deployment requires Cloudflare secrets |
105
+
106
+ To enable preview deployments, repository maintainers can add:
107
+ - \`CLOUDFLARE_API_TOKEN\` secret
108
+ - \`CLOUDFLARE_ACCOUNT_ID\` secret
109
+
110
+ Built with ❤️ by [bolt.diy](https://bolt.diy)
111
+ `;
112
+ }
113
+
114
+ if (previewComment) {
115
+ github.rest.issues.updateComment({
116
+ owner: context.repo.owner,
117
+ repo: context.repo.repo,
118
+ comment_id: previewComment.id,
119
+ body: commentBody
120
+ });
121
+ } else {
122
+ github.rest.issues.createComment({
123
+ owner: context.repo.owner,
124
+ repo: context.repo.repo,
125
+ issue_number: context.issue.number,
126
+ body: commentBody
127
+ });
128
+ }
129
+
130
+ - name: Run smoke tests on preview
131
+ run: |
132
+ if [[ "${{ steps.check-secrets.outputs.configured }}" == "true" ]]; then
133
+ echo "Running smoke tests on preview deployment..."
134
+ echo "Preview URL: ${{ steps.deploy.outputs.url }}"
135
+ # Basic HTTP check instead of Playwright tests
136
+ curl -f ${{ steps.deploy.outputs.url }} || echo "Preview environment check completed"
137
+ else
138
+ echo "✅ Smoke tests skipped - preview deployment not configured"
139
+ echo "This is normal and expected when Cloudflare secrets are not available"
140
+ fi
141
+
142
+ - name: Preview workflow summary
143
+ run: |
144
+ echo "✅ Preview deployment workflow completed successfully"
145
+ if [[ "${{ steps.check-secrets.outputs.configured }}" == "true" ]]; then
146
+ echo "🚀 Preview deployed to: ${{ steps.deploy.outputs.url }}"
147
+ else
148
+ echo "ℹ️ Preview deployment not configured (this is normal)"
149
+ fi
150
+
151
+ cleanup-preview:
152
+ name: Cleanup Preview
153
+ runs-on: ubuntu-latest
154
+ if: github.event.action == 'closed'
155
+
156
+ steps:
157
+ - name: Delete preview environment
158
+ uses: actions/github-script@v7
159
+ continue-on-error: true
160
+ with:
161
+ script: |
162
+ const deployments = await github.rest.repos.listDeployments({
163
+ owner: context.repo.owner,
164
+ repo: context.repo.repo,
165
+ environment: `preview-pr-${{ github.event.pull_request.number }}`,
166
+ });
167
+
168
+ for (const deployment of deployments.data) {
169
+ await github.rest.repos.createDeploymentStatus({
170
+ owner: context.repo.owner,
171
+ repo: context.repo.repo,
172
+ deployment_id: deployment.id,
173
+ state: 'inactive',
174
+ });
175
+ }
176
+
177
+ - name: Remove preview comment
178
+ uses: actions/github-script@v7
179
+ continue-on-error: true
180
+ with:
181
+ script: |
182
+ const { data: comments } = await github.rest.issues.listComments({
183
+ owner: context.repo.owner,
184
+ repo: context.repo.repo,
185
+ issue_number: context.issue.number,
186
+ });
187
+
188
+ for (const comment of comments) {
189
+ if (comment.body.includes('🚀 Preview deployment')) {
190
+ await github.rest.issues.deleteComment({
191
+ owner: context.repo.owner,
192
+ repo: context.repo.repo,
193
+ comment_id: comment.id,
194
+ });
195
+ }
196
+ }
.github/workflows/quality.yaml ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Code Quality
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ # Cancel in-progress runs on the same branch/PR
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ quality-checks:
16
+ name: Quality Analysis
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 30
19
+
20
+ steps:
21
+ - name: Checkout repository
22
+ uses: actions/checkout@v4
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - name: Setup and Build
27
+ uses: ./.github/actions/setup-and-build
28
+
29
+ - name: Check for duplicate dependencies
30
+ run: |
31
+ echo "Checking for duplicate dependencies..."
32
+ pnpm dedupe --check || echo "✅ Duplicate dependency check completed"
33
+
34
+ - name: Check bundle size
35
+ run: |
36
+ pnpm run build
37
+ echo "Bundle analysis completed (bundlesize tool requires configuration)"
38
+ continue-on-error: true
39
+
40
+ - name: Dead code elimination check
41
+ run: |
42
+ echo "Checking for unused imports and dead code..."
43
+ npx unimported || echo "Unimported tool completed with warnings"
44
+ continue-on-error: true
45
+
46
+ - name: Check for unused dependencies
47
+ run: |
48
+ echo "Checking for unused dependencies..."
49
+ npx depcheck --config .depcheckrc.json || echo "Dependency check completed with findings"
50
+ continue-on-error: true
51
+
52
+ - name: Check package.json formatting
53
+ run: |
54
+ echo "Checking package.json formatting..."
55
+ npx sort-package-json package.json --check || echo "Package.json formatting check completed"
56
+ continue-on-error: true
57
+
58
+ - name: Generate complexity report
59
+ run: |
60
+ echo "Analyzing code complexity..."
61
+ npx es6-plato -r -d complexity-report app/ || echo "Complexity analysis completed"
62
+ continue-on-error: true
63
+
64
+ - name: Upload complexity report
65
+ uses: actions/upload-artifact@v4
66
+ if: always()
67
+ with:
68
+ name: complexity-report
69
+ path: complexity-report/
70
+ retention-days: 7
71
+
72
+ accessibility-tests:
73
+ name: Accessibility Tests
74
+ runs-on: ubuntu-latest
75
+ timeout-minutes: 20
76
+
77
+ steps:
78
+ - name: Checkout repository
79
+ uses: actions/checkout@v4
80
+
81
+ - name: Setup and Build
82
+ uses: ./.github/actions/setup-and-build
83
+
84
+ - name: Start development server
85
+ run: |
86
+ pnpm run build
87
+ pnpm run start &
88
+ sleep 15
89
+ env:
90
+ CI: true
91
+
92
+ - name: Run accessibility tests with axe
93
+ run: |
94
+ echo "Running accessibility tests..."
95
+ npx @axe-core/cli http://localhost:5173 --exit || echo "Accessibility tests completed with findings"
96
+ continue-on-error: true
97
+
98
+ performance-audit:
99
+ name: Performance Audit
100
+ runs-on: ubuntu-latest
101
+ timeout-minutes: 25
102
+
103
+ steps:
104
+ - name: Checkout repository
105
+ uses: actions/checkout@v4
106
+
107
+ - name: Setup and Build
108
+ uses: ./.github/actions/setup-and-build
109
+
110
+ - name: Start server for Lighthouse
111
+ run: |
112
+ pnpm run build
113
+ pnpm run start &
114
+ sleep 20
115
+
116
+ - name: Run Lighthouse audit
117
+ run: |
118
+ echo "Running Lighthouse performance audit..."
119
+ npx lighthouse http://localhost:5173 --output-path=./lighthouse-report.html --output=html --chrome-flags="--headless --no-sandbox" || echo "Lighthouse audit completed"
120
+ continue-on-error: true
121
+
122
+ - name: Upload Lighthouse report
123
+ uses: actions/upload-artifact@v4
124
+ if: always()
125
+ with:
126
+ name: lighthouse-report
127
+ path: lighthouse-report.html
128
+ retention-days: 7
129
+
130
+ pr-size-check:
131
+ name: PR Size Check
132
+ runs-on: ubuntu-latest
133
+ if: github.event_name == 'pull_request'
134
+
135
+ steps:
136
+ - name: Checkout
137
+ uses: actions/checkout@v4
138
+ with:
139
+ fetch-depth: 0
140
+
141
+ - name: Calculate PR size
142
+ id: pr-size
143
+ run: |
144
+ # Get the base branch (target branch)
145
+ BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
146
+
147
+ # Count additions and deletions
148
+ ADDITIONS=$(git diff --numstat origin/$BASE_BRANCH...HEAD | awk '{sum += $1} END {print sum}')
149
+ DELETIONS=$(git diff --numstat origin/$BASE_BRANCH...HEAD | awk '{sum += $2} END {print sum}')
150
+ TOTAL_CHANGES=$((ADDITIONS + DELETIONS))
151
+
152
+ echo "additions=$ADDITIONS" >> $GITHUB_OUTPUT
153
+ echo "deletions=$DELETIONS" >> $GITHUB_OUTPUT
154
+ echo "total=$TOTAL_CHANGES" >> $GITHUB_OUTPUT
155
+
156
+ # Determine size category
157
+ if [ $TOTAL_CHANGES -lt 50 ]; then
158
+ echo "size=XS" >> $GITHUB_OUTPUT
159
+ elif [ $TOTAL_CHANGES -lt 200 ]; then
160
+ echo "size=S" >> $GITHUB_OUTPUT
161
+ elif [ $TOTAL_CHANGES -lt 500 ]; then
162
+ echo "size=M" >> $GITHUB_OUTPUT
163
+ elif [ $TOTAL_CHANGES -lt 1000 ]; then
164
+ echo "size=L" >> $GITHUB_OUTPUT
165
+ elif [ $TOTAL_CHANGES -lt 2000 ]; then
166
+ echo "size=XL" >> $GITHUB_OUTPUT
167
+ else
168
+ echo "size=XXL" >> $GITHUB_OUTPUT
169
+ fi
170
+
171
+ - name: PR size summary
172
+ run: |
173
+ echo "✅ PR Size Analysis Complete"
174
+ echo "📊 Changes: +${{ steps.pr-size.outputs.additions }} -${{ steps.pr-size.outputs.deletions }}"
175
+ echo "📏 Size Category: ${{ steps.pr-size.outputs.size }}"
176
+ echo "💡 This information helps reviewers understand the scope of changes"
177
+
178
+ if [ "${{ steps.pr-size.outputs.size }}" = "XXL" ]; then
179
+ echo "ℹ️ This is a large PR - consider breaking it into smaller chunks for future PRs"
180
+ echo "However, large PRs are acceptable for major feature additions like this one"
181
+ fi
.github/workflows/security.yaml ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Security Analysis
2
+
3
+ on:
4
+ push:
5
+ branches: [main, stable]
6
+ pull_request:
7
+ branches: [main]
8
+ schedule:
9
+ # Run weekly security scan on Sundays at 2 AM
10
+ - cron: '0 2 * * 0'
11
+
12
+ permissions:
13
+ actions: read
14
+ contents: read
15
+ security-events: read
16
+
17
+ jobs:
18
+ codeql:
19
+ name: CodeQL Analysis
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 45
22
+
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ language: ['javascript', 'typescript']
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+
32
+ - name: Initialize CodeQL
33
+ uses: github/codeql-action/init@v3
34
+ with:
35
+ languages: ${{ matrix.language }}
36
+ queries: security-extended,security-and-quality
37
+
38
+ - name: Autobuild
39
+ uses: github/codeql-action/autobuild@v3
40
+
41
+ - name: Perform CodeQL Analysis
42
+ uses: github/codeql-action/analyze@v3
43
+ with:
44
+ category: "/language:${{matrix.language}}"
45
+ upload: false
46
+ output: "codeql-results"
47
+
48
+ - name: Upload CodeQL results as artifact
49
+ uses: actions/upload-artifact@v4
50
+ if: always()
51
+ with:
52
+ name: codeql-results-${{ matrix.language }}
53
+ path: codeql-results
54
+
55
+ dependency-scan:
56
+ name: Dependency Vulnerability Scan
57
+ runs-on: ubuntu-latest
58
+
59
+ steps:
60
+ - name: Checkout repository
61
+ uses: actions/checkout@v4
62
+
63
+ - name: Setup Node.js
64
+ uses: actions/setup-node@v4
65
+ with:
66
+ node-version: '20.18.0'
67
+
68
+ - name: Install pnpm
69
+ uses: pnpm/action-setup@v4
70
+ with:
71
+ version: '9.14.4'
72
+
73
+ - name: Install dependencies
74
+ run: pnpm install --frozen-lockfile
75
+
76
+ - name: Run npm audit
77
+ run: pnpm audit --audit-level moderate
78
+ continue-on-error: true
79
+
80
+ - name: Generate SBOM
81
+ uses: anchore/sbom-action@v0
82
+ with:
83
+ path: ./
84
+ format: spdx-json
85
+ artifact-name: sbom.spdx.json
86
+
87
+ - name: Upload SBOM as artifact
88
+ uses: actions/upload-artifact@v4
89
+ if: always()
90
+ with:
91
+ name: sbom-results
92
+ path: |
93
+ sbom.spdx.json
94
+ **/sbom.spdx.json
95
+
96
+ secrets-scan:
97
+ name: Secrets Detection
98
+ runs-on: ubuntu-latest
99
+
100
+ steps:
101
+ - name: Checkout repository
102
+ uses: actions/checkout@v4
103
+ with:
104
+ fetch-depth: 0
105
+
106
+ - name: Run Trivy secrets scan
107
+ uses: aquasecurity/trivy-action@master
108
+ with:
109
+ scan-type: 'fs'
110
+ scan-ref: '.'
111
+ format: 'sarif'
112
+ output: 'trivy-secrets-results.sarif'
113
+ scanners: 'secret'
114
+
115
+ - name: Upload Trivy secrets results as artifact
116
+ uses: actions/upload-artifact@v4
117
+ if: always()
118
+ with:
119
+ name: trivy-secrets-results
120
+ path: trivy-secrets-results.sarif
121
+
.github/workflows/semantic-pr.yaml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Semantic Pull Request
2
+ on:
3
+ pull_request_target:
4
+ types: [opened, reopened, edited, synchronize]
5
+ permissions:
6
+ pull-requests: read
7
+ jobs:
8
+ main:
9
+ name: Validate PR Title
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ # https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.3
13
+ - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017
14
+ env:
15
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16
+ with:
17
+ subjectPattern: ^(?![A-Z]).+$
18
+ subjectPatternError: |
19
+ The subject "{subject}" found in the pull request title "{title}"
20
+ didn't match the configured pattern. Please ensure that the subject
21
+ doesn't start with an uppercase character.
22
+ types: |
23
+ fix
24
+ feat
25
+ chore
26
+ build
27
+ ci
28
+ perf
29
+ docs
30
+ refactor
31
+ revert
32
+ test
.github/workflows/stale.yml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Mark Stale Issues and Pull Requests
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 2 * * *' # Runs daily at 2:00 AM UTC
6
+ workflow_dispatch: # Allows manual triggering of the workflow
7
+
8
+ jobs:
9
+ stale:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Mark stale issues and pull requests
14
+ uses: actions/stale@v8
15
+ with:
16
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
17
+ stale-issue-message: 'This issue has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days.'
18
+ stale-pr-message: 'This pull request has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days.'
19
+ days-before-stale: 10 # Number of days before marking an issue or PR as stale
20
+ days-before-close: 4 # Number of days after being marked stale before closing
21
+ stale-issue-label: 'stale' # Label to apply to stale issues
22
+ stale-pr-label: 'stale' # Label to apply to stale pull requests
23
+ exempt-issue-labels: 'pinned,important' # Issues with these labels won't be marked stale
24
+ exempt-pr-labels: 'pinned,important' # PRs with these labels won't be marked stale
25
+ operations-per-run: 75 # Limits the number of actions per run to avoid API rate limits
.github/workflows/test-workflows.yaml ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Test Workflows
2
+
3
+ # This workflow is for testing our new workflow changes safely
4
+ on:
5
+ push:
6
+ branches: [workflow-testing, test-*]
7
+ pull_request:
8
+ branches: [workflow-testing]
9
+ workflow_dispatch:
10
+ inputs:
11
+ test_type:
12
+ description: 'Type of test to run'
13
+ required: true
14
+ default: 'all'
15
+ type: choice
16
+ options:
17
+ - all
18
+ - ci-only
19
+ - security-only
20
+ - quality-only
21
+
22
+ jobs:
23
+ workflow-test-info:
24
+ name: Workflow Test Information
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - name: Display test information
28
+ run: |
29
+ echo "🧪 Testing new workflow configurations"
30
+ echo "Branch: ${{ github.ref_name }}"
31
+ echo "Event: ${{ github.event_name }}"
32
+ echo "Test type: ${{ github.event.inputs.test_type || 'all' }}"
33
+ echo ""
34
+ echo "This is a safe test environment - no changes will affect production workflows"
35
+
36
+ test-basic-setup:
37
+ name: Test Basic Setup
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - name: Checkout
41
+ uses: actions/checkout@v4
42
+
43
+ - name: Test setup-and-build action
44
+ uses: ./.github/actions/setup-and-build
45
+
46
+ - name: Verify Node.js version
47
+ run: |
48
+ echo "Node.js version: $(node --version)"
49
+ if [[ "$(node --version)" == *"20.18.0"* ]]; then
50
+ echo "✅ Correct Node.js version"
51
+ else
52
+ echo "❌ Wrong Node.js version"
53
+ exit 1
54
+ fi
55
+
56
+ - name: Verify pnpm version
57
+ run: |
58
+ echo "pnpm version: $(pnpm --version)"
59
+ if [[ "$(pnpm --version)" == *"9.14.4"* ]]; then
60
+ echo "✅ Correct pnpm version"
61
+ else
62
+ echo "❌ Wrong pnpm version"
63
+ exit 1
64
+ fi
65
+
66
+ - name: Test build process
67
+ run: |
68
+ echo "✅ Build completed successfully"
69
+
70
+ test-linting:
71
+ name: Test Linting
72
+ runs-on: ubuntu-latest
73
+ steps:
74
+ - name: Checkout
75
+ uses: actions/checkout@v4
76
+
77
+ - name: Setup and Build
78
+ uses: ./.github/actions/setup-and-build
79
+
80
+ - name: Test ESLint
81
+ run: |
82
+ echo "Testing ESLint configuration..."
83
+ pnpm run lint --max-warnings 0 || echo "ESLint found issues (expected for testing)"
84
+
85
+ - name: Test TypeScript
86
+ run: |
87
+ echo "Testing TypeScript compilation..."
88
+ pnpm run typecheck
89
+
90
+ test-caching:
91
+ name: Test Caching Strategy
92
+ runs-on: ubuntu-latest
93
+ steps:
94
+ - name: Checkout
95
+ uses: actions/checkout@v4
96
+
97
+ - name: Setup and Build
98
+ uses: ./.github/actions/setup-and-build
99
+
100
+ - name: Test TypeScript cache
101
+ uses: actions/cache@v4
102
+ with:
103
+ path: |
104
+ .tsbuildinfo
105
+ node_modules/.cache
106
+ key: test-${{ runner.os }}-typescript-${{ hashFiles('**/tsconfig.json', 'app/**/*.ts', 'app/**/*.tsx') }}
107
+ restore-keys: |
108
+ test-${{ runner.os }}-typescript-
109
+
110
+ - name: Test ESLint cache
111
+ uses: actions/cache@v4
112
+ with:
113
+ path: node_modules/.cache/eslint
114
+ key: test-${{ runner.os }}-eslint-${{ hashFiles('.eslintrc*', 'app/**/*.ts', 'app/**/*.tsx') }}
115
+ restore-keys: |
116
+ test-${{ runner.os }}-eslint-
117
+
118
+ - name: Verify caching works
119
+ run: |
120
+ echo "✅ Caching configuration tested"
121
+
122
+ test-security-tools:
123
+ name: Test Security Tools
124
+ runs-on: ubuntu-latest
125
+ if: github.event.inputs.test_type == 'all' || github.event.inputs.test_type == 'security-only'
126
+ steps:
127
+ - name: Checkout
128
+ uses: actions/checkout@v4
129
+
130
+ - name: Setup Node.js
131
+ uses: actions/setup-node@v4
132
+ with:
133
+ node-version: '20.18.0'
134
+
135
+ - name: Install pnpm
136
+ uses: pnpm/action-setup@v4
137
+ with:
138
+ version: '9.14.4'
139
+
140
+ - name: Install dependencies
141
+ run: pnpm install --frozen-lockfile
142
+
143
+ - name: Test dependency audit (non-blocking)
144
+ run: |
145
+ echo "Testing pnpm audit..."
146
+ pnpm audit --audit-level moderate || echo "Audit found issues (this is for testing)"
147
+
148
+ - name: Test Trivy installation
149
+ run: |
150
+ echo "Testing Trivy secrets scanner..."
151
+ docker run --rm -v ${{ github.workspace }}:/workspace aquasecurity/trivy:latest fs /workspace --exit-code 0 --no-progress --format table --scanners secret || echo "Trivy test completed"
152
+
153
+ test-quality-checks:
154
+ name: Test Quality Checks
155
+ runs-on: ubuntu-latest
156
+ if: github.event.inputs.test_type == 'all' || github.event.inputs.test_type == 'quality-only'
157
+ steps:
158
+ - name: Checkout
159
+ uses: actions/checkout@v4
160
+
161
+ - name: Setup and Build
162
+ uses: ./.github/actions/setup-and-build
163
+
164
+ - name: Test bundle size analysis
165
+ run: |
166
+ echo "Testing bundle size analysis..."
167
+ ls -la build/client/ || echo "Build directory structure checked"
168
+
169
+ - name: Test dependency checks
170
+ run: |
171
+ echo "Testing depcheck..."
172
+ npx depcheck --config .depcheckrc.json || echo "Depcheck completed"
173
+
174
+ - name: Test package.json formatting
175
+ run: |
176
+ echo "Testing package.json sorting..."
177
+ npx sort-package-json package.json --check || echo "Package.json check completed"
178
+
179
+ validate-docker-config:
180
+ name: Validate Docker Configuration
181
+ runs-on: ubuntu-latest
182
+ steps:
183
+ - name: Checkout
184
+ uses: actions/checkout@v4
185
+
186
+ - name: Set up Docker Buildx
187
+ uses: docker/setup-buildx-action@v3
188
+
189
+ - name: Test Docker build (without push)
190
+ run: |
191
+ echo "Testing Docker build configuration..."
192
+ docker build --target bolt-ai-production . --no-cache --progress=plain
193
+ echo "✅ Docker build test completed"
194
+
195
+ test-results-summary:
196
+ name: Test Results Summary
197
+ runs-on: ubuntu-latest
198
+ needs: [workflow-test-info, test-basic-setup, test-linting, test-caching, test-security-tools, test-quality-checks, validate-docker-config]
199
+ if: always()
200
+ steps:
201
+ - name: Check all test results
202
+ run: |
203
+ echo "🧪 Workflow Testing Results Summary"
204
+ echo "=================================="
205
+
206
+ if [[ "${{ needs.test-basic-setup.result }}" == "success" ]]; then
207
+ echo "✅ Basic Setup: PASSED"
208
+ else
209
+ echo "❌ Basic Setup: FAILED"
210
+ fi
211
+
212
+ if [[ "${{ needs.test-linting.result }}" == "success" ]]; then
213
+ echo "✅ Linting Tests: PASSED"
214
+ else
215
+ echo "❌ Linting Tests: FAILED"
216
+ fi
217
+
218
+ if [[ "${{ needs.test-caching.result }}" == "success" ]]; then
219
+ echo "✅ Caching Tests: PASSED"
220
+ else
221
+ echo "❌ Caching Tests: FAILED"
222
+ fi
223
+
224
+ if [[ "${{ needs.test-security-tools.result }}" == "success" ]]; then
225
+ echo "✅ Security Tools: PASSED"
226
+ else
227
+ echo "❌ Security Tools: FAILED"
228
+ fi
229
+
230
+ if [[ "${{ needs.test-quality-checks.result }}" == "success" ]]; then
231
+ echo "✅ Quality Checks: PASSED"
232
+ else
233
+ echo "❌ Quality Checks: FAILED"
234
+ fi
235
+
236
+ if [[ "${{ needs.validate-docker-config.result }}" == "success" ]]; then
237
+ echo "✅ Docker Config: PASSED"
238
+ else
239
+ echo "❌ Docker Config: FAILED"
240
+ fi
241
+
242
+ echo ""
243
+ echo "Next steps:"
244
+ echo "1. Review any failures above"
245
+ echo "2. Fix issues in workflow configurations"
246
+ echo "3. Re-test until all checks pass"
247
+ echo "4. Create PR to merge workflow improvements"
.github/workflows/update-stable.yml ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Update Stable Branch
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ prepare-release:
13
+ if: contains(github.event.head_commit.message, '#release')
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 0
20
+
21
+ - name: Configure Git
22
+ run: |
23
+ git config --global user.name 'github-actions[bot]'
24
+ git config --global user.email 'github-actions[bot]@users.noreply.github.com'
25
+
26
+ - name: Setup Node.js
27
+ uses: actions/setup-node@v4
28
+ with:
29
+ node-version: '20.18.0'
30
+
31
+ - name: Install pnpm
32
+ uses: pnpm/action-setup@v2
33
+ with:
34
+ version: '9.14.4'
35
+ run_install: false
36
+
37
+ - name: Get pnpm store directory
38
+ id: pnpm-cache
39
+ shell: bash
40
+ run: |
41
+ echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
42
+
43
+ - name: Setup pnpm cache
44
+ uses: actions/cache@v4
45
+ with:
46
+ path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
47
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
48
+ restore-keys: |
49
+ ${{ runner.os }}-pnpm-store-
50
+
51
+ - name: Get Current Version
52
+ id: current_version
53
+ run: |
54
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
55
+ echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
56
+
57
+ - name: Install semver
58
+ run: pnpm add -g semver
59
+
60
+ - name: Determine Version Bump
61
+ id: version_bump
62
+ run: |
63
+ COMMIT_MSG="${{ github.event.head_commit.message }}"
64
+ if [[ $COMMIT_MSG =~ "#release:major" ]]; then
65
+ echo "bump=major" >> $GITHUB_OUTPUT
66
+ elif [[ $COMMIT_MSG =~ "#release:minor" ]]; then
67
+ echo "bump=minor" >> $GITHUB_OUTPUT
68
+ else
69
+ echo "bump=patch" >> $GITHUB_OUTPUT
70
+ fi
71
+
72
+ - name: Bump Version
73
+ id: bump_version
74
+ run: |
75
+ NEW_VERSION=$(semver -i ${{ steps.version_bump.outputs.bump }} ${{ steps.current_version.outputs.version }})
76
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
77
+
78
+ - name: Update Package.json
79
+ run: |
80
+ NEW_VERSION=${{ steps.bump_version.outputs.new_version }}
81
+ pnpm version $NEW_VERSION --no-git-tag-version --allow-same-version
82
+
83
+ - name: Prepare changelog script
84
+ run: chmod +x .github/scripts/generate-changelog.sh
85
+
86
+ - name: Generate Changelog
87
+ id: changelog
88
+ env:
89
+ NEW_VERSION: ${{ steps.bump_version.outputs.new_version }}
90
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
91
+
92
+ run: .github/scripts/generate-changelog.sh
93
+
94
+ - name: Get the latest commit hash and version tag
95
+ run: |
96
+ echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
97
+ echo "NEW_VERSION=${{ steps.bump_version.outputs.new_version }}" >> $GITHUB_ENV
98
+
99
+ - name: Commit and Tag Release
100
+ run: |
101
+ git pull
102
+ git add package.json pnpm-lock.yaml changelog.md
103
+ git commit -m "chore: release version ${{ steps.bump_version.outputs.new_version }}"
104
+ git tag "v${{ steps.bump_version.outputs.new_version }}"
105
+ git push
106
+ git push --tags
107
+
108
+ - name: Update Stable Branch
109
+ run: |
110
+ if ! git checkout stable 2>/dev/null; then
111
+ echo "Creating new stable branch..."
112
+ git checkout -b stable
113
+ fi
114
+ git merge main --no-ff -m "chore: release version ${{ steps.bump_version.outputs.new_version }}"
115
+ git push --set-upstream origin stable --force
116
+
117
+ - name: Create GitHub Release
118
+ env:
119
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
120
+ run: |
121
+ VERSION="v${{ steps.bump_version.outputs.new_version }}"
122
+ # Save changelog to a file
123
+ echo "${{ steps.changelog.outputs.content }}" > release_notes.md
124
+ gh release create "$VERSION" \
125
+ --title "Release $VERSION" \
126
+ --notes-file release_notes.md \
127
+ --target stable
.gitignore ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ logs
2
+ *.log
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+ pnpm-debug.log*
7
+ lerna-debug.log*
8
+
9
+ node_modules
10
+ dist
11
+ dist-ssr
12
+ *.local
13
+
14
+ .vscode/*
15
+ .vscode/launch.json
16
+ !.vscode/extensions.json
17
+ .idea
18
+ .DS_Store
19
+ *.suo
20
+ *.ntvs*
21
+ *.njsproj
22
+ *.sln
23
+ *.sw?
24
+
25
+ /.history
26
+ /.cache
27
+ /build
28
+ functions/build/
29
+ .env.local
30
+ .env
31
+ .dev.vars
32
+ *.vars
33
+ .wrangler
34
+ _worker.bundle
35
+
36
+ Modelfile
37
+ modelfiles
38
+
39
+ # docs ignore
40
+ site
41
+
42
+ # commit file ignore
43
+ app/commit.json
44
+ changelogUI.md
45
+ docs/instructions/Roadmap.md
46
+ .cursorrules
47
+ *.md
48
+ .qodo
.husky/pre-commit ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+
3
+ echo "🔍 Running pre-commit hook to check the code looks good... 🔍"
4
+
5
+ # Load NVM if available (useful for managing Node.js versions)
6
+ export NVM_DIR="$HOME/.nvm"
7
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
8
+
9
+ # Ensure `pnpm` is available
10
+ echo "Checking if pnpm is available..."
11
+ if ! command -v pnpm >/dev/null 2>&1; then
12
+ echo "❌ pnpm not found! Please ensure pnpm is installed and available in PATH."
13
+ exit 1
14
+ fi
15
+
16
+ # Run typecheck
17
+ echo "Running typecheck..."
18
+ if ! pnpm typecheck; then
19
+ echo "❌ Type checking failed! Please review TypeScript types."
20
+ echo "Once you're done, don't forget to add your changes to the commit! 🚀"
21
+ exit 1
22
+ fi
23
+
24
+ # Run lint
25
+ echo "Running lint..."
26
+ if ! pnpm lint; then
27
+ echo "❌ Linting failed! Run 'pnpm lint:fix' to fix the easy issues."
28
+ echo "Once you're done, don't forget to add your beautification to the commit! 🤩"
29
+ exit 1
30
+ fi
31
+
32
+ echo "👍 All checks passed! Committing changes..."
.lighthouserc.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "ci": {
3
+ "collect": {
4
+ "url": ["http://localhost:5173/"],
5
+ "startServerCommand": "pnpm run start",
6
+ "numberOfRuns": 3
7
+ },
8
+ "assert": {
9
+ "assertions": {
10
+ "categories:performance": ["warn", {"minScore": 0.8}],
11
+ "categories:accessibility": ["warn", {"minScore": 0.9}],
12
+ "categories:best-practices": ["warn", {"minScore": 0.8}],
13
+ "categories:seo": ["warn", {"minScore": 0.8}]
14
+ }
15
+ },
16
+ "upload": {
17
+ "target": "temporary-public-storage"
18
+ }
19
+ }
20
+ }
.prettierignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ pnpm-lock.yaml
2
+ .astro
.prettierrc ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "printWidth": 120,
3
+ "singleQuote": true,
4
+ "useTabs": false,
5
+ "tabWidth": 2,
6
+ "semi": true,
7
+ "bracketSpacing": true
8
+ }
Dockerfile ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---- build stage ----
2
+ FROM node:22-bookworm-slim AS build
3
+ WORKDIR /app
4
+
5
+ # CI-friendly env
6
+ ENV HUSKY=0
7
+ ENV CI=true
8
+
9
+ # Use pnpm
10
+ RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
11
+
12
+ # Ensure git is available for build and runtime scripts
13
+ RUN apt-get update && apt-get install -y --no-install-recommends git \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Accept (optional) build-time public URL for Remix/Vite (Coolify can pass it)
17
+ ARG VITE_PUBLIC_APP_URL
18
+ ENV VITE_PUBLIC_APP_URL=${VITE_PUBLIC_APP_URL}
19
+
20
+ # Install deps efficiently
21
+ COPY package.json pnpm-lock.yaml* ./
22
+ RUN pnpm fetch
23
+
24
+ # Copy source and build
25
+ COPY . .
26
+ # install with dev deps (needed to build)
27
+ RUN pnpm install --offline --frozen-lockfile
28
+
29
+ # Build the Remix app (SSR + client)
30
+ RUN NODE_OPTIONS=--max-old-space-size=4096 pnpm run build
31
+
32
+ # ---- production dependencies stage ----
33
+ FROM build AS prod-deps
34
+
35
+ # Keep only production deps for runtime
36
+ RUN pnpm prune --prod --ignore-scripts
37
+
38
+
39
+ # ---- production stage ----
40
+ FROM prod-deps AS bolt-ai-production
41
+ WORKDIR /app
42
+
43
+ ENV NODE_ENV=production
44
+ ENV PORT=5173
45
+ ENV HOST=0.0.0.0
46
+
47
+ # Non-sensitive build arguments
48
+ ARG VITE_LOG_LEVEL=debug
49
+ ARG DEFAULT_NUM_CTX
50
+
51
+ # Set non-sensitive environment variables
52
+ ENV WRANGLER_SEND_METRICS=false \
53
+ VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
54
+ DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} \
55
+ RUNNING_IN_DOCKER=true
56
+
57
+ # Note: API keys should be provided at runtime via docker run -e or docker-compose
58
+ # Example: docker run -e OPENAI_API_KEY=your_key_here ...
59
+
60
+ # Install curl for healthchecks and copy bindings script
61
+ RUN apt-get update && apt-get install -y --no-install-recommends curl \
62
+ && rm -rf /var/lib/apt/lists/*
63
+
64
+ # Copy built files and scripts
65
+ COPY --from=prod-deps /app/build /app/build
66
+ COPY --from=prod-deps /app/node_modules /app/node_modules
67
+ COPY --from=prod-deps /app/package.json /app/package.json
68
+ COPY --from=prod-deps /app/bindings.sh /app/bindings.sh
69
+
70
+ # Pre-configure wrangler to disable metrics
71
+ RUN mkdir -p /root/.config/.wrangler && \
72
+ echo '{"enabled":false}' > /root/.config/.wrangler/metrics.json
73
+
74
+ # Make bindings script executable
75
+ RUN chmod +x /app/bindings.sh
76
+
77
+ EXPOSE 5173
78
+
79
+ # Healthcheck for deployment platforms
80
+ HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 \
81
+ CMD curl -fsS http://localhost:5173/ || exit 1
82
+
83
+ # Start using dockerstart script with Wrangler
84
+ CMD ["pnpm", "run", "dockerstart"]
85
+
86
+
87
+ # ---- development stage ----
88
+ FROM build AS development
89
+
90
+ # Non-sensitive development arguments
91
+ ARG VITE_LOG_LEVEL=debug
92
+ ARG DEFAULT_NUM_CTX
93
+
94
+ # Set non-sensitive environment variables for development
95
+ ENV VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
96
+ DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} \
97
+ RUNNING_IN_DOCKER=true
98
+
99
+ # Note: API keys should be provided at runtime via docker run -e or docker-compose
100
+ # Example: docker run -e OPENAI_API_KEY=your_key_here ...
101
+
102
+ RUN mkdir -p /app/run
103
+ CMD ["pnpm", "run", "dev", "--host"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 StackBlitz, Inc. and bolt.diy contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
app/components/@settings/core/AvatarDropdown.tsx ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import { motion } from 'framer-motion';
3
+ import { useStore } from '@nanostores/react';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { profileStore } from '~/lib/stores/profile';
6
+ import type { TabType, Profile } from './types';
7
+
8
+ interface AvatarDropdownProps {
9
+ onSelectTab: (tab: TabType) => void;
10
+ }
11
+
12
+ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
13
+ const profile = useStore(profileStore) as Profile;
14
+
15
+ return (
16
+ <DropdownMenu.Root>
17
+ <DropdownMenu.Trigger asChild>
18
+ <motion.button
19
+ className="w-10 h-10 rounded-full bg-transparent flex items-center justify-center focus:outline-none"
20
+ whileHover={{ scale: 1.02 }}
21
+ whileTap={{ scale: 0.98 }}
22
+ >
23
+ {profile?.avatar ? (
24
+ <img
25
+ src={profile.avatar}
26
+ alt={profile?.username || 'Profile'}
27
+ className="w-full h-full rounded-full object-cover"
28
+ loading="eager"
29
+ decoding="sync"
30
+ />
31
+ ) : (
32
+ <div className="w-full h-full rounded-full flex items-center justify-center bg-white dark:bg-gray-800 text-gray-400 dark:text-gray-500">
33
+ <div className="i-ph:user w-6 h-6" />
34
+ </div>
35
+ )}
36
+ </motion.button>
37
+ </DropdownMenu.Trigger>
38
+
39
+ <DropdownMenu.Portal>
40
+ <DropdownMenu.Content
41
+ className={classNames(
42
+ 'min-w-[240px] z-[250]',
43
+ 'bg-white dark:bg-[#141414]',
44
+ 'rounded-lg shadow-lg',
45
+ 'border border-gray-200/50 dark:border-gray-800/50',
46
+ 'animate-in fade-in-0 zoom-in-95',
47
+ 'py-1',
48
+ )}
49
+ sideOffset={5}
50
+ align="end"
51
+ >
52
+ <div
53
+ className={classNames(
54
+ 'px-4 py-3 flex items-center gap-3',
55
+ 'border-b border-gray-200/50 dark:border-gray-800/50',
56
+ )}
57
+ >
58
+ <div className="w-10 h-10 rounded-full overflow-hidden flex-shrink-0 bg-white dark:bg-gray-800 shadow-sm">
59
+ {profile?.avatar ? (
60
+ <img
61
+ src={profile.avatar}
62
+ alt={profile?.username || 'Profile'}
63
+ className={classNames('w-full h-full', 'object-cover', 'transform-gpu', 'image-rendering-crisp')}
64
+ loading="eager"
65
+ decoding="sync"
66
+ />
67
+ ) : (
68
+ <div className="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500 font-medium text-lg">
69
+ <div className="i-ph:user w-6 h-6" />
70
+ </div>
71
+ )}
72
+ </div>
73
+ <div className="flex-1 min-w-0">
74
+ <div className="font-medium text-sm text-gray-900 dark:text-white truncate">
75
+ {profile?.username || 'Guest User'}
76
+ </div>
77
+ {profile?.bio && <div className="text-xs text-gray-500 dark:text-gray-400 truncate">{profile.bio}</div>}
78
+ </div>
79
+ </div>
80
+
81
+ <DropdownMenu.Item
82
+ className={classNames(
83
+ 'flex items-center gap-2 px-4 py-2.5',
84
+ 'text-sm text-gray-700 dark:text-gray-200',
85
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
86
+ 'hover:text-purple-500 dark:hover:text-purple-400',
87
+ 'cursor-pointer transition-all duration-200',
88
+ 'outline-none',
89
+ 'group',
90
+ )}
91
+ onClick={() => onSelectTab('profile')}
92
+ >
93
+ <div className="i-ph:user-circle w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
94
+ Edit Profile
95
+ </DropdownMenu.Item>
96
+
97
+ <DropdownMenu.Item
98
+ className={classNames(
99
+ 'flex items-center gap-2 px-4 py-2.5',
100
+ 'text-sm text-gray-700 dark:text-gray-200',
101
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
102
+ 'hover:text-purple-500 dark:hover:text-purple-400',
103
+ 'cursor-pointer transition-all duration-200',
104
+ 'outline-none',
105
+ 'group',
106
+ )}
107
+ onClick={() => onSelectTab('settings')}
108
+ >
109
+ <div className="i-ph:gear-six w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
110
+ Settings
111
+ </DropdownMenu.Item>
112
+
113
+ <div className="my-1 border-t border-gray-200/50 dark:border-gray-800/50" />
114
+
115
+ <DropdownMenu.Item
116
+ className={classNames(
117
+ 'flex items-center gap-2 px-4 py-2.5',
118
+ 'text-sm text-gray-700 dark:text-gray-200',
119
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
120
+ 'hover:text-purple-500 dark:hover:text-purple-400',
121
+ 'cursor-pointer transition-all duration-200',
122
+ 'outline-none',
123
+ 'group',
124
+ )}
125
+ onClick={() =>
126
+ window.open('https://github.com/stackblitz-labs/bolt.diy/issues/new?template=bug_report.yml', '_blank')
127
+ }
128
+ >
129
+ <div className="i-ph:bug w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
130
+ Report Bug
131
+ </DropdownMenu.Item>
132
+
133
+ <DropdownMenu.Item
134
+ className={classNames(
135
+ 'flex items-center gap-2 px-4 py-2.5',
136
+ 'text-sm text-gray-700 dark:text-gray-200',
137
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
138
+ 'hover:text-purple-500 dark:hover:text-purple-400',
139
+ 'cursor-pointer transition-all duration-200',
140
+ 'outline-none',
141
+ 'group',
142
+ )}
143
+ onClick={async () => {
144
+ try {
145
+ const { downloadDebugLog } = await import('~/utils/debugLogger');
146
+ await downloadDebugLog();
147
+ } catch (error) {
148
+ console.error('Failed to download debug log:', error);
149
+ }
150
+ }}
151
+ >
152
+ <div className="i-ph:download w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
153
+ Download Debug Log
154
+ </DropdownMenu.Item>
155
+
156
+ <DropdownMenu.Item
157
+ className={classNames(
158
+ 'flex items-center gap-2 px-4 py-2.5',
159
+ 'text-sm text-gray-700 dark:text-gray-200',
160
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
161
+ 'hover:text-purple-500 dark:hover:text-purple-400',
162
+ 'cursor-pointer transition-all duration-200',
163
+ 'outline-none',
164
+ 'group',
165
+ )}
166
+ onClick={() => window.open('https://stackblitz-labs.github.io/bolt.diy/', '_blank')}
167
+ >
168
+ <div className="i-ph:question w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
169
+ Help & Documentation
170
+ </DropdownMenu.Item>
171
+ </DropdownMenu.Content>
172
+ </DropdownMenu.Portal>
173
+ </DropdownMenu.Root>
174
+ );
175
+ };
app/components/@settings/core/ControlPanel.tsx ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { useStore } from '@nanostores/react';
3
+ import * as RadixDialog from '@radix-ui/react-dialog';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { TabTile } from '~/components/@settings/shared/components/TabTile';
6
+ import { useFeatures } from '~/lib/hooks/useFeatures';
7
+ import { useNotifications } from '~/lib/hooks/useNotifications';
8
+ import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
9
+ import { tabConfigurationStore, resetTabConfiguration } from '~/lib/stores/settings';
10
+ import { profileStore } from '~/lib/stores/profile';
11
+ import type { TabType, Profile } from './types';
12
+ import { TAB_LABELS, DEFAULT_TAB_CONFIG, TAB_DESCRIPTIONS } from './constants';
13
+ import { DialogTitle } from '~/components/ui/Dialog';
14
+ import { AvatarDropdown } from './AvatarDropdown';
15
+ import BackgroundRays from '~/components/ui/BackgroundRays';
16
+
17
+ // Import all tab components
18
+ import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab';
19
+ import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab';
20
+ import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab';
21
+ import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
22
+ import { DataTab } from '~/components/@settings/tabs/data/DataTab';
23
+ import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
24
+ import GitHubTab from '~/components/@settings/tabs/github/GitHubTab';
25
+ import GitLabTab from '~/components/@settings/tabs/gitlab/GitLabTab';
26
+ import SupabaseTab from '~/components/@settings/tabs/supabase/SupabaseTab';
27
+ import VercelTab from '~/components/@settings/tabs/vercel/VercelTab';
28
+ import NetlifyTab from '~/components/@settings/tabs/netlify/NetlifyTab';
29
+ import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
30
+ import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
31
+ import McpTab from '~/components/@settings/tabs/mcp/McpTab';
32
+
33
+ interface ControlPanelProps {
34
+ open: boolean;
35
+ onClose: () => void;
36
+ }
37
+
38
+ // Beta status for experimental features
39
+ const BETA_TABS = new Set<TabType>(['local-providers', 'mcp']);
40
+
41
+ const BetaLabel = () => (
42
+ <div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20">
43
+ <span className="text-[10px] font-medium text-purple-600 dark:text-purple-400">BETA</span>
44
+ </div>
45
+ );
46
+
47
+ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
48
+ // State
49
+ const [activeTab, setActiveTab] = useState<TabType | null>(null);
50
+ const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
51
+ const [showTabManagement, setShowTabManagement] = useState(false);
52
+
53
+ // Store values
54
+ const tabConfiguration = useStore(tabConfigurationStore);
55
+ const profile = useStore(profileStore) as Profile;
56
+
57
+ // Status hooks
58
+ const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
59
+ const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
60
+ const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
61
+
62
+ // Memoize the base tab configurations to avoid recalculation
63
+ const baseTabConfig = useMemo(() => {
64
+ return new Map(DEFAULT_TAB_CONFIG.map((tab) => [tab.id, tab]));
65
+ }, []);
66
+
67
+ // Add visibleTabs logic using useMemo with optimized calculations
68
+ const visibleTabs = useMemo(() => {
69
+ if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
70
+ console.warn('Invalid tab configuration, resetting to defaults');
71
+ resetTabConfiguration();
72
+
73
+ return [];
74
+ }
75
+
76
+ const notificationsDisabled = profile?.preferences?.notifications === false;
77
+
78
+ // Optimize user mode tab filtering
79
+ return tabConfiguration.userTabs
80
+ .filter((tab) => {
81
+ if (!tab?.id) {
82
+ return false;
83
+ }
84
+
85
+ if (tab.id === 'notifications' && notificationsDisabled) {
86
+ return false;
87
+ }
88
+
89
+ return tab.visible && tab.window === 'user';
90
+ })
91
+ .sort((a, b) => a.order - b.order);
92
+ }, [tabConfiguration, profile?.preferences?.notifications, baseTabConfig]);
93
+
94
+ // Reset to default view when modal opens/closes
95
+ useEffect(() => {
96
+ if (!open) {
97
+ // Reset when closing
98
+ setActiveTab(null);
99
+ setLoadingTab(null);
100
+ setShowTabManagement(false);
101
+ } else {
102
+ // When opening, set to null to show the main view
103
+ setActiveTab(null);
104
+ }
105
+ }, [open]);
106
+
107
+ // Handle closing
108
+ const handleClose = () => {
109
+ setActiveTab(null);
110
+ setLoadingTab(null);
111
+ setShowTabManagement(false);
112
+ onClose();
113
+ };
114
+
115
+ // Handlers
116
+ const handleBack = () => {
117
+ if (showTabManagement) {
118
+ setShowTabManagement(false);
119
+ } else if (activeTab) {
120
+ setActiveTab(null);
121
+ }
122
+ };
123
+
124
+ const getTabComponent = (tabId: TabType) => {
125
+ switch (tabId) {
126
+ case 'profile':
127
+ return <ProfileTab />;
128
+ case 'settings':
129
+ return <SettingsTab />;
130
+ case 'notifications':
131
+ return <NotificationsTab />;
132
+ case 'features':
133
+ return <FeaturesTab />;
134
+ case 'data':
135
+ return <DataTab />;
136
+ case 'cloud-providers':
137
+ return <CloudProvidersTab />;
138
+ case 'local-providers':
139
+ return <LocalProvidersTab />;
140
+ case 'github':
141
+ return <GitHubTab />;
142
+ case 'gitlab':
143
+ return <GitLabTab />;
144
+ case 'supabase':
145
+ return <SupabaseTab />;
146
+ case 'vercel':
147
+ return <VercelTab />;
148
+ case 'netlify':
149
+ return <NetlifyTab />;
150
+ case 'event-logs':
151
+ return <EventLogsTab />;
152
+ case 'mcp':
153
+ return <McpTab />;
154
+
155
+ default:
156
+ return null;
157
+ }
158
+ };
159
+
160
+ const getTabUpdateStatus = (tabId: TabType): boolean => {
161
+ switch (tabId) {
162
+ case 'features':
163
+ return hasNewFeatures;
164
+ case 'notifications':
165
+ return hasUnreadNotifications;
166
+ case 'github':
167
+ case 'gitlab':
168
+ case 'supabase':
169
+ case 'vercel':
170
+ case 'netlify':
171
+ return hasConnectionIssues;
172
+ default:
173
+ return false;
174
+ }
175
+ };
176
+
177
+ const getStatusMessage = (tabId: TabType): string => {
178
+ switch (tabId) {
179
+ case 'features':
180
+ return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
181
+ case 'notifications':
182
+ return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
183
+ case 'github':
184
+ case 'gitlab':
185
+ case 'supabase':
186
+ case 'vercel':
187
+ case 'netlify':
188
+ return currentIssue === 'disconnected'
189
+ ? 'Connection lost'
190
+ : currentIssue === 'high-latency'
191
+ ? 'High latency detected'
192
+ : 'Connection issues detected';
193
+ default:
194
+ return '';
195
+ }
196
+ };
197
+
198
+ const handleTabClick = (tabId: TabType) => {
199
+ setLoadingTab(tabId);
200
+ setActiveTab(tabId);
201
+ setShowTabManagement(false);
202
+
203
+ // Acknowledge notifications based on tab
204
+ switch (tabId) {
205
+ case 'features':
206
+ acknowledgeAllFeatures();
207
+ break;
208
+ case 'notifications':
209
+ markAllAsRead();
210
+ break;
211
+ case 'github':
212
+ case 'gitlab':
213
+ case 'supabase':
214
+ case 'vercel':
215
+ case 'netlify':
216
+ acknowledgeIssue();
217
+ break;
218
+ }
219
+
220
+ // Clear loading state after a delay
221
+ setTimeout(() => setLoadingTab(null), 500);
222
+ };
223
+
224
+ return (
225
+ <RadixDialog.Root open={open}>
226
+ <RadixDialog.Portal>
227
+ <div className="fixed inset-0 flex items-center justify-center z-[100] modern-scrollbar">
228
+ <RadixDialog.Overlay className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm transition-opacity duration-200" />
229
+
230
+ <RadixDialog.Content
231
+ aria-describedby={undefined}
232
+ onEscapeKeyDown={handleClose}
233
+ onPointerDownOutside={handleClose}
234
+ className="relative z-[101]"
235
+ >
236
+ <div
237
+ className={classNames(
238
+ 'w-[1200px] h-[90vh]',
239
+ 'bg-bolt-elements-background-depth-1',
240
+ 'rounded-2xl shadow-2xl',
241
+ 'border border-bolt-elements-borderColor',
242
+ 'flex flex-col overflow-hidden',
243
+ 'relative',
244
+ 'transform transition-all duration-200 ease-out',
245
+ open ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-95 translate-y-4',
246
+ )}
247
+ >
248
+ <div className="absolute inset-0 overflow-hidden rounded-2xl">
249
+ <BackgroundRays />
250
+ </div>
251
+ <div className="relative z-10 flex flex-col h-full">
252
+ {/* Header */}
253
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
254
+ <div className="flex items-center space-x-4">
255
+ {(activeTab || showTabManagement) && (
256
+ <button
257
+ onClick={handleBack}
258
+ className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-colors duration-150"
259
+ >
260
+ <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
261
+ </button>
262
+ )}
263
+ <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
264
+ {showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
265
+ </DialogTitle>
266
+ </div>
267
+
268
+ <div className="flex items-center gap-6">
269
+ {/* Avatar and Dropdown */}
270
+ <div className="pl-6">
271
+ <AvatarDropdown onSelectTab={handleTabClick} />
272
+ </div>
273
+
274
+ {/* Close Button */}
275
+ <button
276
+ onClick={handleClose}
277
+ className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
278
+ >
279
+ <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
280
+ </button>
281
+ </div>
282
+ </div>
283
+
284
+ {/* Content */}
285
+ <div
286
+ className={classNames(
287
+ 'flex-1',
288
+ 'overflow-y-auto',
289
+ 'hover:overflow-y-auto',
290
+ 'scrollbar scrollbar-w-2',
291
+ 'scrollbar-track-transparent',
292
+ 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
293
+ 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
294
+ 'will-change-scroll',
295
+ 'touch-auto',
296
+ )}
297
+ >
298
+ <div
299
+ className={classNames(
300
+ 'p-6 transition-opacity duration-150',
301
+ activeTab || showTabManagement ? 'opacity-100' : 'opacity-100',
302
+ )}
303
+ >
304
+ {activeTab ? (
305
+ getTabComponent(activeTab)
306
+ ) : (
307
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
308
+ {visibleTabs.map((tab, index) => (
309
+ <div
310
+ key={tab.id}
311
+ className={classNames(
312
+ 'aspect-[1.5/1] transition-transform duration-100 ease-out',
313
+ 'hover:scale-[1.01]',
314
+ )}
315
+ style={{
316
+ animationDelay: `${index * 30}ms`,
317
+ animation: open ? 'fadeInUp 200ms ease-out forwards' : 'none',
318
+ }}
319
+ >
320
+ <TabTile
321
+ tab={tab}
322
+ onClick={() => handleTabClick(tab.id as TabType)}
323
+ isActive={activeTab === tab.id}
324
+ hasUpdate={getTabUpdateStatus(tab.id)}
325
+ statusMessage={getStatusMessage(tab.id)}
326
+ description={TAB_DESCRIPTIONS[tab.id]}
327
+ isLoading={loadingTab === tab.id}
328
+ className="h-full relative"
329
+ >
330
+ {BETA_TABS.has(tab.id) && <BetaLabel />}
331
+ </TabTile>
332
+ </div>
333
+ ))}
334
+ </div>
335
+ )}
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ </RadixDialog.Content>
341
+ </div>
342
+ </RadixDialog.Portal>
343
+ </RadixDialog.Root>
344
+ );
345
+ };
app/components/@settings/core/constants.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { TabType } from './types';
2
+ import { User, Settings, Bell, Star, Database, Cloud, Laptop, Github, Wrench, List } from 'lucide-react';
3
+
4
+ // GitLab icon component
5
+ const GitLabIcon = () => (
6
+ <svg viewBox="0 0 24 24" className="w-4 h-4">
7
+ <path
8
+ fill="currentColor"
9
+ d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"
10
+ />
11
+ </svg>
12
+ );
13
+
14
+ // Vercel icon component
15
+ const VercelIcon = () => (
16
+ <svg viewBox="0 0 24 24" className="w-4 h-4">
17
+ <path fill="currentColor" d="M12 2L2 19.777h20L12 2z" />
18
+ </svg>
19
+ );
20
+
21
+ // Netlify icon component
22
+ const NetlifyIcon = () => (
23
+ <svg viewBox="0 0 24 24" className="w-4 h-4">
24
+ <path
25
+ fill="currentColor"
26
+ d="M16.934 8.519a1.044 1.044 0 0 1 .303-.23l2.349-1.045a.983.983 0 0 1 .905 0c.264.12.49.328.651.599l.518 1.065c.17.35.17.761 0 1.11l-.518 1.065a1.119 1.119 0 0 1-.651.599l-2.35 1.045a1.013 1.013 0 0 1-.904 0l-2.35-1.045a1.119 1.119 0 0 1-.651-.599L13.718 9.02a1.2 1.2 0 0 1 0-1.11l.518-1.065a1.119 1.119 0 0 1 .651-.599l2.35-1.045a.983.983 0 0 1 .697-.061zm-6.051 5.751a1.044 1.044 0 0 1 .303-.23l2.349-1.045a.983.983 0 0 1 .905 0c.264.12.49.328.651.599l.518 1.065c.17.35.17.761 0 1.11l-.518 1.065a1.119 1.119 0 0 1-.651.599l-2.35 1.045a1.013 1.013 0 0 1-.904 0l-2.35-1.045a1.119 1.119 0 0 1-.651-.599l-.518-1.065a1.2 1.2 0 0 1 0-1.11l.518-1.065a1.119 1.119 0 0 1 .651-.599l2.35-1.045a.983.983 0 0 1 .697-.061z"
27
+ />
28
+ </svg>
29
+ );
30
+
31
+ // Supabase icon component
32
+ const SupabaseIcon = () => (
33
+ <svg viewBox="0 0 24 24" className="w-4 h-4">
34
+ <path
35
+ fill="currentColor"
36
+ d="M21.362 9.354H12V.396a.396.396 0 0 0-.716-.233L2.203 12.424l-.401.562a1.04 1.04 0 0 0 .836 1.659H12V21.6a.396.396 0 0 0 .716.233l9.081-12.261.401-.562a1.04 1.04 0 0 0-.836-1.656z"
37
+ />
38
+ </svg>
39
+ );
40
+
41
+ export const TAB_ICONS: Record<TabType, React.ComponentType<{ className?: string }>> = {
42
+ profile: User,
43
+ settings: Settings,
44
+ notifications: Bell,
45
+ features: Star,
46
+ data: Database,
47
+ 'cloud-providers': Cloud,
48
+ 'local-providers': Laptop,
49
+ github: Github,
50
+ gitlab: () => <GitLabIcon />,
51
+ netlify: () => <NetlifyIcon />,
52
+ vercel: () => <VercelIcon />,
53
+ supabase: () => <SupabaseIcon />,
54
+ 'event-logs': List,
55
+ mcp: Wrench,
56
+ };
57
+
58
+ export const TAB_LABELS: Record<TabType, string> = {
59
+ profile: 'Profile',
60
+ settings: 'Settings',
61
+ notifications: 'Notifications',
62
+ features: 'Features',
63
+ data: 'Data Management',
64
+ 'cloud-providers': 'Cloud Providers',
65
+ 'local-providers': 'Local Providers',
66
+ github: 'GitHub',
67
+ gitlab: 'GitLab',
68
+ netlify: 'Netlify',
69
+ vercel: 'Vercel',
70
+ supabase: 'Supabase',
71
+ 'event-logs': 'Event Logs',
72
+ mcp: 'MCP Servers',
73
+ };
74
+
75
+ export const TAB_DESCRIPTIONS: Record<TabType, string> = {
76
+ profile: 'Manage your profile and account settings',
77
+ settings: 'Configure application preferences',
78
+ notifications: 'View and manage your notifications',
79
+ features: 'Explore new and upcoming features',
80
+ data: 'Manage your data and storage',
81
+ 'cloud-providers': 'Configure cloud AI providers and models',
82
+ 'local-providers': 'Configure local AI providers and models',
83
+ github: 'Connect and manage GitHub integration',
84
+ gitlab: 'Connect and manage GitLab integration',
85
+ netlify: 'Configure Netlify deployment settings',
86
+ vercel: 'Manage Vercel projects and deployments',
87
+ supabase: 'Setup Supabase database connection',
88
+ 'event-logs': 'View system events and logs',
89
+ mcp: 'Configure MCP (Model Context Protocol) servers',
90
+ };
91
+
92
+ export const DEFAULT_TAB_CONFIG = [
93
+ // User Window Tabs (Always visible by default)
94
+ { id: 'features', visible: true, window: 'user' as const, order: 0 },
95
+ { id: 'data', visible: true, window: 'user' as const, order: 1 },
96
+ { id: 'cloud-providers', visible: true, window: 'user' as const, order: 2 },
97
+ { id: 'local-providers', visible: true, window: 'user' as const, order: 3 },
98
+ { id: 'github', visible: true, window: 'user' as const, order: 4 },
99
+ { id: 'gitlab', visible: true, window: 'user' as const, order: 5 },
100
+ { id: 'netlify', visible: true, window: 'user' as const, order: 6 },
101
+ { id: 'vercel', visible: true, window: 'user' as const, order: 7 },
102
+ { id: 'supabase', visible: true, window: 'user' as const, order: 8 },
103
+ { id: 'notifications', visible: true, window: 'user' as const, order: 9 },
104
+ { id: 'event-logs', visible: true, window: 'user' as const, order: 10 },
105
+ { id: 'mcp', visible: true, window: 'user' as const, order: 11 },
106
+
107
+ // User Window Tabs (In dropdown, initially hidden)
108
+ ];
app/components/@settings/core/types.ts ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ReactNode } from 'react';
2
+ import { User, Folder, Wifi, Settings, Box, Sliders } from 'lucide-react';
3
+
4
+ export type SettingCategory = 'profile' | 'file_sharing' | 'connectivity' | 'system' | 'services' | 'preferences';
5
+
6
+ export type TabType =
7
+ | 'profile'
8
+ | 'settings'
9
+ | 'notifications'
10
+ | 'features'
11
+ | 'data'
12
+ | 'cloud-providers'
13
+ | 'local-providers'
14
+ | 'github'
15
+ | 'gitlab'
16
+ | 'netlify'
17
+ | 'vercel'
18
+ | 'supabase'
19
+ | 'event-logs'
20
+ | 'mcp';
21
+
22
+ export type WindowType = 'user' | 'developer';
23
+
24
+ export interface UserProfile {
25
+ nickname: any;
26
+ name: string;
27
+ email: string;
28
+ avatar?: string;
29
+ theme: 'light' | 'dark' | 'system';
30
+ notifications: boolean;
31
+ password?: string;
32
+ bio?: string;
33
+ language: string;
34
+ timezone: string;
35
+ }
36
+
37
+ export interface SettingItem {
38
+ id: TabType;
39
+ label: string;
40
+ icon: string;
41
+ category: SettingCategory;
42
+ description?: string;
43
+ component: () => ReactNode;
44
+ badge?: string;
45
+ keywords?: string[];
46
+ }
47
+
48
+ export interface TabVisibilityConfig {
49
+ id: TabType;
50
+ visible: boolean;
51
+ window: WindowType;
52
+ order: number;
53
+ isExtraDevTab?: boolean;
54
+ locked?: boolean;
55
+ }
56
+
57
+ export interface DevTabConfig extends TabVisibilityConfig {
58
+ window: 'developer';
59
+ }
60
+
61
+ export interface UserTabConfig extends TabVisibilityConfig {
62
+ window: 'user';
63
+ }
64
+
65
+ export interface TabWindowConfig {
66
+ userTabs: UserTabConfig[];
67
+ }
68
+
69
+ export const TAB_LABELS: Record<TabType, string> = {
70
+ profile: 'Profile',
71
+ settings: 'Settings',
72
+ notifications: 'Notifications',
73
+ features: 'Features',
74
+ data: 'Data Management',
75
+ 'cloud-providers': 'Cloud Providers',
76
+ 'local-providers': 'Local Providers',
77
+ github: 'GitHub',
78
+ gitlab: 'GitLab',
79
+ netlify: 'Netlify',
80
+ vercel: 'Vercel',
81
+ supabase: 'Supabase',
82
+ 'event-logs': 'Event Logs',
83
+ mcp: 'MCP Servers',
84
+ };
85
+
86
+ export const categoryLabels: Record<SettingCategory, string> = {
87
+ profile: 'Profile & Account',
88
+ file_sharing: 'File Sharing',
89
+ connectivity: 'Connectivity',
90
+ system: 'System',
91
+ services: 'Services',
92
+ preferences: 'Preferences',
93
+ };
94
+
95
+ export const categoryIcons: Record<SettingCategory, React.ComponentType<{ className?: string }>> = {
96
+ profile: User,
97
+ file_sharing: Folder,
98
+ connectivity: Wifi,
99
+ system: Settings,
100
+ services: Box,
101
+ preferences: Sliders,
102
+ };
103
+
104
+ export interface Profile {
105
+ username?: string;
106
+ bio?: string;
107
+ avatar?: string;
108
+ preferences?: {
109
+ notifications?: boolean;
110
+ theme?: 'light' | 'dark' | 'system';
111
+ language?: string;
112
+ timezone?: string;
113
+ };
114
+ }
app/components/@settings/index.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Core exports
2
+ export { ControlPanel } from './core/ControlPanel';
3
+ export type { TabType, TabVisibilityConfig } from './core/types';
4
+
5
+ // Constants
6
+ export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constants';
7
+
8
+ // Shared components
9
+ export { TabTile } from './shared/components/TabTile';
10
+
11
+ // Utils
12
+ export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers';
app/components/@settings/shared/components/TabTile.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Tooltip from '@radix-ui/react-tooltip';
2
+ import { classNames } from '~/utils/classNames';
3
+ import type { TabVisibilityConfig } from '~/components/@settings/core/types';
4
+ import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants';
5
+ import { GlowingEffect } from '~/components/ui/GlowingEffect';
6
+
7
+ interface TabTileProps {
8
+ tab: TabVisibilityConfig;
9
+ onClick?: () => void;
10
+ isActive?: boolean;
11
+ hasUpdate?: boolean;
12
+ statusMessage?: string;
13
+ description?: string;
14
+ isLoading?: boolean;
15
+ className?: string;
16
+ children?: React.ReactNode;
17
+ }
18
+
19
+ export const TabTile: React.FC<TabTileProps> = ({
20
+ tab,
21
+ onClick,
22
+ isActive,
23
+ hasUpdate,
24
+ statusMessage,
25
+ description,
26
+ isLoading,
27
+ className,
28
+ children,
29
+ }: TabTileProps) => {
30
+ return (
31
+ <Tooltip.Provider delayDuration={0}>
32
+ <Tooltip.Root>
33
+ <Tooltip.Trigger asChild>
34
+ <div className={classNames('min-h-[160px] list-none', className || '')}>
35
+ <div className="relative h-full rounded-xl border border-[#E5E5E5] dark:border-[#333333] p-0.5">
36
+ <GlowingEffect
37
+ blur={0}
38
+ borderWidth={1}
39
+ spread={20}
40
+ glow={true}
41
+ disabled={false}
42
+ proximity={40}
43
+ inactiveZone={0.3}
44
+ movementDuration={0.4}
45
+ />
46
+ <div
47
+ onClick={onClick}
48
+ className={classNames(
49
+ 'relative flex flex-col items-center justify-center h-full p-4 rounded-lg',
50
+ 'bg-white dark:bg-[#141414]',
51
+ 'group cursor-pointer',
52
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
53
+ 'transition-colors duration-100 ease-out',
54
+ isActive ? 'bg-purple-500/5 dark:bg-purple-500/10' : '',
55
+ isLoading ? 'cursor-wait opacity-70 pointer-events-none' : '',
56
+ )}
57
+ >
58
+ {/* Icon */}
59
+ <div
60
+ className={classNames(
61
+ 'relative',
62
+ 'w-14 h-14',
63
+ 'flex items-center justify-center',
64
+ 'rounded-xl',
65
+ 'bg-gray-100 dark:bg-gray-800',
66
+ 'ring-1 ring-gray-200 dark:ring-gray-700',
67
+ 'group-hover:bg-purple-100 dark:group-hover:bg-gray-700/80',
68
+ 'group-hover:ring-purple-200 dark:group-hover:ring-purple-800/30',
69
+ 'transition-all duration-100 ease-out',
70
+ isActive ? 'bg-purple-500/10 dark:bg-purple-500/10 ring-purple-500/30 dark:ring-purple-500/20' : '',
71
+ )}
72
+ >
73
+ {(() => {
74
+ const IconComponent = TAB_ICONS[tab.id];
75
+ return (
76
+ <IconComponent
77
+ className={classNames(
78
+ 'w-8 h-8',
79
+ 'text-gray-600 dark:text-gray-300',
80
+ 'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
81
+ 'transition-colors duration-100 ease-out',
82
+ isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
83
+ )}
84
+ />
85
+ );
86
+ })()}
87
+ </div>
88
+
89
+ {/* Label and Description */}
90
+ <div className="flex flex-col items-center mt-4 w-full">
91
+ <h3
92
+ className={classNames(
93
+ 'text-[15px] font-medium leading-snug mb-2',
94
+ 'text-gray-700 dark:text-gray-200',
95
+ 'group-hover:text-purple-600 dark:group-hover:text-purple-300/90',
96
+ 'transition-colors duration-100 ease-out',
97
+ isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
98
+ )}
99
+ >
100
+ {TAB_LABELS[tab.id]}
101
+ </h3>
102
+ {description && (
103
+ <p
104
+ className={classNames(
105
+ 'text-[13px] leading-relaxed',
106
+ 'text-gray-500 dark:text-gray-400',
107
+ 'max-w-[85%]',
108
+ 'text-center',
109
+ 'group-hover:text-purple-500 dark:group-hover:text-purple-400/70',
110
+ 'transition-colors duration-100 ease-out',
111
+ isActive ? 'text-purple-400 dark:text-purple-400/80' : '',
112
+ )}
113
+ >
114
+ {description}
115
+ </p>
116
+ )}
117
+ </div>
118
+
119
+ {/* Update Indicator with Tooltip */}
120
+ {hasUpdate && (
121
+ <>
122
+ <div className="absolute top-4 right-4 w-2 h-2 rounded-full bg-purple-500 dark:bg-purple-400 animate-pulse" />
123
+ <Tooltip.Portal>
124
+ <Tooltip.Content
125
+ className={classNames(
126
+ 'px-3 py-1.5 rounded-lg',
127
+ 'bg-[#18181B] text-white',
128
+ 'text-sm font-medium',
129
+ 'select-none',
130
+ 'z-[100]',
131
+ )}
132
+ side="top"
133
+ sideOffset={5}
134
+ >
135
+ {statusMessage}
136
+ <Tooltip.Arrow className="fill-[#18181B]" />
137
+ </Tooltip.Content>
138
+ </Tooltip.Portal>
139
+ </>
140
+ )}
141
+
142
+ {/* Children (e.g. Beta Label) */}
143
+ {children}
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </Tooltip.Trigger>
148
+ </Tooltip.Root>
149
+ </Tooltip.Provider>
150
+ );
151
+ };
app/components/@settings/shared/service-integration/ConnectionForm.tsx ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { classNames } from '~/utils/classNames';
4
+
5
+ interface TokenTypeOption {
6
+ value: string;
7
+ label: string;
8
+ description?: string;
9
+ }
10
+
11
+ interface ConnectionFormProps {
12
+ isConnected: boolean;
13
+ isConnecting: boolean;
14
+ token: string;
15
+ onTokenChange: (token: string) => void;
16
+ onConnect: (e: React.FormEvent) => void;
17
+ onDisconnect: () => void;
18
+ error?: string;
19
+ serviceName: string;
20
+ tokenLabel?: string;
21
+ tokenPlaceholder?: string;
22
+ getTokenUrl: string;
23
+ environmentVariable?: string;
24
+ tokenTypes?: TokenTypeOption[];
25
+ selectedTokenType?: string;
26
+ onTokenTypeChange?: (type: string) => void;
27
+ connectedMessage?: string;
28
+ children?: React.ReactNode; // For additional form fields
29
+ }
30
+
31
+ export function ConnectionForm({
32
+ isConnected,
33
+ isConnecting,
34
+ token,
35
+ onTokenChange,
36
+ onConnect,
37
+ onDisconnect,
38
+ error,
39
+ serviceName,
40
+ tokenLabel = 'Access Token',
41
+ tokenPlaceholder,
42
+ getTokenUrl,
43
+ environmentVariable,
44
+ tokenTypes,
45
+ selectedTokenType,
46
+ onTokenTypeChange,
47
+ connectedMessage = `Connected to ${serviceName}`,
48
+ children,
49
+ }: ConnectionFormProps) {
50
+ return (
51
+ <motion.div
52
+ className="bg-bolt-elements-background dark:bg-bolt-elements-background border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor rounded-lg"
53
+ initial={{ opacity: 0, y: 20 }}
54
+ animate={{ opacity: 1, y: 0 }}
55
+ transition={{ delay: 0.2 }}
56
+ >
57
+ <div className="p-6 space-y-6">
58
+ {!isConnected ? (
59
+ <div className="space-y-4">
60
+ {environmentVariable && (
61
+ <div className="text-xs text-bolt-elements-textSecondary bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1 p-3 rounded-lg mb-4">
62
+ <p className="flex items-center gap-1 mb-1">
63
+ <span className="i-ph:lightbulb w-3.5 h-3.5 text-bolt-elements-icon-success dark:text-bolt-elements-icon-success" />
64
+ <span className="font-medium">Tip:</span> You can also set the{' '}
65
+ <code className="px-1 py-0.5 bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-2 rounded">
66
+ {environmentVariable}
67
+ </code>{' '}
68
+ environment variable to connect automatically.
69
+ </p>
70
+ </div>
71
+ )}
72
+
73
+ <form onSubmit={onConnect} className="space-y-4">
74
+ {tokenTypes && tokenTypes.length > 1 && onTokenTypeChange && (
75
+ <div>
76
+ <label className="block text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mb-2">
77
+ Token Type
78
+ </label>
79
+ <select
80
+ value={selectedTokenType}
81
+ onChange={(e) => onTokenTypeChange(e.target.value)}
82
+ disabled={isConnecting}
83
+ className={classNames(
84
+ 'w-full px-3 py-2 rounded-lg text-sm',
85
+ 'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1',
86
+ 'border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor',
87
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
88
+ 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-item-contentAccent dark:focus:ring-bolt-elements-item-contentAccent',
89
+ 'disabled:opacity-50',
90
+ )}
91
+ >
92
+ {tokenTypes.map((type) => (
93
+ <option key={type.value} value={type.value}>
94
+ {type.label}
95
+ </option>
96
+ ))}
97
+ </select>
98
+ {selectedTokenType && tokenTypes.find((t) => t.value === selectedTokenType)?.description && (
99
+ <p className="mt-1 text-xs text-bolt-elements-textTertiary">
100
+ {tokenTypes.find((t) => t.value === selectedTokenType)?.description}
101
+ </p>
102
+ )}
103
+ </div>
104
+ )}
105
+
106
+ <div>
107
+ <label className="block text-sm text-bolt-elements-textSecondary mb-2">{tokenLabel}</label>
108
+ <input
109
+ type="password"
110
+ value={token}
111
+ onChange={(e) => onTokenChange(e.target.value)}
112
+ disabled={isConnecting}
113
+ placeholder={tokenPlaceholder || `Enter your ${serviceName} access token`}
114
+ className={classNames(
115
+ 'w-full px-3 py-2 rounded-lg text-sm',
116
+ 'bg-bolt-elements-background-depth-1',
117
+ 'border border-bolt-elements-borderColor',
118
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
119
+ 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColorActive',
120
+ 'disabled:opacity-50',
121
+ )}
122
+ />
123
+ <div className="mt-2 text-sm text-bolt-elements-textSecondary">
124
+ <a
125
+ href={getTokenUrl}
126
+ target="_blank"
127
+ rel="noopener noreferrer"
128
+ className="text-bolt-elements-borderColorActive hover:underline inline-flex items-center gap-1"
129
+ >
130
+ Get your token
131
+ <div className="i-ph:arrow-square-out w-4 h-4" />
132
+ </a>
133
+ </div>
134
+ </div>
135
+
136
+ {children}
137
+
138
+ {error && (
139
+ <div className="p-4 rounded-lg bg-red-50 border border-red-200 dark:bg-red-900/20 dark:border-red-700">
140
+ <p className="text-sm text-red-800 dark:text-red-200">{error}</p>
141
+ </div>
142
+ )}
143
+
144
+ <button
145
+ type="submit"
146
+ disabled={isConnecting || !token.trim()}
147
+ className={classNames(
148
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
149
+ 'bg-[#303030] text-white',
150
+ 'hover:bg-[#5E41D0] hover:text-white',
151
+ 'disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200',
152
+ 'transform active:scale-95',
153
+ )}
154
+ >
155
+ {isConnecting ? (
156
+ <>
157
+ <div className="i-ph:spinner-gap animate-spin" />
158
+ Connecting...
159
+ </>
160
+ ) : (
161
+ <>
162
+ <div className="i-ph:plug-charging w-4 h-4" />
163
+ Connect
164
+ </>
165
+ )}
166
+ </button>
167
+ </form>
168
+ </div>
169
+ ) : (
170
+ <div className="flex items-center justify-between">
171
+ <div className="flex items-center gap-3">
172
+ <button
173
+ onClick={onDisconnect}
174
+ className={classNames(
175
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
176
+ 'bg-red-500 text-white',
177
+ 'hover:bg-red-600',
178
+ )}
179
+ >
180
+ <div className="i-ph:plug w-4 h-4" />
181
+ Disconnect
182
+ </button>
183
+ <span className="text-sm text-bolt-elements-textSecondary flex items-center gap-1">
184
+ <div className="i-ph:check-circle w-4 h-4 text-green-500" />
185
+ {connectedMessage}
186
+ </span>
187
+ </div>
188
+ </div>
189
+ )}
190
+ </div>
191
+ </motion.div>
192
+ );
193
+ }
app/components/@settings/shared/service-integration/ConnectionTestIndicator.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { classNames } from '~/utils/classNames';
4
+
5
+ export interface ConnectionTestResult {
6
+ status: 'success' | 'error' | 'testing';
7
+ message: string;
8
+ timestamp?: number;
9
+ }
10
+
11
+ interface ConnectionTestIndicatorProps {
12
+ testResult: ConnectionTestResult | null;
13
+ className?: string;
14
+ }
15
+
16
+ export function ConnectionTestIndicator({ testResult, className }: ConnectionTestIndicatorProps) {
17
+ if (!testResult) {
18
+ return null;
19
+ }
20
+
21
+ return (
22
+ <motion.div
23
+ className={classNames(
24
+ 'p-4 rounded-lg border',
25
+ {
26
+ 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-700': testResult.status === 'success',
27
+ 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-700': testResult.status === 'error',
28
+ 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-700': testResult.status === 'testing',
29
+ },
30
+ className,
31
+ )}
32
+ initial={{ opacity: 0, y: 10 }}
33
+ animate={{ opacity: 1, y: 0 }}
34
+ >
35
+ <div className="flex items-center gap-2">
36
+ {testResult.status === 'success' && (
37
+ <div className="i-ph:check-circle w-5 h-5 text-green-600 dark:text-green-400" />
38
+ )}
39
+ {testResult.status === 'error' && (
40
+ <div className="i-ph:warning-circle w-5 h-5 text-red-600 dark:text-red-400" />
41
+ )}
42
+ {testResult.status === 'testing' && (
43
+ <div className="i-ph:spinner-gap w-5 h-5 animate-spin text-blue-600 dark:text-blue-400" />
44
+ )}
45
+ <span
46
+ className={classNames('text-sm font-medium', {
47
+ 'text-green-800 dark:text-green-200': testResult.status === 'success',
48
+ 'text-red-800 dark:text-red-200': testResult.status === 'error',
49
+ 'text-blue-800 dark:text-blue-200': testResult.status === 'testing',
50
+ })}
51
+ >
52
+ {testResult.message}
53
+ </span>
54
+ </div>
55
+ {testResult.timestamp && (
56
+ <p className="text-xs text-gray-500 mt-1">{new Date(testResult.timestamp).toLocaleString()}</p>
57
+ )}
58
+ </motion.div>
59
+ );
60
+ }
app/components/@settings/shared/service-integration/ErrorState.tsx ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Button } from '~/components/ui/Button';
4
+ import { classNames } from '~/utils/classNames';
5
+ import type { ServiceError } from '~/lib/utils/serviceErrorHandler';
6
+
7
+ interface ErrorStateProps {
8
+ error?: ServiceError | string;
9
+ title?: string;
10
+ onRetry?: () => void;
11
+ onDismiss?: () => void;
12
+ retryLabel?: string;
13
+ className?: string;
14
+ showDetails?: boolean;
15
+ }
16
+
17
+ export function ErrorState({
18
+ error,
19
+ title = 'Something went wrong',
20
+ onRetry,
21
+ onDismiss,
22
+ retryLabel = 'Try again',
23
+ className,
24
+ showDetails = false,
25
+ }: ErrorStateProps) {
26
+ const errorMessage = typeof error === 'string' ? error : error?.message || 'An unknown error occurred';
27
+ const isServiceError = typeof error === 'object' && error !== null;
28
+
29
+ return (
30
+ <motion.div
31
+ className={classNames(
32
+ 'p-6 rounded-lg border border-red-200 bg-red-50 dark:border-red-700 dark:bg-red-900/20',
33
+ className,
34
+ )}
35
+ initial={{ opacity: 0, y: 10 }}
36
+ animate={{ opacity: 1, y: 0 }}
37
+ >
38
+ <div className="flex items-start gap-3">
39
+ <div className="i-ph:warning-circle w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
40
+ <div className="flex-1 min-w-0">
41
+ <h3 className="text-sm font-medium text-red-800 dark:text-red-200 mb-1">{title}</h3>
42
+ <p className="text-sm text-red-700 dark:text-red-300">{errorMessage}</p>
43
+
44
+ {showDetails && isServiceError && error.details && (
45
+ <details className="mt-3">
46
+ <summary className="text-xs text-red-600 dark:text-red-400 cursor-pointer hover:underline">
47
+ Technical details
48
+ </summary>
49
+ <pre className="mt-2 text-xs text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/30 p-2 rounded overflow-auto">
50
+ {JSON.stringify(error.details, null, 2)}
51
+ </pre>
52
+ </details>
53
+ )}
54
+
55
+ <div className="flex items-center gap-2 mt-4">
56
+ {onRetry && (
57
+ <Button
58
+ onClick={onRetry}
59
+ variant="outline"
60
+ size="sm"
61
+ className="text-red-700 border-red-300 hover:bg-red-100 dark:text-red-300 dark:border-red-600 dark:hover:bg-red-900/30"
62
+ >
63
+ <div className="i-ph:arrows-clockwise w-4 h-4 mr-1" />
64
+ {retryLabel}
65
+ </Button>
66
+ )}
67
+ {onDismiss && (
68
+ <Button
69
+ onClick={onDismiss}
70
+ variant="outline"
71
+ size="sm"
72
+ className="text-red-700 border-red-300 hover:bg-red-100 dark:text-red-300 dark:border-red-600 dark:hover:bg-red-900/30"
73
+ >
74
+ Dismiss
75
+ </Button>
76
+ )}
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </motion.div>
81
+ );
82
+ }
83
+
84
+ interface ConnectionErrorProps {
85
+ service: string;
86
+ error: ServiceError | string;
87
+ onRetryConnection: () => void;
88
+ onClearError?: () => void;
89
+ }
90
+
91
+ export function ConnectionError({ service, error, onRetryConnection, onClearError }: ConnectionErrorProps) {
92
+ return (
93
+ <ErrorState
94
+ error={error}
95
+ title={`Failed to connect to ${service}`}
96
+ onRetry={onRetryConnection}
97
+ onDismiss={onClearError}
98
+ retryLabel="Retry connection"
99
+ showDetails={true}
100
+ />
101
+ );
102
+ }
app/components/@settings/shared/service-integration/LoadingState.tsx ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { classNames } from '~/utils/classNames';
4
+
5
+ interface LoadingStateProps {
6
+ message?: string;
7
+ size?: 'sm' | 'md' | 'lg';
8
+ className?: string;
9
+ showProgress?: boolean;
10
+ progress?: number;
11
+ }
12
+
13
+ export function LoadingState({
14
+ message = 'Loading...',
15
+ size = 'md',
16
+ className,
17
+ showProgress = false,
18
+ progress = 0,
19
+ }: LoadingStateProps) {
20
+ const sizeClasses = {
21
+ sm: 'w-4 h-4',
22
+ md: 'w-6 h-6',
23
+ lg: 'w-8 h-8',
24
+ };
25
+
26
+ return (
27
+ <motion.div
28
+ className={classNames('flex flex-col items-center justify-center gap-3', className)}
29
+ initial={{ opacity: 0 }}
30
+ animate={{ opacity: 1 }}
31
+ >
32
+ <div className="flex items-center gap-2">
33
+ <div
34
+ className={classNames(
35
+ 'i-ph:spinner-gap animate-spin text-bolt-elements-item-contentAccent',
36
+ sizeClasses[size],
37
+ )}
38
+ />
39
+ <span className="text-bolt-elements-textSecondary">{message}</span>
40
+ </div>
41
+
42
+ {showProgress && (
43
+ <div className="w-full max-w-xs">
44
+ <div className="w-full bg-bolt-elements-background-depth-2 rounded-full h-1">
45
+ <motion.div
46
+ className="bg-bolt-elements-item-contentAccent h-1 rounded-full"
47
+ initial={{ width: 0 }}
48
+ animate={{ width: `${progress}%` }}
49
+ transition={{ duration: 0.3 }}
50
+ />
51
+ </div>
52
+ </div>
53
+ )}
54
+ </motion.div>
55
+ );
56
+ }
57
+
58
+ interface SkeletonProps {
59
+ className?: string;
60
+ lines?: number;
61
+ }
62
+
63
+ export function Skeleton({ className, lines = 1 }: SkeletonProps) {
64
+ return (
65
+ <div className={classNames('animate-pulse', className)}>
66
+ {Array.from({ length: lines }, (_, i) => (
67
+ <div
68
+ key={i}
69
+ className={classNames(
70
+ 'bg-bolt-elements-background-depth-2 rounded',
71
+ i === lines - 1 ? 'h-4' : 'h-4 mb-2',
72
+ i === lines - 1 && lines > 1 ? 'w-3/4' : 'w-full',
73
+ )}
74
+ />
75
+ ))}
76
+ </div>
77
+ );
78
+ }
79
+
80
+ interface ServiceLoadingProps {
81
+ serviceName: string;
82
+ operation: string;
83
+ progress?: number;
84
+ }
85
+
86
+ export function ServiceLoading({ serviceName, operation, progress }: ServiceLoadingProps) {
87
+ return (
88
+ <LoadingState
89
+ message={`${operation} ${serviceName}...`}
90
+ showProgress={progress !== undefined}
91
+ progress={progress}
92
+ />
93
+ );
94
+ }
app/components/@settings/shared/service-integration/ServiceHeader.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { memo } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Button } from '~/components/ui/Button';
4
+
5
+ interface ServiceHeaderProps {
6
+ icon: React.ComponentType<{ className?: string }>;
7
+ title: string;
8
+ description?: string;
9
+ onTestConnection?: () => void;
10
+ isTestingConnection?: boolean;
11
+ additionalInfo?: React.ReactNode;
12
+ delay?: number;
13
+ }
14
+
15
+ export const ServiceHeader = memo(
16
+ ({
17
+ icon: Icon, // eslint-disable-line @typescript-eslint/naming-convention
18
+ title,
19
+ description,
20
+ onTestConnection,
21
+ isTestingConnection,
22
+ additionalInfo,
23
+ delay = 0.1,
24
+ }: ServiceHeaderProps) => {
25
+ return (
26
+ <>
27
+ <motion.div
28
+ className="flex items-center justify-between gap-2"
29
+ initial={{ opacity: 0, y: 20 }}
30
+ animate={{ opacity: 1, y: 0 }}
31
+ transition={{ delay }}
32
+ >
33
+ <div className="flex items-center gap-2">
34
+ <Icon className="w-5 h-5" />
35
+ <h2 className="text-lg font-medium text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
36
+ {title}
37
+ </h2>
38
+ </div>
39
+ <div className="flex items-center gap-2">
40
+ {additionalInfo}
41
+ {onTestConnection && (
42
+ <Button
43
+ onClick={onTestConnection}
44
+ disabled={isTestingConnection}
45
+ variant="outline"
46
+ className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors"
47
+ >
48
+ {isTestingConnection ? (
49
+ <>
50
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
51
+ Testing...
52
+ </>
53
+ ) : (
54
+ <>
55
+ <div className="i-ph:plug-charging w-4 h-4" />
56
+ Test Connection
57
+ </>
58
+ )}
59
+ </Button>
60
+ )}
61
+ </div>
62
+ </motion.div>
63
+
64
+ {description && (
65
+ <p className="text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary">
66
+ {description}
67
+ </p>
68
+ )}
69
+ </>
70
+ );
71
+ },
72
+ );
app/components/@settings/shared/service-integration/index.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export { ConnectionTestIndicator } from './ConnectionTestIndicator';
2
+ export type { ConnectionTestResult } from './ConnectionTestIndicator';
3
+ export { ServiceHeader } from './ServiceHeader';
4
+ export { ConnectionForm } from './ConnectionForm';
5
+ export { LoadingState, Skeleton, ServiceLoading } from './LoadingState';
6
+ export { ErrorState, ConnectionError } from './ErrorState';
app/components/@settings/tabs/data/DataTab.tsx ADDED
@@ -0,0 +1,721 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { Button } from '~/components/ui/Button';
3
+ import { ConfirmationDialog, SelectionDialog } from '~/components/ui/Dialog';
4
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '~/components/ui/Card';
5
+ import { motion } from 'framer-motion';
6
+ import { useDataOperations } from '~/lib/hooks/useDataOperations';
7
+ import { openDatabase } from '~/lib/persistence/db';
8
+ import { getAllChats, type Chat } from '~/lib/persistence/chats';
9
+ import { DataVisualization } from './DataVisualization';
10
+ import { classNames } from '~/utils/classNames';
11
+ import { toast } from 'react-toastify';
12
+
13
+ // Create a custom hook to connect to the boltHistory database
14
+ function useBoltHistoryDB() {
15
+ const [db, setDb] = useState<IDBDatabase | null>(null);
16
+ const [isLoading, setIsLoading] = useState(true);
17
+ const [error, setError] = useState<Error | null>(null);
18
+
19
+ useEffect(() => {
20
+ const initDB = async () => {
21
+ try {
22
+ setIsLoading(true);
23
+
24
+ const database = await openDatabase();
25
+ setDb(database || null);
26
+ setIsLoading(false);
27
+ } catch (err) {
28
+ setError(err instanceof Error ? err : new Error('Unknown error initializing database'));
29
+ setIsLoading(false);
30
+ }
31
+ };
32
+
33
+ initDB();
34
+
35
+ return () => {
36
+ if (db) {
37
+ db.close();
38
+ }
39
+ };
40
+ }, []);
41
+
42
+ return { db, isLoading, error };
43
+ }
44
+
45
+ // Extend the Chat interface to include the missing properties
46
+ interface ExtendedChat extends Chat {
47
+ title?: string;
48
+ updatedAt?: number;
49
+ }
50
+
51
+ // Helper function to create a chat label and description
52
+ function createChatItem(chat: Chat): ChatItem {
53
+ return {
54
+ id: chat.id,
55
+
56
+ // Use description as title if available, or format a short ID
57
+ label: (chat as ExtendedChat).title || chat.description || `Chat ${chat.id.slice(0, 8)}`,
58
+
59
+ // Format the description with message count and timestamp
60
+ description: `${chat.messages.length} messages - Last updated: ${new Date((chat as ExtendedChat).updatedAt || Date.parse(chat.timestamp)).toLocaleString()}`,
61
+ };
62
+ }
63
+
64
+ interface SettingsCategory {
65
+ id: string;
66
+ label: string;
67
+ description: string;
68
+ }
69
+
70
+ interface ChatItem {
71
+ id: string;
72
+ label: string;
73
+ description: string;
74
+ }
75
+
76
+ export function DataTab() {
77
+ // Use our custom hook for the boltHistory database
78
+ const { db, isLoading: dbLoading } = useBoltHistoryDB();
79
+ const fileInputRef = useRef<HTMLInputElement>(null);
80
+ const apiKeyFileInputRef = useRef<HTMLInputElement>(null);
81
+ const chatFileInputRef = useRef<HTMLInputElement>(null);
82
+
83
+ // State for confirmation dialogs
84
+ const [showResetInlineConfirm, setShowResetInlineConfirm] = useState(false);
85
+ const [showDeleteInlineConfirm, setShowDeleteInlineConfirm] = useState(false);
86
+ const [showSettingsSelection, setShowSettingsSelection] = useState(false);
87
+ const [showChatsSelection, setShowChatsSelection] = useState(false);
88
+
89
+ // State for settings categories and available chats
90
+ const [settingsCategories] = useState<SettingsCategory[]>([
91
+ { id: 'core', label: 'Core Settings', description: 'User profile and main settings' },
92
+ { id: 'providers', label: 'Providers', description: 'API keys and provider configurations' },
93
+ { id: 'features', label: 'Features', description: 'Feature flags and settings' },
94
+ { id: 'ui', label: 'UI', description: 'UI configuration and preferences' },
95
+ { id: 'connections', label: 'Connections', description: 'External service connections' },
96
+ { id: 'debug', label: 'Debug', description: 'Debug settings and logs' },
97
+ { id: 'updates', label: 'Updates', description: 'Update settings and notifications' },
98
+ ]);
99
+
100
+ const [availableChats, setAvailableChats] = useState<ExtendedChat[]>([]);
101
+ const [chatItems, setChatItems] = useState<ChatItem[]>([]);
102
+
103
+ // Data operations hook with boltHistory database
104
+ const {
105
+ isExporting,
106
+ isImporting,
107
+ isResetting,
108
+ isDownloadingTemplate,
109
+ handleExportSettings,
110
+ handleExportSelectedSettings,
111
+ handleExportAllChats,
112
+ handleExportSelectedChats,
113
+ handleImportSettings,
114
+ handleImportChats,
115
+ handleResetSettings,
116
+ handleResetChats,
117
+ handleDownloadTemplate,
118
+ handleImportAPIKeys,
119
+ } = useDataOperations({
120
+ customDb: db || undefined, // Pass the boltHistory database, converting null to undefined
121
+ onReloadSettings: () => window.location.reload(),
122
+ onReloadChats: () => {
123
+ // Reload chats after reset
124
+ if (db) {
125
+ getAllChats(db).then((chats) => {
126
+ // Cast to ExtendedChat to handle additional properties
127
+ const extendedChats = chats as ExtendedChat[];
128
+ setAvailableChats(extendedChats);
129
+ setChatItems(extendedChats.map((chat) => createChatItem(chat)));
130
+ });
131
+ }
132
+ },
133
+ onResetSettings: () => setShowResetInlineConfirm(false),
134
+ onResetChats: () => setShowDeleteInlineConfirm(false),
135
+ });
136
+
137
+ // Loading states for operations not provided by the hook
138
+ const [isDeleting, setIsDeleting] = useState(false);
139
+ const [isImportingKeys, setIsImportingKeys] = useState(false);
140
+
141
+ // Load available chats
142
+ useEffect(() => {
143
+ if (db) {
144
+ console.log('Loading chats from boltHistory database', {
145
+ name: db.name,
146
+ version: db.version,
147
+ objectStoreNames: Array.from(db.objectStoreNames),
148
+ });
149
+
150
+ getAllChats(db)
151
+ .then((chats) => {
152
+ console.log('Found chats:', chats.length);
153
+
154
+ // Cast to ExtendedChat to handle additional properties
155
+ const extendedChats = chats as ExtendedChat[];
156
+ setAvailableChats(extendedChats);
157
+
158
+ // Create ChatItems for selection dialog
159
+ setChatItems(extendedChats.map((chat) => createChatItem(chat)));
160
+ })
161
+ .catch((error) => {
162
+ console.error('Error loading chats:', error);
163
+ toast.error('Failed to load chats: ' + (error instanceof Error ? error.message : 'Unknown error'));
164
+ });
165
+ }
166
+ }, [db]);
167
+
168
+ // Handle file input changes
169
+ const handleFileInputChange = useCallback(
170
+ (event: React.ChangeEvent<HTMLInputElement>) => {
171
+ const file = event.target.files?.[0];
172
+
173
+ if (file) {
174
+ handleImportSettings(file);
175
+ }
176
+ },
177
+ [handleImportSettings],
178
+ );
179
+
180
+ const handleAPIKeyFileInputChange = useCallback(
181
+ (event: React.ChangeEvent<HTMLInputElement>) => {
182
+ const file = event.target.files?.[0];
183
+
184
+ if (file) {
185
+ setIsImportingKeys(true);
186
+ handleImportAPIKeys(file).finally(() => setIsImportingKeys(false));
187
+ }
188
+ },
189
+ [handleImportAPIKeys],
190
+ );
191
+
192
+ const handleChatFileInputChange = useCallback(
193
+ (event: React.ChangeEvent<HTMLInputElement>) => {
194
+ const file = event.target.files?.[0];
195
+
196
+ if (file) {
197
+ handleImportChats(file);
198
+ }
199
+ },
200
+ [handleImportChats],
201
+ );
202
+
203
+ // Wrapper for reset chats to handle loading state
204
+ const handleResetChatsWithState = useCallback(() => {
205
+ setIsDeleting(true);
206
+ handleResetChats().finally(() => setIsDeleting(false));
207
+ }, [handleResetChats]);
208
+
209
+ return (
210
+ <div className="space-y-12">
211
+ {/* Hidden file inputs */}
212
+ <input ref={fileInputRef} type="file" accept=".json" onChange={handleFileInputChange} className="hidden" />
213
+ <input
214
+ ref={apiKeyFileInputRef}
215
+ type="file"
216
+ accept=".json"
217
+ onChange={handleAPIKeyFileInputChange}
218
+ className="hidden"
219
+ />
220
+ <input
221
+ ref={chatFileInputRef}
222
+ type="file"
223
+ accept=".json"
224
+ onChange={handleChatFileInputChange}
225
+ className="hidden"
226
+ />
227
+
228
+ {/* Reset Settings Confirmation Dialog */}
229
+ <ConfirmationDialog
230
+ isOpen={showResetInlineConfirm}
231
+ onClose={() => setShowResetInlineConfirm(false)}
232
+ title="Reset All Settings?"
233
+ description="This will reset all your settings to their default values. This action cannot be undone."
234
+ confirmLabel="Reset Settings"
235
+ cancelLabel="Cancel"
236
+ variant="destructive"
237
+ isLoading={isResetting}
238
+ onConfirm={handleResetSettings}
239
+ />
240
+
241
+ {/* Delete Chats Confirmation Dialog */}
242
+ <ConfirmationDialog
243
+ isOpen={showDeleteInlineConfirm}
244
+ onClose={() => setShowDeleteInlineConfirm(false)}
245
+ title="Delete All Chats?"
246
+ description="This will permanently delete all your chat history. This action cannot be undone."
247
+ confirmLabel="Delete All"
248
+ cancelLabel="Cancel"
249
+ variant="destructive"
250
+ isLoading={isDeleting}
251
+ onConfirm={handleResetChatsWithState}
252
+ />
253
+
254
+ {/* Settings Selection Dialog */}
255
+ <SelectionDialog
256
+ isOpen={showSettingsSelection}
257
+ onClose={() => setShowSettingsSelection(false)}
258
+ title="Select Settings to Export"
259
+ items={settingsCategories}
260
+ onConfirm={(selectedIds) => {
261
+ handleExportSelectedSettings(selectedIds);
262
+ setShowSettingsSelection(false);
263
+ }}
264
+ confirmLabel="Export Selected"
265
+ />
266
+
267
+ {/* Chats Selection Dialog */}
268
+ <SelectionDialog
269
+ isOpen={showChatsSelection}
270
+ onClose={() => setShowChatsSelection(false)}
271
+ title="Select Chats to Export"
272
+ items={chatItems}
273
+ onConfirm={(selectedIds) => {
274
+ handleExportSelectedChats(selectedIds);
275
+ setShowChatsSelection(false);
276
+ }}
277
+ confirmLabel="Export Selected"
278
+ />
279
+
280
+ {/* Chats Section */}
281
+ <div>
282
+ <h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">Chats</h2>
283
+ {dbLoading ? (
284
+ <div className="flex items-center justify-center p-4">
285
+ <div className="i-ph-spinner-gap-bold animate-spin w-6 h-6 mr-2" />
286
+ <span>Loading chats database...</span>
287
+ </div>
288
+ ) : (
289
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
290
+ <Card>
291
+ <CardHeader>
292
+ <div className="flex items-center mb-2">
293
+ <motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
294
+ <div className="i-ph-download-duotone w-5 h-5" />
295
+ </motion.div>
296
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
297
+ Export All Chats
298
+ </CardTitle>
299
+ </div>
300
+ <CardDescription>Export all your chats to a JSON file.</CardDescription>
301
+ </CardHeader>
302
+ <CardFooter>
303
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
304
+ <Button
305
+ onClick={async () => {
306
+ try {
307
+ if (!db) {
308
+ toast.error('Database not available');
309
+ return;
310
+ }
311
+
312
+ console.log('Database information:', {
313
+ name: db.name,
314
+ version: db.version,
315
+ objectStoreNames: Array.from(db.objectStoreNames),
316
+ });
317
+
318
+ if (availableChats.length === 0) {
319
+ toast.warning('No chats available to export');
320
+ return;
321
+ }
322
+
323
+ await handleExportAllChats();
324
+ } catch (error) {
325
+ console.error('Error exporting chats:', error);
326
+ toast.error(
327
+ `Failed to export chats: ${error instanceof Error ? error.message : 'Unknown error'}`,
328
+ );
329
+ }
330
+ }}
331
+ disabled={isExporting || availableChats.length === 0}
332
+ variant="outline"
333
+ size="sm"
334
+ className={classNames(
335
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
336
+ isExporting || availableChats.length === 0 ? 'cursor-not-allowed' : '',
337
+ )}
338
+ >
339
+ {isExporting ? (
340
+ <>
341
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
342
+ Exporting...
343
+ </>
344
+ ) : availableChats.length === 0 ? (
345
+ 'No Chats to Export'
346
+ ) : (
347
+ 'Export All'
348
+ )}
349
+ </Button>
350
+ </motion.div>
351
+ </CardFooter>
352
+ </Card>
353
+
354
+ <Card>
355
+ <CardHeader>
356
+ <div className="flex items-center mb-2">
357
+ <motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
358
+ <div className="i-ph:list-checks w-5 h-5" />
359
+ </motion.div>
360
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
361
+ Export Selected Chats
362
+ </CardTitle>
363
+ </div>
364
+ <CardDescription>Choose specific chats to export.</CardDescription>
365
+ </CardHeader>
366
+ <CardFooter>
367
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
368
+ <Button
369
+ onClick={() => setShowChatsSelection(true)}
370
+ disabled={isExporting || chatItems.length === 0}
371
+ variant="outline"
372
+ size="sm"
373
+ className={classNames(
374
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
375
+ isExporting || chatItems.length === 0 ? 'cursor-not-allowed' : '',
376
+ )}
377
+ >
378
+ {isExporting ? (
379
+ <>
380
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
381
+ Exporting...
382
+ </>
383
+ ) : (
384
+ 'Select Chats'
385
+ )}
386
+ </Button>
387
+ </motion.div>
388
+ </CardFooter>
389
+ </Card>
390
+
391
+ <Card>
392
+ <CardHeader>
393
+ <div className="flex items-center mb-2">
394
+ <motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
395
+ <div className="i-ph-upload-duotone w-5 h-5" />
396
+ </motion.div>
397
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
398
+ Import Chats
399
+ </CardTitle>
400
+ </div>
401
+ <CardDescription>Import chats from a JSON file.</CardDescription>
402
+ </CardHeader>
403
+ <CardFooter>
404
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
405
+ <Button
406
+ onClick={() => chatFileInputRef.current?.click()}
407
+ disabled={isImporting}
408
+ variant="outline"
409
+ size="sm"
410
+ className={classNames(
411
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
412
+ isImporting ? 'cursor-not-allowed' : '',
413
+ )}
414
+ >
415
+ {isImporting ? (
416
+ <>
417
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
418
+ Importing...
419
+ </>
420
+ ) : (
421
+ 'Import Chats'
422
+ )}
423
+ </Button>
424
+ </motion.div>
425
+ </CardFooter>
426
+ </Card>
427
+
428
+ <Card>
429
+ <CardHeader>
430
+ <div className="flex items-center mb-2">
431
+ <motion.div
432
+ className="text-red-500 dark:text-red-400 mr-2"
433
+ whileHover={{ scale: 1.1 }}
434
+ whileTap={{ scale: 0.9 }}
435
+ >
436
+ <div className="i-ph-trash-duotone w-5 h-5" />
437
+ </motion.div>
438
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
439
+ Delete All Chats
440
+ </CardTitle>
441
+ </div>
442
+ <CardDescription>Delete all your chat history.</CardDescription>
443
+ </CardHeader>
444
+ <CardFooter>
445
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
446
+ <Button
447
+ onClick={() => setShowDeleteInlineConfirm(true)}
448
+ disabled={isDeleting || chatItems.length === 0}
449
+ variant="outline"
450
+ size="sm"
451
+ className={classNames(
452
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
453
+ isDeleting || chatItems.length === 0 ? 'cursor-not-allowed' : '',
454
+ )}
455
+ >
456
+ {isDeleting ? (
457
+ <>
458
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
459
+ Deleting...
460
+ </>
461
+ ) : (
462
+ 'Delete All'
463
+ )}
464
+ </Button>
465
+ </motion.div>
466
+ </CardFooter>
467
+ </Card>
468
+ </div>
469
+ )}
470
+ </div>
471
+
472
+ {/* Settings Section */}
473
+ <div>
474
+ <h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">Settings</h2>
475
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
476
+ <Card>
477
+ <CardHeader>
478
+ <div className="flex items-center mb-2">
479
+ <motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
480
+ <div className="i-ph-download-duotone w-5 h-5" />
481
+ </motion.div>
482
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
483
+ Export All Settings
484
+ </CardTitle>
485
+ </div>
486
+ <CardDescription>Export all your settings to a JSON file.</CardDescription>
487
+ </CardHeader>
488
+ <CardFooter>
489
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
490
+ <Button
491
+ onClick={handleExportSettings}
492
+ disabled={isExporting}
493
+ variant="outline"
494
+ size="sm"
495
+ className={classNames(
496
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
497
+ isExporting ? 'cursor-not-allowed' : '',
498
+ )}
499
+ >
500
+ {isExporting ? (
501
+ <>
502
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
503
+ Exporting...
504
+ </>
505
+ ) : (
506
+ 'Export All'
507
+ )}
508
+ </Button>
509
+ </motion.div>
510
+ </CardFooter>
511
+ </Card>
512
+
513
+ <Card>
514
+ <CardHeader>
515
+ <div className="flex items-center mb-2">
516
+ <motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
517
+ <div className="i-ph-filter-duotone w-5 h-5" />
518
+ </motion.div>
519
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
520
+ Export Selected Settings
521
+ </CardTitle>
522
+ </div>
523
+ <CardDescription>Choose specific settings to export.</CardDescription>
524
+ </CardHeader>
525
+ <CardFooter>
526
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
527
+ <Button
528
+ onClick={() => setShowSettingsSelection(true)}
529
+ disabled={isExporting || settingsCategories.length === 0}
530
+ variant="outline"
531
+ size="sm"
532
+ className={classNames(
533
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
534
+ isExporting || settingsCategories.length === 0 ? 'cursor-not-allowed' : '',
535
+ )}
536
+ >
537
+ {isExporting ? (
538
+ <>
539
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
540
+ Exporting...
541
+ </>
542
+ ) : (
543
+ 'Select Settings'
544
+ )}
545
+ </Button>
546
+ </motion.div>
547
+ </CardFooter>
548
+ </Card>
549
+
550
+ <Card>
551
+ <CardHeader>
552
+ <div className="flex items-center mb-2">
553
+ <motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
554
+ <div className="i-ph-upload-duotone w-5 h-5" />
555
+ </motion.div>
556
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
557
+ Import Settings
558
+ </CardTitle>
559
+ </div>
560
+ <CardDescription>Import settings from a JSON file.</CardDescription>
561
+ </CardHeader>
562
+ <CardFooter>
563
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
564
+ <Button
565
+ onClick={() => fileInputRef.current?.click()}
566
+ disabled={isImporting}
567
+ variant="outline"
568
+ size="sm"
569
+ className={classNames(
570
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
571
+ isImporting ? 'cursor-not-allowed' : '',
572
+ )}
573
+ >
574
+ {isImporting ? (
575
+ <>
576
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
577
+ Importing...
578
+ </>
579
+ ) : (
580
+ 'Import Settings'
581
+ )}
582
+ </Button>
583
+ </motion.div>
584
+ </CardFooter>
585
+ </Card>
586
+
587
+ <Card>
588
+ <CardHeader>
589
+ <div className="flex items-center mb-2">
590
+ <motion.div
591
+ className="text-red-500 dark:text-red-400 mr-2"
592
+ whileHover={{ scale: 1.1 }}
593
+ whileTap={{ scale: 0.9 }}
594
+ >
595
+ <div className="i-ph-arrow-counter-clockwise-duotone w-5 h-5" />
596
+ </motion.div>
597
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
598
+ Reset All Settings
599
+ </CardTitle>
600
+ </div>
601
+ <CardDescription>Reset all settings to their default values.</CardDescription>
602
+ </CardHeader>
603
+ <CardFooter>
604
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
605
+ <Button
606
+ onClick={() => setShowResetInlineConfirm(true)}
607
+ disabled={isResetting}
608
+ variant="outline"
609
+ size="sm"
610
+ className={classNames(
611
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
612
+ isResetting ? 'cursor-not-allowed' : '',
613
+ )}
614
+ >
615
+ {isResetting ? (
616
+ <>
617
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
618
+ Resetting...
619
+ </>
620
+ ) : (
621
+ 'Reset All'
622
+ )}
623
+ </Button>
624
+ </motion.div>
625
+ </CardFooter>
626
+ </Card>
627
+ </div>
628
+ </div>
629
+
630
+ {/* API Keys Section */}
631
+ <div>
632
+ <h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">API Keys</h2>
633
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
634
+ <Card>
635
+ <CardHeader>
636
+ <div className="flex items-center mb-2">
637
+ <motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
638
+ <div className="i-ph-file-text-duotone w-5 h-5" />
639
+ </motion.div>
640
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
641
+ Download Template
642
+ </CardTitle>
643
+ </div>
644
+ <CardDescription>Download a template file for your API keys.</CardDescription>
645
+ </CardHeader>
646
+ <CardFooter>
647
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
648
+ <Button
649
+ onClick={handleDownloadTemplate}
650
+ disabled={isDownloadingTemplate}
651
+ variant="outline"
652
+ size="sm"
653
+ className={classNames(
654
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
655
+ isDownloadingTemplate ? 'cursor-not-allowed' : '',
656
+ )}
657
+ >
658
+ {isDownloadingTemplate ? (
659
+ <>
660
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
661
+ Downloading...
662
+ </>
663
+ ) : (
664
+ 'Download'
665
+ )}
666
+ </Button>
667
+ </motion.div>
668
+ </CardFooter>
669
+ </Card>
670
+
671
+ <Card>
672
+ <CardHeader>
673
+ <div className="flex items-center mb-2">
674
+ <motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
675
+ <div className="i-ph-upload-duotone w-5 h-5" />
676
+ </motion.div>
677
+ <CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
678
+ Import API Keys
679
+ </CardTitle>
680
+ </div>
681
+ <CardDescription>Import API keys from a JSON file.</CardDescription>
682
+ </CardHeader>
683
+ <CardFooter>
684
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
685
+ <Button
686
+ onClick={() => apiKeyFileInputRef.current?.click()}
687
+ disabled={isImportingKeys}
688
+ variant="outline"
689
+ size="sm"
690
+ className={classNames(
691
+ 'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
692
+ isImportingKeys ? 'cursor-not-allowed' : '',
693
+ )}
694
+ >
695
+ {isImportingKeys ? (
696
+ <>
697
+ <div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
698
+ Importing...
699
+ </>
700
+ ) : (
701
+ 'Import Keys'
702
+ )}
703
+ </Button>
704
+ </motion.div>
705
+ </CardFooter>
706
+ </Card>
707
+ </div>
708
+ </div>
709
+
710
+ {/* Data Visualization */}
711
+ <div>
712
+ <h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">Data Usage</h2>
713
+ <Card>
714
+ <CardContent className="p-5">
715
+ <DataVisualization chats={availableChats} />
716
+ </CardContent>
717
+ </Card>
718
+ </div>
719
+ </div>
720
+ );
721
+ }
app/components/@settings/tabs/data/DataVisualization.tsx ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ Chart as ChartJS,
4
+ CategoryScale,
5
+ LinearScale,
6
+ BarElement,
7
+ Title,
8
+ Tooltip,
9
+ Legend,
10
+ ArcElement,
11
+ PointElement,
12
+ LineElement,
13
+ } from 'chart.js';
14
+ import { Bar, Pie } from 'react-chartjs-2';
15
+ import type { Chat } from '~/lib/persistence/chats';
16
+ import { classNames } from '~/utils/classNames';
17
+
18
+ // Register ChartJS components
19
+ ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ArcElement, PointElement, LineElement);
20
+
21
+ type DataVisualizationProps = {
22
+ chats: Chat[];
23
+ };
24
+
25
+ export function DataVisualization({ chats }: DataVisualizationProps) {
26
+ const [chatsByDate, setChatsByDate] = useState<Record<string, number>>({});
27
+ const [messagesByRole, setMessagesByRole] = useState<Record<string, number>>({});
28
+ const [apiKeyUsage, setApiKeyUsage] = useState<Array<{ provider: string; count: number }>>([]);
29
+ const [averageMessagesPerChat, setAverageMessagesPerChat] = useState<number>(0);
30
+ const [isDarkMode, setIsDarkMode] = useState(false);
31
+
32
+ useEffect(() => {
33
+ const isDark = document.documentElement.classList.contains('dark');
34
+ setIsDarkMode(isDark);
35
+
36
+ const observer = new MutationObserver((mutations) => {
37
+ mutations.forEach((mutation) => {
38
+ if (mutation.attributeName === 'class') {
39
+ setIsDarkMode(document.documentElement.classList.contains('dark'));
40
+ }
41
+ });
42
+ });
43
+
44
+ observer.observe(document.documentElement, { attributes: true });
45
+
46
+ return () => observer.disconnect();
47
+ }, []);
48
+
49
+ useEffect(() => {
50
+ if (!chats || chats.length === 0) {
51
+ return;
52
+ }
53
+
54
+ // Process chat data
55
+ const chatDates: Record<string, number> = {};
56
+ const roleCounts: Record<string, number> = {};
57
+ const apiUsage: Record<string, number> = {};
58
+ let totalMessages = 0;
59
+
60
+ chats.forEach((chat) => {
61
+ const date = new Date(chat.timestamp).toLocaleDateString();
62
+ chatDates[date] = (chatDates[date] || 0) + 1;
63
+
64
+ chat.messages.forEach((message) => {
65
+ roleCounts[message.role] = (roleCounts[message.role] || 0) + 1;
66
+ totalMessages++;
67
+
68
+ if (message.role === 'assistant') {
69
+ const providerMatch = message.content.match(/provider:\s*([\w-]+)/i);
70
+ const provider = providerMatch ? providerMatch[1] : 'unknown';
71
+ apiUsage[provider] = (apiUsage[provider] || 0) + 1;
72
+ }
73
+ });
74
+ });
75
+
76
+ const sortedDates = Object.keys(chatDates).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
77
+ const sortedChatsByDate: Record<string, number> = {};
78
+ sortedDates.forEach((date) => {
79
+ sortedChatsByDate[date] = chatDates[date];
80
+ });
81
+
82
+ setChatsByDate(sortedChatsByDate);
83
+ setMessagesByRole(roleCounts);
84
+ setApiKeyUsage(Object.entries(apiUsage).map(([provider, count]) => ({ provider, count })));
85
+ setAverageMessagesPerChat(totalMessages / chats.length);
86
+ }, [chats]);
87
+
88
+ // Get theme colors from CSS variables to ensure theme consistency
89
+ const getThemeColor = (varName: string): string => {
90
+ // Get the CSS variable value from document root
91
+ if (typeof document !== 'undefined') {
92
+ return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
93
+ }
94
+
95
+ // Fallback for SSR
96
+ return isDarkMode ? '#FFFFFF' : '#000000';
97
+ };
98
+
99
+ // Theme-aware chart colors with enhanced dark mode visibility using CSS variables
100
+ const chartColors = {
101
+ grid: isDarkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)',
102
+ text: getThemeColor('--bolt-elements-textPrimary'),
103
+ textSecondary: getThemeColor('--bolt-elements-textSecondary'),
104
+ background: getThemeColor('--bolt-elements-bg-depth-1'),
105
+ accent: getThemeColor('--bolt-elements-button-primary-text'),
106
+ border: getThemeColor('--bolt-elements-borderColor'),
107
+ };
108
+
109
+ const getChartColors = (index: number) => {
110
+ // Define color palettes based on Bolt design tokens
111
+ const baseColors = [
112
+ // Indigo
113
+ {
114
+ base: getThemeColor('--bolt-elements-button-primary-text'),
115
+ },
116
+
117
+ // Pink
118
+ {
119
+ base: isDarkMode ? 'rgb(244, 114, 182)' : 'rgb(236, 72, 153)',
120
+ },
121
+
122
+ // Green
123
+ {
124
+ base: getThemeColor('--bolt-elements-icon-success'),
125
+ },
126
+
127
+ // Yellow
128
+ {
129
+ base: isDarkMode ? 'rgb(250, 204, 21)' : 'rgb(234, 179, 8)',
130
+ },
131
+
132
+ // Blue
133
+ {
134
+ base: isDarkMode ? 'rgb(56, 189, 248)' : 'rgb(14, 165, 233)',
135
+ },
136
+ ];
137
+
138
+ // Get the base color for this index
139
+ const color = baseColors[index % baseColors.length].base;
140
+
141
+ // Parse color and generate variations with appropriate opacity
142
+ let r = 0,
143
+ g = 0,
144
+ b = 0;
145
+
146
+ // Handle rgb/rgba format
147
+ const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
148
+ const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([0-9.]+)\)/);
149
+
150
+ if (rgbMatch) {
151
+ [, r, g, b] = rgbMatch.map(Number);
152
+ } else if (rgbaMatch) {
153
+ [, r, g, b] = rgbaMatch.map(Number);
154
+ } else if (color.startsWith('#')) {
155
+ // Handle hex format
156
+ const hex = color.slice(1);
157
+ const bigint = parseInt(hex, 16);
158
+ r = (bigint >> 16) & 255;
159
+ g = (bigint >> 8) & 255;
160
+ b = bigint & 255;
161
+ }
162
+
163
+ return {
164
+ bg: `rgba(${r}, ${g}, ${b}, ${isDarkMode ? 0.7 : 0.5})`,
165
+ border: `rgba(${r}, ${g}, ${b}, ${isDarkMode ? 0.9 : 0.8})`,
166
+ };
167
+ };
168
+
169
+ const chartData = {
170
+ history: {
171
+ labels: Object.keys(chatsByDate),
172
+ datasets: [
173
+ {
174
+ label: 'Chats Created',
175
+ data: Object.values(chatsByDate),
176
+ backgroundColor: getChartColors(0).bg,
177
+ borderColor: getChartColors(0).border,
178
+ borderWidth: 1,
179
+ },
180
+ ],
181
+ },
182
+ roles: {
183
+ labels: Object.keys(messagesByRole),
184
+ datasets: [
185
+ {
186
+ label: 'Messages by Role',
187
+ data: Object.values(messagesByRole),
188
+ backgroundColor: Object.keys(messagesByRole).map((_, i) => getChartColors(i).bg),
189
+ borderColor: Object.keys(messagesByRole).map((_, i) => getChartColors(i).border),
190
+ borderWidth: 1,
191
+ },
192
+ ],
193
+ },
194
+ apiUsage: {
195
+ labels: apiKeyUsage.map((item) => item.provider),
196
+ datasets: [
197
+ {
198
+ label: 'API Usage',
199
+ data: apiKeyUsage.map((item) => item.count),
200
+ backgroundColor: apiKeyUsage.map((_, i) => getChartColors(i).bg),
201
+ borderColor: apiKeyUsage.map((_, i) => getChartColors(i).border),
202
+ borderWidth: 1,
203
+ },
204
+ ],
205
+ },
206
+ };
207
+
208
+ const baseChartOptions = {
209
+ responsive: true,
210
+ maintainAspectRatio: false,
211
+ color: chartColors.text,
212
+ plugins: {
213
+ legend: {
214
+ position: 'top' as const,
215
+ labels: {
216
+ color: chartColors.text,
217
+ font: {
218
+ weight: 'bold' as const,
219
+ size: 12,
220
+ },
221
+ padding: 16,
222
+ usePointStyle: true,
223
+ },
224
+ },
225
+ title: {
226
+ display: true,
227
+ color: chartColors.text,
228
+ font: {
229
+ size: 16,
230
+ weight: 'bold' as const,
231
+ },
232
+ padding: 16,
233
+ },
234
+ tooltip: {
235
+ titleColor: chartColors.text,
236
+ bodyColor: chartColors.text,
237
+ backgroundColor: isDarkMode
238
+ ? 'rgba(23, 23, 23, 0.8)' // Dark bg using Tailwind gray-900
239
+ : 'rgba(255, 255, 255, 0.8)', // Light bg
240
+ borderColor: chartColors.border,
241
+ borderWidth: 1,
242
+ },
243
+ },
244
+ };
245
+
246
+ const chartOptions = {
247
+ ...baseChartOptions,
248
+ plugins: {
249
+ ...baseChartOptions.plugins,
250
+ title: {
251
+ ...baseChartOptions.plugins.title,
252
+ text: 'Chat History',
253
+ },
254
+ },
255
+ scales: {
256
+ x: {
257
+ grid: {
258
+ color: chartColors.grid,
259
+ drawBorder: false,
260
+ },
261
+ border: {
262
+ display: false,
263
+ },
264
+ ticks: {
265
+ color: chartColors.text,
266
+ font: {
267
+ weight: 500,
268
+ },
269
+ },
270
+ },
271
+ y: {
272
+ grid: {
273
+ color: chartColors.grid,
274
+ drawBorder: false,
275
+ },
276
+ border: {
277
+ display: false,
278
+ },
279
+ ticks: {
280
+ color: chartColors.text,
281
+ font: {
282
+ weight: 500,
283
+ },
284
+ },
285
+ },
286
+ },
287
+ };
288
+
289
+ const pieOptions = {
290
+ ...baseChartOptions,
291
+ plugins: {
292
+ ...baseChartOptions.plugins,
293
+ title: {
294
+ ...baseChartOptions.plugins.title,
295
+ text: 'Message Distribution',
296
+ },
297
+ legend: {
298
+ ...baseChartOptions.plugins.legend,
299
+ position: 'right' as const,
300
+ },
301
+ datalabels: {
302
+ color: chartColors.text,
303
+ font: {
304
+ weight: 'bold' as const,
305
+ },
306
+ },
307
+ },
308
+ };
309
+
310
+ if (chats.length === 0) {
311
+ return (
312
+ <div className="text-center py-8">
313
+ <div className="i-ph-chart-line-duotone w-12 h-12 mx-auto mb-4 text-bolt-elements-textTertiary opacity-80" />
314
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-2">No Data Available</h3>
315
+ <p className="text-bolt-elements-textSecondary">
316
+ Start creating chats to see your usage statistics and data visualization.
317
+ </p>
318
+ </div>
319
+ );
320
+ }
321
+
322
+ const cardClasses = classNames(
323
+ 'p-6 rounded-lg shadow-sm',
324
+ 'bg-bolt-elements-bg-depth-1',
325
+ 'border border-bolt-elements-borderColor',
326
+ );
327
+
328
+ const statClasses = classNames('text-3xl font-bold text-bolt-elements-textPrimary', 'flex items-center gap-3');
329
+
330
+ return (
331
+ <div className="space-y-8">
332
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
333
+ <div className={cardClasses}>
334
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Total Chats</h3>
335
+ <div className={statClasses}>
336
+ <div className="i-ph-chats-duotone w-8 h-8 text-indigo-500 dark:text-indigo-400" />
337
+ <span>{chats.length}</span>
338
+ </div>
339
+ </div>
340
+
341
+ <div className={cardClasses}>
342
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Total Messages</h3>
343
+ <div className={statClasses}>
344
+ <div className="i-ph-chat-text-duotone w-8 h-8 text-pink-500 dark:text-pink-400" />
345
+ <span>{Object.values(messagesByRole).reduce((sum, count) => sum + count, 0)}</span>
346
+ </div>
347
+ </div>
348
+
349
+ <div className={cardClasses}>
350
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Avg. Messages/Chat</h3>
351
+ <div className={statClasses}>
352
+ <div className="i-ph-chart-bar-duotone w-8 h-8 text-green-500 dark:text-green-400" />
353
+ <span>{averageMessagesPerChat.toFixed(1)}</span>
354
+ </div>
355
+ </div>
356
+ </div>
357
+
358
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
359
+ <div className={cardClasses}>
360
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-6">Chat History</h3>
361
+ <div className="h-64">
362
+ <Bar data={chartData.history} options={chartOptions} />
363
+ </div>
364
+ </div>
365
+
366
+ <div className={cardClasses}>
367
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-6">Message Distribution</h3>
368
+ <div className="h-64">
369
+ <Pie data={chartData.roles} options={pieOptions} />
370
+ </div>
371
+ </div>
372
+ </div>
373
+
374
+ {apiKeyUsage.length > 0 && (
375
+ <div className={cardClasses}>
376
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-6">API Usage by Provider</h3>
377
+ <div className="h-64">
378
+ <Pie data={chartData.apiUsage} options={pieOptions} />
379
+ </div>
380
+ </div>
381
+ )}
382
+ </div>
383
+ );
384
+ }
app/components/@settings/tabs/event-logs/EventLogsTab.tsx ADDED
@@ -0,0 +1,1013 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Switch } from '~/components/ui/Switch';
4
+ 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 { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
9
+ import { jsPDF } from 'jspdf';
10
+ import { toast } from 'react-toastify';
11
+
12
+ interface SelectOption {
13
+ value: string;
14
+ label: string;
15
+ icon?: string;
16
+ color?: string;
17
+ }
18
+
19
+ const logLevelOptions: SelectOption[] = [
20
+ {
21
+ value: 'all',
22
+ label: 'All Types',
23
+ icon: 'i-ph:funnel',
24
+ color: '#9333ea',
25
+ },
26
+ {
27
+ value: 'provider',
28
+ label: 'LLM',
29
+ icon: 'i-ph:robot',
30
+ color: '#10b981',
31
+ },
32
+ {
33
+ value: 'api',
34
+ label: 'API',
35
+ icon: 'i-ph:cloud',
36
+ color: '#3b82f6',
37
+ },
38
+ {
39
+ value: 'error',
40
+ label: 'Errors',
41
+ icon: 'i-ph:warning-circle',
42
+ color: '#ef4444',
43
+ },
44
+ {
45
+ value: 'warning',
46
+ label: 'Warnings',
47
+ icon: 'i-ph:warning',
48
+ color: '#f59e0b',
49
+ },
50
+ {
51
+ value: 'info',
52
+ label: 'Info',
53
+ icon: 'i-ph:info',
54
+ color: '#3b82f6',
55
+ },
56
+ {
57
+ value: 'debug',
58
+ label: 'Debug',
59
+ icon: 'i-ph:bug',
60
+ color: '#6b7280',
61
+ },
62
+ ];
63
+
64
+ interface LogEntryItemProps {
65
+ log: LogEntry;
66
+ isExpanded: boolean;
67
+ use24Hour: boolean;
68
+ showTimestamp: boolean;
69
+ }
70
+
71
+ const LogEntryItem = ({ log, isExpanded: forceExpanded, use24Hour, showTimestamp }: LogEntryItemProps) => {
72
+ const [localExpanded, setLocalExpanded] = useState(forceExpanded);
73
+
74
+ useEffect(() => {
75
+ setLocalExpanded(forceExpanded);
76
+ }, [forceExpanded]);
77
+
78
+ const timestamp = useMemo(() => {
79
+ const date = new Date(log.timestamp);
80
+ return date.toLocaleTimeString('en-US', { hour12: !use24Hour });
81
+ }, [log.timestamp, use24Hour]);
82
+
83
+ const style = useMemo(() => {
84
+ if (log.category === 'provider') {
85
+ return {
86
+ icon: 'i-ph:robot',
87
+ color: 'text-emerald-500 dark:text-emerald-400',
88
+ bg: 'hover:bg-emerald-500/10 dark:hover:bg-emerald-500/20',
89
+ badge: 'text-emerald-500 bg-emerald-50 dark:bg-emerald-500/10',
90
+ };
91
+ }
92
+
93
+ if (log.category === 'api') {
94
+ return {
95
+ icon: 'i-ph:cloud',
96
+ color: 'text-blue-500 dark:text-blue-400',
97
+ bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20',
98
+ badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10',
99
+ };
100
+ }
101
+
102
+ switch (log.level) {
103
+ case 'error':
104
+ return {
105
+ icon: 'i-ph:warning-circle',
106
+ color: 'text-red-500 dark:text-red-400',
107
+ bg: 'hover:bg-red-500/10 dark:hover:bg-red-500/20',
108
+ badge: 'text-red-500 bg-red-50 dark:bg-red-500/10',
109
+ };
110
+ case 'warning':
111
+ return {
112
+ icon: 'i-ph:warning',
113
+ color: 'text-yellow-500 dark:text-yellow-400',
114
+ bg: 'hover:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
115
+ badge: 'text-yellow-500 bg-yellow-50 dark:bg-yellow-500/10',
116
+ };
117
+ case 'debug':
118
+ return {
119
+ icon: 'i-ph:bug',
120
+ color: 'text-gray-500 dark:text-gray-400',
121
+ bg: 'hover:bg-gray-500/10 dark:hover:bg-gray-500/20',
122
+ badge: 'text-gray-500 bg-gray-50 dark:bg-gray-500/10',
123
+ };
124
+ default:
125
+ return {
126
+ icon: 'i-ph:info',
127
+ color: 'text-blue-500 dark:text-blue-400',
128
+ bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20',
129
+ badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10',
130
+ };
131
+ }
132
+ }, [log.level, log.category]);
133
+
134
+ const renderDetails = (details: any) => {
135
+ if (log.category === 'provider') {
136
+ return (
137
+ <div className="flex flex-col gap-2">
138
+ <div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
139
+ <span>Model: {details.model}</span>
140
+ <span>•</span>
141
+ <span>Tokens: {details.totalTokens}</span>
142
+ <span>•</span>
143
+ <span>Duration: {details.duration}ms</span>
144
+ </div>
145
+ {details.prompt && (
146
+ <div className="flex flex-col gap-1">
147
+ <div className="text-xs font-medium text-gray-700 dark:text-gray-300">Prompt:</div>
148
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
149
+ {details.prompt}
150
+ </pre>
151
+ </div>
152
+ )}
153
+ {details.response && (
154
+ <div className="flex flex-col gap-1">
155
+ <div className="text-xs font-medium text-gray-700 dark:text-gray-300">Response:</div>
156
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
157
+ {details.response}
158
+ </pre>
159
+ </div>
160
+ )}
161
+ </div>
162
+ );
163
+ }
164
+
165
+ if (log.category === 'api') {
166
+ return (
167
+ <div className="flex flex-col gap-2">
168
+ <div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
169
+ <span className={details.method === 'GET' ? 'text-green-500' : 'text-blue-500'}>{details.method}</span>
170
+ <span>•</span>
171
+ <span>Status: {details.statusCode}</span>
172
+ <span>•</span>
173
+ <span>Duration: {details.duration}ms</span>
174
+ </div>
175
+ <div className="text-xs text-gray-600 dark:text-gray-400 break-all">{details.url}</div>
176
+ {details.request && (
177
+ <div className="flex flex-col gap-1">
178
+ <div className="text-xs font-medium text-gray-700 dark:text-gray-300">Request:</div>
179
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
180
+ {JSON.stringify(details.request, null, 2)}
181
+ </pre>
182
+ </div>
183
+ )}
184
+ {details.response && (
185
+ <div className="flex flex-col gap-1">
186
+ <div className="text-xs font-medium text-gray-700 dark:text-gray-300">Response:</div>
187
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
188
+ {JSON.stringify(details.response, null, 2)}
189
+ </pre>
190
+ </div>
191
+ )}
192
+ {details.error && (
193
+ <div className="flex flex-col gap-1">
194
+ <div className="text-xs font-medium text-red-500">Error:</div>
195
+ <pre className="text-xs text-red-400 bg-red-50 dark:bg-red-500/10 rounded p-2 whitespace-pre-wrap">
196
+ {JSON.stringify(details.error, null, 2)}
197
+ </pre>
198
+ </div>
199
+ )}
200
+ </div>
201
+ );
202
+ }
203
+
204
+ return (
205
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded whitespace-pre-wrap">
206
+ {JSON.stringify(details, null, 2)}
207
+ </pre>
208
+ );
209
+ };
210
+
211
+ return (
212
+ <motion.div
213
+ initial={{ opacity: 0, y: 20 }}
214
+ animate={{ opacity: 1, y: 0 }}
215
+ className={classNames(
216
+ 'flex flex-col gap-2',
217
+ 'rounded-lg p-4',
218
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
219
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
220
+ style.bg,
221
+ 'transition-all duration-200',
222
+ )}
223
+ >
224
+ <div className="flex items-start justify-between gap-4">
225
+ <div className="flex items-start gap-3">
226
+ <span className={classNames('text-lg', style.icon, style.color)} />
227
+ <div className="flex flex-col gap-1">
228
+ <div className="text-sm font-medium text-gray-900 dark:text-white">{log.message}</div>
229
+ {log.details && (
230
+ <>
231
+ <button
232
+ onClick={() => setLocalExpanded(!localExpanded)}
233
+ className="text-xs text-gray-500 dark:text-gray-400 hover:text-purple-500 dark:hover:text-purple-400 transition-colors"
234
+ >
235
+ {localExpanded ? 'Hide' : 'Show'} Details
236
+ </button>
237
+ {localExpanded && renderDetails(log.details)}
238
+ </>
239
+ )}
240
+ <div className="flex items-center gap-2">
241
+ <div className={classNames('px-2 py-0.5 rounded text-xs font-medium uppercase', style.badge)}>
242
+ {log.level}
243
+ </div>
244
+ {log.category && (
245
+ <div className="px-2 py-0.5 rounded-full text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
246
+ {log.category}
247
+ </div>
248
+ )}
249
+ </div>
250
+ </div>
251
+ </div>
252
+ {showTimestamp && <time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">{timestamp}</time>}
253
+ </div>
254
+ </motion.div>
255
+ );
256
+ };
257
+
258
+ interface ExportFormat {
259
+ id: string;
260
+ label: string;
261
+ icon: string;
262
+ handler: () => void;
263
+ }
264
+
265
+ export function EventLogsTab() {
266
+ const logs = useStore(logStore.logs);
267
+ const [selectedLevel, setSelectedLevel] = useState<'all' | string>('all');
268
+ const [searchQuery, setSearchQuery] = useState('');
269
+ const [use24Hour, setUse24Hour] = useState(false);
270
+ const [autoExpand, setAutoExpand] = useState(false);
271
+ const [showTimestamps, setShowTimestamps] = useState(true);
272
+ const [showLevelFilter, setShowLevelFilter] = useState(false);
273
+ const [isRefreshing, setIsRefreshing] = useState(false);
274
+ const levelFilterRef = useRef<HTMLDivElement>(null);
275
+
276
+ const filteredLogs = useMemo(() => {
277
+ const allLogs = Object.values(logs);
278
+
279
+ if (selectedLevel === 'all') {
280
+ return allLogs.filter((log) =>
281
+ searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true,
282
+ );
283
+ }
284
+
285
+ return allLogs.filter((log) => {
286
+ const matchesType = log.category === selectedLevel || log.level === selectedLevel;
287
+ const matchesSearch = searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true;
288
+
289
+ return matchesType && matchesSearch;
290
+ });
291
+ }, [logs, selectedLevel, searchQuery]);
292
+
293
+ // Add performance tracking on mount
294
+ useEffect(() => {
295
+ const startTime = performance.now();
296
+
297
+ logStore.logInfo('Event Logs tab mounted', {
298
+ type: 'component_mount',
299
+ message: 'Event Logs tab component mounted',
300
+ component: 'EventLogsTab',
301
+ });
302
+
303
+ return () => {
304
+ const duration = performance.now() - startTime;
305
+ logStore.logPerformanceMetric('EventLogsTab', 'mount-duration', duration);
306
+ };
307
+ }, []);
308
+
309
+ // Log filter changes
310
+ const handleLevelFilterChange = useCallback(
311
+ (newLevel: string) => {
312
+ logStore.logInfo('Log level filter changed', {
313
+ type: 'filter_change',
314
+ message: `Log level filter changed from ${selectedLevel} to ${newLevel}`,
315
+ component: 'EventLogsTab',
316
+ previousLevel: selectedLevel,
317
+ newLevel,
318
+ });
319
+ setSelectedLevel(newLevel as string);
320
+ setShowLevelFilter(false);
321
+ },
322
+ [selectedLevel],
323
+ );
324
+
325
+ // Log search changes with debounce
326
+ useEffect(() => {
327
+ const timeoutId = setTimeout(() => {
328
+ if (searchQuery) {
329
+ logStore.logInfo('Log search performed', {
330
+ type: 'search',
331
+ message: `Search performed with query "${searchQuery}" (${filteredLogs.length} results)`,
332
+ component: 'EventLogsTab',
333
+ query: searchQuery,
334
+ resultsCount: filteredLogs.length,
335
+ });
336
+ }
337
+ }, 1000);
338
+
339
+ return () => clearTimeout(timeoutId);
340
+ }, [searchQuery, filteredLogs.length]);
341
+
342
+ // Enhanced refresh handler
343
+ const handleRefresh = useCallback(async () => {
344
+ const startTime = performance.now();
345
+ setIsRefreshing(true);
346
+
347
+ try {
348
+ await logStore.refreshLogs();
349
+
350
+ const duration = performance.now() - startTime;
351
+
352
+ logStore.logSuccess('Logs refreshed successfully', {
353
+ type: 'refresh',
354
+ message: `Successfully refreshed ${Object.keys(logs).length} logs`,
355
+ component: 'EventLogsTab',
356
+ duration,
357
+ logsCount: Object.keys(logs).length,
358
+ });
359
+ } catch (error) {
360
+ logStore.logError('Failed to refresh logs', error, {
361
+ type: 'refresh_error',
362
+ message: 'Failed to refresh logs',
363
+ component: 'EventLogsTab',
364
+ });
365
+ } finally {
366
+ setTimeout(() => setIsRefreshing(false), 500);
367
+ }
368
+ }, [logs]);
369
+
370
+ // Log preference changes
371
+ const handlePreferenceChange = useCallback((type: string, value: boolean) => {
372
+ logStore.logInfo('Log preference changed', {
373
+ type: 'preference_change',
374
+ message: `Log preference "${type}" changed to ${value}`,
375
+ component: 'EventLogsTab',
376
+ preference: type,
377
+ value,
378
+ });
379
+
380
+ switch (type) {
381
+ case 'timestamps':
382
+ setShowTimestamps(value);
383
+ break;
384
+ case '24hour':
385
+ setUse24Hour(value);
386
+ break;
387
+ case 'autoExpand':
388
+ setAutoExpand(value);
389
+ break;
390
+ }
391
+ }, []);
392
+
393
+ // Close filters when clicking outside
394
+ useEffect(() => {
395
+ const handleClickOutside = (event: MouseEvent) => {
396
+ if (levelFilterRef.current && !levelFilterRef.current.contains(event.target as Node)) {
397
+ setShowLevelFilter(false);
398
+ }
399
+ };
400
+
401
+ document.addEventListener('mousedown', handleClickOutside);
402
+
403
+ return () => {
404
+ document.removeEventListener('mousedown', handleClickOutside);
405
+ };
406
+ }, []);
407
+
408
+ const selectedLevelOption = logLevelOptions.find((opt) => opt.value === selectedLevel);
409
+
410
+ // Export functions
411
+ const exportAsJSON = () => {
412
+ try {
413
+ const exportData = {
414
+ timestamp: new Date().toISOString(),
415
+ logs: filteredLogs,
416
+ filters: {
417
+ level: selectedLevel,
418
+ searchQuery,
419
+ },
420
+ preferences: {
421
+ use24Hour,
422
+ showTimestamps,
423
+ autoExpand,
424
+ },
425
+ };
426
+
427
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
428
+ const url = window.URL.createObjectURL(blob);
429
+ const a = document.createElement('a');
430
+ a.href = url;
431
+ a.download = `bolt-event-logs-${new Date().toISOString()}.json`;
432
+ document.body.appendChild(a);
433
+ a.click();
434
+ window.URL.revokeObjectURL(url);
435
+ document.body.removeChild(a);
436
+ toast.success('Event logs exported successfully as JSON');
437
+ } catch (error) {
438
+ console.error('Failed to export JSON:', error);
439
+ toast.error('Failed to export event logs as JSON');
440
+ }
441
+ };
442
+
443
+ const exportAsCSV = () => {
444
+ try {
445
+ // Convert logs to CSV format
446
+ const headers = ['Timestamp', 'Level', 'Category', 'Message', 'Details'];
447
+ const csvData = [
448
+ headers,
449
+ ...filteredLogs.map((log) => [
450
+ new Date(log.timestamp).toISOString(),
451
+ log.level,
452
+ log.category || '',
453
+ log.message,
454
+ log.details ? JSON.stringify(log.details) : '',
455
+ ]),
456
+ ];
457
+
458
+ const csvContent = csvData
459
+ .map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
460
+ .join('\n');
461
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
462
+ const url = window.URL.createObjectURL(blob);
463
+ const a = document.createElement('a');
464
+ a.href = url;
465
+ a.download = `bolt-event-logs-${new Date().toISOString()}.csv`;
466
+ document.body.appendChild(a);
467
+ a.click();
468
+ window.URL.revokeObjectURL(url);
469
+ document.body.removeChild(a);
470
+ toast.success('Event logs exported successfully as CSV');
471
+ } catch (error) {
472
+ console.error('Failed to export CSV:', error);
473
+ toast.error('Failed to export event logs as CSV');
474
+ }
475
+ };
476
+
477
+ const exportAsPDF = () => {
478
+ try {
479
+ // Create new PDF document
480
+ const doc = new jsPDF();
481
+ const lineHeight = 7;
482
+ let yPos = 20;
483
+ const margin = 20;
484
+ const pageWidth = doc.internal.pageSize.getWidth();
485
+ const maxLineWidth = pageWidth - 2 * margin;
486
+
487
+ // Helper function to add section header
488
+ const addSectionHeader = (title: string) => {
489
+ // Check if we need a new page
490
+ if (yPos > doc.internal.pageSize.getHeight() - 30) {
491
+ doc.addPage();
492
+ yPos = margin;
493
+ }
494
+
495
+ doc.setFillColor('#F3F4F6');
496
+ doc.rect(margin - 2, yPos - 5, pageWidth - 2 * (margin - 2), lineHeight + 6, 'F');
497
+ doc.setFont('helvetica', 'bold');
498
+ doc.setTextColor('#111827');
499
+ doc.setFontSize(12);
500
+ doc.text(title.toUpperCase(), margin, yPos);
501
+ yPos += lineHeight * 2;
502
+ };
503
+
504
+ // Add title and header
505
+ doc.setFillColor('#6366F1');
506
+ doc.rect(0, 0, pageWidth, 50, 'F');
507
+ doc.setTextColor('#FFFFFF');
508
+ doc.setFontSize(24);
509
+ doc.setFont('helvetica', 'bold');
510
+ doc.text('Event Logs Report', margin, 35);
511
+
512
+ // Add subtitle with bolt.diy
513
+ doc.setFontSize(12);
514
+ doc.setFont('helvetica', 'normal');
515
+ doc.text('bolt.diy - AI Development Platform', margin, 45);
516
+ yPos = 70;
517
+
518
+ // Add report summary section
519
+ addSectionHeader('Report Summary');
520
+
521
+ doc.setFontSize(10);
522
+ doc.setFont('helvetica', 'normal');
523
+ doc.setTextColor('#374151');
524
+
525
+ const summaryItems = [
526
+ { label: 'Generated', value: new Date().toLocaleString() },
527
+ { label: 'Total Logs', value: filteredLogs.length.toString() },
528
+ { label: 'Filter Applied', value: selectedLevel === 'all' ? 'All Types' : selectedLevel },
529
+ { label: 'Search Query', value: searchQuery || 'None' },
530
+ { label: 'Time Format', value: use24Hour ? '24-hour' : '12-hour' },
531
+ ];
532
+
533
+ summaryItems.forEach((item) => {
534
+ doc.setFont('helvetica', 'bold');
535
+ doc.text(`${item.label}:`, margin, yPos);
536
+ doc.setFont('helvetica', 'normal');
537
+ doc.text(item.value, margin + 60, yPos);
538
+ yPos += lineHeight;
539
+ });
540
+
541
+ yPos += lineHeight * 2;
542
+
543
+ // Add statistics section
544
+ addSectionHeader('Log Statistics');
545
+
546
+ // Calculate statistics
547
+ const stats = {
548
+ error: filteredLogs.filter((log) => log.level === 'error').length,
549
+ warning: filteredLogs.filter((log) => log.level === 'warning').length,
550
+ info: filteredLogs.filter((log) => log.level === 'info').length,
551
+ debug: filteredLogs.filter((log) => log.level === 'debug').length,
552
+ provider: filteredLogs.filter((log) => log.category === 'provider').length,
553
+ api: filteredLogs.filter((log) => log.category === 'api').length,
554
+ };
555
+
556
+ // Create two columns for statistics
557
+ const leftStats = [
558
+ { label: 'Error Logs', value: stats.error, color: '#DC2626' },
559
+ { label: 'Warning Logs', value: stats.warning, color: '#F59E0B' },
560
+ { label: 'Info Logs', value: stats.info, color: '#3B82F6' },
561
+ ];
562
+
563
+ const rightStats = [
564
+ { label: 'Debug Logs', value: stats.debug, color: '#6B7280' },
565
+ { label: 'LLM Logs', value: stats.provider, color: '#10B981' },
566
+ { label: 'API Logs', value: stats.api, color: '#3B82F6' },
567
+ ];
568
+
569
+ const colWidth = (pageWidth - 2 * margin) / 2;
570
+
571
+ // Draw statistics in two columns
572
+ leftStats.forEach((stat, index) => {
573
+ doc.setTextColor(stat.color);
574
+ doc.setFont('helvetica', 'bold');
575
+ doc.text(stat.value.toString(), margin, yPos);
576
+ doc.setTextColor('#374151');
577
+ doc.setFont('helvetica', 'normal');
578
+ doc.text(stat.label, margin + 20, yPos);
579
+
580
+ if (rightStats[index]) {
581
+ doc.setTextColor(rightStats[index].color);
582
+ doc.setFont('helvetica', 'bold');
583
+ doc.text(rightStats[index].value.toString(), margin + colWidth, yPos);
584
+ doc.setTextColor('#374151');
585
+ doc.setFont('helvetica', 'normal');
586
+ doc.text(rightStats[index].label, margin + colWidth + 20, yPos);
587
+ }
588
+
589
+ yPos += lineHeight;
590
+ });
591
+
592
+ yPos += lineHeight * 2;
593
+
594
+ // Add logs section
595
+ addSectionHeader('Event Logs');
596
+
597
+ // Helper function to add a log entry with improved formatting
598
+ const addLogEntry = (log: LogEntry) => {
599
+ const entryHeight = 20 + (log.details ? 40 : 0); // Estimate entry height
600
+
601
+ // Check if we need a new page
602
+ if (yPos + entryHeight > doc.internal.pageSize.getHeight() - 20) {
603
+ doc.addPage();
604
+ yPos = margin;
605
+ }
606
+
607
+ // Add timestamp and level
608
+ const timestamp = new Date(log.timestamp).toLocaleString(undefined, {
609
+ year: 'numeric',
610
+ month: '2-digit',
611
+ day: '2-digit',
612
+ hour: '2-digit',
613
+ minute: '2-digit',
614
+ second: '2-digit',
615
+ hour12: !use24Hour,
616
+ });
617
+
618
+ // Draw log level badge background
619
+ const levelColors: Record<string, string> = {
620
+ error: '#FEE2E2',
621
+ warning: '#FEF3C7',
622
+ info: '#DBEAFE',
623
+ debug: '#F3F4F6',
624
+ };
625
+
626
+ const textColors: Record<string, string> = {
627
+ error: '#DC2626',
628
+ warning: '#F59E0B',
629
+ info: '#3B82F6',
630
+ debug: '#6B7280',
631
+ };
632
+
633
+ const levelWidth = doc.getTextWidth(log.level.toUpperCase()) + 10;
634
+ doc.setFillColor(levelColors[log.level] || '#F3F4F6');
635
+ doc.roundedRect(margin, yPos - 4, levelWidth, lineHeight + 4, 1, 1, 'F');
636
+
637
+ // Add log level text
638
+ doc.setTextColor(textColors[log.level] || '#6B7280');
639
+ doc.setFont('helvetica', 'bold');
640
+ doc.setFontSize(8);
641
+ doc.text(log.level.toUpperCase(), margin + 5, yPos);
642
+
643
+ // Add timestamp
644
+ doc.setTextColor('#6B7280');
645
+ doc.setFont('helvetica', 'normal');
646
+ doc.setFontSize(9);
647
+ doc.text(timestamp, margin + levelWidth + 10, yPos);
648
+
649
+ // Add category if present
650
+ if (log.category) {
651
+ const categoryX = margin + levelWidth + doc.getTextWidth(timestamp) + 20;
652
+ doc.setFillColor('#F3F4F6');
653
+
654
+ const categoryWidth = doc.getTextWidth(log.category) + 10;
655
+ doc.roundedRect(categoryX, yPos - 4, categoryWidth, lineHeight + 4, 2, 2, 'F');
656
+ doc.setTextColor('#6B7280');
657
+ doc.text(log.category, categoryX + 5, yPos);
658
+ }
659
+
660
+ yPos += lineHeight * 1.5;
661
+
662
+ // Add message
663
+ doc.setTextColor('#111827');
664
+ doc.setFontSize(10);
665
+
666
+ const messageLines = doc.splitTextToSize(log.message, maxLineWidth - 10);
667
+ doc.text(messageLines, margin + 5, yPos);
668
+ yPos += messageLines.length * lineHeight;
669
+
670
+ // Add details if present
671
+ if (log.details) {
672
+ doc.setTextColor('#6B7280');
673
+ doc.setFontSize(8);
674
+
675
+ const detailsStr = JSON.stringify(log.details, null, 2);
676
+ const detailsLines = doc.splitTextToSize(detailsStr, maxLineWidth - 15);
677
+
678
+ // Add details background
679
+ doc.setFillColor('#F9FAFB');
680
+ doc.roundedRect(margin + 5, yPos - 2, maxLineWidth - 10, detailsLines.length * lineHeight + 8, 1, 1, 'F');
681
+
682
+ doc.text(detailsLines, margin + 10, yPos + 4);
683
+ yPos += detailsLines.length * lineHeight + 10;
684
+ }
685
+
686
+ // Add separator line
687
+ doc.setDrawColor('#E5E7EB');
688
+ doc.setLineWidth(0.1);
689
+ doc.line(margin, yPos, pageWidth - margin, yPos);
690
+ yPos += lineHeight * 1.5;
691
+ };
692
+
693
+ // Add all logs
694
+ filteredLogs.forEach((log) => {
695
+ addLogEntry(log);
696
+ });
697
+
698
+ // Add footer to all pages
699
+ const totalPages = doc.internal.pages.length - 1;
700
+
701
+ for (let i = 1; i <= totalPages; i++) {
702
+ doc.setPage(i);
703
+ doc.setFontSize(8);
704
+ doc.setTextColor('#9CA3AF');
705
+
706
+ // Add page numbers
707
+ doc.text(`Page ${i} of ${totalPages}`, pageWidth / 2, doc.internal.pageSize.getHeight() - 10, {
708
+ align: 'center',
709
+ });
710
+
711
+ // Add footer text
712
+ doc.text('Generated by bolt.diy', margin, doc.internal.pageSize.getHeight() - 10);
713
+
714
+ const dateStr = new Date().toLocaleDateString();
715
+ doc.text(dateStr, pageWidth - margin, doc.internal.pageSize.getHeight() - 10, { align: 'right' });
716
+ }
717
+
718
+ // Save the PDF
719
+ doc.save(`bolt-event-logs-${new Date().toISOString()}.pdf`);
720
+ toast.success('Event logs exported successfully as PDF');
721
+ } catch (error) {
722
+ console.error('Failed to export PDF:', error);
723
+ toast.error('Failed to export event logs as PDF');
724
+ }
725
+ };
726
+
727
+ const exportAsText = () => {
728
+ try {
729
+ const textContent = filteredLogs
730
+ .map((log) => {
731
+ const timestamp = new Date(log.timestamp).toLocaleString();
732
+ let content = `[${timestamp}] ${log.level.toUpperCase()}: ${log.message}\n`;
733
+
734
+ if (log.category) {
735
+ content += `Category: ${log.category}\n`;
736
+ }
737
+
738
+ if (log.details) {
739
+ content += `Details:\n${JSON.stringify(log.details, null, 2)}\n`;
740
+ }
741
+
742
+ return content + '-'.repeat(80) + '\n';
743
+ })
744
+ .join('\n');
745
+
746
+ const blob = new Blob([textContent], { type: 'text/plain' });
747
+ const url = window.URL.createObjectURL(blob);
748
+ const a = document.createElement('a');
749
+ a.href = url;
750
+ a.download = `bolt-event-logs-${new Date().toISOString()}.txt`;
751
+ document.body.appendChild(a);
752
+ a.click();
753
+ window.URL.revokeObjectURL(url);
754
+ document.body.removeChild(a);
755
+ toast.success('Event logs exported successfully as text file');
756
+ } catch (error) {
757
+ console.error('Failed to export text file:', error);
758
+ toast.error('Failed to export event logs as text file');
759
+ }
760
+ };
761
+
762
+ const exportFormats: ExportFormat[] = [
763
+ {
764
+ id: 'json',
765
+ label: 'Export as JSON',
766
+ icon: 'i-ph:file-js',
767
+ handler: exportAsJSON,
768
+ },
769
+ {
770
+ id: 'csv',
771
+ label: 'Export as CSV',
772
+ icon: 'i-ph:file-csv',
773
+ handler: exportAsCSV,
774
+ },
775
+ {
776
+ id: 'pdf',
777
+ label: 'Export as PDF',
778
+ icon: 'i-ph:file-pdf',
779
+ handler: exportAsPDF,
780
+ },
781
+ {
782
+ id: 'txt',
783
+ label: 'Export as Text',
784
+ icon: 'i-ph:file-text',
785
+ handler: exportAsText,
786
+ },
787
+ ];
788
+
789
+ const ExportButton = () => {
790
+ const [isOpen, setIsOpen] = useState(false);
791
+
792
+ const handleOpenChange = useCallback((open: boolean) => {
793
+ setIsOpen(open);
794
+ }, []);
795
+
796
+ const handleFormatClick = useCallback((handler: () => void) => {
797
+ handler();
798
+ setIsOpen(false);
799
+ }, []);
800
+
801
+ return (
802
+ <DialogRoot open={isOpen} onOpenChange={handleOpenChange}>
803
+ <button
804
+ onClick={() => setIsOpen(true)}
805
+ className={classNames(
806
+ 'group flex items-center gap-2',
807
+ 'rounded-lg px-3 py-1.5',
808
+ 'text-sm text-gray-900 dark:text-white',
809
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
810
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
811
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
812
+ 'transition-all duration-200',
813
+ )}
814
+ >
815
+ <span className="i-ph:download text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
816
+ Export
817
+ </button>
818
+
819
+ <Dialog showCloseButton>
820
+ <div className="p-6">
821
+ <DialogTitle className="flex items-center gap-2">
822
+ <div className="i-ph:download w-5 h-5" />
823
+ Export Event Logs
824
+ </DialogTitle>
825
+
826
+ <div className="mt-4 flex flex-col gap-2">
827
+ {exportFormats.map((format) => (
828
+ <button
829
+ key={format.id}
830
+ onClick={() => handleFormatClick(format.handler)}
831
+ className={classNames(
832
+ 'flex items-center gap-3 px-4 py-3 text-sm rounded-lg transition-colors w-full text-left',
833
+ 'bg-white dark:bg-[#0A0A0A]',
834
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
835
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
836
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
837
+ 'text-bolt-elements-textPrimary',
838
+ )}
839
+ >
840
+ <div className={classNames(format.icon, 'w-5 h-5')} />
841
+ <div>
842
+ <div className="font-medium">{format.label}</div>
843
+ <div className="text-xs text-bolt-elements-textSecondary mt-0.5">
844
+ {format.id === 'json' && 'Export as a structured JSON file'}
845
+ {format.id === 'csv' && 'Export as a CSV spreadsheet'}
846
+ {format.id === 'pdf' && 'Export as a formatted PDF document'}
847
+ {format.id === 'txt' && 'Export as a formatted text file'}
848
+ </div>
849
+ </div>
850
+ </button>
851
+ ))}
852
+ </div>
853
+ </div>
854
+ </Dialog>
855
+ </DialogRoot>
856
+ );
857
+ };
858
+
859
+ return (
860
+ <div className="flex h-full flex-col gap-6">
861
+ <div className="flex items-center justify-between">
862
+ <DropdownMenu.Root open={showLevelFilter} onOpenChange={setShowLevelFilter}>
863
+ <DropdownMenu.Trigger asChild>
864
+ <button
865
+ className={classNames(
866
+ 'flex items-center gap-2',
867
+ 'rounded-lg px-3 py-1.5',
868
+ 'text-sm text-gray-900 dark:text-white',
869
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
870
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
871
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
872
+ 'transition-all duration-200',
873
+ )}
874
+ >
875
+ <span
876
+ className={classNames('text-lg', selectedLevelOption?.icon || 'i-ph:funnel')}
877
+ style={{ color: selectedLevelOption?.color }}
878
+ />
879
+ {selectedLevelOption?.label || 'All Types'}
880
+ <span className="i-ph:caret-down text-lg text-gray-500 dark:text-gray-400" />
881
+ </button>
882
+ </DropdownMenu.Trigger>
883
+
884
+ <DropdownMenu.Portal>
885
+ <DropdownMenu.Content
886
+ className="min-w-[200px] bg-white dark:bg-[#0A0A0A] rounded-lg shadow-lg py-1 z-[250] animate-in fade-in-0 zoom-in-95 border border-[#E5E5E5] dark:border-[#1A1A1A]"
887
+ sideOffset={5}
888
+ align="start"
889
+ side="bottom"
890
+ >
891
+ {logLevelOptions.map((option) => (
892
+ <DropdownMenu.Item
893
+ key={option.value}
894
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
895
+ onClick={() => handleLevelFilterChange(option.value)}
896
+ >
897
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
898
+ <div
899
+ className={classNames(option.icon, 'text-lg group-hover:text-purple-500 transition-colors')}
900
+ style={{ color: option.color }}
901
+ />
902
+ </div>
903
+ <span className="group-hover:text-purple-500 transition-colors">{option.label}</span>
904
+ </DropdownMenu.Item>
905
+ ))}
906
+ </DropdownMenu.Content>
907
+ </DropdownMenu.Portal>
908
+ </DropdownMenu.Root>
909
+
910
+ <div className="flex items-center gap-4">
911
+ <div className="flex items-center gap-2">
912
+ <Switch
913
+ checked={showTimestamps}
914
+ onCheckedChange={(value) => handlePreferenceChange('timestamps', value)}
915
+ className="data-[state=checked]:bg-purple-500"
916
+ />
917
+ <span className="text-sm text-gray-500 dark:text-gray-400">Show Timestamps</span>
918
+ </div>
919
+
920
+ <div className="flex items-center gap-2">
921
+ <Switch
922
+ checked={use24Hour}
923
+ onCheckedChange={(value) => handlePreferenceChange('24hour', value)}
924
+ className="data-[state=checked]:bg-purple-500"
925
+ />
926
+ <span className="text-sm text-gray-500 dark:text-gray-400">24h Time</span>
927
+ </div>
928
+
929
+ <div className="flex items-center gap-2">
930
+ <Switch
931
+ checked={autoExpand}
932
+ onCheckedChange={(value) => handlePreferenceChange('autoExpand', value)}
933
+ className="data-[state=checked]:bg-purple-500"
934
+ />
935
+ <span className="text-sm text-gray-500 dark:text-gray-400">Auto Expand</span>
936
+ </div>
937
+
938
+ <div className="w-px h-4 bg-gray-200 dark:bg-gray-700" />
939
+
940
+ <button
941
+ onClick={handleRefresh}
942
+ className={classNames(
943
+ 'group flex items-center gap-2',
944
+ 'rounded-lg px-3 py-1.5',
945
+ 'text-sm text-gray-900 dark:text-white',
946
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
947
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
948
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
949
+ 'transition-all duration-200',
950
+ { 'animate-spin': isRefreshing },
951
+ )}
952
+ >
953
+ <span className="i-ph:arrows-clockwise text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
954
+ Refresh
955
+ </button>
956
+
957
+ <ExportButton />
958
+ </div>
959
+ </div>
960
+
961
+ <div className="flex flex-col gap-4">
962
+ <div className="relative">
963
+ <input
964
+ type="text"
965
+ placeholder="Search logs..."
966
+ value={searchQuery}
967
+ onChange={(e) => setSearchQuery(e.target.value)}
968
+ className={classNames(
969
+ 'w-full px-4 py-2 pl-10 rounded-lg',
970
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
971
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
972
+ 'text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400',
973
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500',
974
+ 'transition-all duration-200',
975
+ )}
976
+ />
977
+ <div className="absolute left-3 top-1/2 -translate-y-1/2">
978
+ <div className="i-ph:magnifying-glass text-lg text-gray-500 dark:text-gray-400" />
979
+ </div>
980
+ </div>
981
+
982
+ {filteredLogs.length === 0 ? (
983
+ <motion.div
984
+ initial={{ opacity: 0, y: 20 }}
985
+ animate={{ opacity: 1, y: 0 }}
986
+ className={classNames(
987
+ 'flex flex-col items-center justify-center gap-4',
988
+ 'rounded-lg p-8 text-center',
989
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
990
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
991
+ )}
992
+ >
993
+ <span className="i-ph:clipboard-text text-4xl text-gray-400 dark:text-gray-600" />
994
+ <div className="flex flex-col gap-1">
995
+ <h3 className="text-sm font-medium text-gray-900 dark:text-white">No Logs Found</h3>
996
+ <p className="text-sm text-gray-500 dark:text-gray-400">Try adjusting your search or filters</p>
997
+ </div>
998
+ </motion.div>
999
+ ) : (
1000
+ filteredLogs.map((log) => (
1001
+ <LogEntryItem
1002
+ key={log.id}
1003
+ log={log}
1004
+ isExpanded={autoExpand}
1005
+ use24Hour={use24Hour}
1006
+ showTimestamp={showTimestamps}
1007
+ />
1008
+ ))
1009
+ )}
1010
+ </div>
1011
+ </div>
1012
+ );
1013
+ }
app/components/@settings/tabs/features/FeaturesTab.tsx ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Remove unused imports
2
+ import React, { memo, useCallback } from 'react';
3
+ import { motion } from 'framer-motion';
4
+ import { Switch } from '~/components/ui/Switch';
5
+ import { useSettings } from '~/lib/hooks/useSettings';
6
+ import { classNames } from '~/utils/classNames';
7
+ import { toast } from 'react-toastify';
8
+ import { PromptLibrary } from '~/lib/common/prompt-library';
9
+
10
+ interface FeatureToggle {
11
+ id: string;
12
+ title: string;
13
+ description: string;
14
+ icon: string;
15
+ enabled: boolean;
16
+ beta?: boolean;
17
+ experimental?: boolean;
18
+ tooltip?: string;
19
+ }
20
+
21
+ const FeatureCard = memo(
22
+ ({
23
+ feature,
24
+ index,
25
+ onToggle,
26
+ }: {
27
+ feature: FeatureToggle;
28
+ index: number;
29
+ onToggle: (id: string, enabled: boolean) => void;
30
+ }) => (
31
+ <motion.div
32
+ key={feature.id}
33
+ layoutId={feature.id}
34
+ className={classNames(
35
+ 'relative group cursor-pointer',
36
+ 'bg-bolt-elements-background-depth-2',
37
+ 'hover:bg-bolt-elements-background-depth-3',
38
+ 'transition-colors duration-200',
39
+ 'rounded-lg overflow-hidden',
40
+ )}
41
+ initial={{ opacity: 0, y: 20 }}
42
+ animate={{ opacity: 1, y: 0 }}
43
+ transition={{ delay: index * 0.1 }}
44
+ >
45
+ <div className="p-4">
46
+ <div className="flex items-center justify-between">
47
+ <div className="flex items-center gap-3">
48
+ <div className={classNames(feature.icon, 'w-5 h-5 text-bolt-elements-textSecondary')} />
49
+ <div className="flex items-center gap-2">
50
+ <h4 className="font-medium text-bolt-elements-textPrimary">{feature.title}</h4>
51
+ {feature.beta && (
52
+ <span className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium">Beta</span>
53
+ )}
54
+ {feature.experimental && (
55
+ <span className="px-2 py-0.5 text-xs rounded-full bg-orange-500/10 text-orange-500 font-medium">
56
+ Experimental
57
+ </span>
58
+ )}
59
+ </div>
60
+ </div>
61
+ <Switch checked={feature.enabled} onCheckedChange={(checked) => onToggle(feature.id, checked)} />
62
+ </div>
63
+ <p className="mt-2 text-sm text-bolt-elements-textSecondary">{feature.description}</p>
64
+ {feature.tooltip && <p className="mt-1 text-xs text-bolt-elements-textTertiary">{feature.tooltip}</p>}
65
+ </div>
66
+ </motion.div>
67
+ ),
68
+ );
69
+
70
+ const FeatureSection = memo(
71
+ ({
72
+ title,
73
+ features,
74
+ icon,
75
+ description,
76
+ onToggleFeature,
77
+ }: {
78
+ title: string;
79
+ features: FeatureToggle[];
80
+ icon: string;
81
+ description: string;
82
+ onToggleFeature: (id: string, enabled: boolean) => void;
83
+ }) => (
84
+ <motion.div
85
+ layout
86
+ className="flex flex-col gap-4"
87
+ initial={{ opacity: 0, y: 20 }}
88
+ animate={{ opacity: 1, y: 0 }}
89
+ transition={{ duration: 0.3 }}
90
+ >
91
+ <div className="flex items-center gap-3">
92
+ <div className={classNames(icon, 'text-xl text-purple-500')} />
93
+ <div>
94
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">{title}</h3>
95
+ <p className="text-sm text-bolt-elements-textSecondary">{description}</p>
96
+ </div>
97
+ </div>
98
+
99
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
100
+ {features.map((feature, index) => (
101
+ <FeatureCard key={feature.id} feature={feature} index={index} onToggle={onToggleFeature} />
102
+ ))}
103
+ </div>
104
+ </motion.div>
105
+ ),
106
+ );
107
+
108
+ export default function FeaturesTab() {
109
+ const {
110
+ autoSelectTemplate,
111
+ isLatestBranch,
112
+ contextOptimizationEnabled,
113
+ eventLogs,
114
+ setAutoSelectTemplate,
115
+ enableLatestBranch,
116
+ enableContextOptimization,
117
+ setEventLogs,
118
+ setPromptId,
119
+ promptId,
120
+ } = useSettings();
121
+
122
+ // Enable features by default on first load
123
+ React.useEffect(() => {
124
+ // Only set defaults if values are undefined
125
+ if (isLatestBranch === undefined) {
126
+ enableLatestBranch(false); // Default: OFF - Don't auto-update from main branch
127
+ }
128
+
129
+ if (contextOptimizationEnabled === undefined) {
130
+ enableContextOptimization(true); // Default: ON - Enable context optimization
131
+ }
132
+
133
+ if (autoSelectTemplate === undefined) {
134
+ setAutoSelectTemplate(true); // Default: ON - Enable auto-select templates
135
+ }
136
+
137
+ if (promptId === undefined) {
138
+ setPromptId('default'); // Default: 'default'
139
+ }
140
+
141
+ if (eventLogs === undefined) {
142
+ setEventLogs(true); // Default: ON - Enable event logging
143
+ }
144
+ }, []); // Only run once on component mount
145
+
146
+ const handleToggleFeature = useCallback(
147
+ (id: string, enabled: boolean) => {
148
+ switch (id) {
149
+ case 'latestBranch': {
150
+ enableLatestBranch(enabled);
151
+ toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
152
+ break;
153
+ }
154
+
155
+ case 'autoSelectTemplate': {
156
+ setAutoSelectTemplate(enabled);
157
+ toast.success(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
158
+ break;
159
+ }
160
+
161
+ case 'contextOptimization': {
162
+ enableContextOptimization(enabled);
163
+ toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
164
+ break;
165
+ }
166
+
167
+ case 'eventLogs': {
168
+ setEventLogs(enabled);
169
+ toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
170
+ break;
171
+ }
172
+
173
+ default:
174
+ break;
175
+ }
176
+ },
177
+ [enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs],
178
+ );
179
+
180
+ const features = {
181
+ stable: [
182
+ {
183
+ id: 'latestBranch',
184
+ title: 'Main Branch Updates',
185
+ description: 'Get the latest updates from the main branch',
186
+ icon: 'i-ph:git-branch',
187
+ enabled: isLatestBranch,
188
+ tooltip: 'Enabled by default to receive updates from the main development branch',
189
+ },
190
+ {
191
+ id: 'autoSelectTemplate',
192
+ title: 'Auto Select Template',
193
+ description: 'Automatically select starter template',
194
+ icon: 'i-ph:selection',
195
+ enabled: autoSelectTemplate,
196
+ tooltip: 'Enabled by default to automatically select the most appropriate starter template',
197
+ },
198
+ {
199
+ id: 'contextOptimization',
200
+ title: 'Context Optimization',
201
+ description: 'Optimize context for better responses',
202
+ icon: 'i-ph:brain',
203
+ enabled: contextOptimizationEnabled,
204
+ tooltip: 'Enabled by default for improved AI responses',
205
+ },
206
+ {
207
+ id: 'eventLogs',
208
+ title: 'Event Logging',
209
+ description: 'Enable detailed event logging and history',
210
+ icon: 'i-ph:list-bullets',
211
+ enabled: eventLogs,
212
+ tooltip: 'Enabled by default to record detailed logs of system events and user actions',
213
+ },
214
+ ],
215
+ beta: [],
216
+ };
217
+
218
+ return (
219
+ <div className="flex flex-col gap-8">
220
+ <FeatureSection
221
+ title="Core Features"
222
+ features={features.stable}
223
+ icon="i-ph:check-circle"
224
+ description="Essential features that are enabled by default for optimal performance"
225
+ onToggleFeature={handleToggleFeature}
226
+ />
227
+
228
+ {features.beta.length > 0 && (
229
+ <FeatureSection
230
+ title="Beta Features"
231
+ features={features.beta}
232
+ icon="i-ph:test-tube"
233
+ description="New features that are ready for testing but may have some rough edges"
234
+ onToggleFeature={handleToggleFeature}
235
+ />
236
+ )}
237
+
238
+ <motion.div
239
+ layout
240
+ className={classNames(
241
+ 'bg-bolt-elements-background-depth-2',
242
+ 'hover:bg-bolt-elements-background-depth-3',
243
+ 'transition-all duration-200',
244
+ 'rounded-lg p-4',
245
+ 'group',
246
+ )}
247
+ initial={{ opacity: 0, y: 20 }}
248
+ animate={{ opacity: 1, y: 0 }}
249
+ transition={{ delay: 0.3 }}
250
+ >
251
+ <div className="flex items-center gap-4">
252
+ <div
253
+ className={classNames(
254
+ 'p-2 rounded-lg text-xl',
255
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
256
+ 'transition-colors duration-200',
257
+ 'text-purple-500',
258
+ )}
259
+ >
260
+ <div className="i-ph:book" />
261
+ </div>
262
+ <div className="flex-1">
263
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
264
+ Prompt Library
265
+ </h4>
266
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
267
+ Choose a prompt from the library to use as the system prompt
268
+ </p>
269
+ </div>
270
+ <select
271
+ value={promptId}
272
+ onChange={(e) => {
273
+ setPromptId(e.target.value);
274
+ toast.success('Prompt template updated');
275
+ }}
276
+ className={classNames(
277
+ 'p-2 rounded-lg text-sm min-w-[200px]',
278
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
279
+ 'text-bolt-elements-textPrimary',
280
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
281
+ 'group-hover:border-purple-500/30',
282
+ 'transition-all duration-200',
283
+ )}
284
+ >
285
+ {PromptLibrary.getList().map((x) => (
286
+ <option key={x.id} value={x.id}>
287
+ {x.label}
288
+ </option>
289
+ ))}
290
+ </select>
291
+ </div>
292
+ </motion.div>
293
+ </div>
294
+ );
295
+ }
app/components/@settings/tabs/github/GitHubTab.tsx ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { useGitHubConnection, useGitHubStats } from '~/lib/hooks';
4
+ import { LoadingState, ErrorState, ConnectionTestIndicator, RepositoryCard } from './components/shared';
5
+ import { GitHubConnection } from './components/GitHubConnection';
6
+ import { GitHubUserProfile } from './components/GitHubUserProfile';
7
+ import { GitHubStats } from './components/GitHubStats';
8
+ import { Button } from '~/components/ui/Button';
9
+ import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible';
10
+ import { classNames } from '~/utils/classNames';
11
+ import { ChevronDown } from 'lucide-react';
12
+ import { GitHubErrorBoundary } from './components/GitHubErrorBoundary';
13
+ import { GitHubProgressiveLoader } from './components/GitHubProgressiveLoader';
14
+ import { GitHubCacheManager } from './components/GitHubCacheManager';
15
+
16
+ interface ConnectionTestResult {
17
+ status: 'success' | 'error' | 'testing';
18
+ message: string;
19
+ timestamp?: number;
20
+ }
21
+
22
+ // GitHub logo SVG component
23
+ const GithubLogo = () => (
24
+ <svg viewBox="0 0 24 24" className="w-5 h-5">
25
+ <path
26
+ fill="currentColor"
27
+ d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
28
+ />
29
+ </svg>
30
+ );
31
+
32
+ export default function GitHubTab() {
33
+ const { connection, isConnected, isLoading, error, testConnection } = useGitHubConnection();
34
+ const {
35
+ stats,
36
+ isLoading: isStatsLoading,
37
+ error: statsError,
38
+ } = useGitHubStats(
39
+ connection,
40
+ {
41
+ autoFetch: true,
42
+ cacheTimeout: 30 * 60 * 1000, // 30 minutes
43
+ },
44
+ isConnected && connection ? !connection.token : false,
45
+ ); // Use server-side when no token but connected
46
+
47
+ const [connectionTest, setConnectionTest] = useState<ConnectionTestResult | null>(null);
48
+ const [isStatsExpanded, setIsStatsExpanded] = useState(false);
49
+ const [isReposExpanded, setIsReposExpanded] = useState(false);
50
+
51
+ const handleTestConnection = async () => {
52
+ if (!connection?.user) {
53
+ setConnectionTest({
54
+ status: 'error',
55
+ message: 'No connection established',
56
+ timestamp: Date.now(),
57
+ });
58
+ return;
59
+ }
60
+
61
+ setConnectionTest({
62
+ status: 'testing',
63
+ message: 'Testing connection...',
64
+ });
65
+
66
+ try {
67
+ const isValid = await testConnection();
68
+
69
+ if (isValid) {
70
+ setConnectionTest({
71
+ status: 'success',
72
+ message: `Connected successfully as ${connection.user.login}`,
73
+ timestamp: Date.now(),
74
+ });
75
+ } else {
76
+ setConnectionTest({
77
+ status: 'error',
78
+ message: 'Connection test failed',
79
+ timestamp: Date.now(),
80
+ });
81
+ }
82
+ } catch (error) {
83
+ setConnectionTest({
84
+ status: 'error',
85
+ message: `Connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
86
+ timestamp: Date.now(),
87
+ });
88
+ }
89
+ };
90
+
91
+ // Loading state for initial connection check
92
+ if (isLoading) {
93
+ return (
94
+ <div className="space-y-6">
95
+ <div className="flex items-center gap-2">
96
+ <GithubLogo />
97
+ <h2 className="text-lg font-medium text-bolt-elements-textPrimary">GitHub Integration</h2>
98
+ </div>
99
+ <LoadingState message="Checking GitHub connection..." />
100
+ </div>
101
+ );
102
+ }
103
+
104
+ // Error state for connection issues
105
+ if (error && !connection) {
106
+ return (
107
+ <div className="space-y-6">
108
+ <div className="flex items-center gap-2">
109
+ <GithubLogo />
110
+ <h2 className="text-lg font-medium text-bolt-elements-textPrimary">GitHub Integration</h2>
111
+ </div>
112
+ <ErrorState
113
+ title="Connection Error"
114
+ message={error}
115
+ onRetry={() => window.location.reload()}
116
+ retryLabel="Reload Page"
117
+ />
118
+ </div>
119
+ );
120
+ }
121
+
122
+ // Not connected state
123
+ if (!isConnected || !connection) {
124
+ return (
125
+ <div className="space-y-6">
126
+ <div className="flex items-center gap-2">
127
+ <GithubLogo />
128
+ <h2 className="text-lg font-medium text-bolt-elements-textPrimary">GitHub Integration</h2>
129
+ </div>
130
+ <p className="text-sm text-bolt-elements-textSecondary">
131
+ Connect your GitHub account to enable advanced repository management features, statistics, and seamless
132
+ integration.
133
+ </p>
134
+ <GitHubConnection connectionTest={connectionTest} onTestConnection={handleTestConnection} />
135
+ </div>
136
+ );
137
+ }
138
+
139
+ return (
140
+ <GitHubErrorBoundary>
141
+ <div className="space-y-6">
142
+ {/* Header */}
143
+ <motion.div
144
+ className="flex items-center justify-between gap-2"
145
+ initial={{ opacity: 0, y: 20 }}
146
+ animate={{ opacity: 1, y: 0 }}
147
+ transition={{ delay: 0.1 }}
148
+ >
149
+ <div className="flex items-center gap-2">
150
+ <GithubLogo />
151
+ <h2 className="text-lg font-medium text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
152
+ GitHub Integration
153
+ </h2>
154
+ </div>
155
+ <div className="flex items-center gap-2">
156
+ {connection?.rateLimit && (
157
+ <div className="flex items-center gap-2 px-3 py-1 bg-bolt-elements-background-depth-1 rounded-lg text-xs">
158
+ <div className="i-ph:cloud w-4 h-4 text-bolt-elements-textSecondary" />
159
+ <span className="text-bolt-elements-textSecondary">
160
+ API: {connection.rateLimit.remaining}/{connection.rateLimit.limit}
161
+ </span>
162
+ </div>
163
+ )}
164
+ </div>
165
+ </motion.div>
166
+
167
+ <p className="text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary">
168
+ Manage your GitHub integration with advanced repository features and comprehensive statistics
169
+ </p>
170
+
171
+ {/* Connection Test Results */}
172
+ <ConnectionTestIndicator
173
+ status={connectionTest?.status || null}
174
+ message={connectionTest?.message}
175
+ timestamp={connectionTest?.timestamp}
176
+ />
177
+
178
+ {/* Connection Component */}
179
+ <GitHubConnection connectionTest={connectionTest} onTestConnection={handleTestConnection} />
180
+
181
+ {/* User Profile */}
182
+ {connection.user && <GitHubUserProfile user={connection.user} />}
183
+
184
+ {/* Stats Section */}
185
+ <GitHubStats connection={connection} isExpanded={isStatsExpanded} onToggleExpanded={setIsStatsExpanded} />
186
+
187
+ {/* Repositories Section */}
188
+ {stats?.repos && stats.repos.length > 0 && (
189
+ <motion.div
190
+ initial={{ opacity: 0, y: 20 }}
191
+ animate={{ opacity: 1, y: 0 }}
192
+ transition={{ delay: 0.4 }}
193
+ className="border-t border-bolt-elements-borderColor pt-6"
194
+ >
195
+ <Collapsible open={isReposExpanded} onOpenChange={setIsReposExpanded}>
196
+ <CollapsibleTrigger asChild>
197
+ <div className="flex items-center justify-between p-4 rounded-lg bg-bolt-elements-background dark:bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive/70 dark:hover:border-bolt-elements-borderColorActive/70 transition-all duration-200">
198
+ <div className="flex items-center gap-2">
199
+ <div className="i-ph:folder w-4 h-4 text-bolt-elements-item-contentAccent" />
200
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">
201
+ All Repositories ({stats.repos.length})
202
+ </span>
203
+ </div>
204
+ <ChevronDown
205
+ className={classNames(
206
+ 'w-4 h-4 transform transition-transform duration-200 text-bolt-elements-textSecondary',
207
+ isReposExpanded ? 'rotate-180' : '',
208
+ )}
209
+ />
210
+ </div>
211
+ </CollapsibleTrigger>
212
+
213
+ <CollapsibleContent className="overflow-hidden">
214
+ <div className="mt-4 space-y-4">
215
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
216
+ {(isReposExpanded ? stats.repos : stats.repos.slice(0, 12)).map((repo) => (
217
+ <RepositoryCard
218
+ key={repo.full_name}
219
+ repository={repo}
220
+ variant="detailed"
221
+ showHealthScore
222
+ showExtendedMetrics
223
+ onSelect={() => window.open(repo.html_url, '_blank', 'noopener,noreferrer')}
224
+ />
225
+ ))}
226
+ </div>
227
+
228
+ {stats.repos.length > 12 && !isReposExpanded && (
229
+ <div className="text-center">
230
+ <Button
231
+ variant="outline"
232
+ onClick={() => setIsReposExpanded(true)}
233
+ className="text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary"
234
+ >
235
+ Show {stats.repos.length - 12} more repositories
236
+ </Button>
237
+ </div>
238
+ )}
239
+ </div>
240
+ </CollapsibleContent>
241
+ </Collapsible>
242
+ </motion.div>
243
+ )}
244
+
245
+ {/* Stats Error State */}
246
+ {statsError && !stats && (
247
+ <ErrorState
248
+ title="Failed to Load Statistics"
249
+ message={statsError}
250
+ onRetry={() => window.location.reload()}
251
+ retryLabel="Retry"
252
+ />
253
+ )}
254
+
255
+ {/* Stats Loading State */}
256
+ {isStatsLoading && !stats && (
257
+ <GitHubProgressiveLoader
258
+ isLoading={isStatsLoading}
259
+ loadingMessage="Loading GitHub statistics..."
260
+ showProgress={true}
261
+ progressSteps={[
262
+ { key: 'user', label: 'Fetching user info', completed: !!connection?.user, loading: !connection?.user },
263
+ { key: 'repos', label: 'Loading repositories', completed: false, loading: true },
264
+ { key: 'stats', label: 'Calculating statistics', completed: false },
265
+ { key: 'cache', label: 'Updating cache', completed: false },
266
+ ]}
267
+ >
268
+ <div />
269
+ </GitHubProgressiveLoader>
270
+ )}
271
+
272
+ {/* Cache Management Section - Only show when connected */}
273
+ {isConnected && connection && (
274
+ <div className="mt-8 pt-6 border-t border-bolt-elements-borderColor">
275
+ <GitHubCacheManager showStats={true} />
276
+ </div>
277
+ )}
278
+ </div>
279
+ </GitHubErrorBoundary>
280
+ );
281
+ }
app/components/@settings/tabs/github/components/GitHubAuthDialog.tsx ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import * as Dialog from '@radix-ui/react-dialog';
3
+ import { motion } from 'framer-motion';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { useGitHubConnection } from '~/lib/hooks';
6
+
7
+ interface GitHubAuthDialogProps {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ onSuccess?: () => void;
11
+ }
12
+
13
+ export function GitHubAuthDialog({ isOpen, onClose, onSuccess }: GitHubAuthDialogProps) {
14
+ const { connect, isConnecting, error } = useGitHubConnection();
15
+ const [token, setToken] = useState('');
16
+ const [tokenType, setTokenType] = useState<'classic' | 'fine-grained'>('classic');
17
+
18
+ const handleConnect = async (e: React.FormEvent) => {
19
+ e.preventDefault();
20
+
21
+ if (!token.trim()) {
22
+ return;
23
+ }
24
+
25
+ try {
26
+ await connect(token, tokenType);
27
+ setToken(''); // Clear token on successful connection
28
+ onSuccess?.();
29
+ onClose();
30
+ } catch {
31
+ // Error handling is done in the hook
32
+ }
33
+ };
34
+
35
+ const handleClose = () => {
36
+ setToken('');
37
+ onClose();
38
+ };
39
+
40
+ return (
41
+ <Dialog.Root open={isOpen}>
42
+ <Dialog.Portal>
43
+ <Dialog.Overlay className="fixed inset-0 bg-black/50 z-[200]" />
44
+ <Dialog.Content
45
+ className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-[201] w-full max-w-md"
46
+ onEscapeKeyDown={handleClose}
47
+ onPointerDownOutside={handleClose}
48
+ >
49
+ <motion.div
50
+ className="bg-bolt-elements-background border border-bolt-elements-borderColor rounded-lg shadow-lg"
51
+ initial={{ opacity: 0, scale: 0.9 }}
52
+ animate={{ opacity: 1, scale: 1 }}
53
+ exit={{ opacity: 0, scale: 0.9 }}
54
+ >
55
+ <div className="p-6 space-y-6">
56
+ <div className="flex items-center justify-between">
57
+ <h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Connect to GitHub</h2>
58
+ <button
59
+ onClick={handleClose}
60
+ className="p-1 rounded-md hover:bg-bolt-elements-item-backgroundActive/10"
61
+ >
62
+ <div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary" />
63
+ </button>
64
+ </div>
65
+
66
+ <div className="text-xs text-bolt-elements-textSecondary bg-bolt-elements-background-depth-1 p-3 rounded-lg">
67
+ <p className="flex items-center gap-1 mb-1">
68
+ <span className="i-ph:lightbulb w-3.5 h-3.5 text-bolt-elements-icon-success" />
69
+ <span className="font-medium">Tip:</span> You need a GitHub token to deploy repositories.
70
+ </p>
71
+ <p>Required scopes: repo, read:org, read:user</p>
72
+ </div>
73
+
74
+ <form onSubmit={handleConnect} className="space-y-4">
75
+ <div>
76
+ <label className="block text-sm text-bolt-elements-textSecondary mb-2">Token Type</label>
77
+ <select
78
+ value={tokenType}
79
+ onChange={(e) => setTokenType(e.target.value as 'classic' | 'fine-grained')}
80
+ disabled={isConnecting}
81
+ className={classNames(
82
+ 'w-full px-3 py-2 rounded-lg text-sm',
83
+ 'bg-bolt-elements-background-depth-1',
84
+ 'border border-bolt-elements-borderColor',
85
+ 'text-bolt-elements-textPrimary',
86
+ 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-item-contentAccent',
87
+ 'disabled:opacity-50',
88
+ )}
89
+ >
90
+ <option value="classic">Personal Access Token (Classic)</option>
91
+ <option value="fine-grained">Fine-grained Token</option>
92
+ </select>
93
+ </div>
94
+
95
+ <div>
96
+ <label className="block text-sm text-bolt-elements-textSecondary mb-2">
97
+ {tokenType === 'classic' ? 'Personal Access Token' : 'Fine-grained Token'}
98
+ </label>
99
+ <input
100
+ type="password"
101
+ value={token}
102
+ onChange={(e) => setToken(e.target.value)}
103
+ disabled={isConnecting}
104
+ placeholder={`Enter your GitHub ${
105
+ tokenType === 'classic' ? 'personal access token' : 'fine-grained token'
106
+ }`}
107
+ className={classNames(
108
+ 'w-full px-3 py-2 rounded-lg text-sm',
109
+ 'bg-bolt-elements-background-depth-1',
110
+ 'border border-bolt-elements-borderColor',
111
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
112
+ 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColorActive',
113
+ 'disabled:opacity-50',
114
+ )}
115
+ />
116
+ <div className="mt-2 text-sm text-bolt-elements-textSecondary">
117
+ <a
118
+ href={`https://github.com/settings/tokens${tokenType === 'fine-grained' ? '/beta' : '/new'}`}
119
+ target="_blank"
120
+ rel="noopener noreferrer"
121
+ className="text-bolt-elements-borderColorActive hover:underline inline-flex items-center gap-1"
122
+ >
123
+ Get your token
124
+ <div className="i-ph:arrow-square-out w-4 h-4" />
125
+ </a>
126
+ </div>
127
+ </div>
128
+
129
+ {error && (
130
+ <div className="p-4 rounded-lg bg-red-50 border border-red-200 dark:bg-red-900/20 dark:border-red-700">
131
+ <p className="text-sm text-red-800 dark:text-red-200">{error}</p>
132
+ </div>
133
+ )}
134
+
135
+ <div className="flex items-center justify-end gap-3 pt-4">
136
+ <button
137
+ type="button"
138
+ onClick={handleClose}
139
+ className="px-4 py-2 text-sm text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary"
140
+ >
141
+ Cancel
142
+ </button>
143
+ <button
144
+ type="submit"
145
+ disabled={isConnecting || !token.trim()}
146
+ className={classNames(
147
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
148
+ 'bg-[#303030] text-white',
149
+ 'hover:bg-[#5E41D0] hover:text-white',
150
+ 'disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200',
151
+ )}
152
+ >
153
+ {isConnecting ? (
154
+ <>
155
+ <div className="i-ph:spinner-gap animate-spin" />
156
+ Connecting...
157
+ </>
158
+ ) : (
159
+ <>
160
+ <div className="i-ph:plug-charging w-4 h-4" />
161
+ Connect
162
+ </>
163
+ )}
164
+ </button>
165
+ </div>
166
+ </form>
167
+ </div>
168
+ </motion.div>
169
+ </Dialog.Content>
170
+ </Dialog.Portal>
171
+ </Dialog.Root>
172
+ );
173
+ }
app/components/@settings/tabs/github/components/GitHubCacheManager.tsx ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useCallback, useEffect, useMemo } from 'react';
2
+ import { Button } from '~/components/ui/Button';
3
+ import { classNames } from '~/utils/classNames';
4
+ import { Database, Trash2, RefreshCw, Clock, HardDrive, CheckCircle } from 'lucide-react';
5
+
6
+ interface CacheEntry {
7
+ key: string;
8
+ size: number;
9
+ timestamp: number;
10
+ lastAccessed: number;
11
+ data: any;
12
+ }
13
+
14
+ interface CacheStats {
15
+ totalSize: number;
16
+ totalEntries: number;
17
+ oldestEntry: number;
18
+ newestEntry: number;
19
+ hitRate?: number;
20
+ }
21
+
22
+ interface GitHubCacheManagerProps {
23
+ className?: string;
24
+ showStats?: boolean;
25
+ }
26
+
27
+ // Cache management utilities
28
+ class CacheManagerService {
29
+ private static readonly _cachePrefix = 'github_';
30
+ private static readonly _cacheKeys = [
31
+ 'github_connection',
32
+ 'github_stats_cache',
33
+ 'github_repositories_cache',
34
+ 'github_user_cache',
35
+ 'github_rate_limits',
36
+ ];
37
+
38
+ static getCacheEntries(): CacheEntry[] {
39
+ const entries: CacheEntry[] = [];
40
+
41
+ for (const key of this._cacheKeys) {
42
+ try {
43
+ const data = localStorage.getItem(key);
44
+
45
+ if (data) {
46
+ const parsed = JSON.parse(data);
47
+ entries.push({
48
+ key,
49
+ size: new Blob([data]).size,
50
+ timestamp: parsed.timestamp || Date.now(),
51
+ lastAccessed: parsed.lastAccessed || Date.now(),
52
+ data: parsed,
53
+ });
54
+ }
55
+ } catch (error) {
56
+ console.warn(`Failed to parse cache entry: ${key}`, error);
57
+ }
58
+ }
59
+
60
+ return entries.sort((a, b) => b.lastAccessed - a.lastAccessed);
61
+ }
62
+
63
+ static getCacheStats(): CacheStats {
64
+ const entries = this.getCacheEntries();
65
+
66
+ if (entries.length === 0) {
67
+ return {
68
+ totalSize: 0,
69
+ totalEntries: 0,
70
+ oldestEntry: 0,
71
+ newestEntry: 0,
72
+ };
73
+ }
74
+
75
+ const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0);
76
+ const timestamps = entries.map((e) => e.timestamp);
77
+
78
+ return {
79
+ totalSize,
80
+ totalEntries: entries.length,
81
+ oldestEntry: Math.min(...timestamps),
82
+ newestEntry: Math.max(...timestamps),
83
+ };
84
+ }
85
+
86
+ static clearCache(keys?: string[]): void {
87
+ const keysToRemove = keys || this._cacheKeys;
88
+
89
+ for (const key of keysToRemove) {
90
+ localStorage.removeItem(key);
91
+ }
92
+ }
93
+
94
+ static clearExpiredCache(maxAge: number = 24 * 60 * 60 * 1000): number {
95
+ const entries = this.getCacheEntries();
96
+ const now = Date.now();
97
+ let removedCount = 0;
98
+
99
+ for (const entry of entries) {
100
+ if (now - entry.timestamp > maxAge) {
101
+ localStorage.removeItem(entry.key);
102
+ removedCount++;
103
+ }
104
+ }
105
+
106
+ return removedCount;
107
+ }
108
+
109
+ static compactCache(): void {
110
+ const entries = this.getCacheEntries();
111
+
112
+ for (const entry of entries) {
113
+ try {
114
+ // Re-serialize with minimal data
115
+ const compacted = {
116
+ ...entry.data,
117
+ lastAccessed: Date.now(),
118
+ };
119
+ localStorage.setItem(entry.key, JSON.stringify(compacted));
120
+ } catch (error) {
121
+ console.warn(`Failed to compact cache entry: ${entry.key}`, error);
122
+ }
123
+ }
124
+ }
125
+
126
+ static formatSize(bytes: number): string {
127
+ if (bytes === 0) {
128
+ return '0 B';
129
+ }
130
+
131
+ const k = 1024;
132
+ const sizes = ['B', 'KB', 'MB', 'GB'];
133
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
134
+
135
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
136
+ }
137
+ }
138
+
139
+ export function GitHubCacheManager({ className = '', showStats = true }: GitHubCacheManagerProps) {
140
+ const [cacheEntries, setCacheEntries] = useState<CacheEntry[]>([]);
141
+ const [isLoading, setIsLoading] = useState(false);
142
+ const [lastClearTime, setLastClearTime] = useState<number | null>(null);
143
+
144
+ const refreshCacheData = useCallback(() => {
145
+ setCacheEntries(CacheManagerService.getCacheEntries());
146
+ }, []);
147
+
148
+ useEffect(() => {
149
+ refreshCacheData();
150
+ }, [refreshCacheData]);
151
+
152
+ const cacheStats = useMemo(() => CacheManagerService.getCacheStats(), [cacheEntries]);
153
+
154
+ const handleClearAll = useCallback(async () => {
155
+ setIsLoading(true);
156
+
157
+ try {
158
+ CacheManagerService.clearCache();
159
+ setLastClearTime(Date.now());
160
+ refreshCacheData();
161
+
162
+ // Trigger a page refresh to update all components
163
+ setTimeout(() => {
164
+ window.location.reload();
165
+ }, 1000);
166
+ } catch (error) {
167
+ console.error('Failed to clear cache:', error);
168
+ } finally {
169
+ setIsLoading(false);
170
+ }
171
+ }, [refreshCacheData]);
172
+
173
+ const handleClearExpired = useCallback(() => {
174
+ setIsLoading(true);
175
+
176
+ try {
177
+ const removedCount = CacheManagerService.clearExpiredCache();
178
+ refreshCacheData();
179
+
180
+ if (removedCount > 0) {
181
+ // Show success message or trigger update
182
+ console.log(`Removed ${removedCount} expired cache entries`);
183
+ }
184
+ } catch (error) {
185
+ console.error('Failed to clear expired cache:', error);
186
+ } finally {
187
+ setIsLoading(false);
188
+ }
189
+ }, [refreshCacheData]);
190
+
191
+ const handleCompactCache = useCallback(() => {
192
+ setIsLoading(true);
193
+
194
+ try {
195
+ CacheManagerService.compactCache();
196
+ refreshCacheData();
197
+ } catch (error) {
198
+ console.error('Failed to compact cache:', error);
199
+ } finally {
200
+ setIsLoading(false);
201
+ }
202
+ }, [refreshCacheData]);
203
+
204
+ const handleClearSpecific = useCallback(
205
+ (key: string) => {
206
+ setIsLoading(true);
207
+
208
+ try {
209
+ CacheManagerService.clearCache([key]);
210
+ refreshCacheData();
211
+ } catch (error) {
212
+ console.error(`Failed to clear cache key: ${key}`, error);
213
+ } finally {
214
+ setIsLoading(false);
215
+ }
216
+ },
217
+ [refreshCacheData],
218
+ );
219
+
220
+ if (!showStats && cacheEntries.length === 0) {
221
+ return null;
222
+ }
223
+
224
+ return (
225
+ <div
226
+ className={classNames(
227
+ 'space-y-4 p-4 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-lg',
228
+ className,
229
+ )}
230
+ >
231
+ <div className="flex items-center justify-between">
232
+ <div className="flex items-center gap-2">
233
+ <Database className="w-4 h-4 text-bolt-elements-item-contentAccent" />
234
+ <h3 className="text-sm font-medium text-bolt-elements-textPrimary">GitHub Cache Management</h3>
235
+ </div>
236
+
237
+ <div className="flex items-center gap-2">
238
+ <Button variant="outline" size="sm" onClick={refreshCacheData} disabled={isLoading}>
239
+ <RefreshCw className={classNames('w-3 h-3', isLoading ? 'animate-spin' : '')} />
240
+ </Button>
241
+ </div>
242
+ </div>
243
+
244
+ {showStats && (
245
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
246
+ <div className="bg-bolt-elements-background-depth-2 p-3 rounded-lg">
247
+ <div className="flex items-center gap-2 mb-1">
248
+ <HardDrive className="w-3 h-3 text-bolt-elements-textSecondary" />
249
+ <span className="text-xs font-medium text-bolt-elements-textSecondary">Total Size</span>
250
+ </div>
251
+ <p className="text-sm font-semibold text-bolt-elements-textPrimary">
252
+ {CacheManagerService.formatSize(cacheStats.totalSize)}
253
+ </p>
254
+ </div>
255
+
256
+ <div className="bg-bolt-elements-background-depth-2 p-3 rounded-lg">
257
+ <div className="flex items-center gap-2 mb-1">
258
+ <Database className="w-3 h-3 text-bolt-elements-textSecondary" />
259
+ <span className="text-xs font-medium text-bolt-elements-textSecondary">Entries</span>
260
+ </div>
261
+ <p className="text-sm font-semibold text-bolt-elements-textPrimary">{cacheStats.totalEntries}</p>
262
+ </div>
263
+
264
+ <div className="bg-bolt-elements-background-depth-2 p-3 rounded-lg">
265
+ <div className="flex items-center gap-2 mb-1">
266
+ <Clock className="w-3 h-3 text-bolt-elements-textSecondary" />
267
+ <span className="text-xs font-medium text-bolt-elements-textSecondary">Oldest</span>
268
+ </div>
269
+ <p className="text-xs text-bolt-elements-textSecondary">
270
+ {cacheStats.oldestEntry ? new Date(cacheStats.oldestEntry).toLocaleDateString() : 'N/A'}
271
+ </p>
272
+ </div>
273
+
274
+ <div className="bg-bolt-elements-background-depth-2 p-3 rounded-lg">
275
+ <div className="flex items-center gap-2 mb-1">
276
+ <CheckCircle className="w-3 h-3 text-bolt-elements-textSecondary" />
277
+ <span className="text-xs font-medium text-bolt-elements-textSecondary">Status</span>
278
+ </div>
279
+ <p className="text-xs text-green-600 dark:text-green-400">
280
+ {cacheStats.totalEntries > 0 ? 'Active' : 'Empty'}
281
+ </p>
282
+ </div>
283
+ </div>
284
+ )}
285
+
286
+ {cacheEntries.length > 0 && (
287
+ <div className="space-y-2">
288
+ <h4 className="text-xs font-medium text-bolt-elements-textSecondary">
289
+ Cache Entries ({cacheEntries.length})
290
+ </h4>
291
+
292
+ <div className="space-y-2 max-h-48 overflow-y-auto">
293
+ {cacheEntries.map((entry) => (
294
+ <div
295
+ key={entry.key}
296
+ className="flex items-center justify-between p-2 bg-bolt-elements-background-depth-2 rounded border border-bolt-elements-borderColor"
297
+ >
298
+ <div className="flex-1 min-w-0">
299
+ <p className="text-xs font-medium text-bolt-elements-textPrimary truncate">
300
+ {entry.key.replace('github_', '')}
301
+ </p>
302
+ <p className="text-xs text-bolt-elements-textSecondary">
303
+ {CacheManagerService.formatSize(entry.size)} • {new Date(entry.lastAccessed).toLocaleString()}
304
+ </p>
305
+ </div>
306
+
307
+ <Button
308
+ variant="ghost"
309
+ size="sm"
310
+ onClick={() => handleClearSpecific(entry.key)}
311
+ disabled={isLoading}
312
+ className="ml-2"
313
+ >
314
+ <Trash2 className="w-3 h-3 text-red-500" />
315
+ </Button>
316
+ </div>
317
+ ))}
318
+ </div>
319
+ </div>
320
+ )}
321
+
322
+ <div className="flex flex-wrap gap-2 pt-2 border-t border-bolt-elements-borderColor">
323
+ <Button
324
+ variant="outline"
325
+ size="sm"
326
+ onClick={handleClearExpired}
327
+ disabled={isLoading}
328
+ className="flex items-center gap-1"
329
+ >
330
+ <Clock className="w-3 h-3" />
331
+ <span className="text-xs">Clear Expired</span>
332
+ </Button>
333
+
334
+ <Button
335
+ variant="outline"
336
+ size="sm"
337
+ onClick={handleCompactCache}
338
+ disabled={isLoading}
339
+ className="flex items-center gap-1"
340
+ >
341
+ <RefreshCw className="w-3 h-3" />
342
+ <span className="text-xs">Compact</span>
343
+ </Button>
344
+
345
+ {cacheEntries.length > 0 && (
346
+ <Button
347
+ variant="outline"
348
+ size="sm"
349
+ onClick={handleClearAll}
350
+ disabled={isLoading}
351
+ className="flex items-center gap-1 text-red-600 hover:text-red-700 border-red-200 hover:border-red-300"
352
+ >
353
+ <Trash2 className="w-3 h-3" />
354
+ <span className="text-xs">Clear All</span>
355
+ </Button>
356
+ )}
357
+ </div>
358
+
359
+ {lastClearTime && (
360
+ <div className="flex items-center gap-2 p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-700 rounded text-xs text-green-700 dark:text-green-400">
361
+ <CheckCircle className="w-3 h-3" />
362
+ <span>Cache cleared successfully at {new Date(lastClearTime).toLocaleTimeString()}</span>
363
+ </div>
364
+ )}
365
+ </div>
366
+ );
367
+ }
app/components/@settings/tabs/github/components/GitHubConnection.tsx ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Button } from '~/components/ui/Button';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { useGitHubConnection } from '~/lib/hooks';
6
+
7
+ interface ConnectionTestResult {
8
+ status: 'success' | 'error' | 'testing';
9
+ message: string;
10
+ timestamp?: number;
11
+ }
12
+
13
+ interface GitHubConnectionProps {
14
+ connectionTest: ConnectionTestResult | null;
15
+ onTestConnection: () => void;
16
+ }
17
+
18
+ export function GitHubConnection({ connectionTest, onTestConnection }: GitHubConnectionProps) {
19
+ const { isConnected, isLoading, isConnecting, connect, disconnect, error } = useGitHubConnection();
20
+
21
+ const [token, setToken] = React.useState('');
22
+ const [tokenType, setTokenType] = React.useState<'classic' | 'fine-grained'>('classic');
23
+
24
+ const handleConnect = async (e: React.FormEvent) => {
25
+ e.preventDefault();
26
+ console.log('handleConnect called with token:', token ? 'token provided' : 'no token', 'tokenType:', tokenType);
27
+
28
+ if (!token.trim()) {
29
+ console.log('No token provided, returning early');
30
+ return;
31
+ }
32
+
33
+ try {
34
+ console.log('Calling connect function...');
35
+ await connect(token, tokenType);
36
+ console.log('Connect function completed successfully');
37
+ setToken(''); // Clear token on successful connection
38
+ } catch (error) {
39
+ console.log('Connect function failed:', error);
40
+
41
+ // Error handling is done in the hook
42
+ }
43
+ };
44
+
45
+ if (isLoading) {
46
+ return (
47
+ <div className="flex items-center justify-center p-8">
48
+ <div className="flex items-center gap-2">
49
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
50
+ <span className="text-bolt-elements-textSecondary">Loading connection...</span>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <motion.div
58
+ className="bg-bolt-elements-background dark:bg-bolt-elements-background border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor rounded-lg"
59
+ initial={{ opacity: 0, y: 20 }}
60
+ animate={{ opacity: 1, y: 0 }}
61
+ transition={{ delay: 0.2 }}
62
+ >
63
+ <div className="p-6 space-y-6">
64
+ {!isConnected && (
65
+ <div className="text-xs text-bolt-elements-textSecondary bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1 p-3 rounded-lg mb-4">
66
+ <p className="flex items-center gap-1 mb-1">
67
+ <span className="i-ph:lightbulb w-3.5 h-3.5 text-bolt-elements-icon-success dark:text-bolt-elements-icon-success" />
68
+ <span className="font-medium">Tip:</span> You can also set the{' '}
69
+ <code className="px-1 py-0.5 bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-2 rounded">
70
+ VITE_GITHUB_ACCESS_TOKEN
71
+ </code>{' '}
72
+ environment variable to connect automatically.
73
+ </p>
74
+ <p>
75
+ For fine-grained tokens, also set{' '}
76
+ <code className="px-1 py-0.5 bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-2 rounded">
77
+ VITE_GITHUB_TOKEN_TYPE=fine-grained
78
+ </code>
79
+ </p>
80
+ </div>
81
+ )}
82
+
83
+ <form onSubmit={handleConnect} className="space-y-4">
84
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
85
+ <div>
86
+ <label className="block text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mb-2">
87
+ Token Type
88
+ </label>
89
+ <select
90
+ value={tokenType}
91
+ onChange={(e) => setTokenType(e.target.value as 'classic' | 'fine-grained')}
92
+ disabled={isConnecting || isConnected}
93
+ className={classNames(
94
+ 'w-full px-3 py-2 rounded-lg text-sm',
95
+ 'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1',
96
+ 'border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor',
97
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
98
+ 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-item-contentAccent dark:focus:ring-bolt-elements-item-contentAccent',
99
+ 'disabled:opacity-50',
100
+ )}
101
+ >
102
+ <option value="classic">Personal Access Token (Classic)</option>
103
+ <option value="fine-grained">Fine-grained Token</option>
104
+ </select>
105
+ </div>
106
+
107
+ <div>
108
+ <label className="block text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mb-2">
109
+ {tokenType === 'classic' ? 'Personal Access Token' : 'Fine-grained Token'}
110
+ </label>
111
+ <input
112
+ type="password"
113
+ value={token}
114
+ onChange={(e) => setToken(e.target.value)}
115
+ disabled={isConnecting || isConnected}
116
+ placeholder={`Enter your GitHub ${
117
+ tokenType === 'classic' ? 'personal access token' : 'fine-grained token'
118
+ }`}
119
+ className={classNames(
120
+ 'w-full px-3 py-2 rounded-lg text-sm',
121
+ 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
122
+ 'border border-[#E5E5E5] dark:border-[#333333]',
123
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
124
+ 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColorActive',
125
+ 'disabled:opacity-50',
126
+ )}
127
+ />
128
+ <div className="mt-2 text-sm text-bolt-elements-textSecondary">
129
+ <a
130
+ href={`https://github.com/settings/tokens${tokenType === 'fine-grained' ? '/beta' : '/new'}`}
131
+ target="_blank"
132
+ rel="noopener noreferrer"
133
+ className="text-bolt-elements-borderColorActive hover:underline inline-flex items-center gap-1"
134
+ >
135
+ Get your token
136
+ <div className="i-ph:arrow-square-out w-4 h-4" />
137
+ </a>
138
+ <span className="mx-2">•</span>
139
+ <span>
140
+ Required scopes:{' '}
141
+ {tokenType === 'classic' ? 'repo, read:org, read:user' : 'Repository access, Organization access'}
142
+ </span>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ {error && (
148
+ <div className="p-4 rounded-lg bg-red-50 border border-red-200 dark:bg-red-900/20 dark:border-red-700">
149
+ <p className="text-sm text-red-800 dark:text-red-200">{error}</p>
150
+ </div>
151
+ )}
152
+
153
+ <div className="flex items-center justify-between">
154
+ {!isConnected ? (
155
+ <button
156
+ type="submit"
157
+ disabled={isConnecting || !token.trim()}
158
+ className={classNames(
159
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
160
+ 'bg-[#303030] text-white',
161
+ 'hover:bg-[#5E41D0] hover:text-white',
162
+ 'disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200',
163
+ 'transform active:scale-95',
164
+ )}
165
+ >
166
+ {isConnecting ? (
167
+ <>
168
+ <div className="i-ph:spinner-gap animate-spin" />
169
+ Connecting...
170
+ </>
171
+ ) : (
172
+ <>
173
+ <div className="i-ph:plug-charging w-4 h-4" />
174
+ Connect
175
+ </>
176
+ )}
177
+ </button>
178
+ ) : (
179
+ <div className="flex items-center justify-between w-full">
180
+ <div className="flex items-center gap-4">
181
+ <button
182
+ onClick={disconnect}
183
+ type="button"
184
+ className={classNames(
185
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
186
+ 'bg-red-500 text-white',
187
+ 'hover:bg-red-600',
188
+ )}
189
+ >
190
+ <div className="i-ph:plug w-4 h-4" />
191
+ Disconnect
192
+ </button>
193
+ <span className="text-sm text-bolt-elements-textSecondary flex items-center gap-1">
194
+ <div className="i-ph:check-circle w-4 h-4 text-green-500" />
195
+ Connected to GitHub
196
+ </span>
197
+ </div>
198
+ <div className="flex items-center gap-2">
199
+ <Button
200
+ variant="outline"
201
+ onClick={() => window.open('https://github.com/dashboard', '_blank', 'noopener,noreferrer')}
202
+ className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimary transition-colors"
203
+ >
204
+ <div className="i-ph:layout w-4 h-4" />
205
+ Dashboard
206
+ </Button>
207
+ <Button
208
+ onClick={onTestConnection}
209
+ disabled={connectionTest?.status === 'testing'}
210
+ variant="outline"
211
+ className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimary transition-colors"
212
+ >
213
+ {connectionTest?.status === 'testing' ? (
214
+ <>
215
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
216
+ Testing...
217
+ </>
218
+ ) : (
219
+ <>
220
+ <div className="i-ph:plug-charging w-4 h-4" />
221
+ Test Connection
222
+ </>
223
+ )}
224
+ </Button>
225
+ </div>
226
+ </div>
227
+ )}
228
+ </div>
229
+ </form>
230
+ </div>
231
+ </motion.div>
232
+ );
233
+ }
app/components/@settings/tabs/github/components/GitHubErrorBoundary.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { Component } from 'react';
2
+ import type { ReactNode, ErrorInfo } from 'react';
3
+ import { Button } from '~/components/ui/Button';
4
+ import { AlertTriangle } from 'lucide-react';
5
+
6
+ interface Props {
7
+ children: ReactNode;
8
+ fallback?: ReactNode;
9
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
10
+ }
11
+
12
+ interface State {
13
+ hasError: boolean;
14
+ error: Error | null;
15
+ }
16
+
17
+ export class GitHubErrorBoundary extends Component<Props, State> {
18
+ constructor(props: Props) {
19
+ super(props);
20
+ this.state = { hasError: false, error: null };
21
+ }
22
+
23
+ static getDerivedStateFromError(error: Error): State {
24
+ return { hasError: true, error };
25
+ }
26
+
27
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
28
+ console.error('GitHub Error Boundary caught an error:', error, errorInfo);
29
+
30
+ if (this.props.onError) {
31
+ this.props.onError(error, errorInfo);
32
+ }
33
+ }
34
+
35
+ handleRetry = () => {
36
+ this.setState({ hasError: false, error: null });
37
+ };
38
+
39
+ render() {
40
+ if (this.state.hasError) {
41
+ if (this.props.fallback) {
42
+ return this.props.fallback;
43
+ }
44
+
45
+ return (
46
+ <div className="flex flex-col items-center justify-center p-8 text-center space-y-4 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-lg">
47
+ <div className="w-12 h-12 rounded-full bg-red-50 dark:bg-red-900/20 flex items-center justify-center">
48
+ <AlertTriangle className="w-6 h-6 text-red-500" />
49
+ </div>
50
+
51
+ <div>
52
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-2">GitHub Integration Error</h3>
53
+ <p className="text-sm text-bolt-elements-textSecondary mb-4 max-w-md">
54
+ Something went wrong while loading GitHub data. This could be due to network issues, API limits, or a
55
+ temporary problem.
56
+ </p>
57
+
58
+ {this.state.error && (
59
+ <details className="text-xs text-bolt-elements-textTertiary mb-4">
60
+ <summary className="cursor-pointer hover:text-bolt-elements-textSecondary">Show error details</summary>
61
+ <pre className="mt-2 p-2 bg-bolt-elements-background-depth-2 rounded text-left overflow-auto">
62
+ {this.state.error.message}
63
+ </pre>
64
+ </details>
65
+ )}
66
+ </div>
67
+
68
+ <div className="flex gap-2">
69
+ <Button variant="outline" size="sm" onClick={this.handleRetry}>
70
+ Try Again
71
+ </Button>
72
+ <Button variant="outline" size="sm" onClick={() => window.location.reload()}>
73
+ Reload Page
74
+ </Button>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ return this.props.children;
81
+ }
82
+ }
83
+
84
+ // Higher-order component for wrapping components with error boundary
85
+ export function withGitHubErrorBoundary<P extends object>(component: React.ComponentType<P>) {
86
+ return function WrappedComponent(props: P) {
87
+ return <GitHubErrorBoundary>{React.createElement(component, props)}</GitHubErrorBoundary>;
88
+ };
89
+ }
90
+
91
+ // Hook for handling async errors in GitHub operations
92
+ export function useGitHubErrorHandler() {
93
+ const handleError = React.useCallback((error: unknown, context?: string) => {
94
+ console.error(`GitHub Error ${context ? `(${context})` : ''}:`, error);
95
+
96
+ /*
97
+ * You could integrate with error tracking services here
98
+ * For example: Sentry, LogRocket, etc.
99
+ */
100
+
101
+ return error instanceof Error ? error.message : 'An unknown error occurred';
102
+ }, []);
103
+
104
+ return { handleError };
105
+ }