lattmamb commited on
Commit
6859d42
·
verified ·
1 Parent(s): 85ed004

Upload 1138 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.gitattributes CHANGED
@@ -33,3 +33,15 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ _greenlet.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
37
+ _psycopg.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
38
+ _pydantic_core.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
39
+ collections.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
40
+ immutabledict.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
41
+ pgproto.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
42
+ processors.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
43
+ protocol.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
44
+ python3.9 filter=lfs diff=lfs merge=lfs -text
45
+ resultproxy.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
46
+ t64.exe filter=lfs diff=lfs merge=lfs -text
47
+ util.cpython-39-darwin.so filter=lfs diff=lfs merge=lfs -text
22dc0511fe69_add_toolsource_table.cpython-311.pyc ADDED
Binary file (1.33 kB). View file
 
2ea570019b8f_add_apikey_table.cpython-311.pyc ADDED
Binary file (4.54 kB). View file
 
2ea570019b8f_add_apikey_table.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add ApiKey table
2
+
3
+ Revision ID: 2ea570019b8f
4
+ Revises: 4af13678b83c
5
+ Create Date: 2025-05-03 18:56:32.989446
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ from sqlalchemy.dialects import postgresql
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '2ea570019b8f'
16
+ down_revision: Union[str, None] = '4af13678b83c'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - adjusted ###
24
+ op.create_table('api_keys',
25
+ sa.Column('id', sa.UUID(), nullable=False),
26
+ sa.Column('user_id', sa.UUID(), nullable=False),
27
+ sa.Column('name', sa.String(), nullable=False),
28
+ sa.Column('description', sa.Text(), nullable=True),
29
+ sa.Column('key_prefix', sa.String(length=8), nullable=False),
30
+ sa.Column('hashed_key', sa.String(), nullable=False),
31
+ sa.Column('scopes', postgresql.ARRAY(sa.String()), nullable=True),
32
+ sa.Column('is_active', sa.Boolean(), nullable=False),
33
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
34
+ sa.Column('last_used_at', sa.DateTime(timezone=True), nullable=True),
35
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_api_keys_user_id_users_id')),
36
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_api_keys'))
37
+ )
38
+ with op.batch_alter_table('api_keys', schema=None) as batch_op:
39
+ batch_op.create_index(batch_op.f('ix_api_keys_hashed_key'), ['hashed_key'], unique=False)
40
+ batch_op.create_index(batch_op.f('ix_api_keys_key_prefix'), ['key_prefix'], unique=True)
41
+ batch_op.create_index(batch_op.f('ix_api_keys_user_id'), ['user_id'], unique=False)
42
+
43
+ # Removed incorrect drop/alter table commands for other tables
44
+ # ### end Alembic commands ###
45
+
46
+
47
+ def downgrade() -> None:
48
+ """Downgrade schema."""
49
+ # ### commands auto generated by Alembic - adjusted ###
50
+ with op.batch_alter_table('api_keys', schema=None) as batch_op:
51
+ batch_op.drop_index(batch_op.f('ix_api_keys_user_id'))
52
+ batch_op.drop_index(batch_op.f('ix_api_keys_key_prefix'))
53
+ batch_op.drop_index(batch_op.f('ix_api_keys_hashed_key'))
54
+
55
+ op.drop_table('api_keys')
56
+ # Removed incorrect create/alter table commands for other tables
57
+ # ### end Alembic commands ###
58
+
4af13678b83c_add_toolsource_table.cpython-311.pyc ADDED
Binary file (3.61 kB). View file
 
4af13678b83c_add_toolsource_table.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Add ToolSource table
2
+
3
+ Revision ID: 4af13678b83c
4
+ Revises: e2ca2546bf71
5
+ Create Date: 2025-05-03 18:51:11.601728
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ from sqlalchemy.dialects import postgresql
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '4af13678b83c'
16
+ down_revision: Union[str, None] = 'e2ca2546bf71'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ # Manually corrected: Remove incorrect drop commands and add create_table for tool_sources
25
+ op.create_table('tool_sources',
26
+ sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
27
+ sa.Column('github_url', sa.String(), nullable=False),
28
+ sa.Column('description', sa.Text(), nullable=True),
29
+ sa.Column('status', sa.String(), nullable=False, server_default='active'), # Match default from model
30
+ sa.Column('last_checked_at', sa.DateTime(timezone=True), nullable=True),
31
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
32
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_tool_sources'))
33
+ )
34
+ with op.batch_alter_table('tool_sources', schema=None) as batch_op:
35
+ batch_op.create_index(batch_op.f('ix_tool_sources_github_url'), ['github_url'], unique=True)
36
+ batch_op.create_index(batch_op.f('ix_tool_sources_status'), ['status'], unique=False)
37
+
38
+ # ### end Alembic commands ###
39
+
40
+
41
+ def downgrade() -> None:
42
+ """Downgrade schema."""
43
+ # ### commands auto generated by Alembic - please adjust! ###
44
+ with op.batch_alter_table('tool_sources', schema=None) as batch_op:
45
+ batch_op.drop_index(batch_op.f('ix_tool_sources_status'))
46
+ batch_op.drop_index(batch_op.f('ix_tool_sources_github_url'))
47
+
48
+ op.drop_table('tool_sources')
49
+ # ### end Alembic commands ###
50
+
APIKeyManager.tsx ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { IconButton } from '~/components/ui/IconButton';
3
+ import type { ProviderInfo } from '~/types/model';
4
+ import Cookies from 'js-cookie';
5
+
6
+ interface APIKeyManagerProps {
7
+ provider: ProviderInfo;
8
+ apiKey: string;
9
+ setApiKey: (key: string) => void;
10
+ getApiKeyLink?: string;
11
+ labelForGetApiKey?: string;
12
+ }
13
+
14
+ // cache which stores whether the provider's API key is set via environment variable
15
+ const providerEnvKeyStatusCache: Record<string, boolean> = {};
16
+
17
+ const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
18
+
19
+ export function getApiKeysFromCookies() {
20
+ const storedApiKeys = Cookies.get('apiKeys');
21
+ let parsedKeys: Record<string, string> = {};
22
+
23
+ if (storedApiKeys) {
24
+ parsedKeys = apiKeyMemoizeCache[storedApiKeys];
25
+
26
+ if (!parsedKeys) {
27
+ parsedKeys = apiKeyMemoizeCache[storedApiKeys] = JSON.parse(storedApiKeys);
28
+ }
29
+ }
30
+
31
+ return parsedKeys;
32
+ }
33
+
34
+ // eslint-disable-next-line @typescript-eslint/naming-convention
35
+ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
36
+ const [isEditing, setIsEditing] = useState(false);
37
+ const [tempKey, setTempKey] = useState(apiKey);
38
+ const [isEnvKeySet, setIsEnvKeySet] = useState(false);
39
+
40
+ // Reset states and load saved key when provider changes
41
+ useEffect(() => {
42
+ // Load saved API key from cookies for this provider
43
+ const savedKeys = getApiKeysFromCookies();
44
+ const savedKey = savedKeys[provider.name] || '';
45
+
46
+ setTempKey(savedKey);
47
+ setApiKey(savedKey);
48
+ setIsEditing(false);
49
+ }, [provider.name]);
50
+
51
+ const checkEnvApiKey = useCallback(async () => {
52
+ // Check cache first
53
+ if (providerEnvKeyStatusCache[provider.name] !== undefined) {
54
+ setIsEnvKeySet(providerEnvKeyStatusCache[provider.name]);
55
+ return;
56
+ }
57
+
58
+ try {
59
+ const response = await fetch(`/api/check-env-key?provider=${encodeURIComponent(provider.name)}`);
60
+ const data = await response.json();
61
+ const isSet = (data as { isSet: boolean }).isSet;
62
+
63
+ // Cache the result
64
+ providerEnvKeyStatusCache[provider.name] = isSet;
65
+ setIsEnvKeySet(isSet);
66
+ } catch (error) {
67
+ console.error('Failed to check environment API key:', error);
68
+ setIsEnvKeySet(false);
69
+ }
70
+ }, [provider.name]);
71
+
72
+ useEffect(() => {
73
+ checkEnvApiKey();
74
+ }, [checkEnvApiKey]);
75
+
76
+ const handleSave = () => {
77
+ // Save to parent state
78
+ setApiKey(tempKey);
79
+
80
+ // Save to cookies
81
+ const currentKeys = getApiKeysFromCookies();
82
+ const newKeys = { ...currentKeys, [provider.name]: tempKey };
83
+ Cookies.set('apiKeys', JSON.stringify(newKeys));
84
+
85
+ setIsEditing(false);
86
+ };
87
+
88
+ return (
89
+ <div className="flex items-center justify-between py-3 px-1">
90
+ <div className="flex items-center gap-2 flex-1">
91
+ <div className="flex items-center gap-2">
92
+ <span className="text-sm font-medium text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
93
+ {!isEditing && (
94
+ <div className="flex items-center gap-2">
95
+ {apiKey ? (
96
+ <>
97
+ <div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
98
+ <span className="text-xs text-green-500">Set via UI</span>
99
+ </>
100
+ ) : isEnvKeySet ? (
101
+ <>
102
+ <div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
103
+ <span className="text-xs text-green-500">Set via environment variable</span>
104
+ </>
105
+ ) : (
106
+ <>
107
+ <div className="i-ph:x-circle-fill text-red-500 w-4 h-4" />
108
+ <span className="text-xs text-red-500">Not Set (Please set via UI or ENV_VAR)</span>
109
+ </>
110
+ )}
111
+ </div>
112
+ )}
113
+ </div>
114
+ </div>
115
+
116
+ <div className="flex items-center gap-2 shrink-0">
117
+ {isEditing ? (
118
+ <div className="flex items-center gap-2">
119
+ <input
120
+ type="password"
121
+ value={tempKey}
122
+ placeholder="Enter API Key"
123
+ onChange={(e) => setTempKey(e.target.value)}
124
+ className="w-[300px] px-3 py-1.5 text-sm rounded border border-bolt-elements-borderColor
125
+ bg-bolt-elements-prompt-background text-bolt-elements-textPrimary
126
+ focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
127
+ />
128
+ <IconButton
129
+ onClick={handleSave}
130
+ title="Save API Key"
131
+ className="bg-green-500/10 hover:bg-green-500/20 text-green-500"
132
+ >
133
+ <div className="i-ph:check w-4 h-4" />
134
+ </IconButton>
135
+ <IconButton
136
+ onClick={() => setIsEditing(false)}
137
+ title="Cancel"
138
+ className="bg-red-500/10 hover:bg-red-500/20 text-red-500"
139
+ >
140
+ <div className="i-ph:x w-4 h-4" />
141
+ </IconButton>
142
+ </div>
143
+ ) : (
144
+ <>
145
+ {
146
+ <IconButton
147
+ onClick={() => setIsEditing(true)}
148
+ title="Edit API Key"
149
+ className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500"
150
+ >
151
+ <div className="i-ph:pencil-simple w-4 h-4" />
152
+ </IconButton>
153
+ }
154
+ {provider?.getApiKeyLink && !apiKey && (
155
+ <IconButton
156
+ onClick={() => window.open(provider?.getApiKeyLink)}
157
+ title="Get API Key"
158
+ className="bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 flex items-center gap-2"
159
+ >
160
+ <span className="text-xs whitespace-nowrap">{provider?.labelForGetApiKey || 'Get API Key'}</span>
161
+ <div className={`${provider?.icon || 'i-ph:key'} w-4 h-4`} />
162
+ </IconButton>
163
+ )}
164
+ </>
165
+ )}
166
+ </div>
167
+ </div>
168
+ );
169
+ };
AUTHORS ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Main contributors
2
+ =================
3
+
4
+ MagicStack Inc.:
5
+ Elvis Pranskevichus <elvis@magic.io>
6
+ Yury Selivanov <yury@magic.io>
Activate.ps1 ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <#
2
+ .Synopsis
3
+ Activate a Python virtual environment for the current PowerShell session.
4
+
5
+ .Description
6
+ Pushes the python executable for a virtual environment to the front of the
7
+ $Env:PATH environment variable and sets the prompt to signify that you are
8
+ in a Python virtual environment. Makes use of the command line switches as
9
+ well as the `pyvenv.cfg` file values present in the virtual environment.
10
+
11
+ .Parameter VenvDir
12
+ Path to the directory that contains the virtual environment to activate. The
13
+ default value for this is the parent of the directory that the Activate.ps1
14
+ script is located within.
15
+
16
+ .Parameter Prompt
17
+ The prompt prefix to display when this virtual environment is activated. By
18
+ default, this prompt is the name of the virtual environment folder (VenvDir)
19
+ surrounded by parentheses and followed by a single space (ie. '(.venv) ').
20
+
21
+ .Example
22
+ Activate.ps1
23
+ Activates the Python virtual environment that contains the Activate.ps1 script.
24
+
25
+ .Example
26
+ Activate.ps1 -Verbose
27
+ Activates the Python virtual environment that contains the Activate.ps1 script,
28
+ and shows extra information about the activation as it executes.
29
+
30
+ .Example
31
+ Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
32
+ Activates the Python virtual environment located in the specified location.
33
+
34
+ .Example
35
+ Activate.ps1 -Prompt "MyPython"
36
+ Activates the Python virtual environment that contains the Activate.ps1 script,
37
+ and prefixes the current prompt with the specified string (surrounded in
38
+ parentheses) while the virtual environment is active.
39
+
40
+ .Notes
41
+ On Windows, it may be required to enable this Activate.ps1 script by setting the
42
+ execution policy for the user. You can do this by issuing the following PowerShell
43
+ command:
44
+
45
+ PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
46
+
47
+ For more information on Execution Policies:
48
+ https://go.microsoft.com/fwlink/?LinkID=135170
49
+
50
+ #>
51
+ Param(
52
+ [Parameter(Mandatory = $false)]
53
+ [String]
54
+ $VenvDir,
55
+ [Parameter(Mandatory = $false)]
56
+ [String]
57
+ $Prompt
58
+ )
59
+
60
+ <# Function declarations --------------------------------------------------- #>
61
+
62
+ <#
63
+ .Synopsis
64
+ Remove all shell session elements added by the Activate script, including the
65
+ addition of the virtual environment's Python executable from the beginning of
66
+ the PATH variable.
67
+
68
+ .Parameter NonDestructive
69
+ If present, do not remove this function from the global namespace for the
70
+ session.
71
+
72
+ #>
73
+ function global:deactivate ([switch]$NonDestructive) {
74
+ # Revert to original values
75
+
76
+ # The prior prompt:
77
+ if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
78
+ Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
79
+ Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
80
+ }
81
+
82
+ # The prior PYTHONHOME:
83
+ if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
84
+ Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
85
+ Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
86
+ }
87
+
88
+ # The prior PATH:
89
+ if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
90
+ Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
91
+ Remove-Item -Path Env:_OLD_VIRTUAL_PATH
92
+ }
93
+
94
+ # Just remove the VIRTUAL_ENV altogether:
95
+ if (Test-Path -Path Env:VIRTUAL_ENV) {
96
+ Remove-Item -Path env:VIRTUAL_ENV
97
+ }
98
+
99
+ # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
100
+ if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
101
+ Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
102
+ }
103
+
104
+ # Leave deactivate function in the global namespace if requested:
105
+ if (-not $NonDestructive) {
106
+ Remove-Item -Path function:deactivate
107
+ }
108
+ }
109
+
110
+ <#
111
+ .Description
112
+ Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
113
+ given folder, and returns them in a map.
114
+
115
+ For each line in the pyvenv.cfg file, if that line can be parsed into exactly
116
+ two strings separated by `=` (with any amount of whitespace surrounding the =)
117
+ then it is considered a `key = value` line. The left hand string is the key,
118
+ the right hand is the value.
119
+
120
+ If the value starts with a `'` or a `"` then the first and last character is
121
+ stripped from the value before being captured.
122
+
123
+ .Parameter ConfigDir
124
+ Path to the directory that contains the `pyvenv.cfg` file.
125
+ #>
126
+ function Get-PyVenvConfig(
127
+ [String]
128
+ $ConfigDir
129
+ ) {
130
+ Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
131
+
132
+ # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
133
+ $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
134
+
135
+ # An empty map will be returned if no config file is found.
136
+ $pyvenvConfig = @{ }
137
+
138
+ if ($pyvenvConfigPath) {
139
+
140
+ Write-Verbose "File exists, parse `key = value` lines"
141
+ $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
142
+
143
+ $pyvenvConfigContent | ForEach-Object {
144
+ $keyval = $PSItem -split "\s*=\s*", 2
145
+ if ($keyval[0] -and $keyval[1]) {
146
+ $val = $keyval[1]
147
+
148
+ # Remove extraneous quotations around a string value.
149
+ if ("'""".Contains($val.Substring(0, 1))) {
150
+ $val = $val.Substring(1, $val.Length - 2)
151
+ }
152
+
153
+ $pyvenvConfig[$keyval[0]] = $val
154
+ Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
155
+ }
156
+ }
157
+ }
158
+ return $pyvenvConfig
159
+ }
160
+
161
+
162
+ <# Begin Activate script --------------------------------------------------- #>
163
+
164
+ # Determine the containing directory of this script
165
+ $VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
166
+ $VenvExecDir = Get-Item -Path $VenvExecPath
167
+
168
+ Write-Verbose "Activation script is located in path: '$VenvExecPath'"
169
+ Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
170
+ Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
171
+
172
+ # Set values required in priority: CmdLine, ConfigFile, Default
173
+ # First, get the location of the virtual environment, it might not be
174
+ # VenvExecDir if specified on the command line.
175
+ if ($VenvDir) {
176
+ Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
177
+ }
178
+ else {
179
+ Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
180
+ $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
181
+ Write-Verbose "VenvDir=$VenvDir"
182
+ }
183
+
184
+ # Next, read the `pyvenv.cfg` file to determine any required value such
185
+ # as `prompt`.
186
+ $pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
187
+
188
+ # Next, set the prompt from the command line, or the config file, or
189
+ # just use the name of the virtual environment folder.
190
+ if ($Prompt) {
191
+ Write-Verbose "Prompt specified as argument, using '$Prompt'"
192
+ }
193
+ else {
194
+ Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
195
+ if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
196
+ Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
197
+ $Prompt = $pyvenvCfg['prompt'];
198
+ }
199
+ else {
200
+ Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)"
201
+ Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
202
+ $Prompt = Split-Path -Path $venvDir -Leaf
203
+ }
204
+ }
205
+
206
+ Write-Verbose "Prompt = '$Prompt'"
207
+ Write-Verbose "VenvDir='$VenvDir'"
208
+
209
+ # Deactivate any currently active virtual environment, but leave the
210
+ # deactivate function in place.
211
+ deactivate -nondestructive
212
+
213
+ # Now set the environment variable VIRTUAL_ENV, used by many tools to determine
214
+ # that there is an activated venv.
215
+ $env:VIRTUAL_ENV = $VenvDir
216
+
217
+ if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
218
+
219
+ Write-Verbose "Setting prompt to '$Prompt'"
220
+
221
+ # Set the prompt to include the env name
222
+ # Make sure _OLD_VIRTUAL_PROMPT is global
223
+ function global:_OLD_VIRTUAL_PROMPT { "" }
224
+ Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
225
+ New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
226
+
227
+ function global:prompt {
228
+ Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
229
+ _OLD_VIRTUAL_PROMPT
230
+ }
231
+ }
232
+
233
+ # Clear PYTHONHOME
234
+ if (Test-Path -Path Env:PYTHONHOME) {
235
+ Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
236
+ Remove-Item -Path Env:PYTHONHOME
237
+ }
238
+
239
+ # Add the venv to the PATH
240
+ Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
241
+ $Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
Artifact.tsx ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useStore } from '@nanostores/react';
2
+ import { AnimatePresence, motion } from 'framer-motion';
3
+ import { computed } from 'nanostores';
4
+ import { memo, useEffect, useRef, useState } from 'react';
5
+ import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki';
6
+ import type { ActionState } from '~/lib/runtime/action-runner';
7
+ import { workbenchStore } from '~/lib/stores/workbench';
8
+ import { classNames } from '~/utils/classNames';
9
+ import { cubicEasingFn } from '~/utils/easings';
10
+ import { WORK_DIR } from '~/utils/constants';
11
+
12
+ const highlighterOptions = {
13
+ langs: ['shell'],
14
+ themes: ['light-plus', 'dark-plus'],
15
+ };
16
+
17
+ const shellHighlighter: HighlighterGeneric<BundledLanguage, BundledTheme> =
18
+ import.meta.hot?.data.shellHighlighter ?? (await createHighlighter(highlighterOptions));
19
+
20
+ if (import.meta.hot) {
21
+ import.meta.hot.data.shellHighlighter = shellHighlighter;
22
+ }
23
+
24
+ interface ArtifactProps {
25
+ messageId: string;
26
+ }
27
+
28
+ export const Artifact = memo(({ messageId }: ArtifactProps) => {
29
+ const userToggledActions = useRef(false);
30
+ const [showActions, setShowActions] = useState(false);
31
+ const [allActionFinished, setAllActionFinished] = useState(false);
32
+
33
+ const artifacts = useStore(workbenchStore.artifacts);
34
+ const artifact = artifacts[messageId];
35
+
36
+ const actions = useStore(
37
+ computed(artifact.runner.actions, (actions) => {
38
+ return Object.values(actions);
39
+ }),
40
+ );
41
+
42
+ const toggleActions = () => {
43
+ userToggledActions.current = true;
44
+ setShowActions(!showActions);
45
+ };
46
+
47
+ useEffect(() => {
48
+ if (actions.length && !showActions && !userToggledActions.current) {
49
+ setShowActions(true);
50
+ }
51
+
52
+ if (actions.length !== 0 && artifact.type === 'bundled') {
53
+ const finished = !actions.find((action) => action.status !== 'complete');
54
+
55
+ if (allActionFinished !== finished) {
56
+ setAllActionFinished(finished);
57
+ }
58
+ }
59
+ }, [actions]);
60
+
61
+ return (
62
+ <div className="artifact border border-bolt-elements-borderColor flex flex-col overflow-hidden rounded-lg w-full transition-border duration-150">
63
+ <div className="flex">
64
+ <button
65
+ className="flex items-stretch bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover w-full overflow-hidden"
66
+ onClick={() => {
67
+ const showWorkbench = workbenchStore.showWorkbench.get();
68
+ workbenchStore.showWorkbench.set(!showWorkbench);
69
+ }}
70
+ >
71
+ {artifact.type == 'bundled' && (
72
+ <>
73
+ <div className="p-4">
74
+ {allActionFinished ? (
75
+ <div className={'i-ph:files-light'} style={{ fontSize: '2rem' }}></div>
76
+ ) : (
77
+ <div className={'i-svg-spinners:90-ring-with-bg'} style={{ fontSize: '2rem' }}></div>
78
+ )}
79
+ </div>
80
+ <div className="bg-bolt-elements-artifacts-borderColor w-[1px]" />
81
+ </>
82
+ )}
83
+ <div className="px-5 p-3.5 w-full text-left">
84
+ <div className="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">{artifact?.title}</div>
85
+ <div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">Click to open Workbench</div>
86
+ </div>
87
+ </button>
88
+ <div className="bg-bolt-elements-artifacts-borderColor w-[1px]" />
89
+ <AnimatePresence>
90
+ {actions.length && artifact.type !== 'bundled' && (
91
+ <motion.button
92
+ initial={{ width: 0 }}
93
+ animate={{ width: 'auto' }}
94
+ exit={{ width: 0 }}
95
+ transition={{ duration: 0.15, ease: cubicEasingFn }}
96
+ className="bg-bolt-elements-artifacts-background hover:bg-bolt-elements-artifacts-backgroundHover"
97
+ onClick={toggleActions}
98
+ >
99
+ <div className="p-4">
100
+ <div className={showActions ? 'i-ph:caret-up-bold' : 'i-ph:caret-down-bold'}></div>
101
+ </div>
102
+ </motion.button>
103
+ )}
104
+ </AnimatePresence>
105
+ </div>
106
+ <AnimatePresence>
107
+ {artifact.type !== 'bundled' && showActions && actions.length > 0 && (
108
+ <motion.div
109
+ className="actions"
110
+ initial={{ height: 0 }}
111
+ animate={{ height: 'auto' }}
112
+ exit={{ height: '0px' }}
113
+ transition={{ duration: 0.15 }}
114
+ >
115
+ <div className="bg-bolt-elements-artifacts-borderColor h-[1px]" />
116
+
117
+ <div className="p-5 text-left bg-bolt-elements-actions-background">
118
+ <ActionList actions={actions} />
119
+ </div>
120
+ </motion.div>
121
+ )}
122
+ </AnimatePresence>
123
+ </div>
124
+ );
125
+ });
126
+
127
+ interface ShellCodeBlockProps {
128
+ classsName?: string;
129
+ code: string;
130
+ }
131
+
132
+ function ShellCodeBlock({ classsName, code }: ShellCodeBlockProps) {
133
+ return (
134
+ <div
135
+ className={classNames('text-xs', classsName)}
136
+ dangerouslySetInnerHTML={{
137
+ __html: shellHighlighter.codeToHtml(code, {
138
+ lang: 'shell',
139
+ theme: 'dark-plus',
140
+ }),
141
+ }}
142
+ ></div>
143
+ );
144
+ }
145
+
146
+ interface ActionListProps {
147
+ actions: ActionState[];
148
+ }
149
+
150
+ const actionVariants = {
151
+ hidden: { opacity: 0, y: 20 },
152
+ visible: { opacity: 1, y: 0 },
153
+ };
154
+
155
+ function openArtifactInWorkbench(filePath: any) {
156
+ if (workbenchStore.currentView.get() !== 'code') {
157
+ workbenchStore.currentView.set('code');
158
+ }
159
+
160
+ workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`);
161
+ }
162
+
163
+ const ActionList = memo(({ actions }: ActionListProps) => {
164
+ return (
165
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
166
+ <ul className="list-none space-y-2.5">
167
+ {actions.map((action, index) => {
168
+ const { status, type, content } = action;
169
+ const isLast = index === actions.length - 1;
170
+
171
+ return (
172
+ <motion.li
173
+ key={index}
174
+ variants={actionVariants}
175
+ initial="hidden"
176
+ animate="visible"
177
+ transition={{
178
+ duration: 0.2,
179
+ ease: cubicEasingFn,
180
+ }}
181
+ >
182
+ <div className="flex items-center gap-1.5 text-sm">
183
+ <div className={classNames('text-lg', getIconColor(action.status))}>
184
+ {status === 'running' ? (
185
+ <>
186
+ {type !== 'start' ? (
187
+ <div className="i-svg-spinners:90-ring-with-bg"></div>
188
+ ) : (
189
+ <div className="i-ph:terminal-window-duotone"></div>
190
+ )}
191
+ </>
192
+ ) : status === 'pending' ? (
193
+ <div className="i-ph:circle-duotone"></div>
194
+ ) : status === 'complete' ? (
195
+ <div className="i-ph:check"></div>
196
+ ) : status === 'failed' || status === 'aborted' ? (
197
+ <div className="i-ph:x"></div>
198
+ ) : null}
199
+ </div>
200
+ {type === 'file' ? (
201
+ <div>
202
+ Create{' '}
203
+ <code
204
+ className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
205
+ onClick={() => openArtifactInWorkbench(action.filePath)}
206
+ >
207
+ {action.filePath}
208
+ </code>
209
+ </div>
210
+ ) : type === 'shell' ? (
211
+ <div className="flex items-center w-full min-h-[28px]">
212
+ <span className="flex-1">Run command</span>
213
+ </div>
214
+ ) : type === 'start' ? (
215
+ <a
216
+ onClick={(e) => {
217
+ e.preventDefault();
218
+ workbenchStore.currentView.set('preview');
219
+ }}
220
+ className="flex items-center w-full min-h-[28px]"
221
+ >
222
+ <span className="flex-1">Start Application</span>
223
+ </a>
224
+ ) : null}
225
+ </div>
226
+ {(type === 'shell' || type === 'start') && (
227
+ <ShellCodeBlock
228
+ classsName={classNames('mt-1', {
229
+ 'mb-3.5': !isLast,
230
+ })}
231
+ code={content}
232
+ />
233
+ )}
234
+ </motion.li>
235
+ );
236
+ })}
237
+ </ul>
238
+ </motion.div>
239
+ );
240
+ });
241
+
242
+ function getIconColor(status: ActionState['status']) {
243
+ switch (status) {
244
+ case 'pending': {
245
+ return 'text-bolt-elements-textTertiary';
246
+ }
247
+ case 'running': {
248
+ return 'text-bolt-elements-loader-progress';
249
+ }
250
+ case 'complete': {
251
+ return 'text-bolt-elements-icon-success';
252
+ }
253
+ case 'aborted': {
254
+ return 'text-bolt-elements-textSecondary';
255
+ }
256
+ case 'failed': {
257
+ return 'text-bolt-elements-icon-error';
258
+ }
259
+ default: {
260
+ return undefined;
261
+ }
262
+ }
263
+ }
AssistantMessage.tsx ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo } from 'react';
2
+ import { Markdown } from './Markdown';
3
+ import type { JSONValue } from 'ai';
4
+ import Popover from '~/components/ui/Popover';
5
+ import { workbenchStore } from '~/lib/stores/workbench';
6
+ import { WORK_DIR } from '~/utils/constants';
7
+
8
+ interface AssistantMessageProps {
9
+ content: string;
10
+ annotations?: JSONValue[];
11
+ }
12
+
13
+ function openArtifactInWorkbench(filePath: string) {
14
+ filePath = normalizedFilePath(filePath);
15
+
16
+ if (workbenchStore.currentView.get() !== 'code') {
17
+ workbenchStore.currentView.set('code');
18
+ }
19
+
20
+ workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`);
21
+ }
22
+
23
+ function normalizedFilePath(path: string) {
24
+ let normalizedPath = path;
25
+
26
+ if (normalizedPath.startsWith(WORK_DIR)) {
27
+ normalizedPath = path.replace(WORK_DIR, '');
28
+ }
29
+
30
+ if (normalizedPath.startsWith('/')) {
31
+ normalizedPath = normalizedPath.slice(1);
32
+ }
33
+
34
+ return normalizedPath;
35
+ }
36
+
37
+ export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
38
+ const filteredAnnotations = (annotations?.filter(
39
+ (annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
40
+ ) || []) as { type: string; value: any } & { [key: string]: any }[];
41
+
42
+ let chatSummary: string | undefined = undefined;
43
+
44
+ if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) {
45
+ chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary;
46
+ }
47
+
48
+ let codeContext: string[] | undefined = undefined;
49
+
50
+ if (filteredAnnotations.find((annotation) => annotation.type === 'codeContext')) {
51
+ codeContext = filteredAnnotations.find((annotation) => annotation.type === 'codeContext')?.files;
52
+ }
53
+
54
+ const usage: {
55
+ completionTokens: number;
56
+ promptTokens: number;
57
+ totalTokens: number;
58
+ } = filteredAnnotations.find((annotation) => annotation.type === 'usage')?.value;
59
+
60
+ return (
61
+ <div className="overflow-hidden w-full">
62
+ <>
63
+ <div className=" flex gap-2 items-center text-sm text-bolt-elements-textSecondary mb-2">
64
+ {(codeContext || chatSummary) && (
65
+ <Popover side="right" align="start" trigger={<div className="i-ph:info" />}>
66
+ {chatSummary && (
67
+ <div className="max-w-chat">
68
+ <div className="summary max-h-96 flex flex-col">
69
+ <h2 className="border border-bolt-elements-borderColor rounded-md p4">Summary</h2>
70
+ <div style={{ zoom: 0.7 }} className="overflow-y-auto m4">
71
+ <Markdown>{chatSummary}</Markdown>
72
+ </div>
73
+ </div>
74
+ {codeContext && (
75
+ <div className="code-context flex flex-col p4 border border-bolt-elements-borderColor rounded-md">
76
+ <h2>Context</h2>
77
+ <div className="flex gap-4 mt-4 bolt" style={{ zoom: 0.6 }}>
78
+ {codeContext.map((x) => {
79
+ const normalized = normalizedFilePath(x);
80
+ return (
81
+ <>
82
+ <code
83
+ className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
84
+ onClick={(e) => {
85
+ e.preventDefault();
86
+ e.stopPropagation();
87
+ openArtifactInWorkbench(normalized);
88
+ }}
89
+ >
90
+ {normalized}
91
+ </code>
92
+ </>
93
+ );
94
+ })}
95
+ </div>
96
+ </div>
97
+ )}
98
+ </div>
99
+ )}
100
+ <div className="context"></div>
101
+ </Popover>
102
+ )}
103
+ {usage && (
104
+ <div>
105
+ Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
106
+ </div>
107
+ )}
108
+ </div>
109
+ </>
110
+ <Markdown html>{content}</Markdown>
111
+ </div>
112
+ );
113
+ });
AvatarDropdown.tsx ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ const BetaLabel = () => (
9
+ <span className="px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20 text-[10px] font-medium text-purple-600 dark:text-purple-400 ml-2">
10
+ BETA
11
+ </span>
12
+ );
13
+
14
+ interface AvatarDropdownProps {
15
+ onSelectTab: (tab: TabType) => void;
16
+ }
17
+
18
+ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
19
+ const profile = useStore(profileStore) as Profile;
20
+
21
+ return (
22
+ <DropdownMenu.Root>
23
+ <DropdownMenu.Trigger asChild>
24
+ <motion.button
25
+ className="w-10 h-10 rounded-full bg-transparent flex items-center justify-center focus:outline-none"
26
+ whileHover={{ scale: 1.02 }}
27
+ whileTap={{ scale: 0.98 }}
28
+ >
29
+ {profile?.avatar ? (
30
+ <img
31
+ src={profile.avatar}
32
+ alt={profile?.username || 'Profile'}
33
+ className="w-full h-full rounded-full object-cover"
34
+ loading="eager"
35
+ decoding="sync"
36
+ />
37
+ ) : (
38
+ <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">
39
+ <div className="i-ph:question w-6 h-6" />
40
+ </div>
41
+ )}
42
+ </motion.button>
43
+ </DropdownMenu.Trigger>
44
+
45
+ <DropdownMenu.Portal>
46
+ <DropdownMenu.Content
47
+ className={classNames(
48
+ 'min-w-[240px] z-[250]',
49
+ 'bg-white dark:bg-[#141414]',
50
+ 'rounded-lg shadow-lg',
51
+ 'border border-gray-200/50 dark:border-gray-800/50',
52
+ 'animate-in fade-in-0 zoom-in-95',
53
+ 'py-1',
54
+ )}
55
+ sideOffset={5}
56
+ align="end"
57
+ >
58
+ <div
59
+ className={classNames(
60
+ 'px-4 py-3 flex items-center gap-3',
61
+ 'border-b border-gray-200/50 dark:border-gray-800/50',
62
+ )}
63
+ >
64
+ <div className="w-10 h-10 rounded-full overflow-hidden flex-shrink-0 bg-white dark:bg-gray-800 shadow-sm">
65
+ {profile?.avatar ? (
66
+ <img
67
+ src={profile.avatar}
68
+ alt={profile?.username || 'Profile'}
69
+ className={classNames('w-full h-full', 'object-cover', 'transform-gpu', 'image-rendering-crisp')}
70
+ loading="eager"
71
+ decoding="sync"
72
+ />
73
+ ) : (
74
+ <div className="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500 font-medium text-lg">
75
+ <span className="relative -top-0.5">?</span>
76
+ </div>
77
+ )}
78
+ </div>
79
+ <div className="flex-1 min-w-0">
80
+ <div className="font-medium text-sm text-gray-900 dark:text-white truncate">
81
+ {profile?.username || 'Guest User'}
82
+ </div>
83
+ {profile?.bio && <div className="text-xs text-gray-500 dark:text-gray-400 truncate">{profile.bio}</div>}
84
+ </div>
85
+ </div>
86
+
87
+ <DropdownMenu.Item
88
+ className={classNames(
89
+ 'flex items-center gap-2 px-4 py-2.5',
90
+ 'text-sm text-gray-700 dark:text-gray-200',
91
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
92
+ 'hover:text-purple-500 dark:hover:text-purple-400',
93
+ 'cursor-pointer transition-all duration-200',
94
+ 'outline-none',
95
+ 'group',
96
+ )}
97
+ onClick={() => onSelectTab('profile')}
98
+ >
99
+ <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" />
100
+ Edit Profile
101
+ </DropdownMenu.Item>
102
+
103
+ <DropdownMenu.Item
104
+ className={classNames(
105
+ 'flex items-center gap-2 px-4 py-2.5',
106
+ 'text-sm text-gray-700 dark:text-gray-200',
107
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
108
+ 'hover:text-purple-500 dark:hover:text-purple-400',
109
+ 'cursor-pointer transition-all duration-200',
110
+ 'outline-none',
111
+ 'group',
112
+ )}
113
+ onClick={() => onSelectTab('settings')}
114
+ >
115
+ <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" />
116
+ Settings
117
+ </DropdownMenu.Item>
118
+
119
+ <div className="my-1 border-t border-gray-200/50 dark:border-gray-800/50" />
120
+
121
+ <DropdownMenu.Item
122
+ className={classNames(
123
+ 'flex items-center gap-2 px-4 py-2.5',
124
+ 'text-sm text-gray-700 dark:text-gray-200',
125
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
126
+ 'hover:text-purple-500 dark:hover:text-purple-400',
127
+ 'cursor-pointer transition-all duration-200',
128
+ 'outline-none',
129
+ 'group',
130
+ )}
131
+ onClick={() => onSelectTab('task-manager')}
132
+ >
133
+ <div className="i-ph:activity w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
134
+ Task Manager
135
+ <BetaLabel />
136
+ </DropdownMenu.Item>
137
+
138
+ <DropdownMenu.Item
139
+ className={classNames(
140
+ 'flex items-center gap-2 px-4 py-2.5',
141
+ 'text-sm text-gray-700 dark:text-gray-200',
142
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
143
+ 'hover:text-purple-500 dark:hover:text-purple-400',
144
+ 'cursor-pointer transition-all duration-200',
145
+ 'outline-none',
146
+ 'group',
147
+ )}
148
+ onClick={() => onSelectTab('service-status')}
149
+ >
150
+ <div className="i-ph:heartbeat w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
151
+ Service Status
152
+ <BetaLabel />
153
+ </DropdownMenu.Item>
154
+ </DropdownMenu.Content>
155
+ </DropdownMenu.Portal>
156
+ </DropdownMenu.Root>
157
+ );
158
+ };
BaseChat.module.scss ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .BaseChat {
2
+ &[data-chat-visible='false'] {
3
+ --workbench-inner-width: 100%;
4
+ --workbench-left: 0;
5
+
6
+ .Chat {
7
+ --at-apply: bolt-ease-cubic-bezier;
8
+ transition-property: transform, opacity;
9
+ transition-duration: 0.3s;
10
+ will-change: transform, opacity;
11
+ transform: translateX(-50%);
12
+ opacity: 0;
13
+ }
14
+ }
15
+ }
16
+
17
+ .Chat {
18
+ opacity: 1;
19
+ }
20
+
21
+ .PromptEffectContainer {
22
+ --prompt-container-offset: 50px;
23
+ --prompt-line-stroke-width: 1px;
24
+ position: absolute;
25
+ pointer-events: none;
26
+ inset: calc(var(--prompt-container-offset) / -2);
27
+ width: calc(100% + var(--prompt-container-offset));
28
+ height: calc(100% + var(--prompt-container-offset));
29
+ }
30
+
31
+ .PromptEffectLine {
32
+ width: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width));
33
+ height: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width));
34
+ x: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2);
35
+ y: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2);
36
+ rx: calc(8px - var(--prompt-line-stroke-width));
37
+ fill: transparent;
38
+ stroke-width: var(--prompt-line-stroke-width);
39
+ stroke: url(#line-gradient);
40
+ stroke-dasharray: 35px 65px;
41
+ stroke-dashoffset: 10;
42
+ }
43
+
44
+ .PromptShine {
45
+ fill: url(#shine-gradient);
46
+ mix-blend-mode: overlay;
47
+ }
BaseChat.tsx ADDED
@@ -0,0 +1,630 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * @ts-nocheck
3
+ * Preventing TS checks with files presented in the video for a better presentation.
4
+ */
5
+ import type { JSONValue, Message } from 'ai';
6
+ import React, { type RefCallback, useEffect, useState } from 'react';
7
+ import { ClientOnly } from 'remix-utils/client-only';
8
+ import { Menu } from '~/components/sidebar/Menu.client';
9
+ import { IconButton } from '~/components/ui/IconButton';
10
+ import { Workbench } from '~/components/workbench/Workbench.client';
11
+ import { classNames } from '~/utils/classNames';
12
+ import { PROVIDER_LIST } from '~/utils/constants';
13
+ import { Messages } from './Messages.client';
14
+ import { SendButton } from './SendButton.client';
15
+ import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
16
+ import Cookies from 'js-cookie';
17
+ import * as Tooltip from '@radix-ui/react-tooltip';
18
+
19
+ import styles from './BaseChat.module.scss';
20
+ import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
21
+ import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
22
+ import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
23
+ import GitCloneButton from './GitCloneButton';
24
+
25
+ import FilePreview from './FilePreview';
26
+ import { ModelSelector } from '~/components/chat/ModelSelector';
27
+ import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
28
+ import type { ProviderInfo } from '~/types/model';
29
+ import { ScreenshotStateManager } from './ScreenshotStateManager';
30
+ import { toast } from 'react-toastify';
31
+ import StarterTemplates from './StarterTemplates';
32
+ import type { ActionAlert } from '~/types/actions';
33
+ import ChatAlert from './ChatAlert';
34
+ import type { ModelInfo } from '~/lib/modules/llm/types';
35
+ import ProgressCompilation from './ProgressCompilation';
36
+ import type { ProgressAnnotation } from '~/types/context';
37
+ import type { ActionRunner } from '~/lib/runtime/action-runner';
38
+ import { LOCAL_PROVIDERS } from '~/lib/stores/settings';
39
+
40
+ const TEXTAREA_MIN_HEIGHT = 76;
41
+
42
+ interface BaseChatProps {
43
+ textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
44
+ messageRef?: RefCallback<HTMLDivElement> | undefined;
45
+ scrollRef?: RefCallback<HTMLDivElement> | undefined;
46
+ showChat?: boolean;
47
+ chatStarted?: boolean;
48
+ isStreaming?: boolean;
49
+ onStreamingChange?: (streaming: boolean) => void;
50
+ messages?: Message[];
51
+ description?: string;
52
+ enhancingPrompt?: boolean;
53
+ promptEnhanced?: boolean;
54
+ input?: string;
55
+ model?: string;
56
+ setModel?: (model: string) => void;
57
+ provider?: ProviderInfo;
58
+ setProvider?: (provider: ProviderInfo) => void;
59
+ providerList?: ProviderInfo[];
60
+ handleStop?: () => void;
61
+ sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
62
+ handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
63
+ enhancePrompt?: () => void;
64
+ importChat?: (description: string, messages: Message[]) => Promise<void>;
65
+ exportChat?: () => void;
66
+ uploadedFiles?: File[];
67
+ setUploadedFiles?: (files: File[]) => void;
68
+ imageDataList?: string[];
69
+ setImageDataList?: (dataList: string[]) => void;
70
+ actionAlert?: ActionAlert;
71
+ clearAlert?: () => void;
72
+ data?: JSONValue[] | undefined;
73
+ actionRunner?: ActionRunner;
74
+ }
75
+
76
+ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
77
+ (
78
+ {
79
+ textareaRef,
80
+ messageRef,
81
+ scrollRef,
82
+ showChat = true,
83
+ chatStarted = false,
84
+ isStreaming = false,
85
+ onStreamingChange,
86
+ model,
87
+ setModel,
88
+ provider,
89
+ setProvider,
90
+ providerList,
91
+ input = '',
92
+ enhancingPrompt,
93
+ handleInputChange,
94
+
95
+ // promptEnhanced,
96
+ enhancePrompt,
97
+ sendMessage,
98
+ handleStop,
99
+ importChat,
100
+ exportChat,
101
+ uploadedFiles = [],
102
+ setUploadedFiles,
103
+ imageDataList = [],
104
+ setImageDataList,
105
+ messages,
106
+ actionAlert,
107
+ clearAlert,
108
+ data,
109
+ actionRunner,
110
+ },
111
+ ref,
112
+ ) => {
113
+ const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
114
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>(getApiKeysFromCookies());
115
+ const [modelList, setModelList] = useState<ModelInfo[]>([]);
116
+ const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
117
+ const [isListening, setIsListening] = useState(false);
118
+ const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
119
+ const [transcript, setTranscript] = useState('');
120
+ const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
121
+ const [progressAnnotations, setProgressAnnotations] = useState<ProgressAnnotation[]>([]);
122
+ useEffect(() => {
123
+ if (data) {
124
+ const progressList = data.filter(
125
+ (x) => typeof x === 'object' && (x as any).type === 'progress',
126
+ ) as ProgressAnnotation[];
127
+ setProgressAnnotations(progressList);
128
+ }
129
+ }, [data]);
130
+ useEffect(() => {
131
+ console.log(transcript);
132
+ }, [transcript]);
133
+
134
+ useEffect(() => {
135
+ onStreamingChange?.(isStreaming);
136
+ }, [isStreaming, onStreamingChange]);
137
+
138
+ useEffect(() => {
139
+ if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) {
140
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
141
+ const recognition = new SpeechRecognition();
142
+ recognition.continuous = true;
143
+ recognition.interimResults = true;
144
+
145
+ recognition.onresult = (event) => {
146
+ const transcript = Array.from(event.results)
147
+ .map((result) => result[0])
148
+ .map((result) => result.transcript)
149
+ .join('');
150
+
151
+ setTranscript(transcript);
152
+
153
+ if (handleInputChange) {
154
+ const syntheticEvent = {
155
+ target: { value: transcript },
156
+ } as React.ChangeEvent<HTMLTextAreaElement>;
157
+ handleInputChange(syntheticEvent);
158
+ }
159
+ };
160
+
161
+ recognition.onerror = (event) => {
162
+ console.error('Speech recognition error:', event.error);
163
+ setIsListening(false);
164
+ };
165
+
166
+ setRecognition(recognition);
167
+ }
168
+ }, []);
169
+
170
+ useEffect(() => {
171
+ if (typeof window !== 'undefined') {
172
+ let parsedApiKeys: Record<string, string> | undefined = {};
173
+
174
+ try {
175
+ parsedApiKeys = getApiKeysFromCookies();
176
+ setApiKeys(parsedApiKeys);
177
+ } catch (error) {
178
+ console.error('Error loading API keys from cookies:', error);
179
+ Cookies.remove('apiKeys');
180
+ }
181
+
182
+ setIsModelLoading('all');
183
+ fetch('/api/models')
184
+ .then((response) => response.json())
185
+ .then((data) => {
186
+ const typedData = data as { modelList: ModelInfo[] };
187
+ setModelList(typedData.modelList);
188
+ })
189
+ .catch((error) => {
190
+ console.error('Error fetching model list:', error);
191
+ })
192
+ .finally(() => {
193
+ setIsModelLoading(undefined);
194
+ });
195
+ }
196
+ }, [providerList, provider]);
197
+
198
+ const onApiKeysChange = async (providerName: string, apiKey: string) => {
199
+ const newApiKeys = { ...apiKeys, [providerName]: apiKey };
200
+ setApiKeys(newApiKeys);
201
+ Cookies.set('apiKeys', JSON.stringify(newApiKeys));
202
+
203
+ setIsModelLoading(providerName);
204
+
205
+ let providerModels: ModelInfo[] = [];
206
+
207
+ try {
208
+ const response = await fetch(`/api/models/${encodeURIComponent(providerName)}`);
209
+ const data = await response.json();
210
+ providerModels = (data as { modelList: ModelInfo[] }).modelList;
211
+ } catch (error) {
212
+ console.error('Error loading dynamic models for:', providerName, error);
213
+ }
214
+
215
+ // Only update models for the specific provider
216
+ setModelList((prevModels) => {
217
+ const otherModels = prevModels.filter((model) => model.provider !== providerName);
218
+ return [...otherModels, ...providerModels];
219
+ });
220
+ setIsModelLoading(undefined);
221
+ };
222
+
223
+ const startListening = () => {
224
+ if (recognition) {
225
+ recognition.start();
226
+ setIsListening(true);
227
+ }
228
+ };
229
+
230
+ const stopListening = () => {
231
+ if (recognition) {
232
+ recognition.stop();
233
+ setIsListening(false);
234
+ }
235
+ };
236
+
237
+ const handleSendMessage = (event: React.UIEvent, messageInput?: string) => {
238
+ if (sendMessage) {
239
+ sendMessage(event, messageInput);
240
+
241
+ if (recognition) {
242
+ recognition.abort(); // Stop current recognition
243
+ setTranscript(''); // Clear transcript
244
+ setIsListening(false);
245
+
246
+ // Clear the input by triggering handleInputChange with empty value
247
+ if (handleInputChange) {
248
+ const syntheticEvent = {
249
+ target: { value: '' },
250
+ } as React.ChangeEvent<HTMLTextAreaElement>;
251
+ handleInputChange(syntheticEvent);
252
+ }
253
+ }
254
+ }
255
+ };
256
+
257
+ const handleFileUpload = () => {
258
+ const input = document.createElement('input');
259
+ input.type = 'file';
260
+ input.accept = 'image/*';
261
+
262
+ input.onchange = async (e) => {
263
+ const file = (e.target as HTMLInputElement).files?.[0];
264
+
265
+ if (file) {
266
+ const reader = new FileReader();
267
+
268
+ reader.onload = (e) => {
269
+ const base64Image = e.target?.result as string;
270
+ setUploadedFiles?.([...uploadedFiles, file]);
271
+ setImageDataList?.([...imageDataList, base64Image]);
272
+ };
273
+ reader.readAsDataURL(file);
274
+ }
275
+ };
276
+
277
+ input.click();
278
+ };
279
+
280
+ const handlePaste = async (e: React.ClipboardEvent) => {
281
+ const items = e.clipboardData?.items;
282
+
283
+ if (!items) {
284
+ return;
285
+ }
286
+
287
+ for (const item of items) {
288
+ if (item.type.startsWith('image/')) {
289
+ e.preventDefault();
290
+
291
+ const file = item.getAsFile();
292
+
293
+ if (file) {
294
+ const reader = new FileReader();
295
+
296
+ reader.onload = (e) => {
297
+ const base64Image = e.target?.result as string;
298
+ setUploadedFiles?.([...uploadedFiles, file]);
299
+ setImageDataList?.([...imageDataList, base64Image]);
300
+ };
301
+ reader.readAsDataURL(file);
302
+ }
303
+
304
+ break;
305
+ }
306
+ }
307
+ };
308
+
309
+ const baseChat = (
310
+ <div
311
+ ref={ref}
312
+ className={classNames(styles.BaseChat, 'relative flex h-full w-full overflow-hidden')}
313
+ data-chat-visible={showChat}
314
+ >
315
+ <ClientOnly>{() => <Menu />}</ClientOnly>
316
+ <div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
317
+ <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
318
+ {!chatStarted && (
319
+ <div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">
320
+ <h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
321
+ Where ideas begin
322
+ </h1>
323
+ <p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
324
+ Bring ideas to life in seconds or get help on existing projects.
325
+ </p>
326
+ </div>
327
+ )}
328
+ <div
329
+ className={classNames('pt-6 px-2 sm:px-6', {
330
+ 'h-full flex flex-col': chatStarted,
331
+ })}
332
+ ref={scrollRef}
333
+ >
334
+ <ClientOnly>
335
+ {() => {
336
+ return chatStarted ? (
337
+ <Messages
338
+ ref={messageRef}
339
+ className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
340
+ messages={messages}
341
+ isStreaming={isStreaming}
342
+ />
343
+ ) : null;
344
+ }}
345
+ </ClientOnly>
346
+ <div
347
+ className={classNames('flex flex-col gap-4 w-full max-w-chat mx-auto z-prompt mb-6', {
348
+ 'sticky bottom-2': chatStarted,
349
+ })}
350
+ >
351
+ <div className="bg-bolt-elements-background-depth-2">
352
+ {actionAlert && (
353
+ <ChatAlert
354
+ alert={actionAlert}
355
+ clearAlert={() => clearAlert?.()}
356
+ postMessage={(message) => {
357
+ sendMessage?.({} as any, message);
358
+ clearAlert?.();
359
+ }}
360
+ />
361
+ )}
362
+ </div>
363
+ {progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
364
+ <div
365
+ className={classNames(
366
+ 'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
367
+
368
+ /*
369
+ * {
370
+ * 'sticky bottom-2': chatStarted,
371
+ * },
372
+ */
373
+ )}
374
+ >
375
+ <svg className={classNames(styles.PromptEffectContainer)}>
376
+ <defs>
377
+ <linearGradient
378
+ id="line-gradient"
379
+ x1="20%"
380
+ y1="0%"
381
+ x2="-14%"
382
+ y2="10%"
383
+ gradientUnits="userSpaceOnUse"
384
+ gradientTransform="rotate(-45)"
385
+ >
386
+ <stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
387
+ <stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
388
+ <stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
389
+ <stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
390
+ </linearGradient>
391
+ <linearGradient id="shine-gradient">
392
+ <stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
393
+ <stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
394
+ <stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
395
+ <stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
396
+ </linearGradient>
397
+ </defs>
398
+ <rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
399
+ <rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
400
+ </svg>
401
+ <div>
402
+ <ClientOnly>
403
+ {() => (
404
+ <div className={isModelSettingsCollapsed ? 'hidden' : ''}>
405
+ <ModelSelector
406
+ key={provider?.name + ':' + modelList.length}
407
+ model={model}
408
+ setModel={setModel}
409
+ modelList={modelList}
410
+ provider={provider}
411
+ setProvider={setProvider}
412
+ providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}
413
+ apiKeys={apiKeys}
414
+ modelLoading={isModelLoading}
415
+ />
416
+ {(providerList || []).length > 0 && provider && !LOCAL_PROVIDERS.includes(provider.name) && (
417
+ <APIKeyManager
418
+ provider={provider}
419
+ apiKey={apiKeys[provider.name] || ''}
420
+ setApiKey={(key) => {
421
+ onApiKeysChange(provider.name, key);
422
+ }}
423
+ />
424
+ )}
425
+ </div>
426
+ )}
427
+ </ClientOnly>
428
+ </div>
429
+ <FilePreview
430
+ files={uploadedFiles}
431
+ imageDataList={imageDataList}
432
+ onRemove={(index) => {
433
+ setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index));
434
+ setImageDataList?.(imageDataList.filter((_, i) => i !== index));
435
+ }}
436
+ />
437
+ <ClientOnly>
438
+ {() => (
439
+ <ScreenshotStateManager
440
+ setUploadedFiles={setUploadedFiles}
441
+ setImageDataList={setImageDataList}
442
+ uploadedFiles={uploadedFiles}
443
+ imageDataList={imageDataList}
444
+ />
445
+ )}
446
+ </ClientOnly>
447
+ <div
448
+ className={classNames(
449
+ 'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
450
+ )}
451
+ >
452
+ <textarea
453
+ ref={textareaRef}
454
+ className={classNames(
455
+ 'w-full pl-4 pt-4 pr-16 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
456
+ 'transition-all duration-200',
457
+ 'hover:border-bolt-elements-focus',
458
+ )}
459
+ onDragEnter={(e) => {
460
+ e.preventDefault();
461
+ e.currentTarget.style.border = '2px solid #1488fc';
462
+ }}
463
+ onDragOver={(e) => {
464
+ e.preventDefault();
465
+ e.currentTarget.style.border = '2px solid #1488fc';
466
+ }}
467
+ onDragLeave={(e) => {
468
+ e.preventDefault();
469
+ e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
470
+ }}
471
+ onDrop={(e) => {
472
+ e.preventDefault();
473
+ e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
474
+
475
+ const files = Array.from(e.dataTransfer.files);
476
+ files.forEach((file) => {
477
+ if (file.type.startsWith('image/')) {
478
+ const reader = new FileReader();
479
+
480
+ reader.onload = (e) => {
481
+ const base64Image = e.target?.result as string;
482
+ setUploadedFiles?.([...uploadedFiles, file]);
483
+ setImageDataList?.([...imageDataList, base64Image]);
484
+ };
485
+ reader.readAsDataURL(file);
486
+ }
487
+ });
488
+ }}
489
+ onKeyDown={(event) => {
490
+ if (event.key === 'Enter') {
491
+ if (event.shiftKey) {
492
+ return;
493
+ }
494
+
495
+ event.preventDefault();
496
+
497
+ if (isStreaming) {
498
+ handleStop?.();
499
+ return;
500
+ }
501
+
502
+ // ignore if using input method engine
503
+ if (event.nativeEvent.isComposing) {
504
+ return;
505
+ }
506
+
507
+ handleSendMessage?.(event);
508
+ }
509
+ }}
510
+ value={input}
511
+ onChange={(event) => {
512
+ handleInputChange?.(event);
513
+ }}
514
+ onPaste={handlePaste}
515
+ style={{
516
+ minHeight: TEXTAREA_MIN_HEIGHT,
517
+ maxHeight: TEXTAREA_MAX_HEIGHT,
518
+ }}
519
+ placeholder="How can Bolt help you today?"
520
+ translate="no"
521
+ />
522
+ <ClientOnly>
523
+ {() => (
524
+ <SendButton
525
+ show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
526
+ isStreaming={isStreaming}
527
+ disabled={!providerList || providerList.length === 0}
528
+ onClick={(event) => {
529
+ if (isStreaming) {
530
+ handleStop?.();
531
+ return;
532
+ }
533
+
534
+ if (input.length > 0 || uploadedFiles.length > 0) {
535
+ handleSendMessage?.(event);
536
+ }
537
+ }}
538
+ />
539
+ )}
540
+ </ClientOnly>
541
+ <div className="flex justify-between items-center text-sm p-4 pt-2">
542
+ <div className="flex gap-1 items-center">
543
+ <IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
544
+ <div className="i-ph:paperclip text-xl"></div>
545
+ </IconButton>
546
+ <IconButton
547
+ title="Enhance prompt"
548
+ disabled={input.length === 0 || enhancingPrompt}
549
+ className={classNames('transition-all', enhancingPrompt ? 'opacity-100' : '')}
550
+ onClick={() => {
551
+ enhancePrompt?.();
552
+ toast.success('Prompt enhanced!');
553
+ }}
554
+ >
555
+ {enhancingPrompt ? (
556
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
557
+ ) : (
558
+ <div className="i-bolt:stars text-xl"></div>
559
+ )}
560
+ </IconButton>
561
+
562
+ <SpeechRecognitionButton
563
+ isListening={isListening}
564
+ onStart={startListening}
565
+ onStop={stopListening}
566
+ disabled={isStreaming}
567
+ />
568
+ {chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
569
+ <IconButton
570
+ title="Model Settings"
571
+ className={classNames('transition-all flex items-center gap-1', {
572
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':
573
+ isModelSettingsCollapsed,
574
+ 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':
575
+ !isModelSettingsCollapsed,
576
+ })}
577
+ onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}
578
+ disabled={!providerList || providerList.length === 0}
579
+ >
580
+ <div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
581
+ {isModelSettingsCollapsed ? <span className="text-xs">{model}</span> : <span />}
582
+ </IconButton>
583
+ </div>
584
+ {input.length > 3 ? (
585
+ <div className="text-xs text-bolt-elements-textTertiary">
586
+ Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd>{' '}
587
+ + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd>{' '}
588
+ a new line
589
+ </div>
590
+ ) : null}
591
+ </div>
592
+ </div>
593
+ </div>
594
+ </div>
595
+ </div>
596
+ <div className="flex flex-col justify-center gap-5">
597
+ {!chatStarted && (
598
+ <div className="flex justify-center gap-2">
599
+ {ImportButtons(importChat)}
600
+ <GitCloneButton importChat={importChat} />
601
+ </div>
602
+ )}
603
+ {!chatStarted &&
604
+ ExamplePrompts((event, messageInput) => {
605
+ if (isStreaming) {
606
+ handleStop?.();
607
+ return;
608
+ }
609
+
610
+ handleSendMessage?.(event, messageInput);
611
+ })}
612
+ {!chatStarted && <StarterTemplates />}
613
+ </div>
614
+ </div>
615
+ <ClientOnly>
616
+ {() => (
617
+ <Workbench
618
+ actionRunner={actionRunner ?? ({} as ActionRunner)}
619
+ chatStarted={chatStarted}
620
+ isStreaming={isStreaming}
621
+ />
622
+ )}
623
+ </ClientOnly>
624
+ </div>
625
+ </div>
626
+ );
627
+
628
+ return <Tooltip.Provider delayDuration={200}>{baseChat}</Tooltip.Provider>;
629
+ },
630
+ );
CITATION.cff ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This CITATION.cff file was generated with cffinit.
2
+ # Visit https://bit.ly/cffinit to generate yours today!
3
+
4
+ cff-version: 1.2.0
5
+ title: AutoGPT
6
+ message: >-
7
+ If you use this software, please cite it using the
8
+ metadata from this file.
9
+ type: software
10
+ authors:
11
+ - name: Significant Gravitas
12
+ website: 'https://agpt.co'
13
+ repository-code: 'https://github.com/Significant-Gravitas/AutoGPT'
14
+ url: 'https://agpt.co'
15
+ abstract: >-
16
+ A collection of tools and experimental open-source attempts to make GPT-4 fully
17
+ autonomous.
18
+ keywords:
19
+ - AI
20
+ - Agent
21
+ license: MIT
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code of Conduct for AutoGPT
2
+
3
+ ## 1. Purpose
4
+
5
+ The purpose of this Code of Conduct is to provide guidelines for contributors to the AutoGPT projects on GitHub. We aim to create a positive and inclusive environment where all participants can contribute and collaborate effectively. By participating in this project, you agree to abide by this Code of Conduct.
6
+
7
+ ## 2. Scope
8
+
9
+ This Code of Conduct applies to all contributors, maintainers, and users of the AutoGPT project. It extends to all project spaces, including but not limited to issues, pull requests, code reviews, comments, and other forms of communication within the project.
10
+
11
+ ## 3. Our Standards
12
+
13
+ We encourage the following behavior:
14
+
15
+ * Being respectful and considerate to others
16
+ * Actively seeking diverse perspectives
17
+ * Providing constructive feedback and assistance
18
+ * Demonstrating empathy and understanding
19
+
20
+ We discourage the following behavior:
21
+
22
+ * Harassment or discrimination of any kind
23
+ * Disrespectful, offensive, or inappropriate language or content
24
+ * Personal attacks or insults
25
+ * Unwarranted criticism or negativity
26
+
27
+ ## 4. Reporting and Enforcement
28
+
29
+ If you witness or experience any violations of this Code of Conduct, please report them to the project maintainers by email or other appropriate means. The maintainers will investigate and take appropriate action, which may include warnings, temporary or permanent bans, or other measures as necessary.
30
+
31
+ Maintainers are responsible for ensuring compliance with this Code of Conduct and may take action to address any violations.
32
+
33
+ ## 5. Acknowledgements
34
+
35
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
36
+
37
+ ## 6. Contact
38
+
39
+ If you have any questions or concerns, please contact the project maintainers on Discord:
40
+ https://discord.gg/autogpt
CONTRIBUTING.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AutoGPT Contribution Guide
2
+ If you are reading this, you are probably looking for the full **[contribution guide]**,
3
+ which is part of our [wiki].
4
+
5
+ [contribution guide]: https://github.com/Significant-Gravitas/AutoGPT/wiki/Contributing
6
+ [wiki]: https://github.com/Significant-Gravitas/AutoGPT/wiki
7
+ [roadmap]: https://github.com/Significant-Gravitas/AutoGPT/discussions/6971
8
+ [kanban board]: https://github.com/orgs/Significant-Gravitas/projects/1
9
+
10
+ ## Contributing to the AutoGPT Platform Folder
11
+ All contributions to [the autogpt_platform folder](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform) will be under our [Contribution License Agreement](https://github.com/Significant-Gravitas/AutoGPT/blob/master/autogpt_platform/Contributor%20License%20Agreement%20(CLA).md). By making a pull request contributing to this folder, you agree to the terms of our CLA for your contribution. All contributions to other folders will be under the MIT license.
12
+
13
+ ## In short
14
+ 1. Avoid duplicate work, issues, PRs etc.
15
+ 2. We encourage you to collaborate with fellow community members on some of our bigger
16
+ [todo's][roadmap]!
17
+ * We highly recommend to post your idea and discuss it in the [dev channel].
18
+ 3. Create a draft PR when starting work on bigger changes.
19
+ 4. Adhere to the [Code Guidelines]
20
+ 5. Clearly explain your changes when submitting a PR.
21
+ 6. Don't submit broken code: test/validate your changes.
22
+ 7. Avoid making unnecessary changes, especially if they're purely based on your personal
23
+ preferences. Doing so is the maintainers' job. ;-)
24
+ 8. Please also consider contributing something other than code; see the
25
+ [contribution guide] for options.
26
+
27
+ [dev channel]: https://discord.com/channels/1092243196446249134/1095817829405704305
28
+ [code guidelines]: https://github.com/Significant-Gravitas/AutoGPT/wiki/Contributing#code-guidelines
29
+
30
+ If you wish to involve with the project (beyond just contributing PRs), please read the
31
+ wiki page about [Catalyzing](https://github.com/Significant-Gravitas/AutoGPT/wiki/Catalyzing).
32
+
33
+ In fact, why not just look through the whole wiki (it's only a few pages) and
34
+ hop on our Discord. See you there! :-)
35
+
36
+ ❤️ & 🔆
37
+ The team @ AutoGPT
38
+ https://discord.gg/autogpt
CObjects.cpp ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #ifndef COBJECTS_CPP
2
+ #define COBJECTS_CPP
3
+ /*****************************************************************************
4
+ * C interface
5
+ *
6
+ * These are exported using the CObject API
7
+ */
8
+ #ifdef __clang__
9
+ # pragma clang diagnostic push
10
+ # pragma clang diagnostic ignored "-Wunused-function"
11
+ #endif
12
+
13
+ #include "greenlet_exceptions.hpp"
14
+
15
+ #include "greenlet_internal.hpp"
16
+ #include "greenlet_refs.hpp"
17
+
18
+
19
+ #include "TThreadStateDestroy.cpp"
20
+
21
+ #include "PyGreenlet.hpp"
22
+
23
+ using greenlet::PyErrOccurred;
24
+ using greenlet::Require;
25
+
26
+
27
+
28
+ extern "C" {
29
+ static PyGreenlet*
30
+ PyGreenlet_GetCurrent(void)
31
+ {
32
+ return GET_THREAD_STATE().state().get_current().relinquish_ownership();
33
+ }
34
+
35
+ static int
36
+ PyGreenlet_SetParent(PyGreenlet* g, PyGreenlet* nparent)
37
+ {
38
+ return green_setparent((PyGreenlet*)g, (PyObject*)nparent, NULL);
39
+ }
40
+
41
+ static PyGreenlet*
42
+ PyGreenlet_New(PyObject* run, PyGreenlet* parent)
43
+ {
44
+ using greenlet::refs::NewDictReference;
45
+ // In the past, we didn't use green_new and green_init, but that
46
+ // was a maintenance issue because we duplicated code. This way is
47
+ // much safer, but slightly slower. If that's a problem, we could
48
+ // refactor green_init to separate argument parsing from initialization.
49
+ OwnedGreenlet g = OwnedGreenlet::consuming(green_new(&PyGreenlet_Type, nullptr, nullptr));
50
+ if (!g) {
51
+ return NULL;
52
+ }
53
+
54
+ try {
55
+ NewDictReference kwargs;
56
+ if (run) {
57
+ kwargs.SetItem(mod_globs->str_run, run);
58
+ }
59
+ if (parent) {
60
+ kwargs.SetItem("parent", (PyObject*)parent);
61
+ }
62
+
63
+ Require(green_init(g.borrow(), mod_globs->empty_tuple, kwargs.borrow()));
64
+ }
65
+ catch (const PyErrOccurred&) {
66
+ return nullptr;
67
+ }
68
+
69
+ return g.relinquish_ownership();
70
+ }
71
+
72
+ static PyObject*
73
+ PyGreenlet_Switch(PyGreenlet* self, PyObject* args, PyObject* kwargs)
74
+ {
75
+ if (!PyGreenlet_Check(self)) {
76
+ PyErr_BadArgument();
77
+ return NULL;
78
+ }
79
+
80
+ if (args == NULL) {
81
+ args = mod_globs->empty_tuple;
82
+ }
83
+
84
+ if (kwargs == NULL || !PyDict_Check(kwargs)) {
85
+ kwargs = NULL;
86
+ }
87
+
88
+ return green_switch(self, args, kwargs);
89
+ }
90
+
91
+ static PyObject*
92
+ PyGreenlet_Throw(PyGreenlet* self, PyObject* typ, PyObject* val, PyObject* tb)
93
+ {
94
+ if (!PyGreenlet_Check(self)) {
95
+ PyErr_BadArgument();
96
+ return nullptr;
97
+ }
98
+ try {
99
+ PyErrPieces err_pieces(typ, val, tb);
100
+ return internal_green_throw(self, err_pieces).relinquish_ownership();
101
+ }
102
+ catch (const PyErrOccurred&) {
103
+ return nullptr;
104
+ }
105
+ }
106
+
107
+
108
+
109
+ static int
110
+ Extern_PyGreenlet_MAIN(PyGreenlet* self)
111
+ {
112
+ if (!PyGreenlet_Check(self)) {
113
+ PyErr_BadArgument();
114
+ return -1;
115
+ }
116
+ return self->pimpl->main();
117
+ }
118
+
119
+ static int
120
+ Extern_PyGreenlet_ACTIVE(PyGreenlet* self)
121
+ {
122
+ if (!PyGreenlet_Check(self)) {
123
+ PyErr_BadArgument();
124
+ return -1;
125
+ }
126
+ return self->pimpl->active();
127
+ }
128
+
129
+ static int
130
+ Extern_PyGreenlet_STARTED(PyGreenlet* self)
131
+ {
132
+ if (!PyGreenlet_Check(self)) {
133
+ PyErr_BadArgument();
134
+ return -1;
135
+ }
136
+ return self->pimpl->started();
137
+ }
138
+
139
+ static PyGreenlet*
140
+ Extern_PyGreenlet_GET_PARENT(PyGreenlet* self)
141
+ {
142
+ if (!PyGreenlet_Check(self)) {
143
+ PyErr_BadArgument();
144
+ return NULL;
145
+ }
146
+ // This can return NULL even if there is no exception
147
+ return self->pimpl->parent().acquire();
148
+ }
149
+ } // extern C.
150
+
151
+ /** End C API ****************************************************************/
152
+ #ifdef __clang__
153
+ # pragma clang diagnostic pop
154
+ #endif
155
+
156
+
157
+ #endif
Chat.client.tsx ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * @ts-nocheck
3
+ * Preventing TS checks with files presented in the video for a better presentation.
4
+ */
5
+ import { useStore } from '@nanostores/react';
6
+ import type { Message } from 'ai';
7
+ import { useChat } from 'ai/react';
8
+ import { useAnimate } from 'framer-motion';
9
+ import { memo, useCallback, useEffect, useRef, useState } from 'react';
10
+ import { cssTransition, toast, ToastContainer } from 'react-toastify';
11
+ import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
12
+ import { description, useChatHistory } from '~/lib/persistence';
13
+ import { chatStore } from '~/lib/stores/chat';
14
+ import { workbenchStore } from '~/lib/stores/workbench';
15
+ import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants';
16
+ import { cubicEasingFn } from '~/utils/easings';
17
+ import { createScopedLogger, renderLogger } from '~/utils/logger';
18
+ import { BaseChat } from './BaseChat';
19
+ import Cookies from 'js-cookie';
20
+ import { debounce } from '~/utils/debounce';
21
+ import { useSettings } from '~/lib/hooks/useSettings';
22
+ import type { ProviderInfo } from '~/types/model';
23
+ import { useSearchParams } from '@remix-run/react';
24
+ import { createSampler } from '~/utils/sampler';
25
+ import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
26
+ import { logStore } from '~/lib/stores/logs';
27
+ import { streamingState } from '~/lib/stores/streaming';
28
+ import { filesToArtifacts } from '~/utils/fileUtils';
29
+
30
+ const toastAnimation = cssTransition({
31
+ enter: 'animated fadeInRight',
32
+ exit: 'animated fadeOutRight',
33
+ });
34
+
35
+ const logger = createScopedLogger('Chat');
36
+
37
+ export function Chat() {
38
+ renderLogger.trace('Chat');
39
+
40
+ const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
41
+ const title = useStore(description);
42
+ useEffect(() => {
43
+ workbenchStore.setReloadedMessages(initialMessages.map((m) => m.id));
44
+ }, [initialMessages]);
45
+
46
+ return (
47
+ <>
48
+ {ready && (
49
+ <ChatImpl
50
+ description={title}
51
+ initialMessages={initialMessages}
52
+ exportChat={exportChat}
53
+ storeMessageHistory={storeMessageHistory}
54
+ importChat={importChat}
55
+ />
56
+ )}
57
+ <ToastContainer
58
+ closeButton={({ closeToast }) => {
59
+ return (
60
+ <button className="Toastify__close-button" onClick={closeToast}>
61
+ <div className="i-ph:x text-lg" />
62
+ </button>
63
+ );
64
+ }}
65
+ icon={({ type }) => {
66
+ /**
67
+ * @todo Handle more types if we need them. This may require extra color palettes.
68
+ */
69
+ switch (type) {
70
+ case 'success': {
71
+ return <div className="i-ph:check-bold text-bolt-elements-icon-success text-2xl" />;
72
+ }
73
+ case 'error': {
74
+ return <div className="i-ph:warning-circle-bold text-bolt-elements-icon-error text-2xl" />;
75
+ }
76
+ }
77
+
78
+ return undefined;
79
+ }}
80
+ position="bottom-right"
81
+ pauseOnFocusLoss
82
+ transition={toastAnimation}
83
+ />
84
+ </>
85
+ );
86
+ }
87
+
88
+ const processSampledMessages = createSampler(
89
+ (options: {
90
+ messages: Message[];
91
+ initialMessages: Message[];
92
+ isLoading: boolean;
93
+ parseMessages: (messages: Message[], isLoading: boolean) => void;
94
+ storeMessageHistory: (messages: Message[]) => Promise<void>;
95
+ }) => {
96
+ const { messages, initialMessages, isLoading, parseMessages, storeMessageHistory } = options;
97
+ parseMessages(messages, isLoading);
98
+
99
+ if (messages.length > initialMessages.length) {
100
+ storeMessageHistory(messages).catch((error) => toast.error(error.message));
101
+ }
102
+ },
103
+ 50,
104
+ );
105
+
106
+ interface ChatProps {
107
+ initialMessages: Message[];
108
+ storeMessageHistory: (messages: Message[]) => Promise<void>;
109
+ importChat: (description: string, messages: Message[]) => Promise<void>;
110
+ exportChat: () => void;
111
+ description?: string;
112
+ }
113
+
114
+ export const ChatImpl = memo(
115
+ ({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => {
116
+ useShortcuts();
117
+
118
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
119
+ const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
120
+ const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
121
+ const [imageDataList, setImageDataList] = useState<string[]>([]);
122
+ const [searchParams, setSearchParams] = useSearchParams();
123
+ const [fakeLoading, setFakeLoading] = useState(false);
124
+ const files = useStore(workbenchStore.files);
125
+ const actionAlert = useStore(workbenchStore.alert);
126
+ const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings();
127
+
128
+ const [model, setModel] = useState(() => {
129
+ const savedModel = Cookies.get('selectedModel');
130
+ return savedModel || DEFAULT_MODEL;
131
+ });
132
+ const [provider, setProvider] = useState(() => {
133
+ const savedProvider = Cookies.get('selectedProvider');
134
+ return (PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER) as ProviderInfo;
135
+ });
136
+
137
+ const { showChat } = useStore(chatStore);
138
+
139
+ const [animationScope, animate] = useAnimate();
140
+
141
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
142
+
143
+ const {
144
+ messages,
145
+ isLoading,
146
+ input,
147
+ handleInputChange,
148
+ setInput,
149
+ stop,
150
+ append,
151
+ setMessages,
152
+ reload,
153
+ error,
154
+ data: chatData,
155
+ setData,
156
+ } = useChat({
157
+ api: '/api/chat',
158
+ body: {
159
+ apiKeys,
160
+ files,
161
+ promptId,
162
+ contextOptimization: contextOptimizationEnabled,
163
+ },
164
+ sendExtraMessageFields: true,
165
+ onError: (e) => {
166
+ logger.error('Request failed\n\n', e, error);
167
+ logStore.logError('Chat request failed', e, {
168
+ component: 'Chat',
169
+ action: 'request',
170
+ error: e.message,
171
+ });
172
+ toast.error(
173
+ 'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
174
+ );
175
+ },
176
+ onFinish: (message, response) => {
177
+ const usage = response.usage;
178
+ setData(undefined);
179
+
180
+ if (usage) {
181
+ console.log('Token usage:', usage);
182
+ logStore.logProvider('Chat response completed', {
183
+ component: 'Chat',
184
+ action: 'response',
185
+ model,
186
+ provider: provider.name,
187
+ usage,
188
+ messageLength: message.content.length,
189
+ });
190
+ }
191
+
192
+ logger.debug('Finished streaming');
193
+ },
194
+ initialMessages,
195
+ initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
196
+ });
197
+ useEffect(() => {
198
+ const prompt = searchParams.get('prompt');
199
+
200
+ // console.log(prompt, searchParams, model, provider);
201
+
202
+ if (prompt) {
203
+ setSearchParams({});
204
+ runAnimation();
205
+ append({
206
+ role: 'user',
207
+ content: [
208
+ {
209
+ type: 'text',
210
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${prompt}`,
211
+ },
212
+ ] as any, // Type assertion to bypass compiler check
213
+ });
214
+ }
215
+ }, [model, provider, searchParams]);
216
+
217
+ const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
218
+ const { parsedMessages, parseMessages } = useMessageParser();
219
+
220
+ const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
221
+
222
+ useEffect(() => {
223
+ chatStore.setKey('started', initialMessages.length > 0);
224
+ }, []);
225
+
226
+ useEffect(() => {
227
+ processSampledMessages({
228
+ messages,
229
+ initialMessages,
230
+ isLoading,
231
+ parseMessages,
232
+ storeMessageHistory,
233
+ });
234
+ }, [messages, isLoading, parseMessages]);
235
+
236
+ const scrollTextArea = () => {
237
+ const textarea = textareaRef.current;
238
+
239
+ if (textarea) {
240
+ textarea.scrollTop = textarea.scrollHeight;
241
+ }
242
+ };
243
+
244
+ const abort = () => {
245
+ stop();
246
+ chatStore.setKey('aborted', true);
247
+ workbenchStore.abortAllActions();
248
+
249
+ logStore.logProvider('Chat response aborted', {
250
+ component: 'Chat',
251
+ action: 'abort',
252
+ model,
253
+ provider: provider.name,
254
+ });
255
+ };
256
+
257
+ useEffect(() => {
258
+ const textarea = textareaRef.current;
259
+
260
+ if (textarea) {
261
+ textarea.style.height = 'auto';
262
+
263
+ const scrollHeight = textarea.scrollHeight;
264
+
265
+ textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`;
266
+ textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';
267
+ }
268
+ }, [input, textareaRef]);
269
+
270
+ const runAnimation = async () => {
271
+ if (chatStarted) {
272
+ return;
273
+ }
274
+
275
+ await Promise.all([
276
+ animate('#examples', { opacity: 0, display: 'none' }, { duration: 0.1 }),
277
+ animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),
278
+ ]);
279
+
280
+ chatStore.setKey('started', true);
281
+
282
+ setChatStarted(true);
283
+ };
284
+
285
+ const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
286
+ const messageContent = messageInput || input;
287
+
288
+ if (!messageContent?.trim()) {
289
+ return;
290
+ }
291
+
292
+ if (isLoading) {
293
+ abort();
294
+ return;
295
+ }
296
+
297
+ runAnimation();
298
+
299
+ if (!chatStarted) {
300
+ setFakeLoading(true);
301
+
302
+ if (autoSelectTemplate) {
303
+ const { template, title } = await selectStarterTemplate({
304
+ message: messageContent,
305
+ model,
306
+ provider,
307
+ });
308
+
309
+ if (template !== 'blank') {
310
+ const temResp = await getTemplates(template, title).catch((e) => {
311
+ if (e.message.includes('rate limit')) {
312
+ toast.warning('Rate limit exceeded. Skipping starter template\n Continuing with blank template');
313
+ } else {
314
+ toast.warning('Failed to import starter template\n Continuing with blank template');
315
+ }
316
+
317
+ return null;
318
+ });
319
+
320
+ if (temResp) {
321
+ const { assistantMessage, userMessage } = temResp;
322
+ setMessages([
323
+ {
324
+ id: `1-${new Date().getTime()}`,
325
+ role: 'user',
326
+ content: messageContent,
327
+ },
328
+ {
329
+ id: `2-${new Date().getTime()}`,
330
+ role: 'assistant',
331
+ content: assistantMessage,
332
+ },
333
+ {
334
+ id: `3-${new Date().getTime()}`,
335
+ role: 'user',
336
+ content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userMessage}`,
337
+ annotations: ['hidden'],
338
+ },
339
+ ]);
340
+ reload();
341
+ setFakeLoading(false);
342
+
343
+ return;
344
+ }
345
+ }
346
+ }
347
+
348
+ // If autoSelectTemplate is disabled or template selection failed, proceed with normal message
349
+ setMessages([
350
+ {
351
+ id: `${new Date().getTime()}`,
352
+ role: 'user',
353
+ content: [
354
+ {
355
+ type: 'text',
356
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
357
+ },
358
+ ...imageDataList.map((imageData) => ({
359
+ type: 'image',
360
+ image: imageData,
361
+ })),
362
+ ] as any,
363
+ },
364
+ ]);
365
+ reload();
366
+ setFakeLoading(false);
367
+
368
+ return;
369
+ }
370
+
371
+ if (error != null) {
372
+ setMessages(messages.slice(0, -1));
373
+ }
374
+
375
+ const modifiedFiles = workbenchStore.getModifiedFiles();
376
+
377
+ chatStore.setKey('aborted', false);
378
+
379
+ if (modifiedFiles !== undefined) {
380
+ const userUpdateArtifact = filesToArtifacts(modifiedFiles, `${Date.now()}`);
381
+ append({
382
+ role: 'user',
383
+ content: [
384
+ {
385
+ type: 'text',
386
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userUpdateArtifact}${messageContent}`,
387
+ },
388
+ ...imageDataList.map((imageData) => ({
389
+ type: 'image',
390
+ image: imageData,
391
+ })),
392
+ ] as any,
393
+ });
394
+
395
+ workbenchStore.resetAllFileModifications();
396
+ } else {
397
+ append({
398
+ role: 'user',
399
+ content: [
400
+ {
401
+ type: 'text',
402
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
403
+ },
404
+ ...imageDataList.map((imageData) => ({
405
+ type: 'image',
406
+ image: imageData,
407
+ })),
408
+ ] as any,
409
+ });
410
+ }
411
+
412
+ setInput('');
413
+ Cookies.remove(PROMPT_COOKIE_KEY);
414
+
415
+ setUploadedFiles([]);
416
+ setImageDataList([]);
417
+
418
+ resetEnhancer();
419
+
420
+ textareaRef.current?.blur();
421
+ };
422
+
423
+ /**
424
+ * Handles the change event for the textarea and updates the input state.
425
+ * @param event - The change event from the textarea.
426
+ */
427
+ const onTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
428
+ handleInputChange(event);
429
+ };
430
+
431
+ /**
432
+ * Debounced function to cache the prompt in cookies.
433
+ * Caches the trimmed value of the textarea input after a delay to optimize performance.
434
+ */
435
+ const debouncedCachePrompt = useCallback(
436
+ debounce((event: React.ChangeEvent<HTMLTextAreaElement>) => {
437
+ const trimmedValue = event.target.value.trim();
438
+ Cookies.set(PROMPT_COOKIE_KEY, trimmedValue, { expires: 30 });
439
+ }, 1000),
440
+ [],
441
+ );
442
+
443
+ const [messageRef, scrollRef] = useSnapScroll();
444
+
445
+ useEffect(() => {
446
+ const storedApiKeys = Cookies.get('apiKeys');
447
+
448
+ if (storedApiKeys) {
449
+ setApiKeys(JSON.parse(storedApiKeys));
450
+ }
451
+ }, []);
452
+
453
+ const handleModelChange = (newModel: string) => {
454
+ setModel(newModel);
455
+ Cookies.set('selectedModel', newModel, { expires: 30 });
456
+ };
457
+
458
+ const handleProviderChange = (newProvider: ProviderInfo) => {
459
+ setProvider(newProvider);
460
+ Cookies.set('selectedProvider', newProvider.name, { expires: 30 });
461
+ };
462
+
463
+ return (
464
+ <BaseChat
465
+ ref={animationScope}
466
+ textareaRef={textareaRef}
467
+ input={input}
468
+ showChat={showChat}
469
+ chatStarted={chatStarted}
470
+ isStreaming={isLoading || fakeLoading}
471
+ onStreamingChange={(streaming) => {
472
+ streamingState.set(streaming);
473
+ }}
474
+ enhancingPrompt={enhancingPrompt}
475
+ promptEnhanced={promptEnhanced}
476
+ sendMessage={sendMessage}
477
+ model={model}
478
+ setModel={handleModelChange}
479
+ provider={provider}
480
+ setProvider={handleProviderChange}
481
+ providerList={activeProviders}
482
+ messageRef={messageRef}
483
+ scrollRef={scrollRef}
484
+ handleInputChange={(e) => {
485
+ onTextareaChange(e);
486
+ debouncedCachePrompt(e);
487
+ }}
488
+ handleStop={abort}
489
+ description={description}
490
+ importChat={importChat}
491
+ exportChat={exportChat}
492
+ messages={messages.map((message, i) => {
493
+ if (message.role === 'user') {
494
+ return message;
495
+ }
496
+
497
+ return {
498
+ ...message,
499
+ content: parsedMessages[i] || '',
500
+ };
501
+ })}
502
+ enhancePrompt={() => {
503
+ enhancePrompt(
504
+ input,
505
+ (input) => {
506
+ setInput(input);
507
+ scrollTextArea();
508
+ },
509
+ model,
510
+ provider,
511
+ apiKeys,
512
+ );
513
+ }}
514
+ uploadedFiles={uploadedFiles}
515
+ setUploadedFiles={setUploadedFiles}
516
+ imageDataList={imageDataList}
517
+ setImageDataList={setImageDataList}
518
+ actionAlert={actionAlert}
519
+ clearAlert={() => workbenchStore.clearAlert()}
520
+ data={chatData}
521
+ />
522
+ );
523
+ },
524
+ );
ChatAlert.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AnimatePresence, motion } from 'framer-motion';
2
+ import type { ActionAlert } from '~/types/actions';
3
+ import { classNames } from '~/utils/classNames';
4
+
5
+ interface Props {
6
+ alert: ActionAlert;
7
+ clearAlert: () => void;
8
+ postMessage: (message: string) => void;
9
+ }
10
+
11
+ export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
12
+ const { description, content, source } = alert;
13
+
14
+ const isPreview = source === 'preview';
15
+ const title = isPreview ? 'Preview Error' : 'Terminal Error';
16
+ const message = isPreview
17
+ ? 'We encountered an error while running the preview. Would you like Bolt to analyze and help resolve this issue?'
18
+ : 'We encountered an error while running terminal commands. Would you like Bolt to analyze and help resolve this issue?';
19
+
20
+ return (
21
+ <AnimatePresence>
22
+ <motion.div
23
+ initial={{ opacity: 0, y: -20 }}
24
+ animate={{ opacity: 1, y: 0 }}
25
+ exit={{ opacity: 0, y: -20 }}
26
+ transition={{ duration: 0.3 }}
27
+ className={`rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4 mb-2`}
28
+ >
29
+ <div className="flex items-start">
30
+ {/* Icon */}
31
+ <motion.div
32
+ className="flex-shrink-0"
33
+ initial={{ scale: 0 }}
34
+ animate={{ scale: 1 }}
35
+ transition={{ delay: 0.2 }}
36
+ >
37
+ <div className={`i-ph:warning-duotone text-xl text-bolt-elements-button-danger-text`}></div>
38
+ </motion.div>
39
+ {/* Content */}
40
+ <div className="ml-3 flex-1">
41
+ <motion.h3
42
+ initial={{ opacity: 0 }}
43
+ animate={{ opacity: 1 }}
44
+ transition={{ delay: 0.1 }}
45
+ className={`text-sm font-medium text-bolt-elements-textPrimary`}
46
+ >
47
+ {title}
48
+ </motion.h3>
49
+ <motion.div
50
+ initial={{ opacity: 0 }}
51
+ animate={{ opacity: 1 }}
52
+ transition={{ delay: 0.2 }}
53
+ className={`mt-2 text-sm text-bolt-elements-textSecondary`}
54
+ >
55
+ <p>{message}</p>
56
+ {description && (
57
+ <div className="text-xs text-bolt-elements-textSecondary p-2 bg-bolt-elements-background-depth-3 rounded mt-4 mb-4">
58
+ Error: {description}
59
+ </div>
60
+ )}
61
+ </motion.div>
62
+
63
+ {/* Actions */}
64
+ <motion.div
65
+ className="mt-4"
66
+ initial={{ opacity: 0, y: 10 }}
67
+ animate={{ opacity: 1, y: 0 }}
68
+ transition={{ delay: 0.3 }}
69
+ >
70
+ <div className={classNames(' flex gap-2')}>
71
+ <button
72
+ onClick={() =>
73
+ postMessage(
74
+ `*Fix this ${isPreview ? 'preview' : 'terminal'} error* \n\`\`\`${isPreview ? 'js' : 'sh'}\n${content}\n\`\`\`\n`,
75
+ )
76
+ }
77
+ className={classNames(
78
+ `px-2 py-1.5 rounded-md text-sm font-medium`,
79
+ 'bg-bolt-elements-button-primary-background',
80
+ 'hover:bg-bolt-elements-button-primary-backgroundHover',
81
+ 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-danger-background',
82
+ 'text-bolt-elements-button-primary-text',
83
+ 'flex items-center gap-1.5',
84
+ )}
85
+ >
86
+ <div className="i-ph:chat-circle-duotone"></div>
87
+ Ask Bolt
88
+ </button>
89
+ <button
90
+ onClick={clearAlert}
91
+ className={classNames(
92
+ `px-2 py-1.5 rounded-md text-sm font-medium`,
93
+ 'bg-bolt-elements-button-secondary-background',
94
+ 'hover:bg-bolt-elements-button-secondary-backgroundHover',
95
+ 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
96
+ 'text-bolt-elements-button-secondary-text',
97
+ )}
98
+ >
99
+ Dismiss
100
+ </button>
101
+ </div>
102
+ </motion.div>
103
+ </div>
104
+ </div>
105
+ </motion.div>
106
+ </AnimatePresence>
107
+ );
108
+ }
ChatInterface.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // /home/ubuntu/visionos-frontend/src/components/ChatInterface.tsx
2
+ import React from 'react';
3
+
4
+ const ChatInterface: React.FC = () => {
5
+ return (
6
+ <div className="flex flex-col h-full border rounded-lg shadow-md">
7
+ {/* Message Display Area */}
8
+ <div className="flex-grow p-4 overflow-y-auto bg-gray-50">
9
+ {/* Placeholder for messages */}
10
+ <p className="text-gray-500">Chat messages will appear here...</p>
11
+ </div>
12
+
13
+ {/* Input Area */}
14
+ <div className="p-4 border-t bg-white">
15
+ <input
16
+ type="text"
17
+ placeholder="Type your message..."
18
+ className="w-full p-2 border rounded"
19
+ />
20
+ {/* Placeholder for Send Button */}
21
+ <button className="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
22
+ Send
23
+ </button>
24
+ </div>
25
+ </div>
26
+ );
27
+ };
28
+
29
+ export default ChatInterface;
30
+
CloudProvidersTab.tsx ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import { Switch } from '~/components/ui/Switch';
3
+ import { useSettings } from '~/lib/hooks/useSettings';
4
+ import { URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
5
+ import type { IProviderConfig } from '~/types/model';
6
+ import { logStore } from '~/lib/stores/logs';
7
+ import { motion } from 'framer-motion';
8
+ import { classNames } from '~/utils/classNames';
9
+ import { toast } from 'react-toastify';
10
+ import { providerBaseUrlEnvKeys } from '~/utils/constants';
11
+ import { SiAmazon, SiGoogle, SiHuggingface, SiPerplexity, SiOpenai } from 'react-icons/si';
12
+ import { BsRobot, BsCloud } from 'react-icons/bs';
13
+ import { TbBrain, TbCloudComputing } from 'react-icons/tb';
14
+ import { BiCodeBlock, BiChip } from 'react-icons/bi';
15
+ import { FaCloud, FaBrain } from 'react-icons/fa';
16
+ import type { IconType } from 'react-icons';
17
+
18
+ // Add type for provider names to ensure type safety
19
+ type ProviderName =
20
+ | 'AmazonBedrock'
21
+ | 'Anthropic'
22
+ | 'Cohere'
23
+ | 'Deepseek'
24
+ | 'Google'
25
+ | 'Groq'
26
+ | 'HuggingFace'
27
+ | 'Hyperbolic'
28
+ | 'Mistral'
29
+ | 'OpenAI'
30
+ | 'OpenRouter'
31
+ | 'Perplexity'
32
+ | 'Together'
33
+ | 'XAI';
34
+
35
+ // Update the PROVIDER_ICONS type to use the ProviderName type
36
+ const PROVIDER_ICONS: Record<ProviderName, IconType> = {
37
+ AmazonBedrock: SiAmazon,
38
+ Anthropic: FaBrain,
39
+ Cohere: BiChip,
40
+ Deepseek: BiCodeBlock,
41
+ Google: SiGoogle,
42
+ Groq: BsCloud,
43
+ HuggingFace: SiHuggingface,
44
+ Hyperbolic: TbCloudComputing,
45
+ Mistral: TbBrain,
46
+ OpenAI: SiOpenai,
47
+ OpenRouter: FaCloud,
48
+ Perplexity: SiPerplexity,
49
+ Together: BsCloud,
50
+ XAI: BsRobot,
51
+ };
52
+
53
+ // Update PROVIDER_DESCRIPTIONS to use the same type
54
+ const PROVIDER_DESCRIPTIONS: Partial<Record<ProviderName, string>> = {
55
+ Anthropic: 'Access Claude and other Anthropic models',
56
+ OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models',
57
+ };
58
+
59
+ const CloudProvidersTab = () => {
60
+ const settings = useSettings();
61
+ const [editingProvider, setEditingProvider] = useState<string | null>(null);
62
+ const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
63
+ const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
64
+
65
+ // Load and filter providers
66
+ useEffect(() => {
67
+ const newFilteredProviders = Object.entries(settings.providers || {})
68
+ .filter(([key]) => !['Ollama', 'LMStudio', 'OpenAILike'].includes(key))
69
+ .map(([key, value]) => ({
70
+ name: key,
71
+ settings: value.settings,
72
+ staticModels: value.staticModels || [],
73
+ getDynamicModels: value.getDynamicModels,
74
+ getApiKeyLink: value.getApiKeyLink,
75
+ labelForGetApiKey: value.labelForGetApiKey,
76
+ icon: value.icon,
77
+ }));
78
+
79
+ const sorted = newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
80
+ setFilteredProviders(sorted);
81
+
82
+ // Update category enabled state
83
+ const allEnabled = newFilteredProviders.every((p) => p.settings.enabled);
84
+ setCategoryEnabled(allEnabled);
85
+ }, [settings.providers]);
86
+
87
+ const handleToggleCategory = useCallback(
88
+ (enabled: boolean) => {
89
+ // Update all providers
90
+ filteredProviders.forEach((provider) => {
91
+ settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
92
+ });
93
+
94
+ setCategoryEnabled(enabled);
95
+ toast.success(enabled ? 'All cloud providers enabled' : 'All cloud providers disabled');
96
+ },
97
+ [filteredProviders, settings],
98
+ );
99
+
100
+ const handleToggleProvider = useCallback(
101
+ (provider: IProviderConfig, enabled: boolean) => {
102
+ // Update the provider settings in the store
103
+ settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
104
+
105
+ if (enabled) {
106
+ logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
107
+ toast.success(`${provider.name} enabled`);
108
+ } else {
109
+ logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
110
+ toast.success(`${provider.name} disabled`);
111
+ }
112
+ },
113
+ [settings],
114
+ );
115
+
116
+ const handleUpdateBaseUrl = useCallback(
117
+ (provider: IProviderConfig, baseUrl: string) => {
118
+ const newBaseUrl: string | undefined = baseUrl.trim() || undefined;
119
+
120
+ // Update the provider settings in the store
121
+ settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
122
+
123
+ logStore.logProvider(`Base URL updated for ${provider.name}`, {
124
+ provider: provider.name,
125
+ baseUrl: newBaseUrl,
126
+ });
127
+ toast.success(`${provider.name} base URL updated`);
128
+ setEditingProvider(null);
129
+ },
130
+ [settings],
131
+ );
132
+
133
+ return (
134
+ <div className="space-y-6">
135
+ <motion.div
136
+ className="space-y-4"
137
+ initial={{ opacity: 0, y: 20 }}
138
+ animate={{ opacity: 1, y: 0 }}
139
+ transition={{ duration: 0.3 }}
140
+ >
141
+ <div className="flex items-center justify-between gap-4 mt-8 mb-4">
142
+ <div className="flex items-center gap-2">
143
+ <div
144
+ className={classNames(
145
+ 'w-8 h-8 flex items-center justify-center rounded-lg',
146
+ 'bg-bolt-elements-background-depth-3',
147
+ 'text-purple-500',
148
+ )}
149
+ >
150
+ <TbCloudComputing className="w-5 h-5" />
151
+ </div>
152
+ <div>
153
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary">Cloud Providers</h4>
154
+ <p className="text-sm text-bolt-elements-textSecondary">Connect to cloud-based AI models and services</p>
155
+ </div>
156
+ </div>
157
+
158
+ <div className="flex items-center gap-2">
159
+ <span className="text-sm text-bolt-elements-textSecondary">Enable All Cloud</span>
160
+ <Switch checked={categoryEnabled} onCheckedChange={handleToggleCategory} />
161
+ </div>
162
+ </div>
163
+
164
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
165
+ {filteredProviders.map((provider, index) => (
166
+ <motion.div
167
+ key={provider.name}
168
+ className={classNames(
169
+ 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm',
170
+ 'bg-bolt-elements-background-depth-2',
171
+ 'hover:bg-bolt-elements-background-depth-3',
172
+ 'transition-all duration-200',
173
+ 'relative overflow-hidden group',
174
+ 'flex flex-col',
175
+ )}
176
+ initial={{ opacity: 0, y: 20 }}
177
+ animate={{ opacity: 1, y: 0 }}
178
+ transition={{ delay: index * 0.1 }}
179
+ whileHover={{ scale: 1.02 }}
180
+ >
181
+ <div className="absolute top-0 right-0 p-2 flex gap-1">
182
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
183
+ <motion.span
184
+ className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
185
+ whileHover={{ scale: 1.05 }}
186
+ whileTap={{ scale: 0.95 }}
187
+ >
188
+ Configurable
189
+ </motion.span>
190
+ )}
191
+ </div>
192
+
193
+ <div className="flex items-start gap-4 p-4">
194
+ <motion.div
195
+ className={classNames(
196
+ 'w-10 h-10 flex items-center justify-center rounded-xl',
197
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
198
+ 'transition-all duration-200',
199
+ provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
200
+ )}
201
+ whileHover={{ scale: 1.1 }}
202
+ whileTap={{ scale: 0.9 }}
203
+ >
204
+ <div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
205
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
206
+ className: 'w-full h-full',
207
+ 'aria-label': `${provider.name} logo`,
208
+ })}
209
+ </div>
210
+ </motion.div>
211
+
212
+ <div className="flex-1 min-w-0">
213
+ <div className="flex items-center justify-between gap-4 mb-2">
214
+ <div>
215
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
216
+ {provider.name}
217
+ </h4>
218
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
219
+ {PROVIDER_DESCRIPTIONS[provider.name as keyof typeof PROVIDER_DESCRIPTIONS] ||
220
+ (URL_CONFIGURABLE_PROVIDERS.includes(provider.name)
221
+ ? 'Configure custom endpoint for this provider'
222
+ : 'Standard AI provider integration')}
223
+ </p>
224
+ </div>
225
+ <Switch
226
+ checked={provider.settings.enabled}
227
+ onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
228
+ />
229
+ </div>
230
+
231
+ {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
232
+ <motion.div
233
+ initial={{ opacity: 0, height: 0 }}
234
+ animate={{ opacity: 1, height: 'auto' }}
235
+ exit={{ opacity: 0, height: 0 }}
236
+ transition={{ duration: 0.2 }}
237
+ >
238
+ <div className="flex items-center gap-2 mt-4">
239
+ {editingProvider === provider.name ? (
240
+ <input
241
+ type="text"
242
+ defaultValue={provider.settings.baseUrl}
243
+ placeholder={`Enter ${provider.name} base URL`}
244
+ className={classNames(
245
+ 'flex-1 px-3 py-1.5 rounded-lg text-sm',
246
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
247
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
248
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
249
+ 'transition-all duration-200',
250
+ )}
251
+ onKeyDown={(e) => {
252
+ if (e.key === 'Enter') {
253
+ handleUpdateBaseUrl(provider, e.currentTarget.value);
254
+ } else if (e.key === 'Escape') {
255
+ setEditingProvider(null);
256
+ }
257
+ }}
258
+ onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
259
+ autoFocus
260
+ />
261
+ ) : (
262
+ <div
263
+ className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
264
+ onClick={() => setEditingProvider(provider.name)}
265
+ >
266
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
267
+ <div className="i-ph:link text-sm" />
268
+ <span className="group-hover/url:text-purple-500 transition-colors">
269
+ {provider.settings.baseUrl || 'Click to set base URL'}
270
+ </span>
271
+ </div>
272
+ </div>
273
+ )}
274
+ </div>
275
+
276
+ {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
277
+ <div className="mt-2 text-xs text-green-500">
278
+ <div className="flex items-center gap-1">
279
+ <div className="i-ph:info" />
280
+ <span>Environment URL set in .env file</span>
281
+ </div>
282
+ </div>
283
+ )}
284
+ </motion.div>
285
+ )}
286
+ </div>
287
+ </div>
288
+
289
+ <motion.div
290
+ className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
291
+ animate={{
292
+ borderColor: provider.settings.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
293
+ scale: provider.settings.enabled ? 1 : 0.98,
294
+ }}
295
+ transition={{ duration: 0.2 }}
296
+ />
297
+ </motion.div>
298
+ ))}
299
+ </div>
300
+ </motion.div>
301
+ </div>
302
+ );
303
+ };
304
+
305
+ export default CloudProvidersTab;
CodeBlock.module.scss ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .CopyButtonContainer {
2
+ button:before {
3
+ content: 'Copied';
4
+ font-size: 12px;
5
+ position: absolute;
6
+ left: -53px;
7
+ padding: 2px 6px;
8
+ height: 30px;
9
+ }
10
+ }
CodeBlock.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo, useEffect, useState } from 'react';
2
+ import { bundledLanguages, codeToHtml, isSpecialLang, type BundledLanguage, type SpecialLanguage } from 'shiki';
3
+ import { classNames } from '~/utils/classNames';
4
+ import { createScopedLogger } from '~/utils/logger';
5
+
6
+ import styles from './CodeBlock.module.scss';
7
+
8
+ const logger = createScopedLogger('CodeBlock');
9
+
10
+ interface CodeBlockProps {
11
+ className?: string;
12
+ code: string;
13
+ language?: BundledLanguage | SpecialLanguage;
14
+ theme?: 'light-plus' | 'dark-plus';
15
+ disableCopy?: boolean;
16
+ }
17
+
18
+ export const CodeBlock = memo(
19
+ ({ className, code, language = 'plaintext', theme = 'dark-plus', disableCopy = false }: CodeBlockProps) => {
20
+ const [html, setHTML] = useState<string | undefined>(undefined);
21
+ const [copied, setCopied] = useState(false);
22
+
23
+ const copyToClipboard = () => {
24
+ if (copied) {
25
+ return;
26
+ }
27
+
28
+ navigator.clipboard.writeText(code);
29
+
30
+ setCopied(true);
31
+
32
+ setTimeout(() => {
33
+ setCopied(false);
34
+ }, 2000);
35
+ };
36
+
37
+ useEffect(() => {
38
+ if (language && !isSpecialLang(language) && !(language in bundledLanguages)) {
39
+ logger.warn(`Unsupported language '${language}'`);
40
+ }
41
+
42
+ logger.trace(`Language = ${language}`);
43
+
44
+ const processCode = async () => {
45
+ setHTML(await codeToHtml(code, { lang: language, theme }));
46
+ };
47
+
48
+ processCode();
49
+ }, [code]);
50
+
51
+ return (
52
+ <div className={classNames('relative group text-left', className)}>
53
+ <div
54
+ className={classNames(
55
+ styles.CopyButtonContainer,
56
+ 'bg-transparant absolute top-[10px] right-[10px] rounded-md z-10 text-lg flex items-center justify-center opacity-0 group-hover:opacity-100',
57
+ {
58
+ 'rounded-l-0 opacity-100': copied,
59
+ },
60
+ )}
61
+ >
62
+ {!disableCopy && (
63
+ <button
64
+ className={classNames(
65
+ 'flex items-center bg-accent-500 p-[6px] justify-center before:bg-white before:rounded-l-md before:text-gray-500 before:border-r before:border-gray-300 rounded-md transition-theme',
66
+ {
67
+ 'before:opacity-0': !copied,
68
+ 'before:opacity-100': copied,
69
+ },
70
+ )}
71
+ title="Copy Code"
72
+ onClick={() => copyToClipboard()}
73
+ >
74
+ <div className="i-ph:clipboard-text-duotone"></div>
75
+ </button>
76
+ )}
77
+ </div>
78
+ <div dangerouslySetInnerHTML={{ __html: html ?? '' }}></div>
79
+ </div>
80
+ );
81
+ },
82
+ );
ConnectionForm.tsx ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from 'react';
2
+ import { classNames } from '~/utils/classNames';
3
+ import type { GitHubAuthState } from '~/components/@settings/tabs/connections/types/GitHub';
4
+ import Cookies from 'js-cookie';
5
+ import { getLocalStorage } from '~/lib/persistence';
6
+
7
+ const GITHUB_TOKEN_KEY = 'github_token';
8
+
9
+ interface ConnectionFormProps {
10
+ authState: GitHubAuthState;
11
+ setAuthState: React.Dispatch<React.SetStateAction<GitHubAuthState>>;
12
+ onSave: (e: React.FormEvent) => void;
13
+ onDisconnect: () => void;
14
+ }
15
+
16
+ export function ConnectionForm({ authState, setAuthState, onSave, onDisconnect }: ConnectionFormProps) {
17
+ // Check for saved token on mount
18
+ useEffect(() => {
19
+ const savedToken = Cookies.get(GITHUB_TOKEN_KEY) || getLocalStorage(GITHUB_TOKEN_KEY);
20
+
21
+ if (savedToken && !authState.tokenInfo?.token) {
22
+ setAuthState((prev: GitHubAuthState) => ({
23
+ ...prev,
24
+ tokenInfo: {
25
+ token: savedToken,
26
+ scope: [],
27
+ avatar_url: '',
28
+ name: null,
29
+ created_at: new Date().toISOString(),
30
+ followers: 0,
31
+ },
32
+ }));
33
+ }
34
+ }, []);
35
+
36
+ return (
37
+ <div className="rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] overflow-hidden">
38
+ <div className="p-6">
39
+ <div className="flex items-center justify-between mb-6">
40
+ <div className="flex items-center gap-3">
41
+ <div className="p-2 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
42
+ <div className="i-ph:plug-fill text-bolt-elements-textTertiary" />
43
+ </div>
44
+ <div>
45
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Connection Settings</h3>
46
+ <p className="text-sm text-bolt-elements-textSecondary">Configure your GitHub connection</p>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <form onSubmit={onSave} className="space-y-4">
52
+ <div>
53
+ <label htmlFor="username" className="block text-sm font-medium text-bolt-elements-textSecondary mb-2">
54
+ GitHub Username
55
+ </label>
56
+ <input
57
+ id="username"
58
+ type="text"
59
+ value={authState.username}
60
+ onChange={(e) => setAuthState((prev: GitHubAuthState) => ({ ...prev, username: e.target.value }))}
61
+ className={classNames(
62
+ 'w-full px-4 py-2.5 bg-[#F5F5F5] dark:bg-[#1A1A1A] border rounded-lg',
63
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary text-base',
64
+ 'border-[#E5E5E5] dark:border-[#1A1A1A]',
65
+ 'focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500',
66
+ 'transition-all duration-200',
67
+ )}
68
+ placeholder="e.g., octocat"
69
+ />
70
+ </div>
71
+
72
+ <div>
73
+ <div className="flex items-center justify-between mb-2">
74
+ <label htmlFor="token" className="block text-sm font-medium text-bolt-elements-textSecondary">
75
+ Personal Access Token
76
+ </label>
77
+ <a
78
+ href="https://github.com/settings/tokens/new?scopes=repo,user,read:org,workflow,delete_repo,write:packages,read:packages"
79
+ target="_blank"
80
+ rel="noopener noreferrer"
81
+ className={classNames(
82
+ 'inline-flex items-center gap-1.5 text-xs',
83
+ 'text-purple-500 hover:text-purple-600 dark:text-purple-400 dark:hover:text-purple-300',
84
+ 'transition-colors duration-200',
85
+ )}
86
+ >
87
+ <span>Generate new token</span>
88
+ <div className="i-ph:plus-circle" />
89
+ </a>
90
+ </div>
91
+ <input
92
+ id="token"
93
+ type="password"
94
+ value={authState.tokenInfo?.token || ''}
95
+ onChange={(e) =>
96
+ setAuthState((prev: GitHubAuthState) => ({
97
+ ...prev,
98
+ tokenInfo: {
99
+ token: e.target.value,
100
+ scope: [],
101
+ avatar_url: '',
102
+ name: null,
103
+ created_at: new Date().toISOString(),
104
+ followers: 0,
105
+ },
106
+ username: '',
107
+ isConnected: false,
108
+ isVerifying: false,
109
+ isLoadingRepos: false,
110
+ }))
111
+ }
112
+ className={classNames(
113
+ 'w-full px-4 py-2.5 bg-[#F5F5F5] dark:bg-[#1A1A1A] border rounded-lg',
114
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary text-base',
115
+ 'border-[#E5E5E5] dark:border-[#1A1A1A]',
116
+ 'focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500',
117
+ 'transition-all duration-200',
118
+ )}
119
+ placeholder="ghp_xxxxxxxxxxxx"
120
+ />
121
+ </div>
122
+
123
+ <div className="flex items-center justify-between pt-4 border-t border-[#E5E5E5] dark:border-[#1A1A1A]">
124
+ <div className="flex items-center gap-4">
125
+ {!authState.isConnected ? (
126
+ <button
127
+ type="submit"
128
+ disabled={authState.isVerifying || !authState.username || !authState.tokenInfo?.token}
129
+ className={classNames(
130
+ 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
131
+ 'bg-purple-500 hover:bg-purple-600',
132
+ 'text-white',
133
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
134
+ )}
135
+ >
136
+ {authState.isVerifying ? (
137
+ <>
138
+ <div className="i-ph:spinner animate-spin" />
139
+ <span>Verifying...</span>
140
+ </>
141
+ ) : (
142
+ <>
143
+ <div className="i-ph:plug-fill" />
144
+ <span>Connect</span>
145
+ </>
146
+ )}
147
+ </button>
148
+ ) : (
149
+ <>
150
+ <button
151
+ onClick={onDisconnect}
152
+ className={classNames(
153
+ 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
154
+ 'bg-[#F5F5F5] hover:bg-red-500/10 hover:text-red-500',
155
+ 'dark:bg-[#1A1A1A] dark:hover:bg-red-500/20 dark:hover:text-red-500',
156
+ 'text-bolt-elements-textPrimary',
157
+ )}
158
+ >
159
+ <div className="i-ph:plug-fill" />
160
+ <span>Disconnect</span>
161
+ </button>
162
+ <span className="inline-flex items-center gap-2 px-3 py-1.5 text-sm text-green-600 dark:text-green-400 bg-green-500/5 rounded-lg border border-green-500/20">
163
+ <div className="i-ph:check-circle-fill" />
164
+ <span>Connected</span>
165
+ </span>
166
+ </>
167
+ )}
168
+ </div>
169
+ {authState.rateLimits && (
170
+ <div className="flex items-center gap-2 text-sm text-bolt-elements-textTertiary">
171
+ <div className="i-ph:clock-countdown opacity-60" />
172
+ <span>Rate limit resets at {authState.rateLimits.reset.toLocaleTimeString()}</span>
173
+ </div>
174
+ )}
175
+ </div>
176
+ </form>
177
+ </div>
178
+ </div>
179
+ );
180
+ }
ConnectionsTab.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from 'framer-motion';
2
+ import { GithubConnection } from './GithubConnection';
3
+ import { NetlifyConnection } from './NetlifyConnection';
4
+
5
+ export default function ConnectionsTab() {
6
+ return (
7
+ <div className="space-y-4">
8
+ {/* Header */}
9
+ <motion.div
10
+ className="flex items-center gap-2 mb-2"
11
+ initial={{ opacity: 0, y: 20 }}
12
+ animate={{ opacity: 1, y: 0 }}
13
+ transition={{ delay: 0.1 }}
14
+ >
15
+ <div className="i-ph:plugs-connected w-5 h-5 text-purple-500" />
16
+ <h2 className="text-lg font-medium text-bolt-elements-textPrimary">Connection Settings</h2>
17
+ </motion.div>
18
+ <p className="text-sm text-bolt-elements-textSecondary mb-6">
19
+ Manage your external service connections and integrations
20
+ </p>
21
+
22
+ <div className="grid grid-cols-1 gap-4">
23
+ <GithubConnection />
24
+ <NetlifyConnection />
25
+ </div>
26
+ </div>
27
+ );
28
+ }
ControlPanel.tsx ADDED
@@ -0,0 +1,555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { useStore } from '@nanostores/react';
4
+ import { Switch } from '@radix-ui/react-switch';
5
+ import * as RadixDialog from '@radix-ui/react-dialog';
6
+ import { classNames } from '~/utils/classNames';
7
+ import { TabManagement } from '~/components/@settings/shared/components/TabManagement';
8
+ import { TabTile } from '~/components/@settings/shared/components/TabTile';
9
+ import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
10
+ import { useFeatures } from '~/lib/hooks/useFeatures';
11
+ import { useNotifications } from '~/lib/hooks/useNotifications';
12
+ import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
13
+ import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
14
+ import {
15
+ tabConfigurationStore,
16
+ developerModeStore,
17
+ setDeveloperMode,
18
+ resetTabConfiguration,
19
+ } from '~/lib/stores/settings';
20
+ import { profileStore } from '~/lib/stores/profile';
21
+ import type { TabType, TabVisibilityConfig, Profile } from './types';
22
+ import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
23
+ import { DialogTitle } from '~/components/ui/Dialog';
24
+ import { AvatarDropdown } from './AvatarDropdown';
25
+ import BackgroundRays from '~/components/ui/BackgroundRays';
26
+
27
+ // Import all tab components
28
+ import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab';
29
+ import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab';
30
+ import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab';
31
+ import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
32
+ import DataTab from '~/components/@settings/tabs/data/DataTab';
33
+ import DebugTab from '~/components/@settings/tabs/debug/DebugTab';
34
+ import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
35
+ import UpdateTab from '~/components/@settings/tabs/update/UpdateTab';
36
+ import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
37
+ import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
38
+ import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
39
+ import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
40
+ import TaskManagerTab from '~/components/@settings/tabs/task-manager/TaskManagerTab';
41
+
42
+ interface ControlPanelProps {
43
+ open: boolean;
44
+ onClose: () => void;
45
+ }
46
+
47
+ interface TabWithDevType extends TabVisibilityConfig {
48
+ isExtraDevTab?: boolean;
49
+ }
50
+
51
+ interface ExtendedTabConfig extends TabVisibilityConfig {
52
+ isExtraDevTab?: boolean;
53
+ }
54
+
55
+ interface BaseTabConfig {
56
+ id: TabType;
57
+ visible: boolean;
58
+ window: 'user' | 'developer';
59
+ order: number;
60
+ }
61
+
62
+ interface AnimatedSwitchProps {
63
+ checked: boolean;
64
+ onCheckedChange: (checked: boolean) => void;
65
+ id: string;
66
+ label: string;
67
+ }
68
+
69
+ const TAB_DESCRIPTIONS: Record<TabType, string> = {
70
+ profile: 'Manage your profile and account settings',
71
+ settings: 'Configure application preferences',
72
+ notifications: 'View and manage your notifications',
73
+ features: 'Explore new and upcoming features',
74
+ data: 'Manage your data and storage',
75
+ 'cloud-providers': 'Configure cloud AI providers and models',
76
+ 'local-providers': 'Configure local AI providers and models',
77
+ 'service-status': 'Monitor cloud LLM service status',
78
+ connection: 'Check connection status and settings',
79
+ debug: 'Debug tools and system information',
80
+ 'event-logs': 'View system events and logs',
81
+ update: 'Check for updates and release notes',
82
+ 'task-manager': 'Monitor system resources and processes',
83
+ 'tab-management': 'Configure visible tabs and their order',
84
+ };
85
+
86
+ // Beta status for experimental features
87
+ const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']);
88
+
89
+ const BetaLabel = () => (
90
+ <div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20">
91
+ <span className="text-[10px] font-medium text-purple-600 dark:text-purple-400">BETA</span>
92
+ </div>
93
+ );
94
+
95
+ const AnimatedSwitch = ({ checked, onCheckedChange, id, label }: AnimatedSwitchProps) => {
96
+ return (
97
+ <div className="flex items-center gap-2">
98
+ <Switch
99
+ id={id}
100
+ checked={checked}
101
+ onCheckedChange={onCheckedChange}
102
+ className={classNames(
103
+ 'relative inline-flex h-6 w-11 items-center rounded-full',
104
+ 'transition-all duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)]',
105
+ 'bg-gray-200 dark:bg-gray-700',
106
+ 'data-[state=checked]:bg-purple-500',
107
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/20',
108
+ 'cursor-pointer',
109
+ 'group',
110
+ )}
111
+ >
112
+ <motion.span
113
+ className={classNames(
114
+ 'absolute left-[2px] top-[2px]',
115
+ 'inline-block h-5 w-5 rounded-full',
116
+ 'bg-white shadow-lg',
117
+ 'transition-shadow duration-300',
118
+ 'group-hover:shadow-md group-active:shadow-sm',
119
+ 'group-hover:scale-95 group-active:scale-90',
120
+ )}
121
+ initial={false}
122
+ transition={{
123
+ type: 'spring',
124
+ stiffness: 500,
125
+ damping: 30,
126
+ duration: 0.2,
127
+ }}
128
+ animate={{
129
+ x: checked ? '1.25rem' : '0rem',
130
+ }}
131
+ >
132
+ <motion.div
133
+ className="absolute inset-0 rounded-full bg-white"
134
+ initial={false}
135
+ animate={{
136
+ scale: checked ? 1 : 0.8,
137
+ }}
138
+ transition={{ duration: 0.2 }}
139
+ />
140
+ </motion.span>
141
+ <span className="sr-only">Toggle {label}</span>
142
+ </Switch>
143
+ <div className="flex items-center gap-2">
144
+ <label
145
+ htmlFor={id}
146
+ className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
147
+ >
148
+ {label}
149
+ </label>
150
+ </div>
151
+ </div>
152
+ );
153
+ };
154
+
155
+ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
156
+ // State
157
+ const [activeTab, setActiveTab] = useState<TabType | null>(null);
158
+ const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
159
+ const [showTabManagement, setShowTabManagement] = useState(false);
160
+
161
+ // Store values
162
+ const tabConfiguration = useStore(tabConfigurationStore);
163
+ const developerMode = useStore(developerModeStore);
164
+ const profile = useStore(profileStore) as Profile;
165
+
166
+ // Status hooks
167
+ const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
168
+ const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
169
+ const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
170
+ const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
171
+ const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
172
+
173
+ // Memoize the base tab configurations to avoid recalculation
174
+ const baseTabConfig = useMemo(() => {
175
+ return new Map(DEFAULT_TAB_CONFIG.map((tab) => [tab.id, tab]));
176
+ }, []);
177
+
178
+ // Add visibleTabs logic using useMemo with optimized calculations
179
+ const visibleTabs = useMemo(() => {
180
+ if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
181
+ console.warn('Invalid tab configuration, resetting to defaults');
182
+ resetTabConfiguration();
183
+
184
+ return [];
185
+ }
186
+
187
+ const notificationsDisabled = profile?.preferences?.notifications === false;
188
+
189
+ // In developer mode, show ALL tabs without restrictions
190
+ if (developerMode) {
191
+ const seenTabs = new Set<TabType>();
192
+ const devTabs: ExtendedTabConfig[] = [];
193
+
194
+ // Process tabs in order of priority: developer, user, default
195
+ const processTab = (tab: BaseTabConfig) => {
196
+ if (!seenTabs.has(tab.id)) {
197
+ seenTabs.add(tab.id);
198
+ devTabs.push({
199
+ id: tab.id,
200
+ visible: true,
201
+ window: 'developer',
202
+ order: tab.order || devTabs.length,
203
+ });
204
+ }
205
+ };
206
+
207
+ // Process tabs in priority order
208
+ tabConfiguration.developerTabs?.forEach((tab) => processTab(tab as BaseTabConfig));
209
+ tabConfiguration.userTabs.forEach((tab) => processTab(tab as BaseTabConfig));
210
+ DEFAULT_TAB_CONFIG.forEach((tab) => processTab(tab as BaseTabConfig));
211
+
212
+ // Add Tab Management tile
213
+ devTabs.push({
214
+ id: 'tab-management' as TabType,
215
+ visible: true,
216
+ window: 'developer',
217
+ order: devTabs.length,
218
+ isExtraDevTab: true,
219
+ });
220
+
221
+ return devTabs.sort((a, b) => a.order - b.order);
222
+ }
223
+
224
+ // Optimize user mode tab filtering
225
+ return tabConfiguration.userTabs
226
+ .filter((tab) => {
227
+ if (!tab?.id) {
228
+ return false;
229
+ }
230
+
231
+ if (tab.id === 'notifications' && notificationsDisabled) {
232
+ return false;
233
+ }
234
+
235
+ return tab.visible && tab.window === 'user';
236
+ })
237
+ .sort((a, b) => a.order - b.order);
238
+ }, [tabConfiguration, developerMode, profile?.preferences?.notifications, baseTabConfig]);
239
+
240
+ // Optimize animation performance with layout animations
241
+ const gridLayoutVariants = {
242
+ hidden: { opacity: 0 },
243
+ visible: {
244
+ opacity: 1,
245
+ transition: {
246
+ staggerChildren: 0.05,
247
+ delayChildren: 0.1,
248
+ },
249
+ },
250
+ };
251
+
252
+ const itemVariants = {
253
+ hidden: { opacity: 0, scale: 0.8 },
254
+ visible: {
255
+ opacity: 1,
256
+ scale: 1,
257
+ transition: {
258
+ type: 'spring',
259
+ stiffness: 200,
260
+ damping: 20,
261
+ mass: 0.6,
262
+ },
263
+ },
264
+ };
265
+
266
+ // Reset to default view when modal opens/closes
267
+ useEffect(() => {
268
+ if (!open) {
269
+ // Reset when closing
270
+ setActiveTab(null);
271
+ setLoadingTab(null);
272
+ setShowTabManagement(false);
273
+ } else {
274
+ // When opening, set to null to show the main view
275
+ setActiveTab(null);
276
+ }
277
+ }, [open]);
278
+
279
+ // Handle closing
280
+ const handleClose = () => {
281
+ setActiveTab(null);
282
+ setLoadingTab(null);
283
+ setShowTabManagement(false);
284
+ onClose();
285
+ };
286
+
287
+ // Handlers
288
+ const handleBack = () => {
289
+ if (showTabManagement) {
290
+ setShowTabManagement(false);
291
+ } else if (activeTab) {
292
+ setActiveTab(null);
293
+ }
294
+ };
295
+
296
+ const handleDeveloperModeChange = (checked: boolean) => {
297
+ console.log('Developer mode changed:', checked);
298
+ setDeveloperMode(checked);
299
+ };
300
+
301
+ // Add effect to log developer mode changes
302
+ useEffect(() => {
303
+ console.log('Current developer mode:', developerMode);
304
+ }, [developerMode]);
305
+
306
+ const getTabComponent = (tabId: TabType | 'tab-management') => {
307
+ if (tabId === 'tab-management') {
308
+ return <TabManagement />;
309
+ }
310
+
311
+ switch (tabId) {
312
+ case 'profile':
313
+ return <ProfileTab />;
314
+ case 'settings':
315
+ return <SettingsTab />;
316
+ case 'notifications':
317
+ return <NotificationsTab />;
318
+ case 'features':
319
+ return <FeaturesTab />;
320
+ case 'data':
321
+ return <DataTab />;
322
+ case 'cloud-providers':
323
+ return <CloudProvidersTab />;
324
+ case 'local-providers':
325
+ return <LocalProvidersTab />;
326
+ case 'connection':
327
+ return <ConnectionsTab />;
328
+ case 'debug':
329
+ return <DebugTab />;
330
+ case 'event-logs':
331
+ return <EventLogsTab />;
332
+ case 'update':
333
+ return <UpdateTab />;
334
+ case 'task-manager':
335
+ return <TaskManagerTab />;
336
+ case 'service-status':
337
+ return <ServiceStatusTab />;
338
+ default:
339
+ return null;
340
+ }
341
+ };
342
+
343
+ const getTabUpdateStatus = (tabId: TabType): boolean => {
344
+ switch (tabId) {
345
+ case 'update':
346
+ return hasUpdate;
347
+ case 'features':
348
+ return hasNewFeatures;
349
+ case 'notifications':
350
+ return hasUnreadNotifications;
351
+ case 'connection':
352
+ return hasConnectionIssues;
353
+ case 'debug':
354
+ return hasActiveWarnings;
355
+ default:
356
+ return false;
357
+ }
358
+ };
359
+
360
+ const getStatusMessage = (tabId: TabType): string => {
361
+ switch (tabId) {
362
+ case 'update':
363
+ return `New update available (v${currentVersion})`;
364
+ case 'features':
365
+ return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
366
+ case 'notifications':
367
+ return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
368
+ case 'connection':
369
+ return currentIssue === 'disconnected'
370
+ ? 'Connection lost'
371
+ : currentIssue === 'high-latency'
372
+ ? 'High latency detected'
373
+ : 'Connection issues detected';
374
+ case 'debug': {
375
+ const warnings = activeIssues.filter((i) => i.type === 'warning').length;
376
+ const errors = activeIssues.filter((i) => i.type === 'error').length;
377
+
378
+ return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
379
+ }
380
+ default:
381
+ return '';
382
+ }
383
+ };
384
+
385
+ const handleTabClick = (tabId: TabType) => {
386
+ setLoadingTab(tabId);
387
+ setActiveTab(tabId);
388
+ setShowTabManagement(false);
389
+
390
+ // Acknowledge notifications based on tab
391
+ switch (tabId) {
392
+ case 'update':
393
+ acknowledgeUpdate();
394
+ break;
395
+ case 'features':
396
+ acknowledgeAllFeatures();
397
+ break;
398
+ case 'notifications':
399
+ markAllAsRead();
400
+ break;
401
+ case 'connection':
402
+ acknowledgeIssue();
403
+ break;
404
+ case 'debug':
405
+ acknowledgeAllIssues();
406
+ break;
407
+ }
408
+
409
+ // Clear loading state after a delay
410
+ setTimeout(() => setLoadingTab(null), 500);
411
+ };
412
+
413
+ return (
414
+ <RadixDialog.Root open={open}>
415
+ <RadixDialog.Portal>
416
+ <div className="fixed inset-0 flex items-center justify-center z-[100]">
417
+ <RadixDialog.Overlay asChild>
418
+ <motion.div
419
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
420
+ initial={{ opacity: 0 }}
421
+ animate={{ opacity: 1 }}
422
+ exit={{ opacity: 0 }}
423
+ transition={{ duration: 0.2 }}
424
+ />
425
+ </RadixDialog.Overlay>
426
+
427
+ <RadixDialog.Content
428
+ aria-describedby={undefined}
429
+ onEscapeKeyDown={handleClose}
430
+ onPointerDownOutside={handleClose}
431
+ className="relative z-[101]"
432
+ >
433
+ <motion.div
434
+ className={classNames(
435
+ 'w-[1200px] h-[90vh]',
436
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
437
+ 'rounded-2xl shadow-2xl',
438
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
439
+ 'flex flex-col overflow-hidden',
440
+ 'relative',
441
+ )}
442
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
443
+ animate={{ opacity: 1, scale: 1, y: 0 }}
444
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
445
+ transition={{ duration: 0.2 }}
446
+ >
447
+ <div className="absolute inset-0 overflow-hidden rounded-2xl">
448
+ <BackgroundRays />
449
+ </div>
450
+ <div className="relative z-10 flex flex-col h-full">
451
+ {/* Header */}
452
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
453
+ <div className="flex items-center space-x-4">
454
+ {(activeTab || showTabManagement) && (
455
+ <button
456
+ onClick={handleBack}
457
+ 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"
458
+ >
459
+ <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
460
+ </button>
461
+ )}
462
+ <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
463
+ {showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
464
+ </DialogTitle>
465
+ </div>
466
+
467
+ <div className="flex items-center gap-6">
468
+ {/* Mode Toggle */}
469
+ <div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
470
+ <AnimatedSwitch
471
+ id="developer-mode"
472
+ checked={developerMode}
473
+ onCheckedChange={handleDeveloperModeChange}
474
+ label={developerMode ? 'Developer Mode' : 'User Mode'}
475
+ />
476
+ </div>
477
+
478
+ {/* Avatar and Dropdown */}
479
+ <div className="border-l border-gray-200 dark:border-gray-800 pl-6">
480
+ <AvatarDropdown onSelectTab={handleTabClick} />
481
+ </div>
482
+
483
+ {/* Close Button */}
484
+ <button
485
+ onClick={handleClose}
486
+ className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
487
+ >
488
+ <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
489
+ </button>
490
+ </div>
491
+ </div>
492
+
493
+ {/* Content */}
494
+ <div
495
+ className={classNames(
496
+ 'flex-1',
497
+ 'overflow-y-auto',
498
+ 'hover:overflow-y-auto',
499
+ 'scrollbar scrollbar-w-2',
500
+ 'scrollbar-track-transparent',
501
+ 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
502
+ 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
503
+ 'will-change-scroll',
504
+ 'touch-auto',
505
+ )}
506
+ >
507
+ <motion.div
508
+ key={activeTab || 'home'}
509
+ initial={{ opacity: 0 }}
510
+ animate={{ opacity: 1 }}
511
+ exit={{ opacity: 0 }}
512
+ transition={{ duration: 0.2 }}
513
+ className="p-6"
514
+ >
515
+ {showTabManagement ? (
516
+ <TabManagement />
517
+ ) : activeTab ? (
518
+ getTabComponent(activeTab)
519
+ ) : (
520
+ <motion.div
521
+ className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative"
522
+ variants={gridLayoutVariants}
523
+ initial="hidden"
524
+ animate="visible"
525
+ >
526
+ <AnimatePresence mode="popLayout">
527
+ {(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
528
+ <motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]">
529
+ <TabTile
530
+ tab={tab}
531
+ onClick={() => handleTabClick(tab.id as TabType)}
532
+ isActive={activeTab === tab.id}
533
+ hasUpdate={getTabUpdateStatus(tab.id)}
534
+ statusMessage={getStatusMessage(tab.id)}
535
+ description={TAB_DESCRIPTIONS[tab.id]}
536
+ isLoading={loadingTab === tab.id}
537
+ className="h-full relative"
538
+ >
539
+ {BETA_TABS.has(tab.id) && <BetaLabel />}
540
+ </TabTile>
541
+ </motion.div>
542
+ ))}
543
+ </AnimatePresence>
544
+ </motion.div>
545
+ )}
546
+ </motion.div>
547
+ </div>
548
+ </div>
549
+ </motion.div>
550
+ </RadixDialog.Content>
551
+ </div>
552
+ </RadixDialog.Portal>
553
+ </RadixDialog.Root>
554
+ );
555
+ };
CreateBranchDialog.tsx ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import * as Dialog from '@radix-ui/react-dialog';
3
+ import { classNames } from '~/utils/classNames';
4
+ import type { GitHubRepoInfo } from '~/components/@settings/tabs/connections/types/GitHub';
5
+ import { GitBranch } from '@phosphor-icons/react';
6
+
7
+ interface GitHubBranch {
8
+ name: string;
9
+ default?: boolean;
10
+ }
11
+
12
+ interface CreateBranchDialogProps {
13
+ isOpen: boolean;
14
+ onClose: () => void;
15
+ onConfirm: (branchName: string, sourceBranch: string) => void;
16
+ repository: GitHubRepoInfo;
17
+ branches?: GitHubBranch[];
18
+ }
19
+
20
+ export function CreateBranchDialog({ isOpen, onClose, onConfirm, repository, branches }: CreateBranchDialogProps) {
21
+ const [branchName, setBranchName] = useState('');
22
+ const [sourceBranch, setSourceBranch] = useState(branches?.find((b) => b.default)?.name || 'main');
23
+
24
+ const handleSubmit = (e: React.FormEvent) => {
25
+ e.preventDefault();
26
+ onConfirm(branchName, sourceBranch);
27
+ setBranchName('');
28
+ onClose();
29
+ };
30
+
31
+ return (
32
+ <Dialog.Root open={isOpen} onOpenChange={onClose}>
33
+ <Dialog.Portal>
34
+ <Dialog.Overlay className="fixed inset-0 bg-black/50 dark:bg-black/80" />
35
+ <Dialog.Content
36
+ className={classNames(
37
+ 'fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
38
+ 'w-full max-w-md p-6 rounded-xl shadow-lg',
39
+ 'bg-white dark:bg-[#0A0A0A]',
40
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
41
+ )}
42
+ >
43
+ <Dialog.Title className="text-lg font-medium text-bolt-elements-textPrimary mb-4">
44
+ Create New Branch
45
+ </Dialog.Title>
46
+
47
+ <form onSubmit={handleSubmit}>
48
+ <div className="space-y-4">
49
+ <div>
50
+ <label htmlFor="branchName" className="block text-sm font-medium text-bolt-elements-textSecondary mb-2">
51
+ Branch Name
52
+ </label>
53
+ <input
54
+ id="branchName"
55
+ type="text"
56
+ value={branchName}
57
+ onChange={(e) => setBranchName(e.target.value)}
58
+ placeholder="feature/my-new-branch"
59
+ className={classNames(
60
+ 'w-full px-3 py-2 rounded-lg',
61
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
62
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
63
+ 'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
64
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/50',
65
+ )}
66
+ required
67
+ />
68
+ </div>
69
+
70
+ <div>
71
+ <label
72
+ htmlFor="sourceBranch"
73
+ className="block text-sm font-medium text-bolt-elements-textSecondary mb-2"
74
+ >
75
+ Source Branch
76
+ </label>
77
+ <select
78
+ id="sourceBranch"
79
+ value={sourceBranch}
80
+ onChange={(e) => setSourceBranch(e.target.value)}
81
+ className={classNames(
82
+ 'w-full px-3 py-2 rounded-lg',
83
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
84
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
85
+ 'text-bolt-elements-textPrimary',
86
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/50',
87
+ )}
88
+ >
89
+ {branches?.map((branch) => (
90
+ <option key={branch.name} value={branch.name}>
91
+ {branch.name} {branch.default ? '(default)' : ''}
92
+ </option>
93
+ ))}
94
+ </select>
95
+ </div>
96
+
97
+ <div className="mt-4 p-3 bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg">
98
+ <h4 className="text-sm font-medium text-bolt-elements-textSecondary mb-2">Branch Overview</h4>
99
+ <ul className="space-y-2 text-sm text-bolt-elements-textSecondary">
100
+ <li className="flex items-center gap-2">
101
+ <GitBranch className="text-lg" />
102
+ Repository: {repository.name}
103
+ </li>
104
+ {branchName && (
105
+ <li className="flex items-center gap-2">
106
+ <div className="i-ph:check-circle text-green-500" />
107
+ New branch will be created as: {branchName}
108
+ </li>
109
+ )}
110
+ <li className="flex items-center gap-2">
111
+ <div className="i-ph:check-circle text-green-500" />
112
+ Based on: {sourceBranch}
113
+ </li>
114
+ </ul>
115
+ </div>
116
+ </div>
117
+
118
+ <div className="mt-6 flex justify-end gap-3">
119
+ <button
120
+ type="button"
121
+ onClick={onClose}
122
+ className={classNames(
123
+ 'px-4 py-2 rounded-lg text-sm font-medium',
124
+ 'text-bolt-elements-textPrimary',
125
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
126
+ 'hover:bg-purple-500/10 hover:text-purple-500',
127
+ 'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
128
+ 'transition-colors',
129
+ )}
130
+ >
131
+ Cancel
132
+ </button>
133
+ <button
134
+ type="submit"
135
+ className={classNames(
136
+ 'px-4 py-2 rounded-lg text-sm font-medium',
137
+ 'text-white bg-purple-500',
138
+ 'hover:bg-purple-600',
139
+ 'transition-colors',
140
+ )}
141
+ >
142
+ Create Branch
143
+ </button>
144
+ </div>
145
+ </form>
146
+ </Dialog.Content>
147
+ </Dialog.Portal>
148
+ </Dialog.Root>
149
+ );
150
+ }
DataTab.tsx ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { toast } from 'react-toastify';
4
+ import { DialogRoot, DialogClose, Dialog, DialogTitle } from '~/components/ui/Dialog';
5
+ import { db, getAll, deleteById } from '~/lib/persistence';
6
+
7
+ export default function DataTab() {
8
+ const [isDownloadingTemplate, setIsDownloadingTemplate] = useState(false);
9
+ const [isImportingKeys, setIsImportingKeys] = useState(false);
10
+ const [isResetting, setIsResetting] = useState(false);
11
+ const [isDeleting, setIsDeleting] = useState(false);
12
+ const [showResetInlineConfirm, setShowResetInlineConfirm] = useState(false);
13
+ const [showDeleteInlineConfirm, setShowDeleteInlineConfirm] = useState(false);
14
+ const fileInputRef = useRef<HTMLInputElement>(null);
15
+ const apiKeyFileInputRef = useRef<HTMLInputElement>(null);
16
+
17
+ const handleExportAllChats = async () => {
18
+ try {
19
+ if (!db) {
20
+ throw new Error('Database not initialized');
21
+ }
22
+
23
+ // Get all chats from IndexedDB
24
+ const allChats = await getAll(db);
25
+ const exportData = {
26
+ chats: allChats,
27
+ exportDate: new Date().toISOString(),
28
+ };
29
+
30
+ // Download as JSON
31
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
32
+ const url = URL.createObjectURL(blob);
33
+ const a = document.createElement('a');
34
+ a.href = url;
35
+ a.download = `bolt-chats-${new Date().toISOString()}.json`;
36
+ document.body.appendChild(a);
37
+ a.click();
38
+ document.body.removeChild(a);
39
+ URL.revokeObjectURL(url);
40
+
41
+ toast.success('Chats exported successfully');
42
+ } catch (error) {
43
+ console.error('Export error:', error);
44
+ toast.error('Failed to export chats');
45
+ }
46
+ };
47
+
48
+ const handleExportSettings = () => {
49
+ try {
50
+ const settings = {
51
+ userProfile: localStorage.getItem('bolt_user_profile'),
52
+ settings: localStorage.getItem('bolt_settings'),
53
+ exportDate: new Date().toISOString(),
54
+ };
55
+
56
+ const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
57
+ const url = URL.createObjectURL(blob);
58
+ const a = document.createElement('a');
59
+ a.href = url;
60
+ a.download = `bolt-settings-${new Date().toISOString()}.json`;
61
+ document.body.appendChild(a);
62
+ a.click();
63
+ document.body.removeChild(a);
64
+ URL.revokeObjectURL(url);
65
+
66
+ toast.success('Settings exported successfully');
67
+ } catch (error) {
68
+ console.error('Export error:', error);
69
+ toast.error('Failed to export settings');
70
+ }
71
+ };
72
+
73
+ const handleImportSettings = async (event: React.ChangeEvent<HTMLInputElement>) => {
74
+ const file = event.target.files?.[0];
75
+
76
+ if (!file) {
77
+ return;
78
+ }
79
+
80
+ try {
81
+ const content = await file.text();
82
+ const settings = JSON.parse(content);
83
+
84
+ if (settings.userProfile) {
85
+ localStorage.setItem('bolt_user_profile', settings.userProfile);
86
+ }
87
+
88
+ if (settings.settings) {
89
+ localStorage.setItem('bolt_settings', settings.settings);
90
+ }
91
+
92
+ window.location.reload(); // Reload to apply settings
93
+ toast.success('Settings imported successfully');
94
+ } catch (error) {
95
+ console.error('Import error:', error);
96
+ toast.error('Failed to import settings');
97
+ }
98
+ };
99
+
100
+ const handleImportAPIKeys = async (event: React.ChangeEvent<HTMLInputElement>) => {
101
+ const file = event.target.files?.[0];
102
+
103
+ if (!file) {
104
+ return;
105
+ }
106
+
107
+ setIsImportingKeys(true);
108
+
109
+ try {
110
+ const content = await file.text();
111
+ const keys = JSON.parse(content);
112
+
113
+ // Validate and save each key
114
+ Object.entries(keys).forEach(([key, value]) => {
115
+ if (typeof value !== 'string') {
116
+ throw new Error(`Invalid value for key: ${key}`);
117
+ }
118
+
119
+ localStorage.setItem(`bolt_${key.toLowerCase()}`, value);
120
+ });
121
+
122
+ toast.success('API keys imported successfully');
123
+ } catch (error) {
124
+ console.error('Error importing API keys:', error);
125
+ toast.error('Failed to import API keys');
126
+ } finally {
127
+ setIsImportingKeys(false);
128
+
129
+ if (apiKeyFileInputRef.current) {
130
+ apiKeyFileInputRef.current.value = '';
131
+ }
132
+ }
133
+ };
134
+
135
+ const handleDownloadTemplate = () => {
136
+ setIsDownloadingTemplate(true);
137
+
138
+ try {
139
+ const template = {
140
+ Anthropic_API_KEY: '',
141
+ OpenAI_API_KEY: '',
142
+ Google_API_KEY: '',
143
+ Groq_API_KEY: '',
144
+ HuggingFace_API_KEY: '',
145
+ OpenRouter_API_KEY: '',
146
+ Deepseek_API_KEY: '',
147
+ Mistral_API_KEY: '',
148
+ OpenAILike_API_KEY: '',
149
+ Together_API_KEY: '',
150
+ xAI_API_KEY: '',
151
+ Perplexity_API_KEY: '',
152
+ Cohere_API_KEY: '',
153
+ AzureOpenAI_API_KEY: '',
154
+ OPENAI_LIKE_API_BASE_URL: '',
155
+ LMSTUDIO_API_BASE_URL: '',
156
+ OLLAMA_API_BASE_URL: '',
157
+ TOGETHER_API_BASE_URL: '',
158
+ };
159
+
160
+ const blob = new Blob([JSON.stringify(template, null, 2)], { type: 'application/json' });
161
+ const url = URL.createObjectURL(blob);
162
+ const a = document.createElement('a');
163
+ a.href = url;
164
+ a.download = 'bolt-api-keys-template.json';
165
+ document.body.appendChild(a);
166
+ a.click();
167
+ document.body.removeChild(a);
168
+ URL.revokeObjectURL(url);
169
+
170
+ toast.success('Template downloaded successfully');
171
+ } catch (error) {
172
+ console.error('Error downloading template:', error);
173
+ toast.error('Failed to download template');
174
+ } finally {
175
+ setIsDownloadingTemplate(false);
176
+ }
177
+ };
178
+
179
+ const handleResetSettings = async () => {
180
+ setIsResetting(true);
181
+
182
+ try {
183
+ // Clear all stored settings from localStorage
184
+ localStorage.removeItem('bolt_user_profile');
185
+ localStorage.removeItem('bolt_settings');
186
+ localStorage.removeItem('bolt_chat_history');
187
+
188
+ // Clear all data from IndexedDB
189
+ if (!db) {
190
+ throw new Error('Database not initialized');
191
+ }
192
+
193
+ // Get all chats and delete them
194
+ const chats = await getAll(db as IDBDatabase);
195
+ const deletePromises = chats.map((chat) => deleteById(db as IDBDatabase, chat.id));
196
+ await Promise.all(deletePromises);
197
+
198
+ // Close the dialog first
199
+ setShowResetInlineConfirm(false);
200
+
201
+ // Then reload and show success message
202
+ window.location.reload();
203
+ toast.success('Settings reset successfully');
204
+ } catch (error) {
205
+ console.error('Reset error:', error);
206
+ setShowResetInlineConfirm(false);
207
+ toast.error('Failed to reset settings');
208
+ } finally {
209
+ setIsResetting(false);
210
+ }
211
+ };
212
+
213
+ const handleDeleteAllChats = async () => {
214
+ setIsDeleting(true);
215
+
216
+ try {
217
+ // Clear chat history from localStorage
218
+ localStorage.removeItem('bolt_chat_history');
219
+
220
+ // Clear chats from IndexedDB
221
+ if (!db) {
222
+ throw new Error('Database not initialized');
223
+ }
224
+
225
+ // Get all chats and delete them one by one
226
+ const chats = await getAll(db as IDBDatabase);
227
+ const deletePromises = chats.map((chat) => deleteById(db as IDBDatabase, chat.id));
228
+ await Promise.all(deletePromises);
229
+
230
+ // Close the dialog first
231
+ setShowDeleteInlineConfirm(false);
232
+
233
+ // Then show the success message
234
+ toast.success('Chat history deleted successfully');
235
+ } catch (error) {
236
+ console.error('Delete error:', error);
237
+ setShowDeleteInlineConfirm(false);
238
+ toast.error('Failed to delete chat history');
239
+ } finally {
240
+ setIsDeleting(false);
241
+ }
242
+ };
243
+
244
+ return (
245
+ <div className="space-y-6">
246
+ <input ref={fileInputRef} type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
247
+ {/* Reset Settings Dialog */}
248
+ <DialogRoot open={showResetInlineConfirm} onOpenChange={setShowResetInlineConfirm}>
249
+ <Dialog showCloseButton={false} className="z-[1000]">
250
+ <div className="p-6">
251
+ <div className="flex items-center gap-3">
252
+ <div className="i-ph:warning-circle-fill w-5 h-5 text-yellow-500" />
253
+ <DialogTitle>Reset All Settings?</DialogTitle>
254
+ </div>
255
+ <p className="text-sm text-bolt-elements-textSecondary mt-2">
256
+ This will reset all your settings to their default values. This action cannot be undone.
257
+ </p>
258
+ <div className="flex justify-end items-center gap-3 mt-6">
259
+ <DialogClose asChild>
260
+ <button className="px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white">
261
+ Cancel
262
+ </button>
263
+ </DialogClose>
264
+ <motion.button
265
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-white dark:bg-[#1A1A1A] text-yellow-600 dark:text-yellow-500 hover:bg-yellow-50 dark:hover:bg-yellow-500/10 border border-transparent hover:border-yellow-500/10 dark:hover:border-yellow-500/20"
266
+ onClick={handleResetSettings}
267
+ disabled={isResetting}
268
+ whileHover={{ scale: 1.02 }}
269
+ whileTap={{ scale: 0.98 }}
270
+ >
271
+ {isResetting ? (
272
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
273
+ ) : (
274
+ <div className="i-ph:arrow-counter-clockwise w-4 h-4" />
275
+ )}
276
+ Reset Settings
277
+ </motion.button>
278
+ </div>
279
+ </div>
280
+ </Dialog>
281
+ </DialogRoot>
282
+
283
+ {/* Delete Confirmation Dialog */}
284
+ <DialogRoot open={showDeleteInlineConfirm} onOpenChange={setShowDeleteInlineConfirm}>
285
+ <Dialog showCloseButton={false} className="z-[1000]">
286
+ <div className="p-6">
287
+ <div className="flex items-center gap-3">
288
+ <div className="i-ph:warning-circle-fill w-5 h-5 text-red-500" />
289
+ <DialogTitle>Delete All Chats?</DialogTitle>
290
+ </div>
291
+ <p className="text-sm text-bolt-elements-textSecondary mt-2">
292
+ This will permanently delete all your chat history. This action cannot be undone.
293
+ </p>
294
+ <div className="flex justify-end items-center gap-3 mt-6">
295
+ <DialogClose asChild>
296
+ <button className="px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white">
297
+ Cancel
298
+ </button>
299
+ </DialogClose>
300
+ <motion.button
301
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-white dark:bg-[#1A1A1A] text-red-500 dark:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 border border-transparent hover:border-red-500/10 dark:hover:border-red-500/20"
302
+ onClick={handleDeleteAllChats}
303
+ disabled={isDeleting}
304
+ whileHover={{ scale: 1.02 }}
305
+ whileTap={{ scale: 0.98 }}
306
+ >
307
+ {isDeleting ? (
308
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
309
+ ) : (
310
+ <div className="i-ph:trash w-4 h-4" />
311
+ )}
312
+ Delete All
313
+ </motion.button>
314
+ </div>
315
+ </div>
316
+ </Dialog>
317
+ </DialogRoot>
318
+
319
+ {/* Chat History Section */}
320
+ <motion.div
321
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A]"
322
+ initial={{ opacity: 0, y: 20 }}
323
+ animate={{ opacity: 1, y: 0 }}
324
+ transition={{ delay: 0.1 }}
325
+ >
326
+ <div className="flex items-center gap-2 mb-2">
327
+ <div className="i-ph:chat-circle-duotone w-5 h-5 text-purple-500" />
328
+ <h3 className="text-lg font-medium text-gray-900 dark:text-white">Chat History</h3>
329
+ </div>
330
+ <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">Export or delete all your chat history.</p>
331
+ <div className="flex gap-4">
332
+ <motion.button
333
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
334
+ whileHover={{ scale: 1.02 }}
335
+ whileTap={{ scale: 0.98 }}
336
+ onClick={handleExportAllChats}
337
+ >
338
+ <div className="i-ph:download-simple w-4 h-4" />
339
+ Export All Chats
340
+ </motion.button>
341
+ <motion.button
342
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-red-50 text-red-500 text-sm hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20"
343
+ whileHover={{ scale: 1.02 }}
344
+ whileTap={{ scale: 0.98 }}
345
+ onClick={() => setShowDeleteInlineConfirm(true)}
346
+ >
347
+ <div className="i-ph:trash w-4 h-4" />
348
+ Delete All Chats
349
+ </motion.button>
350
+ </div>
351
+ </motion.div>
352
+
353
+ {/* Settings Backup Section */}
354
+ <motion.div
355
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A]"
356
+ initial={{ opacity: 0, y: 20 }}
357
+ animate={{ opacity: 1, y: 0 }}
358
+ transition={{ delay: 0.2 }}
359
+ >
360
+ <div className="flex items-center gap-2 mb-2">
361
+ <div className="i-ph:gear-duotone w-5 h-5 text-purple-500" />
362
+ <h3 className="text-lg font-medium text-gray-900 dark:text-white">Settings Backup</h3>
363
+ </div>
364
+ <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
365
+ Export your settings to a JSON file or import settings from a previously exported file.
366
+ </p>
367
+ <div className="flex gap-4">
368
+ <motion.button
369
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
370
+ whileHover={{ scale: 1.02 }}
371
+ whileTap={{ scale: 0.98 }}
372
+ onClick={handleExportSettings}
373
+ >
374
+ <div className="i-ph:download-simple w-4 h-4" />
375
+ Export Settings
376
+ </motion.button>
377
+ <motion.button
378
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
379
+ whileHover={{ scale: 1.02 }}
380
+ whileTap={{ scale: 0.98 }}
381
+ onClick={() => fileInputRef.current?.click()}
382
+ >
383
+ <div className="i-ph:upload-simple w-4 h-4" />
384
+ Import Settings
385
+ </motion.button>
386
+ <motion.button
387
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yellow-50 text-yellow-600 text-sm hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20 dark:text-yellow-500"
388
+ whileHover={{ scale: 1.02 }}
389
+ whileTap={{ scale: 0.98 }}
390
+ onClick={() => setShowResetInlineConfirm(true)}
391
+ >
392
+ <div className="i-ph:arrow-counter-clockwise w-4 h-4" />
393
+ Reset Settings
394
+ </motion.button>
395
+ </div>
396
+ </motion.div>
397
+
398
+ {/* API Keys Management Section */}
399
+ <motion.div
400
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A]"
401
+ initial={{ opacity: 0, y: 20 }}
402
+ animate={{ opacity: 1, y: 0 }}
403
+ transition={{ delay: 0.3 }}
404
+ >
405
+ <div className="flex items-center gap-2 mb-2">
406
+ <div className="i-ph:key-duotone w-5 h-5 text-purple-500" />
407
+ <h3 className="text-lg font-medium text-gray-900 dark:text-white">API Keys Management</h3>
408
+ </div>
409
+ <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
410
+ Import API keys from a JSON file or download a template to fill in your keys.
411
+ </p>
412
+ <div className="flex gap-4">
413
+ <input
414
+ ref={apiKeyFileInputRef}
415
+ type="file"
416
+ accept=".json"
417
+ onChange={handleImportAPIKeys}
418
+ className="hidden"
419
+ />
420
+ <motion.button
421
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
422
+ whileHover={{ scale: 1.02 }}
423
+ whileTap={{ scale: 0.98 }}
424
+ onClick={handleDownloadTemplate}
425
+ disabled={isDownloadingTemplate}
426
+ >
427
+ {isDownloadingTemplate ? (
428
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
429
+ ) : (
430
+ <div className="i-ph:download-simple w-4 h-4" />
431
+ )}
432
+ Download Template
433
+ </motion.button>
434
+ <motion.button
435
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
436
+ whileHover={{ scale: 1.02 }}
437
+ whileTap={{ scale: 0.98 }}
438
+ onClick={() => apiKeyFileInputRef.current?.click()}
439
+ disabled={isImportingKeys}
440
+ >
441
+ {isImportingKeys ? (
442
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
443
+ ) : (
444
+ <div className="i-ph:upload-simple w-4 h-4" />
445
+ )}
446
+ Import API Keys
447
+ </motion.button>
448
+ </div>
449
+ </motion.div>
450
+ </div>
451
+ );
452
+ }
DebugTab.tsx ADDED
@@ -0,0 +1,2045 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useMemo, useCallback } from 'react';
2
+ import { toast } from 'react-toastify';
3
+ import { classNames } from '~/utils/classNames';
4
+ import { logStore, type LogEntry } from '~/lib/stores/logs';
5
+ import { useStore } from '@nanostores/react';
6
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/Collapsible';
7
+ import { Progress } from '~/components/ui/Progress';
8
+ import { ScrollArea } from '~/components/ui/ScrollArea';
9
+ import { Badge } from '~/components/ui/Badge';
10
+ import { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
11
+ import { jsPDF } from 'jspdf';
12
+ import { useSettings } from '~/lib/hooks/useSettings';
13
+
14
+ interface SystemInfo {
15
+ os: string;
16
+ arch: string;
17
+ platform: string;
18
+ cpus: string;
19
+ memory: {
20
+ total: string;
21
+ free: string;
22
+ used: string;
23
+ percentage: number;
24
+ };
25
+ node: string;
26
+ browser: {
27
+ name: string;
28
+ version: string;
29
+ language: string;
30
+ userAgent: string;
31
+ cookiesEnabled: boolean;
32
+ online: boolean;
33
+ platform: string;
34
+ cores: number;
35
+ };
36
+ screen: {
37
+ width: number;
38
+ height: number;
39
+ colorDepth: number;
40
+ pixelRatio: number;
41
+ };
42
+ time: {
43
+ timezone: string;
44
+ offset: number;
45
+ locale: string;
46
+ };
47
+ performance: {
48
+ memory: {
49
+ jsHeapSizeLimit: number;
50
+ totalJSHeapSize: number;
51
+ usedJSHeapSize: number;
52
+ usagePercentage: number;
53
+ };
54
+ timing: {
55
+ loadTime: number;
56
+ domReadyTime: number;
57
+ readyStart: number;
58
+ redirectTime: number;
59
+ appcacheTime: number;
60
+ unloadEventTime: number;
61
+ lookupDomainTime: number;
62
+ connectTime: number;
63
+ requestTime: number;
64
+ initDomTreeTime: number;
65
+ loadEventTime: number;
66
+ };
67
+ navigation: {
68
+ type: number;
69
+ redirectCount: number;
70
+ };
71
+ };
72
+ network: {
73
+ downlink: number;
74
+ effectiveType: string;
75
+ rtt: number;
76
+ saveData: boolean;
77
+ type: string;
78
+ };
79
+ battery?: {
80
+ charging: boolean;
81
+ chargingTime: number;
82
+ dischargingTime: number;
83
+ level: number;
84
+ };
85
+ storage: {
86
+ quota: number;
87
+ usage: number;
88
+ persistent: boolean;
89
+ temporary: boolean;
90
+ };
91
+ }
92
+
93
+ interface GitHubRepoInfo {
94
+ fullName: string;
95
+ defaultBranch: string;
96
+ stars: number;
97
+ forks: number;
98
+ openIssues?: number;
99
+ }
100
+
101
+ interface GitInfo {
102
+ local: {
103
+ commitHash: string;
104
+ branch: string;
105
+ commitTime: string;
106
+ author: string;
107
+ email: string;
108
+ remoteUrl: string;
109
+ repoName: string;
110
+ };
111
+ github?: {
112
+ currentRepo: GitHubRepoInfo;
113
+ upstream?: GitHubRepoInfo;
114
+ };
115
+ isForked?: boolean;
116
+ }
117
+
118
+ interface WebAppInfo {
119
+ name: string;
120
+ version: string;
121
+ description: string;
122
+ license: string;
123
+ environment: string;
124
+ timestamp: string;
125
+ runtimeInfo: {
126
+ nodeVersion: string;
127
+ };
128
+ dependencies: {
129
+ production: Array<{ name: string; version: string; type: string }>;
130
+ development: Array<{ name: string; version: string; type: string }>;
131
+ peer: Array<{ name: string; version: string; type: string }>;
132
+ optional: Array<{ name: string; version: string; type: string }>;
133
+ };
134
+ gitInfo: GitInfo;
135
+ }
136
+
137
+ // Add Ollama service status interface
138
+ interface OllamaServiceStatus {
139
+ isRunning: boolean;
140
+ lastChecked: Date;
141
+ error?: string;
142
+ models?: Array<{
143
+ name: string;
144
+ size: string;
145
+ quantization: string;
146
+ }>;
147
+ }
148
+
149
+ interface ExportFormat {
150
+ id: string;
151
+ label: string;
152
+ icon: string;
153
+ handler: () => void;
154
+ }
155
+
156
+ const DependencySection = ({
157
+ title,
158
+ deps,
159
+ }: {
160
+ title: string;
161
+ deps: Array<{ name: string; version: string; type: string }>;
162
+ }) => {
163
+ const [isOpen, setIsOpen] = useState(false);
164
+
165
+ if (deps.length === 0) {
166
+ return null;
167
+ }
168
+
169
+ return (
170
+ <Collapsible open={isOpen} onOpenChange={setIsOpen}>
171
+ <CollapsibleTrigger
172
+ className={classNames(
173
+ 'flex w-full items-center justify-between p-4',
174
+ 'bg-white dark:bg-[#0A0A0A]',
175
+ 'hover:bg-purple-50/50 dark:hover:bg-[#1a1a1a]',
176
+ 'border-b border-[#E5E5E5] dark:border-[#1A1A1A]',
177
+ 'transition-colors duration-200',
178
+ 'first:rounded-t-lg last:rounded-b-lg',
179
+ { 'hover:rounded-lg': !isOpen },
180
+ )}
181
+ >
182
+ <div className="flex items-center gap-3">
183
+ <div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
184
+ <span className="text-base text-bolt-elements-textPrimary">
185
+ {title} Dependencies ({deps.length})
186
+ </span>
187
+ </div>
188
+ <div className="flex items-center gap-2">
189
+ <span className="text-sm text-bolt-elements-textSecondary">{isOpen ? 'Hide' : 'Show'}</span>
190
+ <div
191
+ className={classNames(
192
+ 'i-ph:caret-down w-4 h-4 transform transition-transform duration-200 text-bolt-elements-textSecondary',
193
+ isOpen ? 'rotate-180' : '',
194
+ )}
195
+ />
196
+ </div>
197
+ </CollapsibleTrigger>
198
+ <CollapsibleContent>
199
+ <ScrollArea
200
+ className={classNames(
201
+ 'h-[200px] w-full',
202
+ 'bg-white dark:bg-[#0A0A0A]',
203
+ 'border-b border-[#E5E5E5] dark:border-[#1A1A1A]',
204
+ 'last:rounded-b-lg last:border-b-0',
205
+ )}
206
+ >
207
+ <div className="space-y-2 p-4">
208
+ {deps.map((dep) => (
209
+ <div key={dep.name} className="flex items-center justify-between text-sm">
210
+ <span className="text-bolt-elements-textPrimary">{dep.name}</span>
211
+ <span className="text-bolt-elements-textSecondary">{dep.version}</span>
212
+ </div>
213
+ ))}
214
+ </div>
215
+ </ScrollArea>
216
+ </CollapsibleContent>
217
+ </Collapsible>
218
+ );
219
+ };
220
+
221
+ export default function DebugTab() {
222
+ const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
223
+ const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
224
+ const [ollamaStatus, setOllamaStatus] = useState<OllamaServiceStatus>({
225
+ isRunning: false,
226
+ lastChecked: new Date(),
227
+ });
228
+ const [loading, setLoading] = useState({
229
+ systemInfo: false,
230
+ webAppInfo: false,
231
+ errors: false,
232
+ performance: false,
233
+ });
234
+ const [openSections, setOpenSections] = useState({
235
+ system: false,
236
+ webapp: false,
237
+ errors: false,
238
+ performance: false,
239
+ });
240
+
241
+ const { providers } = useSettings();
242
+
243
+ // Subscribe to logStore updates
244
+ const logs = useStore(logStore.logs);
245
+ const errorLogs = useMemo(() => {
246
+ return Object.values(logs).filter(
247
+ (log): log is LogEntry => typeof log === 'object' && log !== null && 'level' in log && log.level === 'error',
248
+ );
249
+ }, [logs]);
250
+
251
+ // Set up error listeners when component mounts
252
+ useEffect(() => {
253
+ const handleError = (event: ErrorEvent) => {
254
+ logStore.logError(event.message, event.error, {
255
+ filename: event.filename,
256
+ lineNumber: event.lineno,
257
+ columnNumber: event.colno,
258
+ });
259
+ };
260
+
261
+ const handleRejection = (event: PromiseRejectionEvent) => {
262
+ logStore.logError('Unhandled Promise Rejection', event.reason);
263
+ };
264
+
265
+ window.addEventListener('error', handleError);
266
+ window.addEventListener('unhandledrejection', handleRejection);
267
+
268
+ return () => {
269
+ window.removeEventListener('error', handleError);
270
+ window.removeEventListener('unhandledrejection', handleRejection);
271
+ };
272
+ }, []);
273
+
274
+ // Check for errors when the errors section is opened
275
+ useEffect(() => {
276
+ if (openSections.errors) {
277
+ checkErrors();
278
+ }
279
+ }, [openSections.errors]);
280
+
281
+ // Load initial data when component mounts
282
+ useEffect(() => {
283
+ const loadInitialData = async () => {
284
+ await Promise.all([getSystemInfo(), getWebAppInfo()]);
285
+ };
286
+
287
+ loadInitialData();
288
+ }, []);
289
+
290
+ // Refresh data when sections are opened
291
+ useEffect(() => {
292
+ if (openSections.system) {
293
+ getSystemInfo();
294
+ }
295
+
296
+ if (openSections.webapp) {
297
+ getWebAppInfo();
298
+ }
299
+ }, [openSections.system, openSections.webapp]);
300
+
301
+ // Add periodic refresh of git info
302
+ useEffect(() => {
303
+ if (!openSections.webapp) {
304
+ return undefined;
305
+ }
306
+
307
+ // Initial fetch
308
+ const fetchGitInfo = async () => {
309
+ try {
310
+ const response = await fetch('/api/system/git-info');
311
+ const updatedGitInfo = (await response.json()) as GitInfo;
312
+
313
+ setWebAppInfo((prev) => {
314
+ if (!prev) {
315
+ return null;
316
+ }
317
+
318
+ // Only update if the data has changed
319
+ if (JSON.stringify(prev.gitInfo) === JSON.stringify(updatedGitInfo)) {
320
+ return prev;
321
+ }
322
+
323
+ return {
324
+ ...prev,
325
+ gitInfo: updatedGitInfo,
326
+ };
327
+ });
328
+ } catch (error) {
329
+ console.error('Failed to fetch git info:', error);
330
+ }
331
+ };
332
+
333
+ fetchGitInfo();
334
+
335
+ // Refresh every 5 minutes instead of every second
336
+ const interval = setInterval(fetchGitInfo, 5 * 60 * 1000);
337
+
338
+ return () => clearInterval(interval);
339
+ }, [openSections.webapp]);
340
+
341
+ const getSystemInfo = async () => {
342
+ try {
343
+ setLoading((prev) => ({ ...prev, systemInfo: true }));
344
+
345
+ // Get browser info
346
+ const ua = navigator.userAgent;
347
+ const browserName = ua.includes('Firefox')
348
+ ? 'Firefox'
349
+ : ua.includes('Chrome')
350
+ ? 'Chrome'
351
+ : ua.includes('Safari')
352
+ ? 'Safari'
353
+ : ua.includes('Edge')
354
+ ? 'Edge'
355
+ : 'Unknown';
356
+ const browserVersion = ua.match(/(Firefox|Chrome|Safari|Edge)\/([0-9.]+)/)?.[2] || 'Unknown';
357
+
358
+ // Get performance metrics
359
+ const memory = (performance as any).memory || {};
360
+ const timing = performance.timing;
361
+ const navigation = performance.navigation;
362
+ const connection = (navigator as any).connection;
363
+
364
+ // Get battery info
365
+ let batteryInfo;
366
+
367
+ try {
368
+ const battery = await (navigator as any).getBattery();
369
+ batteryInfo = {
370
+ charging: battery.charging,
371
+ chargingTime: battery.chargingTime,
372
+ dischargingTime: battery.dischargingTime,
373
+ level: battery.level * 100,
374
+ };
375
+ } catch {
376
+ console.log('Battery API not supported');
377
+ }
378
+
379
+ // Get storage info
380
+ let storageInfo = {
381
+ quota: 0,
382
+ usage: 0,
383
+ persistent: false,
384
+ temporary: false,
385
+ };
386
+
387
+ try {
388
+ const storage = await navigator.storage.estimate();
389
+ const persistent = await navigator.storage.persist();
390
+ storageInfo = {
391
+ quota: storage.quota || 0,
392
+ usage: storage.usage || 0,
393
+ persistent,
394
+ temporary: !persistent,
395
+ };
396
+ } catch {
397
+ console.log('Storage API not supported');
398
+ }
399
+
400
+ // Get memory info from browser performance API
401
+ const performanceMemory = (performance as any).memory || {};
402
+ const totalMemory = performanceMemory.jsHeapSizeLimit || 0;
403
+ const usedMemory = performanceMemory.usedJSHeapSize || 0;
404
+ const freeMemory = totalMemory - usedMemory;
405
+ const memoryPercentage = totalMemory ? (usedMemory / totalMemory) * 100 : 0;
406
+
407
+ const systemInfo: SystemInfo = {
408
+ os: navigator.platform,
409
+ arch: navigator.userAgent.includes('x64') ? 'x64' : navigator.userAgent.includes('arm') ? 'arm' : 'unknown',
410
+ platform: navigator.platform,
411
+ cpus: navigator.hardwareConcurrency + ' cores',
412
+ memory: {
413
+ total: formatBytes(totalMemory),
414
+ free: formatBytes(freeMemory),
415
+ used: formatBytes(usedMemory),
416
+ percentage: Math.round(memoryPercentage),
417
+ },
418
+ node: 'browser',
419
+ browser: {
420
+ name: browserName,
421
+ version: browserVersion,
422
+ language: navigator.language,
423
+ userAgent: navigator.userAgent,
424
+ cookiesEnabled: navigator.cookieEnabled,
425
+ online: navigator.onLine,
426
+ platform: navigator.platform,
427
+ cores: navigator.hardwareConcurrency,
428
+ },
429
+ screen: {
430
+ width: window.screen.width,
431
+ height: window.screen.height,
432
+ colorDepth: window.screen.colorDepth,
433
+ pixelRatio: window.devicePixelRatio,
434
+ },
435
+ time: {
436
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
437
+ offset: new Date().getTimezoneOffset(),
438
+ locale: navigator.language,
439
+ },
440
+ performance: {
441
+ memory: {
442
+ jsHeapSizeLimit: memory.jsHeapSizeLimit || 0,
443
+ totalJSHeapSize: memory.totalJSHeapSize || 0,
444
+ usedJSHeapSize: memory.usedJSHeapSize || 0,
445
+ usagePercentage: memory.totalJSHeapSize ? (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100 : 0,
446
+ },
447
+ timing: {
448
+ loadTime: timing.loadEventEnd - timing.navigationStart,
449
+ domReadyTime: timing.domContentLoadedEventEnd - timing.navigationStart,
450
+ readyStart: timing.fetchStart - timing.navigationStart,
451
+ redirectTime: timing.redirectEnd - timing.redirectStart,
452
+ appcacheTime: timing.domainLookupStart - timing.fetchStart,
453
+ unloadEventTime: timing.unloadEventEnd - timing.unloadEventStart,
454
+ lookupDomainTime: timing.domainLookupEnd - timing.domainLookupStart,
455
+ connectTime: timing.connectEnd - timing.connectStart,
456
+ requestTime: timing.responseEnd - timing.requestStart,
457
+ initDomTreeTime: timing.domInteractive - timing.responseEnd,
458
+ loadEventTime: timing.loadEventEnd - timing.loadEventStart,
459
+ },
460
+ navigation: {
461
+ type: navigation.type,
462
+ redirectCount: navigation.redirectCount,
463
+ },
464
+ },
465
+ network: {
466
+ downlink: connection?.downlink || 0,
467
+ effectiveType: connection?.effectiveType || 'unknown',
468
+ rtt: connection?.rtt || 0,
469
+ saveData: connection?.saveData || false,
470
+ type: connection?.type || 'unknown',
471
+ },
472
+ battery: batteryInfo,
473
+ storage: storageInfo,
474
+ };
475
+
476
+ setSystemInfo(systemInfo);
477
+ toast.success('System information updated');
478
+ } catch (error) {
479
+ toast.error('Failed to get system information');
480
+ console.error('Failed to get system information:', error);
481
+ } finally {
482
+ setLoading((prev) => ({ ...prev, systemInfo: false }));
483
+ }
484
+ };
485
+
486
+ const getWebAppInfo = async () => {
487
+ try {
488
+ setLoading((prev) => ({ ...prev, webAppInfo: true }));
489
+
490
+ const [appResponse, gitResponse] = await Promise.all([
491
+ fetch('/api/system/app-info'),
492
+ fetch('/api/system/git-info'),
493
+ ]);
494
+
495
+ if (!appResponse.ok || !gitResponse.ok) {
496
+ throw new Error('Failed to fetch webapp info');
497
+ }
498
+
499
+ const appData = (await appResponse.json()) as Omit<WebAppInfo, 'gitInfo'>;
500
+ const gitData = (await gitResponse.json()) as GitInfo;
501
+
502
+ console.log('Git Info Response:', gitData); // Add logging to debug
503
+
504
+ setWebAppInfo({
505
+ ...appData,
506
+ gitInfo: gitData,
507
+ });
508
+
509
+ toast.success('WebApp information updated');
510
+
511
+ return true;
512
+ } catch (error) {
513
+ console.error('Failed to fetch webapp info:', error);
514
+ toast.error('Failed to fetch webapp information');
515
+ setWebAppInfo(null);
516
+
517
+ return false;
518
+ } finally {
519
+ setLoading((prev) => ({ ...prev, webAppInfo: false }));
520
+ }
521
+ };
522
+
523
+ // Helper function to format bytes to human readable format
524
+ const formatBytes = (bytes: number) => {
525
+ const units = ['B', 'KB', 'MB', 'GB'];
526
+ let size = bytes;
527
+ let unitIndex = 0;
528
+
529
+ while (size >= 1024 && unitIndex < units.length - 1) {
530
+ size /= 1024;
531
+ unitIndex++;
532
+ }
533
+
534
+ return `${Math.round(size)} ${units[unitIndex]}`;
535
+ };
536
+
537
+ const handleLogPerformance = () => {
538
+ try {
539
+ setLoading((prev) => ({ ...prev, performance: true }));
540
+
541
+ // Get performance metrics using modern Performance API
542
+ const performanceEntries = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
543
+ const memory = (performance as any).memory;
544
+
545
+ // Calculate timing metrics
546
+ const timingMetrics = {
547
+ loadTime: performanceEntries.loadEventEnd - performanceEntries.startTime,
548
+ domReadyTime: performanceEntries.domContentLoadedEventEnd - performanceEntries.startTime,
549
+ fetchTime: performanceEntries.responseEnd - performanceEntries.fetchStart,
550
+ redirectTime: performanceEntries.redirectEnd - performanceEntries.redirectStart,
551
+ dnsTime: performanceEntries.domainLookupEnd - performanceEntries.domainLookupStart,
552
+ tcpTime: performanceEntries.connectEnd - performanceEntries.connectStart,
553
+ ttfb: performanceEntries.responseStart - performanceEntries.requestStart,
554
+ processingTime: performanceEntries.loadEventEnd - performanceEntries.responseEnd,
555
+ };
556
+
557
+ // Get resource timing data
558
+ const resourceEntries = performance.getEntriesByType('resource');
559
+ const resourceStats = {
560
+ totalResources: resourceEntries.length,
561
+ totalSize: resourceEntries.reduce((total, entry) => total + ((entry as any).transferSize || 0), 0),
562
+ totalTime: Math.max(...resourceEntries.map((entry) => entry.duration)),
563
+ };
564
+
565
+ // Get memory metrics
566
+ const memoryMetrics = memory
567
+ ? {
568
+ jsHeapSizeLimit: memory.jsHeapSizeLimit,
569
+ totalJSHeapSize: memory.totalJSHeapSize,
570
+ usedJSHeapSize: memory.usedJSHeapSize,
571
+ heapUtilization: (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100,
572
+ }
573
+ : null;
574
+
575
+ // Get frame rate metrics
576
+ let fps = 0;
577
+
578
+ if ('requestAnimationFrame' in window) {
579
+ const times: number[] = [];
580
+
581
+ function calculateFPS(now: number) {
582
+ times.push(now);
583
+
584
+ if (times.length > 10) {
585
+ const fps = Math.round((1000 * 10) / (now - times[0]));
586
+ times.shift();
587
+
588
+ return fps;
589
+ }
590
+
591
+ requestAnimationFrame(calculateFPS);
592
+
593
+ return 0;
594
+ }
595
+
596
+ fps = calculateFPS(performance.now());
597
+ }
598
+
599
+ // Log all performance metrics
600
+ logStore.logSystem('Performance Metrics', {
601
+ timing: timingMetrics,
602
+ resources: resourceStats,
603
+ memory: memoryMetrics,
604
+ fps,
605
+ timestamp: new Date().toISOString(),
606
+ navigationEntry: {
607
+ type: performanceEntries.type,
608
+ redirectCount: performanceEntries.redirectCount,
609
+ },
610
+ });
611
+
612
+ toast.success('Performance metrics logged');
613
+ } catch (error) {
614
+ toast.error('Failed to log performance metrics');
615
+ console.error('Failed to log performance metrics:', error);
616
+ } finally {
617
+ setLoading((prev) => ({ ...prev, performance: false }));
618
+ }
619
+ };
620
+
621
+ const checkErrors = async () => {
622
+ try {
623
+ setLoading((prev) => ({ ...prev, errors: true }));
624
+
625
+ // Get errors from log store
626
+ const storedErrors = errorLogs;
627
+
628
+ if (storedErrors.length === 0) {
629
+ toast.success('No errors found');
630
+ } else {
631
+ toast.warning(`Found ${storedErrors.length} error(s)`);
632
+ }
633
+ } catch (error) {
634
+ toast.error('Failed to check errors');
635
+ console.error('Failed to check errors:', error);
636
+ } finally {
637
+ setLoading((prev) => ({ ...prev, errors: false }));
638
+ }
639
+ };
640
+
641
+ const exportDebugInfo = () => {
642
+ try {
643
+ const debugData = {
644
+ timestamp: new Date().toISOString(),
645
+ system: systemInfo,
646
+ webApp: webAppInfo,
647
+ errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
648
+ performance: {
649
+ memory: (performance as any).memory || {},
650
+ timing: performance.timing,
651
+ navigation: performance.navigation,
652
+ },
653
+ };
654
+
655
+ const blob = new Blob([JSON.stringify(debugData, null, 2)], { type: 'application/json' });
656
+ const url = window.URL.createObjectURL(blob);
657
+ const a = document.createElement('a');
658
+ a.href = url;
659
+ a.download = `bolt-debug-info-${new Date().toISOString()}.json`;
660
+ document.body.appendChild(a);
661
+ a.click();
662
+ window.URL.revokeObjectURL(url);
663
+ document.body.removeChild(a);
664
+ toast.success('Debug information exported successfully');
665
+ } catch (error) {
666
+ console.error('Failed to export debug info:', error);
667
+ toast.error('Failed to export debug information');
668
+ }
669
+ };
670
+
671
+ const exportAsCSV = () => {
672
+ try {
673
+ const debugData = {
674
+ system: systemInfo,
675
+ webApp: webAppInfo,
676
+ errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
677
+ performance: {
678
+ memory: (performance as any).memory || {},
679
+ timing: performance.timing,
680
+ navigation: performance.navigation,
681
+ },
682
+ };
683
+
684
+ // Convert the data to CSV format
685
+ const csvData = [
686
+ ['Category', 'Key', 'Value'],
687
+ ...Object.entries(debugData).flatMap(([category, data]) =>
688
+ Object.entries(data || {}).map(([key, value]) => [
689
+ category,
690
+ key,
691
+ typeof value === 'object' ? JSON.stringify(value) : String(value),
692
+ ]),
693
+ ),
694
+ ];
695
+
696
+ // Create CSV content
697
+ const csvContent = csvData.map((row) => row.join(',')).join('\n');
698
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
699
+ const url = window.URL.createObjectURL(blob);
700
+ const a = document.createElement('a');
701
+ a.href = url;
702
+ a.download = `bolt-debug-info-${new Date().toISOString()}.csv`;
703
+ document.body.appendChild(a);
704
+ a.click();
705
+ window.URL.revokeObjectURL(url);
706
+ document.body.removeChild(a);
707
+ toast.success('Debug information exported as CSV');
708
+ } catch (error) {
709
+ console.error('Failed to export CSV:', error);
710
+ toast.error('Failed to export debug information as CSV');
711
+ }
712
+ };
713
+
714
+ const exportAsPDF = () => {
715
+ try {
716
+ const debugData = {
717
+ system: systemInfo,
718
+ webApp: webAppInfo,
719
+ errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
720
+ performance: {
721
+ memory: (performance as any).memory || {},
722
+ timing: performance.timing,
723
+ navigation: performance.navigation,
724
+ },
725
+ };
726
+
727
+ // Create new PDF document
728
+ const doc = new jsPDF();
729
+ const lineHeight = 7;
730
+ let yPos = 20;
731
+ const margin = 20;
732
+ const pageWidth = doc.internal.pageSize.getWidth();
733
+ const maxLineWidth = pageWidth - 2 * margin;
734
+
735
+ // Add key-value pair with better formatting
736
+ const addKeyValue = (key: string, value: any, indent = 0) => {
737
+ // Check if we need a new page
738
+ if (yPos > doc.internal.pageSize.getHeight() - 20) {
739
+ doc.addPage();
740
+ yPos = margin;
741
+ }
742
+
743
+ doc.setFontSize(10);
744
+ doc.setTextColor('#374151');
745
+ doc.setFont('helvetica', 'bold');
746
+
747
+ // Format the key with proper spacing
748
+ const formattedKey = key.replace(/([A-Z])/g, ' $1').trim();
749
+ doc.text(formattedKey + ':', margin + indent, yPos);
750
+ doc.setFont('helvetica', 'normal');
751
+ doc.setTextColor('#6B7280');
752
+
753
+ let valueText;
754
+
755
+ if (typeof value === 'object' && value !== null) {
756
+ // Skip rendering if value is empty object
757
+ if (Object.keys(value).length === 0) {
758
+ return;
759
+ }
760
+
761
+ yPos += lineHeight;
762
+ Object.entries(value).forEach(([subKey, subValue]) => {
763
+ // Check for page break before each sub-item
764
+ if (yPos > doc.internal.pageSize.getHeight() - 20) {
765
+ doc.addPage();
766
+ yPos = margin;
767
+ }
768
+
769
+ const formattedSubKey = subKey.replace(/([A-Z])/g, ' $1').trim();
770
+ addKeyValue(formattedSubKey, subValue, indent + 10);
771
+ });
772
+
773
+ return;
774
+ } else {
775
+ valueText = String(value);
776
+ }
777
+
778
+ const valueX = margin + indent + doc.getTextWidth(formattedKey + ': ');
779
+ const maxValueWidth = maxLineWidth - indent - doc.getTextWidth(formattedKey + ': ');
780
+ const lines = doc.splitTextToSize(valueText, maxValueWidth);
781
+
782
+ // Check if we need a new page for the value
783
+ if (yPos + lines.length * lineHeight > doc.internal.pageSize.getHeight() - 20) {
784
+ doc.addPage();
785
+ yPos = margin;
786
+ }
787
+
788
+ doc.text(lines, valueX, yPos);
789
+ yPos += lines.length * lineHeight;
790
+ };
791
+
792
+ // Add section header with page break check
793
+ const addSectionHeader = (title: string) => {
794
+ // Check if we need a new page
795
+ if (yPos + 20 > doc.internal.pageSize.getHeight() - 20) {
796
+ doc.addPage();
797
+ yPos = margin;
798
+ }
799
+
800
+ yPos += lineHeight;
801
+ doc.setFillColor('#F3F4F6');
802
+ doc.rect(margin - 2, yPos - 5, pageWidth - 2 * (margin - 2), lineHeight + 6, 'F');
803
+ doc.setFont('helvetica', 'bold');
804
+ doc.setTextColor('#111827');
805
+ doc.setFontSize(12);
806
+ doc.text(title.toUpperCase(), margin, yPos);
807
+ doc.setFont('helvetica', 'normal');
808
+ yPos += lineHeight * 1.5;
809
+ };
810
+
811
+ // Add horizontal line with page break check
812
+ const addHorizontalLine = () => {
813
+ // Check if we need a new page
814
+ if (yPos + 10 > doc.internal.pageSize.getHeight() - 20) {
815
+ doc.addPage();
816
+ yPos = margin;
817
+
818
+ return; // Skip drawing line if we just started a new page
819
+ }
820
+
821
+ doc.setDrawColor('#E5E5E5');
822
+ doc.line(margin, yPos, pageWidth - margin, yPos);
823
+ yPos += lineHeight;
824
+ };
825
+
826
+ // Helper function to add footer to all pages
827
+ const addFooters = () => {
828
+ const totalPages = doc.internal.pages.length - 1;
829
+
830
+ for (let i = 1; i <= totalPages; i++) {
831
+ doc.setPage(i);
832
+ doc.setFontSize(8);
833
+ doc.setTextColor('#9CA3AF');
834
+ doc.text(`Page ${i} of ${totalPages}`, pageWidth / 2, doc.internal.pageSize.getHeight() - 10, {
835
+ align: 'center',
836
+ });
837
+ }
838
+ };
839
+
840
+ // Title and Header (first page only)
841
+ doc.setFillColor('#6366F1');
842
+ doc.rect(0, 0, pageWidth, 40, 'F');
843
+ doc.setTextColor('#FFFFFF');
844
+ doc.setFontSize(24);
845
+ doc.setFont('helvetica', 'bold');
846
+ doc.text('Debug Information Report', margin, 25);
847
+ yPos = 50;
848
+
849
+ // Timestamp and metadata
850
+ doc.setTextColor('#6B7280');
851
+ doc.setFontSize(10);
852
+ doc.setFont('helvetica', 'normal');
853
+
854
+ const timestamp = new Date().toLocaleString(undefined, {
855
+ year: 'numeric',
856
+ month: '2-digit',
857
+ day: '2-digit',
858
+ hour: '2-digit',
859
+ minute: '2-digit',
860
+ second: '2-digit',
861
+ });
862
+ doc.text(`Generated: ${timestamp}`, margin, yPos);
863
+ yPos += lineHeight * 2;
864
+
865
+ // System Information Section
866
+ if (debugData.system) {
867
+ addSectionHeader('System Information');
868
+
869
+ // OS and Architecture
870
+ addKeyValue('Operating System', debugData.system.os);
871
+ addKeyValue('Architecture', debugData.system.arch);
872
+ addKeyValue('Platform', debugData.system.platform);
873
+ addKeyValue('CPU Cores', debugData.system.cpus);
874
+
875
+ // Memory
876
+ const memory = debugData.system.memory;
877
+ addKeyValue('Memory', {
878
+ 'Total Memory': memory.total,
879
+ 'Used Memory': memory.used,
880
+ 'Free Memory': memory.free,
881
+ Usage: memory.percentage + '%',
882
+ });
883
+
884
+ // Browser Information
885
+ const browser = debugData.system.browser;
886
+ addKeyValue('Browser', {
887
+ Name: browser.name,
888
+ Version: browser.version,
889
+ Language: browser.language,
890
+ Platform: browser.platform,
891
+ 'Cookies Enabled': browser.cookiesEnabled ? 'Yes' : 'No',
892
+ 'Online Status': browser.online ? 'Online' : 'Offline',
893
+ });
894
+
895
+ // Screen Information
896
+ const screen = debugData.system.screen;
897
+ addKeyValue('Screen', {
898
+ Resolution: `${screen.width}x${screen.height}`,
899
+ 'Color Depth': screen.colorDepth + ' bit',
900
+ 'Pixel Ratio': screen.pixelRatio + 'x',
901
+ });
902
+
903
+ // Time Information
904
+ const time = debugData.system.time;
905
+ addKeyValue('Time Settings', {
906
+ Timezone: time.timezone,
907
+ 'UTC Offset': time.offset / 60 + ' hours',
908
+ Locale: time.locale,
909
+ });
910
+
911
+ addHorizontalLine();
912
+ }
913
+
914
+ // Web App Information Section
915
+ if (debugData.webApp) {
916
+ addSectionHeader('Web App Information');
917
+
918
+ // Basic Info
919
+ addKeyValue('Application', {
920
+ Name: debugData.webApp.name,
921
+ Version: debugData.webApp.version,
922
+ Environment: debugData.webApp.environment,
923
+ 'Node Version': debugData.webApp.runtimeInfo.nodeVersion,
924
+ });
925
+
926
+ // Git Information
927
+ if (debugData.webApp.gitInfo) {
928
+ const gitInfo = debugData.webApp.gitInfo.local;
929
+ addKeyValue('Git Information', {
930
+ Branch: gitInfo.branch,
931
+ Commit: gitInfo.commitHash,
932
+ Author: gitInfo.author,
933
+ 'Commit Time': gitInfo.commitTime,
934
+ Repository: gitInfo.repoName,
935
+ });
936
+
937
+ if (debugData.webApp.gitInfo.github) {
938
+ const githubInfo = debugData.webApp.gitInfo.github.currentRepo;
939
+ addKeyValue('GitHub Information', {
940
+ Repository: githubInfo.fullName,
941
+ 'Default Branch': githubInfo.defaultBranch,
942
+ Stars: githubInfo.stars,
943
+ Forks: githubInfo.forks,
944
+ 'Open Issues': githubInfo.openIssues || 0,
945
+ });
946
+ }
947
+ }
948
+
949
+ addHorizontalLine();
950
+ }
951
+
952
+ // Performance Section
953
+ if (debugData.performance) {
954
+ addSectionHeader('Performance Metrics');
955
+
956
+ // Memory Usage
957
+ const memory = debugData.performance.memory || {};
958
+ const totalHeap = memory.totalJSHeapSize || 0;
959
+ const usedHeap = memory.usedJSHeapSize || 0;
960
+ const usagePercentage = memory.usagePercentage || 0;
961
+
962
+ addKeyValue('Memory Usage', {
963
+ 'Total Heap Size': formatBytes(totalHeap),
964
+ 'Used Heap Size': formatBytes(usedHeap),
965
+ Usage: usagePercentage.toFixed(1) + '%',
966
+ });
967
+
968
+ // Timing Metrics
969
+ const timing = debugData.performance.timing || {};
970
+ const navigationStart = timing.navigationStart || 0;
971
+ const loadEventEnd = timing.loadEventEnd || 0;
972
+ const domContentLoadedEventEnd = timing.domContentLoadedEventEnd || 0;
973
+ const responseEnd = timing.responseEnd || 0;
974
+ const requestStart = timing.requestStart || 0;
975
+
976
+ const loadTime = loadEventEnd > navigationStart ? loadEventEnd - navigationStart : 0;
977
+ const domReadyTime =
978
+ domContentLoadedEventEnd > navigationStart ? domContentLoadedEventEnd - navigationStart : 0;
979
+ const requestTime = responseEnd > requestStart ? responseEnd - requestStart : 0;
980
+
981
+ addKeyValue('Page Load Metrics', {
982
+ 'Total Load Time': (loadTime / 1000).toFixed(2) + ' seconds',
983
+ 'DOM Ready Time': (domReadyTime / 1000).toFixed(2) + ' seconds',
984
+ 'Request Time': (requestTime / 1000).toFixed(2) + ' seconds',
985
+ });
986
+
987
+ // Network Information
988
+ if (debugData.system?.network) {
989
+ const network = debugData.system.network;
990
+ addKeyValue('Network Information', {
991
+ 'Connection Type': network.type || 'Unknown',
992
+ 'Effective Type': network.effectiveType || 'Unknown',
993
+ 'Download Speed': (network.downlink || 0) + ' Mbps',
994
+ 'Latency (RTT)': (network.rtt || 0) + ' ms',
995
+ 'Data Saver': network.saveData ? 'Enabled' : 'Disabled',
996
+ });
997
+ }
998
+
999
+ addHorizontalLine();
1000
+ }
1001
+
1002
+ // Errors Section
1003
+ if (debugData.errors && debugData.errors.length > 0) {
1004
+ addSectionHeader('Error Log');
1005
+
1006
+ debugData.errors.forEach((error: LogEntry, index: number) => {
1007
+ doc.setTextColor('#DC2626');
1008
+ doc.setFontSize(10);
1009
+ doc.setFont('helvetica', 'bold');
1010
+ doc.text(`Error ${index + 1}:`, margin, yPos);
1011
+ yPos += lineHeight;
1012
+
1013
+ doc.setFont('helvetica', 'normal');
1014
+ doc.setTextColor('#6B7280');
1015
+ addKeyValue('Message', error.message, 10);
1016
+
1017
+ if (error.stack) {
1018
+ addKeyValue('Stack', error.stack, 10);
1019
+ }
1020
+
1021
+ if (error.source) {
1022
+ addKeyValue('Source', error.source, 10);
1023
+ }
1024
+
1025
+ yPos += lineHeight;
1026
+ });
1027
+ }
1028
+
1029
+ // Add footers to all pages at the end
1030
+ addFooters();
1031
+
1032
+ // Save the PDF
1033
+ doc.save(`bolt-debug-info-${new Date().toISOString()}.pdf`);
1034
+ toast.success('Debug information exported as PDF');
1035
+ } catch (error) {
1036
+ console.error('Failed to export PDF:', error);
1037
+ toast.error('Failed to export debug information as PDF');
1038
+ }
1039
+ };
1040
+
1041
+ const exportAsText = () => {
1042
+ try {
1043
+ const debugData = {
1044
+ system: systemInfo,
1045
+ webApp: webAppInfo,
1046
+ errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
1047
+ performance: {
1048
+ memory: (performance as any).memory || {},
1049
+ timing: performance.timing,
1050
+ navigation: performance.navigation,
1051
+ },
1052
+ };
1053
+
1054
+ const textContent = Object.entries(debugData)
1055
+ .map(([category, data]) => {
1056
+ return `${category.toUpperCase()}\n${'-'.repeat(30)}\n${JSON.stringify(data, null, 2)}\n\n`;
1057
+ })
1058
+ .join('\n');
1059
+
1060
+ const blob = new Blob([textContent], { type: 'text/plain' });
1061
+ const url = window.URL.createObjectURL(blob);
1062
+ const a = document.createElement('a');
1063
+ a.href = url;
1064
+ a.download = `bolt-debug-info-${new Date().toISOString()}.txt`;
1065
+ document.body.appendChild(a);
1066
+ a.click();
1067
+ window.URL.revokeObjectURL(url);
1068
+ document.body.removeChild(a);
1069
+ toast.success('Debug information exported as text file');
1070
+ } catch (error) {
1071
+ console.error('Failed to export text file:', error);
1072
+ toast.error('Failed to export debug information as text file');
1073
+ }
1074
+ };
1075
+
1076
+ const exportFormats: ExportFormat[] = [
1077
+ {
1078
+ id: 'json',
1079
+ label: 'Export as JSON',
1080
+ icon: 'i-ph:file-json',
1081
+ handler: exportDebugInfo,
1082
+ },
1083
+ {
1084
+ id: 'csv',
1085
+ label: 'Export as CSV',
1086
+ icon: 'i-ph:file-csv',
1087
+ handler: exportAsCSV,
1088
+ },
1089
+ {
1090
+ id: 'pdf',
1091
+ label: 'Export as PDF',
1092
+ icon: 'i-ph:file-pdf',
1093
+ handler: exportAsPDF,
1094
+ },
1095
+ {
1096
+ id: 'txt',
1097
+ label: 'Export as Text',
1098
+ icon: 'i-ph:file-text',
1099
+ handler: exportAsText,
1100
+ },
1101
+ ];
1102
+
1103
+ // Add Ollama health check function
1104
+ const checkOllamaStatus = useCallback(async () => {
1105
+ try {
1106
+ const ollamaProvider = providers?.Ollama;
1107
+ const baseUrl = ollamaProvider?.settings?.baseUrl || 'http://127.0.0.1:11434';
1108
+
1109
+ // First check if service is running
1110
+ const versionResponse = await fetch(`${baseUrl}/api/version`);
1111
+
1112
+ if (!versionResponse.ok) {
1113
+ throw new Error('Service not running');
1114
+ }
1115
+
1116
+ // Then fetch installed models
1117
+ const modelsResponse = await fetch(`${baseUrl}/api/tags`);
1118
+
1119
+ const modelsData = (await modelsResponse.json()) as {
1120
+ models: Array<{ name: string; size: string; quantization: string }>;
1121
+ };
1122
+
1123
+ setOllamaStatus({
1124
+ isRunning: true,
1125
+ lastChecked: new Date(),
1126
+ models: modelsData.models,
1127
+ });
1128
+ } catch {
1129
+ setOllamaStatus({
1130
+ isRunning: false,
1131
+ error: 'Connection failed',
1132
+ lastChecked: new Date(),
1133
+ models: undefined,
1134
+ });
1135
+ }
1136
+ }, [providers]);
1137
+
1138
+ // Monitor Ollama provider status and check periodically
1139
+ useEffect(() => {
1140
+ const ollamaProvider = providers?.Ollama;
1141
+
1142
+ if (ollamaProvider?.settings?.enabled) {
1143
+ // Check immediately when provider is enabled
1144
+ checkOllamaStatus();
1145
+
1146
+ // Set up periodic checks every 10 seconds
1147
+ const intervalId = setInterval(checkOllamaStatus, 10000);
1148
+
1149
+ return () => clearInterval(intervalId);
1150
+ }
1151
+
1152
+ return undefined;
1153
+ }, [providers, checkOllamaStatus]);
1154
+
1155
+ // Replace the existing export button with this new component
1156
+ const ExportButton = () => {
1157
+ const [isOpen, setIsOpen] = useState(false);
1158
+
1159
+ const handleOpenChange = useCallback((open: boolean) => {
1160
+ setIsOpen(open);
1161
+ }, []);
1162
+
1163
+ const handleFormatClick = useCallback((handler: () => void) => {
1164
+ handler();
1165
+ setIsOpen(false);
1166
+ }, []);
1167
+
1168
+ return (
1169
+ <DialogRoot open={isOpen} onOpenChange={handleOpenChange}>
1170
+ <button
1171
+ onClick={() => setIsOpen(true)}
1172
+ className={classNames(
1173
+ 'group flex items-center gap-2',
1174
+ 'rounded-lg px-3 py-1.5',
1175
+ 'text-sm text-gray-900 dark:text-white',
1176
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
1177
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
1178
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
1179
+ 'transition-all duration-200',
1180
+ )}
1181
+ >
1182
+ <span className="i-ph:download text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
1183
+ Export
1184
+ </button>
1185
+
1186
+ <Dialog showCloseButton>
1187
+ <div className="p-6">
1188
+ <DialogTitle className="flex items-center gap-2">
1189
+ <div className="i-ph:download w-5 h-5" />
1190
+ Export Debug Information
1191
+ </DialogTitle>
1192
+
1193
+ <div className="mt-4 flex flex-col gap-2">
1194
+ {exportFormats.map((format) => (
1195
+ <button
1196
+ key={format.id}
1197
+ onClick={() => handleFormatClick(format.handler)}
1198
+ className={classNames(
1199
+ 'flex items-center gap-3 px-4 py-3 text-sm rounded-lg transition-colors w-full text-left',
1200
+ 'bg-white dark:bg-[#0A0A0A]',
1201
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
1202
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
1203
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
1204
+ 'text-bolt-elements-textPrimary',
1205
+ )}
1206
+ >
1207
+ <div className={classNames(format.icon, 'w-5 h-5')} />
1208
+ <div>
1209
+ <div className="font-medium">{format.label}</div>
1210
+ <div className="text-xs text-bolt-elements-textSecondary mt-0.5">
1211
+ {format.id === 'json' && 'Export as a structured JSON file'}
1212
+ {format.id === 'csv' && 'Export as a CSV spreadsheet'}
1213
+ {format.id === 'pdf' && 'Export as a formatted PDF document'}
1214
+ {format.id === 'txt' && 'Export as a formatted text file'}
1215
+ </div>
1216
+ </div>
1217
+ </button>
1218
+ ))}
1219
+ </div>
1220
+ </div>
1221
+ </Dialog>
1222
+ </DialogRoot>
1223
+ );
1224
+ };
1225
+
1226
+ // Add helper function to get Ollama status text and color
1227
+ const getOllamaStatus = () => {
1228
+ const ollamaProvider = providers?.Ollama;
1229
+ const isOllamaEnabled = ollamaProvider?.settings?.enabled;
1230
+
1231
+ if (!isOllamaEnabled) {
1232
+ return {
1233
+ status: 'Disabled',
1234
+ color: 'text-red-500',
1235
+ bgColor: 'bg-red-500',
1236
+ message: 'Ollama provider is disabled in settings',
1237
+ };
1238
+ }
1239
+
1240
+ if (!ollamaStatus.isRunning) {
1241
+ return {
1242
+ status: 'Not Running',
1243
+ color: 'text-red-500',
1244
+ bgColor: 'bg-red-500',
1245
+ message: ollamaStatus.error || 'Ollama service is not running',
1246
+ };
1247
+ }
1248
+
1249
+ const modelCount = ollamaStatus.models?.length ?? 0;
1250
+
1251
+ return {
1252
+ status: 'Running',
1253
+ color: 'text-green-500',
1254
+ bgColor: 'bg-green-500',
1255
+ message: `Ollama service is running with ${modelCount} installed models (Provider: Enabled)`,
1256
+ };
1257
+ };
1258
+
1259
+ // Add type for status result
1260
+ type StatusResult = {
1261
+ status: string;
1262
+ color: string;
1263
+ bgColor: string;
1264
+ message: string;
1265
+ };
1266
+
1267
+ const status = getOllamaStatus() as StatusResult;
1268
+
1269
+ return (
1270
+ <div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
1271
+ {/* Quick Stats Banner */}
1272
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
1273
+ {/* Errors Card */}
1274
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
1275
+ <div className="flex items-center gap-2">
1276
+ <div className="i-ph:warning-octagon text-purple-500 w-4 h-4" />
1277
+ <div className="text-sm text-bolt-elements-textSecondary">Errors</div>
1278
+ </div>
1279
+ <div className="flex items-center gap-2 mt-2">
1280
+ <span
1281
+ className={classNames('text-2xl font-semibold', errorLogs.length > 0 ? 'text-red-500' : 'text-green-500')}
1282
+ >
1283
+ {errorLogs.length}
1284
+ </span>
1285
+ </div>
1286
+ <div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
1287
+ <div
1288
+ className={classNames(
1289
+ 'w-3.5 h-3.5',
1290
+ errorLogs.length > 0 ? 'i-ph:warning text-red-500' : 'i-ph:check-circle text-green-500',
1291
+ )}
1292
+ />
1293
+ {errorLogs.length > 0 ? 'Errors detected' : 'No errors detected'}
1294
+ </div>
1295
+ </div>
1296
+
1297
+ {/* Memory Usage Card */}
1298
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
1299
+ <div className="flex items-center gap-2">
1300
+ <div className="i-ph:cpu text-purple-500 w-4 h-4" />
1301
+ <div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
1302
+ </div>
1303
+ <div className="flex items-center gap-2 mt-2">
1304
+ <span
1305
+ className={classNames(
1306
+ 'text-2xl font-semibold',
1307
+ (systemInfo?.memory?.percentage ?? 0) > 80
1308
+ ? 'text-red-500'
1309
+ : (systemInfo?.memory?.percentage ?? 0) > 60
1310
+ ? 'text-yellow-500'
1311
+ : 'text-green-500',
1312
+ )}
1313
+ >
1314
+ {systemInfo?.memory?.percentage ?? 0}%
1315
+ </span>
1316
+ </div>
1317
+ <Progress
1318
+ value={systemInfo?.memory?.percentage ?? 0}
1319
+ className={classNames(
1320
+ 'mt-2',
1321
+ (systemInfo?.memory?.percentage ?? 0) > 80
1322
+ ? '[&>div]:bg-red-500'
1323
+ : (systemInfo?.memory?.percentage ?? 0) > 60
1324
+ ? '[&>div]:bg-yellow-500'
1325
+ : '[&>div]:bg-green-500',
1326
+ )}
1327
+ />
1328
+ <div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
1329
+ <div className="i-ph:info w-3.5 h-3.5 text-purple-500" />
1330
+ Used: {systemInfo?.memory.used ?? '0 GB'} / {systemInfo?.memory.total ?? '0 GB'}
1331
+ </div>
1332
+ </div>
1333
+
1334
+ {/* Page Load Time Card */}
1335
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
1336
+ <div className="flex items-center gap-2">
1337
+ <div className="i-ph:timer text-purple-500 w-4 h-4" />
1338
+ <div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
1339
+ </div>
1340
+ <div className="flex items-center gap-2 mt-2">
1341
+ <span
1342
+ className={classNames(
1343
+ 'text-2xl font-semibold',
1344
+ (systemInfo?.performance.timing.loadTime ?? 0) > 2000
1345
+ ? 'text-red-500'
1346
+ : (systemInfo?.performance.timing.loadTime ?? 0) > 1000
1347
+ ? 'text-yellow-500'
1348
+ : 'text-green-500',
1349
+ )}
1350
+ >
1351
+ {systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) : '-'}s
1352
+ </span>
1353
+ </div>
1354
+ <div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
1355
+ <div className="i-ph:code w-3.5 h-3.5 text-purple-500" />
1356
+ DOM Ready: {systemInfo ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2) : '-'}s
1357
+ </div>
1358
+ </div>
1359
+
1360
+ {/* Network Speed Card */}
1361
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
1362
+ <div className="flex items-center gap-2">
1363
+ <div className="i-ph:wifi-high text-purple-500 w-4 h-4" />
1364
+ <div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
1365
+ </div>
1366
+ <div className="flex items-center gap-2 mt-2">
1367
+ <span
1368
+ className={classNames(
1369
+ 'text-2xl font-semibold',
1370
+ (systemInfo?.network.downlink ?? 0) < 5
1371
+ ? 'text-red-500'
1372
+ : (systemInfo?.network.downlink ?? 0) < 10
1373
+ ? 'text-yellow-500'
1374
+ : 'text-green-500',
1375
+ )}
1376
+ >
1377
+ {systemInfo?.network.downlink ?? '-'} Mbps
1378
+ </span>
1379
+ </div>
1380
+ <div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
1381
+ <div className="i-ph:activity w-3.5 h-3.5 text-purple-500" />
1382
+ RTT: {systemInfo?.network.rtt ?? '-'} ms
1383
+ </div>
1384
+ </div>
1385
+
1386
+ {/* Ollama Service Card - Now spans all 4 columns */}
1387
+ <div className="md:col-span-4 p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[260px] flex flex-col">
1388
+ <div className="flex items-center justify-between">
1389
+ <div className="flex items-center gap-3">
1390
+ <div className="i-ph:robot text-purple-500 w-5 h-5" />
1391
+ <div>
1392
+ <div className="text-base font-medium text-bolt-elements-textPrimary">Ollama Service</div>
1393
+ <div className="text-xs text-bolt-elements-textSecondary mt-0.5">{status.message}</div>
1394
+ </div>
1395
+ </div>
1396
+ <div className="flex items-center gap-3">
1397
+ <div className="flex items-center gap-2 px-2.5 py-1 rounded-full bg-bolt-elements-background-depth-3">
1398
+ <div
1399
+ className={classNames('w-2 h-2 rounded-full animate-pulse', status.bgColor, {
1400
+ 'shadow-lg shadow-green-500/20': status.status === 'Running',
1401
+ 'shadow-lg shadow-red-500/20': status.status === 'Not Running',
1402
+ })}
1403
+ />
1404
+ <span className={classNames('text-xs font-medium flex items-center gap-1', status.color)}>
1405
+ {status.status}
1406
+ </span>
1407
+ </div>
1408
+ <div className="text-[10px] text-bolt-elements-textTertiary flex items-center gap-1.5">
1409
+ <div className="i-ph:clock w-3 h-3" />
1410
+ {ollamaStatus.lastChecked.toLocaleTimeString()}
1411
+ </div>
1412
+ </div>
1413
+ </div>
1414
+
1415
+ <div className="mt-6 flex-1 min-h-0 flex flex-col">
1416
+ {status.status === 'Running' && ollamaStatus.models && ollamaStatus.models.length > 0 ? (
1417
+ <>
1418
+ <div className="text-xs font-medium text-bolt-elements-textSecondary flex items-center justify-between mb-3">
1419
+ <div className="flex items-center gap-2">
1420
+ <div className="i-ph:cube-duotone w-4 h-4 text-purple-500" />
1421
+ <span>Installed Models</span>
1422
+ <Badge variant="secondary" className="ml-1">
1423
+ {ollamaStatus.models.length}
1424
+ </Badge>
1425
+ </div>
1426
+ </div>
1427
+ <div className="overflow-y-auto flex-1 scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-400 dark:hover:scrollbar-thumb-gray-600">
1428
+ <div className="grid grid-cols-2 gap-3 pr-2">
1429
+ {ollamaStatus.models.map((model) => (
1430
+ <div
1431
+ key={model.name}
1432
+ className="text-sm bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-4 rounded-lg px-4 py-3 flex items-center justify-between transition-colors group"
1433
+ >
1434
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
1435
+ <div className="i-ph:cube w-4 h-4 text-purple-500/70 group-hover:text-purple-500 transition-colors" />
1436
+ <span className="font-mono truncate">{model.name}</span>
1437
+ </div>
1438
+ <Badge variant="outline" className="ml-2 text-xs font-mono">
1439
+ {Math.round(parseInt(model.size) / 1024 / 1024)}MB
1440
+ </Badge>
1441
+ </div>
1442
+ ))}
1443
+ </div>
1444
+ </div>
1445
+ </>
1446
+ ) : (
1447
+ <div className="flex-1 flex items-center justify-center">
1448
+ <div className="flex flex-col items-center gap-3 max-w-[280px] text-center">
1449
+ <div
1450
+ className={classNames('w-12 h-12', {
1451
+ 'i-ph:warning-circle text-red-500/80':
1452
+ status.status === 'Not Running' || status.status === 'Disabled',
1453
+ 'i-ph:cube-duotone text-purple-500/80': status.status === 'Running',
1454
+ })}
1455
+ />
1456
+ <span className="text-sm text-bolt-elements-textSecondary">{status.message}</span>
1457
+ </div>
1458
+ </div>
1459
+ )}
1460
+ </div>
1461
+ </div>
1462
+ </div>
1463
+
1464
+ {/* Action Buttons */}
1465
+ <div className="flex flex-wrap gap-4">
1466
+ <button
1467
+ onClick={getSystemInfo}
1468
+ disabled={loading.systemInfo}
1469
+ className={classNames(
1470
+ 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
1471
+ 'bg-white dark:bg-[#0A0A0A]',
1472
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
1473
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
1474
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
1475
+ 'text-bolt-elements-textPrimary',
1476
+ { 'opacity-50 cursor-not-allowed': loading.systemInfo },
1477
+ )}
1478
+ >
1479
+ {loading.systemInfo ? (
1480
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
1481
+ ) : (
1482
+ <div className="i-ph:gear w-4 h-4" />
1483
+ )}
1484
+ Update System Info
1485
+ </button>
1486
+
1487
+ <button
1488
+ onClick={handleLogPerformance}
1489
+ disabled={loading.performance}
1490
+ className={classNames(
1491
+ 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
1492
+ 'bg-white dark:bg-[#0A0A0A]',
1493
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
1494
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
1495
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
1496
+ 'text-bolt-elements-textPrimary',
1497
+ { 'opacity-50 cursor-not-allowed': loading.performance },
1498
+ )}
1499
+ >
1500
+ {loading.performance ? (
1501
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
1502
+ ) : (
1503
+ <div className="i-ph:chart-bar w-4 h-4" />
1504
+ )}
1505
+ Log Performance
1506
+ </button>
1507
+
1508
+ <button
1509
+ onClick={checkErrors}
1510
+ disabled={loading.errors}
1511
+ className={classNames(
1512
+ 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
1513
+ 'bg-white dark:bg-[#0A0A0A]',
1514
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
1515
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
1516
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
1517
+ 'text-bolt-elements-textPrimary',
1518
+ { 'opacity-50 cursor-not-allowed': loading.errors },
1519
+ )}
1520
+ >
1521
+ {loading.errors ? (
1522
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
1523
+ ) : (
1524
+ <div className="i-ph:warning w-4 h-4" />
1525
+ )}
1526
+ Check Errors
1527
+ </button>
1528
+
1529
+ <button
1530
+ onClick={getWebAppInfo}
1531
+ disabled={loading.webAppInfo}
1532
+ className={classNames(
1533
+ 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
1534
+ 'bg-white dark:bg-[#0A0A0A]',
1535
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
1536
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
1537
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
1538
+ 'text-bolt-elements-textPrimary',
1539
+ { 'opacity-50 cursor-not-allowed': loading.webAppInfo },
1540
+ )}
1541
+ >
1542
+ {loading.webAppInfo ? (
1543
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
1544
+ ) : (
1545
+ <div className="i-ph:info w-4 h-4" />
1546
+ )}
1547
+ Fetch WebApp Info
1548
+ </button>
1549
+
1550
+ <ExportButton />
1551
+ </div>
1552
+
1553
+ {/* System Information */}
1554
+ <Collapsible
1555
+ open={openSections.system}
1556
+ onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, system: open }))}
1557
+ className="w-full"
1558
+ >
1559
+ <CollapsibleTrigger className="w-full">
1560
+ <div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
1561
+ <div className="flex items-center gap-3">
1562
+ <div className="i-ph:cpu text-purple-500 w-5 h-5" />
1563
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
1564
+ </div>
1565
+ <div
1566
+ className={classNames(
1567
+ 'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
1568
+ openSections.system ? 'rotate-180' : '',
1569
+ )}
1570
+ />
1571
+ </div>
1572
+ </CollapsibleTrigger>
1573
+
1574
+ <CollapsibleContent>
1575
+ <div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
1576
+ {systemInfo ? (
1577
+ <div className="grid grid-cols-2 gap-6">
1578
+ <div className="space-y-2">
1579
+ <div className="text-sm flex items-center gap-2">
1580
+ <div className="i-ph:desktop text-bolt-elements-textSecondary w-4 h-4" />
1581
+ <span className="text-bolt-elements-textSecondary">OS: </span>
1582
+ <span className="text-bolt-elements-textPrimary">{systemInfo.os}</span>
1583
+ </div>
1584
+ <div className="text-sm flex items-center gap-2">
1585
+ <div className="i-ph:device-mobile text-bolt-elements-textSecondary w-4 h-4" />
1586
+ <span className="text-bolt-elements-textSecondary">Platform: </span>
1587
+ <span className="text-bolt-elements-textPrimary">{systemInfo.platform}</span>
1588
+ </div>
1589
+ <div className="text-sm flex items-center gap-2">
1590
+ <div className="i-ph:microchip text-bolt-elements-textSecondary w-4 h-4" />
1591
+ <span className="text-bolt-elements-textSecondary">Architecture: </span>
1592
+ <span className="text-bolt-elements-textPrimary">{systemInfo.arch}</span>
1593
+ </div>
1594
+ <div className="text-sm flex items-center gap-2">
1595
+ <div className="i-ph:cpu text-bolt-elements-textSecondary w-4 h-4" />
1596
+ <span className="text-bolt-elements-textSecondary">CPU Cores: </span>
1597
+ <span className="text-bolt-elements-textPrimary">{systemInfo.cpus}</span>
1598
+ </div>
1599
+ <div className="text-sm flex items-center gap-2">
1600
+ <div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
1601
+ <span className="text-bolt-elements-textSecondary">Node Version: </span>
1602
+ <span className="text-bolt-elements-textPrimary">{systemInfo.node}</span>
1603
+ </div>
1604
+ <div className="text-sm flex items-center gap-2">
1605
+ <div className="i-ph:wifi-high text-bolt-elements-textSecondary w-4 h-4" />
1606
+ <span className="text-bolt-elements-textSecondary">Network Type: </span>
1607
+ <span className="text-bolt-elements-textPrimary">
1608
+ {systemInfo.network.type} ({systemInfo.network.effectiveType})
1609
+ </span>
1610
+ </div>
1611
+ <div className="text-sm flex items-center gap-2">
1612
+ <div className="i-ph:gauge text-bolt-elements-textSecondary w-4 h-4" />
1613
+ <span className="text-bolt-elements-textSecondary">Network Speed: </span>
1614
+ <span className="text-bolt-elements-textPrimary">
1615
+ {systemInfo.network.downlink}Mbps (RTT: {systemInfo.network.rtt}ms)
1616
+ </span>
1617
+ </div>
1618
+ {systemInfo.battery && (
1619
+ <div className="text-sm flex items-center gap-2">
1620
+ <div className="i-ph:battery-charging text-bolt-elements-textSecondary w-4 h-4" />
1621
+ <span className="text-bolt-elements-textSecondary">Battery: </span>
1622
+ <span className="text-bolt-elements-textPrimary">
1623
+ {systemInfo.battery.level.toFixed(1)}% {systemInfo.battery.charging ? '(Charging)' : ''}
1624
+ </span>
1625
+ </div>
1626
+ )}
1627
+ <div className="text-sm flex items-center gap-2">
1628
+ <div className="i-ph:hard-drive text-bolt-elements-textSecondary w-4 h-4" />
1629
+ <span className="text-bolt-elements-textSecondary">Storage: </span>
1630
+ <span className="text-bolt-elements-textPrimary">
1631
+ {(systemInfo.storage.usage / (1024 * 1024 * 1024)).toFixed(2)}GB /{' '}
1632
+ {(systemInfo.storage.quota / (1024 * 1024 * 1024)).toFixed(2)}GB
1633
+ </span>
1634
+ </div>
1635
+ </div>
1636
+ <div className="space-y-2">
1637
+ <div className="text-sm flex items-center gap-2">
1638
+ <div className="i-ph:database text-bolt-elements-textSecondary w-4 h-4" />
1639
+ <span className="text-bolt-elements-textSecondary">Memory Usage: </span>
1640
+ <span className="text-bolt-elements-textPrimary">
1641
+ {systemInfo.memory.used} / {systemInfo.memory.total} ({systemInfo.memory.percentage}%)
1642
+ </span>
1643
+ </div>
1644
+ <div className="text-sm flex items-center gap-2">
1645
+ <div className="i-ph:browser text-bolt-elements-textSecondary w-4 h-4" />
1646
+ <span className="text-bolt-elements-textSecondary">Browser: </span>
1647
+ <span className="text-bolt-elements-textPrimary">
1648
+ {systemInfo.browser.name} {systemInfo.browser.version}
1649
+ </span>
1650
+ </div>
1651
+ <div className="text-sm flex items-center gap-2">
1652
+ <div className="i-ph:monitor text-bolt-elements-textSecondary w-4 h-4" />
1653
+ <span className="text-bolt-elements-textSecondary">Screen: </span>
1654
+ <span className="text-bolt-elements-textPrimary">
1655
+ {systemInfo.screen.width}x{systemInfo.screen.height} ({systemInfo.screen.pixelRatio}x)
1656
+ </span>
1657
+ </div>
1658
+ <div className="text-sm flex items-center gap-2">
1659
+ <div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
1660
+ <span className="text-bolt-elements-textSecondary">Timezone: </span>
1661
+ <span className="text-bolt-elements-textPrimary">{systemInfo.time.timezone}</span>
1662
+ </div>
1663
+ <div className="text-sm flex items-center gap-2">
1664
+ <div className="i-ph:translate text-bolt-elements-textSecondary w-4 h-4" />
1665
+ <span className="text-bolt-elements-textSecondary">Language: </span>
1666
+ <span className="text-bolt-elements-textPrimary">{systemInfo.browser.language}</span>
1667
+ </div>
1668
+ <div className="text-sm flex items-center gap-2">
1669
+ <div className="i-ph:chart-pie text-bolt-elements-textSecondary w-4 h-4" />
1670
+ <span className="text-bolt-elements-textSecondary">JS Heap: </span>
1671
+ <span className="text-bolt-elements-textPrimary">
1672
+ {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
1673
+ {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB (
1674
+ {systemInfo.performance.memory.usagePercentage.toFixed(1)}%)
1675
+ </span>
1676
+ </div>
1677
+ <div className="text-sm flex items-center gap-2">
1678
+ <div className="i-ph:timer text-bolt-elements-textSecondary w-4 h-4" />
1679
+ <span className="text-bolt-elements-textSecondary">Page Load: </span>
1680
+ <span className="text-bolt-elements-textPrimary">
1681
+ {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
1682
+ </span>
1683
+ </div>
1684
+ <div className="text-sm flex items-center gap-2">
1685
+ <div className="i-ph:code text-bolt-elements-textSecondary w-4 h-4" />
1686
+ <span className="text-bolt-elements-textSecondary">DOM Ready: </span>
1687
+ <span className="text-bolt-elements-textPrimary">
1688
+ {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
1689
+ </span>
1690
+ </div>
1691
+ </div>
1692
+ </div>
1693
+ ) : (
1694
+ <div className="text-sm text-bolt-elements-textSecondary">Loading system information...</div>
1695
+ )}
1696
+ </div>
1697
+ </CollapsibleContent>
1698
+ </Collapsible>
1699
+
1700
+ {/* Performance Metrics */}
1701
+ <Collapsible
1702
+ open={openSections.performance}
1703
+ onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, performance: open }))}
1704
+ className="w-full"
1705
+ >
1706
+ <CollapsibleTrigger className="w-full">
1707
+ <div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
1708
+ <div className="flex items-center gap-3">
1709
+ <div className="i-ph:chart-line text-purple-500 w-5 h-5" />
1710
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
1711
+ </div>
1712
+ <div
1713
+ className={classNames(
1714
+ 'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
1715
+ openSections.performance ? 'rotate-180' : '',
1716
+ )}
1717
+ />
1718
+ </div>
1719
+ </CollapsibleTrigger>
1720
+
1721
+ <CollapsibleContent>
1722
+ <div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
1723
+ {systemInfo && (
1724
+ <div className="grid grid-cols-2 gap-4">
1725
+ <div className="space-y-2">
1726
+ <div className="text-sm">
1727
+ <span className="text-bolt-elements-textSecondary">Page Load Time: </span>
1728
+ <span className="text-bolt-elements-textPrimary">
1729
+ {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
1730
+ </span>
1731
+ </div>
1732
+ <div className="text-sm">
1733
+ <span className="text-bolt-elements-textSecondary">DOM Ready Time: </span>
1734
+ <span className="text-bolt-elements-textPrimary">
1735
+ {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
1736
+ </span>
1737
+ </div>
1738
+ <div className="text-sm">
1739
+ <span className="text-bolt-elements-textSecondary">Request Time: </span>
1740
+ <span className="text-bolt-elements-textPrimary">
1741
+ {(systemInfo.performance.timing.requestTime / 1000).toFixed(2)}s
1742
+ </span>
1743
+ </div>
1744
+ <div className="text-sm">
1745
+ <span className="text-bolt-elements-textSecondary">Redirect Time: </span>
1746
+ <span className="text-bolt-elements-textPrimary">
1747
+ {(systemInfo.performance.timing.redirectTime / 1000).toFixed(2)}s
1748
+ </span>
1749
+ </div>
1750
+ </div>
1751
+ <div className="space-y-2">
1752
+ <div className="text-sm">
1753
+ <span className="text-bolt-elements-textSecondary">JS Heap Usage: </span>
1754
+ <span className="text-bolt-elements-textPrimary">
1755
+ {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
1756
+ {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB
1757
+ </span>
1758
+ </div>
1759
+ <div className="text-sm">
1760
+ <span className="text-bolt-elements-textSecondary">Heap Utilization: </span>
1761
+ <span className="text-bolt-elements-textPrimary">
1762
+ {systemInfo.performance.memory.usagePercentage.toFixed(1)}%
1763
+ </span>
1764
+ </div>
1765
+ <div className="text-sm">
1766
+ <span className="text-bolt-elements-textSecondary">Navigation Type: </span>
1767
+ <span className="text-bolt-elements-textPrimary">
1768
+ {systemInfo.performance.navigation.type === 0
1769
+ ? 'Navigate'
1770
+ : systemInfo.performance.navigation.type === 1
1771
+ ? 'Reload'
1772
+ : systemInfo.performance.navigation.type === 2
1773
+ ? 'Back/Forward'
1774
+ : 'Other'}
1775
+ </span>
1776
+ </div>
1777
+ <div className="text-sm">
1778
+ <span className="text-bolt-elements-textSecondary">Redirects: </span>
1779
+ <span className="text-bolt-elements-textPrimary">
1780
+ {systemInfo.performance.navigation.redirectCount}
1781
+ </span>
1782
+ </div>
1783
+ </div>
1784
+ </div>
1785
+ )}
1786
+ </div>
1787
+ </CollapsibleContent>
1788
+ </Collapsible>
1789
+
1790
+ {/* WebApp Information */}
1791
+ <Collapsible
1792
+ open={openSections.webapp}
1793
+ onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, webapp: open }))}
1794
+ className="w-full"
1795
+ >
1796
+ <CollapsibleTrigger className="w-full">
1797
+ <div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
1798
+ <div className="flex items-center gap-3">
1799
+ <div className="i-ph:info text-blue-500 w-5 h-5" />
1800
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
1801
+ {loading.webAppInfo && <span className="loading loading-spinner loading-sm" />}
1802
+ </div>
1803
+ <div
1804
+ className={classNames(
1805
+ 'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
1806
+ openSections.webapp ? 'rotate-180' : '',
1807
+ )}
1808
+ />
1809
+ </div>
1810
+ </CollapsibleTrigger>
1811
+
1812
+ <CollapsibleContent>
1813
+ <div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
1814
+ {loading.webAppInfo ? (
1815
+ <div className="flex items-center justify-center p-8">
1816
+ <span className="loading loading-spinner loading-lg" />
1817
+ </div>
1818
+ ) : !webAppInfo ? (
1819
+ <div className="flex flex-col items-center justify-center p-8 text-bolt-elements-textSecondary">
1820
+ <div className="i-ph:warning-circle w-8 h-8 mb-2" />
1821
+ <p>Failed to load WebApp information</p>
1822
+ <button
1823
+ onClick={() => getWebAppInfo()}
1824
+ className="mt-4 px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
1825
+ >
1826
+ Retry
1827
+ </button>
1828
+ </div>
1829
+ ) : (
1830
+ <div className="grid grid-cols-2 gap-6">
1831
+ <div>
1832
+ <h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Basic Information</h3>
1833
+ <div className="space-y-3">
1834
+ <div className="text-sm flex items-center gap-2">
1835
+ <div className="i-ph:app-window text-bolt-elements-textSecondary w-4 h-4" />
1836
+ <span className="text-bolt-elements-textSecondary">Name:</span>
1837
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.name}</span>
1838
+ </div>
1839
+ <div className="text-sm flex items-center gap-2">
1840
+ <div className="i-ph:tag text-bolt-elements-textSecondary w-4 h-4" />
1841
+ <span className="text-bolt-elements-textSecondary">Version:</span>
1842
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.version}</span>
1843
+ </div>
1844
+ <div className="text-sm flex items-center gap-2">
1845
+ <div className="i-ph:certificate text-bolt-elements-textSecondary w-4 h-4" />
1846
+ <span className="text-bolt-elements-textSecondary">License:</span>
1847
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.license}</span>
1848
+ </div>
1849
+ <div className="text-sm flex items-center gap-2">
1850
+ <div className="i-ph:cloud text-bolt-elements-textSecondary w-4 h-4" />
1851
+ <span className="text-bolt-elements-textSecondary">Environment:</span>
1852
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.environment}</span>
1853
+ </div>
1854
+ <div className="text-sm flex items-center gap-2">
1855
+ <div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
1856
+ <span className="text-bolt-elements-textSecondary">Node Version:</span>
1857
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.runtimeInfo.nodeVersion}</span>
1858
+ </div>
1859
+ </div>
1860
+ </div>
1861
+
1862
+ <div>
1863
+ <h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Git Information</h3>
1864
+ <div className="space-y-3">
1865
+ <div className="text-sm flex items-center gap-2">
1866
+ <div className="i-ph:git-branch text-bolt-elements-textSecondary w-4 h-4" />
1867
+ <span className="text-bolt-elements-textSecondary">Branch:</span>
1868
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.branch}</span>
1869
+ </div>
1870
+ <div className="text-sm flex items-center gap-2">
1871
+ <div className="i-ph:git-commit text-bolt-elements-textSecondary w-4 h-4" />
1872
+ <span className="text-bolt-elements-textSecondary">Commit:</span>
1873
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitHash}</span>
1874
+ </div>
1875
+ <div className="text-sm flex items-center gap-2">
1876
+ <div className="i-ph:user text-bolt-elements-textSecondary w-4 h-4" />
1877
+ <span className="text-bolt-elements-textSecondary">Author:</span>
1878
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.author}</span>
1879
+ </div>
1880
+ <div className="text-sm flex items-center gap-2">
1881
+ <div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
1882
+ <span className="text-bolt-elements-textSecondary">Commit Time:</span>
1883
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitTime}</span>
1884
+ </div>
1885
+
1886
+ {webAppInfo.gitInfo.github && (
1887
+ <>
1888
+ <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
1889
+ <div className="text-sm flex items-center gap-2">
1890
+ <div className="i-ph:git-repository text-bolt-elements-textSecondary w-4 h-4" />
1891
+ <span className="text-bolt-elements-textSecondary">Repository:</span>
1892
+ <span className="text-bolt-elements-textPrimary">
1893
+ {webAppInfo.gitInfo.github.currentRepo.fullName}
1894
+ {webAppInfo.gitInfo.isForked && ' (fork)'}
1895
+ </span>
1896
+ </div>
1897
+
1898
+ <div className="mt-2 flex items-center gap-4 text-sm">
1899
+ <div className="flex items-center gap-1">
1900
+ <div className="i-ph:star text-yellow-500 w-4 h-4" />
1901
+ <span className="text-bolt-elements-textSecondary">
1902
+ {webAppInfo.gitInfo.github.currentRepo.stars}
1903
+ </span>
1904
+ </div>
1905
+ <div className="flex items-center gap-1">
1906
+ <div className="i-ph:git-fork text-blue-500 w-4 h-4" />
1907
+ <span className="text-bolt-elements-textSecondary">
1908
+ {webAppInfo.gitInfo.github.currentRepo.forks}
1909
+ </span>
1910
+ </div>
1911
+ <div className="flex items-center gap-1">
1912
+ <div className="i-ph:warning-circle text-red-500 w-4 h-4" />
1913
+ <span className="text-bolt-elements-textSecondary">
1914
+ {webAppInfo.gitInfo.github.currentRepo.openIssues}
1915
+ </span>
1916
+ </div>
1917
+ </div>
1918
+ </div>
1919
+
1920
+ {webAppInfo.gitInfo.github.upstream && (
1921
+ <div className="mt-2">
1922
+ <div className="text-sm flex items-center gap-2">
1923
+ <div className="i-ph:git-fork text-bolt-elements-textSecondary w-4 h-4" />
1924
+ <span className="text-bolt-elements-textSecondary">Upstream:</span>
1925
+ <span className="text-bolt-elements-textPrimary">
1926
+ {webAppInfo.gitInfo.github.upstream.fullName}
1927
+ </span>
1928
+ </div>
1929
+
1930
+ <div className="mt-2 flex items-center gap-4 text-sm">
1931
+ <div className="flex items-center gap-1">
1932
+ <div className="i-ph:star text-yellow-500 w-4 h-4" />
1933
+ <span className="text-bolt-elements-textSecondary">
1934
+ {webAppInfo.gitInfo.github.upstream.stars}
1935
+ </span>
1936
+ </div>
1937
+ <div className="flex items-center gap-1">
1938
+ <div className="i-ph:git-fork text-blue-500 w-4 h-4" />
1939
+ <span className="text-bolt-elements-textSecondary">
1940
+ {webAppInfo.gitInfo.github.upstream.forks}
1941
+ </span>
1942
+ </div>
1943
+ </div>
1944
+ </div>
1945
+ )}
1946
+ </>
1947
+ )}
1948
+ </div>
1949
+ </div>
1950
+ </div>
1951
+ )}
1952
+
1953
+ {webAppInfo && (
1954
+ <div className="mt-6">
1955
+ <h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Dependencies</h3>
1956
+ <div className="bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded-lg divide-y divide-[#E5E5E5] dark:divide-[#1A1A1A]">
1957
+ <DependencySection title="Production" deps={webAppInfo.dependencies.production} />
1958
+ <DependencySection title="Development" deps={webAppInfo.dependencies.development} />
1959
+ <DependencySection title="Peer" deps={webAppInfo.dependencies.peer} />
1960
+ <DependencySection title="Optional" deps={webAppInfo.dependencies.optional} />
1961
+ </div>
1962
+ </div>
1963
+ )}
1964
+ </div>
1965
+ </CollapsibleContent>
1966
+ </Collapsible>
1967
+
1968
+ {/* Error Check */}
1969
+ <Collapsible
1970
+ open={openSections.errors}
1971
+ onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, errors: open }))}
1972
+ className="w-full"
1973
+ >
1974
+ <CollapsibleTrigger className="w-full">
1975
+ <div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
1976
+ <div className="flex items-center gap-3">
1977
+ <div className="i-ph:warning text-red-500 w-5 h-5" />
1978
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
1979
+ {errorLogs.length > 0 && (
1980
+ <Badge variant="destructive" className="ml-2">
1981
+ {errorLogs.length} Errors
1982
+ </Badge>
1983
+ )}
1984
+ </div>
1985
+ <div
1986
+ className={classNames(
1987
+ 'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
1988
+ openSections.errors ? 'rotate-180' : '',
1989
+ )}
1990
+ />
1991
+ </div>
1992
+ </CollapsibleTrigger>
1993
+
1994
+ <CollapsibleContent>
1995
+ <div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
1996
+ <ScrollArea className="h-[300px]">
1997
+ <div className="space-y-4">
1998
+ <div className="text-sm text-bolt-elements-textSecondary">
1999
+ Checks for:
2000
+ <ul className="list-disc list-inside mt-2 space-y-1">
2001
+ <li>Unhandled JavaScript errors</li>
2002
+ <li>Unhandled Promise rejections</li>
2003
+ <li>Runtime exceptions</li>
2004
+ <li>Network errors</li>
2005
+ </ul>
2006
+ </div>
2007
+ <div className="text-sm">
2008
+ <span className="text-bolt-elements-textSecondary">Status: </span>
2009
+ <span className="text-bolt-elements-textPrimary">
2010
+ {loading.errors
2011
+ ? 'Checking...'
2012
+ : errorLogs.length > 0
2013
+ ? `${errorLogs.length} errors found`
2014
+ : 'No errors found'}
2015
+ </span>
2016
+ </div>
2017
+ {errorLogs.length > 0 && (
2018
+ <div className="mt-4">
2019
+ <div className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Recent Errors:</div>
2020
+ <div className="space-y-2">
2021
+ {errorLogs.map((error) => (
2022
+ <div key={error.id} className="text-sm text-red-500 dark:text-red-400 p-2 rounded bg-red-500/5">
2023
+ <div className="font-medium">{error.message}</div>
2024
+ {error.source && (
2025
+ <div className="text-xs mt-1 text-red-400">
2026
+ Source: {error.source}
2027
+ {error.details?.lineNumber && `:${error.details.lineNumber}`}
2028
+ </div>
2029
+ )}
2030
+ {error.stack && (
2031
+ <div className="text-xs mt-1 text-red-400 font-mono whitespace-pre-wrap">{error.stack}</div>
2032
+ )}
2033
+ </div>
2034
+ ))}
2035
+ </div>
2036
+ </div>
2037
+ )}
2038
+ </div>
2039
+ </ScrollArea>
2040
+ </div>
2041
+ </CollapsibleContent>
2042
+ </Collapsible>
2043
+ </div>
2044
+ );
2045
+ }
DraggableTabList.tsx ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useDrag, useDrop } from 'react-dnd';
2
+ import { motion } from 'framer-motion';
3
+ import { classNames } from '~/utils/classNames';
4
+ import type { TabVisibilityConfig } from '~/components/@settings/core/types';
5
+ import { TAB_LABELS } from '~/components/@settings/core/types';
6
+ import { Switch } from '~/components/ui/Switch';
7
+
8
+ interface DraggableTabListProps {
9
+ tabs: TabVisibilityConfig[];
10
+ onReorder: (tabs: TabVisibilityConfig[]) => void;
11
+ onWindowChange?: (tab: TabVisibilityConfig, window: 'user' | 'developer') => void;
12
+ onVisibilityChange?: (tab: TabVisibilityConfig, visible: boolean) => void;
13
+ showControls?: boolean;
14
+ }
15
+
16
+ interface DraggableTabItemProps {
17
+ tab: TabVisibilityConfig;
18
+ index: number;
19
+ moveTab: (dragIndex: number, hoverIndex: number) => void;
20
+ showControls?: boolean;
21
+ onWindowChange?: (tab: TabVisibilityConfig, window: 'user' | 'developer') => void;
22
+ onVisibilityChange?: (tab: TabVisibilityConfig, visible: boolean) => void;
23
+ }
24
+
25
+ interface DragItem {
26
+ type: string;
27
+ index: number;
28
+ id: string;
29
+ }
30
+
31
+ const DraggableTabItem = ({
32
+ tab,
33
+ index,
34
+ moveTab,
35
+ showControls,
36
+ onWindowChange,
37
+ onVisibilityChange,
38
+ }: DraggableTabItemProps) => {
39
+ const [{ isDragging }, dragRef] = useDrag({
40
+ type: 'tab',
41
+ item: { type: 'tab', index, id: tab.id },
42
+ collect: (monitor) => ({
43
+ isDragging: monitor.isDragging(),
44
+ }),
45
+ });
46
+
47
+ const [, dropRef] = useDrop({
48
+ accept: 'tab',
49
+ hover: (item: DragItem, monitor) => {
50
+ if (!monitor.isOver({ shallow: true })) {
51
+ return;
52
+ }
53
+
54
+ if (item.index === index) {
55
+ return;
56
+ }
57
+
58
+ if (item.id === tab.id) {
59
+ return;
60
+ }
61
+
62
+ moveTab(item.index, index);
63
+ item.index = index;
64
+ },
65
+ });
66
+
67
+ const ref = (node: HTMLDivElement | null) => {
68
+ dragRef(node);
69
+ dropRef(node);
70
+ };
71
+
72
+ return (
73
+ <motion.div
74
+ ref={ref}
75
+ initial={false}
76
+ animate={{
77
+ scale: isDragging ? 1.02 : 1,
78
+ boxShadow: isDragging ? '0 8px 16px rgba(0,0,0,0.1)' : 'none',
79
+ }}
80
+ className={classNames(
81
+ 'flex items-center justify-between p-4 rounded-lg',
82
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
83
+ 'border border-[#E5E5E5] dark:border-[#333333]',
84
+ isDragging ? 'z-50' : '',
85
+ )}
86
+ >
87
+ <div className="flex items-center gap-4">
88
+ <div className="cursor-grab">
89
+ <div className="i-ph:dots-six-vertical w-4 h-4 text-bolt-elements-textSecondary" />
90
+ </div>
91
+ <div>
92
+ <div className="font-medium text-bolt-elements-textPrimary">{TAB_LABELS[tab.id]}</div>
93
+ {showControls && (
94
+ <div className="text-xs text-bolt-elements-textSecondary">
95
+ Order: {tab.order}, Window: {tab.window}
96
+ </div>
97
+ )}
98
+ </div>
99
+ </div>
100
+ {showControls && !tab.locked && (
101
+ <div className="flex items-center gap-4">
102
+ <div className="flex items-center gap-2">
103
+ <Switch
104
+ checked={tab.visible}
105
+ onCheckedChange={(checked: boolean) => onVisibilityChange?.(tab, checked)}
106
+ className="data-[state=checked]:bg-purple-500"
107
+ aria-label={`Toggle ${TAB_LABELS[tab.id]} visibility`}
108
+ />
109
+ <label className="text-sm text-bolt-elements-textSecondary">Visible</label>
110
+ </div>
111
+ <div className="flex items-center gap-2">
112
+ <label className="text-sm text-bolt-elements-textSecondary">User</label>
113
+ <Switch
114
+ checked={tab.window === 'developer'}
115
+ onCheckedChange={(checked: boolean) => onWindowChange?.(tab, checked ? 'developer' : 'user')}
116
+ className="data-[state=checked]:bg-purple-500"
117
+ aria-label={`Toggle ${TAB_LABELS[tab.id]} window assignment`}
118
+ />
119
+ <label className="text-sm text-bolt-elements-textSecondary">Dev</label>
120
+ </div>
121
+ </div>
122
+ )}
123
+ </motion.div>
124
+ );
125
+ };
126
+
127
+ export const DraggableTabList = ({
128
+ tabs,
129
+ onReorder,
130
+ onWindowChange,
131
+ onVisibilityChange,
132
+ showControls = false,
133
+ }: DraggableTabListProps) => {
134
+ const moveTab = (dragIndex: number, hoverIndex: number) => {
135
+ const items = Array.from(tabs);
136
+ const [reorderedItem] = items.splice(dragIndex, 1);
137
+ items.splice(hoverIndex, 0, reorderedItem);
138
+
139
+ // Update order numbers based on position
140
+ const reorderedTabs = items.map((tab, index) => ({
141
+ ...tab,
142
+ order: index + 1,
143
+ }));
144
+
145
+ onReorder(reorderedTabs);
146
+ };
147
+
148
+ return (
149
+ <div className="space-y-2">
150
+ {tabs.map((tab, index) => (
151
+ <DraggableTabItem
152
+ key={tab.id}
153
+ tab={tab}
154
+ index={index}
155
+ moveTab={moveTab}
156
+ showControls={showControls}
157
+ onWindowChange={onWindowChange}
158
+ onVisibilityChange={onVisibilityChange}
159
+ />
160
+ ))}
161
+ </div>
162
+ );
163
+ };
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-json',
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
+ }
ExamplePrompts.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ const EXAMPLE_PROMPTS = [
4
+ { text: 'Build a todo app in React using Tailwind' },
5
+ { text: 'Build a simple blog using Astro' },
6
+ { text: 'Create a cookie consent form using Material UI' },
7
+ { text: 'Make a space invaders game' },
8
+ { text: 'Make a Tic Tac Toe game in html, css and js only' },
9
+ ];
10
+
11
+ export function ExamplePrompts(sendMessage?: { (event: React.UIEvent, messageInput?: string): void | undefined }) {
12
+ return (
13
+ <div id="examples" className="relative flex flex-col gap-9 w-full max-w-3xl mx-auto flex justify-center mt-6">
14
+ <div
15
+ className="flex flex-wrap justify-center gap-2"
16
+ style={{
17
+ animation: '.25s ease-out 0s 1 _fade-and-move-in_g2ptj_1 forwards',
18
+ }}
19
+ >
20
+ {EXAMPLE_PROMPTS.map((examplePrompt, index: number) => {
21
+ return (
22
+ <button
23
+ key={index}
24
+ onClick={(event) => {
25
+ sendMessage?.(event, examplePrompt.text);
26
+ }}
27
+ className="border border-bolt-elements-borderColor rounded-full bg-gray-50 hover:bg-gray-100 dark:bg-gray-950 dark:hover:bg-gray-900 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary px-3 py-1 text-xs transition-theme"
28
+ >
29
+ {examplePrompt.text}
30
+ </button>
31
+ );
32
+ })}
33
+ </div>
34
+ </div>
35
+ );
36
+ }
ExportChatButton.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import WithTooltip from '~/components/ui/Tooltip';
2
+ import { IconButton } from '~/components/ui/IconButton';
3
+ import React from 'react';
4
+
5
+ export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => {
6
+ return (
7
+ <WithTooltip tooltip="Export Chat">
8
+ <IconButton title="Export Chat" onClick={() => exportChat?.()}>
9
+ <div className="i-ph:download-simple text-xl"></div>
10
+ </IconButton>
11
+ </WithTooltip>
12
+ );
13
+ };
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
+ }
FilePreview.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface FilePreviewProps {
4
+ files: File[];
5
+ imageDataList: string[];
6
+ onRemove: (index: number) => void;
7
+ }
8
+
9
+ const FilePreview: React.FC<FilePreviewProps> = ({ files, imageDataList, onRemove }) => {
10
+ if (!files || files.length === 0) {
11
+ return null;
12
+ }
13
+
14
+ return (
15
+ <div className="flex flex-row overflow-x-auto -mt-2">
16
+ {files.map((file, index) => (
17
+ <div key={file.name + file.size} className="mr-2 relative">
18
+ {imageDataList[index] && (
19
+ <div className="relative pt-4 pr-4">
20
+ <img src={imageDataList[index]} alt={file.name} className="max-h-20" />
21
+ <button
22
+ onClick={() => onRemove(index)}
23
+ className="absolute top-1 right-1 z-10 bg-black rounded-full w-5 h-5 shadow-md hover:bg-gray-900 transition-colors flex items-center justify-center"
24
+ >
25
+ <div className="i-ph:x w-3 h-3 text-gray-200" />
26
+ </button>
27
+ </div>
28
+ )}
29
+ </div>
30
+ ))}
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export default FilePreview;
GitCloneButton.tsx ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ignore from 'ignore';
2
+ import { useGit } from '~/lib/hooks/useGit';
3
+ import type { Message } from 'ai';
4
+ import { detectProjectCommands, createCommandsMessage, escapeBoltTags } from '~/utils/projectCommands';
5
+ import { generateId } from '~/utils/fileUtils';
6
+ import { useState } from 'react';
7
+ import { toast } from 'react-toastify';
8
+ import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
9
+ import { RepositorySelectionDialog } from '~/components/@settings/tabs/connections/components/RepositorySelectionDialog';
10
+ import { classNames } from '~/utils/classNames';
11
+ import { Button } from '~/components/ui/Button';
12
+ import type { IChatMetadata } from '~/lib/persistence/db';
13
+
14
+ const IGNORE_PATTERNS = [
15
+ 'node_modules/**',
16
+ '.git/**',
17
+ '.github/**',
18
+ '.vscode/**',
19
+ 'dist/**',
20
+ 'build/**',
21
+ '.next/**',
22
+ 'coverage/**',
23
+ '.cache/**',
24
+ '.idea/**',
25
+ '**/*.log',
26
+ '**/.DS_Store',
27
+ '**/npm-debug.log*',
28
+ '**/yarn-debug.log*',
29
+ '**/yarn-error.log*',
30
+ '**/*lock.json',
31
+ '**/*lock.yaml',
32
+ ];
33
+
34
+ const ig = ignore().add(IGNORE_PATTERNS);
35
+
36
+ const MAX_FILE_SIZE = 100 * 1024; // 100KB limit per file
37
+ const MAX_TOTAL_SIZE = 500 * 1024; // 500KB total limit
38
+
39
+ interface GitCloneButtonProps {
40
+ className?: string;
41
+ importChat?: (description: string, messages: Message[], metadata?: IChatMetadata) => Promise<void>;
42
+ }
43
+
44
+ export default function GitCloneButton({ importChat, className }: GitCloneButtonProps) {
45
+ const { ready, gitClone } = useGit();
46
+ const [loading, setLoading] = useState(false);
47
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
48
+
49
+ const handleClone = async (repoUrl: string) => {
50
+ if (!ready) {
51
+ return;
52
+ }
53
+
54
+ setLoading(true);
55
+
56
+ try {
57
+ const { workdir, data } = await gitClone(repoUrl);
58
+
59
+ if (importChat) {
60
+ const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
61
+ const textDecoder = new TextDecoder('utf-8');
62
+
63
+ let totalSize = 0;
64
+ const skippedFiles: string[] = [];
65
+ const fileContents = [];
66
+
67
+ for (const filePath of filePaths) {
68
+ const { data: content, encoding } = data[filePath];
69
+
70
+ // Skip binary files
71
+ if (
72
+ content instanceof Uint8Array &&
73
+ !filePath.match(/\.(txt|md|astro|mjs|js|jsx|ts|tsx|json|html|css|scss|less|yml|yaml|xml|svg)$/i)
74
+ ) {
75
+ skippedFiles.push(filePath);
76
+ continue;
77
+ }
78
+
79
+ try {
80
+ const textContent =
81
+ encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '';
82
+
83
+ if (!textContent) {
84
+ continue;
85
+ }
86
+
87
+ // Check file size
88
+ const fileSize = new TextEncoder().encode(textContent).length;
89
+
90
+ if (fileSize > MAX_FILE_SIZE) {
91
+ skippedFiles.push(`${filePath} (too large: ${Math.round(fileSize / 1024)}KB)`);
92
+ continue;
93
+ }
94
+
95
+ // Check total size
96
+ if (totalSize + fileSize > MAX_TOTAL_SIZE) {
97
+ skippedFiles.push(`${filePath} (would exceed total size limit)`);
98
+ continue;
99
+ }
100
+
101
+ totalSize += fileSize;
102
+ fileContents.push({
103
+ path: filePath,
104
+ content: textContent,
105
+ });
106
+ } catch (e: any) {
107
+ skippedFiles.push(`${filePath} (error: ${e.message})`);
108
+ }
109
+ }
110
+
111
+ const commands = await detectProjectCommands(fileContents);
112
+ const commandsMessage = createCommandsMessage(commands);
113
+
114
+ const filesMessage: Message = {
115
+ role: 'assistant',
116
+ content: `Cloning the repo ${repoUrl} into ${workdir}
117
+ ${
118
+ skippedFiles.length > 0
119
+ ? `\nSkipped files (${skippedFiles.length}):
120
+ ${skippedFiles.map((f) => `- ${f}`).join('\n')}`
121
+ : ''
122
+ }
123
+
124
+ <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
125
+ ${fileContents
126
+ .map(
127
+ (file) =>
128
+ `<boltAction type="file" filePath="${file.path}">
129
+ ${escapeBoltTags(file.content)}
130
+ </boltAction>`,
131
+ )
132
+ .join('\n')}
133
+ </boltArtifact>`,
134
+ id: generateId(),
135
+ createdAt: new Date(),
136
+ };
137
+
138
+ const messages = [filesMessage];
139
+
140
+ if (commandsMessage) {
141
+ messages.push(commandsMessage);
142
+ }
143
+
144
+ await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
145
+ }
146
+ } catch (error) {
147
+ console.error('Error during import:', error);
148
+ toast.error('Failed to import repository');
149
+ } finally {
150
+ setLoading(false);
151
+ }
152
+ };
153
+
154
+ return (
155
+ <>
156
+ <Button
157
+ onClick={() => setIsDialogOpen(true)}
158
+ title="Clone a Git Repo"
159
+ variant="outline"
160
+ size="lg"
161
+ className={classNames(
162
+ 'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
163
+ 'text-bolt-elements-textPrimary dark:text-white',
164
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
165
+ 'border-[#E5E5E5] dark:border-[#333333]',
166
+ 'h-10 px-4 py-2 min-w-[120px] justify-center',
167
+ 'transition-all duration-200 ease-in-out',
168
+ className,
169
+ )}
170
+ disabled={!ready || loading}
171
+ >
172
+ <span className="i-ph:git-branch w-4 h-4" />
173
+ Clone a Git Repo
174
+ </Button>
175
+
176
+ <RepositorySelectionDialog isOpen={isDialogOpen} onClose={() => setIsDialogOpen(false)} onSelect={handleClone} />
177
+
178
+ {loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
179
+ </>
180
+ );
181
+ }
GitHub.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface GitHubUserResponse {
2
+ login: string;
3
+ avatar_url: string;
4
+ html_url: string;
5
+ name: string;
6
+ bio: string;
7
+ public_repos: number;
8
+ followers: number;
9
+ following: number;
10
+ public_gists: number;
11
+ created_at: string;
12
+ updated_at: string;
13
+ }
14
+
15
+ export interface GitHubRepoInfo {
16
+ name: string;
17
+ full_name: string;
18
+ html_url: string;
19
+ description: string;
20
+ stargazers_count: number;
21
+ forks_count: number;
22
+ default_branch: string;
23
+ updated_at: string;
24
+ language: string;
25
+ languages_url: string;
26
+ }
27
+
28
+ export interface GitHubOrganization {
29
+ login: string;
30
+ avatar_url: string;
31
+ description: string;
32
+ html_url: string;
33
+ }
34
+
35
+ export interface GitHubEvent {
36
+ id: string;
37
+ type: string;
38
+ created_at: string;
39
+ repo: {
40
+ name: string;
41
+ url: string;
42
+ };
43
+ payload: {
44
+ action?: string;
45
+ ref?: string;
46
+ ref_type?: string;
47
+ description?: string;
48
+ };
49
+ }
50
+
51
+ export interface GitHubLanguageStats {
52
+ [key: string]: number;
53
+ }
54
+
55
+ export interface GitHubStats {
56
+ repos: GitHubRepoInfo[];
57
+ totalStars: number;
58
+ totalForks: number;
59
+ organizations: GitHubOrganization[];
60
+ recentActivity: GitHubEvent[];
61
+ languages: GitHubLanguageStats;
62
+ totalGists: number;
63
+ }
64
+
65
+ export interface GitHubConnection {
66
+ user: GitHubUserResponse | null;
67
+ token: string;
68
+ tokenType: 'classic' | 'fine-grained';
69
+ stats?: GitHubStats;
70
+ }
71
+
72
+ export interface GitHubTokenInfo {
73
+ token: string;
74
+ scope: string[];
75
+ avatar_url: string;
76
+ name: string | null;
77
+ created_at: string;
78
+ followers: number;
79
+ }
80
+
81
+ export interface GitHubRateLimits {
82
+ limit: number;
83
+ remaining: number;
84
+ reset: Date;
85
+ used: number;
86
+ }
87
+
88
+ export interface GitHubAuthState {
89
+ username: string;
90
+ tokenInfo: GitHubTokenInfo | null;
91
+ isConnected: boolean;
92
+ isVerifying: boolean;
93
+ isLoadingRepos: boolean;
94
+ rateLimits?: GitHubRateLimits;
95
+ }
GithubConnection.tsx ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { toast } from 'react-toastify';
4
+ import { logStore } from '~/lib/stores/logs';
5
+ import { classNames } from '~/utils/classNames';
6
+
7
+ interface GitHubUserResponse {
8
+ login: string;
9
+ avatar_url: string;
10
+ html_url: string;
11
+ name: string;
12
+ bio: string;
13
+ public_repos: number;
14
+ followers: number;
15
+ following: number;
16
+ created_at: string;
17
+ public_gists: number;
18
+ }
19
+
20
+ interface GitHubRepoInfo {
21
+ name: string;
22
+ full_name: string;
23
+ html_url: string;
24
+ description: string;
25
+ stargazers_count: number;
26
+ forks_count: number;
27
+ default_branch: string;
28
+ updated_at: string;
29
+ languages_url: string;
30
+ }
31
+
32
+ interface GitHubOrganization {
33
+ login: string;
34
+ avatar_url: string;
35
+ html_url: string;
36
+ }
37
+
38
+ interface GitHubEvent {
39
+ id: string;
40
+ type: string;
41
+ repo: {
42
+ name: string;
43
+ };
44
+ created_at: string;
45
+ }
46
+
47
+ interface GitHubLanguageStats {
48
+ [language: string]: number;
49
+ }
50
+
51
+ interface GitHubStats {
52
+ repos: GitHubRepoInfo[];
53
+ totalStars: number;
54
+ totalForks: number;
55
+ organizations: GitHubOrganization[];
56
+ recentActivity: GitHubEvent[];
57
+ languages: GitHubLanguageStats;
58
+ totalGists: number;
59
+ }
60
+
61
+ interface GitHubConnection {
62
+ user: GitHubUserResponse | null;
63
+ token: string;
64
+ tokenType: 'classic' | 'fine-grained';
65
+ stats?: GitHubStats;
66
+ }
67
+
68
+ export function GithubConnection() {
69
+ const [connection, setConnection] = useState<GitHubConnection>({
70
+ user: null,
71
+ token: '',
72
+ tokenType: 'classic',
73
+ });
74
+ const [isLoading, setIsLoading] = useState(true);
75
+ const [isConnecting, setIsConnecting] = useState(false);
76
+ const [isFetchingStats, setIsFetchingStats] = useState(false);
77
+ const [isStatsExpanded, setIsStatsExpanded] = useState(false);
78
+
79
+ const fetchGitHubStats = async (token: string) => {
80
+ try {
81
+ setIsFetchingStats(true);
82
+
83
+ const reposResponse = await fetch(
84
+ 'https://api.github.com/user/repos?sort=updated&per_page=10&affiliation=owner,organization_member,collaborator',
85
+ {
86
+ headers: {
87
+ Authorization: `Bearer ${token}`,
88
+ },
89
+ },
90
+ );
91
+
92
+ if (!reposResponse.ok) {
93
+ throw new Error('Failed to fetch repositories');
94
+ }
95
+
96
+ const repos = (await reposResponse.json()) as GitHubRepoInfo[];
97
+
98
+ const orgsResponse = await fetch('https://api.github.com/user/orgs', {
99
+ headers: {
100
+ Authorization: `Bearer ${token}`,
101
+ },
102
+ });
103
+
104
+ if (!orgsResponse.ok) {
105
+ throw new Error('Failed to fetch organizations');
106
+ }
107
+
108
+ const organizations = (await orgsResponse.json()) as GitHubOrganization[];
109
+
110
+ const eventsResponse = await fetch('https://api.github.com/users/' + connection.user?.login + '/events/public', {
111
+ headers: {
112
+ Authorization: `Bearer ${token}`,
113
+ },
114
+ });
115
+
116
+ if (!eventsResponse.ok) {
117
+ throw new Error('Failed to fetch events');
118
+ }
119
+
120
+ const recentActivity = ((await eventsResponse.json()) as GitHubEvent[]).slice(0, 5);
121
+
122
+ const languagePromises = repos.map((repo) =>
123
+ fetch(repo.languages_url, {
124
+ headers: {
125
+ Authorization: `Bearer ${token}`,
126
+ },
127
+ }).then((res) => res.json() as Promise<Record<string, number>>),
128
+ );
129
+
130
+ const repoLanguages = await Promise.all(languagePromises);
131
+ const languages: GitHubLanguageStats = {};
132
+
133
+ repoLanguages.forEach((repoLang) => {
134
+ Object.entries(repoLang).forEach(([lang, bytes]) => {
135
+ languages[lang] = (languages[lang] || 0) + bytes;
136
+ });
137
+ });
138
+
139
+ const totalStars = repos.reduce((acc, repo) => acc + repo.stargazers_count, 0);
140
+ const totalForks = repos.reduce((acc, repo) => acc + repo.forks_count, 0);
141
+ const totalGists = connection.user?.public_gists || 0;
142
+
143
+ setConnection((prev) => ({
144
+ ...prev,
145
+ stats: {
146
+ repos,
147
+ totalStars,
148
+ totalForks,
149
+ organizations,
150
+ recentActivity,
151
+ languages,
152
+ totalGists,
153
+ },
154
+ }));
155
+ } catch (error) {
156
+ logStore.logError('Failed to fetch GitHub stats', { error });
157
+ toast.error('Failed to fetch GitHub statistics');
158
+ } finally {
159
+ setIsFetchingStats(false);
160
+ }
161
+ };
162
+
163
+ useEffect(() => {
164
+ const savedConnection = localStorage.getItem('github_connection');
165
+
166
+ if (savedConnection) {
167
+ const parsed = JSON.parse(savedConnection);
168
+
169
+ if (!parsed.tokenType) {
170
+ parsed.tokenType = 'classic';
171
+ }
172
+
173
+ setConnection(parsed);
174
+
175
+ if (parsed.user && parsed.token) {
176
+ fetchGitHubStats(parsed.token);
177
+ }
178
+ }
179
+
180
+ setIsLoading(false);
181
+ }, []);
182
+
183
+ if (isLoading || isConnecting || isFetchingStats) {
184
+ return <LoadingSpinner />;
185
+ }
186
+
187
+ const fetchGithubUser = async (token: string) => {
188
+ try {
189
+ setIsConnecting(true);
190
+
191
+ const response = await fetch('https://api.github.com/user', {
192
+ headers: {
193
+ Authorization: `Bearer ${token}`,
194
+ },
195
+ });
196
+
197
+ if (!response.ok) {
198
+ throw new Error('Invalid token or unauthorized');
199
+ }
200
+
201
+ const data = (await response.json()) as GitHubUserResponse;
202
+ const newConnection: GitHubConnection = {
203
+ user: data,
204
+ token,
205
+ tokenType: connection.tokenType,
206
+ };
207
+
208
+ localStorage.setItem('github_connection', JSON.stringify(newConnection));
209
+ setConnection(newConnection);
210
+
211
+ await fetchGitHubStats(token);
212
+
213
+ toast.success('Successfully connected to GitHub');
214
+ } catch (error) {
215
+ logStore.logError('Failed to authenticate with GitHub', { error });
216
+ toast.error('Failed to connect to GitHub');
217
+ setConnection({ user: null, token: '', tokenType: 'classic' });
218
+ } finally {
219
+ setIsConnecting(false);
220
+ }
221
+ };
222
+
223
+ const handleConnect = async (event: React.FormEvent) => {
224
+ event.preventDefault();
225
+ await fetchGithubUser(connection.token);
226
+ };
227
+
228
+ const handleDisconnect = () => {
229
+ localStorage.removeItem('github_connection');
230
+ setConnection({ user: null, token: '', tokenType: 'classic' });
231
+ toast.success('Disconnected from GitHub');
232
+ };
233
+
234
+ return (
235
+ <motion.div
236
+ className="bg-[#FFFFFF] dark:bg-[#0A0A0A] rounded-lg border border-[#E5E5E5] dark:border-[#1A1A1A]"
237
+ initial={{ opacity: 0, y: 20 }}
238
+ animate={{ opacity: 1, y: 0 }}
239
+ transition={{ delay: 0.2 }}
240
+ >
241
+ <div className="p-6 space-y-6">
242
+ <div className="flex items-center gap-2">
243
+ <div className="i-ph:github-logo w-5 h-5 text-bolt-elements-textPrimary" />
244
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">GitHub Connection</h3>
245
+ </div>
246
+
247
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
248
+ <div>
249
+ <label className="block text-sm text-bolt-elements-textSecondary mb-2">Token Type</label>
250
+ <select
251
+ value={connection.tokenType}
252
+ onChange={(e) =>
253
+ setConnection((prev) => ({ ...prev, tokenType: e.target.value as 'classic' | 'fine-grained' }))
254
+ }
255
+ disabled={isConnecting || !!connection.user}
256
+ className={classNames(
257
+ 'w-full px-3 py-2 rounded-lg text-sm',
258
+ 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
259
+ 'border border-[#E5E5E5] dark:border-[#333333]',
260
+ 'text-bolt-elements-textPrimary',
261
+ 'focus:outline-none focus:ring-1 focus:ring-purple-500',
262
+ 'disabled:opacity-50',
263
+ )}
264
+ >
265
+ <option value="classic">Personal Access Token (Classic)</option>
266
+ <option value="fine-grained">Fine-grained Token</option>
267
+ </select>
268
+ </div>
269
+
270
+ <div>
271
+ <label className="block text-sm text-bolt-elements-textSecondary mb-2">
272
+ {connection.tokenType === 'classic' ? 'Personal Access Token' : 'Fine-grained Token'}
273
+ </label>
274
+ <input
275
+ type="password"
276
+ value={connection.token}
277
+ onChange={(e) => setConnection((prev) => ({ ...prev, token: e.target.value }))}
278
+ disabled={isConnecting || !!connection.user}
279
+ placeholder={`Enter your GitHub ${
280
+ connection.tokenType === 'classic' ? 'personal access token' : 'fine-grained token'
281
+ }`}
282
+ className={classNames(
283
+ 'w-full px-3 py-2 rounded-lg text-sm',
284
+ 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
285
+ 'border border-[#E5E5E5] dark:border-[#333333]',
286
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
287
+ 'focus:outline-none focus:ring-1 focus:ring-purple-500',
288
+ 'disabled:opacity-50',
289
+ )}
290
+ />
291
+ <div className="mt-2 text-sm text-bolt-elements-textSecondary">
292
+ <a
293
+ href={`https://github.com/settings/tokens${connection.tokenType === 'fine-grained' ? '/beta' : '/new'}`}
294
+ target="_blank"
295
+ rel="noopener noreferrer"
296
+ className="text-purple-500 hover:underline inline-flex items-center gap-1"
297
+ >
298
+ Get your token
299
+ <div className="i-ph:arrow-square-out w-10 h-5" />
300
+ </a>
301
+ <span className="mx-2">•</span>
302
+ <span>
303
+ Required scopes:{' '}
304
+ {connection.tokenType === 'classic'
305
+ ? 'repo, read:org, read:user'
306
+ : 'Repository access, Organization access'}
307
+ </span>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ <div className="flex items-center gap-3">
313
+ {!connection.user ? (
314
+ <button
315
+ onClick={handleConnect}
316
+ disabled={isConnecting || !connection.token}
317
+ className={classNames(
318
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
319
+ 'bg-purple-500 text-white',
320
+ 'hover:bg-purple-600',
321
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
322
+ )}
323
+ >
324
+ {isConnecting ? (
325
+ <>
326
+ <div className="i-ph:spinner-gap animate-spin" />
327
+ Connecting...
328
+ </>
329
+ ) : (
330
+ <>
331
+ <div className="i-ph:plug-charging w-4 h-4" />
332
+ Connect
333
+ </>
334
+ )}
335
+ </button>
336
+ ) : (
337
+ <button
338
+ onClick={handleDisconnect}
339
+ className={classNames(
340
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
341
+ 'bg-red-500 text-white',
342
+ 'hover:bg-red-600',
343
+ )}
344
+ >
345
+ <div className="i-ph:plug-x w-4 h-4" />
346
+ Disconnect
347
+ </button>
348
+ )}
349
+
350
+ {connection.user && (
351
+ <span className="text-sm text-bolt-elements-textSecondary flex items-center gap-1">
352
+ <div className="i-ph:check-circle w-4 h-4" />
353
+ Connected to GitHub
354
+ </span>
355
+ )}
356
+ </div>
357
+
358
+ {connection.user && connection.stats && (
359
+ <div className="mt-6 border-t border-[#E5E5E5] dark:border-[#1A1A1A] pt-6">
360
+ <button onClick={() => setIsStatsExpanded(!isStatsExpanded)} className="w-full bg-transparent">
361
+ <div className="flex items-center gap-4">
362
+ <img src={connection.user.avatar_url} alt={connection.user.login} className="w-16 h-16 rounded-full" />
363
+ <div className="flex-1">
364
+ <div className="flex items-center justify-between">
365
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">
366
+ {connection.user.name || connection.user.login}
367
+ </h3>
368
+ <div
369
+ className={classNames(
370
+ 'i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary transition-transform',
371
+ isStatsExpanded ? 'rotate-180' : '',
372
+ )}
373
+ />
374
+ </div>
375
+ {connection.user.bio && (
376
+ <p className="text-sm text-start text-bolt-elements-textSecondary">{connection.user.bio}</p>
377
+ )}
378
+ <div className="flex gap-4 mt-2 text-sm text-bolt-elements-textSecondary">
379
+ <span className="flex items-center gap-1">
380
+ <div className="i-ph:users w-4 h-4" />
381
+ {connection.user.followers} followers
382
+ </span>
383
+ <span className="flex items-center gap-1">
384
+ <div className="i-ph:book-bookmark w-4 h-4" />
385
+ {connection.user.public_repos} public repos
386
+ </span>
387
+ <span className="flex items-center gap-1">
388
+ <div className="i-ph:star w-4 h-4" />
389
+ {connection.stats.totalStars} stars
390
+ </span>
391
+ <span className="flex items-center gap-1">
392
+ <div className="i-ph:git-fork w-4 h-4" />
393
+ {connection.stats.totalForks} forks
394
+ </span>
395
+ </div>
396
+ </div>
397
+ </div>
398
+ </button>
399
+
400
+ {isStatsExpanded && (
401
+ <div className="pt-4">
402
+ {connection.stats.organizations.length > 0 && (
403
+ <div className="mb-6">
404
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Organizations</h4>
405
+ <div className="flex flex-wrap gap-3">
406
+ {connection.stats.organizations.map((org) => (
407
+ <a
408
+ key={org.login}
409
+ href={org.html_url}
410
+ target="_blank"
411
+ rel="noopener noreferrer"
412
+ className="flex items-center gap-2 p-2 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] hover:bg-[#F0F0F0] dark:hover:bg-[#252525] transition-colors"
413
+ >
414
+ <img src={org.avatar_url} alt={org.login} className="w-6 h-6 rounded-md" />
415
+ <span className="text-sm text-bolt-elements-textPrimary">{org.login}</span>
416
+ </a>
417
+ ))}
418
+ </div>
419
+ </div>
420
+ )}
421
+
422
+ {/* Languages Section */}
423
+ <div className="mb-6">
424
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Top Languages</h4>
425
+ <div className="flex flex-wrap gap-2">
426
+ {Object.entries(connection.stats.languages)
427
+ .sort(([, a], [, b]) => b - a)
428
+ .slice(0, 5)
429
+ .map(([language]) => (
430
+ <span
431
+ key={language}
432
+ className="px-3 py-1 text-xs rounded-full bg-purple-500/10 text-purple-500 dark:bg-purple-500/20"
433
+ >
434
+ {language}
435
+ </span>
436
+ ))}
437
+ </div>
438
+ </div>
439
+
440
+ {/* Recent Activity Section */}
441
+ <div className="mb-6">
442
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Recent Activity</h4>
443
+ <div className="space-y-3">
444
+ {connection.stats.recentActivity.map((event) => (
445
+ <div key={event.id} className="p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] text-sm">
446
+ <div className="flex items-center gap-2 text-bolt-elements-textPrimary">
447
+ <div className="i-ph:git-commit w-4 h-4 text-bolt-elements-textSecondary" />
448
+ <span className="font-medium">{event.type.replace('Event', '')}</span>
449
+ <span>on</span>
450
+ <a
451
+ href={`https://github.com/${event.repo.name}`}
452
+ target="_blank"
453
+ rel="noopener noreferrer"
454
+ className="text-purple-500 hover:underline"
455
+ >
456
+ {event.repo.name}
457
+ </a>
458
+ </div>
459
+ <div className="mt-1 text-xs text-bolt-elements-textSecondary">
460
+ {new Date(event.created_at).toLocaleDateString()} at{' '}
461
+ {new Date(event.created_at).toLocaleTimeString()}
462
+ </div>
463
+ </div>
464
+ ))}
465
+ </div>
466
+ </div>
467
+
468
+ {/* Additional Stats */}
469
+ <div className="grid grid-cols-4 gap-4 mb-6">
470
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
471
+ <div className="text-sm text-bolt-elements-textSecondary">Member Since</div>
472
+ <div className="text-lg font-medium text-bolt-elements-textPrimary">
473
+ {new Date(connection.user.created_at).toLocaleDateString()}
474
+ </div>
475
+ </div>
476
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
477
+ <div className="text-sm text-bolt-elements-textSecondary">Public Gists</div>
478
+ <div className="text-lg font-medium text-bolt-elements-textPrimary">
479
+ {connection.stats.totalGists}
480
+ </div>
481
+ </div>
482
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
483
+ <div className="text-sm text-bolt-elements-textSecondary">Organizations</div>
484
+ <div className="text-lg font-medium text-bolt-elements-textPrimary">
485
+ {connection.stats.organizations.length}
486
+ </div>
487
+ </div>
488
+ <div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
489
+ <div className="text-sm text-bolt-elements-textSecondary">Languages</div>
490
+ <div className="text-lg font-medium text-bolt-elements-textPrimary">
491
+ {Object.keys(connection.stats.languages).length}
492
+ </div>
493
+ </div>
494
+ </div>
495
+
496
+ {/* Repositories Section */}
497
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Recent Repositories</h4>
498
+ <div className="space-y-3">
499
+ {connection.stats.repos.map((repo) => (
500
+ <a
501
+ key={repo.full_name}
502
+ href={repo.html_url}
503
+ target="_blank"
504
+ rel="noopener noreferrer"
505
+ className="block p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] hover:bg-[#F0F0F0] dark:hover:bg-[#252525] transition-colors"
506
+ >
507
+ <div className="flex items-center justify-between">
508
+ <div>
509
+ <h5 className="text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2">
510
+ <div className="i-ph:git-repository w-4 h-4 text-bolt-elements-textSecondary" />
511
+ {repo.name}
512
+ </h5>
513
+ {repo.description && (
514
+ <p className="text-xs text-bolt-elements-textSecondary mt-1">{repo.description}</p>
515
+ )}
516
+ <div className="flex items-center gap-2 mt-2 text-xs text-bolt-elements-textSecondary">
517
+ <span className="flex items-center gap-1">
518
+ <div className="i-ph:git-branch w-3 h-3" />
519
+ {repo.default_branch}
520
+ </span>
521
+ <span>•</span>
522
+ <span>Updated {new Date(repo.updated_at).toLocaleDateString()}</span>
523
+ </div>
524
+ </div>
525
+ <div className="flex items-center gap-3 text-xs text-bolt-elements-textSecondary">
526
+ <span className="flex items-center gap-1">
527
+ <div className="i-ph:star w-3 h-3" />
528
+ {repo.stargazers_count}
529
+ </span>
530
+ <span className="flex items-center gap-1">
531
+ <div className="i-ph:git-fork w-3 h-3" />
532
+ {repo.forks_count}
533
+ </span>
534
+ </div>
535
+ </div>
536
+ </a>
537
+ ))}
538
+ </div>
539
+ </div>
540
+ )}
541
+ </div>
542
+ )}
543
+ </div>
544
+ </motion.div>
545
+ );
546
+ }
547
+
548
+ function LoadingSpinner() {
549
+ return (
550
+ <div className="flex items-center justify-center p-4">
551
+ <div className="flex items-center gap-2">
552
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
553
+ <span className="text-bolt-elements-textSecondary">Loading...</span>
554
+ </div>
555
+ </div>
556
+ );
557
+ }
INSTALLER ADDED
@@ -0,0 +1 @@
 
 
1
+ pip
ImportButtons.tsx ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from 'ai';
2
+ import { toast } from 'react-toastify';
3
+ import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
4
+ import { Button } from '~/components/ui/Button';
5
+ import { classNames } from '~/utils/classNames';
6
+
7
+ type ChatData = {
8
+ messages?: Message[]; // Standard Bolt format
9
+ description?: string; // Optional description
10
+ };
11
+
12
+ export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
13
+ return (
14
+ <div className="flex flex-col items-center justify-center w-auto">
15
+ <input
16
+ type="file"
17
+ id="chat-import"
18
+ className="hidden"
19
+ accept=".json"
20
+ onChange={async (e) => {
21
+ const file = e.target.files?.[0];
22
+
23
+ if (file && importChat) {
24
+ try {
25
+ const reader = new FileReader();
26
+
27
+ reader.onload = async (e) => {
28
+ try {
29
+ const content = e.target?.result as string;
30
+ const data = JSON.parse(content) as ChatData;
31
+
32
+ // Standard format
33
+ if (Array.isArray(data.messages)) {
34
+ await importChat(data.description || 'Imported Chat', data.messages);
35
+ toast.success('Chat imported successfully');
36
+
37
+ return;
38
+ }
39
+
40
+ toast.error('Invalid chat file format');
41
+ } catch (error: unknown) {
42
+ if (error instanceof Error) {
43
+ toast.error('Failed to parse chat file: ' + error.message);
44
+ } else {
45
+ toast.error('Failed to parse chat file');
46
+ }
47
+ }
48
+ };
49
+ reader.onerror = () => toast.error('Failed to read chat file');
50
+ reader.readAsText(file);
51
+ } catch (error) {
52
+ toast.error(error instanceof Error ? error.message : 'Failed to import chat');
53
+ }
54
+ e.target.value = ''; // Reset file input
55
+ } else {
56
+ toast.error('Something went wrong');
57
+ }
58
+ }}
59
+ />
60
+ <div className="flex flex-col items-center gap-4 max-w-2xl text-center">
61
+ <div className="flex gap-2">
62
+ <Button
63
+ onClick={() => {
64
+ const input = document.getElementById('chat-import');
65
+ input?.click();
66
+ }}
67
+ variant="outline"
68
+ size="lg"
69
+ className={classNames(
70
+ 'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
71
+ 'text-bolt-elements-textPrimary dark:text-white',
72
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
73
+ 'border-[#E5E5E5] dark:border-[#333333]',
74
+ 'h-10 px-4 py-2 min-w-[120px] justify-center',
75
+ 'transition-all duration-200 ease-in-out',
76
+ )}
77
+ >
78
+ <span className="i-ph:upload-simple w-4 h-4" />
79
+ Import Chat
80
+ </Button>
81
+ <ImportFolderButton
82
+ importChat={importChat}
83
+ className={classNames(
84
+ 'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
85
+ 'text-bolt-elements-textPrimary dark:text-white',
86
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
87
+ 'border border-[#E5E5E5] dark:border-[#333333]',
88
+ 'h-10 px-4 py-2 min-w-[120px] justify-center',
89
+ 'transition-all duration-200 ease-in-out rounded-lg',
90
+ )}
91
+ />
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
ImportFolderButton.tsx ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import type { Message } from 'ai';
3
+ import { toast } from 'react-toastify';
4
+ import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
5
+ import { createChatFromFolder } from '~/utils/folderImport';
6
+ import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
7
+ import { Button } from '~/components/ui/Button';
8
+ import { classNames } from '~/utils/classNames';
9
+
10
+ interface ImportFolderButtonProps {
11
+ className?: string;
12
+ importChat?: (description: string, messages: Message[]) => Promise<void>;
13
+ }
14
+
15
+ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ className, importChat }) => {
16
+ const [isLoading, setIsLoading] = useState(false);
17
+
18
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
19
+ const allFiles = Array.from(e.target.files || []);
20
+
21
+ const filteredFiles = allFiles.filter((file) => {
22
+ const path = file.webkitRelativePath.split('/').slice(1).join('/');
23
+ const include = shouldIncludeFile(path);
24
+
25
+ return include;
26
+ });
27
+
28
+ if (filteredFiles.length === 0) {
29
+ const error = new Error('No valid files found');
30
+ logStore.logError('File import failed - no valid files', error, { folderName: 'Unknown Folder' });
31
+ toast.error('No files found in the selected folder');
32
+
33
+ return;
34
+ }
35
+
36
+ if (filteredFiles.length > MAX_FILES) {
37
+ const error = new Error(`Too many files: ${filteredFiles.length}`);
38
+ logStore.logError('File import failed - too many files', error, {
39
+ fileCount: filteredFiles.length,
40
+ maxFiles: MAX_FILES,
41
+ });
42
+ toast.error(
43
+ `This folder contains ${filteredFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
44
+ );
45
+
46
+ return;
47
+ }
48
+
49
+ const folderName = filteredFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
50
+ setIsLoading(true);
51
+
52
+ const loadingToast = toast.loading(`Importing ${folderName}...`);
53
+
54
+ try {
55
+ const fileChecks = await Promise.all(
56
+ filteredFiles.map(async (file) => ({
57
+ file,
58
+ isBinary: await isBinaryFile(file),
59
+ })),
60
+ );
61
+
62
+ const textFiles = fileChecks.filter((f) => !f.isBinary).map((f) => f.file);
63
+ const binaryFilePaths = fileChecks
64
+ .filter((f) => f.isBinary)
65
+ .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
66
+
67
+ if (textFiles.length === 0) {
68
+ const error = new Error('No text files found');
69
+ logStore.logError('File import failed - no text files', error, { folderName });
70
+ toast.error('No text files found in the selected folder');
71
+
72
+ return;
73
+ }
74
+
75
+ if (binaryFilePaths.length > 0) {
76
+ logStore.logWarning(`Skipping binary files during import`, {
77
+ folderName,
78
+ binaryCount: binaryFilePaths.length,
79
+ });
80
+ toast.info(`Skipping ${binaryFilePaths.length} binary files`);
81
+ }
82
+
83
+ const messages = await createChatFromFolder(textFiles, binaryFilePaths, folderName);
84
+
85
+ if (importChat) {
86
+ await importChat(folderName, [...messages]);
87
+ }
88
+
89
+ logStore.logSystem('Folder imported successfully', {
90
+ folderName,
91
+ textFileCount: textFiles.length,
92
+ binaryFileCount: binaryFilePaths.length,
93
+ });
94
+ toast.success('Folder imported successfully');
95
+ } catch (error) {
96
+ logStore.logError('Failed to import folder', error, { folderName });
97
+ console.error('Failed to import folder:', error);
98
+ toast.error('Failed to import folder');
99
+ } finally {
100
+ setIsLoading(false);
101
+ toast.dismiss(loadingToast);
102
+ e.target.value = ''; // Reset file input
103
+ }
104
+ };
105
+
106
+ return (
107
+ <>
108
+ <input
109
+ type="file"
110
+ id="folder-import"
111
+ className="hidden"
112
+ webkitdirectory=""
113
+ directory=""
114
+ onChange={handleFileChange}
115
+ {...({} as any)}
116
+ />
117
+ <Button
118
+ onClick={() => {
119
+ const input = document.getElementById('folder-import');
120
+ input?.click();
121
+ }}
122
+ title="Import Folder"
123
+ variant="outline"
124
+ size="lg"
125
+ className={classNames(
126
+ 'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
127
+ 'text-bolt-elements-textPrimary dark:text-white',
128
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
129
+ 'border-[#E5E5E5] dark:border-[#333333]',
130
+ 'h-10 px-4 py-2 min-w-[120px] justify-center',
131
+ 'transition-all duration-200 ease-in-out',
132
+ className,
133
+ )}
134
+ disabled={isLoading}
135
+ >
136
+ <span className="i-ph:upload-simple w-4 h-4" />
137
+ {isLoading ? 'Importing...' : 'Import Folder'}
138
+ </Button>
139
+ </>
140
+ );
141
+ };
LICENSE ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ All portions of this repository are under one of two licenses. The majority of the AutoGPT repository is under the MIT License below. The autogpt_platform folder is under the
2
+ Polyform Shield License.
3
+
4
+
5
+ MIT License
6
+
7
+
8
+ Copyright (c) 2023 Toran Bruce Richards
9
+
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
LICENSE.APACHE2 ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
LICENSE.MIT ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The MIT License (MIT)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
LICENSE.PSF ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
2
+ --------------------------------------------
3
+
4
+ 1. This LICENSE AGREEMENT is between the Python Software Foundation
5
+ ("PSF"), and the Individual or Organization ("Licensee") accessing and
6
+ otherwise using this software ("Python") in source or binary form and
7
+ its associated documentation.
8
+
9
+ 2. Subject to the terms and conditions of this License Agreement, PSF hereby
10
+ grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
11
+ analyze, test, perform and/or display publicly, prepare derivative works,
12
+ distribute, and otherwise use Python alone or in any derivative version,
13
+ provided, however, that PSF's License Agreement and PSF's notice of copyright,
14
+ i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
15
+ 2011 Python Software Foundation; All Rights Reserved" are retained in Python
16
+ alone or in any derivative version prepared by Licensee.
17
+
18
+ 3. In the event Licensee prepares a derivative work that is based on
19
+ or incorporates Python or any part thereof, and wants to make
20
+ the derivative work available to others as provided herein, then
21
+ Licensee hereby agrees to include in any such work a brief summary of
22
+ the changes made to Python.
23
+
24
+ 4. PSF is making Python available to Licensee on an "AS IS"
25
+ basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
26
+ IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
27
+ DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
28
+ FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
29
+ INFRINGE ANY THIRD PARTY RIGHTS.
30
+
31
+ 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
32
+ FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
33
+ A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
34
+ OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
35
+
36
+ 6. This License Agreement will automatically terminate upon a material
37
+ breach of its terms and conditions.
38
+
39
+ 7. Nothing in this License Agreement shall be deemed to create any
40
+ relationship of agency, partnership, or joint venture between PSF and
41
+ Licensee. This License Agreement does not grant permission to use PSF
42
+ trademarks or trade name in a trademark sense to endorse or promote
43
+ products or services of Licensee, or any third party.
44
+
45
+ 8. By copying, installing or otherwise using Python, Licensee
46
+ agrees to be bound by the terms and conditions of this License
47
+ Agreement.
LICENSE.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2013-2024, Kim Davies and contributors.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are
8
+ met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright
11
+ notice, this list of conditions and the following disclaimer.
12
+
13
+ 2. Redistributions in binary form must reproduce the above copyright
14
+ notice, this list of conditions and the following disclaimer in the
15
+ documentation and/or other materials provided with the distribution.
16
+
17
+ 3. Neither the name of the copyright holder nor the names of its
18
+ contributors may be used to endorse or promote products derived from
19
+ this software without specific prior written permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
27
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
28
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
29
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
30
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
LICENSE.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2008-2021 The pip developers (see AUTHORS.txt file)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Layout.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // /home/ubuntu/visionos-frontend/src/components/Layout.tsx
2
+ import React from 'react';
3
+ import Link from 'next/link'; // Import Link for navigation
4
+
5
+ interface LayoutProps {
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ const Layout: React.FC<LayoutProps> = ({ children }) => {
10
+ return (
11
+ <div className="flex flex-col min-h-screen">
12
+ {/* Header/Navigation */}
13
+ <header className="bg-gray-800 text-white p-4">
14
+ <nav className="container mx-auto flex justify-between items-center">
15
+ <h1 className="text-xl font-bold">
16
+ <Link href="/">VisionOS UI</Link>
17
+ </h1>
18
+ <ul className="flex space-x-4">
19
+ <li><Link href="/" className="hover:text-gray-300">Chat</Link></li>
20
+ <li><Link href="/workflow" className="hover:text-gray-300">Workflow</Link></li>
21
+ <li><Link href="/settings" className="hover:text-gray-300">Settings</Link></li>
22
+ {/* Add more navigation links here */}
23
+ </ul>
24
+ </nav>
25
+ </header>
26
+
27
+ {/* Main Content Area */}
28
+ <main className="flex-grow p-4 container mx-auto">
29
+ {children}
30
+ </main>
31
+
32
+ {/* Footer */}
33
+ <footer className="bg-gray-200 p-4 text-center text-sm text-gray-600">
34
+ © 2025 VisionOS
35
+ </footer>
36
+ </div>
37
+ );
38
+ };
39
+
40
+ export default Layout;
41
+
LocalProvidersTab.tsx ADDED
@@ -0,0 +1,777 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import { Switch } from '~/components/ui/Switch';
3
+ import { useSettings } from '~/lib/hooks/useSettings';
4
+ import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
5
+ import type { IProviderConfig } from '~/types/model';
6
+ import { logStore } from '~/lib/stores/logs';
7
+ import { motion, AnimatePresence } from 'framer-motion';
8
+ import { classNames } from '~/utils/classNames';
9
+ import { BsRobot } from 'react-icons/bs';
10
+ import type { IconType } from 'react-icons';
11
+ import { BiChip } from 'react-icons/bi';
12
+ import { TbBrandOpenai } from 'react-icons/tb';
13
+ import { providerBaseUrlEnvKeys } from '~/utils/constants';
14
+ import { useToast } from '~/components/ui/use-toast';
15
+ import { Progress } from '~/components/ui/Progress';
16
+ import OllamaModelInstaller from './OllamaModelInstaller';
17
+
18
+ // Add type for provider names to ensure type safety
19
+ type ProviderName = 'Ollama' | 'LMStudio' | 'OpenAILike';
20
+
21
+ // Update the PROVIDER_ICONS type to use the ProviderName type
22
+ const PROVIDER_ICONS: Record<ProviderName, IconType> = {
23
+ Ollama: BsRobot,
24
+ LMStudio: BsRobot,
25
+ OpenAILike: TbBrandOpenai,
26
+ };
27
+
28
+ // Update PROVIDER_DESCRIPTIONS to use the same type
29
+ const PROVIDER_DESCRIPTIONS: Record<ProviderName, string> = {
30
+ Ollama: 'Run open-source models locally on your machine',
31
+ LMStudio: 'Local model inference with LM Studio',
32
+ OpenAILike: 'Connect to OpenAI-compatible API endpoints',
33
+ };
34
+
35
+ // Add a constant for the Ollama API base URL
36
+ const OLLAMA_API_URL = 'http://127.0.0.1:11434';
37
+
38
+ interface OllamaModel {
39
+ name: string;
40
+ digest: string;
41
+ size: number;
42
+ modified_at: string;
43
+ details?: {
44
+ family: string;
45
+ parameter_size: string;
46
+ quantization_level: string;
47
+ };
48
+ status?: 'idle' | 'updating' | 'updated' | 'error' | 'checking';
49
+ error?: string;
50
+ newDigest?: string;
51
+ progress?: {
52
+ current: number;
53
+ total: number;
54
+ status: string;
55
+ };
56
+ }
57
+
58
+ interface OllamaPullResponse {
59
+ status: string;
60
+ completed?: number;
61
+ total?: number;
62
+ digest?: string;
63
+ }
64
+
65
+ const isOllamaPullResponse = (data: unknown): data is OllamaPullResponse => {
66
+ return (
67
+ typeof data === 'object' &&
68
+ data !== null &&
69
+ 'status' in data &&
70
+ typeof (data as OllamaPullResponse).status === 'string'
71
+ );
72
+ };
73
+
74
+ export default function LocalProvidersTab() {
75
+ const { providers, updateProviderSettings } = useSettings();
76
+ const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
77
+ const [categoryEnabled, setCategoryEnabled] = useState(false);
78
+ const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
79
+ const [isLoadingModels, setIsLoadingModels] = useState(false);
80
+ const [editingProvider, setEditingProvider] = useState<string | null>(null);
81
+ const { toast } = useToast();
82
+
83
+ // Effect to filter and sort providers
84
+ useEffect(() => {
85
+ const newFilteredProviders = Object.entries(providers || {})
86
+ .filter(([key]) => [...LOCAL_PROVIDERS, 'OpenAILike'].includes(key))
87
+ .map(([key, value]) => {
88
+ const provider = value as IProviderConfig;
89
+ const envKey = providerBaseUrlEnvKeys[key]?.baseUrlKey;
90
+ const envUrl = envKey ? (import.meta.env[envKey] as string | undefined) : undefined;
91
+
92
+ // Set base URL if provided by environment
93
+ if (envUrl && !provider.settings.baseUrl) {
94
+ updateProviderSettings(key, {
95
+ ...provider.settings,
96
+ baseUrl: envUrl,
97
+ });
98
+ }
99
+
100
+ return {
101
+ name: key,
102
+ settings: {
103
+ ...provider.settings,
104
+ baseUrl: provider.settings.baseUrl || envUrl,
105
+ },
106
+ staticModels: provider.staticModels || [],
107
+ getDynamicModels: provider.getDynamicModels,
108
+ getApiKeyLink: provider.getApiKeyLink,
109
+ labelForGetApiKey: provider.labelForGetApiKey,
110
+ icon: provider.icon,
111
+ } as IProviderConfig;
112
+ });
113
+
114
+ // Custom sort function to ensure LMStudio appears before OpenAILike
115
+ const sorted = newFilteredProviders.sort((a, b) => {
116
+ if (a.name === 'LMStudio') {
117
+ return -1;
118
+ }
119
+
120
+ if (b.name === 'LMStudio') {
121
+ return 1;
122
+ }
123
+
124
+ if (a.name === 'OpenAILike') {
125
+ return 1;
126
+ }
127
+
128
+ if (b.name === 'OpenAILike') {
129
+ return -1;
130
+ }
131
+
132
+ return a.name.localeCompare(b.name);
133
+ });
134
+ setFilteredProviders(sorted);
135
+ }, [providers, updateProviderSettings]);
136
+
137
+ // Add effect to update category toggle state based on provider states
138
+ useEffect(() => {
139
+ const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
140
+ setCategoryEnabled(newCategoryState);
141
+ }, [filteredProviders]);
142
+
143
+ // Fetch Ollama models when enabled
144
+ useEffect(() => {
145
+ const ollamaProvider = filteredProviders.find((p) => p.name === 'Ollama');
146
+
147
+ if (ollamaProvider?.settings.enabled) {
148
+ fetchOllamaModels();
149
+ }
150
+ }, [filteredProviders]);
151
+
152
+ const fetchOllamaModels = async () => {
153
+ try {
154
+ setIsLoadingModels(true);
155
+
156
+ const response = await fetch('http://127.0.0.1:11434/api/tags');
157
+ const data = (await response.json()) as { models: OllamaModel[] };
158
+
159
+ setOllamaModels(
160
+ data.models.map((model) => ({
161
+ ...model,
162
+ status: 'idle' as const,
163
+ })),
164
+ );
165
+ } catch (error) {
166
+ console.error('Error fetching Ollama models:', error);
167
+ } finally {
168
+ setIsLoadingModels(false);
169
+ }
170
+ };
171
+
172
+ const updateOllamaModel = async (modelName: string): Promise<boolean> => {
173
+ try {
174
+ const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
175
+ method: 'POST',
176
+ headers: { 'Content-Type': 'application/json' },
177
+ body: JSON.stringify({ name: modelName }),
178
+ });
179
+
180
+ if (!response.ok) {
181
+ throw new Error(`Failed to update ${modelName}`);
182
+ }
183
+
184
+ const reader = response.body?.getReader();
185
+
186
+ if (!reader) {
187
+ throw new Error('No response reader available');
188
+ }
189
+
190
+ while (true) {
191
+ const { done, value } = await reader.read();
192
+
193
+ if (done) {
194
+ break;
195
+ }
196
+
197
+ const text = new TextDecoder().decode(value);
198
+ const lines = text.split('\n').filter(Boolean);
199
+
200
+ for (const line of lines) {
201
+ const rawData = JSON.parse(line);
202
+
203
+ if (!isOllamaPullResponse(rawData)) {
204
+ console.error('Invalid response format:', rawData);
205
+ continue;
206
+ }
207
+
208
+ setOllamaModels((current) =>
209
+ current.map((m) =>
210
+ m.name === modelName
211
+ ? {
212
+ ...m,
213
+ progress: {
214
+ current: rawData.completed || 0,
215
+ total: rawData.total || 0,
216
+ status: rawData.status,
217
+ },
218
+ newDigest: rawData.digest,
219
+ }
220
+ : m,
221
+ ),
222
+ );
223
+ }
224
+ }
225
+
226
+ const updatedResponse = await fetch('http://127.0.0.1:11434/api/tags');
227
+ const updatedData = (await updatedResponse.json()) as { models: OllamaModel[] };
228
+ const updatedModel = updatedData.models.find((m) => m.name === modelName);
229
+
230
+ return updatedModel !== undefined;
231
+ } catch (error) {
232
+ console.error(`Error updating ${modelName}:`, error);
233
+ return false;
234
+ }
235
+ };
236
+
237
+ const handleToggleCategory = useCallback(
238
+ async (enabled: boolean) => {
239
+ filteredProviders.forEach((provider) => {
240
+ updateProviderSettings(provider.name, { ...provider.settings, enabled });
241
+ });
242
+ toast(enabled ? 'All local providers enabled' : 'All local providers disabled');
243
+ },
244
+ [filteredProviders, updateProviderSettings],
245
+ );
246
+
247
+ const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
248
+ updateProviderSettings(provider.name, {
249
+ ...provider.settings,
250
+ enabled,
251
+ });
252
+
253
+ if (enabled) {
254
+ logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
255
+ toast(`${provider.name} enabled`);
256
+ } else {
257
+ logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
258
+ toast(`${provider.name} disabled`);
259
+ }
260
+ };
261
+
262
+ const handleUpdateBaseUrl = (provider: IProviderConfig, newBaseUrl: string) => {
263
+ updateProviderSettings(provider.name, {
264
+ ...provider.settings,
265
+ baseUrl: newBaseUrl,
266
+ });
267
+ toast(`${provider.name} base URL updated`);
268
+ setEditingProvider(null);
269
+ };
270
+
271
+ const handleUpdateOllamaModel = async (modelName: string) => {
272
+ const updateSuccess = await updateOllamaModel(modelName);
273
+
274
+ if (updateSuccess) {
275
+ toast(`Updated ${modelName}`);
276
+ } else {
277
+ toast(`Failed to update ${modelName}`);
278
+ }
279
+ };
280
+
281
+ const handleDeleteOllamaModel = async (modelName: string) => {
282
+ try {
283
+ const response = await fetch(`${OLLAMA_API_URL}/api/delete`, {
284
+ method: 'DELETE',
285
+ headers: {
286
+ 'Content-Type': 'application/json',
287
+ },
288
+ body: JSON.stringify({ name: modelName }),
289
+ });
290
+
291
+ if (!response.ok) {
292
+ throw new Error(`Failed to delete ${modelName}`);
293
+ }
294
+
295
+ setOllamaModels((current) => current.filter((m) => m.name !== modelName));
296
+ toast(`Deleted ${modelName}`);
297
+ } catch (err) {
298
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
299
+ console.error(`Error deleting ${modelName}:`, errorMessage);
300
+ toast(`Failed to delete ${modelName}`);
301
+ }
302
+ };
303
+
304
+ // Update model details display
305
+ const ModelDetails = ({ model }: { model: OllamaModel }) => (
306
+ <div className="flex items-center gap-3 text-xs text-bolt-elements-textSecondary">
307
+ <div className="flex items-center gap-1">
308
+ <div className="i-ph:code text-purple-500" />
309
+ <span>{model.digest.substring(0, 7)}</span>
310
+ </div>
311
+ {model.details && (
312
+ <>
313
+ <div className="flex items-center gap-1">
314
+ <div className="i-ph:database text-purple-500" />
315
+ <span>{model.details.parameter_size}</span>
316
+ </div>
317
+ <div className="flex items-center gap-1">
318
+ <div className="i-ph:cube text-purple-500" />
319
+ <span>{model.details.quantization_level}</span>
320
+ </div>
321
+ </>
322
+ )}
323
+ </div>
324
+ );
325
+
326
+ // Update model actions to not use Tooltip
327
+ const ModelActions = ({
328
+ model,
329
+ onUpdate,
330
+ onDelete,
331
+ }: {
332
+ model: OllamaModel;
333
+ onUpdate: () => void;
334
+ onDelete: () => void;
335
+ }) => (
336
+ <div className="flex items-center gap-2">
337
+ <motion.button
338
+ onClick={onUpdate}
339
+ disabled={model.status === 'updating'}
340
+ className={classNames(
341
+ 'rounded-lg p-2',
342
+ 'bg-purple-500/10 text-purple-500',
343
+ 'hover:bg-purple-500/20',
344
+ 'transition-all duration-200',
345
+ { 'opacity-50 cursor-not-allowed': model.status === 'updating' },
346
+ )}
347
+ whileHover={{ scale: 1.05 }}
348
+ whileTap={{ scale: 0.95 }}
349
+ title="Update model"
350
+ >
351
+ {model.status === 'updating' ? (
352
+ <div className="flex items-center gap-2">
353
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
354
+ <span className="text-sm">Updating...</span>
355
+ </div>
356
+ ) : (
357
+ <div className="i-ph:arrows-clockwise text-lg" />
358
+ )}
359
+ </motion.button>
360
+ <motion.button
361
+ onClick={onDelete}
362
+ disabled={model.status === 'updating'}
363
+ className={classNames(
364
+ 'rounded-lg p-2',
365
+ 'bg-red-500/10 text-red-500',
366
+ 'hover:bg-red-500/20',
367
+ 'transition-all duration-200',
368
+ { 'opacity-50 cursor-not-allowed': model.status === 'updating' },
369
+ )}
370
+ whileHover={{ scale: 1.05 }}
371
+ whileTap={{ scale: 0.95 }}
372
+ title="Delete model"
373
+ >
374
+ <div className="i-ph:trash text-lg" />
375
+ </motion.button>
376
+ </div>
377
+ );
378
+
379
+ return (
380
+ <div
381
+ className={classNames(
382
+ 'rounded-lg bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
383
+ 'hover:bg-bolt-elements-background-depth-2',
384
+ 'transition-all duration-200',
385
+ )}
386
+ role="region"
387
+ aria-label="Local Providers Configuration"
388
+ >
389
+ <motion.div
390
+ className="space-y-6"
391
+ initial={{ opacity: 0, y: 20 }}
392
+ animate={{ opacity: 1, y: 0 }}
393
+ transition={{ duration: 0.3 }}
394
+ >
395
+ {/* Header section */}
396
+ <div className="flex items-center justify-between gap-4 border-b border-bolt-elements-borderColor pb-4">
397
+ <div className="flex items-center gap-3">
398
+ <motion.div
399
+ className={classNames(
400
+ 'w-10 h-10 flex items-center justify-center rounded-xl',
401
+ 'bg-purple-500/10 text-purple-500',
402
+ )}
403
+ whileHover={{ scale: 1.05 }}
404
+ >
405
+ <BiChip className="w-6 h-6" />
406
+ </motion.div>
407
+ <div>
408
+ <div className="flex items-center gap-2">
409
+ <h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Local AI Models</h2>
410
+ </div>
411
+ <p className="text-sm text-bolt-elements-textSecondary">Configure and manage your local AI providers</p>
412
+ </div>
413
+ </div>
414
+
415
+ <div className="flex items-center gap-2">
416
+ <span className="text-sm text-bolt-elements-textSecondary">Enable All</span>
417
+ <Switch
418
+ checked={categoryEnabled}
419
+ onCheckedChange={handleToggleCategory}
420
+ aria-label="Toggle all local providers"
421
+ />
422
+ </div>
423
+ </div>
424
+
425
+ {/* Ollama Section */}
426
+ {filteredProviders
427
+ .filter((provider) => provider.name === 'Ollama')
428
+ .map((provider) => (
429
+ <motion.div
430
+ key={provider.name}
431
+ className={classNames(
432
+ 'bg-bolt-elements-background-depth-2 rounded-xl',
433
+ 'hover:bg-bolt-elements-background-depth-3',
434
+ 'transition-all duration-200 p-5',
435
+ 'relative overflow-hidden group',
436
+ )}
437
+ initial={{ opacity: 0, y: 20 }}
438
+ animate={{ opacity: 1, y: 0 }}
439
+ whileHover={{ scale: 1.01 }}
440
+ >
441
+ {/* Provider Header */}
442
+ <div className="flex items-start justify-between gap-4">
443
+ <div className="flex items-start gap-4">
444
+ <motion.div
445
+ className={classNames(
446
+ 'w-12 h-12 flex items-center justify-center rounded-xl',
447
+ 'bg-bolt-elements-background-depth-3',
448
+ provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
449
+ )}
450
+ whileHover={{ scale: 1.1, rotate: 5 }}
451
+ >
452
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
453
+ className: 'w-7 h-7',
454
+ 'aria-label': `${provider.name} icon`,
455
+ })}
456
+ </motion.div>
457
+ <div>
458
+ <div className="flex items-center gap-2">
459
+ <h3 className="text-md font-semibold text-bolt-elements-textPrimary">{provider.name}</h3>
460
+ <span className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500">Local</span>
461
+ </div>
462
+ <p className="text-sm text-bolt-elements-textSecondary mt-1">
463
+ {PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
464
+ </p>
465
+ </div>
466
+ </div>
467
+ <Switch
468
+ checked={provider.settings.enabled}
469
+ onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
470
+ aria-label={`Toggle ${provider.name} provider`}
471
+ />
472
+ </div>
473
+
474
+ {/* URL Configuration Section */}
475
+ <AnimatePresence>
476
+ {provider.settings.enabled && (
477
+ <motion.div
478
+ initial={{ opacity: 0, height: 0 }}
479
+ animate={{ opacity: 1, height: 'auto' }}
480
+ exit={{ opacity: 0, height: 0 }}
481
+ className="mt-4"
482
+ >
483
+ <div className="flex flex-col gap-2">
484
+ <label className="text-sm text-bolt-elements-textSecondary">API Endpoint</label>
485
+ {editingProvider === provider.name ? (
486
+ <input
487
+ type="text"
488
+ defaultValue={provider.settings.baseUrl || OLLAMA_API_URL}
489
+ placeholder="Enter Ollama base URL"
490
+ className={classNames(
491
+ 'w-full px-3 py-2 rounded-lg text-sm',
492
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
493
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
494
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
495
+ 'transition-all duration-200',
496
+ )}
497
+ onKeyDown={(e) => {
498
+ if (e.key === 'Enter') {
499
+ handleUpdateBaseUrl(provider, e.currentTarget.value);
500
+ } else if (e.key === 'Escape') {
501
+ setEditingProvider(null);
502
+ }
503
+ }}
504
+ onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
505
+ autoFocus
506
+ />
507
+ ) : (
508
+ <div
509
+ onClick={() => setEditingProvider(provider.name)}
510
+ className={classNames(
511
+ 'w-full px-3 py-2 rounded-lg text-sm cursor-pointer',
512
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
513
+ 'hover:border-purple-500/30 hover:bg-bolt-elements-background-depth-4',
514
+ 'transition-all duration-200',
515
+ )}
516
+ >
517
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
518
+ <div className="i-ph:link text-sm" />
519
+ <span>{provider.settings.baseUrl || OLLAMA_API_URL}</span>
520
+ </div>
521
+ </div>
522
+ )}
523
+ </div>
524
+ </motion.div>
525
+ )}
526
+ </AnimatePresence>
527
+
528
+ {/* Ollama Models Section */}
529
+ {provider.settings.enabled && (
530
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-6 space-y-4">
531
+ <div className="flex items-center justify-between">
532
+ <div className="flex items-center gap-2">
533
+ <div className="i-ph:cube-duotone text-purple-500" />
534
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary">Installed Models</h4>
535
+ </div>
536
+ {isLoadingModels ? (
537
+ <div className="flex items-center gap-2">
538
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
539
+ <span className="text-sm text-bolt-elements-textSecondary">Loading models...</span>
540
+ </div>
541
+ ) : (
542
+ <span className="text-sm text-bolt-elements-textSecondary">
543
+ {ollamaModels.length} models available
544
+ </span>
545
+ )}
546
+ </div>
547
+
548
+ <div className="space-y-3">
549
+ {isLoadingModels ? (
550
+ <div className="space-y-3">
551
+ {Array.from({ length: 3 }).map((_, i) => (
552
+ <div
553
+ key={i}
554
+ className="h-20 w-full bg-bolt-elements-background-depth-3 rounded-lg animate-pulse"
555
+ />
556
+ ))}
557
+ </div>
558
+ ) : ollamaModels.length === 0 ? (
559
+ <div className="text-center py-8 text-bolt-elements-textSecondary">
560
+ <div className="i-ph:cube-transparent text-4xl mx-auto mb-2" />
561
+ <p>No models installed yet</p>
562
+ <p className="text-sm text-bolt-elements-textTertiary px-1">
563
+ Browse models at{' '}
564
+ <a
565
+ href="https://ollama.com/library"
566
+ target="_blank"
567
+ rel="noopener noreferrer"
568
+ className="text-purple-500 hover:underline inline-flex items-center gap-0.5 text-base font-medium"
569
+ >
570
+ ollama.com/library
571
+ <div className="i-ph:arrow-square-out text-xs" />
572
+ </a>{' '}
573
+ and copy model names to install
574
+ </p>
575
+ </div>
576
+ ) : (
577
+ ollamaModels.map((model) => (
578
+ <motion.div
579
+ key={model.name}
580
+ className={classNames(
581
+ 'p-4 rounded-xl',
582
+ 'bg-bolt-elements-background-depth-3',
583
+ 'hover:bg-bolt-elements-background-depth-4',
584
+ 'transition-all duration-200',
585
+ )}
586
+ whileHover={{ scale: 1.01 }}
587
+ >
588
+ <div className="flex items-center justify-between">
589
+ <div className="space-y-2">
590
+ <div className="flex items-center gap-2">
591
+ <h5 className="text-sm font-medium text-bolt-elements-textPrimary">{model.name}</h5>
592
+ <ModelStatusBadge status={model.status} />
593
+ </div>
594
+ <ModelDetails model={model} />
595
+ </div>
596
+ <ModelActions
597
+ model={model}
598
+ onUpdate={() => handleUpdateOllamaModel(model.name)}
599
+ onDelete={() => {
600
+ if (window.confirm(`Are you sure you want to delete ${model.name}?`)) {
601
+ handleDeleteOllamaModel(model.name);
602
+ }
603
+ }}
604
+ />
605
+ </div>
606
+ {model.progress && (
607
+ <div className="mt-3">
608
+ <Progress
609
+ value={Math.round((model.progress.current / model.progress.total) * 100)}
610
+ className="h-1"
611
+ />
612
+ <div className="flex justify-between mt-1 text-xs text-bolt-elements-textSecondary">
613
+ <span>{model.progress.status}</span>
614
+ <span>{Math.round((model.progress.current / model.progress.total) * 100)}%</span>
615
+ </div>
616
+ </div>
617
+ )}
618
+ </motion.div>
619
+ ))
620
+ )}
621
+ </div>
622
+
623
+ {/* Model Installation Section */}
624
+ <OllamaModelInstaller onModelInstalled={fetchOllamaModels} />
625
+ </motion.div>
626
+ )}
627
+ </motion.div>
628
+ ))}
629
+
630
+ {/* Other Providers Section */}
631
+ <div className="border-t border-bolt-elements-borderColor pt-6 mt-8">
632
+ <h3 className="text-lg font-semibold text-bolt-elements-textPrimary mb-4">Other Local Providers</h3>
633
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
634
+ {filteredProviders
635
+ .filter((provider) => provider.name !== 'Ollama')
636
+ .map((provider, index) => (
637
+ <motion.div
638
+ key={provider.name}
639
+ className={classNames(
640
+ 'bg-bolt-elements-background-depth-2 rounded-xl',
641
+ 'hover:bg-bolt-elements-background-depth-3',
642
+ 'transition-all duration-200 p-5',
643
+ 'relative overflow-hidden group',
644
+ )}
645
+ initial={{ opacity: 0, y: 20 }}
646
+ animate={{ opacity: 1, y: 0 }}
647
+ transition={{ delay: index * 0.1 }}
648
+ whileHover={{ scale: 1.01 }}
649
+ >
650
+ {/* Provider Header */}
651
+ <div className="flex items-start justify-between gap-4">
652
+ <div className="flex items-start gap-4">
653
+ <motion.div
654
+ className={classNames(
655
+ 'w-12 h-12 flex items-center justify-center rounded-xl',
656
+ 'bg-bolt-elements-background-depth-3',
657
+ provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
658
+ )}
659
+ whileHover={{ scale: 1.1, rotate: 5 }}
660
+ >
661
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
662
+ className: 'w-7 h-7',
663
+ 'aria-label': `${provider.name} icon`,
664
+ })}
665
+ </motion.div>
666
+ <div>
667
+ <div className="flex items-center gap-2">
668
+ <h3 className="text-md font-semibold text-bolt-elements-textPrimary">{provider.name}</h3>
669
+ <div className="flex gap-1">
670
+ <span className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500">
671
+ Local
672
+ </span>
673
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
674
+ <span className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500">
675
+ Configurable
676
+ </span>
677
+ )}
678
+ </div>
679
+ </div>
680
+ <p className="text-sm text-bolt-elements-textSecondary mt-1">
681
+ {PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
682
+ </p>
683
+ </div>
684
+ </div>
685
+ <Switch
686
+ checked={provider.settings.enabled}
687
+ onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
688
+ aria-label={`Toggle ${provider.name} provider`}
689
+ />
690
+ </div>
691
+
692
+ {/* URL Configuration Section */}
693
+ <AnimatePresence>
694
+ {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
695
+ <motion.div
696
+ initial={{ opacity: 0, height: 0 }}
697
+ animate={{ opacity: 1, height: 'auto' }}
698
+ exit={{ opacity: 0, height: 0 }}
699
+ className="mt-4"
700
+ >
701
+ <div className="flex flex-col gap-2">
702
+ <label className="text-sm text-bolt-elements-textSecondary">API Endpoint</label>
703
+ {editingProvider === provider.name ? (
704
+ <input
705
+ type="text"
706
+ defaultValue={provider.settings.baseUrl}
707
+ placeholder={`Enter ${provider.name} base URL`}
708
+ className={classNames(
709
+ 'w-full px-3 py-2 rounded-lg text-sm',
710
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
711
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
712
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
713
+ 'transition-all duration-200',
714
+ )}
715
+ onKeyDown={(e) => {
716
+ if (e.key === 'Enter') {
717
+ handleUpdateBaseUrl(provider, e.currentTarget.value);
718
+ } else if (e.key === 'Escape') {
719
+ setEditingProvider(null);
720
+ }
721
+ }}
722
+ onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
723
+ autoFocus
724
+ />
725
+ ) : (
726
+ <div
727
+ onClick={() => setEditingProvider(provider.name)}
728
+ className={classNames(
729
+ 'w-full px-3 py-2 rounded-lg text-sm cursor-pointer',
730
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
731
+ 'hover:border-purple-500/30 hover:bg-bolt-elements-background-depth-4',
732
+ 'transition-all duration-200',
733
+ )}
734
+ >
735
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
736
+ <div className="i-ph:link text-sm" />
737
+ <span>{provider.settings.baseUrl || 'Click to set base URL'}</span>
738
+ </div>
739
+ </div>
740
+ )}
741
+ </div>
742
+ </motion.div>
743
+ )}
744
+ </AnimatePresence>
745
+ </motion.div>
746
+ ))}
747
+ </div>
748
+ </div>
749
+ </motion.div>
750
+ </div>
751
+ );
752
+ }
753
+
754
+ // Helper component for model status badge
755
+ function ModelStatusBadge({ status }: { status?: string }) {
756
+ if (!status || status === 'idle') {
757
+ return null;
758
+ }
759
+
760
+ const statusConfig = {
761
+ updating: { bg: 'bg-yellow-500/10', text: 'text-yellow-500', label: 'Updating' },
762
+ updated: { bg: 'bg-green-500/10', text: 'text-green-500', label: 'Updated' },
763
+ error: { bg: 'bg-red-500/10', text: 'text-red-500', label: 'Error' },
764
+ };
765
+
766
+ const config = statusConfig[status as keyof typeof statusConfig];
767
+
768
+ if (!config) {
769
+ return null;
770
+ }
771
+
772
+ return (
773
+ <span className={classNames('px-2 py-0.5 rounded-full text-xs font-medium', config.bg, config.text)}>
774
+ {config.label}
775
+ </span>
776
+ );
777
+ }