codacus commited on
Commit
7e650af
·
2 Parent(s): b304749 25e6bb3

Merge branch 'main' into add-loading-on-git-import-from-url

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +5 -0
  2. .github/ISSUE_TEMPLATE/config.yml +8 -0
  3. .github/workflows/commit.yaml +10 -3
  4. .github/workflows/pr-release-validation.yaml +31 -0
  5. .github/workflows/update-stable.yml +193 -0
  6. .gitignore +3 -0
  7. .husky/pre-commit +27 -10
  8. CONTRIBUTING.md +2 -2
  9. FAQ.md +13 -20
  10. README.md +135 -136
  11. app/commit.json +1 -1
  12. app/components/chat/AssistantMessage.tsx +18 -1
  13. app/components/chat/BaseChat.module.scss +0 -76
  14. app/components/chat/BaseChat.tsx +64 -80
  15. app/components/chat/Chat.client.tsx +17 -2
  16. app/components/chat/GitCloneButton.tsx +9 -14
  17. app/components/chat/ImportFolderButton.tsx +23 -0
  18. app/components/chat/Messages.client.tsx +9 -5
  19. app/components/chat/ModelSelector.tsx +18 -53
  20. app/components/chat/ScreenshotStateManager.tsx +33 -0
  21. app/components/chat/UserMessage.tsx +15 -21
  22. app/components/chat/chatExportAndImport/ImportButtons.tsx +0 -1
  23. app/components/editor/codemirror/languages.ts +7 -0
  24. app/components/header/Header.tsx +7 -8
  25. app/{styles/components/Settings.scss → components/settings/Settings.module.scss} +4 -1
  26. app/components/settings/SettingsWindow.tsx +128 -0
  27. app/components/settings/chat-history/ChatHistoryTab.tsx +119 -0
  28. app/components/settings/connections/ConnectionsTab.tsx +54 -0
  29. app/components/settings/debug/DebugTab.tsx +620 -0
  30. app/components/settings/event-logs/EventLogsTab.tsx +219 -0
  31. app/components/settings/features/FeaturesTab.tsx +75 -0
  32. app/components/settings/providers/ProvidersTab.tsx +106 -0
  33. app/components/sidebar/Menu.client.tsx +24 -6
  34. app/components/ui/BackgroundRays/index.tsx +18 -0
  35. app/components/ui/BackgroundRays/styles.module.scss +246 -0
  36. app/components/ui/IconButton.tsx +41 -34
  37. app/components/ui/Settings.tsx +0 -444
  38. app/components/ui/SettingsButton.tsx +1 -2
  39. app/components/ui/SettingsSlider.tsx +0 -63
  40. app/components/ui/Switch.tsx +37 -0
  41. app/components/ui/Tooltip.tsx +62 -56
  42. app/components/workbench/FileTree.tsx +115 -33
  43. app/components/workbench/Preview.tsx +22 -2
  44. app/components/workbench/ScreenshotSelector.tsx +293 -0
  45. app/components/workbench/Workbench.client.tsx +13 -11
  46. app/lib/.server/llm/api-key.ts +2 -0
  47. app/lib/.server/llm/model.ts +20 -2
  48. app/lib/.server/llm/stream-text.ts +119 -11
  49. app/lib/common/prompt-library.ts +49 -0
  50. app/lib/common/prompts/optimized.ts +199 -0
.env.example CHANGED
@@ -70,6 +70,11 @@ LMSTUDIO_API_BASE_URL=
70
  # You only need this environment variable set if you want to use xAI models
71
  XAI_API_KEY=
72
 
 
 
 
 
 
73
  # Include this environment variable if you want more logging for debugging locally
74
  VITE_LOG_LEVEL=debug
75
 
 
70
  # You only need this environment variable set if you want to use xAI models
71
  XAI_API_KEY=
72
 
73
+ # Get your Perplexity API Key here -
74
+ # https://www.perplexity.ai/settings/api
75
+ # You only need this environment variable set if you want to use Perplexity models
76
+ PERPLEXITY_API_KEY=
77
+
78
  # Include this environment variable if you want more logging for debugging locally
79
  VITE_LOG_LEVEL=debug
80
 
.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Bolt.new related issues
4
+ url: https://github.com/stackblitz/bolt.new/issues/new/choose
5
+ about: Report issues related to Bolt.new (not Bolt.diy)
6
+ - name: Chat
7
+ url: https://thinktank.ottomator.ai
8
+ about: Ask questions and discuss with other Bolt.diy users.
.github/workflows/commit.yaml CHANGED
@@ -10,18 +10,25 @@ permissions:
10
 
11
  jobs:
12
  update-commit:
 
13
  runs-on: ubuntu-latest
14
 
15
  steps:
16
  - name: Checkout the code
17
  uses: actions/checkout@v3
18
 
 
 
 
 
19
  - name: Get the latest commit hash
20
- run: echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
21
-
 
 
22
  - name: Update commit file
23
  run: |
24
- echo "{ \"commit\": \"$COMMIT_HASH\" }" > app/commit.json
25
 
26
  - name: Commit and push the update
27
  run: |
 
10
 
11
  jobs:
12
  update-commit:
13
+ if: contains(github.event.head_commit.message, '#release') != true
14
  runs-on: ubuntu-latest
15
 
16
  steps:
17
  - name: Checkout the code
18
  uses: actions/checkout@v3
19
 
20
+ - name: Setup Node.js
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: '20'
24
  - name: Get the latest commit hash
25
+ run: |
26
+ echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
27
+ echo "CURRENT_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
28
+
29
  - name: Update commit file
30
  run: |
31
+ echo "{ \"commit\": \"$COMMIT_HASH\" , \"version\": \"$CURRENT_VERSION\" }" > app/commit.json
32
 
33
  - name: Commit and push the update
34
  run: |
.github/workflows/pr-release-validation.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: PR Validation
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened, labeled, unlabeled]
6
+ branches:
7
+ - main
8
+
9
+ jobs:
10
+ validate:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Validate PR Labels
17
+ run: |
18
+ if [[ "${{ contains(github.event.pull_request.labels.*.name, 'stable-release') }}" == "true" ]]; then
19
+ echo "✓ PR has stable-release label"
20
+
21
+ # Check version bump labels
22
+ if [[ "${{ contains(github.event.pull_request.labels.*.name, 'major') }}" == "true" ]]; then
23
+ echo "✓ Major version bump requested"
24
+ elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'minor') }}" == "true" ]]; then
25
+ echo "✓ Minor version bump requested"
26
+ else
27
+ echo "✓ Patch version bump will be applied"
28
+ fi
29
+ else
30
+ echo "This PR doesn't have the stable-release label. No release will be created."
31
+ fi
.github/workflows/update-stable.yml ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Update Stable Branch
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ prepare-release:
13
+ if: contains(github.event.head_commit.message, '#release')
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 0
20
+
21
+ - name: Configure Git
22
+ run: |
23
+ git config --global user.name 'github-actions[bot]'
24
+ git config --global user.email 'github-actions[bot]@users.noreply.github.com'
25
+
26
+ - name: Setup Node.js
27
+ uses: actions/setup-node@v4
28
+ with:
29
+ node-version: '20'
30
+
31
+ - name: Install pnpm
32
+ uses: pnpm/action-setup@v2
33
+ with:
34
+ version: latest
35
+ run_install: false
36
+
37
+ - name: Get pnpm store directory
38
+ id: pnpm-cache
39
+ shell: bash
40
+ run: |
41
+ echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
42
+
43
+ - name: Setup pnpm cache
44
+ uses: actions/cache@v4
45
+ with:
46
+ path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
47
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
48
+ restore-keys: |
49
+ ${{ runner.os }}-pnpm-store-
50
+
51
+ - name: Get Current Version
52
+ id: current_version
53
+ run: |
54
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
55
+ echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
56
+
57
+ - name: Install semver
58
+ run: pnpm add -g semver
59
+
60
+ - name: Determine Version Bump
61
+ id: version_bump
62
+ run: |
63
+ COMMIT_MSG="${{ github.event.head_commit.message }}"
64
+ if [[ $COMMIT_MSG =~ "#release:major" ]]; then
65
+ echo "bump=major" >> $GITHUB_OUTPUT
66
+ elif [[ $COMMIT_MSG =~ "#release:minor" ]]; then
67
+ echo "bump=minor" >> $GITHUB_OUTPUT
68
+ else
69
+ echo "bump=patch" >> $GITHUB_OUTPUT
70
+ fi
71
+
72
+ - name: Bump Version
73
+ id: bump_version
74
+ run: |
75
+ NEW_VERSION=$(semver -i ${{ steps.version_bump.outputs.bump }} ${{ steps.current_version.outputs.version }})
76
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
77
+
78
+ - name: Update Package.json
79
+ run: |
80
+ NEW_VERSION=${{ steps.bump_version.outputs.new_version }}
81
+ pnpm version $NEW_VERSION --no-git-tag-version --allow-same-version
82
+
83
+ - name: Generate Changelog
84
+ id: changelog
85
+ run: |
86
+ # Get the latest tag
87
+ LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
88
+
89
+ # Start changelog file
90
+ echo "# Release v${{ steps.bump_version.outputs.new_version }}" > changelog.md
91
+ echo "" >> changelog.md
92
+
93
+ if [ -z "$LATEST_TAG" ]; then
94
+ echo "### 🎉 First Release" >> changelog.md
95
+ echo "" >> changelog.md
96
+ COMPARE_BASE="$(git rev-list --max-parents=0 HEAD)"
97
+ else
98
+ echo "### 🔄 Changes since $LATEST_TAG" >> changelog.md
99
+ echo "" >> changelog.md
100
+ COMPARE_BASE="$LATEST_TAG"
101
+ fi
102
+
103
+ # Function to extract conventional commit type
104
+ get_commit_type() {
105
+ if [[ $1 =~ ^feat:|^feature: ]]; then echo "✨ Features";
106
+ elif [[ $1 =~ ^fix: ]]; then echo "🐛 Bug Fixes";
107
+ elif [[ $1 =~ ^docs: ]]; then echo "📚 Documentation";
108
+ elif [[ $1 =~ ^style: ]]; then echo "💎 Styles";
109
+ elif [[ $1 =~ ^refactor: ]]; then echo "♻️ Code Refactoring";
110
+ elif [[ $1 =~ ^perf: ]]; then echo "⚡️ Performance Improvements";
111
+ elif [[ $1 =~ ^test: ]]; then echo "✅ Tests";
112
+ elif [[ $1 =~ ^build: ]]; then echo "🛠️ Build System";
113
+ elif [[ $1 =~ ^ci: ]]; then echo "⚙️ CI";
114
+ elif [[ $1 =~ ^chore: ]]; then echo "🔧 Chores";
115
+ else echo "🔍 Other Changes";
116
+ fi
117
+ }
118
+
119
+ # Generate categorized changelog
120
+ declare -A CATEGORIES
121
+ declare -A COMMITS_BY_CATEGORY
122
+
123
+ # Get commits since last tag or all commits if no tag exists
124
+ while IFS= read -r commit_line; do
125
+ HASH=$(echo "$commit_line" | cut -d'|' -f1)
126
+ MSG=$(echo "$commit_line" | cut -d'|' -f2)
127
+ PR_NUM=$(echo "$commit_line" | cut -d'|' -f3)
128
+
129
+ CATEGORY=$(get_commit_type "$MSG")
130
+ CATEGORIES["$CATEGORY"]=1
131
+
132
+ # Format commit message with PR link if available
133
+ if [ -n "$PR_NUM" ]; then
134
+ COMMITS_BY_CATEGORY["$CATEGORY"]+="- ${MSG#*: } ([#$PR_NUM](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/$PR_NUM))"$'\n'
135
+ else
136
+ COMMITS_BY_CATEGORY["$CATEGORY"]+="- ${MSG#*: }"$'\n'
137
+ fi
138
+ done < <(git log "${COMPARE_BASE}..HEAD" --pretty=format:"%H|%s|%(trailers:key=PR-Number,valueonly)" --reverse)
139
+
140
+ # Write categorized commits to changelog
141
+ for category in "✨ Features" "🐛 Bug Fixes" "📚 Documentation" "💎 Styles" "♻️ Code Refactoring" "⚡️ Performance Improvements" "✅ Tests" "🛠️ Build System" "⚙️ CI" "🔧 Chores" "🔍 Other Changes"; do
142
+ if [ -n "${COMMITS_BY_CATEGORY[$category]}" ]; then
143
+ echo "#### $category" >> changelog.md
144
+ echo "" >> changelog.md
145
+ echo "${COMMITS_BY_CATEGORY[$category]}" >> changelog.md
146
+ echo "" >> changelog.md
147
+ fi
148
+ done
149
+
150
+ # Add compare link if not first release
151
+ if [ -n "$LATEST_TAG" ]; then
152
+ echo "**Full Changelog**: [\`$LATEST_TAG..v${{ steps.bump_version.outputs.new_version }}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/$LATEST_TAG...v${{ steps.bump_version.outputs.new_version }})" >> changelog.md
153
+ fi
154
+
155
+ # Save changelog content for the release
156
+ CHANGELOG_CONTENT=$(cat changelog.md)
157
+ echo "content<<EOF" >> $GITHUB_OUTPUT
158
+ echo "$CHANGELOG_CONTENT" >> $GITHUB_OUTPUT
159
+ echo "EOF" >> $GITHUB_OUTPUT
160
+
161
+ - name: Get the latest commit hash and version tag
162
+ run: |
163
+ echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
164
+ echo "CURRENT_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
165
+
166
+ - name: Commit and Tag Release
167
+ run: |
168
+ git pull
169
+ echo "{ \"commit\": \"$COMMIT_HASH\" , \"version\": \"$CURRENT_VERSION\" }" > app/commit.json
170
+ git add package.json pnpm-lock.yaml changelog.md app/commit.json
171
+ git commit -m "chore: release version ${{ steps.bump_version.outputs.new_version }}"
172
+ git tag "v${{ steps.bump_version.outputs.new_version }}"
173
+ git push
174
+ git push --tags
175
+
176
+ - name: Update Stable Branch
177
+ run: |
178
+ if ! git checkout stable 2>/dev/null; then
179
+ echo "Creating new stable branch..."
180
+ git checkout -b stable
181
+ fi
182
+ git merge main --no-ff -m "chore: release version ${{ steps.bump_version.outputs.new_version }}"
183
+ git push --set-upstream origin stable --force
184
+
185
+ - name: Create GitHub Release
186
+ env:
187
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
188
+ run: |
189
+ VERSION="v${{ steps.bump_version.outputs.new_version }}"
190
+ gh release create "$VERSION" \
191
+ --title "Release $VERSION" \
192
+ --notes "${{ steps.changelog.outputs.content }}" \
193
+ --target stable
.gitignore CHANGED
@@ -37,3 +37,6 @@ modelfiles
37
 
38
  # docs ignore
39
  site
 
 
 
 
37
 
38
  # docs ignore
39
  site
40
+
41
+ # commit file ignore
42
+ app/commit.json
.husky/pre-commit CHANGED
@@ -2,25 +2,42 @@
2
 
3
  echo "🔍 Running pre-commit hook to check the code looks good... 🔍"
4
 
 
5
  export NVM_DIR="$HOME/.nvm"
6
- [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load nvm if you're using i
7
 
8
- echo "Running typecheck..."
9
- which pnpm
 
 
 
 
10
 
 
 
11
  if ! pnpm typecheck; then
12
- echo "❌ Type checking failed! Please review TypeScript types."
13
- echo "Once you're done, don't forget to add your changes to the commit! 🚀"
14
- echo "Typecheck exit code: $?"
15
- exit 1
16
  fi
17
 
 
18
  echo "Running lint..."
19
  if ! pnpm lint; then
20
- echo "❌ Linting failed! 'pnpm lint:fix' will help you fix the easy ones."
21
  echo "Once you're done, don't forget to add your beautification to the commit! 🤩"
22
- echo "lint exit code: $?"
23
  exit 1
24
  fi
25
 
26
- echo "👍 All good! Committing changes..."
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  echo "🔍 Running pre-commit hook to check the code looks good... 🔍"
4
 
5
+ # Load NVM if available (useful for managing Node.js versions)
6
  export NVM_DIR="$HOME/.nvm"
7
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
8
 
9
+ # Ensure `pnpm` is available
10
+ echo "Checking if pnpm is available..."
11
+ if ! command -v pnpm >/dev/null 2>&1; then
12
+ echo "❌ pnpm not found! Please ensure pnpm is installed and available in PATH."
13
+ exit 1
14
+ fi
15
 
16
+ # Run typecheck
17
+ echo "Running typecheck..."
18
  if ! pnpm typecheck; then
19
+ echo "❌ Type checking failed! Please review TypeScript types."
20
+ echo "Once you're done, don't forget to add your changes to the commit! 🚀"
21
+ exit 1
 
22
  fi
23
 
24
+ # Run lint
25
  echo "Running lint..."
26
  if ! pnpm lint; then
27
+ echo "❌ Linting failed! Run 'pnpm lint:fix' to fix the easy issues."
28
  echo "Once you're done, don't forget to add your beautification to the commit! 🤩"
 
29
  exit 1
30
  fi
31
 
32
+ # Update commit.json with the latest commit hash
33
+ echo "Updating commit.json with the latest commit hash..."
34
+ COMMIT_HASH=$(git rev-parse HEAD)
35
+ if [ $? -ne 0 ]; then
36
+ echo "❌ Failed to get commit hash. Ensure you are in a git repository."
37
+ exit 1
38
+ fi
39
+
40
+ echo "{ \"commit\": \"$COMMIT_HASH\" }" > app/commit.json
41
+ git add app/commit.json
42
+
43
+ echo "👍 All checks passed! Committing changes..."
CONTRIBUTING.md CHANGED
@@ -1,6 +1,6 @@
1
- # Contributing to oTToDev
2
 
3
- First off, thank you for considering contributing to oTToDev! This fork aims to expand the capabilities of the original project by integrating multiple LLM providers and enhancing functionality. Every contribution helps make oTToDev a better tool for developers worldwide.
4
 
5
  ## 📋 Table of Contents
6
  - [Code of Conduct](#code-of-conduct)
 
1
+ # Contributing to bolt.diy
2
 
3
+ First off, thank you for considering contributing to bolt.diy! This fork aims to expand the capabilities of the original project by integrating multiple LLM providers and enhancing functionality. Every contribution helps make bolt.diy a better tool for developers worldwide.
4
 
5
  ## 📋 Table of Contents
6
  - [Code of Conduct](#code-of-conduct)
FAQ.md CHANGED
@@ -1,49 +1,42 @@
1
- [![Bolt.new: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.new)
2
 
3
- # Bolt.new Fork by Cole Medin - oTToDev
4
 
5
  ## FAQ
6
 
7
- ### How do I get the best results with oTToDev?
8
 
9
- - **Be specific about your stack**: If you want to use specific frameworks or libraries (like Astro, Tailwind, ShadCN, or any other popular JavaScript framework), mention them in your initial prompt to ensure Bolt scaffolds the project accordingly.
10
 
11
  - **Use the enhance prompt icon**: Before sending your prompt, try clicking the 'enhance' icon to have the AI model help you refine your prompt, then edit the results before submitting.
12
 
13
- - **Scaffold the basics first, then add features**: Make sure the basic structure of your application is in place before diving into more advanced functionality. This helps oTToDev understand the foundation of your project and ensure everything is wired up right before building out more advanced functionality.
14
 
15
- - **Batch simple instructions**: Save time by combining simple instructions into one message. For example, you can ask oTToDev to change the color scheme, add mobile responsiveness, and restart the dev server, all in one go saving you time and reducing API credit consumption significantly.
16
-
17
- ### Do you plan on merging oTToDev back into the official Bolt.new repo?
18
-
19
- More news coming on this coming early next month - stay tuned!
20
 
21
  ### Why are there so many open issues/pull requests?
22
 
23
- oTToDev was started simply to showcase how to edit an open source project and to do something cool with local LLMs on my (@ColeMedin) YouTube channel! However, it quickly
24
- grew into a massive community project that I am working hard to keep up with the demand of by forming a team of maintainers and getting as many people involved as I can.
25
- That effort is going well and all of our maintainers are ABSOLUTE rockstars, but it still takes time to organize everything so we can efficiently get through all
26
- the issues and PRs. But rest assured, we are working hard and even working on some partnerships behind the scenes to really help this project take off!
27
 
28
- ### How do local LLMs fair compared to larger models like Claude 3.5 Sonnet for oTToDev/Bolt.new?
29
 
30
  As much as the gap is quickly closing between open source and massive close source models, you’re still going to get the best results with the very large models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b. This is one of the big tasks we have at hand - figuring out how to prompt better, use agents, and improve the platform as a whole to make it work better for even the smaller local LLMs!
31
 
32
  ### I'm getting the error: "There was an error processing this request"
33
 
34
- If you see this error within oTToDev, that is just the application telling you there is a problem at a high level, and this could mean a number of different things. To find the actual error, please check BOTH the terminal where you started the application (with Docker or pnpm) and the developer console in the browser. For most browsers, you can access the developer console by pressing F12 or right clicking anywhere in the browser and selecting “Inspect”. Then go to the “console” tab in the top right.
35
 
36
  ### I'm getting the error: "x-api-key header missing"
37
 
38
- We have seen this error a couple times and for some reason just restarting the Docker container has fixed it. This seems to be Ollama specific. Another thing to try is try to run oTToDev with Docker or pnpm, whichever you didn’t run first. We are still on the hunt for why this happens once and a while!
39
 
40
- ### I'm getting a blank preview when oTToDev runs my app!
41
 
42
- We promise you that we are constantly testing new PRs coming into oTToDev and the preview is core functionality, so the application is not broken! When you get a blank preview or don’t get a preview, this is generally because the LLM hallucinated bad code or incorrect commands. We are working on making this more transparent so it is obvious. Sometimes the error will appear in developer console too so check that as well.
43
 
44
  ### How to add a LLM:
45
 
46
- To make new LLMs available to use in this version of Bolt.new, head on over to `app/utils/constants.ts` and find the constant MODEL_LIST. Each element in this array is an object that has the model ID for the name (get this from the provider's API documentation), a label for the frontend model dropdown, and the provider.
47
 
48
  By default, Anthropic, OpenAI, Groq, and Ollama are implemented as providers, but the YouTube video for this repo covers how to extend this to work with more providers if you wish!
49
 
 
1
+ [![bolt.diy: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.diy)
2
 
3
+ # bolt.diy
4
 
5
  ## FAQ
6
 
7
+ ### How do I get the best results with bolt.diy?
8
 
9
+ - **Be specific about your stack**: If you want to use specific frameworks or libraries (like Astro, Tailwind, ShadCN, or any other popular JavaScript framework), mention them in your initial prompt to ensure bolt scaffolds the project accordingly.
10
 
11
  - **Use the enhance prompt icon**: Before sending your prompt, try clicking the 'enhance' icon to have the AI model help you refine your prompt, then edit the results before submitting.
12
 
13
+ - **Scaffold the basics first, then add features**: Make sure the basic structure of your application is in place before diving into more advanced functionality. This helps Bolt.diy understand the foundation of your project and ensure everything is wired up right before building out more advanced functionality.
14
 
15
+ - **Batch simple instructions**: Save time by combining simple instructions into one message. For example, you can ask Bolt.diy to change the color scheme, add mobile responsiveness, and restart the dev server, all in one go saving you time and reducing API credit consumption significantly.
 
 
 
 
16
 
17
  ### Why are there so many open issues/pull requests?
18
 
19
+ bolt.diy was started simply to showcase how to edit an open source project and to do something cool with local LLMs on my (@ColeMedin) YouTube channel! However, it quickly grew into a massive community project that I am working hard to keep up with the demand of by forming a team of maintainers and getting as many people involved as I can. That effort is going well and all of our maintainers are ABSOLUTE rockstars, but it still takes time to organize everything so we can efficiently get through all the issues and PRs. But rest assured, we are working hard and even working on some partnerships behind the scenes to really help this project take off!
 
 
 
20
 
21
+ ### How do local LLMs fair compared to larger models like Claude 3.5 Sonnet for bolt.diy/bolt.new?
22
 
23
  As much as the gap is quickly closing between open source and massive close source models, you’re still going to get the best results with the very large models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b. This is one of the big tasks we have at hand - figuring out how to prompt better, use agents, and improve the platform as a whole to make it work better for even the smaller local LLMs!
24
 
25
  ### I'm getting the error: "There was an error processing this request"
26
 
27
+ If you see this error within bolt.diy, that is just the application telling you there is a problem at a high level, and this could mean a number of different things. To find the actual error, please check BOTH the terminal where you started the application (with Docker or pnpm) and the developer console in the browser. For most browsers, you can access the developer console by pressing F12 or right clicking anywhere in the browser and selecting “Inspect”. Then go to the “console” tab in the top right.
28
 
29
  ### I'm getting the error: "x-api-key header missing"
30
 
31
+ We have seen this error a couple times and for some reason just restarting the Docker container has fixed it. This seems to be Ollama specific. Another thing to try is try to run bolt.diy with Docker or pnpm, whichever you didn’t run first. We are still on the hunt for why this happens once and a while!
32
 
33
+ ### I'm getting a blank preview when bolt.diy runs my app!
34
 
35
+ We promise you that we are constantly testing new PRs coming into bolt.diy and the preview is core functionality, so the application is not broken! When you get a blank preview or don’t get a preview, this is generally because the LLM hallucinated bad code or incorrect commands. We are working on making this more transparent so it is obvious. Sometimes the error will appear in developer console too so check that as well.
36
 
37
  ### How to add a LLM:
38
 
39
+ To make new LLMs available to use in this version of bolt.new, head on over to `app/utils/constants.ts` and find the constant MODEL_LIST. Each element in this array is an object that has the model ID for the name (get this from the provider's API documentation), a label for the frontend model dropdown, and the provider.
40
 
41
  By default, Anthropic, OpenAI, Groq, and Ollama are implemented as providers, but the YouTube video for this repo covers how to extend this to work with more providers if you wish!
42
 
README.md CHANGED
@@ -1,12 +1,14 @@
1
- [![Bolt.new: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.new)
2
 
3
- # Bolt.new Fork by Cole Medin - oTToDev
4
 
5
- This fork of Bolt.new (oTToDev) allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, LMStudio, Mistral, xAI, HuggingFace, DeepSeek, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.
6
 
7
- Check the [oTToDev Docs](https://coleam00.github.io/bolt.new-any-llm/) for more information.
8
 
9
- ## Join the community for oTToDev!
 
 
10
 
11
  https://thinktank.ottomator.ai
12
 
@@ -18,7 +20,7 @@ https://thinktank.ottomator.ai
18
  - ✅ Autogenerate Ollama models from what is downloaded (@yunatamos)
19
  - ✅ Filter models by provider (@jasonm23)
20
  - ✅ Download project as ZIP (@fabwaseem)
21
- - ✅ Improvements to the main Bolt.new prompt in `app\lib\.server\llm\prompts.ts` (@kofi-bhr)
22
  - ✅ DeepSeek API Integration (@zenith110)
23
  - ✅ Mistral API Integration (@ArulGandhi)
24
  - ✅ "Open AI Like" API Integration (@ZerxZ)
@@ -41,8 +43,12 @@ https://thinktank.ottomator.ai
41
  - ✅ Mobile friendly (@qwikode)
42
  - ✅ Better prompt enhancing (@SujalXplores)
43
  - ✅ Attach images to prompts (@atrokhym)
44
- - ✅ Detect package.json and commands to auto install and run preview for folder and git import (@wonderwhy-er)
45
- - **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
 
 
 
 
46
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
47
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
48
  - ⬜ Deploy directly to Vercel/Netlify/other similar platforms
@@ -54,187 +60,180 @@ https://thinktank.ottomator.ai
54
  - ⬜ Perplexity Integration
55
  - ⬜ Vertex AI Integration
56
 
57
- ## Bolt.new: AI-Powered Full-Stack Web Development in the Browser
58
-
59
- Bolt.new is an AI-powered web development agent that allows you to prompt, run, edit, and deploy full-stack applications directly from your browser—no local setup required. If you're here to build your own AI-powered web dev agent using the Bolt open source codebase, [click here to get started!](./CONTRIBUTING.md)
60
-
61
- ## What Makes Bolt.new Different
62
-
63
- Claude, v0, etc are incredible- but you can't install packages, run backends, or edit code. That’s where Bolt.new stands out:
64
-
65
- - **Full-Stack in the Browser**: Bolt.new integrates cutting-edge AI models with an in-browser development environment powered by **StackBlitz’s WebContainers**. This allows you to:
66
- - Install and run npm tools and libraries (like Vite, Next.js, and more)
67
- - Run Node.js servers
68
- - Interact with third-party APIs
69
- - Deploy to production from chat
70
- - Share your work via a URL
71
-
72
- - **AI with Environment Control**: Unlike traditional dev environments where the AI can only assist in code generation, Bolt.new gives AI models **complete control** over the entire environment including the filesystem, node server, package manager, terminal, and browser console. This empowers AI agents to handle the whole app lifecycle—from creation to deployment.
73
-
74
- Whether you’re an experienced developer, a PM, or a designer, Bolt.new allows you to easily build production-grade full-stack applications.
75
 
76
- For developers interested in building their own AI-powered development tools with WebContainers, check out the open-source Bolt codebase in this repo!
 
 
 
 
 
 
77
 
78
- ## Setup
79
 
80
- Many of you are new users to installing software from Github. If you have any installation troubles reach out and submit an "issue" using the links above, or feel free to enhance this documentation by forking, editing the instructions, and doing a pull request.
81
 
82
- 1. Install Git from https://git-scm.com/downloads
83
 
84
- 2. Install Node.js from https://nodejs.org/en/download/
 
85
 
86
- Pay attention to the installer notes after completion.
 
 
 
 
 
 
87
 
88
- On all operating systems, the path to Node.js should automatically be added to your system path. But you can check your path if you want to be sure. On Windows, you can search for "edit the system environment variables" in your system, select "Environment Variables..." once you are in the system properties, and then check for a path to Node in your "Path" system variable. On a Mac or Linux machine, it will tell you to check if /usr/local/bin is in your $PATH. To determine if usr/local/bin is included in $PATH open your Terminal and run:
89
 
90
- ```
91
- echo $PATH .
92
- ```
93
 
94
- If you see usr/local/bin in the output then you're good to go.
 
 
95
 
96
- 3. Clone the repository (if you haven't already) by opening a Terminal window (or CMD with admin permissions) and then typing in this:
97
 
98
- ```
99
- git clone https://github.com/coleam00/bolt.new-any-llm.git
100
- ```
101
 
102
- 3. Rename .env.example to .env.local and add your LLM API keys. You will find this file on a Mac at "[your name]/bold.new-any-llm/.env.example". For Windows and Linux the path will be similar.
 
103
 
104
- ![image](https://github.com/user-attachments/assets/7e6a532c-2268-401f-8310-e8d20c731328)
 
 
 
 
105
 
106
- If you can't see the file indicated above, its likely you can't view hidden files. On Mac, open a Terminal window and enter this command below. On Windows, you will see the hidden files option in File Explorer Settings. A quick Google search will help you if you are stuck here.
107
 
108
- ```
109
- defaults write com.apple.finder AppleShowAllFiles YES
110
- ```
111
 
112
- **NOTE**: you only have to set the ones you want to use and Ollama doesn't need an API key because it runs locally on your computer:
 
 
113
 
114
- Get your GROQ API Key here: https://console.groq.com/keys
 
 
 
115
 
116
- Get your Open AI API Key by following these instructions: https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
117
 
118
- Get your Anthropic API Key in your account settings: https://console.anthropic.com/settings/keys
119
 
120
- ```
121
- GROQ_API_KEY=XXX
122
- OPENAI_API_KEY=XXX
123
- ANTHROPIC_API_KEY=XXX
124
- ```
125
 
126
- Optionally, you can set the debug level:
127
 
128
- ```
129
- VITE_LOG_LEVEL=debug
130
- ```
 
 
 
 
 
131
 
132
- And if using Ollama set the DEFAULT_NUM_CTX, the example below uses 8K context and ollama running on localhost port 11434:
 
 
 
 
133
 
134
- ```
135
- OLLAMA_API_BASE_URL=http://localhost:11434
136
- DEFAULT_NUM_CTX=8192
137
- ```
138
 
139
- **Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
 
140
 
141
- ## Run with Docker
142
 
143
- Prerequisites:
144
 
145
- Git and Node.js as mentioned above, as well as Docker: https://www.docker.com/
 
 
 
 
146
 
147
- ### 1a. Using Helper Scripts
 
 
 
 
148
 
149
- NPM scripts are provided for convenient building:
 
 
 
 
 
150
 
151
- ```bash
152
- # Development build
153
- npm run dockerbuild
154
 
155
- # Production build
156
- npm run dockerbuild:prod
157
- ```
158
 
159
- ### 1b. Direct Docker Build Commands (alternative to using NPM scripts)
160
 
161
- You can use Docker's target feature to specify the build environment instead of using NPM scripts if you wish:
162
 
163
- ```bash
164
- # Development build
165
- docker build . --target bolt-ai-development
166
 
167
- # Production build
168
- docker build . --target bolt-ai-production
169
- ```
170
 
171
- ### 2. Docker Compose with Profiles to Run the Container
 
 
172
 
173
- Use Docker Compose profiles to manage different environments:
 
174
 
175
- ```bash
176
- # Development environment
177
- docker-compose --profile development up
178
 
179
- # Production environment
180
- docker-compose --profile production up
181
- ```
182
 
183
- When you run the Docker Compose command with the development profile, any changes you
184
- make on your machine to the code will automatically be reflected in the site running
185
- on the container (i.e. hot reloading still applies!).
186
 
187
- ## Run Without Docker
188
 
189
- 1. Install dependencies using Terminal (or CMD in Windows with admin permissions):
190
 
191
- ```
192
- pnpm install
193
- ```
194
-
195
- If you get an error saying "command not found: pnpm" or similar, then that means pnpm isn't installed. You can install it via this:
196
-
197
- ```
198
- sudo npm install -g pnpm
199
- ```
200
-
201
- 2. Start the application with the command:
202
-
203
- ```bash
204
- pnpm run dev
205
- ```
206
  ## Available Scripts
207
 
208
- - `pnpm run dev`: Starts the development server.
209
- - `pnpm run build`: Builds the project.
210
- - `pnpm run start`: Runs the built application locally using Wrangler Pages. This script uses `bindings.sh` to set up necessary bindings so you don't have to duplicate environment variables.
211
- - `pnpm run preview`: Builds the project and then starts it locally, useful for testing the production build. Note, HTTP streaming currently doesn't work as expected with `wrangler pages dev`.
212
- - `pnpm test`: Runs the test suite using Vitest.
213
- - `pnpm run typecheck`: Runs TypeScript type checking.
214
- - `pnpm run typegen`: Generates TypeScript types using Wrangler.
215
- - `pnpm run deploy`: Builds the project and deploys it to Cloudflare Pages.
216
- - `pnpm run lint:fix`: Runs the linter and automatically fixes issues according to your ESLint configuration.
217
-
218
- ## Development
219
-
220
- To start the development server:
221
 
222
- ```bash
223
- pnpm run dev
224
- ```
225
 
226
- This will start the Remix Vite development server. You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway.
227
 
228
- ## How do I contribute to oTToDev?
229
 
230
- [Please check out our dedicated page for contributing to oTToDev here!](CONTRIBUTING.md)
231
 
232
- ## What are the future plans for oTToDev?
233
 
234
- [Check out our Roadmap here!](https://roadmap.sh/r/ottodev-roadmap-2ovzo)
235
 
236
- Lot more updates to this roadmap coming soon!
237
 
238
  ## FAQ
239
 
240
- [Please check out our dedicated page for FAQ's related to oTToDev here!](FAQ.md)
 
1
+ [![bolt.diy: AI-Powered Full-Stack Web Development in the Browser](./public/social_preview_index.jpg)](https://bolt.diy)
2
 
3
+ # bolt.diy (Previously oTToDev)
4
 
5
+ Welcome to bolt.diy, the official open source version of Bolt.new (previously known as oTToDev and bolt.new ANY LLM), which allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, LMStudio, Mistral, xAI, HuggingFace, DeepSeek, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.
6
 
7
+ Check the [bolt.diy Docs](https://stackblitz-labs.github.io/bolt.diy/) for more information. This documentation is still being updated after the transfer.
8
 
9
+ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMedin) but has quickly grown into a massive community effort to build the BEST open source AI coding assistant!
10
+
11
+ ## Join the community for bolt.diy!
12
 
13
  https://thinktank.ottomator.ai
14
 
 
20
  - ✅ Autogenerate Ollama models from what is downloaded (@yunatamos)
21
  - ✅ Filter models by provider (@jasonm23)
22
  - ✅ Download project as ZIP (@fabwaseem)
23
+ - ✅ Improvements to the main bolt.new prompt in `app\lib\.server\llm\prompts.ts` (@kofi-bhr)
24
  - ✅ DeepSeek API Integration (@zenith110)
25
  - ✅ Mistral API Integration (@ArulGandhi)
26
  - ✅ "Open AI Like" API Integration (@ZerxZ)
 
43
  - ✅ Mobile friendly (@qwikode)
44
  - ✅ Better prompt enhancing (@SujalXplores)
45
  - ✅ Attach images to prompts (@atrokhym)
46
+ - ✅ Added Git Clone button (@thecodacus)
47
+ - Git Import from url (@thecodacus)
48
+ - ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus)
49
+ - ✅ Detect package.json and commands to auto install & run preview for folder and git import (@wonderwhy-er)
50
+ - ✅ Selection tool to target changes visually (@emcconnell)
51
+ - ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
52
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
53
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
54
  - ⬜ Deploy directly to Vercel/Netlify/other similar platforms
 
60
  - ⬜ Perplexity Integration
61
  - ⬜ Vertex AI Integration
62
 
63
+ ## bolt.diy Features
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ - **AI-powered full-stack web development** directly in your browser.
66
+ - **Support for multiple LLMs** with an extensible architecture to integrate additional models.
67
+ - **Attach images to prompts** for better contextual understanding.
68
+ - **Integrated terminal** to view output of LLM-run commands.
69
+ - **Revert code to earlier versions** for easier debugging and quicker changes.
70
+ - **Download projects as ZIP** for easy portability.
71
+ - **Integration-ready Docker support** for a hassle-free setup.
72
 
73
+ ## Setup bolt.diy
74
 
75
+ If you're new to installing software from GitHub, don't worry! If you encounter any issues, feel free to submit an "issue" using the provided links or improve this documentation by forking the repository, editing the instructions, and submitting a pull request. The following instruction will help you get the stable branch up and running on your local machine in no time.
76
 
77
+ ### Prerequisites
78
 
79
+ 1. **Install Git**: [Download Git](https://git-scm.com/downloads)
80
+ 2. **Install Node.js**: [Download Node.js](https://nodejs.org/en/download/)
81
 
82
+ - After installation, the Node.js path is usually added to your system automatically. To verify:
83
+ - **Windows**: Search for "Edit the system environment variables," click "Environment Variables," and check if `Node.js` is in the `Path` variable.
84
+ - **Mac/Linux**: Open a terminal and run:
85
+ ```bash
86
+ echo $PATH
87
+ ```
88
+ Look for `/usr/local/bin` in the output.
89
 
90
+ ### Clone the Repository
91
 
92
+ Clone the repository using Git:
 
 
93
 
94
+ ```bash
95
+ git clone -b stable https://github.com/stackblitz-labs/bolt.diy
96
+ ```
97
 
98
+ ### (Optional) Configure Environment Variables
99
 
100
+ Most environment variables can be configured directly through the settings menu of the application. However, if you need to manually configure them:
 
 
101
 
102
+ 1. Rename `.env.example` to `.env.local`.
103
+ 2. Add your LLM API keys. For example:
104
 
105
+ ```env
106
+ GROQ_API_KEY=YOUR_GROQ_API_KEY
107
+ OPENAI_API_KEY=YOUR_OPENAI_API_KEY
108
+ ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
109
+ ```
110
 
111
+ **Note**: Ollama does not require an API key as it runs locally.
112
 
113
+ 3. Optionally, set additional configurations:
 
 
114
 
115
+ ```env
116
+ # Debugging
117
+ VITE_LOG_LEVEL=debug
118
 
119
+ # Ollama settings (example: 8K context, localhost port 11434)
120
+ OLLAMA_API_BASE_URL=http://localhost:11434
121
+ DEFAULT_NUM_CTX=8192
122
+ ```
123
 
124
+ **Important**: Do not commit your `.env.local` file to version control. This file is already included in `.gitignore`.
125
 
126
+ ---
127
 
128
+ ## Run the Application
 
 
 
 
129
 
130
+ ### Option 1: Without Docker
131
 
132
+ 1. **Install Dependencies**:
133
+ ```bash
134
+ pnpm install
135
+ ```
136
+ If `pnpm` is not installed, install it using:
137
+ ```bash
138
+ sudo npm install -g pnpm
139
+ ```
140
 
141
+ 2. **Start the Application**:
142
+ ```bash
143
+ pnpm run dev
144
+ ```
145
+ This will start the Remix Vite development server. You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway.
146
 
147
+ ### Option 2: With Docker
 
 
 
148
 
149
+ #### Prerequisites
150
+ - Ensure Git, Node.js, and Docker are installed: [Download Docker](https://www.docker.com/)
151
 
152
+ #### Steps
153
 
154
+ 1. **Build the Docker Image**:
155
 
156
+ Use the provided NPM scripts:
157
+ ```bash
158
+ npm run dockerbuild # Development build
159
+ npm run dockerbuild:prod # Production build
160
+ ```
161
 
162
+ Alternatively, use Docker commands directly:
163
+ ```bash
164
+ docker build . --target bolt-ai-development # Development build
165
+ docker build . --target bolt-ai-production # Production build
166
+ ```
167
 
168
+ 2. **Run the Container**:
169
+ Use Docker Compose profiles to manage environments:
170
+ ```bash
171
+ docker-compose --profile development up # Development
172
+ docker-compose --profile production up # Production
173
+ ```
174
 
175
+ - With the development profile, changes to your code will automatically reflect in the running container (hot reloading).
 
 
176
 
177
+ ---
 
 
178
 
179
+ ### Update Your Local Version to the Latest
180
 
181
+ To keep your local version of bolt.diy up to date with the latest changes, follow these steps for your operating system:
182
 
183
+ #### 1. **Navigate to your project folder**
184
+ Navigate to the directory where you cloned the repository and open a terminal:
 
185
 
186
+ #### 2. **Fetch the Latest Changes**
187
+ Use Git to pull the latest changes from the main repository:
 
188
 
189
+ ```bash
190
+ git pull origin main
191
+ ```
192
 
193
+ #### 3. **Update Dependencies**
194
+ After pulling the latest changes, update the project dependencies by running the following command:
195
 
196
+ ```bash
197
+ pnpm install
198
+ ```
199
 
200
+ #### 4. **Run the Application**
201
+ Once the updates are complete, you can start the application again with:
 
202
 
203
+ ```bash
204
+ pnpm run dev
205
+ ```
206
 
207
+ This ensures that you're running the latest version of bolt.diy and can take advantage of all the newest features and bug fixes.
208
 
209
+ ---
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  ## Available Scripts
212
 
213
+ - **`pnpm run dev`**: Starts the development server.
214
+ - **`pnpm run build`**: Builds the project.
215
+ - **`pnpm run start`**: Runs the built application locally using Wrangler Pages.
216
+ - **`pnpm run preview`**: Builds and runs the production build locally.
217
+ - **`pnpm test`**: Runs the test suite using Vitest.
218
+ - **`pnpm run typecheck`**: Runs TypeScript type checking.
219
+ - **`pnpm run typegen`**: Generates TypeScript types using Wrangler.
220
+ - **`pnpm run deploy`**: Deploys the project to Cloudflare Pages.
221
+ - **`pnpm run lint:fix`**: Automatically fixes linting issues.
 
 
 
 
222
 
223
+ ---
 
 
224
 
225
+ ## Contributing
226
 
227
+ We welcome contributions! Check out our [Contributing Guide](CONTRIBUTING.md) to get started.
228
 
229
+ ---
230
 
231
+ ## Roadmap
232
 
233
+ Explore upcoming features and priorities on our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo).
234
 
235
+ ---
236
 
237
  ## FAQ
238
 
239
+ For answers to common questions, visit our [FAQ Page](FAQ.md).
app/commit.json CHANGED
@@ -1 +1 @@
1
- { "commit": "db8c65ec2ba2f28382cb5e792a3f7495fb9a8e03" }
 
1
+ { "commit": "b304749b21f340e03c94abc0cc91ccf82f559195" }
app/components/chat/AssistantMessage.tsx CHANGED
@@ -1,13 +1,30 @@
1
  import { memo } from 'react';
2
  import { Markdown } from './Markdown';
 
3
 
4
  interface AssistantMessageProps {
5
  content: string;
 
6
  }
7
 
8
- export const AssistantMessage = memo(({ content }: AssistantMessageProps) => {
 
 
 
 
 
 
 
 
 
 
9
  return (
10
  <div className="overflow-hidden w-full">
 
 
 
 
 
11
  <Markdown html>{content}</Markdown>
12
  </div>
13
  );
 
1
  import { memo } from 'react';
2
  import { Markdown } from './Markdown';
3
+ import type { JSONValue } from 'ai';
4
 
5
  interface AssistantMessageProps {
6
  content: string;
7
+ annotations?: JSONValue[];
8
  }
9
 
10
+ export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
11
+ const filteredAnnotations = (annotations?.filter(
12
+ (annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
13
+ ) || []) as { type: string; value: any }[];
14
+
15
+ const usage: {
16
+ completionTokens: number;
17
+ promptTokens: number;
18
+ totalTokens: number;
19
+ } = filteredAnnotations.find((annotation) => annotation.type === 'usage')?.value;
20
+
21
  return (
22
  <div className="overflow-hidden w-full">
23
+ {usage && (
24
+ <div className="text-sm text-bolt-elements-textSecondary mb-2">
25
+ Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
26
+ </div>
27
+ )}
28
  <Markdown html>{content}</Markdown>
29
  </div>
30
  );
app/components/chat/BaseChat.module.scss CHANGED
@@ -18,82 +18,6 @@
18
  opacity: 1;
19
  }
20
 
21
- .RayContainer {
22
- --gradient-opacity: 0.85;
23
- --ray-gradient: radial-gradient(rgba(83, 196, 255, var(--gradient-opacity)) 0%, rgba(43, 166, 255, 0) 100%);
24
- transition: opacity 0.25s linear;
25
- position: fixed;
26
- inset: 0;
27
- pointer-events: none;
28
- user-select: none;
29
- }
30
-
31
- .LightRayOne {
32
- width: 480px;
33
- height: 680px;
34
- transform: rotate(80deg);
35
- top: -540px;
36
- left: 250px;
37
- filter: blur(110px);
38
- position: absolute;
39
- border-radius: 100%;
40
- background: var(--ray-gradient);
41
- }
42
-
43
- .LightRayTwo {
44
- width: 110px;
45
- height: 400px;
46
- transform: rotate(-20deg);
47
- top: -280px;
48
- left: 350px;
49
- mix-blend-mode: overlay;
50
- opacity: 0.6;
51
- filter: blur(60px);
52
- position: absolute;
53
- border-radius: 100%;
54
- background: var(--ray-gradient);
55
- }
56
-
57
- .LightRayThree {
58
- width: 400px;
59
- height: 370px;
60
- top: -350px;
61
- left: 200px;
62
- mix-blend-mode: overlay;
63
- opacity: 0.6;
64
- filter: blur(21px);
65
- position: absolute;
66
- border-radius: 100%;
67
- background: var(--ray-gradient);
68
- }
69
-
70
- .LightRayFour {
71
- position: absolute;
72
- width: 330px;
73
- height: 370px;
74
- top: -330px;
75
- left: 50px;
76
- mix-blend-mode: overlay;
77
- opacity: 0.5;
78
- filter: blur(21px);
79
- border-radius: 100%;
80
- background: var(--ray-gradient);
81
- }
82
-
83
- .LightRayFive {
84
- position: absolute;
85
- width: 110px;
86
- height: 400px;
87
- transform: rotate(-40deg);
88
- top: -280px;
89
- left: -10px;
90
- mix-blend-mode: overlay;
91
- opacity: 0.8;
92
- filter: blur(60px);
93
- border-radius: 100%;
94
- background: var(--ray-gradient);
95
- }
96
-
97
  .PromptEffectContainer {
98
  --prompt-container-offset: 50px;
99
  --prompt-line-stroke-width: 1px;
 
18
  opacity: 1;
19
  }
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  .PromptEffectContainer {
22
  --prompt-container-offset: 50px;
23
  --prompt-line-stroke-width: 1px;
app/components/chat/BaseChat.tsx CHANGED
@@ -17,7 +17,6 @@ import Cookies from 'js-cookie';
17
  import * as Tooltip from '@radix-ui/react-tooltip';
18
 
19
  import styles from './BaseChat.module.scss';
20
- import type { ProviderInfo } from '~/utils/types';
21
  import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
22
  import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
23
  import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
@@ -26,6 +25,9 @@ import GitCloneButton from './GitCloneButton';
26
  import FilePreview from './FilePreview';
27
  import { ModelSelector } from '~/components/chat/ModelSelector';
28
  import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
 
 
 
29
 
30
  const TEXTAREA_MIN_HEIGHT = 76;
31
 
@@ -45,6 +47,7 @@ interface BaseChatProps {
45
  setModel?: (model: string) => void;
46
  provider?: ProviderInfo;
47
  setProvider?: (provider: ProviderInfo) => void;
 
48
  handleStop?: () => void;
49
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
50
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
@@ -70,10 +73,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
70
  setModel,
71
  provider,
72
  setProvider,
 
73
  input = '',
74
  enhancingPrompt,
75
  handleInputChange,
76
- promptEnhanced,
 
77
  enhancePrompt,
78
  sendMessage,
79
  handleStop,
@@ -108,46 +113,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
108
  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
109
  const [transcript, setTranscript] = useState('');
110
 
111
- // Load enabled providers from cookies
112
- const [enabledProviders, setEnabledProviders] = useState(() => {
113
- const savedProviders = Cookies.get('providers');
114
-
115
- if (savedProviders) {
116
- try {
117
- const parsedProviders = JSON.parse(savedProviders);
118
- return PROVIDER_LIST.filter((p) => parsedProviders[p.name]);
119
- } catch (error) {
120
- console.error('Failed to parse providers from cookies:', error);
121
- return PROVIDER_LIST;
122
- }
123
- }
124
-
125
- return PROVIDER_LIST;
126
- });
127
-
128
- // Update enabled providers when cookies change
129
  useEffect(() => {
130
- const updateProvidersFromCookies = () => {
131
- const savedProviders = Cookies.get('providers');
132
-
133
- if (savedProviders) {
134
- try {
135
- const parsedProviders = JSON.parse(savedProviders);
136
- setEnabledProviders(PROVIDER_LIST.filter((p) => parsedProviders[p.name]));
137
- } catch (error) {
138
- console.error('Failed to parse providers from cookies:', error);
139
- }
140
- }
141
- };
142
-
143
- updateProvidersFromCookies();
144
 
145
- const interval = setInterval(updateProvidersFromCookies, 1000);
146
-
147
- return () => clearInterval(interval);
148
- }, [PROVIDER_LIST]);
149
-
150
- console.log(transcript);
151
  useEffect(() => {
152
  // Load API keys from cookies on component mount
153
  try {
@@ -167,7 +136,26 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
167
  Cookies.remove('apiKeys');
168
  }
169
 
170
- initializeModelList().then((modelList) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  setModelList(modelList);
172
  });
173
 
@@ -291,24 +279,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
291
  const baseChat = (
292
  <div
293
  ref={ref}
294
- className={classNames(
295
- styles.BaseChat,
296
- 'relative flex flex-col lg:flex-row h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
297
- )}
298
  data-chat-visible={showChat}
299
  >
300
- <div className={classNames(styles.RayContainer)}>
301
- <div className={classNames(styles.LightRayOne)}></div>
302
- <div className={classNames(styles.LightRayTwo)}></div>
303
- <div className={classNames(styles.LightRayThree)}></div>
304
- <div className={classNames(styles.LightRayFour)}></div>
305
- <div className={classNames(styles.LightRayFive)}></div>
306
- </div>
307
  <ClientOnly>{() => <Menu />}</ClientOnly>
308
  <div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
309
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
310
  {!chatStarted && (
311
- <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center px-4 lg:px-0">
312
  <h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
313
  Where ideas begin
314
  </h1>
@@ -353,15 +331,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
353
  gradientUnits="userSpaceOnUse"
354
  gradientTransform="rotate(-45)"
355
  >
356
- <stop offset="0%" stopColor="#1488fc" stopOpacity="0%"></stop>
357
- <stop offset="40%" stopColor="#1488fc" stopOpacity="80%"></stop>
358
- <stop offset="50%" stopColor="#1488fc" stopOpacity="80%"></stop>
359
- <stop offset="100%" stopColor="#1488fc" stopOpacity="0%"></stop>
360
  </linearGradient>
361
  <linearGradient id="shine-gradient">
362
  <stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
363
- <stop offset="40%" stopColor="#8adaff" stopOpacity="80%"></stop>
364
- <stop offset="50%" stopColor="#8adaff" stopOpacity="80%"></stop>
365
  <stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
366
  </linearGradient>
367
  </defs>
@@ -377,10 +355,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
377
  modelList={modelList}
378
  provider={provider}
379
  setProvider={setProvider}
380
- providerList={PROVIDER_LIST}
381
  apiKeys={apiKeys}
382
  />
383
- {enabledProviders.length > 0 && provider && (
384
  <APIKeyManager
385
  provider={provider}
386
  apiKey={apiKeys[provider.name] || ''}
@@ -401,6 +379,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
401
  setImageDataList?.(imageDataList.filter((_, i) => i !== index));
402
  }}
403
  />
 
 
 
 
 
 
 
 
 
 
404
  <div
405
  className={classNames(
406
  'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
@@ -409,7 +397,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
409
  <textarea
410
  ref={textareaRef}
411
  className={classNames(
412
- 'w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
413
  'transition-all duration-200',
414
  'hover:border-bolt-elements-focus',
415
  )}
@@ -456,6 +444,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
456
  return;
457
  }
458
 
 
 
 
 
 
459
  handleSendMessage?.(event);
460
  }
461
  }}
@@ -476,7 +469,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
476
  <SendButton
477
  show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
478
  isStreaming={isStreaming}
479
- disabled={enabledProviders.length === 0}
480
  onClick={(event) => {
481
  if (isStreaming) {
482
  handleStop?.();
@@ -498,25 +491,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
498
  <IconButton
499
  title="Enhance prompt"
500
  disabled={input.length === 0 || enhancingPrompt}
501
- className={classNames(
502
- 'transition-all',
503
- enhancingPrompt ? 'opacity-100' : '',
504
- promptEnhanced ? 'text-bolt-elements-item-contentAccent' : '',
505
- promptEnhanced ? 'pr-1.5' : '',
506
- promptEnhanced ? 'enabled:hover:bg-bolt-elements-item-backgroundAccent' : '',
507
- )}
508
- onClick={() => enhancePrompt?.()}
509
  >
510
  {enhancingPrompt ? (
511
- <>
512
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
513
- <div className="ml-1.5">Enhancing prompt...</div>
514
- </>
515
  ) : (
516
- <>
517
- <div className="i-bolt:stars text-xl"></div>
518
- {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
519
- </>
520
  )}
521
  </IconButton>
522
 
@@ -536,7 +520,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
536
  !isModelSettingsCollapsed,
537
  })}
538
  onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}
539
- disabled={enabledProviders.length === 0}
540
  >
541
  <div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
542
  {isModelSettingsCollapsed ? <span className="text-xs">{model}</span> : <span />}
 
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';
 
25
  import FilePreview from './FilePreview';
26
  import { ModelSelector } from '~/components/chat/ModelSelector';
27
  import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
28
+ import type { IProviderSetting, ProviderInfo } from '~/types/model';
29
+ import { ScreenshotStateManager } from './ScreenshotStateManager';
30
+ import { toast } from 'react-toastify';
31
 
32
  const TEXTAREA_MIN_HEIGHT = 76;
33
 
 
47
  setModel?: (model: string) => void;
48
  provider?: ProviderInfo;
49
  setProvider?: (provider: ProviderInfo) => void;
50
+ providerList?: ProviderInfo[];
51
  handleStop?: () => void;
52
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
53
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
 
73
  setModel,
74
  provider,
75
  setProvider,
76
+ providerList,
77
  input = '',
78
  enhancingPrompt,
79
  handleInputChange,
80
+
81
+ // promptEnhanced,
82
  enhancePrompt,
83
  sendMessage,
84
  handleStop,
 
113
  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
114
  const [transcript, setTranscript] = useState('');
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  useEffect(() => {
117
+ console.log(transcript);
118
+ }, [transcript]);
 
 
 
 
 
 
 
 
 
 
 
 
119
 
 
 
 
 
 
 
120
  useEffect(() => {
121
  // Load API keys from cookies on component mount
122
  try {
 
136
  Cookies.remove('apiKeys');
137
  }
138
 
139
+ let providerSettings: Record<string, IProviderSetting> | undefined = undefined;
140
+
141
+ try {
142
+ const savedProviderSettings = Cookies.get('providers');
143
+
144
+ if (savedProviderSettings) {
145
+ const parsedProviderSettings = JSON.parse(savedProviderSettings);
146
+
147
+ if (typeof parsedProviderSettings === 'object' && parsedProviderSettings !== null) {
148
+ providerSettings = parsedProviderSettings;
149
+ }
150
+ }
151
+ } catch (error) {
152
+ console.error('Error loading Provider Settings from cookies:', error);
153
+
154
+ // Clear invalid cookie data
155
+ Cookies.remove('providers');
156
+ }
157
+
158
+ initializeModelList(providerSettings).then((modelList) => {
159
  setModelList(modelList);
160
  });
161
 
 
279
  const baseChat = (
280
  <div
281
  ref={ref}
282
+ className={classNames(styles.BaseChat, 'relative flex h-full w-full overflow-hidden')}
 
 
 
283
  data-chat-visible={showChat}
284
  >
 
 
 
 
 
 
 
285
  <ClientOnly>{() => <Menu />}</ClientOnly>
286
  <div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
287
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
288
  {!chatStarted && (
289
+ <div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">
290
  <h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
291
  Where ideas begin
292
  </h1>
 
331
  gradientUnits="userSpaceOnUse"
332
  gradientTransform="rotate(-45)"
333
  >
334
+ <stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
335
+ <stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
336
+ <stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
337
+ <stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
338
  </linearGradient>
339
  <linearGradient id="shine-gradient">
340
  <stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
341
+ <stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
342
+ <stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
343
  <stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
344
  </linearGradient>
345
  </defs>
 
355
  modelList={modelList}
356
  provider={provider}
357
  setProvider={setProvider}
358
+ providerList={providerList || PROVIDER_LIST}
359
  apiKeys={apiKeys}
360
  />
361
+ {(providerList || []).length > 0 && provider && (
362
  <APIKeyManager
363
  provider={provider}
364
  apiKey={apiKeys[provider.name] || ''}
 
379
  setImageDataList?.(imageDataList.filter((_, i) => i !== index));
380
  }}
381
  />
382
+ <ClientOnly>
383
+ {() => (
384
+ <ScreenshotStateManager
385
+ setUploadedFiles={setUploadedFiles}
386
+ setImageDataList={setImageDataList}
387
+ uploadedFiles={uploadedFiles}
388
+ imageDataList={imageDataList}
389
+ />
390
+ )}
391
+ </ClientOnly>
392
  <div
393
  className={classNames(
394
  'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
 
397
  <textarea
398
  ref={textareaRef}
399
  className={classNames(
400
+ 'w-full pl-4 pt-4 pr-16 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
401
  'transition-all duration-200',
402
  'hover:border-bolt-elements-focus',
403
  )}
 
444
  return;
445
  }
446
 
447
+ // ignore if using input method engine
448
+ if (event.nativeEvent.isComposing) {
449
+ return;
450
+ }
451
+
452
  handleSendMessage?.(event);
453
  }
454
  }}
 
469
  <SendButton
470
  show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
471
  isStreaming={isStreaming}
472
+ disabled={!providerList || providerList.length === 0}
473
  onClick={(event) => {
474
  if (isStreaming) {
475
  handleStop?.();
 
491
  <IconButton
492
  title="Enhance prompt"
493
  disabled={input.length === 0 || enhancingPrompt}
494
+ className={classNames('transition-all', enhancingPrompt ? 'opacity-100' : '')}
495
+ onClick={() => {
496
+ enhancePrompt?.();
497
+ toast.success('Prompt enhanced!');
498
+ }}
 
 
 
499
  >
500
  {enhancingPrompt ? (
501
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
 
 
 
502
  ) : (
503
+ <div className="i-bolt:stars text-xl"></div>
 
 
 
504
  )}
505
  </IconButton>
506
 
 
520
  !isModelSettingsCollapsed,
521
  })}
522
  onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}
523
+ disabled={!providerList || providerList.length === 0}
524
  >
525
  <div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
526
  {isModelSettingsCollapsed ? <span className="text-xs">{model}</span> : <span />}
app/components/chat/Chat.client.tsx CHANGED
@@ -17,8 +17,9 @@ 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 type { ProviderInfo } from '~/utils/types';
21
  import { debounce } from '~/utils/debounce';
 
 
22
 
23
  const toastAnimation = cssTransition({
24
  enter: 'animated fadeInRight',
@@ -91,6 +92,8 @@ export const ChatImpl = memo(
91
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
92
  const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
93
  const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
 
 
94
 
95
  const [model, setModel] = useState(() => {
96
  const savedModel = Cookies.get('selectedModel');
@@ -111,14 +114,25 @@ export const ChatImpl = memo(
111
  api: '/api/chat',
112
  body: {
113
  apiKeys,
 
 
114
  },
 
115
  onError: (error) => {
116
  logger.error('Request failed\n\n', error);
117
  toast.error(
118
  'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
119
  );
120
  },
121
- onFinish: () => {
 
 
 
 
 
 
 
 
122
  logger.debug('Finished streaming');
123
  },
124
  initialMessages,
@@ -316,6 +330,7 @@ export const ChatImpl = memo(
316
  setModel={handleModelChange}
317
  provider={provider}
318
  setProvider={handleProviderChange}
 
319
  messageRef={messageRef}
320
  scrollRef={scrollRef}
321
  handleInputChange={(e) => {
 
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
 
24
  const toastAnimation = cssTransition({
25
  enter: 'animated fadeInRight',
 
92
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
93
  const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
94
  const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
95
+ const files = useStore(workbenchStore.files);
96
+ const { activeProviders, promptId } = useSettings();
97
 
98
  const [model, setModel] = useState(() => {
99
  const savedModel = Cookies.get('selectedModel');
 
114
  api: '/api/chat',
115
  body: {
116
  apiKeys,
117
+ files,
118
+ promptId,
119
  },
120
+ sendExtraMessageFields: true,
121
  onError: (error) => {
122
  logger.error('Request failed\n\n', error);
123
  toast.error(
124
  'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
125
  );
126
  },
127
+ onFinish: (message, response) => {
128
+ const usage = response.usage;
129
+
130
+ if (usage) {
131
+ console.log('Token usage:', usage);
132
+
133
+ // You can now use the usage data as needed
134
+ }
135
+
136
  logger.debug('Finished streaming');
137
  },
138
  initialMessages,
 
330
  setModel={handleModelChange}
331
  provider={provider}
332
  setProvider={handleProviderChange}
333
+ providerList={activeProviders}
334
  messageRef={messageRef}
335
  scrollRef={scrollRef}
336
  handleInputChange={(e) => {
app/components/chat/GitCloneButton.tsx CHANGED
@@ -1,7 +1,6 @@
1
  import ignore from 'ignore';
2
  import { useGit } from '~/lib/hooks/useGit';
3
  import type { Message } from 'ai';
4
- import WithTooltip from '~/components/ui/Tooltip';
5
  import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
6
  import { generateId } from '~/utils/fileUtils';
7
 
@@ -73,7 +72,7 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
73
  const filesMessage: Message = {
74
  role: 'assistant',
75
  content: `Cloning the repo ${repoUrl} into ${workdir}
76
- <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
77
  ${fileContents
78
  .map(
79
  (file) =>
@@ -99,17 +98,13 @@ ${file.content}
99
  };
100
 
101
  return (
102
- <WithTooltip tooltip="Clone A Git Repo">
103
- <button
104
- onClick={(e) => {
105
- onClick(e);
106
- }}
107
- title="Clone A Git Repo"
108
- className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
109
- >
110
- <span className="i-ph:git-branch" />
111
- Clone A Git Repo
112
- </button>
113
- </WithTooltip>
114
  );
115
  }
 
1
  import ignore from 'ignore';
2
  import { useGit } from '~/lib/hooks/useGit';
3
  import type { Message } from 'ai';
 
4
  import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
5
  import { generateId } from '~/utils/fileUtils';
6
 
 
72
  const filesMessage: Message = {
73
  role: 'assistant',
74
  content: `Cloning the repo ${repoUrl} into ${workdir}
75
+ <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
76
  ${fileContents
77
  .map(
78
  (file) =>
 
98
  };
99
 
100
  return (
101
+ <button
102
+ onClick={onClick}
103
+ title="Clone a Git Repo"
104
+ className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
105
+ >
106
+ <span className="i-ph:git-branch" />
107
+ Clone a Git Repo
108
+ </button>
 
 
 
 
109
  );
110
  }
app/components/chat/ImportFolderButton.tsx CHANGED
@@ -3,6 +3,7 @@ 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
 
7
  interface ImportFolderButtonProps {
8
  className?: string;
@@ -16,9 +17,15 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
16
  const allFiles = Array.from(e.target.files || []);
17
 
18
  if (allFiles.length > MAX_FILES) {
 
 
 
 
 
19
  toast.error(
20
  `This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
21
  );
 
22
  return;
23
  }
24
 
@@ -31,7 +38,10 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
31
  const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
32
 
33
  if (filteredFiles.length === 0) {
 
 
34
  toast.error('No files found in the selected folder');
 
35
  return;
36
  }
37
 
@@ -48,11 +58,18 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
48
  .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
49
 
50
  if (textFiles.length === 0) {
 
 
51
  toast.error('No text files found in the selected folder');
 
52
  return;
53
  }
54
 
55
  if (binaryFilePaths.length > 0) {
 
 
 
 
56
  toast.info(`Skipping ${binaryFilePaths.length} binary files`);
57
  }
58
 
@@ -62,8 +79,14 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
62
  await importChat(folderName, [...messages]);
63
  }
64
 
 
 
 
 
 
65
  toast.success('Folder imported successfully');
66
  } catch (error) {
 
67
  console.error('Failed to import folder:', error);
68
  toast.error('Failed to import folder');
69
  } finally {
 
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
 
8
  interface ImportFolderButtonProps {
9
  className?: string;
 
17
  const allFiles = Array.from(e.target.files || []);
18
 
19
  if (allFiles.length > MAX_FILES) {
20
+ const error = new Error(`Too many files: ${allFiles.length}`);
21
+ logStore.logError('File import failed - too many files', error, {
22
+ fileCount: allFiles.length,
23
+ maxFiles: MAX_FILES,
24
+ });
25
  toast.error(
26
  `This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
27
  );
28
+
29
  return;
30
  }
31
 
 
38
  const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
39
 
40
  if (filteredFiles.length === 0) {
41
+ const error = new Error('No valid files found');
42
+ logStore.logError('File import failed - no valid files', error, { folderName });
43
  toast.error('No files found in the selected folder');
44
+
45
  return;
46
  }
47
 
 
58
  .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
59
 
60
  if (textFiles.length === 0) {
61
+ const error = new Error('No text files found');
62
+ logStore.logError('File import failed - no text files', error, { folderName });
63
  toast.error('No text files found in the selected folder');
64
+
65
  return;
66
  }
67
 
68
  if (binaryFilePaths.length > 0) {
69
+ logStore.logWarning(`Skipping binary files during import`, {
70
+ folderName,
71
+ binaryCount: binaryFilePaths.length,
72
+ });
73
  toast.info(`Skipping ${binaryFilePaths.length} binary files`);
74
  }
75
 
 
79
  await importChat(folderName, [...messages]);
80
  }
81
 
82
+ logStore.logSystem('Folder imported successfully', {
83
+ folderName,
84
+ textFileCount: textFiles.length,
85
+ binaryFileCount: binaryFilePaths.length,
86
+ });
87
  toast.success('Folder imported successfully');
88
  } catch (error) {
89
+ logStore.logError('Failed to import folder', error, { folderName });
90
  console.error('Failed to import folder:', error);
91
  toast.error('Failed to import folder');
92
  } finally {
app/components/chat/Messages.client.tsx CHANGED
@@ -65,12 +65,16 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
65
  </div>
66
  )}
67
  <div className="grid grid-col-1 w-full">
68
- {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
 
 
 
 
69
  </div>
70
  {!isUserMessage && (
71
  <div className="flex gap-2 flex-col lg:flex-row">
72
- <WithTooltip tooltip="Revert to this message">
73
- {messageId && (
74
  <button
75
  onClick={() => handleRewind(messageId)}
76
  key="i-ph:arrow-u-up-left"
@@ -79,8 +83,8 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
79
  'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
80
  )}
81
  />
82
- )}
83
- </WithTooltip>
84
 
85
  <WithTooltip tooltip="Fork chat from this message">
86
  <button
 
65
  </div>
66
  )}
67
  <div className="grid grid-col-1 w-full">
68
+ {isUserMessage ? (
69
+ <UserMessage content={content} />
70
+ ) : (
71
+ <AssistantMessage content={content} annotations={message.annotations} />
72
+ )}
73
  </div>
74
  {!isUserMessage && (
75
  <div className="flex gap-2 flex-col lg:flex-row">
76
+ {messageId && (
77
+ <WithTooltip tooltip="Revert to this message">
78
  <button
79
  onClick={() => handleRewind(messageId)}
80
  key="i-ph:arrow-u-up-left"
 
83
  'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
84
  )}
85
  />
86
+ </WithTooltip>
87
+ )}
88
 
89
  <WithTooltip tooltip="Fork chat from this message">
90
  <button
app/components/chat/ModelSelector.tsx CHANGED
@@ -1,7 +1,6 @@
1
  import type { ProviderInfo } from '~/types/model';
2
  import type { ModelInfo } from '~/utils/types';
3
- import { useEffect, useState } from 'react';
4
- import Cookies from 'js-cookie';
5
 
6
  interface ModelSelectorProps {
7
  model?: string;
@@ -22,62 +21,28 @@ export const ModelSelector = ({
22
  providerList,
23
  }: ModelSelectorProps) => {
24
  // Load enabled providers from cookies
25
- const [enabledProviders, setEnabledProviders] = useState(() => {
26
- const savedProviders = Cookies.get('providers');
27
-
28
- if (savedProviders) {
29
- try {
30
- const parsedProviders = JSON.parse(savedProviders);
31
- return providerList.filter((p) => parsedProviders[p.name]);
32
- } catch (error) {
33
- console.error('Failed to parse providers from cookies:', error);
34
- return providerList;
35
- }
36
- }
37
-
38
- return providerList;
39
- });
40
 
41
  // Update enabled providers when cookies change
42
  useEffect(() => {
43
- // Function to update providers from cookies
44
- const updateProvidersFromCookies = () => {
45
- const savedProviders = Cookies.get('providers');
46
-
47
- if (savedProviders) {
48
- try {
49
- const parsedProviders = JSON.parse(savedProviders);
50
- const newEnabledProviders = providerList.filter((p) => parsedProviders[p.name]);
51
- setEnabledProviders(newEnabledProviders);
52
 
53
- // If current provider is disabled, switch to first enabled provider
54
- if (provider && !parsedProviders[provider.name] && newEnabledProviders.length > 0) {
55
- const firstEnabledProvider = newEnabledProviders[0];
56
- setProvider?.(firstEnabledProvider);
57
 
58
- // Also update the model to the first available one for the new provider
59
- const firstModel = modelList.find((m) => m.provider === firstEnabledProvider.name);
60
 
61
- if (firstModel) {
62
- setModel?.(firstModel.name);
63
- }
64
- }
65
- } catch (error) {
66
- console.error('Failed to parse providers from cookies:', error);
67
- }
68
  }
69
- };
70
-
71
- // Initial update
72
- updateProvidersFromCookies();
73
-
74
- // Set up an interval to check for cookie changes
75
- const interval = setInterval(updateProvidersFromCookies, 1000);
76
-
77
- return () => clearInterval(interval);
78
  }, [providerList, provider, setProvider, modelList, setModel]);
79
 
80
- if (enabledProviders.length === 0) {
81
  return (
82
  <div className="mb-2 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary">
83
  <p className="text-center">
@@ -93,7 +58,7 @@ export const ModelSelector = ({
93
  <select
94
  value={provider?.name ?? ''}
95
  onChange={(e) => {
96
- const newProvider = enabledProviders.find((p: ProviderInfo) => p.name === e.target.value);
97
 
98
  if (newProvider && setProvider) {
99
  setProvider(newProvider);
@@ -107,7 +72,7 @@ export const ModelSelector = ({
107
  }}
108
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
109
  >
110
- {enabledProviders.map((provider: ProviderInfo) => (
111
  <option key={provider.name} value={provider.name}>
112
  {provider.name}
113
  </option>
@@ -121,8 +86,8 @@ export const ModelSelector = ({
121
  >
122
  {[...modelList]
123
  .filter((e) => e.provider == provider?.name && e.name)
124
- .map((modelOption) => (
125
- <option key={modelOption.name} value={modelOption.name}>
126
  {modelOption.label}
127
  </option>
128
  ))}
 
1
  import type { ProviderInfo } from '~/types/model';
2
  import type { ModelInfo } from '~/utils/types';
3
+ import { useEffect } from 'react';
 
4
 
5
  interface ModelSelectorProps {
6
  model?: string;
 
21
  providerList,
22
  }: ModelSelectorProps) => {
23
  // Load enabled providers from cookies
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  // Update enabled providers when cookies change
26
  useEffect(() => {
27
+ // If current provider is disabled, switch to first enabled provider
28
+ if (providerList.length == 0) {
29
+ return;
30
+ }
 
 
 
 
 
31
 
32
+ if (provider && !providerList.map((p) => p.name).includes(provider.name)) {
33
+ const firstEnabledProvider = providerList[0];
34
+ setProvider?.(firstEnabledProvider);
 
35
 
36
+ // Also update the model to the first available one for the new provider
37
+ const firstModel = modelList.find((m) => m.provider === firstEnabledProvider.name);
38
 
39
+ if (firstModel) {
40
+ setModel?.(firstModel.name);
 
 
 
 
 
41
  }
42
+ }
 
 
 
 
 
 
 
 
43
  }, [providerList, provider, setProvider, modelList, setModel]);
44
 
45
+ if (providerList.length === 0) {
46
  return (
47
  <div className="mb-2 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary">
48
  <p className="text-center">
 
58
  <select
59
  value={provider?.name ?? ''}
60
  onChange={(e) => {
61
+ const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);
62
 
63
  if (newProvider && setProvider) {
64
  setProvider(newProvider);
 
72
  }}
73
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
74
  >
75
+ {providerList.map((provider: ProviderInfo) => (
76
  <option key={provider.name} value={provider.name}>
77
  {provider.name}
78
  </option>
 
86
  >
87
  {[...modelList]
88
  .filter((e) => e.provider == provider?.name && e.name)
89
+ .map((modelOption, index) => (
90
+ <option key={index} value={modelOption.name}>
91
  {modelOption.label}
92
  </option>
93
  ))}
app/components/chat/ScreenshotStateManager.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from 'react';
2
+
3
+ interface ScreenshotStateManagerProps {
4
+ setUploadedFiles?: (files: File[]) => void;
5
+ setImageDataList?: (dataList: string[]) => void;
6
+ uploadedFiles: File[];
7
+ imageDataList: string[];
8
+ }
9
+
10
+ export const ScreenshotStateManager = ({
11
+ setUploadedFiles,
12
+ setImageDataList,
13
+ uploadedFiles,
14
+ imageDataList,
15
+ }: ScreenshotStateManagerProps) => {
16
+ useEffect(() => {
17
+ if (setUploadedFiles && setImageDataList) {
18
+ (window as any).__BOLT_SET_UPLOADED_FILES__ = setUploadedFiles;
19
+ (window as any).__BOLT_SET_IMAGE_DATA_LIST__ = setImageDataList;
20
+ (window as any).__BOLT_UPLOADED_FILES__ = uploadedFiles;
21
+ (window as any).__BOLT_IMAGE_DATA_LIST__ = imageDataList;
22
+ }
23
+
24
+ return () => {
25
+ delete (window as any).__BOLT_SET_UPLOADED_FILES__;
26
+ delete (window as any).__BOLT_SET_IMAGE_DATA_LIST__;
27
+ delete (window as any).__BOLT_UPLOADED_FILES__;
28
+ delete (window as any).__BOLT_IMAGE_DATA_LIST__;
29
+ };
30
+ }, [setUploadedFiles, setImageDataList, uploadedFiles, imageDataList]);
31
+
32
+ return null;
33
+ };
app/components/chat/UserMessage.tsx CHANGED
@@ -12,42 +12,36 @@ interface UserMessageProps {
12
  export function UserMessage({ content }: UserMessageProps) {
13
  if (Array.isArray(content)) {
14
  const textItem = content.find((item) => item.type === 'text');
15
- const textContent = sanitizeUserMessage(textItem?.text || '');
16
  const images = content.filter((item) => item.type === 'image' && item.image);
17
 
18
  return (
19
  <div className="overflow-hidden pt-[4px]">
20
- <div className="flex items-start gap-4">
21
- <div className="flex-1">
22
- <Markdown limitedMarkdown>{textContent}</Markdown>
23
- </div>
24
- {images.length > 0 && (
25
- <div className="flex-shrink-0 w-[160px]">
26
- {images.map((item, index) => (
27
- <div key={index} className="relative">
28
- <img
29
- src={item.image}
30
- alt={`Uploaded image ${index + 1}`}
31
- className="w-full h-[160px] rounded-lg object-cover border border-bolt-elements-borderColor"
32
- />
33
- </div>
34
- ))}
35
- </div>
36
- )}
37
  </div>
38
  </div>
39
  );
40
  }
41
 
42
- const textContent = sanitizeUserMessage(content);
43
 
44
  return (
45
  <div className="overflow-hidden pt-[4px]">
46
- <Markdown limitedMarkdown>{textContent}</Markdown>
47
  </div>
48
  );
49
  }
50
 
51
- function sanitizeUserMessage(content: string) {
52
  return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
53
  }
 
12
  export function UserMessage({ content }: UserMessageProps) {
13
  if (Array.isArray(content)) {
14
  const textItem = content.find((item) => item.type === 'text');
15
+ const textContent = stripMetadata(textItem?.text || '');
16
  const images = content.filter((item) => item.type === 'image' && item.image);
17
 
18
  return (
19
  <div className="overflow-hidden pt-[4px]">
20
+ <div className="flex flex-col gap-4">
21
+ {textContent && <Markdown html>{textContent}</Markdown>}
22
+ {images.map((item, index) => (
23
+ <img
24
+ key={index}
25
+ src={item.image}
26
+ alt={`Image ${index + 1}`}
27
+ className="max-w-full h-auto rounded-lg"
28
+ style={{ maxHeight: '512px', objectFit: 'contain' }}
29
+ />
30
+ ))}
 
 
 
 
 
 
31
  </div>
32
  </div>
33
  );
34
  }
35
 
36
+ const textContent = stripMetadata(content);
37
 
38
  return (
39
  <div className="overflow-hidden pt-[4px]">
40
+ <Markdown html>{textContent}</Markdown>
41
  </div>
42
  );
43
  }
44
 
45
+ function stripMetadata(content: string) {
46
  return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
47
  }
app/components/chat/chatExportAndImport/ImportButtons.tsx CHANGED
@@ -1,6 +1,5 @@
1
  import type { Message } from 'ai';
2
  import { toast } from 'react-toastify';
3
- import React from 'react';
4
  import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
5
 
6
  export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
 
1
  import type { Message } from 'ai';
2
  import { toast } from 'react-toastify';
 
3
  import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
4
 
5
  export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
app/components/editor/codemirror/languages.ts CHANGED
@@ -1,6 +1,13 @@
1
  import { LanguageDescription } from '@codemirror/language';
2
 
3
  export const supportedLanguages = [
 
 
 
 
 
 
 
4
  LanguageDescription.of({
5
  name: 'TS',
6
  extensions: ['ts'],
 
1
  import { LanguageDescription } from '@codemirror/language';
2
 
3
  export const supportedLanguages = [
4
+ LanguageDescription.of({
5
+ name: 'VUE',
6
+ extensions: ['vue'],
7
+ async load() {
8
+ return import('@codemirror/lang-vue').then((module) => module.vue());
9
+ },
10
+ }),
11
  LanguageDescription.of({
12
  name: 'TS',
13
  extensions: ['ts'],
app/components/header/Header.tsx CHANGED
@@ -10,18 +10,17 @@ export function Header() {
10
 
11
  return (
12
  <header
13
- className={classNames(
14
- 'flex items-center bg-bolt-elements-background-depth-1 p-5 border-b h-[var(--header-height)]',
15
- {
16
- 'border-transparent': !chat.started,
17
- 'border-bolt-elements-borderColor': chat.started,
18
- },
19
- )}
20
  >
21
  <div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
22
  <div className="i-ph:sidebar-simple-duotone text-xl" />
23
  <a href="/" className="text-2xl font-semibold text-accent flex items-center">
24
- <span className="i-bolt:logo-text?mask w-[46px] inline-block" />
 
 
25
  </a>
26
  </div>
27
  {chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
 
10
 
11
  return (
12
  <header
13
+ className={classNames('flex items-center p-5 border-b h-[var(--header-height)]', {
14
+ 'border-transparent': !chat.started,
15
+ 'border-bolt-elements-borderColor': chat.started,
16
+ })}
 
 
 
17
  >
18
  <div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
19
  <div className="i-ph:sidebar-simple-duotone text-xl" />
20
  <a href="/" className="text-2xl font-semibold text-accent flex items-center">
21
+ {/* <span className="i-bolt:logo-text?mask w-[46px] inline-block" /> */}
22
+ <img src="/logo-light-styled.png" alt="logo" className="w-[90px] inline-block dark:hidden" />
23
+ <img src="/logo-dark-styled.png" alt="logo" className="w-[90px] inline-block hidden dark:block" />
24
  </a>
25
  </div>
26
  {chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
app/{styles/components/Settings.scss → components/settings/Settings.module.scss} RENAMED
@@ -45,6 +45,9 @@
45
  border-radius: 0.5rem;
46
  padding: 1rem;
47
  margin-bottom: 1rem;
 
 
 
48
 
49
  button {
50
  background-color: var(--bolt-elements-button-danger-background);
@@ -57,4 +60,4 @@
57
  background-color: var(--bolt-elements-button-danger-backgroundHover);
58
  }
59
  }
60
- }
 
45
  border-radius: 0.5rem;
46
  padding: 1rem;
47
  margin-bottom: 1rem;
48
+ border-style: solid;
49
+ border-color: var(--bolt-elements-button-danger-backgroundHover);
50
+ border-width: thin;
51
 
52
  button {
53
  background-color: var(--bolt-elements-button-danger-background);
 
60
  background-color: var(--bolt-elements-button-danger-backgroundHover);
61
  }
62
  }
63
+ }
app/components/settings/SettingsWindow.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as RadixDialog from '@radix-ui/react-dialog';
2
+ import { motion } from 'framer-motion';
3
+ import { useState, type ReactElement } from 'react';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { DialogTitle, dialogVariants, dialogBackdropVariants } from '~/components/ui/Dialog';
6
+ import { IconButton } from '~/components/ui/IconButton';
7
+ import styles from './Settings.module.scss';
8
+ import ChatHistoryTab from './chat-history/ChatHistoryTab';
9
+ import ProvidersTab from './providers/ProvidersTab';
10
+ import { useSettings } from '~/lib/hooks/useSettings';
11
+ import FeaturesTab from './features/FeaturesTab';
12
+ import DebugTab from './debug/DebugTab';
13
+ import EventLogsTab from './event-logs/EventLogsTab';
14
+ import ConnectionsTab from './connections/ConnectionsTab';
15
+
16
+ interface SettingsProps {
17
+ open: boolean;
18
+ onClose: () => void;
19
+ }
20
+
21
+ type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection';
22
+
23
+ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
24
+ const { debug, eventLogs } = useSettings();
25
+ const [activeTab, setActiveTab] = useState<TabType>('chat-history');
26
+
27
+ const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
28
+ { id: 'chat-history', label: 'Chat History', icon: 'i-ph:book', component: <ChatHistoryTab /> },
29
+ { id: 'providers', label: 'Providers', icon: 'i-ph:key', component: <ProvidersTab /> },
30
+ { id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
31
+ { id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
32
+ ...(debug
33
+ ? [
34
+ {
35
+ id: 'debug' as TabType,
36
+ label: 'Debug Tab',
37
+ icon: 'i-ph:bug',
38
+ component: <DebugTab />,
39
+ },
40
+ ]
41
+ : []),
42
+ ...(eventLogs
43
+ ? [
44
+ {
45
+ id: 'event-logs' as TabType,
46
+ label: 'Event Logs',
47
+ icon: 'i-ph:list-bullets',
48
+ component: <EventLogsTab />,
49
+ },
50
+ ]
51
+ : []),
52
+ ];
53
+
54
+ return (
55
+ <RadixDialog.Root open={open}>
56
+ <RadixDialog.Portal>
57
+ <RadixDialog.Overlay asChild onClick={onClose}>
58
+ <motion.div
59
+ className="bg-black/50 fixed inset-0 z-max backdrop-blur-sm"
60
+ initial="closed"
61
+ animate="open"
62
+ exit="closed"
63
+ variants={dialogBackdropVariants}
64
+ />
65
+ </RadixDialog.Overlay>
66
+ <RadixDialog.Content asChild>
67
+ <motion.div
68
+ className="fixed top-[50%] left-[50%] z-max h-[85vh] w-[90vw] max-w-[900px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg shadow-lg focus:outline-none overflow-hidden"
69
+ initial="closed"
70
+ animate="open"
71
+ exit="closed"
72
+ variants={dialogVariants}
73
+ >
74
+ <div className="flex h-full">
75
+ <div
76
+ className={classNames(
77
+ 'w-48 border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1 p-4 flex flex-col justify-between',
78
+ styles['settings-tabs'],
79
+ )}
80
+ >
81
+ <DialogTitle className="flex-shrink-0 text-lg font-semibold text-bolt-elements-textPrimary mb-2">
82
+ Settings
83
+ </DialogTitle>
84
+ {tabs.map((tab) => (
85
+ <button
86
+ key={tab.id}
87
+ onClick={() => setActiveTab(tab.id)}
88
+ className={classNames(activeTab === tab.id ? styles.active : '')}
89
+ >
90
+ <div className={tab.icon} />
91
+ {tab.label}
92
+ </button>
93
+ ))}
94
+ <div className="mt-auto flex flex-col gap-2">
95
+ <a
96
+ href="https://github.com/stackblitz-labs/bolt.diy"
97
+ target="_blank"
98
+ rel="noopener noreferrer"
99
+ className={classNames(styles['settings-button'], 'flex items-center gap-2')}
100
+ >
101
+ <div className="i-ph:github-logo" />
102
+ GitHub
103
+ </a>
104
+ <a
105
+ href="https://stackblitz-labs.github.io/bolt.diy/"
106
+ target="_blank"
107
+ rel="noopener noreferrer"
108
+ className={classNames(styles['settings-button'], 'flex items-center gap-2')}
109
+ >
110
+ <div className="i-ph:book" />
111
+ Docs
112
+ </a>
113
+ </div>
114
+ </div>
115
+
116
+ <div className="flex-1 flex flex-col p-8 pt-10 bg-bolt-elements-background-depth-2">
117
+ <div className="flex-1 overflow-y-auto">{tabs.find((tab) => tab.id === activeTab)?.component}</div>
118
+ </div>
119
+ </div>
120
+ <RadixDialog.Close asChild onClick={onClose}>
121
+ <IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
122
+ </RadixDialog.Close>
123
+ </motion.div>
124
+ </RadixDialog.Content>
125
+ </RadixDialog.Portal>
126
+ </RadixDialog.Root>
127
+ );
128
+ };
app/components/settings/chat-history/ChatHistoryTab.tsx ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useNavigate } from '@remix-run/react';
2
+ import React, { useState } from 'react';
3
+ import { toast } from 'react-toastify';
4
+ import { db, deleteById, getAll } from '~/lib/persistence';
5
+ import { classNames } from '~/utils/classNames';
6
+ import styles from '~/components/settings/Settings.module.scss';
7
+ import { logStore } from '~/lib/stores/logs'; // Import logStore for event logging
8
+
9
+ export default function ChatHistoryTab() {
10
+ const navigate = useNavigate();
11
+ const [isDeleting, setIsDeleting] = useState(false);
12
+ const downloadAsJson = (data: any, filename: string) => {
13
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
14
+ const url = URL.createObjectURL(blob);
15
+ const link = document.createElement('a');
16
+ link.href = url;
17
+ link.download = filename;
18
+ document.body.appendChild(link);
19
+ link.click();
20
+ document.body.removeChild(link);
21
+ URL.revokeObjectURL(url);
22
+ };
23
+
24
+ const handleDeleteAllChats = async () => {
25
+ const confirmDelete = window.confirm('Are you sure you want to delete all chats? This action cannot be undone.');
26
+
27
+ if (!confirmDelete) {
28
+ return; // Exit if the user cancels
29
+ }
30
+
31
+ if (!db) {
32
+ const error = new Error('Database is not available');
33
+ logStore.logError('Failed to delete chats - DB unavailable', error);
34
+ toast.error('Database is not available');
35
+
36
+ return;
37
+ }
38
+
39
+ try {
40
+ setIsDeleting(true);
41
+
42
+ const allChats = await getAll(db);
43
+ await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
44
+ logStore.logSystem('All chats deleted successfully', { count: allChats.length });
45
+ toast.success('All chats deleted successfully');
46
+ navigate('/', { replace: true });
47
+ } catch (error) {
48
+ logStore.logError('Failed to delete chats', error);
49
+ toast.error('Failed to delete chats');
50
+ console.error(error);
51
+ } finally {
52
+ setIsDeleting(false);
53
+ }
54
+ };
55
+
56
+ const handleExportAllChats = async () => {
57
+ if (!db) {
58
+ const error = new Error('Database is not available');
59
+ logStore.logError('Failed to export chats - DB unavailable', error);
60
+ toast.error('Database is not available');
61
+
62
+ return;
63
+ }
64
+
65
+ try {
66
+ const allChats = await getAll(db);
67
+ const exportData = {
68
+ chats: allChats,
69
+ exportDate: new Date().toISOString(),
70
+ };
71
+
72
+ downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
73
+ logStore.logSystem('Chats exported successfully', { count: allChats.length });
74
+ toast.success('Chats exported successfully');
75
+ } catch (error) {
76
+ logStore.logError('Failed to export chats', error);
77
+ toast.error('Failed to export chats');
78
+ console.error(error);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <>
84
+ <div className="p-4">
85
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Chat History</h3>
86
+ <button
87
+ onClick={handleExportAllChats}
88
+ className={classNames(
89
+ 'bg-bolt-elements-button-primary-background',
90
+ 'rounded-lg px-4 py-2 mb-4 transition-colors duration-200',
91
+ 'hover:bg-bolt-elements-button-primary-backgroundHover',
92
+ 'text-bolt-elements-button-primary-text',
93
+ )}
94
+ >
95
+ Export All Chats
96
+ </button>
97
+
98
+ <div
99
+ className={classNames('text-bolt-elements-textPrimary rounded-lg py-4 mb-4', styles['settings-danger-area'])}
100
+ >
101
+ <h4 className="font-semibold">Danger Area</h4>
102
+ <p className="mb-2">This action cannot be undone!</p>
103
+ <button
104
+ onClick={handleDeleteAllChats}
105
+ disabled={isDeleting}
106
+ className={classNames(
107
+ 'bg-bolt-elements-button-danger-background',
108
+ 'rounded-lg px-4 py-2 transition-colors duration-200',
109
+ isDeleting ? 'opacity-50 cursor-not-allowed' : 'hover:bg-bolt-elements-button-danger-backgroundHover',
110
+ 'text-bolt-elements-button-danger-text',
111
+ )}
112
+ >
113
+ {isDeleting ? 'Deleting...' : 'Delete All Chats'}
114
+ </button>
115
+ </div>
116
+ </div>
117
+ </>
118
+ );
119
+ }
app/components/settings/connections/ConnectionsTab.tsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { toast } from 'react-toastify';
3
+ import Cookies from 'js-cookie';
4
+ import { logStore } from '~/lib/stores/logs';
5
+
6
+ export default function ConnectionsTab() {
7
+ const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || '');
8
+ const [githubToken, setGithubToken] = useState(Cookies.get('githubToken') || '');
9
+
10
+ const handleSaveConnection = () => {
11
+ Cookies.set('githubUsername', githubUsername);
12
+ Cookies.set('githubToken', githubToken);
13
+ logStore.logSystem('GitHub connection settings updated', {
14
+ username: githubUsername,
15
+ hasToken: !!githubToken,
16
+ });
17
+ toast.success('GitHub credentials saved successfully!');
18
+ Cookies.set('git:github.com', JSON.stringify({ username: githubToken, password: 'x-oauth-basic' }));
19
+ };
20
+
21
+ return (
22
+ <div className="p-4 mb-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3">
23
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">GitHub Connection</h3>
24
+ <div className="flex mb-4">
25
+ <div className="flex-1 mr-2">
26
+ <label className="block text-sm text-bolt-elements-textSecondary mb-1">GitHub Username:</label>
27
+ <input
28
+ type="text"
29
+ value={githubUsername}
30
+ onChange={(e) => setGithubUsername(e.target.value)}
31
+ className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
32
+ />
33
+ </div>
34
+ <div className="flex-1">
35
+ <label className="block text-sm text-bolt-elements-textSecondary mb-1">Personal Access Token:</label>
36
+ <input
37
+ type="password"
38
+ value={githubToken}
39
+ onChange={(e) => setGithubToken(e.target.value)}
40
+ className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
41
+ />
42
+ </div>
43
+ </div>
44
+ <div className="flex mb-4">
45
+ <button
46
+ onClick={handleSaveConnection}
47
+ className="bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 mr-2 transition-colors duration-200 hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text"
48
+ >
49
+ Save Connection
50
+ </button>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
app/components/settings/debug/DebugTab.tsx ADDED
@@ -0,0 +1,620 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { useSettings } from '~/lib/hooks/useSettings';
3
+ import commit from '~/commit.json';
4
+ import { toast } from 'react-toastify';
5
+
6
+ interface ProviderStatus {
7
+ name: string;
8
+ enabled: boolean;
9
+ isLocal: boolean;
10
+ isRunning: boolean | null;
11
+ error?: string;
12
+ lastChecked: Date;
13
+ responseTime?: number;
14
+ url: string | null;
15
+ }
16
+
17
+ interface SystemInfo {
18
+ os: string;
19
+ browser: string;
20
+ screen: string;
21
+ language: string;
22
+ timezone: string;
23
+ memory: string;
24
+ cores: number;
25
+ deviceType: string;
26
+ colorDepth: string;
27
+ pixelRatio: number;
28
+ online: boolean;
29
+ cookiesEnabled: boolean;
30
+ doNotTrack: boolean;
31
+ }
32
+
33
+ interface IProviderConfig {
34
+ name: string;
35
+ settings: {
36
+ enabled: boolean;
37
+ baseUrl?: string;
38
+ };
39
+ }
40
+
41
+ interface CommitData {
42
+ commit: string;
43
+ version?: string;
44
+ }
45
+
46
+ const connitJson: CommitData = commit;
47
+
48
+ const LOCAL_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
49
+ const versionHash = connitJson.commit;
50
+ const versionTag = connitJson.version;
51
+ const GITHUB_URLS = {
52
+ original: 'https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/main',
53
+ fork: 'https://api.github.com/repos/Stijnus/bolt.new-any-llm/commits/main',
54
+ commitJson: (branch: string) =>
55
+ `https://raw.githubusercontent.com/stackblitz-labs/bolt.diy/${branch}/app/commit.json`,
56
+ };
57
+
58
+ function getSystemInfo(): SystemInfo {
59
+ const formatBytes = (bytes: number): string => {
60
+ if (bytes === 0) {
61
+ return '0 Bytes';
62
+ }
63
+
64
+ const k = 1024;
65
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
66
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
67
+
68
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
69
+ };
70
+
71
+ const getBrowserInfo = (): string => {
72
+ const ua = navigator.userAgent;
73
+ let browser = 'Unknown';
74
+
75
+ if (ua.includes('Firefox/')) {
76
+ browser = 'Firefox';
77
+ } else if (ua.includes('Chrome/')) {
78
+ if (ua.includes('Edg/')) {
79
+ browser = 'Edge';
80
+ } else if (ua.includes('OPR/')) {
81
+ browser = 'Opera';
82
+ } else {
83
+ browser = 'Chrome';
84
+ }
85
+ } else if (ua.includes('Safari/')) {
86
+ if (!ua.includes('Chrome')) {
87
+ browser = 'Safari';
88
+ }
89
+ }
90
+
91
+ // Extract version number
92
+ const match = ua.match(new RegExp(`${browser}\\/([\\d.]+)`));
93
+ const version = match ? ` ${match[1]}` : '';
94
+
95
+ return `${browser}${version}`;
96
+ };
97
+
98
+ const getOperatingSystem = (): string => {
99
+ const ua = navigator.userAgent;
100
+ const platform = navigator.platform;
101
+
102
+ if (ua.includes('Win')) {
103
+ return 'Windows';
104
+ }
105
+
106
+ if (ua.includes('Mac')) {
107
+ if (ua.includes('iPhone') || ua.includes('iPad')) {
108
+ return 'iOS';
109
+ }
110
+
111
+ return 'macOS';
112
+ }
113
+
114
+ if (ua.includes('Linux')) {
115
+ return 'Linux';
116
+ }
117
+
118
+ if (ua.includes('Android')) {
119
+ return 'Android';
120
+ }
121
+
122
+ return platform || 'Unknown';
123
+ };
124
+
125
+ const getDeviceType = (): string => {
126
+ const ua = navigator.userAgent;
127
+
128
+ if (ua.includes('Mobile')) {
129
+ return 'Mobile';
130
+ }
131
+
132
+ if (ua.includes('Tablet')) {
133
+ return 'Tablet';
134
+ }
135
+
136
+ return 'Desktop';
137
+ };
138
+
139
+ // Get more detailed memory info if available
140
+ const getMemoryInfo = (): string => {
141
+ if ('memory' in performance) {
142
+ const memory = (performance as any).memory;
143
+ return `${formatBytes(memory.jsHeapSizeLimit)} (Used: ${formatBytes(memory.usedJSHeapSize)})`;
144
+ }
145
+
146
+ return 'Not available';
147
+ };
148
+
149
+ return {
150
+ os: getOperatingSystem(),
151
+ browser: getBrowserInfo(),
152
+ screen: `${window.screen.width}x${window.screen.height}`,
153
+ language: navigator.language,
154
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
155
+ memory: getMemoryInfo(),
156
+ cores: navigator.hardwareConcurrency || 0,
157
+ deviceType: getDeviceType(),
158
+
159
+ // Add new fields
160
+ colorDepth: `${window.screen.colorDepth}-bit`,
161
+ pixelRatio: window.devicePixelRatio,
162
+ online: navigator.onLine,
163
+ cookiesEnabled: navigator.cookieEnabled,
164
+ doNotTrack: navigator.doNotTrack === '1',
165
+ };
166
+ }
167
+
168
+ const checkProviderStatus = async (url: string | null, providerName: string): Promise<ProviderStatus> => {
169
+ if (!url) {
170
+ console.log(`[Debug] No URL provided for ${providerName}`);
171
+ return {
172
+ name: providerName,
173
+ enabled: false,
174
+ isLocal: true,
175
+ isRunning: false,
176
+ error: 'No URL configured',
177
+ lastChecked: new Date(),
178
+ url: null,
179
+ };
180
+ }
181
+
182
+ console.log(`[Debug] Checking status for ${providerName} at ${url}`);
183
+
184
+ const startTime = performance.now();
185
+
186
+ try {
187
+ if (providerName.toLowerCase() === 'ollama') {
188
+ // Special check for Ollama root endpoint
189
+ try {
190
+ console.log(`[Debug] Checking Ollama root endpoint: ${url}`);
191
+
192
+ const controller = new AbortController();
193
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
194
+
195
+ const response = await fetch(url, {
196
+ signal: controller.signal,
197
+ headers: {
198
+ Accept: 'text/plain,application/json',
199
+ },
200
+ });
201
+ clearTimeout(timeoutId);
202
+
203
+ const text = await response.text();
204
+ console.log(`[Debug] Ollama root response:`, text);
205
+
206
+ if (text.includes('Ollama is running')) {
207
+ console.log(`[Debug] Ollama running confirmed via root endpoint`);
208
+ return {
209
+ name: providerName,
210
+ enabled: false,
211
+ isLocal: true,
212
+ isRunning: true,
213
+ lastChecked: new Date(),
214
+ responseTime: performance.now() - startTime,
215
+ url,
216
+ };
217
+ }
218
+ } catch (error) {
219
+ console.log(`[Debug] Ollama root check failed:`, error);
220
+
221
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
222
+
223
+ if (errorMessage.includes('aborted')) {
224
+ return {
225
+ name: providerName,
226
+ enabled: false,
227
+ isLocal: true,
228
+ isRunning: false,
229
+ error: 'Connection timeout',
230
+ lastChecked: new Date(),
231
+ responseTime: performance.now() - startTime,
232
+ url,
233
+ };
234
+ }
235
+ }
236
+ }
237
+
238
+ // Try different endpoints based on provider
239
+ const checkUrls = [`${url}/api/health`, `${url}/v1/models`];
240
+ console.log(`[Debug] Checking additional endpoints:`, checkUrls);
241
+
242
+ const results = await Promise.all(
243
+ checkUrls.map(async (checkUrl) => {
244
+ try {
245
+ console.log(`[Debug] Trying endpoint: ${checkUrl}`);
246
+
247
+ const controller = new AbortController();
248
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
249
+
250
+ const response = await fetch(checkUrl, {
251
+ signal: controller.signal,
252
+ headers: {
253
+ Accept: 'application/json',
254
+ },
255
+ });
256
+ clearTimeout(timeoutId);
257
+
258
+ const ok = response.ok;
259
+ console.log(`[Debug] Endpoint ${checkUrl} response:`, ok);
260
+
261
+ if (ok) {
262
+ try {
263
+ const data = await response.json();
264
+ console.log(`[Debug] Endpoint ${checkUrl} data:`, data);
265
+ } catch {
266
+ console.log(`[Debug] Could not parse JSON from ${checkUrl}`);
267
+ }
268
+ }
269
+
270
+ return ok;
271
+ } catch (error) {
272
+ console.log(`[Debug] Endpoint ${checkUrl} failed:`, error);
273
+ return false;
274
+ }
275
+ }),
276
+ );
277
+
278
+ const isRunning = results.some((result) => result);
279
+ console.log(`[Debug] Final status for ${providerName}:`, isRunning);
280
+
281
+ return {
282
+ name: providerName,
283
+ enabled: false,
284
+ isLocal: true,
285
+ isRunning,
286
+ lastChecked: new Date(),
287
+ responseTime: performance.now() - startTime,
288
+ url,
289
+ };
290
+ } catch (error) {
291
+ console.log(`[Debug] Provider check failed for ${providerName}:`, error);
292
+ return {
293
+ name: providerName,
294
+ enabled: false,
295
+ isLocal: true,
296
+ isRunning: false,
297
+ error: error instanceof Error ? error.message : 'Unknown error',
298
+ lastChecked: new Date(),
299
+ responseTime: performance.now() - startTime,
300
+ url,
301
+ };
302
+ }
303
+ };
304
+
305
+ export default function DebugTab() {
306
+ const { providers, isLatestBranch } = useSettings();
307
+ const [activeProviders, setActiveProviders] = useState<ProviderStatus[]>([]);
308
+ const [updateMessage, setUpdateMessage] = useState<string>('');
309
+ const [systemInfo] = useState<SystemInfo>(getSystemInfo());
310
+ const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
311
+
312
+ const updateProviderStatuses = async () => {
313
+ if (!providers) {
314
+ return;
315
+ }
316
+
317
+ try {
318
+ const entries = Object.entries(providers) as [string, IProviderConfig][];
319
+ const statuses = await Promise.all(
320
+ entries
321
+ .filter(([, provider]) => LOCAL_PROVIDERS.includes(provider.name))
322
+ .map(async ([, provider]) => {
323
+ const envVarName =
324
+ provider.name.toLowerCase() === 'ollama'
325
+ ? 'OLLAMA_API_BASE_URL'
326
+ : provider.name.toLowerCase() === 'lmstudio'
327
+ ? 'LMSTUDIO_API_BASE_URL'
328
+ : `REACT_APP_${provider.name.toUpperCase()}_URL`;
329
+
330
+ // Access environment variables through import.meta.env
331
+ const url = import.meta.env[envVarName] || provider.settings.baseUrl || null; // Ensure baseUrl is used
332
+ console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`);
333
+
334
+ const status = await checkProviderStatus(url, provider.name);
335
+
336
+ return {
337
+ ...status,
338
+ enabled: provider.settings.enabled ?? false,
339
+ };
340
+ }),
341
+ );
342
+
343
+ setActiveProviders(statuses);
344
+ } catch (error) {
345
+ console.error('[Debug] Failed to update provider statuses:', error);
346
+ }
347
+ };
348
+
349
+ useEffect(() => {
350
+ updateProviderStatuses();
351
+
352
+ const interval = setInterval(updateProviderStatuses, 30000);
353
+
354
+ return () => clearInterval(interval);
355
+ }, [providers]);
356
+
357
+ const handleCheckForUpdate = useCallback(async () => {
358
+ if (isCheckingUpdate) {
359
+ return;
360
+ }
361
+
362
+ try {
363
+ setIsCheckingUpdate(true);
364
+ setUpdateMessage('Checking for updates...');
365
+
366
+ const branchToCheck = isLatestBranch ? 'main' : 'stable';
367
+ console.log(`[Debug] Checking for updates against ${branchToCheck} branch`);
368
+
369
+ const localCommitResponse = await fetch(GITHUB_URLS.commitJson(branchToCheck));
370
+
371
+ if (!localCommitResponse.ok) {
372
+ throw new Error('Failed to fetch local commit info');
373
+ }
374
+
375
+ const localCommitData = (await localCommitResponse.json()) as CommitData;
376
+ const remoteCommitHash = localCommitData.commit;
377
+ const currentCommitHash = versionHash;
378
+
379
+ if (remoteCommitHash !== currentCommitHash) {
380
+ setUpdateMessage(
381
+ `Update available from ${branchToCheck} branch!\n` +
382
+ `Current: ${currentCommitHash.slice(0, 7)}\n` +
383
+ `Latest: ${remoteCommitHash.slice(0, 7)}`,
384
+ );
385
+ } else {
386
+ setUpdateMessage(`You are on the latest version from the ${branchToCheck} branch`);
387
+ }
388
+ } catch (error) {
389
+ setUpdateMessage('Failed to check for updates');
390
+ console.error('[Debug] Failed to check for updates:', error);
391
+ } finally {
392
+ setIsCheckingUpdate(false);
393
+ }
394
+ }, [isCheckingUpdate, isLatestBranch]);
395
+
396
+ const handleCopyToClipboard = useCallback(() => {
397
+ const debugInfo = {
398
+ System: systemInfo,
399
+ Providers: activeProviders.map((provider) => ({
400
+ name: provider.name,
401
+ enabled: provider.enabled,
402
+ isLocal: provider.isLocal,
403
+ running: provider.isRunning,
404
+ error: provider.error,
405
+ lastChecked: provider.lastChecked,
406
+ responseTime: provider.responseTime,
407
+ url: provider.url,
408
+ })),
409
+ Version: {
410
+ hash: versionHash.slice(0, 7),
411
+ branch: isLatestBranch ? 'main' : 'stable',
412
+ },
413
+ Timestamp: new Date().toISOString(),
414
+ };
415
+
416
+ navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
417
+ toast.success('Debug information copied to clipboard!');
418
+ });
419
+ }, [activeProviders, systemInfo, isLatestBranch]);
420
+
421
+ return (
422
+ <div className="p-4 space-y-6">
423
+ <div className="flex items-center justify-between">
424
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Debug Information</h3>
425
+ <div className="flex gap-2">
426
+ <button
427
+ onClick={handleCopyToClipboard}
428
+ className="bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 transition-colors duration-200 hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text"
429
+ >
430
+ Copy Debug Info
431
+ </button>
432
+ <button
433
+ onClick={handleCheckForUpdate}
434
+ disabled={isCheckingUpdate}
435
+ className={`bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 transition-colors duration-200
436
+ ${!isCheckingUpdate ? 'hover:bg-bolt-elements-button-primary-backgroundHover' : 'opacity-75 cursor-not-allowed'}
437
+ text-bolt-elements-button-primary-text`}
438
+ >
439
+ {isCheckingUpdate ? 'Checking...' : 'Check for Updates'}
440
+ </button>
441
+ </div>
442
+ </div>
443
+
444
+ {updateMessage && (
445
+ <div
446
+ className={`bg-bolt-elements-surface rounded-lg p-3 ${
447
+ updateMessage.includes('Update available') ? 'border-l-4 border-yellow-400' : ''
448
+ }`}
449
+ >
450
+ <p className="text-bolt-elements-textSecondary whitespace-pre-line">{updateMessage}</p>
451
+ {updateMessage.includes('Update available') && (
452
+ <div className="mt-3 text-sm">
453
+ <p className="font-medium text-bolt-elements-textPrimary">To update:</p>
454
+ <ol className="list-decimal ml-4 mt-1 text-bolt-elements-textSecondary">
455
+ <li>
456
+ Pull the latest changes:{' '}
457
+ <code className="bg-bolt-elements-surface-hover px-1 rounded">git pull upstream main</code>
458
+ </li>
459
+ <li>
460
+ Install any new dependencies:{' '}
461
+ <code className="bg-bolt-elements-surface-hover px-1 rounded">pnpm install</code>
462
+ </li>
463
+ <li>Restart the application</li>
464
+ </ol>
465
+ </div>
466
+ )}
467
+ </div>
468
+ )}
469
+
470
+ <section className="space-y-4">
471
+ <div>
472
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary mb-2">System Information</h4>
473
+ <div className="bg-bolt-elements-surface rounded-lg p-4">
474
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
475
+ <div>
476
+ <p className="text-xs text-bolt-elements-textSecondary">Operating System</p>
477
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.os}</p>
478
+ </div>
479
+ <div>
480
+ <p className="text-xs text-bolt-elements-textSecondary">Device Type</p>
481
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.deviceType}</p>
482
+ </div>
483
+ <div>
484
+ <p className="text-xs text-bolt-elements-textSecondary">Browser</p>
485
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.browser}</p>
486
+ </div>
487
+ <div>
488
+ <p className="text-xs text-bolt-elements-textSecondary">Display</p>
489
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">
490
+ {systemInfo.screen} ({systemInfo.colorDepth}) @{systemInfo.pixelRatio}x
491
+ </p>
492
+ </div>
493
+ <div>
494
+ <p className="text-xs text-bolt-elements-textSecondary">Connection</p>
495
+ <p className="text-sm font-medium flex items-center gap-2">
496
+ <span
497
+ className={`inline-block w-2 h-2 rounded-full ${systemInfo.online ? 'bg-green-500' : 'bg-red-500'}`}
498
+ />
499
+ <span className={`${systemInfo.online ? 'text-green-600' : 'text-red-600'}`}>
500
+ {systemInfo.online ? 'Online' : 'Offline'}
501
+ </span>
502
+ </p>
503
+ </div>
504
+ <div>
505
+ <p className="text-xs text-bolt-elements-textSecondary">Screen Resolution</p>
506
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.screen}</p>
507
+ </div>
508
+ <div>
509
+ <p className="text-xs text-bolt-elements-textSecondary">Language</p>
510
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.language}</p>
511
+ </div>
512
+ <div>
513
+ <p className="text-xs text-bolt-elements-textSecondary">Timezone</p>
514
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.timezone}</p>
515
+ </div>
516
+ <div>
517
+ <p className="text-xs text-bolt-elements-textSecondary">CPU Cores</p>
518
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.cores}</p>
519
+ </div>
520
+ </div>
521
+ <div className="mt-3 pt-3 border-t border-bolt-elements-surface-hover">
522
+ <p className="text-xs text-bolt-elements-textSecondary">Version</p>
523
+ <p className="text-sm font-medium text-bolt-elements-textPrimary font-mono">
524
+ {versionHash.slice(0, 7)}
525
+ <span className="ml-2 text-xs text-bolt-elements-textSecondary">
526
+ (v{versionTag || '0.0.1'}) - {isLatestBranch ? 'nightly' : 'stable'}
527
+ </span>
528
+ </p>
529
+ </div>
530
+ </div>
531
+ </div>
532
+
533
+ <div>
534
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary mb-2">Local LLM Status</h4>
535
+ <div className="bg-bolt-elements-surface rounded-lg">
536
+ <div className="grid grid-cols-1 divide-y">
537
+ {activeProviders.map((provider) => (
538
+ <div key={provider.name} className="p-3 flex flex-col space-y-2">
539
+ <div className="flex items-center justify-between">
540
+ <div className="flex items-center gap-3">
541
+ <div className="flex-shrink-0">
542
+ <div
543
+ className={`w-2 h-2 rounded-full ${
544
+ !provider.enabled ? 'bg-gray-300' : provider.isRunning ? 'bg-green-400' : 'bg-red-400'
545
+ }`}
546
+ />
547
+ </div>
548
+ <div>
549
+ <p className="text-sm font-medium text-bolt-elements-textPrimary">{provider.name}</p>
550
+ {provider.url && (
551
+ <p className="text-xs text-bolt-elements-textSecondary truncate max-w-[300px]">
552
+ {provider.url}
553
+ </p>
554
+ )}
555
+ </div>
556
+ </div>
557
+ <div className="flex items-center gap-2">
558
+ <span
559
+ className={`px-2 py-0.5 text-xs rounded-full ${
560
+ provider.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
561
+ }`}
562
+ >
563
+ {provider.enabled ? 'Enabled' : 'Disabled'}
564
+ </span>
565
+ {provider.enabled && (
566
+ <span
567
+ className={`px-2 py-0.5 text-xs rounded-full ${
568
+ provider.isRunning ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
569
+ }`}
570
+ >
571
+ {provider.isRunning ? 'Running' : 'Not Running'}
572
+ </span>
573
+ )}
574
+ </div>
575
+ </div>
576
+
577
+ <div className="pl-5 flex flex-col space-y-1 text-xs">
578
+ {/* Status Details */}
579
+ <div className="flex flex-wrap gap-2">
580
+ <span className="text-bolt-elements-textSecondary">
581
+ Last checked: {new Date(provider.lastChecked).toLocaleTimeString()}
582
+ </span>
583
+ {provider.responseTime && (
584
+ <span className="text-bolt-elements-textSecondary">
585
+ Response time: {Math.round(provider.responseTime)}ms
586
+ </span>
587
+ )}
588
+ </div>
589
+
590
+ {/* Error Message */}
591
+ {provider.error && (
592
+ <div className="mt-1 text-red-600 bg-red-50 rounded-md p-2">
593
+ <span className="font-medium">Error:</span> {provider.error}
594
+ </div>
595
+ )}
596
+
597
+ {/* Connection Info */}
598
+ {provider.url && (
599
+ <div className="text-bolt-elements-textSecondary">
600
+ <span className="font-medium">Endpoints checked:</span>
601
+ <ul className="list-disc list-inside pl-2 mt-1">
602
+ <li>{provider.url} (root)</li>
603
+ <li>{provider.url}/api/health</li>
604
+ <li>{provider.url}/v1/models</li>
605
+ </ul>
606
+ </div>
607
+ )}
608
+ </div>
609
+ </div>
610
+ ))}
611
+ {activeProviders.length === 0 && (
612
+ <div className="p-4 text-center text-bolt-elements-textSecondary">No local LLMs configured</div>
613
+ )}
614
+ </div>
615
+ </div>
616
+ </div>
617
+ </section>
618
+ </div>
619
+ );
620
+ }
app/components/settings/event-logs/EventLogsTab.tsx ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useEffect, useState, useMemo } from 'react';
2
+ import { useSettings } from '~/lib/hooks/useSettings';
3
+ import { toast } from 'react-toastify';
4
+ import { Switch } from '~/components/ui/Switch';
5
+ import { logStore, type LogEntry } from '~/lib/stores/logs';
6
+ import { useStore } from '@nanostores/react';
7
+ import { classNames } from '~/utils/classNames';
8
+
9
+ export default function EventLogsTab() {
10
+ const {} = useSettings();
11
+ const showLogs = useStore(logStore.showLogs);
12
+ const [logLevel, setLogLevel] = useState<LogEntry['level'] | 'all'>('info');
13
+ const [autoScroll, setAutoScroll] = useState(true);
14
+ const [searchQuery, setSearchQuery] = useState('');
15
+ const [, forceUpdate] = useState({});
16
+
17
+ const filteredLogs = useMemo(() => {
18
+ const logs = logStore.getLogs();
19
+ return logs.filter((log) => {
20
+ const matchesLevel = !logLevel || log.level === logLevel || logLevel === 'all';
21
+ const matchesSearch =
22
+ !searchQuery ||
23
+ log.message?.toLowerCase().includes(searchQuery.toLowerCase()) ||
24
+ JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase());
25
+
26
+ return matchesLevel && matchesSearch;
27
+ });
28
+ }, [logLevel, searchQuery]);
29
+
30
+ // Effect to initialize showLogs
31
+ useEffect(() => {
32
+ logStore.showLogs.set(true);
33
+ }, []);
34
+
35
+ useEffect(() => {
36
+ // System info logs
37
+ logStore.logSystem('Application initialized', {
38
+ version: process.env.NEXT_PUBLIC_APP_VERSION,
39
+ environment: process.env.NODE_ENV,
40
+ });
41
+
42
+ // Debug logs for system state
43
+ logStore.logDebug('System configuration loaded', {
44
+ runtime: 'Next.js',
45
+ features: ['AI Chat', 'Event Logging'],
46
+ });
47
+
48
+ // Warning logs for potential issues
49
+ logStore.logWarning('Resource usage threshold approaching', {
50
+ memoryUsage: '75%',
51
+ cpuLoad: '60%',
52
+ });
53
+
54
+ // Error logs with detailed context
55
+ logStore.logError('API connection failed', new Error('Connection timeout'), {
56
+ endpoint: '/api/chat',
57
+ retryCount: 3,
58
+ lastAttempt: new Date().toISOString(),
59
+ });
60
+ }, []);
61
+
62
+ useEffect(() => {
63
+ const container = document.querySelector('.logs-container');
64
+
65
+ if (container && autoScroll) {
66
+ container.scrollTop = container.scrollHeight;
67
+ }
68
+ }, [filteredLogs, autoScroll]);
69
+
70
+ const handleClearLogs = useCallback(() => {
71
+ if (confirm('Are you sure you want to clear all logs?')) {
72
+ logStore.clearLogs();
73
+ toast.success('Logs cleared successfully');
74
+ forceUpdate({}); // Force a re-render after clearing logs
75
+ }
76
+ }, []);
77
+
78
+ const handleExportLogs = useCallback(() => {
79
+ try {
80
+ const logText = logStore
81
+ .getLogs()
82
+ .map(
83
+ (log) =>
84
+ `[${log.level.toUpperCase()}] ${log.timestamp} - ${log.message}${
85
+ log.details ? '\nDetails: ' + JSON.stringify(log.details, null, 2) : ''
86
+ }`,
87
+ )
88
+ .join('\n\n');
89
+
90
+ const blob = new Blob([logText], { type: 'text/plain' });
91
+ const url = URL.createObjectURL(blob);
92
+ const a = document.createElement('a');
93
+ a.href = url;
94
+ a.download = `event-logs-${new Date().toISOString()}.txt`;
95
+ document.body.appendChild(a);
96
+ a.click();
97
+ document.body.removeChild(a);
98
+ URL.revokeObjectURL(url);
99
+ toast.success('Logs exported successfully');
100
+ } catch (error) {
101
+ toast.error('Failed to export logs');
102
+ console.error('Export error:', error);
103
+ }
104
+ }, []);
105
+
106
+ const getLevelColor = (level: LogEntry['level']) => {
107
+ switch (level) {
108
+ case 'info':
109
+ return 'text-blue-500';
110
+ case 'warning':
111
+ return 'text-yellow-500';
112
+ case 'error':
113
+ return 'text-red-500';
114
+ case 'debug':
115
+ return 'text-gray-500';
116
+ default:
117
+ return 'text-bolt-elements-textPrimary';
118
+ }
119
+ };
120
+
121
+ return (
122
+ <div className="p-4 h-full flex flex-col">
123
+ <div className="flex flex-col space-y-4 mb-4">
124
+ {/* Title and Toggles Row */}
125
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
126
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3>
127
+ <div className="flex flex-wrap items-center gap-4">
128
+ <div className="flex items-center space-x-2">
129
+ <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Show Actions</span>
130
+ <Switch checked={showLogs} onCheckedChange={(checked) => logStore.showLogs.set(checked)} />
131
+ </div>
132
+ <div className="flex items-center space-x-2">
133
+ <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Auto-scroll</span>
134
+ <Switch checked={autoScroll} onCheckedChange={setAutoScroll} />
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ {/* Controls Row */}
140
+ <div className="flex flex-wrap items-center gap-2">
141
+ <select
142
+ value={logLevel}
143
+ onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])}
144
+ className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[20%] text-sm min-w-[100px]"
145
+ >
146
+ <option value="all">All</option>
147
+ <option value="info">Info</option>
148
+ <option value="warning">Warning</option>
149
+ <option value="error">Error</option>
150
+ <option value="debug">Debug</option>
151
+ </select>
152
+ <div className="flex-1 min-w-[200px]">
153
+ <input
154
+ type="text"
155
+ placeholder="Search logs..."
156
+ value={searchQuery}
157
+ onChange={(e) => setSearchQuery(e.target.value)}
158
+ className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
159
+ />
160
+ </div>
161
+ {showLogs && (
162
+ <div className="flex items-center gap-2 flex-nowrap">
163
+ <button
164
+ onClick={handleExportLogs}
165
+ className={classNames(
166
+ 'bg-bolt-elements-button-primary-background',
167
+ 'rounded-lg px-4 py-2 transition-colors duration-200',
168
+ 'hover:bg-bolt-elements-button-primary-backgroundHover',
169
+ 'text-bolt-elements-button-primary-text',
170
+ )}
171
+ >
172
+ Export Logs
173
+ </button>
174
+ <button
175
+ onClick={handleClearLogs}
176
+ className={classNames(
177
+ 'bg-bolt-elements-button-danger-background',
178
+ 'rounded-lg px-4 py-2 transition-colors duration-200',
179
+ 'hover:bg-bolt-elements-button-danger-backgroundHover',
180
+ 'text-bolt-elements-button-danger-text',
181
+ )}
182
+ >
183
+ Clear Logs
184
+ </button>
185
+ </div>
186
+ )}
187
+ </div>
188
+ </div>
189
+
190
+ <div className="bg-bolt-elements-bg-depth-1 rounded-lg p-4 h-[calc(100vh - 250px)] min-h-[400px] overflow-y-auto logs-container overflow-y-auto">
191
+ {filteredLogs.length === 0 ? (
192
+ <div className="text-center text-bolt-elements-textSecondary py-8">No logs found</div>
193
+ ) : (
194
+ filteredLogs.map((log, index) => (
195
+ <div
196
+ key={index}
197
+ className="text-sm mb-3 font-mono border-b border-bolt-elements-borderColor pb-2 last:border-0"
198
+ >
199
+ <div className="flex items-start space-x-2 flex-wrap">
200
+ <span className={`font-bold ${getLevelColor(log.level)} whitespace-nowrap`}>
201
+ [{log.level.toUpperCase()}]
202
+ </span>
203
+ <span className="text-bolt-elements-textSecondary whitespace-nowrap">
204
+ {new Date(log.timestamp).toLocaleString()}
205
+ </span>
206
+ <span className="text-bolt-elements-textPrimary break-all">{log.message}</span>
207
+ </div>
208
+ {log.details && (
209
+ <pre className="mt-2 text-xs text-bolt-elements-textSecondary overflow-x-auto whitespace-pre-wrap break-all">
210
+ {JSON.stringify(log.details, null, 2)}
211
+ </pre>
212
+ )}
213
+ </div>
214
+ ))
215
+ )}
216
+ </div>
217
+ </div>
218
+ );
219
+ }
app/components/settings/features/FeaturesTab.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Switch } from '~/components/ui/Switch';
3
+ import { PromptLibrary } from '~/lib/common/prompt-library';
4
+ import { useSettings } from '~/lib/hooks/useSettings';
5
+
6
+ export default function FeaturesTab() {
7
+ const {
8
+ debug,
9
+ enableDebugMode,
10
+ isLocalModel,
11
+ enableLocalModels,
12
+ enableEventLogs,
13
+ isLatestBranch,
14
+ enableLatestBranch,
15
+ promptId,
16
+ setPromptId,
17
+ } = useSettings();
18
+
19
+ const handleToggle = (enabled: boolean) => {
20
+ enableDebugMode(enabled);
21
+ enableEventLogs(enabled);
22
+ };
23
+
24
+ return (
25
+ <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
26
+ <div className="mb-6">
27
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Optional Features</h3>
28
+ <div className="space-y-4">
29
+ <div className="flex items-center justify-between">
30
+ <span className="text-bolt-elements-textPrimary">Debug Features</span>
31
+ <Switch className="ml-auto" checked={debug} onCheckedChange={handleToggle} />
32
+ </div>
33
+ <div className="flex items-center justify-between">
34
+ <div>
35
+ <span className="text-bolt-elements-textPrimary">Use Main Branch</span>
36
+ <p className="text-sm text-bolt-elements-textSecondary">
37
+ Check for updates against the main branch instead of stable
38
+ </p>
39
+ </div>
40
+ <Switch className="ml-auto" checked={isLatestBranch} onCheckedChange={enableLatestBranch} />
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
46
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Experimental Features</h3>
47
+ <p className="text-sm text-bolt-elements-textSecondary mb-4">
48
+ Disclaimer: Experimental features may be unstable and are subject to change.
49
+ </p>
50
+
51
+ <div className="flex items-center justify-between mb-2">
52
+ <span className="text-bolt-elements-textPrimary">Experimental Providers</span>
53
+ <Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
54
+ </div>
55
+ <div className="flex items-start justify-between pt-4 mb-2 gap-2">
56
+ <div className="flex-1 max-w-[200px]">
57
+ <span className="text-bolt-elements-textPrimary">Prompt Library</span>
58
+ <p className="text-sm text-bolt-elements-textSecondary mb-4">
59
+ Choose a prompt from the library to use as the system prompt.
60
+ </p>
61
+ </div>
62
+ <select
63
+ value={promptId}
64
+ onChange={(e) => setPromptId(e.target.value)}
65
+ className="flex-1 p-2 ml-auto rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all text-sm min-w-[100px]"
66
+ >
67
+ {PromptLibrary.getList().map((x) => (
68
+ <option value={x.id}>{x.label}</option>
69
+ ))}
70
+ </select>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ );
75
+ }
app/components/settings/providers/ProvidersTab.tsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } 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
+
8
+ // Import a default fallback icon
9
+ import DefaultIcon from '/icons/Default.svg'; // Adjust the path as necessary
10
+
11
+ export default function ProvidersTab() {
12
+ const { providers, updateProviderSettings, isLocalModel } = useSettings();
13
+ const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
14
+
15
+ // Load base URLs from cookies
16
+ const [searchTerm, setSearchTerm] = useState('');
17
+
18
+ useEffect(() => {
19
+ let newFilteredProviders: IProviderConfig[] = Object.entries(providers).map(([key, value]) => ({
20
+ ...value,
21
+ name: key,
22
+ }));
23
+
24
+ if (searchTerm && searchTerm.length > 0) {
25
+ newFilteredProviders = newFilteredProviders.filter((provider) =>
26
+ provider.name.toLowerCase().includes(searchTerm.toLowerCase()),
27
+ );
28
+ }
29
+
30
+ if (!isLocalModel) {
31
+ newFilteredProviders = newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name));
32
+ }
33
+
34
+ newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
35
+
36
+ setFilteredProviders(newFilteredProviders);
37
+ }, [providers, searchTerm, isLocalModel]);
38
+
39
+ return (
40
+ <div className="p-4">
41
+ <div className="flex mb-4">
42
+ <input
43
+ type="text"
44
+ placeholder="Search providers..."
45
+ value={searchTerm}
46
+ onChange={(e) => setSearchTerm(e.target.value)}
47
+ className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
48
+ />
49
+ </div>
50
+ {filteredProviders.map((provider) => (
51
+ <div
52
+ key={provider.name}
53
+ className="flex flex-col mb-2 provider-item hover:bg-bolt-elements-bg-depth-3 p-4 rounded-lg border border-bolt-elements-borderColor "
54
+ >
55
+ <div className="flex items-center justify-between mb-2">
56
+ <div className="flex items-center gap-2">
57
+ <img
58
+ src={`/icons/${provider.name}.svg`} // Attempt to load the specific icon
59
+ onError={(e) => {
60
+ // Fallback to default icon on error
61
+ e.currentTarget.src = DefaultIcon;
62
+ }}
63
+ alt={`${provider.name} icon`}
64
+ className="w-6 h-6 dark:invert"
65
+ />
66
+ <span className="text-bolt-elements-textPrimary">{provider.name}</span>
67
+ </div>
68
+ <Switch
69
+ className="ml-auto"
70
+ checked={provider.settings.enabled}
71
+ onCheckedChange={(enabled) => {
72
+ updateProviderSettings(provider.name, { ...provider.settings, enabled });
73
+
74
+ if (enabled) {
75
+ logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
76
+ } else {
77
+ logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
78
+ }
79
+ }}
80
+ />
81
+ </div>
82
+ {/* Base URL input for configurable providers */}
83
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.settings.enabled && (
84
+ <div className="mt-2">
85
+ <label className="block text-sm text-bolt-elements-textSecondary mb-1">Base URL:</label>
86
+ <input
87
+ type="text"
88
+ value={provider.settings.baseUrl || ''}
89
+ onChange={(e) => {
90
+ const newBaseUrl = e.target.value;
91
+ updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
92
+ logStore.logProvider(`Base URL updated for ${provider.name}`, {
93
+ provider: provider.name,
94
+ baseUrl: newBaseUrl,
95
+ });
96
+ }}
97
+ placeholder={`Enter ${provider.name} base URL`}
98
+ className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
99
+ />
100
+ </div>
101
+ )}
102
+ </div>
103
+ ))}
104
+ </div>
105
+ );
106
+ }
app/components/sidebar/Menu.client.tsx CHANGED
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
6
- import { Settings } from '~/components/ui/Settings';
7
  import { SettingsButton } from '~/components/ui/SettingsButton';
8
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
9
  import { cubicEasingFn } from '~/utils/easings';
@@ -35,6 +35,25 @@ const menuVariants = {
35
 
36
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  export const Menu = () => {
39
  const { duplicateCurrentChat, exportChat } = useChatHistory();
40
  const menuRef = useRef<HTMLDivElement>(null);
@@ -126,18 +145,17 @@ export const Menu = () => {
126
  variants={menuVariants}
127
  className="flex selection-accent flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
128
  >
129
- <div className="flex items-center h-[var(--header-height)]">{/* Placeholder */}</div>
 
130
  <div className="flex-1 flex flex-col h-full w-full overflow-hidden">
131
  <div className="p-4 select-none">
132
  <a
133
  href="/"
134
- className="flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
135
  >
136
  <span className="inline-block i-bolt:chat scale-110" />
137
  Start new chat
138
  </a>
139
- </div>
140
- <div className="pl-4 pr-4 my-2">
141
  <div className="relative w-full">
142
  <input
143
  className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
@@ -208,7 +226,7 @@ export const Menu = () => {
208
  <ThemeSwitch />
209
  </div>
210
  </div>
211
- <Settings open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
212
  </motion.div>
213
  );
214
  };
 
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
6
+ import { SettingsWindow } from '~/components/settings/SettingsWindow';
7
  import { SettingsButton } from '~/components/ui/SettingsButton';
8
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
9
  import { cubicEasingFn } from '~/utils/easings';
 
35
 
36
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
37
 
38
+ function CurrentDateTime() {
39
+ const [dateTime, setDateTime] = useState(new Date());
40
+
41
+ useEffect(() => {
42
+ const timer = setInterval(() => {
43
+ setDateTime(new Date());
44
+ }, 60000); // Update every minute
45
+
46
+ return () => clearInterval(timer);
47
+ }, []);
48
+
49
+ return (
50
+ <div className="flex items-center gap-2 px-4 py-3 font-bold text-gray-700 dark:text-gray-300 border-b border-bolt-elements-borderColor">
51
+ <div className="h-4 w-4 i-ph:clock-thin" />
52
+ {dateTime.toLocaleDateString()} {dateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
53
+ </div>
54
+ );
55
+ }
56
+
57
  export const Menu = () => {
58
  const { duplicateCurrentChat, exportChat } = useChatHistory();
59
  const menuRef = useRef<HTMLDivElement>(null);
 
145
  variants={menuVariants}
146
  className="flex selection-accent flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
147
  >
148
+ <div className="h-[60px]" /> {/* Spacer for top margin */}
149
+ <CurrentDateTime />
150
  <div className="flex-1 flex flex-col h-full w-full overflow-hidden">
151
  <div className="p-4 select-none">
152
  <a
153
  href="/"
154
+ className="flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme mb-4"
155
  >
156
  <span className="inline-block i-bolt:chat scale-110" />
157
  Start new chat
158
  </a>
 
 
159
  <div className="relative w-full">
160
  <input
161
  className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
 
226
  <ThemeSwitch />
227
  </div>
228
  </div>
229
+ <SettingsWindow open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
230
  </motion.div>
231
  );
232
  };
app/components/ui/BackgroundRays/index.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import styles from './styles.module.scss';
2
+
3
+ const BackgroundRays = () => {
4
+ return (
5
+ <div className={`${styles.rayContainer} `}>
6
+ <div className={`${styles.lightRay} ${styles.ray1}`}></div>
7
+ <div className={`${styles.lightRay} ${styles.ray2}`}></div>
8
+ <div className={`${styles.lightRay} ${styles.ray3}`}></div>
9
+ <div className={`${styles.lightRay} ${styles.ray4}`}></div>
10
+ <div className={`${styles.lightRay} ${styles.ray5}`}></div>
11
+ <div className={`${styles.lightRay} ${styles.ray6}`}></div>
12
+ <div className={`${styles.lightRay} ${styles.ray7}`}></div>
13
+ <div className={`${styles.lightRay} ${styles.ray8}`}></div>
14
+ </div>
15
+ );
16
+ };
17
+
18
+ export default BackgroundRays;
app/components/ui/BackgroundRays/styles.module.scss ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .rayContainer {
2
+ // Theme-specific colors
3
+ --ray-color-primary: color-mix(in srgb, var(--primary-color), transparent 30%);
4
+ --ray-color-secondary: color-mix(in srgb, var(--secondary-color), transparent 30%);
5
+ --ray-color-accent: color-mix(in srgb, var(--accent-color), transparent 30%);
6
+
7
+ // Theme-specific gradients
8
+ --ray-gradient-primary: radial-gradient(var(--ray-color-primary) 0%, transparent 70%);
9
+ --ray-gradient-secondary: radial-gradient(var(--ray-color-secondary) 0%, transparent 70%);
10
+ --ray-gradient-accent: radial-gradient(var(--ray-color-accent) 0%, transparent 70%);
11
+
12
+ position: fixed;
13
+ inset: 0;
14
+ overflow: hidden;
15
+ animation: fadeIn 1.5s ease-out;
16
+ pointer-events: none;
17
+ z-index: 0;
18
+ // background-color: transparent;
19
+
20
+ :global(html[data-theme='dark']) & {
21
+ mix-blend-mode: screen;
22
+ }
23
+
24
+ :global(html[data-theme='light']) & {
25
+ mix-blend-mode: multiply;
26
+ }
27
+ }
28
+
29
+ .lightRay {
30
+ position: absolute;
31
+ border-radius: 100%;
32
+
33
+ :global(html[data-theme='dark']) & {
34
+ mix-blend-mode: screen;
35
+ }
36
+
37
+ :global(html[data-theme='light']) & {
38
+ mix-blend-mode: multiply;
39
+ opacity: 0.4;
40
+ }
41
+ }
42
+
43
+ .ray1 {
44
+ width: 600px;
45
+ height: 800px;
46
+ background: var(--ray-gradient-primary);
47
+ transform: rotate(65deg);
48
+ top: -500px;
49
+ left: -100px;
50
+ filter: blur(80px);
51
+ opacity: 0.6;
52
+ animation: float1 15s infinite ease-in-out;
53
+ }
54
+
55
+ .ray2 {
56
+ width: 400px;
57
+ height: 600px;
58
+ background: var(--ray-gradient-secondary);
59
+ transform: rotate(-30deg);
60
+ top: -300px;
61
+ left: 200px;
62
+ filter: blur(60px);
63
+ opacity: 0.6;
64
+ animation: float2 18s infinite ease-in-out;
65
+ }
66
+
67
+ .ray3 {
68
+ width: 500px;
69
+ height: 400px;
70
+ background: var(--ray-gradient-accent);
71
+ top: -320px;
72
+ left: 500px;
73
+ filter: blur(65px);
74
+ opacity: 0.5;
75
+ animation: float3 20s infinite ease-in-out;
76
+ }
77
+
78
+ .ray4 {
79
+ width: 400px;
80
+ height: 450px;
81
+ background: var(--ray-gradient-secondary);
82
+ top: -350px;
83
+ left: 800px;
84
+ filter: blur(55px);
85
+ opacity: 0.55;
86
+ animation: float4 17s infinite ease-in-out;
87
+ }
88
+
89
+ .ray5 {
90
+ width: 350px;
91
+ height: 500px;
92
+ background: var(--ray-gradient-primary);
93
+ transform: rotate(-45deg);
94
+ top: -250px;
95
+ left: 1000px;
96
+ filter: blur(45px);
97
+ opacity: 0.6;
98
+ animation: float5 16s infinite ease-in-out;
99
+ }
100
+
101
+ .ray6 {
102
+ width: 300px;
103
+ height: 700px;
104
+ background: var(--ray-gradient-accent);
105
+ transform: rotate(75deg);
106
+ top: -400px;
107
+ left: 600px;
108
+ filter: blur(75px);
109
+ opacity: 0.45;
110
+ animation: float6 19s infinite ease-in-out;
111
+ }
112
+
113
+ .ray7 {
114
+ width: 450px;
115
+ height: 600px;
116
+ background: var(--ray-gradient-primary);
117
+ transform: rotate(45deg);
118
+ top: -450px;
119
+ left: 350px;
120
+ filter: blur(65px);
121
+ opacity: 0.55;
122
+ animation: float7 21s infinite ease-in-out;
123
+ }
124
+
125
+ .ray8 {
126
+ width: 380px;
127
+ height: 550px;
128
+ background: var(--ray-gradient-secondary);
129
+ transform: rotate(-60deg);
130
+ top: -380px;
131
+ left: 750px;
132
+ filter: blur(58px);
133
+ opacity: 0.6;
134
+ animation: float8 14s infinite ease-in-out;
135
+ }
136
+
137
+ @keyframes float1 {
138
+ 0%,
139
+ 100% {
140
+ transform: rotate(65deg) translate(0, 0);
141
+ }
142
+ 25% {
143
+ transform: rotate(70deg) translate(30px, 20px);
144
+ }
145
+ 50% {
146
+ transform: rotate(60deg) translate(-20px, 40px);
147
+ }
148
+ 75% {
149
+ transform: rotate(68deg) translate(-40px, 10px);
150
+ }
151
+ }
152
+
153
+ @keyframes float2 {
154
+ 0%,
155
+ 100% {
156
+ transform: rotate(-30deg) scale(1);
157
+ }
158
+ 33% {
159
+ transform: rotate(-25deg) scale(1.1);
160
+ }
161
+ 66% {
162
+ transform: rotate(-35deg) scale(0.95);
163
+ }
164
+ }
165
+
166
+ @keyframes float3 {
167
+ 0%,
168
+ 100% {
169
+ transform: translate(0, 0) rotate(0deg);
170
+ }
171
+ 25% {
172
+ transform: translate(40px, 20px) rotate(5deg);
173
+ }
174
+ 75% {
175
+ transform: translate(-30px, 40px) rotate(-5deg);
176
+ }
177
+ }
178
+
179
+ @keyframes float4 {
180
+ 0%,
181
+ 100% {
182
+ transform: scale(1) rotate(0deg);
183
+ }
184
+ 50% {
185
+ transform: scale(1.15) rotate(10deg);
186
+ }
187
+ }
188
+
189
+ @keyframes float5 {
190
+ 0%,
191
+ 100% {
192
+ transform: rotate(-45deg) translate(0, 0);
193
+ }
194
+ 33% {
195
+ transform: rotate(-40deg) translate(25px, -20px);
196
+ }
197
+ 66% {
198
+ transform: rotate(-50deg) translate(-25px, 20px);
199
+ }
200
+ }
201
+
202
+ @keyframes float6 {
203
+ 0%,
204
+ 100% {
205
+ transform: rotate(75deg) scale(1);
206
+ filter: blur(75px);
207
+ }
208
+ 50% {
209
+ transform: rotate(85deg) scale(1.1);
210
+ filter: blur(65px);
211
+ }
212
+ }
213
+
214
+ @keyframes float7 {
215
+ 0%,
216
+ 100% {
217
+ transform: rotate(45deg) translate(0, 0);
218
+ opacity: 0.55;
219
+ }
220
+ 50% {
221
+ transform: rotate(40deg) translate(-30px, 30px);
222
+ opacity: 0.65;
223
+ }
224
+ }
225
+
226
+ @keyframes float8 {
227
+ 0%,
228
+ 100% {
229
+ transform: rotate(-60deg) scale(1);
230
+ }
231
+ 25% {
232
+ transform: rotate(-55deg) scale(1.05);
233
+ }
234
+ 75% {
235
+ transform: rotate(-65deg) scale(0.95);
236
+ }
237
+ }
238
+
239
+ @keyframes fadeIn {
240
+ from {
241
+ opacity: 0;
242
+ }
243
+ to {
244
+ opacity: 1;
245
+ }
246
+ }
app/components/ui/IconButton.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { memo } from 'react';
2
  import { classNames } from '~/utils/classNames';
3
 
4
  type IconSize = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
@@ -25,41 +25,48 @@ type IconButtonWithChildrenProps = {
25
 
26
  type IconButtonProps = IconButtonWithoutChildrenProps | IconButtonWithChildrenProps;
27
 
 
28
  export const IconButton = memo(
29
- ({
30
- icon,
31
- size = 'xl',
32
- className,
33
- iconClassName,
34
- disabledClassName,
35
- disabled = false,
36
- title,
37
- onClick,
38
- children,
39
- }: IconButtonProps) => {
40
- return (
41
- <button
42
- className={classNames(
43
- 'flex items-center text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',
44
- {
45
- [classNames('opacity-30', disabledClassName)]: disabled,
46
- },
47
- className,
48
- )}
49
- title={title}
50
- disabled={disabled}
51
- onClick={(event) => {
52
- if (disabled) {
53
- return;
54
- }
 
 
 
 
 
55
 
56
- onClick?.(event);
57
- }}
58
- >
59
- {children ? children : <div className={classNames(icon, getIconSize(size), iconClassName)}></div>}
60
- </button>
61
- );
62
- },
 
63
  );
64
 
65
  function getIconSize(size: IconSize) {
 
1
+ import { memo, forwardRef, type ForwardedRef } from 'react';
2
  import { classNames } from '~/utils/classNames';
3
 
4
  type IconSize = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
 
25
 
26
  type IconButtonProps = IconButtonWithoutChildrenProps | IconButtonWithChildrenProps;
27
 
28
+ // Componente IconButton com suporte a refs
29
  export const IconButton = memo(
30
+ forwardRef(
31
+ (
32
+ {
33
+ icon,
34
+ size = 'xl',
35
+ className,
36
+ iconClassName,
37
+ disabledClassName,
38
+ disabled = false,
39
+ title,
40
+ onClick,
41
+ children,
42
+ }: IconButtonProps,
43
+ ref: ForwardedRef<HTMLButtonElement>,
44
+ ) => {
45
+ return (
46
+ <button
47
+ ref={ref}
48
+ className={classNames(
49
+ 'flex items-center text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',
50
+ {
51
+ [classNames('opacity-30', disabledClassName)]: disabled,
52
+ },
53
+ className,
54
+ )}
55
+ title={title}
56
+ disabled={disabled}
57
+ onClick={(event) => {
58
+ if (disabled) {
59
+ return;
60
+ }
61
 
62
+ onClick?.(event);
63
+ }}
64
+ >
65
+ {children ? children : <div className={classNames(icon, getIconSize(size), iconClassName)}></div>}
66
+ </button>
67
+ );
68
+ },
69
+ ),
70
  );
71
 
72
  function getIconSize(size: IconSize) {
app/components/ui/Settings.tsx DELETED
@@ -1,444 +0,0 @@
1
- import * as RadixDialog from '@radix-ui/react-dialog';
2
- import { motion } from 'framer-motion';
3
- import { useState } from 'react';
4
- import { classNames } from '~/utils/classNames';
5
- import { DialogTitle, dialogVariants, dialogBackdropVariants } from './Dialog';
6
- import { IconButton } from './IconButton';
7
- import { providersList } from '~/lib/stores/settings';
8
- import { db, getAll, deleteById } from '~/lib/persistence';
9
- import { toast } from 'react-toastify';
10
- import { useNavigate } from '@remix-run/react';
11
- import commit from '~/commit.json';
12
- import Cookies from 'js-cookie';
13
- import '~/styles/components/SettingsSlider.scss';
14
- import '~/styles/components/Settings.scss';
15
-
16
- interface SettingsProps {
17
- open: boolean;
18
- onClose: () => void;
19
- }
20
-
21
- type TabType = 'chat-history' | 'providers' | 'features' | 'debug';
22
-
23
- // Providers that support base URL configuration
24
- const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
25
-
26
- export const Settings = ({ open, onClose }: SettingsProps) => {
27
- const navigate = useNavigate();
28
- const [activeTab, setActiveTab] = useState<TabType>('chat-history');
29
- const [isDebugEnabled, setIsDebugEnabled] = useState(false);
30
- const [searchTerm, setSearchTerm] = useState('');
31
- const [isDeleting, setIsDeleting] = useState(false);
32
- const [isJustSayEnabled, setIsJustSayEnabled] = useState(false);
33
-
34
- // Load base URLs from cookies
35
- const [baseUrls, setBaseUrls] = useState(() => {
36
- const savedUrls = Cookies.get('providerBaseUrls');
37
-
38
- if (savedUrls) {
39
- try {
40
- return JSON.parse(savedUrls);
41
- } catch (error) {
42
- console.error('Failed to parse base URLs from cookies:', error);
43
- return {
44
- Ollama: 'http://localhost:11434',
45
- LMStudio: 'http://localhost:1234',
46
- OpenAILike: '',
47
- };
48
- }
49
- }
50
-
51
- return {
52
- Ollama: 'http://localhost:11434',
53
- LMStudio: 'http://localhost:1234',
54
- OpenAILike: '',
55
- };
56
- });
57
-
58
- const handleBaseUrlChange = (provider: string, url: string) => {
59
- setBaseUrls((prev: Record<string, string>) => {
60
- const newUrls = { ...prev, [provider]: url };
61
- Cookies.set('providerBaseUrls', JSON.stringify(newUrls));
62
-
63
- return newUrls;
64
- });
65
- };
66
-
67
- const tabs: { id: TabType; label: string; icon: string }[] = [
68
- { id: 'chat-history', label: 'Chat History', icon: 'i-ph:book' },
69
- { id: 'providers', label: 'Providers', icon: 'i-ph:key' },
70
- { id: 'features', label: 'Features', icon: 'i-ph:star' },
71
- ...(isDebugEnabled ? [{ id: 'debug' as TabType, label: 'Debug Tab', icon: 'i-ph:bug' }] : []),
72
- ];
73
-
74
- // Load providers from cookies on mount
75
- const [providers, setProviders] = useState(() => {
76
- const savedProviders = Cookies.get('providers');
77
-
78
- if (savedProviders) {
79
- try {
80
- const parsedProviders = JSON.parse(savedProviders);
81
-
82
- // Merge saved enabled states with the base provider list
83
- return providersList.map((provider) => ({
84
- ...provider,
85
- isEnabled: parsedProviders[provider.name] || false,
86
- }));
87
- } catch (error) {
88
- console.error('Failed to parse providers from cookies:', error);
89
- }
90
- }
91
-
92
- return providersList;
93
- });
94
-
95
- const handleToggleProvider = (providerName: string) => {
96
- setProviders((prevProviders) => {
97
- const newProviders = prevProviders.map((provider) =>
98
- provider.name === providerName ? { ...provider, isEnabled: !provider.isEnabled } : provider,
99
- );
100
-
101
- // Save to cookies
102
- const enabledStates = newProviders.reduce(
103
- (acc, provider) => ({
104
- ...acc,
105
- [provider.name]: provider.isEnabled,
106
- }),
107
- {},
108
- );
109
- Cookies.set('providers', JSON.stringify(enabledStates));
110
-
111
- return newProviders;
112
- });
113
- };
114
-
115
- const filteredProviders = providers
116
- .filter((provider) => provider.name.toLowerCase().includes(searchTerm.toLowerCase()))
117
- .sort((a, b) => a.name.localeCompare(b.name));
118
-
119
- const handleCopyToClipboard = () => {
120
- const debugInfo = {
121
- OS: navigator.platform,
122
- Browser: navigator.userAgent,
123
- ActiveFeatures: providers.filter((provider) => provider.isEnabled).map((provider) => provider.name),
124
- BaseURLs: {
125
- Ollama: process.env.REACT_APP_OLLAMA_URL,
126
- OpenAI: process.env.REACT_APP_OPENAI_URL,
127
- LMStudio: process.env.REACT_APP_LM_STUDIO_URL,
128
- },
129
- Version: versionHash,
130
- };
131
- navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
132
- alert('Debug information copied to clipboard!');
133
- });
134
- };
135
-
136
- const downloadAsJson = (data: any, filename: string) => {
137
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
138
- const url = URL.createObjectURL(blob);
139
- const link = document.createElement('a');
140
- link.href = url;
141
- link.download = filename;
142
- document.body.appendChild(link);
143
- link.click();
144
- document.body.removeChild(link);
145
- URL.revokeObjectURL(url);
146
- };
147
-
148
- const handleDeleteAllChats = async () => {
149
- if (!db) {
150
- toast.error('Database is not available');
151
- return;
152
- }
153
-
154
- try {
155
- setIsDeleting(true);
156
-
157
- const allChats = await getAll(db);
158
-
159
- // Delete all chats one by one
160
- await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
161
-
162
- toast.success('All chats deleted successfully');
163
- navigate('/', { replace: true });
164
- } catch (error) {
165
- toast.error('Failed to delete chats');
166
- console.error(error);
167
- } finally {
168
- setIsDeleting(false);
169
- }
170
- };
171
-
172
- const handleExportAllChats = async () => {
173
- if (!db) {
174
- toast.error('Database is not available');
175
- return;
176
- }
177
-
178
- try {
179
- const allChats = await getAll(db);
180
- const exportData = {
181
- chats: allChats,
182
- exportDate: new Date().toISOString(),
183
- };
184
-
185
- downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
186
- toast.success('Chats exported successfully');
187
- } catch (error) {
188
- toast.error('Failed to export chats');
189
- console.error(error);
190
- }
191
- };
192
-
193
- const versionHash = commit.commit; // Get the version hash from commit.json
194
-
195
- return (
196
- <RadixDialog.Root open={open}>
197
- <RadixDialog.Portal>
198
- <RadixDialog.Overlay asChild>
199
- <motion.div
200
- className="bg-black/50 fixed inset-0 z-max"
201
- initial="closed"
202
- animate="open"
203
- exit="closed"
204
- variants={dialogBackdropVariants}
205
- />
206
- </RadixDialog.Overlay>
207
- <RadixDialog.Content asChild>
208
- <motion.div
209
- className="fixed top-[50%] left-[50%] z-max h-[85vh] w-[90vw] max-w-[900px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg shadow-lg focus:outline-none overflow-hidden"
210
- initial="closed"
211
- animate="open"
212
- exit="closed"
213
- variants={dialogVariants}
214
- >
215
- <div className="flex h-full">
216
- <div className="w-48 border-r border-bolt-elements-borderColor bg-white dark:bg-gray-900 p-4 flex flex-col justify-between settings-tabs">
217
- {tabs.map((tab) => (
218
- <button
219
- key={tab.id}
220
- onClick={() => setActiveTab(tab.id)}
221
- className={classNames(activeTab === tab.id ? 'active' : '')}
222
- >
223
- <div className={tab.icon} />
224
- {tab.label}
225
- </button>
226
- ))}
227
- <div className="mt-auto flex flex-col gap-2">
228
- <a
229
- href="https://github.com/coleam00/bolt.new-any-llm"
230
- target="_blank"
231
- rel="noopener noreferrer"
232
- className="settings-button flex items-center gap-2"
233
- >
234
- <div className="i-ph:github-logo" />
235
- GitHub
236
- </a>
237
- <a
238
- href="https://coleam00.github.io/bolt.new-any-llm"
239
- target="_blank"
240
- rel="noopener noreferrer"
241
- className="settings-button flex items-center gap-2"
242
- >
243
- <div className="i-ph:book" />
244
- Docs
245
- </a>
246
- </div>
247
- </div>
248
-
249
- <div className="flex-1 flex flex-col p-8 bg-gray-50 dark:bg-gray-800">
250
- <DialogTitle className="flex-shrink-0 text-lg font-semibold text-bolt-elements-textPrimary">
251
- Settings
252
- </DialogTitle>
253
- <div className="flex-1 overflow-y-auto">
254
- {activeTab === 'chat-history' && (
255
- <div className="p-4">
256
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Chat History</h3>
257
- <button
258
- onClick={handleExportAllChats}
259
- className="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600 mb-4 transition-colors duration-200"
260
- >
261
- Export All Chats
262
- </button>
263
-
264
- <div className="text-bolt-elements-textPrimary rounded-lg p-4 mb-4 settings-danger-area">
265
- <h4 className="font-semibold">Danger Area</h4>
266
- <p className="mb-2">This action cannot be undone!</p>
267
- <button
268
- onClick={handleDeleteAllChats}
269
- disabled={isDeleting}
270
- className={classNames(
271
- 'bg-red-700 text-white rounded-lg px-4 py-2 transition-colors duration-200',
272
- isDeleting ? 'opacity-50 cursor-not-allowed' : 'hover:bg-red-800',
273
- )}
274
- >
275
- {isDeleting ? 'Deleting...' : 'Delete All Chats'}
276
- </button>
277
- </div>
278
- </div>
279
- )}
280
- {activeTab === 'providers' && (
281
- <div className="p-4">
282
- <div className="flex items-center justify-between mb-4">
283
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Providers</h3>
284
- <input
285
- type="text"
286
- placeholder="Search providers..."
287
- value={searchTerm}
288
- onChange={(e) => setSearchTerm(e.target.value)}
289
- className="mb-4 p-2 rounded border border-gray-300"
290
- />
291
- </div>
292
- {filteredProviders.map((provider) => (
293
- <div
294
- key={provider.name}
295
- className="flex flex-col mb-6 provider-item hover:bg-bolt-elements-bg-depth-3 p-4 rounded-lg"
296
- >
297
- <div className="flex items-center justify-between mb-2">
298
- <span className="text-bolt-elements-textPrimary">{provider.name}</span>
299
- <label className="relative inline-flex items-center cursor-pointer">
300
- <input
301
- type="checkbox"
302
- className="sr-only"
303
- checked={provider.isEnabled}
304
- onChange={() => handleToggleProvider(provider.name)}
305
- />
306
- <div
307
- className={classNames(
308
- 'settings-toggle__track',
309
- provider.isEnabled
310
- ? 'settings-toggle__track--enabled'
311
- : 'settings-toggle__track--disabled',
312
- )}
313
- ></div>
314
- <div
315
- className={classNames(
316
- 'settings-toggle__thumb',
317
- provider.isEnabled ? 'settings-toggle__thumb--enabled' : '',
318
- )}
319
- ></div>
320
- </label>
321
- </div>
322
- {/* Base URL input for configurable providers */}
323
- {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.isEnabled && (
324
- <div className="mt-2">
325
- <label className="block text-sm text-bolt-elements-textSecondary mb-1">Base URL:</label>
326
- <input
327
- type="text"
328
- value={baseUrls[provider.name]}
329
- onChange={(e) => handleBaseUrlChange(provider.name, e.target.value)}
330
- placeholder={`Enter ${provider.name} base URL`}
331
- className="w-full p-2 rounded border border-bolt-elements-borderColor bg-bolt-elements-bg-depth-2 text-bolt-elements-textPrimary text-sm"
332
- />
333
- </div>
334
- )}
335
- </div>
336
- ))}
337
- </div>
338
- )}
339
- {activeTab === 'features' && (
340
- <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
341
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Feature Settings</h3>
342
- <div className="flex items-center justify-between mb-2">
343
- <span className="text-bolt-elements-textPrimary">Debug Info</span>
344
- <label className="relative inline-flex items-center cursor-pointer">
345
- <input
346
- type="checkbox"
347
- className="sr-only"
348
- checked={isDebugEnabled}
349
- onChange={() => setIsDebugEnabled(!isDebugEnabled)}
350
- />
351
- <div
352
- className={classNames(
353
- 'settings-toggle__track',
354
- isDebugEnabled ? 'settings-toggle__track--enabled' : 'settings-toggle__track--disabled',
355
- )}
356
- ></div>
357
- <div
358
- className={classNames(
359
- 'settings-toggle__thumb',
360
- isDebugEnabled ? 'settings-toggle__thumb--enabled' : '',
361
- )}
362
- ></div>
363
- </label>
364
- </div>
365
- </div>
366
- )}
367
- {activeTab === 'features' && (
368
- <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg">
369
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Experimental Area</h3>
370
- <div className="flex items-center justify-between mb-2">
371
- <span className="text-bolt-elements-textPrimary">Replace with local models</span>
372
- <label className="relative inline-flex items-center cursor-pointer">
373
- <input
374
- type="checkbox"
375
- className="sr-only"
376
- checked={isJustSayEnabled}
377
- onChange={() => setIsJustSayEnabled(!isJustSayEnabled)}
378
- />
379
- <div
380
- className={classNames(
381
- 'settings-toggle__track',
382
- isJustSayEnabled ? 'settings-toggle__track--enabled' : 'settings-toggle__track--disabled',
383
- )}
384
- ></div>
385
- <div
386
- className={classNames(
387
- 'settings-toggle__thumb',
388
- isJustSayEnabled ? 'settings-toggle__thumb--enabled' : '',
389
- )}
390
- ></div>
391
- </label>
392
- </div>
393
- </div>
394
- )}
395
- {activeTab === 'debug' && isDebugEnabled && (
396
- <div className="p-4">
397
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Debug Tab</h3>
398
- <button
399
- onClick={handleCopyToClipboard}
400
- className="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600 mb-4 transition-colors duration-200"
401
- >
402
- Copy to Clipboard
403
- </button>
404
-
405
- <h4 className="text-md font-medium text-bolt-elements-textPrimary">System Information</h4>
406
- <p className="text-bolt-elements-textSecondary">OS: {navigator.platform}</p>
407
- <p className="text-bolt-elements-textSecondary">Browser: {navigator.userAgent}</p>
408
-
409
- <h4 className="text-md font-medium text-bolt-elements-textPrimary mt-4">Active Features</h4>
410
- <ul>
411
- {providers
412
- .filter((provider) => provider.isEnabled)
413
- .map((provider) => (
414
- <li key={provider.name} className="text-bolt-elements-textSecondary">
415
- {provider.name}
416
- </li>
417
- ))}
418
- </ul>
419
-
420
- <h4 className="text-md font-medium text-bolt-elements-textPrimary mt-4">Base URLs</h4>
421
- <ul>
422
- <li className="text-bolt-elements-textSecondary">Ollama: {process.env.REACT_APP_OLLAMA_URL}</li>
423
- <li className="text-bolt-elements-textSecondary">OpenAI: {process.env.REACT_APP_OPENAI_URL}</li>
424
- <li className="text-bolt-elements-textSecondary">
425
- LM Studio: {process.env.REACT_APP_LM_STUDIO_URL}
426
- </li>
427
- </ul>
428
-
429
- <h4 className="text-md font-medium text-bolt-elements-textPrimary mt-4">Version Information</h4>
430
- <p className="text-bolt-elements-textSecondary">Version Hash: {versionHash}</p>
431
- </div>
432
- )}
433
- </div>
434
- </div>
435
- </div>
436
- <RadixDialog.Close asChild onClick={onClose}>
437
- <IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
438
- </RadixDialog.Close>
439
- </motion.div>
440
- </RadixDialog.Content>
441
- </RadixDialog.Portal>
442
- </RadixDialog.Root>
443
- );
444
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/components/ui/SettingsButton.tsx CHANGED
@@ -1,6 +1,5 @@
1
  import { memo } from 'react';
2
- import { IconButton } from './IconButton';
3
-
4
  interface SettingsButtonProps {
5
  onClick: () => void;
6
  }
 
1
  import { memo } from 'react';
2
+ import { IconButton } from '~/components/ui/IconButton';
 
3
  interface SettingsButtonProps {
4
  onClick: () => void;
5
  }
app/components/ui/SettingsSlider.tsx DELETED
@@ -1,63 +0,0 @@
1
- import { motion } from 'framer-motion';
2
- import { memo } from 'react';
3
- import { classNames } from '~/utils/classNames';
4
- import '~/styles/components/SettingsSlider.scss';
5
-
6
- interface SliderOption<T> {
7
- value: T;
8
- text: string;
9
- }
10
-
11
- export interface SliderOptions<T> {
12
- left: SliderOption<T>;
13
- right: SliderOption<T>;
14
- }
15
-
16
- interface SettingsSliderProps<T> {
17
- selected: T;
18
- options: SliderOptions<T>;
19
- setSelected?: (selected: T) => void;
20
- }
21
-
22
- export const SettingsSlider = memo(<T,>({ selected, options, setSelected }: SettingsSliderProps<T>) => {
23
- const isLeftSelected = selected === options.left.value;
24
-
25
- return (
26
- <div className="settings-slider">
27
- <motion.div
28
- className={classNames(
29
- 'settings-slider__thumb',
30
- isLeftSelected ? 'settings-slider__thumb--left' : 'settings-slider__thumb--right',
31
- )}
32
- initial={false}
33
- animate={{
34
- x: isLeftSelected ? 0 : '100%',
35
- opacity: 0.2,
36
- }}
37
- transition={{
38
- type: 'spring',
39
- stiffness: 300,
40
- damping: 30,
41
- }}
42
- />
43
- <button
44
- onClick={() => setSelected?.(options.left.value)}
45
- className={classNames(
46
- 'settings-slider__button',
47
- isLeftSelected ? 'settings-slider__button--selected' : 'settings-slider__button--unselected',
48
- )}
49
- >
50
- {options.left.text}
51
- </button>
52
- <button
53
- onClick={() => setSelected?.(options.right.value)}
54
- className={classNames(
55
- 'settings-slider__button',
56
- !isLeftSelected ? 'settings-slider__button--selected' : 'settings-slider__button--unselected',
57
- )}
58
- >
59
- {options.right.text}
60
- </button>
61
- </div>
62
- );
63
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/components/ui/Switch.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo } from 'react';
2
+ import * as SwitchPrimitive from '@radix-ui/react-switch';
3
+ import { classNames } from '~/utils/classNames';
4
+
5
+ interface SwitchProps {
6
+ className?: string;
7
+ checked?: boolean;
8
+ onCheckedChange?: (event: boolean) => void;
9
+ }
10
+
11
+ export const Switch = memo(({ className, onCheckedChange, checked }: SwitchProps) => {
12
+ return (
13
+ <SwitchPrimitive.Root
14
+ className={classNames(
15
+ 'relative h-6 w-11 cursor-pointer rounded-full bg-bolt-elements-button-primary-background',
16
+ 'transition-colors duration-200 ease-in-out',
17
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2',
18
+ 'disabled:cursor-not-allowed disabled:opacity-50',
19
+ 'data-[state=checked]:bg-bolt-elements-item-contentAccent',
20
+ className,
21
+ )}
22
+ checked={checked}
23
+ onCheckedChange={(e) => onCheckedChange?.(e)}
24
+ >
25
+ <SwitchPrimitive.Thumb
26
+ className={classNames(
27
+ 'block h-5 w-5 rounded-full bg-white',
28
+ 'shadow-lg shadow-black/20',
29
+ 'transition-transform duration-200 ease-in-out',
30
+ 'translate-x-0.5',
31
+ 'data-[state=checked]:translate-x-[1.375rem]',
32
+ 'will-change-transform',
33
+ )}
34
+ />
35
+ </SwitchPrimitive.Root>
36
+ );
37
+ });
app/components/ui/Tooltip.tsx CHANGED
@@ -1,8 +1,9 @@
1
  import * as Tooltip from '@radix-ui/react-tooltip';
 
2
 
3
  interface TooltipProps {
4
  tooltip: React.ReactNode;
5
- children: React.ReactNode;
6
  sideOffset?: number;
7
  className?: string;
8
  arrowClassName?: string;
@@ -12,62 +13,67 @@ interface TooltipProps {
12
  delay?: number;
13
  }
14
 
15
- const WithTooltip = ({
16
- tooltip,
17
- children,
18
- sideOffset = 5,
19
- className = '',
20
- arrowClassName = '',
21
- tooltipStyle = {},
22
- position = 'top',
23
- maxWidth = 250,
24
- delay = 0,
25
- }: TooltipProps) => {
26
- return (
27
- <Tooltip.Root delayDuration={delay}>
28
- <Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
29
- <Tooltip.Portal>
30
- <Tooltip.Content
31
- side={position}
32
- className={`
33
- z-[2000]
34
- px-2.5
35
- py-1.5
36
- max-h-[300px]
37
- select-none
38
- rounded-md
39
- bg-bolt-elements-background-depth-3
40
- text-bolt-elements-textPrimary
41
- text-sm
42
- leading-tight
43
- shadow-lg
44
- animate-in
45
- fade-in-0
46
- zoom-in-95
47
- data-[state=closed]:animate-out
48
- data-[state=closed]:fade-out-0
49
- data-[state=closed]:zoom-out-95
50
- ${className}
51
- `}
52
- sideOffset={sideOffset}
53
- style={{
54
- maxWidth,
55
- ...tooltipStyle,
56
- }}
57
- >
58
- <div className="break-words">{tooltip}</div>
59
- <Tooltip.Arrow
60
  className={`
61
- fill-bolt-elements-background-depth-3
62
- ${arrowClassName}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  `}
64
- width={12}
65
- height={6}
66
- />
67
- </Tooltip.Content>
68
- </Tooltip.Portal>
69
- </Tooltip.Root>
70
- );
71
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  export default WithTooltip;
 
1
  import * as Tooltip from '@radix-ui/react-tooltip';
2
+ import { forwardRef, type ForwardedRef, type ReactElement } from 'react';
3
 
4
  interface TooltipProps {
5
  tooltip: React.ReactNode;
6
+ children: ReactElement;
7
  sideOffset?: number;
8
  className?: string;
9
  arrowClassName?: string;
 
13
  delay?: number;
14
  }
15
 
16
+ const WithTooltip = forwardRef(
17
+ (
18
+ {
19
+ tooltip,
20
+ children,
21
+ sideOffset = 5,
22
+ className = '',
23
+ arrowClassName = '',
24
+ tooltipStyle = {},
25
+ position = 'top',
26
+ maxWidth = 250,
27
+ delay = 0,
28
+ }: TooltipProps,
29
+ _ref: ForwardedRef<HTMLElement>,
30
+ ) => {
31
+ return (
32
+ <Tooltip.Root delayDuration={delay}>
33
+ <Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
34
+ <Tooltip.Portal>
35
+ <Tooltip.Content
36
+ side={position}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  className={`
38
+ z-[2000]
39
+ px-2.5
40
+ py-1.5
41
+ max-h-[300px]
42
+ select-none
43
+ rounded-md
44
+ bg-bolt-elements-background-depth-3
45
+ text-bolt-elements-textPrimary
46
+ text-sm
47
+ leading-tight
48
+ shadow-lg
49
+ animate-in
50
+ fade-in-0
51
+ zoom-in-95
52
+ data-[state=closed]:animate-out
53
+ data-[state=closed]:fade-out-0
54
+ data-[state=closed]:zoom-out-95
55
+ ${className}
56
  `}
57
+ sideOffset={sideOffset}
58
+ style={{
59
+ maxWidth,
60
+ ...tooltipStyle,
61
+ }}
62
+ >
63
+ <div className="break-words">{tooltip}</div>
64
+ <Tooltip.Arrow
65
+ className={`
66
+ fill-bolt-elements-background-depth-3
67
+ ${arrowClassName}
68
+ `}
69
+ width={12}
70
+ height={6}
71
+ />
72
+ </Tooltip.Content>
73
+ </Tooltip.Portal>
74
+ </Tooltip.Root>
75
+ );
76
+ },
77
+ );
78
 
79
  export default WithTooltip;
app/components/workbench/FileTree.tsx CHANGED
@@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useState, type ReactNode } from 'react';
2
  import type { FileMap } from '~/lib/stores/files';
3
  import { classNames } from '~/utils/classNames';
4
  import { createScopedLogger, renderLogger } from '~/utils/logger';
 
5
 
6
  const logger = createScopedLogger('FileTree');
7
 
@@ -110,6 +111,22 @@ export const FileTree = memo(
110
  });
111
  };
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  return (
114
  <div className={classNames('text-sm', className, 'overflow-y-auto')}>
115
  {filteredFileList.map((fileOrFolder) => {
@@ -121,6 +138,12 @@ export const FileTree = memo(
121
  selected={selectedFile === fileOrFolder.fullPath}
122
  file={fileOrFolder}
123
  unsavedChanges={unsavedFiles?.has(fileOrFolder.fullPath)}
 
 
 
 
 
 
124
  onClick={() => {
125
  onFileSelect?.(fileOrFolder.fullPath);
126
  }}
@@ -134,6 +157,12 @@ export const FileTree = memo(
134
  folder={fileOrFolder}
135
  selected={allowFolderSelection && selectedFile === fileOrFolder.fullPath}
136
  collapsed={collapsedFolders.has(fileOrFolder.fullPath)}
 
 
 
 
 
 
137
  onClick={() => {
138
  toggleCollapseState(fileOrFolder.fullPath);
139
  }}
@@ -156,26 +185,67 @@ interface FolderProps {
156
  folder: FolderNode;
157
  collapsed: boolean;
158
  selected?: boolean;
 
 
159
  onClick: () => void;
160
  }
161
 
162
- function Folder({ folder: { depth, name }, collapsed, selected = false, onClick }: FolderProps) {
 
 
 
 
 
 
163
  return (
164
- <NodeButton
165
- className={classNames('group', {
166
- 'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive':
167
- !selected,
168
- 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
169
- })}
170
- depth={depth}
171
- iconClasses={classNames({
172
- 'i-ph:caret-right scale-98': collapsed,
173
- 'i-ph:caret-down scale-98': !collapsed,
174
- })}
175
- onClick={onClick}
176
  >
177
- {name}
178
- </NodeButton>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  );
180
  }
181
 
@@ -183,31 +253,43 @@ interface FileProps {
183
  file: FileNode;
184
  selected: boolean;
185
  unsavedChanges?: boolean;
 
 
186
  onClick: () => void;
187
  }
188
 
189
- function File({ file: { depth, name }, onClick, selected, unsavedChanges = false }: FileProps) {
 
 
 
 
 
 
 
190
  return (
191
- <NodeButton
192
- className={classNames('group', {
193
- 'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault': !selected,
194
- 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
195
- })}
196
- depth={depth}
197
- iconClasses={classNames('i-ph:file-duotone scale-98', {
198
- 'group-hover:text-bolt-elements-item-contentActive': !selected,
199
- })}
200
- onClick={onClick}
201
- >
202
- <div
203
- className={classNames('flex items-center', {
204
  'group-hover:text-bolt-elements-item-contentActive': !selected,
205
  })}
 
206
  >
207
- <div className="flex-1 truncate pr-2">{name}</div>
208
- {unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
209
- </div>
210
- </NodeButton>
 
 
 
 
 
 
211
  );
212
  }
213
 
 
2
  import type { FileMap } from '~/lib/stores/files';
3
  import { classNames } from '~/utils/classNames';
4
  import { createScopedLogger, renderLogger } from '~/utils/logger';
5
+ import * as ContextMenu from '@radix-ui/react-context-menu';
6
 
7
  const logger = createScopedLogger('FileTree');
8
 
 
111
  });
112
  };
113
 
114
+ const onCopyPath = (fileOrFolder: FileNode | FolderNode) => {
115
+ try {
116
+ navigator.clipboard.writeText(fileOrFolder.fullPath);
117
+ } catch (error) {
118
+ logger.error(error);
119
+ }
120
+ };
121
+
122
+ const onCopyRelativePath = (fileOrFolder: FileNode | FolderNode) => {
123
+ try {
124
+ navigator.clipboard.writeText(fileOrFolder.fullPath.substring((rootFolder || '').length));
125
+ } catch (error) {
126
+ logger.error(error);
127
+ }
128
+ };
129
+
130
  return (
131
  <div className={classNames('text-sm', className, 'overflow-y-auto')}>
132
  {filteredFileList.map((fileOrFolder) => {
 
138
  selected={selectedFile === fileOrFolder.fullPath}
139
  file={fileOrFolder}
140
  unsavedChanges={unsavedFiles?.has(fileOrFolder.fullPath)}
141
+ onCopyPath={() => {
142
+ onCopyPath(fileOrFolder);
143
+ }}
144
+ onCopyRelativePath={() => {
145
+ onCopyRelativePath(fileOrFolder);
146
+ }}
147
  onClick={() => {
148
  onFileSelect?.(fileOrFolder.fullPath);
149
  }}
 
157
  folder={fileOrFolder}
158
  selected={allowFolderSelection && selectedFile === fileOrFolder.fullPath}
159
  collapsed={collapsedFolders.has(fileOrFolder.fullPath)}
160
+ onCopyPath={() => {
161
+ onCopyPath(fileOrFolder);
162
+ }}
163
+ onCopyRelativePath={() => {
164
+ onCopyRelativePath(fileOrFolder);
165
+ }}
166
  onClick={() => {
167
  toggleCollapseState(fileOrFolder.fullPath);
168
  }}
 
185
  folder: FolderNode;
186
  collapsed: boolean;
187
  selected?: boolean;
188
+ onCopyPath: () => void;
189
+ onCopyRelativePath: () => void;
190
  onClick: () => void;
191
  }
192
 
193
+ interface FolderContextMenuProps {
194
+ onCopyPath?: () => void;
195
+ onCopyRelativePath?: () => void;
196
+ children: ReactNode;
197
+ }
198
+
199
+ function ContextMenuItem({ onSelect, children }: { onSelect?: () => void; children: ReactNode }) {
200
  return (
201
+ <ContextMenu.Item
202
+ onSelect={onSelect}
203
+ className="flex items-center gap-2 px-2 py-1.5 outline-0 text-sm text-bolt-elements-textPrimary cursor-pointer ws-nowrap text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive rounded-md"
 
 
 
 
 
 
 
 
 
204
  >
205
+ <span className="size-4 shrink-0"></span>
206
+ <span>{children}</span>
207
+ </ContextMenu.Item>
208
+ );
209
+ }
210
+
211
+ function FileContextMenu({ onCopyPath, onCopyRelativePath, children }: FolderContextMenuProps) {
212
+ return (
213
+ <ContextMenu.Root>
214
+ <ContextMenu.Trigger>{children}</ContextMenu.Trigger>
215
+ <ContextMenu.Portal>
216
+ <ContextMenu.Content
217
+ style={{ zIndex: 998 }}
218
+ className="border border-bolt-elements-borderColor rounded-md z-context-menu bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2 data-[state=open]:animate-in animate-duration-100 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-98 w-56"
219
+ >
220
+ <ContextMenu.Group className="p-1 border-b-px border-solid border-bolt-elements-borderColor">
221
+ <ContextMenuItem onSelect={onCopyPath}>Copy path</ContextMenuItem>
222
+ <ContextMenuItem onSelect={onCopyRelativePath}>Copy relative path</ContextMenuItem>
223
+ </ContextMenu.Group>
224
+ </ContextMenu.Content>
225
+ </ContextMenu.Portal>
226
+ </ContextMenu.Root>
227
+ );
228
+ }
229
+
230
+ function Folder({ folder, collapsed, selected = false, onCopyPath, onCopyRelativePath, onClick }: FolderProps) {
231
+ return (
232
+ <FileContextMenu onCopyPath={onCopyPath} onCopyRelativePath={onCopyRelativePath}>
233
+ <NodeButton
234
+ className={classNames('group', {
235
+ 'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive':
236
+ !selected,
237
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
238
+ })}
239
+ depth={folder.depth}
240
+ iconClasses={classNames({
241
+ 'i-ph:caret-right scale-98': collapsed,
242
+ 'i-ph:caret-down scale-98': !collapsed,
243
+ })}
244
+ onClick={onClick}
245
+ >
246
+ {folder.name}
247
+ </NodeButton>
248
+ </FileContextMenu>
249
  );
250
  }
251
 
 
253
  file: FileNode;
254
  selected: boolean;
255
  unsavedChanges?: boolean;
256
+ onCopyPath: () => void;
257
+ onCopyRelativePath: () => void;
258
  onClick: () => void;
259
  }
260
 
261
+ function File({
262
+ file: { depth, name },
263
+ onClick,
264
+ onCopyPath,
265
+ onCopyRelativePath,
266
+ selected,
267
+ unsavedChanges = false,
268
+ }: FileProps) {
269
  return (
270
+ <FileContextMenu onCopyPath={onCopyPath} onCopyRelativePath={onCopyRelativePath}>
271
+ <NodeButton
272
+ className={classNames('group', {
273
+ 'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault':
274
+ !selected,
275
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
276
+ })}
277
+ depth={depth}
278
+ iconClasses={classNames('i-ph:file-duotone scale-98', {
 
 
 
 
279
  'group-hover:text-bolt-elements-item-contentActive': !selected,
280
  })}
281
+ onClick={onClick}
282
  >
283
+ <div
284
+ className={classNames('flex items-center', {
285
+ 'group-hover:text-bolt-elements-item-contentActive': !selected,
286
+ })}
287
+ >
288
+ <div className="flex-1 truncate pr-2">{name}</div>
289
+ {unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
290
+ </div>
291
+ </NodeButton>
292
+ </FileContextMenu>
293
  );
294
  }
295
 
app/components/workbench/Preview.tsx CHANGED
@@ -3,6 +3,7 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
3
  import { IconButton } from '~/components/ui/IconButton';
4
  import { workbenchStore } from '~/lib/stores/workbench';
5
  import { PortDropdown } from './PortDropdown';
 
6
 
7
  type ResizeSide = 'left' | 'right' | null;
8
 
@@ -20,6 +21,7 @@ export const Preview = memo(() => {
20
 
21
  const [url, setUrl] = useState('');
22
  const [iframeUrl, setIframeUrl] = useState<string | undefined>();
 
23
 
24
  // Toggle between responsive mode and device mode
25
  const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
@@ -218,12 +220,17 @@ export const Preview = memo(() => {
218
  )}
219
  <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
220
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
221
-
 
 
 
 
222
  <div
223
  className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
224
  focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
225
  >
226
  <input
 
227
  ref={inputRef}
228
  className="w-full bg-transparent outline-none"
229
  type="text"
@@ -281,7 +288,20 @@ export const Preview = memo(() => {
281
  }}
282
  >
283
  {activePreview ? (
284
- <iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} allowFullScreen />
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  ) : (
286
  <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
287
  )}
 
3
  import { IconButton } from '~/components/ui/IconButton';
4
  import { workbenchStore } from '~/lib/stores/workbench';
5
  import { PortDropdown } from './PortDropdown';
6
+ import { ScreenshotSelector } from './ScreenshotSelector';
7
 
8
  type ResizeSide = 'left' | 'right' | null;
9
 
 
21
 
22
  const [url, setUrl] = useState('');
23
  const [iframeUrl, setIframeUrl] = useState<string | undefined>();
24
+ const [isSelectionMode, setIsSelectionMode] = useState(false);
25
 
26
  // Toggle between responsive mode and device mode
27
  const [isDeviceModeOn, setIsDeviceModeOn] = useState(false);
 
220
  )}
221
  <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
222
  <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
223
+ <IconButton
224
+ icon="i-ph:selection"
225
+ onClick={() => setIsSelectionMode(!isSelectionMode)}
226
+ className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
227
+ />
228
  <div
229
  className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive
230
  focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive"
231
  >
232
  <input
233
+ title="URL"
234
  ref={inputRef}
235
  className="w-full bg-transparent outline-none"
236
  type="text"
 
288
  }}
289
  >
290
  {activePreview ? (
291
+ <>
292
+ <iframe
293
+ ref={iframeRef}
294
+ title="preview"
295
+ className="border-none w-full h-full bg-white"
296
+ src={iframeUrl}
297
+ allowFullScreen
298
+ />
299
+ <ScreenshotSelector
300
+ isSelectionMode={isSelectionMode}
301
+ setIsSelectionMode={setIsSelectionMode}
302
+ containerRef={iframeRef}
303
+ />
304
+ </>
305
  ) : (
306
  <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
307
  )}
app/components/workbench/ScreenshotSelector.tsx ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo, useCallback, useEffect, useRef, useState } from 'react';
2
+ import { toast } from 'react-toastify';
3
+
4
+ interface ScreenshotSelectorProps {
5
+ isSelectionMode: boolean;
6
+ setIsSelectionMode: (mode: boolean) => void;
7
+ containerRef: React.RefObject<HTMLElement>;
8
+ }
9
+
10
+ export const ScreenshotSelector = memo(
11
+ ({ isSelectionMode, setIsSelectionMode, containerRef }: ScreenshotSelectorProps) => {
12
+ const [isCapturing, setIsCapturing] = useState(false);
13
+ const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null);
14
+ const [selectionEnd, setSelectionEnd] = useState<{ x: number; y: number } | null>(null);
15
+ const mediaStreamRef = useRef<MediaStream | null>(null);
16
+ const videoRef = useRef<HTMLVideoElement | null>(null);
17
+
18
+ useEffect(() => {
19
+ // Cleanup function to stop all tracks when component unmounts
20
+ return () => {
21
+ if (videoRef.current) {
22
+ videoRef.current.pause();
23
+ videoRef.current.srcObject = null;
24
+ videoRef.current.remove();
25
+ videoRef.current = null;
26
+ }
27
+
28
+ if (mediaStreamRef.current) {
29
+ mediaStreamRef.current.getTracks().forEach((track) => track.stop());
30
+ mediaStreamRef.current = null;
31
+ }
32
+ };
33
+ }, []);
34
+
35
+ const initializeStream = async () => {
36
+ if (!mediaStreamRef.current) {
37
+ try {
38
+ const stream = await navigator.mediaDevices.getDisplayMedia({
39
+ audio: false,
40
+ video: {
41
+ displaySurface: 'window',
42
+ preferCurrentTab: true,
43
+ surfaceSwitching: 'include',
44
+ systemAudio: 'exclude',
45
+ },
46
+ } as MediaStreamConstraints);
47
+
48
+ // Add handler for when sharing stops
49
+ stream.addEventListener('inactive', () => {
50
+ if (videoRef.current) {
51
+ videoRef.current.pause();
52
+ videoRef.current.srcObject = null;
53
+ videoRef.current.remove();
54
+ videoRef.current = null;
55
+ }
56
+
57
+ if (mediaStreamRef.current) {
58
+ mediaStreamRef.current.getTracks().forEach((track) => track.stop());
59
+ mediaStreamRef.current = null;
60
+ }
61
+
62
+ setIsSelectionMode(false);
63
+ setSelectionStart(null);
64
+ setSelectionEnd(null);
65
+ setIsCapturing(false);
66
+ });
67
+
68
+ mediaStreamRef.current = stream;
69
+
70
+ // Initialize video element if needed
71
+ if (!videoRef.current) {
72
+ const video = document.createElement('video');
73
+ video.style.opacity = '0';
74
+ video.style.position = 'fixed';
75
+ video.style.pointerEvents = 'none';
76
+ video.style.zIndex = '-1';
77
+ document.body.appendChild(video);
78
+ videoRef.current = video;
79
+ }
80
+
81
+ // Set up video with the stream
82
+ videoRef.current.srcObject = stream;
83
+ await videoRef.current.play();
84
+ } catch (error) {
85
+ console.error('Failed to initialize stream:', error);
86
+ setIsSelectionMode(false);
87
+ toast.error('Failed to initialize screen capture');
88
+ }
89
+ }
90
+
91
+ return mediaStreamRef.current;
92
+ };
93
+
94
+ const handleCopySelection = useCallback(async () => {
95
+ if (!isSelectionMode || !selectionStart || !selectionEnd || !containerRef.current) {
96
+ return;
97
+ }
98
+
99
+ setIsCapturing(true);
100
+
101
+ try {
102
+ const stream = await initializeStream();
103
+
104
+ if (!stream || !videoRef.current) {
105
+ return;
106
+ }
107
+
108
+ // Wait for video to be ready
109
+ await new Promise((resolve) => setTimeout(resolve, 300));
110
+
111
+ // Create temporary canvas for full screenshot
112
+ const tempCanvas = document.createElement('canvas');
113
+ tempCanvas.width = videoRef.current.videoWidth;
114
+ tempCanvas.height = videoRef.current.videoHeight;
115
+
116
+ const tempCtx = tempCanvas.getContext('2d');
117
+
118
+ if (!tempCtx) {
119
+ throw new Error('Failed to get temporary canvas context');
120
+ }
121
+
122
+ // Draw the full video frame
123
+ tempCtx.drawImage(videoRef.current, 0, 0);
124
+
125
+ // Calculate scale factor between video and screen
126
+ const scaleX = videoRef.current.videoWidth / window.innerWidth;
127
+ const scaleY = videoRef.current.videoHeight / window.innerHeight;
128
+
129
+ // Get window scroll position
130
+ const scrollX = window.scrollX;
131
+ const scrollY = window.scrollY + 40;
132
+
133
+ // Get the container's position in the page
134
+ const containerRect = containerRef.current.getBoundingClientRect();
135
+
136
+ // Offset adjustments for more accurate clipping
137
+ const leftOffset = -9; // Adjust left position
138
+ const bottomOffset = -14; // Adjust bottom position
139
+
140
+ // Calculate the scaled coordinates with scroll offset and adjustments
141
+ const scaledX = Math.round(
142
+ (containerRect.left + Math.min(selectionStart.x, selectionEnd.x) + scrollX + leftOffset) * scaleX,
143
+ );
144
+ const scaledY = Math.round(
145
+ (containerRect.top + Math.min(selectionStart.y, selectionEnd.y) + scrollY + bottomOffset) * scaleY,
146
+ );
147
+ const scaledWidth = Math.round(Math.abs(selectionEnd.x - selectionStart.x) * scaleX);
148
+ const scaledHeight = Math.round(Math.abs(selectionEnd.y - selectionStart.y) * scaleY);
149
+
150
+ // Create final canvas for the cropped area
151
+ const canvas = document.createElement('canvas');
152
+ canvas.width = Math.round(Math.abs(selectionEnd.x - selectionStart.x));
153
+ canvas.height = Math.round(Math.abs(selectionEnd.y - selectionStart.y));
154
+
155
+ const ctx = canvas.getContext('2d');
156
+
157
+ if (!ctx) {
158
+ throw new Error('Failed to get canvas context');
159
+ }
160
+
161
+ // Draw the cropped area
162
+ ctx.drawImage(tempCanvas, scaledX, scaledY, scaledWidth, scaledHeight, 0, 0, canvas.width, canvas.height);
163
+
164
+ // Convert to blob
165
+ const blob = await new Promise<Blob>((resolve, reject) => {
166
+ canvas.toBlob((blob) => {
167
+ if (blob) {
168
+ resolve(blob);
169
+ } else {
170
+ reject(new Error('Failed to create blob'));
171
+ }
172
+ }, 'image/png');
173
+ });
174
+
175
+ // Create a FileReader to convert blob to base64
176
+ const reader = new FileReader();
177
+
178
+ reader.onload = (e) => {
179
+ const base64Image = e.target?.result as string;
180
+
181
+ // Find the textarea element
182
+ const textarea = document.querySelector('textarea');
183
+
184
+ if (textarea) {
185
+ // Get the setters from the BaseChat component
186
+ const setUploadedFiles = (window as any).__BOLT_SET_UPLOADED_FILES__;
187
+ const setImageDataList = (window as any).__BOLT_SET_IMAGE_DATA_LIST__;
188
+ const uploadedFiles = (window as any).__BOLT_UPLOADED_FILES__ || [];
189
+ const imageDataList = (window as any).__BOLT_IMAGE_DATA_LIST__ || [];
190
+
191
+ if (setUploadedFiles && setImageDataList) {
192
+ // Update the files and image data
193
+ const file = new File([blob], 'screenshot.png', { type: 'image/png' });
194
+ setUploadedFiles([...uploadedFiles, file]);
195
+ setImageDataList([...imageDataList, base64Image]);
196
+ toast.success('Screenshot captured and added to chat');
197
+ } else {
198
+ toast.error('Could not add screenshot to chat');
199
+ }
200
+ }
201
+ };
202
+ reader.readAsDataURL(blob);
203
+ } catch (error) {
204
+ console.error('Failed to capture screenshot:', error);
205
+ toast.error('Failed to capture screenshot');
206
+
207
+ if (mediaStreamRef.current) {
208
+ mediaStreamRef.current.getTracks().forEach((track) => track.stop());
209
+ mediaStreamRef.current = null;
210
+ }
211
+ } finally {
212
+ setIsCapturing(false);
213
+ setSelectionStart(null);
214
+ setSelectionEnd(null);
215
+ setIsSelectionMode(false); // Turn off selection mode after capture
216
+ }
217
+ }, [isSelectionMode, selectionStart, selectionEnd, containerRef, setIsSelectionMode]);
218
+
219
+ const handleSelectionStart = useCallback(
220
+ (e: React.MouseEvent) => {
221
+ e.preventDefault();
222
+ e.stopPropagation();
223
+
224
+ if (!isSelectionMode) {
225
+ return;
226
+ }
227
+
228
+ const rect = e.currentTarget.getBoundingClientRect();
229
+ const x = e.clientX - rect.left;
230
+ const y = e.clientY - rect.top;
231
+ setSelectionStart({ x, y });
232
+ setSelectionEnd({ x, y });
233
+ },
234
+ [isSelectionMode],
235
+ );
236
+
237
+ const handleSelectionMove = useCallback(
238
+ (e: React.MouseEvent) => {
239
+ e.preventDefault();
240
+ e.stopPropagation();
241
+
242
+ if (!isSelectionMode || !selectionStart) {
243
+ return;
244
+ }
245
+
246
+ const rect = e.currentTarget.getBoundingClientRect();
247
+ const x = e.clientX - rect.left;
248
+ const y = e.clientY - rect.top;
249
+ setSelectionEnd({ x, y });
250
+ },
251
+ [isSelectionMode, selectionStart],
252
+ );
253
+
254
+ if (!isSelectionMode) {
255
+ return null;
256
+ }
257
+
258
+ return (
259
+ <div
260
+ className="absolute inset-0 cursor-crosshair"
261
+ onMouseDown={handleSelectionStart}
262
+ onMouseMove={handleSelectionMove}
263
+ onMouseUp={handleCopySelection}
264
+ onMouseLeave={() => {
265
+ if (selectionStart) {
266
+ setSelectionStart(null);
267
+ }
268
+ }}
269
+ style={{
270
+ backgroundColor: isCapturing ? 'transparent' : 'rgba(0, 0, 0, 0.1)',
271
+ userSelect: 'none',
272
+ WebkitUserSelect: 'none',
273
+ pointerEvents: 'all',
274
+ opacity: isCapturing ? 0 : 1,
275
+ zIndex: 50,
276
+ transition: 'opacity 0.1s ease-in-out',
277
+ }}
278
+ >
279
+ {selectionStart && selectionEnd && !isCapturing && (
280
+ <div
281
+ className="absolute border-2 border-blue-500 bg-blue-200 bg-opacity-20"
282
+ style={{
283
+ left: Math.min(selectionStart.x, selectionEnd.x),
284
+ top: Math.min(selectionStart.y, selectionEnd.y),
285
+ width: Math.abs(selectionEnd.x - selectionStart.x),
286
+ height: Math.abs(selectionEnd.y - selectionStart.y),
287
+ }}
288
+ />
289
+ )}
290
+ </div>
291
+ );
292
+ },
293
+ );
app/components/workbench/Workbench.client.tsx CHANGED
@@ -17,6 +17,7 @@ import { renderLogger } from '~/utils/logger';
17
  import { EditorPanel } from './EditorPanel';
18
  import { Preview } from './Preview';
19
  import useViewport from '~/lib/hooks';
 
20
 
21
  interface WorkspaceProps {
22
  chatStarted?: boolean;
@@ -180,21 +181,22 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
180
  return;
181
  }
182
 
183
- const githubUsername = prompt('Please enter your GitHub username:');
 
184
 
185
- if (!githubUsername) {
186
- alert('GitHub username is required. Push to GitHub cancelled.');
187
- return;
188
- }
189
 
190
- const githubToken = prompt('Please enter your GitHub personal access token:');
 
 
 
191
 
192
- if (!githubToken) {
193
- alert('GitHub token is required. Push to GitHub cancelled.');
194
- return;
195
  }
196
-
197
- workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
198
  }}
199
  >
200
  <div className="i-ph:github-logo" />
 
17
  import { EditorPanel } from './EditorPanel';
18
  import { Preview } from './Preview';
19
  import useViewport from '~/lib/hooks';
20
+ import Cookies from 'js-cookie';
21
 
22
  interface WorkspaceProps {
23
  chatStarted?: boolean;
 
181
  return;
182
  }
183
 
184
+ const githubUsername = Cookies.get('githubUsername');
185
+ const githubToken = Cookies.get('githubToken');
186
 
187
+ if (!githubUsername || !githubToken) {
188
+ const usernameInput = prompt('Please enter your GitHub username:');
189
+ const tokenInput = prompt('Please enter your GitHub personal access token:');
 
190
 
191
+ if (!usernameInput || !tokenInput) {
192
+ alert('GitHub username and token are required. Push to GitHub cancelled.');
193
+ return;
194
+ }
195
 
196
+ workbenchStore.pushToGitHub(repoName, usernameInput, tokenInput);
197
+ } else {
198
+ workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
199
  }
 
 
200
  }}
201
  >
202
  <div className="i-ph:github-logo" />
app/lib/.server/llm/api-key.ts CHANGED
@@ -39,6 +39,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re
39
  return env.TOGETHER_API_KEY || cloudflareEnv.TOGETHER_API_KEY;
40
  case 'xAI':
41
  return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
 
 
42
  case 'Cohere':
43
  return env.COHERE_API_KEY;
44
  case 'AzureOpenAI':
 
39
  return env.TOGETHER_API_KEY || cloudflareEnv.TOGETHER_API_KEY;
40
  case 'xAI':
41
  return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
42
+ case 'Perplexity':
43
+ return env.PERPLEXITY_API_KEY || cloudflareEnv.PERPLEXITY_API_KEY;
44
  case 'Cohere':
45
  return env.COHERE_API_KEY;
46
  case 'AzureOpenAI':
app/lib/.server/llm/model.ts CHANGED
@@ -11,6 +11,7 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
11
  import { createMistral } from '@ai-sdk/mistral';
12
  import { createCohere } from '@ai-sdk/cohere';
13
  import type { LanguageModelV1 } from 'ai';
 
14
 
15
  export const DEFAULT_NUM_CTX = process.env.DEFAULT_NUM_CTX ? parseInt(process.env.DEFAULT_NUM_CTX, 10) : 32768;
16
 
@@ -127,14 +128,29 @@ export function getXAIModel(apiKey: OptionalApiKey, model: string) {
127
  return openai(model);
128
  }
129
 
130
- export function getModel(provider: string, model: string, env: Env, apiKeys?: Record<string, string>) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  /*
132
  * let apiKey; // Declare first
133
  * let baseURL;
134
  */
135
 
136
  const apiKey = getAPIKey(env, provider, apiKeys); // Then assign
137
- const baseURL = getBaseURL(env, provider);
138
 
139
  switch (provider) {
140
  case 'Anthropic':
@@ -163,6 +179,8 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re
163
  return getXAIModel(apiKey, model);
164
  case 'Cohere':
165
  return getCohereAIModel(apiKey, model);
 
 
166
  default:
167
  return getOllamaModel(baseURL, model);
168
  }
 
11
  import { createMistral } from '@ai-sdk/mistral';
12
  import { createCohere } from '@ai-sdk/cohere';
13
  import type { LanguageModelV1 } from 'ai';
14
+ import type { IProviderSetting } from '~/types/model';
15
 
16
  export const DEFAULT_NUM_CTX = process.env.DEFAULT_NUM_CTX ? parseInt(process.env.DEFAULT_NUM_CTX, 10) : 32768;
17
 
 
128
  return openai(model);
129
  }
130
 
131
+ export function getPerplexityModel(apiKey: OptionalApiKey, model: string) {
132
+ const perplexity = createOpenAI({
133
+ baseURL: 'https://api.perplexity.ai/',
134
+ apiKey,
135
+ });
136
+
137
+ return perplexity(model);
138
+ }
139
+
140
+ export function getModel(
141
+ provider: string,
142
+ model: string,
143
+ env: Env,
144
+ apiKeys?: Record<string, string>,
145
+ providerSettings?: Record<string, IProviderSetting>,
146
+ ) {
147
  /*
148
  * let apiKey; // Declare first
149
  * let baseURL;
150
  */
151
 
152
  const apiKey = getAPIKey(env, provider, apiKeys); // Then assign
153
+ const baseURL = providerSettings?.[provider].baseUrl || getBaseURL(env, provider);
154
 
155
  switch (provider) {
156
  case 'Anthropic':
 
179
  return getXAIModel(apiKey, model);
180
  case 'Cohere':
181
  return getCohereAIModel(apiKey, model);
182
+ case 'Perplexity':
183
+ return getPerplexityModel(apiKey, model);
184
  default:
185
  return getOllamaModel(baseURL, model);
186
  }
app/lib/.server/llm/stream-text.ts CHANGED
@@ -1,8 +1,20 @@
1
  import { convertToCoreMessages, streamText as _streamText } from 'ai';
2
  import { getModel } from '~/lib/.server/llm/model';
3
  import { MAX_TOKENS } from './constants';
4
- import { getSystemPrompt } from './prompts';
5
- import { DEFAULT_MODEL, DEFAULT_PROVIDER, getModelList, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  interface ToolResult<Name extends string, Args, Result> {
8
  toolCallId: string;
@@ -22,6 +34,78 @@ export type Messages = Message[];
22
 
23
  export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
26
  const textContent = Array.isArray(message.content)
27
  ? message.content.find((item) => item.type === 'text')?.text || ''
@@ -58,15 +142,19 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid
58
  return { model, provider, content: cleanedContent };
59
  }
60
 
61
- export async function streamText(
62
- messages: Messages,
63
- env: Env,
64
- options?: StreamingOptions,
65
- apiKeys?: Record<string, string>,
66
- ) {
 
 
 
 
67
  let currentModel = DEFAULT_MODEL;
68
  let currentProvider = DEFAULT_PROVIDER.name;
69
- const MODEL_LIST = await getModelList(apiKeys || {});
70
  const processedMessages = messages.map((message) => {
71
  if (message.role === 'user') {
72
  const { model, provider, content } = extractPropertiesFromMessage(message);
@@ -77,6 +165,12 @@ export async function streamText(
77
 
78
  currentProvider = provider;
79
 
 
 
 
 
 
 
80
  return { ...message, content };
81
  }
82
 
@@ -87,9 +181,23 @@ export async function streamText(
87
 
88
  const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  return _streamText({
91
- model: getModel(currentProvider, currentModel, env, apiKeys) as any,
92
- system: getSystemPrompt(),
93
  maxTokens: dynamicMaxTokens,
94
  messages: convertToCoreMessages(processedMessages as any),
95
  ...options,
 
1
  import { convertToCoreMessages, streamText as _streamText } from 'ai';
2
  import { getModel } from '~/lib/.server/llm/model';
3
  import { MAX_TOKENS } from './constants';
4
+ import { getSystemPrompt } from '~/lib/common/prompts/prompts';
5
+ import {
6
+ DEFAULT_MODEL,
7
+ DEFAULT_PROVIDER,
8
+ getModelList,
9
+ MODEL_REGEX,
10
+ MODIFICATIONS_TAG_NAME,
11
+ PROVIDER_REGEX,
12
+ WORK_DIR,
13
+ } from '~/utils/constants';
14
+ import ignore from 'ignore';
15
+ import type { IProviderSetting } from '~/types/model';
16
+ import { PromptLibrary } from '~/lib/common/prompt-library';
17
+ import { allowedHTMLElements } from '~/utils/markdown';
18
 
19
  interface ToolResult<Name extends string, Args, Result> {
20
  toolCallId: string;
 
34
 
35
  export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
36
 
37
+ export interface File {
38
+ type: 'file';
39
+ content: string;
40
+ isBinary: boolean;
41
+ }
42
+
43
+ export interface Folder {
44
+ type: 'folder';
45
+ }
46
+
47
+ type Dirent = File | Folder;
48
+
49
+ export type FileMap = Record<string, Dirent | undefined>;
50
+
51
+ export function simplifyBoltActions(input: string): string {
52
+ // Using regex to match boltAction tags that have type="file"
53
+ const regex = /(<boltAction[^>]*type="file"[^>]*>)([\s\S]*?)(<\/boltAction>)/g;
54
+
55
+ // Replace each matching occurrence
56
+ return input.replace(regex, (_0, openingTag, _2, closingTag) => {
57
+ return `${openingTag}\n ...\n ${closingTag}`;
58
+ });
59
+ }
60
+
61
+ // Common patterns to ignore, similar to .gitignore
62
+ const IGNORE_PATTERNS = [
63
+ 'node_modules/**',
64
+ '.git/**',
65
+ 'dist/**',
66
+ 'build/**',
67
+ '.next/**',
68
+ 'coverage/**',
69
+ '.cache/**',
70
+ '.vscode/**',
71
+ '.idea/**',
72
+ '**/*.log',
73
+ '**/.DS_Store',
74
+ '**/npm-debug.log*',
75
+ '**/yarn-debug.log*',
76
+ '**/yarn-error.log*',
77
+ '**/*lock.json',
78
+ '**/*lock.yml',
79
+ ];
80
+ const ig = ignore().add(IGNORE_PATTERNS);
81
+
82
+ function createFilesContext(files: FileMap) {
83
+ let filePaths = Object.keys(files);
84
+ filePaths = filePaths.filter((x) => {
85
+ const relPath = x.replace('/home/project/', '');
86
+ return !ig.ignores(relPath);
87
+ });
88
+
89
+ const fileContexts = filePaths
90
+ .filter((x) => files[x] && files[x].type == 'file')
91
+ .map((path) => {
92
+ const dirent = files[path];
93
+
94
+ if (!dirent || dirent.type == 'folder') {
95
+ return '';
96
+ }
97
+
98
+ const codeWithLinesNumbers = dirent.content
99
+ .split('\n')
100
+ .map((v, i) => `${i + 1}|${v}`)
101
+ .join('\n');
102
+
103
+ return `<file path="${path}">\n${codeWithLinesNumbers}\n</file>`;
104
+ });
105
+
106
+ return `Below are the code files present in the webcontainer:\ncode format:\n<line number>|<line content>\n <codebase>${fileContexts.join('\n\n')}\n\n</codebase>`;
107
+ }
108
+
109
  function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
110
  const textContent = Array.isArray(message.content)
111
  ? message.content.find((item) => item.type === 'text')?.text || ''
 
142
  return { model, provider, content: cleanedContent };
143
  }
144
 
145
+ export async function streamText(props: {
146
+ messages: Messages;
147
+ env: Env;
148
+ options?: StreamingOptions;
149
+ apiKeys?: Record<string, string>;
150
+ files?: FileMap;
151
+ providerSettings?: Record<string, IProviderSetting>;
152
+ promptId?: string;
153
+ }) {
154
+ const { messages, env, options, apiKeys, files, providerSettings, promptId } = props;
155
  let currentModel = DEFAULT_MODEL;
156
  let currentProvider = DEFAULT_PROVIDER.name;
157
+ const MODEL_LIST = await getModelList(apiKeys || {}, providerSettings);
158
  const processedMessages = messages.map((message) => {
159
  if (message.role === 'user') {
160
  const { model, provider, content } = extractPropertiesFromMessage(message);
 
165
 
166
  currentProvider = provider;
167
 
168
+ return { ...message, content };
169
+ } else if (message.role == 'assistant') {
170
+ const content = message.content;
171
+
172
+ // content = simplifyBoltActions(content);
173
+
174
  return { ...message, content };
175
  }
176
 
 
181
 
182
  const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
183
 
184
+ let systemPrompt =
185
+ PromptLibrary.getPropmtFromLibrary(promptId || 'default', {
186
+ cwd: WORK_DIR,
187
+ allowedHtmlElements: allowedHTMLElements,
188
+ modificationTagName: MODIFICATIONS_TAG_NAME,
189
+ }) ?? getSystemPrompt();
190
+ let codeContext = '';
191
+
192
+ if (files) {
193
+ codeContext = createFilesContext(files);
194
+ codeContext = '';
195
+ systemPrompt = `${systemPrompt}\n\n ${codeContext}`;
196
+ }
197
+
198
  return _streamText({
199
+ model: getModel(currentProvider, currentModel, env, apiKeys, providerSettings) as any,
200
+ system: systemPrompt,
201
  maxTokens: dynamicMaxTokens,
202
  messages: convertToCoreMessages(processedMessages as any),
203
  ...options,
app/lib/common/prompt-library.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getSystemPrompt } from './prompts/prompts';
2
+ import optimized from './prompts/optimized';
3
+
4
+ export interface PromptOptions {
5
+ cwd: string;
6
+ allowedHtmlElements: string[];
7
+ modificationTagName: string;
8
+ }
9
+
10
+ export class PromptLibrary {
11
+ static library: Record<
12
+ string,
13
+ {
14
+ label: string;
15
+ description: string;
16
+ get: (options: PromptOptions) => string;
17
+ }
18
+ > = {
19
+ default: {
20
+ label: 'Default Prompt',
21
+ description: 'This is the battle tested default system Prompt',
22
+ get: (options) => getSystemPrompt(options.cwd),
23
+ },
24
+ optimized: {
25
+ label: 'Optimized Prompt (experimental)',
26
+ description: 'an Experimental version of the prompt for lower token usage',
27
+ get: (options) => optimized(options),
28
+ },
29
+ };
30
+ static getList() {
31
+ return Object.entries(this.library).map(([key, value]) => {
32
+ const { label, description } = value;
33
+ return {
34
+ id: key,
35
+ label,
36
+ description,
37
+ };
38
+ });
39
+ }
40
+ static getPropmtFromLibrary(promptId: string, options: PromptOptions) {
41
+ const prompt = this.library[promptId];
42
+
43
+ if (!prompt) {
44
+ throw 'Prompt Now Found';
45
+ }
46
+
47
+ return this.library[promptId]?.get(options);
48
+ }
49
+ }
app/lib/common/prompts/optimized.ts ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { PromptOptions } from '~/lib/common/prompt-library';
2
+
3
+ export default (options: PromptOptions) => {
4
+ const { cwd, allowedHtmlElements, modificationTagName } = options;
5
+ return `
6
+ You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.
7
+
8
+ <system_constraints>
9
+ - Operating in WebContainer, an in-browser Node.js runtime
10
+ - Limited Python support: standard library only, no pip
11
+ - No C/C++ compiler, native binaries, or Git
12
+ - Prefer Node.js scripts over shell scripts
13
+ - Use Vite for web servers
14
+ - Databases: prefer libsql, sqlite, or non-native solutions
15
+ - When for react dont forget to write vite config and index.html to the project
16
+
17
+ Available shell commands: cat, cp, ls, mkdir, mv, rm, rmdir, touch, hostname, ps, pwd, uptime, env, node, python3, code, jq, curl, head, sort, tail, clear, which, export, chmod, scho, kill, ln, xxd, alias, getconf, loadenv, wasm, xdg-open, command, exit, source
18
+ </system_constraints>
19
+
20
+ <code_formatting_info>
21
+ Use 2 spaces for indentation
22
+ </code_formatting_info>
23
+
24
+ <message_formatting_info>
25
+ Available HTML elements: ${allowedHtmlElements.join(', ')}
26
+ </message_formatting_info>
27
+
28
+ <diff_spec>
29
+ File modifications in \`<${modificationTagName}>\` section:
30
+ - \`<diff path="/path/to/file">\`: GNU unified diff format
31
+ - \`<file path="/path/to/file">\`: Full new content
32
+ </diff_spec>
33
+
34
+ <chain_of_thought_instructions>
35
+ do not mention the phrase "chain of thought"
36
+ Before solutions, briefly outline implementation steps (2-4 lines max):
37
+ - List concrete steps
38
+ - Identify key components
39
+ - Note potential challenges
40
+ - Do not write the actual code just the plan and structure if needed
41
+ - Once completed planning start writing the artifacts
42
+ </chain_of_thought_instructions>
43
+
44
+ <artifact_info>
45
+ Create a single, comprehensive artifact for each project:
46
+ - Use \`<boltArtifact>\` tags with \`title\` and \`id\` attributes
47
+ - Use \`<boltAction>\` tags with \`type\` attribute:
48
+ - shell: Run commands
49
+ - file: Write/update files (use \`filePath\` attribute)
50
+ - start: Start dev server (only when necessary)
51
+ - Order actions logically
52
+ - Install dependencies first
53
+ - Provide full, updated content for all files
54
+ - Use coding best practices: modular, clean, readable code
55
+ </artifact_info>
56
+
57
+
58
+ # CRITICAL RULES - NEVER IGNORE
59
+
60
+ ## File and Command Handling
61
+ 1. ALWAYS use artifacts for file contents and commands - NO EXCEPTIONS
62
+ 2. When writing a file, INCLUDE THE ENTIRE FILE CONTENT - NO PARTIAL UPDATES
63
+ 3. For modifications, ONLY alter files that require changes - DO NOT touch unaffected files
64
+
65
+ ## Response Format
66
+ 4. Use markdown EXCLUSIVELY - HTML tags are ONLY allowed within artifacts
67
+ 5. Be concise - Explain ONLY when explicitly requested
68
+ 6. NEVER use the word "artifact" in responses
69
+
70
+ ## Development Process
71
+ 7. ALWAYS think and plan comprehensively before providing a solution
72
+ 8. Current working directory: \`${cwd} \` - Use this for all file paths
73
+ 9. Don't use cli scaffolding to steup the project, use cwd as Root of the project
74
+ 11. For nodejs projects ALWAYS install dependencies after writing package.json file
75
+
76
+ ## Coding Standards
77
+ 10. ALWAYS create smaller, atomic components and modules
78
+ 11. Modularity is PARAMOUNT - Break down functionality into logical, reusable parts
79
+ 12. IMMEDIATELY refactor any file exceeding 250 lines
80
+ 13. ALWAYS plan refactoring before implementation - Consider impacts on the entire system
81
+
82
+ ## Artifact Usage
83
+ 22. Use \`<boltArtifact>\` tags with \`title\` and \`id\` attributes for each project
84
+ 23. Use \`<boltAction>\` tags with appropriate \`type\` attribute:
85
+ - \`shell\`: For running commands
86
+ - \`file\`: For writing/updating files (include \`filePath\` attribute)
87
+ - \`start\`: For starting dev servers (use only when necessary/ or new dependencies are installed)
88
+ 24. Order actions logically - dependencies MUST be installed first
89
+ 25. For Vite project must include vite config and index.html for entry point
90
+ 26. Provide COMPLETE, up-to-date content for all files - NO placeholders or partial updates
91
+
92
+ CRITICAL: These rules are ABSOLUTE and MUST be followed WITHOUT EXCEPTION in EVERY response.
93
+
94
+ Examples:
95
+ <examples>
96
+ <example>
97
+ <user_query>Can you help me create a JavaScript function to calculate the factorial of a number?</user_query>
98
+ <assistant_response>
99
+ Certainly, I can help you create a JavaScript function to calculate the factorial of a number.
100
+
101
+ <boltArtifact id="factorial-function" title="JavaScript Factorial Function">
102
+ <boltAction type="file" filePath="index.js">
103
+ function factorial(n) {
104
+ ...
105
+ }
106
+
107
+ ...
108
+ </boltAction>
109
+ <boltAction type="shell">
110
+ node index.js
111
+ </boltAction>
112
+ </boltArtifact>
113
+ </assistant_response>
114
+ </example>
115
+
116
+ <example>
117
+ <user_query>Build a snake game</user_query>
118
+ <assistant_response>
119
+ Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step.
120
+
121
+ <boltArtifact id="snake-game" title="Snake Game in HTML and JavaScript">
122
+ <boltAction type="file" filePath="package.json">
123
+ {
124
+ "name": "snake",
125
+ "scripts": {
126
+ "dev": "vite"
127
+ }
128
+ ...
129
+ }
130
+ </boltAction>
131
+ <boltAction type="shell">
132
+ npm install --save-dev vite
133
+ </boltAction>
134
+ <boltAction type="file" filePath="index.html">
135
+ ...
136
+ </boltAction>
137
+ <boltAction type="start">
138
+ npm run dev
139
+ </boltAction>
140
+ </boltArtifact>
141
+
142
+ Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail.
143
+ </assistant_response>
144
+ </example>
145
+
146
+ <example>
147
+ <user_query>Make a bouncing ball with real gravity using React</user_query>
148
+ <assistant_response>
149
+ Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.
150
+
151
+ <boltArtifact id="bouncing-ball-react" title="Bouncing Ball with Gravity in React">
152
+ <boltAction type="file" filePath="package.json">
153
+ {
154
+ "name": "bouncing-ball",
155
+ "private": true,
156
+ "version": "0.0.0",
157
+ "type": "module",
158
+ "scripts": {
159
+ "dev": "vite",
160
+ "build": "vite build",
161
+ "preview": "vite preview"
162
+ },
163
+ "dependencies": {
164
+ "react": "^18.2.0",
165
+ "react-dom": "^18.2.0",
166
+ "react-spring": "^9.7.1"
167
+ },
168
+ "devDependencies": {
169
+ "@types/react": "^18.0.28",
170
+ "@types/react-dom": "^18.0.11",
171
+ "@vitejs/plugin-react": "^3.1.0",
172
+ "vite": "^4.2.0"
173
+ }
174
+ }
175
+ </boltAction>
176
+ <boltAction type="file" filePath="index.html">
177
+ ...
178
+ </boltAction>
179
+ <boltAction type="file" filePath="src/main.jsx">
180
+ ...
181
+ </boltAction>
182
+ <boltAction type="file" filePath="src/index.css">
183
+ ...
184
+ </boltAction>
185
+ <boltAction type="file" filePath="src/App.jsx">
186
+ ...
187
+ </boltAction>
188
+ <boltAction type="start">
189
+ npm run dev
190
+ </boltAction>
191
+ </boltArtifact>
192
+
193
+ You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom.
194
+ </assistant_response>
195
+ </example>
196
+ </examples>
197
+ Always use artifacts for file contents and commands, following the format shown in these examples.
198
+ `;
199
+ };