codacus commited on
Commit
2088301
·
2 Parent(s): e3bdd69 25e6bb3

Merge branch 'main' into prompt-url-params

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 +11 -18
  10. README.md +134 -137
  11. app/commit.json +1 -1
  12. app/components/chat/AssistantMessage.tsx +18 -1
  13. app/components/chat/BaseChat.tsx +28 -19
  14. app/components/chat/Chat.client.tsx +14 -2
  15. app/components/chat/GitCloneButton.tsx +9 -14
  16. app/components/chat/ImportFolderButton.tsx +23 -0
  17. app/components/chat/Messages.client.tsx +9 -5
  18. app/components/chat/ModelSelector.tsx +16 -51
  19. app/components/chat/ScreenshotStateManager.tsx +33 -0
  20. app/components/chat/UserMessage.tsx +15 -21
  21. app/components/chat/chatExportAndImport/ImportButtons.tsx +0 -1
  22. app/components/settings/SettingsWindow.tsx +14 -4
  23. app/components/settings/chat-history/ChatHistoryTab.tsx +17 -3
  24. app/components/settings/connections/ConnectionsTab.tsx +6 -0
  25. app/components/settings/debug/DebugTab.tsx +600 -49
  26. app/components/settings/event-logs/EventLogsTab.tsx +219 -0
  27. app/components/settings/features/FeaturesTab.tsx +51 -5
  28. app/components/settings/providers/ProvidersTab.tsx +33 -5
  29. app/components/sidebar/Menu.client.tsx +22 -4
  30. app/components/ui/IconButton.tsx +41 -34
  31. app/components/ui/Tooltip.tsx +62 -56
  32. app/components/workbench/FileTree.tsx +115 -33
  33. app/components/workbench/Preview.tsx +22 -2
  34. app/components/workbench/ScreenshotSelector.tsx +293 -0
  35. app/lib/.server/llm/api-key.ts +2 -0
  36. app/lib/.server/llm/model.ts +11 -0
  37. app/lib/.server/llm/stream-text.ts +109 -4
  38. app/lib/common/prompt-library.ts +49 -0
  39. app/lib/common/prompts/optimized.ts +199 -0
  40. app/lib/{.server/llm → common/prompts}/prompts.ts +0 -0
  41. app/lib/hooks/useMessageParser.ts +2 -2
  42. app/lib/hooks/useSettings.tsx +107 -6
  43. app/lib/persistence/useChatHistory.ts +4 -0
  44. app/lib/stores/logs.ts +149 -0
  45. app/lib/stores/settings.ts +7 -1
  46. app/lib/stores/theme.ts +2 -3
  47. app/lib/stores/workbench.ts +14 -3
  48. app/root.tsx +18 -1
  49. app/routes/api.chat.ts +61 -17
  50. app/routes/api.enhancer.ts +35 -29
.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 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)
 
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,12 +1,12 @@
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 - 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
 
@@ -14,36 +14,29 @@
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
- ### Do you plan on merging Bolt.diy 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
- 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
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 Bolt.diy/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 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.
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 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!
39
 
40
- ### I'm getting a blank preview when Bolt.diy runs my app!
41
 
42
- 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.
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
 
 
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,14 +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.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,7 +20,7 @@ https://thinktank.ottomator.ai
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,8 +43,12 @@ https://thinktank.ottomator.ai
43
  - ✅ Mobile friendly (@qwikode)
44
  - ✅ Better prompt enhancing (@SujalXplores)
45
  - ✅ Attach images to prompts (@atrokhym)
46
- - ✅ Detect package.json and commands to auto install and run preview for folder and git import (@wonderwhy-er)
47
- - **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
 
 
 
 
48
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
49
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
50
  - ⬜ Deploy directly to Vercel/Netlify/other similar platforms
@@ -56,187 +60,180 @@ https://thinktank.ottomator.ai
56
  - ⬜ Perplexity Integration
57
  - ⬜ Vertex AI Integration
58
 
59
- ## Bolt.new: AI-Powered Full-Stack Web Development in the Browser
60
 
61
- Bolt.new (and by extension Bolt.diy) 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)
 
 
 
 
 
 
62
 
63
- ## What Makes Bolt.new Different
64
 
65
- Claude, v0, etc are incredible- but you can't install packages, run backends, or edit code. That’s where Bolt.new stands out:
66
 
67
- - **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:
68
- - Install and run npm tools and libraries (like Vite, Next.js, and more)
69
- - Run Node.js servers
70
- - Interact with third-party APIs
71
- - Deploy to production from chat
72
- - Share your work via a URL
73
 
74
- - **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.
 
75
 
76
- Whether you’re an experienced developer, a PM, or a designer, Bolt.new allows you to easily build production-grade full-stack applications.
 
 
 
 
 
 
77
 
78
- For developers interested in building their own AI-powered development tools with WebContainers, check out the open-source Bolt codebase in this repo!
79
 
80
- ## Setup
81
 
82
- 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.
 
 
83
 
84
- 1. Install Git from https://git-scm.com/downloads
85
 
86
- 2. Install Node.js from https://nodejs.org/en/download/
87
 
88
- Pay attention to the installer notes after completion.
 
89
 
90
- 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:
 
 
 
 
91
 
92
- ```
93
- echo $PATH .
94
- ```
95
 
96
- If you see usr/local/bin in the output then you're good to go.
97
 
98
- 3. Clone the repository (if you haven't already) by opening a Terminal window (or CMD with admin permissions) and then typing in this:
 
 
99
 
100
- ```
101
- git clone https://github.com/stackblitz-labs/bolt.diy.git
102
- ```
 
103
 
104
- 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.
105
 
106
- ![image](https://github.com/user-attachments/assets/7e6a532c-2268-401f-8310-e8d20c731328)
107
 
108
- 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.
109
 
110
- ```
111
- defaults write com.apple.finder AppleShowAllFiles YES
112
- ```
113
 
114
- **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:
 
 
 
 
 
 
 
115
 
116
- Get your GROQ API Key here: https://console.groq.com/keys
 
 
 
 
117
 
118
- 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
119
 
120
- Get your Anthropic API Key in your account settings: https://console.anthropic.com/settings/keys
 
121
 
122
- ```
123
- GROQ_API_KEY=XXX
124
- OPENAI_API_KEY=XXX
125
- ANTHROPIC_API_KEY=XXX
126
- ```
127
 
128
- Optionally, you can set the debug level:
129
 
130
- ```
131
- VITE_LOG_LEVEL=debug
132
- ```
 
 
133
 
134
- And if using Ollama set the DEFAULT_NUM_CTX, the example below uses 8K context and ollama running on localhost port 11434:
 
 
 
 
135
 
136
- ```
137
- OLLAMA_API_BASE_URL=http://localhost:11434
138
- DEFAULT_NUM_CTX=8192
139
- ```
 
 
140
 
141
- **Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
142
 
143
- ## Run with Docker
144
 
145
- Prerequisites:
146
 
147
- Git and Node.js as mentioned above, as well as Docker: https://www.docker.com/
148
 
149
- ### 1a. Using Helper Scripts
 
150
 
151
- NPM scripts are provided for convenient building:
 
152
 
153
- ```bash
154
- # Development build
155
- npm run dockerbuild
156
 
157
- # Production build
158
- npm run dockerbuild:prod
159
- ```
160
 
161
- ### 1b. Direct Docker Build Commands (alternative to using NPM scripts)
 
 
162
 
163
- You can use Docker's target feature to specify the build environment instead of using NPM scripts if you wish:
 
164
 
165
- ```bash
166
- # Development build
167
- docker build . --target bolt-ai-development
168
 
169
- # Production build
170
- docker build . --target bolt-ai-production
171
- ```
172
 
173
- ### 2. Docker Compose with Profiles to Run the Container
174
 
175
- Use Docker Compose profiles to manage different environments:
176
-
177
- ```bash
178
- # Development environment
179
- docker-compose --profile development up
180
-
181
- # Production environment
182
- docker-compose --profile production up
183
- ```
184
-
185
- When you run the Docker Compose command with the development profile, any changes you
186
- make on your machine to the code will automatically be reflected in the site running
187
- on the container (i.e. hot reloading still applies!).
188
-
189
- ## Run Without Docker
190
-
191
- 1. Install dependencies using Terminal (or CMD in Windows with admin permissions):
192
-
193
- ```
194
- pnpm install
195
- ```
196
-
197
- 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:
198
-
199
- ```
200
- sudo npm install -g pnpm
201
- ```
202
-
203
- 2. Start the application with the command:
204
-
205
- ```bash
206
- pnpm run dev
207
- ```
208
  ## Available Scripts
209
 
210
- - `pnpm run dev`: Starts the development server.
211
- - `pnpm run build`: Builds the project.
212
- - `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.
213
- - `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`.
214
- - `pnpm test`: Runs the test suite using Vitest.
215
- - `pnpm run typecheck`: Runs TypeScript type checking.
216
- - `pnpm run typegen`: Generates TypeScript types using Wrangler.
217
- - `pnpm run deploy`: Builds the project and deploys it to Cloudflare Pages.
218
- - `pnpm run lint:fix`: Runs the linter and automatically fixes issues according to your ESLint configuration.
219
-
220
- ## Development
221
-
222
- To start the development server:
223
 
224
- ```bash
225
- pnpm run dev
226
- ```
227
 
228
- 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.
229
 
230
- ## How do I contribute to Bolt.diy?
231
 
232
- [Please check out our dedicated page for contributing to Bolt.diy here!](CONTRIBUTING.md)
233
 
234
- ## What are the future plans for Bolt.diy?
235
 
236
- [Check out our Roadmap here!](https://roadmap.sh/r/ottodev-roadmap-2ovzo)
237
 
238
- Lot more updates to this roadmap coming soon!
239
 
240
  ## FAQ
241
 
242
- [Please check out our dedicated page for FAQ's related to Bolt.diy 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": "5e1936f5de539324f840305bd94a22260c339511" }
 
1
+ { "commit": "e3bdd6944be8e4b4a6bb6c81f569fb3d5f444942" }
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.tsx CHANGED
@@ -26,6 +26,8 @@ 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
 
30
  const TEXTAREA_MIN_HEIGHT = 76;
31
 
@@ -75,7 +77,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
75
  input = '',
76
  enhancingPrompt,
77
  handleInputChange,
78
- promptEnhanced,
 
79
  enhancePrompt,
80
  sendMessage,
81
  handleStop,
@@ -283,7 +286,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
283
  <div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
284
  <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
285
  {!chatStarted && (
286
- <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center px-4 lg:px-0">
287
  <h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
288
  Where ideas begin
289
  </h1>
@@ -376,6 +379,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
376
  setImageDataList?.(imageDataList.filter((_, i) => i !== index));
377
  }}
378
  />
 
 
 
 
 
 
 
 
 
 
379
  <div
380
  className={classNames(
381
  'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
@@ -384,7 +397,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
384
  <textarea
385
  ref={textareaRef}
386
  className={classNames(
387
- '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',
388
  'transition-all duration-200',
389
  'hover:border-bolt-elements-focus',
390
  )}
@@ -431,6 +444,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
431
  return;
432
  }
433
 
 
 
 
 
 
434
  handleSendMessage?.(event);
435
  }
436
  }}
@@ -473,25 +491,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
473
  <IconButton
474
  title="Enhance prompt"
475
  disabled={input.length === 0 || enhancingPrompt}
476
- className={classNames(
477
- 'transition-all',
478
- enhancingPrompt ? 'opacity-100' : '',
479
- promptEnhanced ? 'text-bolt-elements-item-contentAccent' : '',
480
- promptEnhanced ? 'pr-1.5' : '',
481
- promptEnhanced ? 'enabled:hover:bg-bolt-elements-item-backgroundAccent' : '',
482
- )}
483
- onClick={() => enhancePrompt?.()}
484
  >
485
  {enhancingPrompt ? (
486
- <>
487
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
488
- <div className="ml-1.5">Enhancing prompt...</div>
489
- </>
490
  ) : (
491
- <>
492
- <div className="i-bolt:stars text-xl"></div>
493
- {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
494
- </>
495
  )}
496
  </IconButton>
497
 
 
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
 
 
77
  input = '',
78
  enhancingPrompt,
79
  handleInputChange,
80
+
81
+ // promptEnhanced,
82
  enhancePrompt,
83
  sendMessage,
84
  handleStop,
 
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>
 
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
  }}
 
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
 
app/components/chat/Chat.client.tsx CHANGED
@@ -93,8 +93,9 @@ export const ChatImpl = memo(
93
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
94
  const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
95
  const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
96
- const { activeProviders } = useSettings();
97
  const [searchParams, setSearchParams] = useSearchParams();
 
 
98
 
99
  const [model, setModel] = useState(() => {
100
  const savedModel = Cookies.get('selectedModel');
@@ -115,14 +116,25 @@ export const ChatImpl = memo(
115
  api: '/api/chat',
116
  body: {
117
  apiKeys,
 
 
118
  },
 
119
  onError: (error) => {
120
  logger.error('Request failed\n\n', error);
121
  toast.error(
122
  'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
123
  );
124
  },
125
- onFinish: () => {
 
 
 
 
 
 
 
 
126
  logger.debug('Finished streaming');
127
  },
128
  initialMessages,
 
93
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
94
  const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
95
  const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
 
96
  const [searchParams, setSearchParams] = useSearchParams();
97
+ const files = useStore(workbenchStore.files);
98
+ const { activeProviders, promptId } = useSettings();
99
 
100
  const [model, setModel] = useState(() => {
101
  const savedModel = Cookies.get('selectedModel');
 
116
  api: '/api/chat',
117
  body: {
118
  apiKeys,
119
+ files,
120
+ promptId,
121
  },
122
+ sendExtraMessageFields: true,
123
  onError: (error) => {
124
  logger.error('Request failed\n\n', error);
125
  toast.error(
126
  'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
127
  );
128
  },
129
+ onFinish: (message, response) => {
130
+ const usage = response.usage;
131
+
132
+ if (usage) {
133
+ console.log('Token usage:', usage);
134
+
135
+ // You can now use the usage data as needed
136
+ }
137
+
138
  logger.debug('Finished streaming');
139
  },
140
  initialMessages,
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>
 
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>
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/settings/SettingsWindow.tsx CHANGED
@@ -10,6 +10,7 @@ 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 ConnectionsTab from './connections/ConnectionsTab';
14
 
15
  interface SettingsProps {
@@ -17,18 +18,17 @@ interface SettingsProps {
17
  onClose: () => void;
18
  }
19
 
20
- type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'connection';
21
 
22
- // Providers that support base URL configuration
23
  export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
24
- const { debug } = 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: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
31
  { id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
 
32
  ...(debug
33
  ? [
34
  {
@@ -39,6 +39,16 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
39
  },
40
  ]
41
  : []),
 
 
 
 
 
 
 
 
 
 
42
  ];
43
 
44
  return (
 
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 {
 
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
  {
 
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 (
app/components/settings/chat-history/ChatHistoryTab.tsx CHANGED
@@ -4,6 +4,7 @@ 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
 
8
  export default function ChatHistoryTab() {
9
  const navigate = useNavigate();
@@ -21,8 +22,17 @@ export default function ChatHistoryTab() {
21
  };
22
 
23
  const handleDeleteAllChats = async () => {
 
 
 
 
 
 
24
  if (!db) {
 
 
25
  toast.error('Database is not available');
 
26
  return;
27
  }
28
 
@@ -30,13 +40,12 @@ export default function ChatHistoryTab() {
30
  setIsDeleting(true);
31
 
32
  const allChats = await getAll(db);
33
-
34
- // Delete all chats one by one
35
  await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
36
-
37
  toast.success('All chats deleted successfully');
38
  navigate('/', { replace: true });
39
  } catch (error) {
 
40
  toast.error('Failed to delete chats');
41
  console.error(error);
42
  } finally {
@@ -46,7 +55,10 @@ export default function ChatHistoryTab() {
46
 
47
  const handleExportAllChats = async () => {
48
  if (!db) {
 
 
49
  toast.error('Database is not available');
 
50
  return;
51
  }
52
 
@@ -58,8 +70,10 @@ export default function ChatHistoryTab() {
58
  };
59
 
60
  downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
 
61
  toast.success('Chats exported successfully');
62
  } catch (error) {
 
63
  toast.error('Failed to export chats');
64
  console.error(error);
65
  }
 
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();
 
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
 
 
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 {
 
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
 
 
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
  }
app/components/settings/connections/ConnectionsTab.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import React, { useState } from 'react';
2
  import { toast } from 'react-toastify';
3
  import Cookies from 'js-cookie';
 
4
 
5
  export default function ConnectionsTab() {
6
  const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || '');
@@ -9,7 +10,12 @@ export default function ConnectionsTab() {
9
  const handleSaveConnection = () => {
10
  Cookies.set('githubUsername', githubUsername);
11
  Cookies.set('githubToken', githubToken);
 
 
 
 
12
  toast.success('GitHub credentials saved successfully!');
 
13
  };
14
 
15
  return (
 
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') || '');
 
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 (
app/components/settings/debug/DebugTab.tsx CHANGED
@@ -1,69 +1,620 @@
1
  import React, { useCallback, useEffect, useState } from 'react';
2
  import { useSettings } from '~/lib/hooks/useSettings';
3
  import commit from '~/commit.json';
 
4
 
5
- const versionHash = commit.commit; // Get the version hash from commit.json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  export default function DebugTab() {
8
- const { providers } = useSettings();
9
- const [activeProviders, setActiveProviders] = useState<string[]>([]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  useEffect(() => {
11
- setActiveProviders(
12
- Object.entries(providers)
13
- .filter(([_key, provider]) => provider.settings.enabled)
14
- .map(([_key, provider]) => provider.name),
15
- );
16
  }, [providers]);
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const handleCopyToClipboard = useCallback(() => {
19
  const debugInfo = {
20
- OS: navigator.platform,
21
- Browser: navigator.userAgent,
22
- ActiveFeatures: activeProviders,
23
- BaseURLs: {
24
- Ollama: process.env.REACT_APP_OLLAMA_URL,
25
- OpenAI: process.env.REACT_APP_OPENAI_URL,
26
- LMStudio: process.env.REACT_APP_LM_STUDIO_URL,
 
 
 
 
 
 
 
27
  },
28
- Version: versionHash,
29
  };
 
30
  navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
31
- alert('Debug information copied to clipboard!');
32
  });
33
- }, [providers]);
34
 
35
  return (
36
- <div className="p-4">
37
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Debug Tab</h3>
38
- <button
39
- onClick={handleCopyToClipboard}
40
- className="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600 mb-4 transition-colors duration-200"
41
- >
42
- Copy to Clipboard
43
- </button>
44
-
45
- <h4 className="text-md font-medium text-bolt-elements-textPrimary">System Information</h4>
46
- <p className="text-bolt-elements-textSecondary">OS: {navigator.platform}</p>
47
- <p className="text-bolt-elements-textSecondary">Browser: {navigator.userAgent}</p>
48
-
49
- <h4 className="text-md font-medium text-bolt-elements-textPrimary mt-4">Active Features</h4>
50
- <ul>
51
- {activeProviders.map((name) => (
52
- <li key={name} className="text-bolt-elements-textSecondary">
53
- {name}
54
- </li>
55
- ))}
56
- </ul>
57
-
58
- <h4 className="text-md font-medium text-bolt-elements-textPrimary mt-4">Base URLs</h4>
59
- <ul>
60
- <li className="text-bolt-elements-textSecondary">Ollama: {process.env.REACT_APP_OLLAMA_URL}</li>
61
- <li className="text-bolt-elements-textSecondary">OpenAI: {process.env.REACT_APP_OPENAI_URL}</li>
62
- <li className="text-bolt-elements-textSecondary">LM Studio: {process.env.REACT_APP_LM_STUDIO_URL}</li>
63
- </ul>
64
-
65
- <h4 className="text-md font-medium text-bolt-elements-textPrimary mt-4">Version Information</h4>
66
- <p className="text-bolt-elements-textSecondary">Version Hash: {versionHash}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  </div>
68
  );
69
  }
 
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 CHANGED
@@ -1,16 +1,44 @@
1
  import React from 'react';
2
  import { Switch } from '~/components/ui/Switch';
 
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
 
5
  export default function FeaturesTab() {
6
- const { debug, enableDebugMode, isLocalModel, enableLocalModels } = useSettings();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  return (
8
  <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
9
  <div className="mb-6">
10
  <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Optional Features</h3>
11
- <div className="flex items-center justify-between mb-2">
12
- <span className="text-bolt-elements-textPrimary">Debug Info</span>
13
- <Switch className="ml-auto" checked={debug} onCheckedChange={enableDebugMode} />
 
 
 
 
 
 
 
 
 
 
 
14
  </div>
15
  </div>
16
 
@@ -19,10 +47,28 @@ export default function FeaturesTab() {
19
  <p className="text-sm text-bolt-elements-textSecondary mb-4">
20
  Disclaimer: Experimental features may be unstable and are subject to change.
21
  </p>
 
22
  <div className="flex items-center justify-between mb-2">
23
- <span className="text-bolt-elements-textPrimary">Enable Local Models</span>
24
  <Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
25
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  </div>
27
  </div>
28
  );
 
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
 
 
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
  );
app/components/settings/providers/ProvidersTab.tsx CHANGED
@@ -3,6 +3,10 @@ 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
 
7
  export default function ProvidersTab() {
8
  const { providers, updateProviderSettings, isLocalModel } = useSettings();
@@ -49,11 +53,30 @@ export default function ProvidersTab() {
49
  className="flex flex-col mb-2 provider-item hover:bg-bolt-elements-bg-depth-3 p-4 rounded-lg border border-bolt-elements-borderColor "
50
  >
51
  <div className="flex items-center justify-between mb-2">
52
- <span className="text-bolt-elements-textPrimary">{provider.name}</span>
 
 
 
 
 
 
 
 
 
 
 
53
  <Switch
54
  className="ml-auto"
55
  checked={provider.settings.enabled}
56
- onCheckedChange={(enabled) => updateProviderSettings(provider.name, { ...provider.settings, enabled })}
 
 
 
 
 
 
 
 
57
  />
58
  </div>
59
  {/* Base URL input for configurable providers */}
@@ -63,9 +86,14 @@ export default function ProvidersTab() {
63
  <input
64
  type="text"
65
  value={provider.settings.baseUrl || ''}
66
- onChange={(e) =>
67
- updateProviderSettings(provider.name, { ...provider.settings, baseUrl: e.target.value })
68
- }
 
 
 
 
 
69
  placeholder={`Enter ${provider.name} base URL`}
70
  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"
71
  />
 
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();
 
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 */}
 
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
  />
app/components/sidebar/Menu.client.tsx CHANGED
@@ -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"
 
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"
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/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/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
@@ -128,6 +128,15 @@ export function getXAIModel(apiKey: OptionalApiKey, model: string) {
128
  return openai(model);
129
  }
130
 
 
 
 
 
 
 
 
 
 
131
  export function getModel(
132
  provider: string,
133
  model: string,
@@ -170,6 +179,8 @@ export function getModel(
170
  return getXAIModel(apiKey, model);
171
  case 'Cohere':
172
  return getCohereAIModel(apiKey, model);
 
 
173
  default:
174
  return getOllamaModel(baseURL, model);
175
  }
 
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,
 
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,9 +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
  import type { IProviderSetting } from '~/types/model';
 
 
7
 
8
  interface ToolResult<Name extends string, Args, Result> {
9
  toolCallId: string;
@@ -23,6 +34,78 @@ export type Messages = Message[];
23
 
24
  export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } {
27
  const textContent = Array.isArray(message.content)
28
  ? message.content.find((item) => item.type === 'text')?.text || ''
@@ -64,9 +147,11 @@ export async function streamText(props: {
64
  env: Env;
65
  options?: StreamingOptions;
66
  apiKeys?: Record<string, string>;
 
67
  providerSettings?: Record<string, IProviderSetting>;
 
68
  }) {
69
- const { messages, env, options, apiKeys, providerSettings } = props;
70
  let currentModel = DEFAULT_MODEL;
71
  let currentProvider = DEFAULT_PROVIDER.name;
72
  const MODEL_LIST = await getModelList(apiKeys || {}, providerSettings);
@@ -80,6 +165,12 @@ export async function streamText(props: {
80
 
81
  currentProvider = provider;
82
 
 
 
 
 
 
 
83
  return { ...message, content };
84
  }
85
 
@@ -90,9 +181,23 @@ export async function streamText(props: {
90
 
91
  const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  return _streamText({
94
  model: getModel(currentProvider, currentModel, env, apiKeys, providerSettings) as any,
95
- system: getSystemPrompt(),
96
  maxTokens: dynamicMaxTokens,
97
  messages: convertToCoreMessages(processedMessages as any),
98
  ...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 || ''
 
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);
 
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
+ };
app/lib/{.server/llm → common/prompts}/prompts.ts RENAMED
File without changes
app/lib/hooks/useMessageParser.ts CHANGED
@@ -23,14 +23,14 @@ const messageParser = new StreamingMessageParser({
23
  logger.trace('onActionOpen', data.action);
24
 
25
  // we only add shell actions when when the close tag got parsed because only then we have the content
26
- if (data.action.type !== 'shell') {
27
  workbenchStore.addAction(data);
28
  }
29
  },
30
  onActionClose: (data) => {
31
  logger.trace('onActionClose', data.action);
32
 
33
- if (data.action.type === 'shell') {
34
  workbenchStore.addAction(data);
35
  }
36
 
 
23
  logger.trace('onActionOpen', data.action);
24
 
25
  // we only add shell actions when when the close tag got parsed because only then we have the content
26
+ if (data.action.type === 'file') {
27
  workbenchStore.addAction(data);
28
  }
29
  },
30
  onActionClose: (data) => {
31
  logger.trace('onActionClose', data.action);
32
 
33
+ if (data.action.type !== 'file') {
34
  workbenchStore.addAction(data);
35
  }
36
 
app/lib/hooks/useSettings.tsx CHANGED
@@ -1,15 +1,56 @@
1
  import { useStore } from '@nanostores/react';
2
- import { isDebugMode, isLocalModelsEnabled, LOCAL_PROVIDERS, providersStore } from '~/lib/stores/settings';
 
 
 
 
 
 
 
 
3
  import { useCallback, useEffect, useState } from 'react';
4
  import Cookies from 'js-cookie';
5
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
 
 
 
 
 
 
 
 
 
6
 
7
  export function useSettings() {
8
  const providers = useStore(providersStore);
9
  const debug = useStore(isDebugMode);
 
 
10
  const isLocalModel = useStore(isLocalModelsEnabled);
 
11
  const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  // reading values from cookies on mount
14
  useEffect(() => {
15
  const savedProviders = Cookies.get('providers');
@@ -23,7 +64,7 @@ export function useSettings() {
23
  ...currentProvider,
24
  settings: {
25
  ...parsedProviders[provider],
26
- enabled: parsedProviders[provider].enabled || true,
27
  },
28
  });
29
  });
@@ -39,12 +80,45 @@ export function useSettings() {
39
  isDebugMode.set(savedDebugMode === 'true');
40
  }
41
 
 
 
 
 
 
 
 
42
  // load local models from cookies
43
  const savedLocalModels = Cookies.get('isLocalModelsEnabled');
44
 
45
  if (savedLocalModels) {
46
  isLocalModelsEnabled.set(savedLocalModels === 'true');
47
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }, []);
49
 
50
  // writing values to cookies on change
@@ -70,28 +144,55 @@ export function useSettings() {
70
  }, [providers, isLocalModel]);
71
 
72
  // helper function to update settings
73
- const updateProviderSettings = useCallback((provider: string, config: IProviderSetting) => {
74
- const settings = providers[provider].settings;
75
- providersStore.setKey(provider, { ...providers[provider], settings: { ...settings, ...config } });
76
- }, []);
 
 
 
77
 
78
  const enableDebugMode = useCallback((enabled: boolean) => {
79
  isDebugMode.set(enabled);
 
80
  Cookies.set('isDebugEnabled', String(enabled));
81
  }, []);
82
 
 
 
 
 
 
 
83
  const enableLocalModels = useCallback((enabled: boolean) => {
84
  isLocalModelsEnabled.set(enabled);
 
85
  Cookies.set('isLocalModelsEnabled', String(enabled));
86
  }, []);
87
 
 
 
 
 
 
 
 
 
 
 
88
  return {
89
  providers,
90
  activeProviders,
91
  updateProviderSettings,
92
  debug,
93
  enableDebugMode,
 
 
94
  isLocalModel,
95
  enableLocalModels,
 
 
 
 
96
  };
97
  }
 
1
  import { useStore } from '@nanostores/react';
2
+ import {
3
+ isDebugMode,
4
+ isEventLogsEnabled,
5
+ isLocalModelsEnabled,
6
+ LOCAL_PROVIDERS,
7
+ promptStore,
8
+ providersStore,
9
+ latestBranchStore,
10
+ } from '~/lib/stores/settings';
11
  import { useCallback, useEffect, useState } from 'react';
12
  import Cookies from 'js-cookie';
13
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
14
+ import { logStore } from '~/lib/stores/logs'; // assuming logStore is imported from this location
15
+ import commit from '~/commit.json';
16
+
17
+ interface CommitData {
18
+ commit: string;
19
+ version?: string;
20
+ }
21
+
22
+ const commitJson: CommitData = commit;
23
 
24
  export function useSettings() {
25
  const providers = useStore(providersStore);
26
  const debug = useStore(isDebugMode);
27
+ const eventLogs = useStore(isEventLogsEnabled);
28
+ const promptId = useStore(promptStore);
29
  const isLocalModel = useStore(isLocalModelsEnabled);
30
+ const isLatestBranch = useStore(latestBranchStore);
31
  const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
32
 
33
+ // Function to check if we're on stable version
34
+ const checkIsStableVersion = async () => {
35
+ try {
36
+ const stableResponse = await fetch(
37
+ `https://raw.githubusercontent.com/stackblitz-labs/bolt.diy/refs/tags/v${commitJson.version}/app/commit.json`,
38
+ );
39
+
40
+ if (!stableResponse.ok) {
41
+ console.warn('Failed to fetch stable commit info');
42
+ return false;
43
+ }
44
+
45
+ const stableData = (await stableResponse.json()) as CommitData;
46
+
47
+ return commit.commit === stableData.commit;
48
+ } catch (error) {
49
+ console.warn('Error checking stable version:', error);
50
+ return false;
51
+ }
52
+ };
53
+
54
  // reading values from cookies on mount
55
  useEffect(() => {
56
  const savedProviders = Cookies.get('providers');
 
64
  ...currentProvider,
65
  settings: {
66
  ...parsedProviders[provider],
67
+ enabled: parsedProviders[provider].enabled ?? true,
68
  },
69
  });
70
  });
 
80
  isDebugMode.set(savedDebugMode === 'true');
81
  }
82
 
83
+ // load event logs from cookies
84
+ const savedEventLogs = Cookies.get('isEventLogsEnabled');
85
+
86
+ if (savedEventLogs) {
87
+ isEventLogsEnabled.set(savedEventLogs === 'true');
88
+ }
89
+
90
  // load local models from cookies
91
  const savedLocalModels = Cookies.get('isLocalModelsEnabled');
92
 
93
  if (savedLocalModels) {
94
  isLocalModelsEnabled.set(savedLocalModels === 'true');
95
  }
96
+
97
+ const promptId = Cookies.get('promptId');
98
+
99
+ if (promptId) {
100
+ promptStore.set(promptId);
101
+ }
102
+
103
+ // load latest branch setting from cookies or determine based on version
104
+ const savedLatestBranch = Cookies.get('isLatestBranch');
105
+ let checkCommit = Cookies.get('commitHash');
106
+
107
+ if (checkCommit === undefined) {
108
+ checkCommit = commit.commit;
109
+ }
110
+
111
+ if (savedLatestBranch === undefined || checkCommit !== commit.commit) {
112
+ // If setting hasn't been set by user, check version
113
+ checkIsStableVersion().then((isStable) => {
114
+ const shouldUseLatest = !isStable;
115
+ latestBranchStore.set(shouldUseLatest);
116
+ Cookies.set('isLatestBranch', String(shouldUseLatest));
117
+ Cookies.set('commitHash', String(commit.commit));
118
+ });
119
+ } else {
120
+ latestBranchStore.set(savedLatestBranch === 'true');
121
+ }
122
  }, []);
123
 
124
  // writing values to cookies on change
 
144
  }, [providers, isLocalModel]);
145
 
146
  // helper function to update settings
147
+ const updateProviderSettings = useCallback(
148
+ (provider: string, config: IProviderSetting) => {
149
+ const settings = providers[provider].settings;
150
+ providersStore.setKey(provider, { ...providers[provider], settings: { ...settings, ...config } });
151
+ },
152
+ [providers],
153
+ );
154
 
155
  const enableDebugMode = useCallback((enabled: boolean) => {
156
  isDebugMode.set(enabled);
157
+ logStore.logSystem(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
158
  Cookies.set('isDebugEnabled', String(enabled));
159
  }, []);
160
 
161
+ const enableEventLogs = useCallback((enabled: boolean) => {
162
+ isEventLogsEnabled.set(enabled);
163
+ logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
164
+ Cookies.set('isEventLogsEnabled', String(enabled));
165
+ }, []);
166
+
167
  const enableLocalModels = useCallback((enabled: boolean) => {
168
  isLocalModelsEnabled.set(enabled);
169
+ logStore.logSystem(`Local models ${enabled ? 'enabled' : 'disabled'}`);
170
  Cookies.set('isLocalModelsEnabled', String(enabled));
171
  }, []);
172
 
173
+ const setPromptId = useCallback((promptId: string) => {
174
+ promptStore.set(promptId);
175
+ Cookies.set('promptId', promptId);
176
+ }, []);
177
+ const enableLatestBranch = useCallback((enabled: boolean) => {
178
+ latestBranchStore.set(enabled);
179
+ logStore.logSystem(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
180
+ Cookies.set('isLatestBranch', String(enabled));
181
+ }, []);
182
+
183
  return {
184
  providers,
185
  activeProviders,
186
  updateProviderSettings,
187
  debug,
188
  enableDebugMode,
189
+ eventLogs,
190
+ enableEventLogs,
191
  isLocalModel,
192
  enableLocalModels,
193
+ promptId,
194
+ setPromptId,
195
+ isLatestBranch,
196
+ enableLatestBranch,
197
  };
198
  }
app/lib/persistence/useChatHistory.ts CHANGED
@@ -4,6 +4,7 @@ import { atom } from 'nanostores';
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
 
7
  import {
8
  getMessages,
9
  getNextId,
@@ -43,6 +44,8 @@ export function useChatHistory() {
43
  setReady(true);
44
 
45
  if (persistenceEnabled) {
 
 
46
  toast.error('Chat persistence is unavailable');
47
  }
48
 
@@ -69,6 +72,7 @@ export function useChatHistory() {
69
  setReady(true);
70
  })
71
  .catch((error) => {
 
72
  toast.error(error.message);
73
  });
74
  }
 
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
7
+ import { logStore } from '~/lib/stores/logs'; // Import logStore
8
  import {
9
  getMessages,
10
  getNextId,
 
44
  setReady(true);
45
 
46
  if (persistenceEnabled) {
47
+ const error = new Error('Chat persistence is unavailable');
48
+ logStore.logError('Chat persistence initialization failed', error);
49
  toast.error('Chat persistence is unavailable');
50
  }
51
 
 
72
  setReady(true);
73
  })
74
  .catch((error) => {
75
+ logStore.logError('Failed to load chat messages', error);
76
  toast.error(error.message);
77
  });
78
  }
app/lib/stores/logs.ts ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { atom, map } from 'nanostores';
2
+ import Cookies from 'js-cookie';
3
+ import { createScopedLogger } from '~/utils/logger';
4
+
5
+ const logger = createScopedLogger('LogStore');
6
+
7
+ export interface LogEntry {
8
+ id: string;
9
+ timestamp: string;
10
+ level: 'info' | 'warning' | 'error' | 'debug';
11
+ message: string;
12
+ details?: Record<string, any>;
13
+ category: 'system' | 'provider' | 'user' | 'error';
14
+ }
15
+
16
+ const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
17
+
18
+ class LogStore {
19
+ private _logs = map<Record<string, LogEntry>>({});
20
+ showLogs = atom(true);
21
+
22
+ constructor() {
23
+ // Load saved logs from cookies on initialization
24
+ this._loadLogs();
25
+ }
26
+
27
+ private _loadLogs() {
28
+ const savedLogs = Cookies.get('eventLogs');
29
+
30
+ if (savedLogs) {
31
+ try {
32
+ const parsedLogs = JSON.parse(savedLogs);
33
+ this._logs.set(parsedLogs);
34
+ } catch (error) {
35
+ logger.error('Failed to parse logs from cookies:', error);
36
+ }
37
+ }
38
+ }
39
+
40
+ private _saveLogs() {
41
+ const currentLogs = this._logs.get();
42
+ Cookies.set('eventLogs', JSON.stringify(currentLogs));
43
+ }
44
+
45
+ private _generateId(): string {
46
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
47
+ }
48
+
49
+ private _trimLogs() {
50
+ const currentLogs = Object.entries(this._logs.get());
51
+
52
+ if (currentLogs.length > MAX_LOGS) {
53
+ const sortedLogs = currentLogs.sort(
54
+ ([, a], [, b]) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
55
+ );
56
+ const newLogs = Object.fromEntries(sortedLogs.slice(0, MAX_LOGS));
57
+ this._logs.set(newLogs);
58
+ }
59
+ }
60
+
61
+ addLog(
62
+ message: string,
63
+ level: LogEntry['level'] = 'info',
64
+ category: LogEntry['category'] = 'system',
65
+ details?: Record<string, any>,
66
+ ) {
67
+ const id = this._generateId();
68
+ const entry: LogEntry = {
69
+ id,
70
+ timestamp: new Date().toISOString(),
71
+ level,
72
+ message,
73
+ details,
74
+ category,
75
+ };
76
+
77
+ this._logs.setKey(id, entry);
78
+ this._trimLogs();
79
+ this._saveLogs();
80
+
81
+ return id;
82
+ }
83
+
84
+ // System events
85
+ logSystem(message: string, details?: Record<string, any>) {
86
+ return this.addLog(message, 'info', 'system', details);
87
+ }
88
+
89
+ // Provider events
90
+ logProvider(message: string, details?: Record<string, any>) {
91
+ return this.addLog(message, 'info', 'provider', details);
92
+ }
93
+
94
+ // User actions
95
+ logUserAction(message: string, details?: Record<string, any>) {
96
+ return this.addLog(message, 'info', 'user', details);
97
+ }
98
+
99
+ // Error events
100
+ logError(message: string, error?: Error | unknown, details?: Record<string, any>) {
101
+ const errorDetails = {
102
+ ...(details || {}),
103
+ error:
104
+ error instanceof Error
105
+ ? {
106
+ message: error.message,
107
+ stack: error.stack,
108
+ }
109
+ : error,
110
+ };
111
+ return this.addLog(message, 'error', 'error', errorDetails);
112
+ }
113
+
114
+ // Warning events
115
+ logWarning(message: string, details?: Record<string, any>) {
116
+ return this.addLog(message, 'warning', 'system', details);
117
+ }
118
+
119
+ // Debug events
120
+ logDebug(message: string, details?: Record<string, any>) {
121
+ return this.addLog(message, 'debug', 'system', details);
122
+ }
123
+
124
+ clearLogs() {
125
+ this._logs.set({});
126
+ this._saveLogs();
127
+ }
128
+
129
+ getLogs() {
130
+ return Object.values(this._logs.get()).sort(
131
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
132
+ );
133
+ }
134
+
135
+ getFilteredLogs(level?: LogEntry['level'], category?: LogEntry['category'], searchQuery?: string) {
136
+ return this.getLogs().filter((log) => {
137
+ const matchesLevel = !level || level === 'debug' || log.level === level;
138
+ const matchesCategory = !category || log.category === category;
139
+ const matchesSearch =
140
+ !searchQuery ||
141
+ log.message.toLowerCase().includes(searchQuery.toLowerCase()) ||
142
+ JSON.stringify(log.details).toLowerCase().includes(searchQuery.toLowerCase());
143
+
144
+ return matchesLevel && matchesCategory && matchesSearch;
145
+ });
146
+ }
147
+ }
148
+
149
+ export const logStore = new LogStore();
app/lib/stores/settings.ts CHANGED
@@ -35,7 +35,7 @@ PROVIDER_LIST.forEach((provider) => {
35
  initialProviderSettings[provider.name] = {
36
  ...provider,
37
  settings: {
38
- enabled: false,
39
  },
40
  };
41
  });
@@ -43,4 +43,10 @@ export const providersStore = map<ProviderSetting>(initialProviderSettings);
43
 
44
  export const isDebugMode = atom(false);
45
 
 
 
46
  export const isLocalModelsEnabled = atom(true);
 
 
 
 
 
35
  initialProviderSettings[provider.name] = {
36
  ...provider,
37
  settings: {
38
+ enabled: true,
39
  },
40
  };
41
  });
 
43
 
44
  export const isDebugMode = atom(false);
45
 
46
+ export const isEventLogsEnabled = atom(false);
47
+
48
  export const isLocalModelsEnabled = atom(true);
49
+
50
+ export const promptStore = atom<string>('default');
51
+
52
+ export const latestBranchStore = atom(false);
app/lib/stores/theme.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { atom } from 'nanostores';
 
2
 
3
  export type Theme = 'dark' | 'light';
4
 
@@ -26,10 +27,8 @@ function initStore() {
26
  export function toggleTheme() {
27
  const currentTheme = themeStore.get();
28
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
29
-
30
  themeStore.set(newTheme);
31
-
32
  localStorage.setItem(kTheme, newTheme);
33
-
34
  document.querySelector('html')?.setAttribute('data-theme', newTheme);
35
  }
 
1
  import { atom } from 'nanostores';
2
+ import { logStore } from './logs';
3
 
4
  export type Theme = 'dark' | 'light';
5
 
 
27
  export function toggleTheme() {
28
  const currentTheme = themeStore.get();
29
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
 
30
  themeStore.set(newTheme);
31
+ logStore.logSystem(`Theme changed to ${newTheme} mode`);
32
  localStorage.setItem(kTheme, newTheme);
 
33
  document.querySelector('html')?.setAttribute('data-theme', newTheme);
34
  }
app/lib/stores/workbench.ts CHANGED
@@ -16,6 +16,7 @@ import * as nodePath from 'node:path';
16
  import { extractRelativePath } from '~/utils/diff';
17
  import { description } from '~/lib/persistence';
18
  import Cookies from 'js-cookie';
 
19
 
20
  export interface ArtifactState {
21
  id: string;
@@ -262,9 +263,9 @@ export class WorkbenchStore {
262
  this.artifacts.setKey(messageId, { ...artifact, ...state });
263
  }
264
  addAction(data: ActionCallbackData) {
265
- this._addAction(data);
266
 
267
- // this.addToExecutionQueue(()=>this._addAction(data))
268
  }
269
  async _addAction(data: ActionCallbackData) {
270
  const { messageId } = data;
@@ -280,7 +281,7 @@ export class WorkbenchStore {
280
 
281
  runAction(data: ActionCallbackData, isStreaming: boolean = false) {
282
  if (isStreaming) {
283
- this._runAction(data, isStreaming);
284
  } else {
285
  this.addToExecutionQueue(() => this._runAction(data, isStreaming));
286
  }
@@ -294,6 +295,12 @@ export class WorkbenchStore {
294
  unreachable('Artifact not found');
295
  }
296
 
 
 
 
 
 
 
297
  if (data.action.type === 'file') {
298
  const wc = await webcontainer;
299
  const fullPath = nodePath.join(wc.workdir, data.action.filePath);
@@ -323,6 +330,10 @@ export class WorkbenchStore {
323
  }
324
  }
325
 
 
 
 
 
326
  #getArtifact(id: string) {
327
  const artifacts = this.artifacts.get();
328
  return artifacts[id];
 
16
  import { extractRelativePath } from '~/utils/diff';
17
  import { description } from '~/lib/persistence';
18
  import Cookies from 'js-cookie';
19
+ import { createSampler } from '~/utils/sampler';
20
 
21
  export interface ArtifactState {
22
  id: string;
 
263
  this.artifacts.setKey(messageId, { ...artifact, ...state });
264
  }
265
  addAction(data: ActionCallbackData) {
266
+ // this._addAction(data);
267
 
268
+ this.addToExecutionQueue(() => this._addAction(data));
269
  }
270
  async _addAction(data: ActionCallbackData) {
271
  const { messageId } = data;
 
281
 
282
  runAction(data: ActionCallbackData, isStreaming: boolean = false) {
283
  if (isStreaming) {
284
+ this.actionStreamSampler(data, isStreaming);
285
  } else {
286
  this.addToExecutionQueue(() => this._runAction(data, isStreaming));
287
  }
 
295
  unreachable('Artifact not found');
296
  }
297
 
298
+ const action = artifact.runner.actions.get()[data.actionId];
299
+
300
+ if (!action || action.executed) {
301
+ return;
302
+ }
303
+
304
  if (data.action.type === 'file') {
305
  const wc = await webcontainer;
306
  const fullPath = nodePath.join(wc.workdir, data.action.filePath);
 
330
  }
331
  }
332
 
333
+ actionStreamSampler = createSampler(async (data: ActionCallbackData, isStreaming: boolean = false) => {
334
+ return await this._runAction(data, isStreaming);
335
+ }, 100); // TODO: remove this magic number to have it configurable
336
+
337
  #getArtifact(id: string) {
338
  const artifacts = this.artifacts.get();
339
  return artifacts[id];
app/root.tsx CHANGED
@@ -78,6 +78,23 @@ export function Layout({ children }: { children: React.ReactNode }) {
78
  );
79
  }
80
 
 
 
81
  export default function App() {
82
- return <Outlet />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
 
78
  );
79
  }
80
 
81
+ import { logStore } from './lib/stores/logs';
82
+
83
  export default function App() {
84
+ const theme = useStore(themeStore);
85
+
86
+ useEffect(() => {
87
+ logStore.logSystem('Application initialized', {
88
+ theme,
89
+ platform: navigator.platform,
90
+ userAgent: navigator.userAgent,
91
+ timestamp: new Date().toISOString(),
92
+ });
93
+ }, []);
94
+
95
+ return (
96
+ <Layout>
97
+ <Outlet />
98
+ </Layout>
99
+ );
100
  }
app/routes/api.chat.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { type ActionFunctionArgs } from '@remix-run/cloudflare';
 
2
  import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
3
- import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
4
  import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
5
  import SwitchableStream from '~/lib/.server/llm/switchable-stream';
6
  import type { IProviderSetting } from '~/types/model';
@@ -9,17 +10,15 @@ export async function action(args: ActionFunctionArgs) {
9
  return chatAction(args);
10
  }
11
 
12
- function parseCookies(cookieHeader: string) {
13
- const cookies: any = {};
14
 
15
- // Split the cookie string by semicolons and spaces
16
  const items = cookieHeader.split(';').map((cookie) => cookie.trim());
17
 
18
  items.forEach((item) => {
19
  const [name, ...rest] = item.split('=');
20
 
21
  if (name && rest) {
22
- // Decode the name and value, and join value parts in case it contains '='
23
  const decodedName = decodeURIComponent(name.trim());
24
  const decodedValue = decodeURIComponent(rest.join('=').trim());
25
  cookies[decodedName] = decodedValue;
@@ -30,14 +29,13 @@ function parseCookies(cookieHeader: string) {
30
  }
31
 
32
  async function chatAction({ context, request }: ActionFunctionArgs) {
33
- const { messages } = await request.json<{
34
  messages: Messages;
35
- model: string;
 
36
  }>();
37
 
38
  const cookieHeader = request.headers.get('Cookie');
39
-
40
- // Parse the cookie's value (returns an object or null if no cookie exists)
41
  const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
42
  const providerSettings: Record<string, IProviderSetting> = JSON.parse(
43
  parseCookies(cookieHeader || '').providers || '{}',
@@ -45,12 +43,42 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
45
 
46
  const stream = new SwitchableStream();
47
 
 
 
 
 
 
 
48
  try {
49
  const options: StreamingOptions = {
50
  toolChoice: 'none',
51
- onFinish: async ({ text: content, finishReason }) => {
 
 
 
 
 
 
 
 
52
  if (finishReason !== 'length') {
53
- return stream.close();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  }
55
 
56
  if (stream.switches >= MAX_RESPONSE_SEGMENTS) {
@@ -64,15 +92,31 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
64
  messages.push({ role: 'assistant', content });
65
  messages.push({ role: 'user', content: CONTINUE_PROMPT });
66
 
67
- const result = await streamText({ messages, env: context.cloudflare.env, options, apiKeys, providerSettings });
68
-
69
- return stream.switchSource(result.toAIStream());
 
 
 
 
 
 
 
 
70
  },
71
  };
72
 
73
- const result = await streamText({ messages, env: context.cloudflare.env, options, apiKeys, providerSettings });
 
 
 
 
 
 
 
 
74
 
75
- stream.switchSource(result.toAIStream());
76
 
77
  return new Response(stream.readable, {
78
  status: 200,
@@ -81,7 +125,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
81
  },
82
  });
83
  } catch (error: any) {
84
- console.log(error);
85
 
86
  if (error.message?.includes('API key')) {
87
  throw new Response('Invalid or missing API key', {
 
1
  import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2
+ import { createDataStream } from 'ai';
3
  import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
4
+ import { CONTINUE_PROMPT } from '~/lib/common/prompts/prompts';
5
  import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
6
  import SwitchableStream from '~/lib/.server/llm/switchable-stream';
7
  import type { IProviderSetting } from '~/types/model';
 
10
  return chatAction(args);
11
  }
12
 
13
+ function parseCookies(cookieHeader: string): Record<string, string> {
14
+ const cookies: Record<string, string> = {};
15
 
 
16
  const items = cookieHeader.split(';').map((cookie) => cookie.trim());
17
 
18
  items.forEach((item) => {
19
  const [name, ...rest] = item.split('=');
20
 
21
  if (name && rest) {
 
22
  const decodedName = decodeURIComponent(name.trim());
23
  const decodedValue = decodeURIComponent(rest.join('=').trim());
24
  cookies[decodedName] = decodedValue;
 
29
  }
30
 
31
  async function chatAction({ context, request }: ActionFunctionArgs) {
32
+ const { messages, files, promptId } = await request.json<{
33
  messages: Messages;
34
+ files: any;
35
+ promptId?: string;
36
  }>();
37
 
38
  const cookieHeader = request.headers.get('Cookie');
 
 
39
  const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
40
  const providerSettings: Record<string, IProviderSetting> = JSON.parse(
41
  parseCookies(cookieHeader || '').providers || '{}',
 
43
 
44
  const stream = new SwitchableStream();
45
 
46
+ const cumulativeUsage = {
47
+ completionTokens: 0,
48
+ promptTokens: 0,
49
+ totalTokens: 0,
50
+ };
51
+
52
  try {
53
  const options: StreamingOptions = {
54
  toolChoice: 'none',
55
+ onFinish: async ({ text: content, finishReason, usage }) => {
56
+ console.log('usage', usage);
57
+
58
+ if (usage) {
59
+ cumulativeUsage.completionTokens += usage.completionTokens || 0;
60
+ cumulativeUsage.promptTokens += usage.promptTokens || 0;
61
+ cumulativeUsage.totalTokens += usage.totalTokens || 0;
62
+ }
63
+
64
  if (finishReason !== 'length') {
65
+ return stream
66
+ .switchSource(
67
+ createDataStream({
68
+ async execute(dataStream) {
69
+ dataStream.writeMessageAnnotation({
70
+ type: 'usage',
71
+ value: {
72
+ completionTokens: cumulativeUsage.completionTokens,
73
+ promptTokens: cumulativeUsage.promptTokens,
74
+ totalTokens: cumulativeUsage.totalTokens,
75
+ },
76
+ });
77
+ },
78
+ onError: (error: any) => `Custom error: ${error.message}`,
79
+ }),
80
+ )
81
+ .then(() => stream.close());
82
  }
83
 
84
  if (stream.switches >= MAX_RESPONSE_SEGMENTS) {
 
92
  messages.push({ role: 'assistant', content });
93
  messages.push({ role: 'user', content: CONTINUE_PROMPT });
94
 
95
+ const result = await streamText({
96
+ messages,
97
+ env: context.cloudflare.env,
98
+ options,
99
+ apiKeys,
100
+ files,
101
+ providerSettings,
102
+ promptId,
103
+ });
104
+
105
+ return stream.switchSource(result.toDataStream());
106
  },
107
  };
108
 
109
+ const result = await streamText({
110
+ messages,
111
+ env: context.cloudflare.env,
112
+ options,
113
+ apiKeys,
114
+ files,
115
+ providerSettings,
116
+ promptId,
117
+ });
118
 
119
+ stream.switchSource(result.toDataStream());
120
 
121
  return new Response(stream.readable, {
122
  status: 200,
 
125
  },
126
  });
127
  } catch (error: any) {
128
+ console.error(error);
129
 
130
  if (error.message?.includes('API key')) {
131
  throw new Response('Invalid or missing API key', {
app/routes/api.enhancer.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2
- import { StreamingTextResponse, parseStreamPart } from 'ai';
 
3
  import { streamText } from '~/lib/.server/llm/stream-text';
4
  import { stripIndents } from '~/utils/stripIndent';
5
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
@@ -73,32 +74,32 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
73
  `[Model: ${model}]\n\n[Provider: ${providerName}]\n\n` +
74
  stripIndents`
75
  You are a professional prompt engineer specializing in crafting precise, effective prompts.
76
- Your task is to enhance prompts by making them more specific, actionable, and effective.
77
-
78
- I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
79
-
80
- For valid prompts:
81
- - Make instructions explicit and unambiguous
82
- - Add relevant context and constraints
83
- - Remove redundant information
84
- - Maintain the core intent
85
- - Ensure the prompt is self-contained
86
- - Use professional language
87
-
88
- For invalid or unclear prompts:
89
- - Respond with a clear, professional guidance message
90
- - Keep responses concise and actionable
91
- - Maintain a helpful, constructive tone
92
- - Focus on what the user should provide
93
- - Use a standard template for consistency
94
-
95
- IMPORTANT: Your response must ONLY contain the enhanced prompt text.
96
- Do not include any explanations, metadata, or wrapper tags.
97
-
98
- <original_prompt>
99
- ${message}
100
- </original_prompt>
101
- `,
102
  },
103
  ],
104
  env: context.cloudflare.env,
@@ -113,7 +114,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
113
 
114
  for (const line of lines) {
115
  try {
116
- const parsed = parseStreamPart(line);
117
 
118
  if (parsed.type === 'text') {
119
  controller.enqueue(encoder.encode(parsed.value));
@@ -128,7 +129,12 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
128
 
129
  const transformedStream = result.toDataStream().pipeThrough(transformStream);
130
 
131
- return new StreamingTextResponse(transformedStream);
 
 
 
 
 
132
  } catch (error: unknown) {
133
  console.log(error);
134
 
 
1
  import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2
+
3
+ //import { StreamingTextResponse, parseStreamPart } from 'ai';
4
  import { streamText } from '~/lib/.server/llm/stream-text';
5
  import { stripIndents } from '~/utils/stripIndent';
6
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
 
74
  `[Model: ${model}]\n\n[Provider: ${providerName}]\n\n` +
75
  stripIndents`
76
  You are a professional prompt engineer specializing in crafting precise, effective prompts.
77
+ Your task is to enhance prompts by making them more specific, actionable, and effective.
78
+
79
+ I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
80
+
81
+ For valid prompts:
82
+ - Make instructions explicit and unambiguous
83
+ - Add relevant context and constraints
84
+ - Remove redundant information
85
+ - Maintain the core intent
86
+ - Ensure the prompt is self-contained
87
+ - Use professional language
88
+
89
+ For invalid or unclear prompts:
90
+ - Respond with clear, professional guidance
91
+ - Keep responses concise and actionable
92
+ - Maintain a helpful, constructive tone
93
+ - Focus on what the user should provide
94
+ - Use a standard template for consistency
95
+
96
+ IMPORTANT: Your response must ONLY contain the enhanced prompt text.
97
+ Do not include any explanations, metadata, or wrapper tags.
98
+
99
+ <original_prompt>
100
+ ${message}
101
+ </original_prompt>
102
+ `,
103
  },
104
  ],
105
  env: context.cloudflare.env,
 
114
 
115
  for (const line of lines) {
116
  try {
117
+ const parsed = JSON.parse(line);
118
 
119
  if (parsed.type === 'text') {
120
  controller.enqueue(encoder.encode(parsed.value));
 
129
 
130
  const transformedStream = result.toDataStream().pipeThrough(transformStream);
131
 
132
+ return new Response(transformedStream, {
133
+ status: 200,
134
+ headers: {
135
+ 'Content-Type': 'text/plain; charset=utf-8',
136
+ },
137
+ });
138
  } catch (error: unknown) {
139
  console.log(error);
140