├── .dockerignore ├── .editorconfig ├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.md ├── actions │ └── setup-and-build │ │ └── action.yaml ├── scripts │ └── generate-changelog.sh └── workflows │ ├── ci.yaml │ ├── docs.yaml │ ├── pr-release-validation.yaml │ ├── semantic-pr.yaml │ ├── stale.yml │ └── update-stable.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .tool-versions ├── CONTRIBUTING.md ├── Dockerfile ├── FAQ.md ├── LICENSE ├── README.md ├── app ├── commit.json ├── components │ ├── chat │ │ ├── APIKeyManager.tsx │ │ ├── Artifact.tsx │ │ ├── AssistantMessage.tsx │ │ ├── BaseChat.module.scss │ │ ├── BaseChat.tsx │ │ ├── Chat.client.tsx │ │ ├── ChatAlert.tsx │ │ ├── CodeBlock.module.scss │ │ ├── CodeBlock.tsx │ │ ├── ExamplePrompts.tsx │ │ ├── FilePreview.tsx │ │ ├── GitCloneButton.tsx │ │ ├── ImportFolderButton.tsx │ │ ├── Markdown.module.scss │ │ ├── Markdown.spec.ts │ │ ├── Markdown.tsx │ │ ├── Messages.client.tsx │ │ ├── ModelSelector.tsx │ │ ├── ScreenshotStateManager.tsx │ │ ├── SendButton.client.tsx │ │ ├── SpeechRecognition.tsx │ │ ├── StarterTemplates.tsx │ │ ├── UserMessage.tsx │ │ ├── WelcomeIntro.tsx │ │ └── chatExportAndImport │ │ │ ├── ExportChatButton.tsx │ │ │ └── ImportButtons.tsx │ ├── editor │ │ └── codemirror │ │ │ ├── BinaryContent.tsx │ │ │ ├── CodeMirrorEditor.tsx │ │ │ ├── cm-theme.ts │ │ │ ├── indent.ts │ │ │ └── languages.ts │ ├── git │ │ └── GitUrlImport.client.tsx │ ├── header │ │ ├── Header.tsx │ │ └── HeaderActionButtons.client.tsx │ ├── settings │ │ ├── Settings.module.scss │ │ ├── SettingsWindow.tsx │ │ ├── connections │ │ │ └── ConnectionsTab.tsx │ │ ├── data │ │ │ └── DataTab.tsx │ │ ├── debug │ │ │ └── DebugTab.tsx │ │ ├── event-logs │ │ │ └── EventLogsTab.tsx │ │ ├── features │ │ │ └── FeaturesTab.tsx │ │ └── providers │ │ │ └── ProvidersTab.tsx │ ├── sidebar │ │ ├── HistoryItem.tsx │ │ ├── Menu.client.tsx │ │ └── date-binning.ts │ ├── ui │ │ ├── BackgroundRays │ │ │ ├── index.tsx │ │ │ └── styles.module.scss │ │ ├── Dialog.tsx │ │ ├── IconButton.tsx │ │ ├── LoadingDots.tsx │ │ ├── LoadingOverlay.tsx │ │ ├── PanelHeader.tsx │ │ ├── PanelHeaderButton.tsx │ │ ├── SettingsButton.tsx │ │ ├── Slider.tsx │ │ ├── Switch.tsx │ │ ├── ThemeSwitch.tsx │ │ └── Tooltip.tsx │ └── workbench │ │ ├── EditorPanel.tsx │ │ ├── FileBreadcrumb.tsx │ │ ├── FileTree.tsx │ │ ├── PortDropdown.tsx │ │ ├── Preview.tsx │ │ ├── ScreenshotSelector.tsx │ │ ├── Workbench.client.tsx │ │ └── terminal │ │ ├── Terminal.tsx │ │ ├── TerminalTabs.tsx │ │ └── theme.ts ├── entry.client.tsx ├── entry.server.tsx ├── lib │ ├── .server │ │ └── llm │ │ │ ├── constants.ts │ │ │ ├── stream-text.ts │ │ │ └── switchable-stream.ts │ ├── common │ │ ├── prompt-library.ts │ │ └── prompts │ │ │ ├── optimized.ts │ │ │ └── prompts.ts │ ├── crypto.ts │ ├── fetch.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useEditChatDescription.ts │ │ ├── useGit.ts │ │ ├── useMessageParser.ts │ │ ├── usePromptEnhancer.ts │ │ ├── useSearchFilter.ts │ │ ├── useSettings.tsx │ │ ├── useShortcuts.ts │ │ ├── useSnapScroll.ts │ │ └── useViewport.ts │ ├── modules │ │ └── llm │ │ │ ├── base-provider.ts │ │ │ ├── manager.ts │ │ │ ├── providers │ │ │ ├── anthropic.ts │ │ │ ├── cohere.ts │ │ │ ├── deepseek.ts │ │ │ ├── google.ts │ │ │ ├── groq.ts │ │ │ ├── huggingface.ts │ │ │ ├── lmstudio.ts │ │ │ ├── mistral.ts │ │ │ ├── ollama.ts │ │ │ ├── open-router.ts │ │ │ ├── openai-like.ts │ │ │ ├── openai.ts │ │ │ ├── perplexity.ts │ │ │ ├── together.ts │ │ │ └── xai.ts │ │ │ ├── registry.ts │ │ │ └── types.ts │ ├── persistence │ │ ├── ChatDescription.client.tsx │ │ ├── db.ts │ │ ├── index.ts │ │ └── useChatHistory.ts │ ├── runtime │ │ ├── __snapshots__ │ │ │ └── message-parser.spec.ts.snap │ │ ├── action-runner.ts │ │ ├── message-parser.spec.ts │ │ └── message-parser.ts │ ├── stores │ │ ├── chat.ts │ │ ├── editor.ts │ │ ├── files.ts │ │ ├── logs.ts │ │ ├── previews.ts │ │ ├── settings.ts │ │ ├── terminal.ts │ │ ├── theme.ts │ │ └── workbench.ts │ └── webcontainer │ │ ├── auth.client.ts │ │ └── index.ts ├── root.tsx ├── routes │ ├── _index.tsx │ ├── api.chat.ts │ ├── api.enhancer.ts │ ├── api.models.ts │ ├── chat.$id.tsx │ └── git.tsx ├── styles │ ├── animations.scss │ ├── components │ │ ├── code.scss │ │ ├── editor.scss │ │ ├── resize-handle.scss │ │ ├── terminal.scss │ │ └── toast.scss │ ├── index.scss │ ├── variables.scss │ └── z-index.scss ├── types │ ├── actions.ts │ ├── artifact.ts │ ├── global.d.ts │ ├── model.ts │ ├── template.ts │ ├── terminal.ts │ └── theme.ts ├── utils │ ├── buffer.ts │ ├── classNames.ts │ ├── constants.ts │ ├── debounce.ts │ ├── diff.spec.ts │ ├── diff.ts │ ├── easings.ts │ ├── fileUtils.ts │ ├── folderImport.ts │ ├── logger.ts │ ├── markdown.ts │ ├── mobile.ts │ ├── projectCommands.ts │ ├── promises.ts │ ├── react.ts │ ├── sampler.ts │ ├── shell.ts │ ├── stacktrace.ts │ ├── stripIndent.ts │ ├── terminal.ts │ ├── types.ts │ └── unreachable.ts └── vite-env.d.ts ├── bindings.sh ├── changelog.md ├── docker-compose.yaml ├── docs ├── .gitignore ├── README.md ├── docs │ ├── CONTRIBUTING.md │ ├── FAQ.md │ └── index.md ├── images │ ├── api-key-ui-section.png │ ├── bolt-settings-button.png │ └── provider-base-url.png ├── mkdocs.yml ├── poetry.lock └── pyproject.toml ├── eslint.config.mjs ├── functions └── [[path]].ts ├── icons ├── angular.svg ├── astro.svg ├── chat.svg ├── logo-text.svg ├── logo.svg ├── nativescript.svg ├── nextjs.svg ├── nuxt.svg ├── qwik.svg ├── react.svg ├── remix.svg ├── remotion.svg ├── slidev.svg ├── stars.svg ├── svelte.svg ├── typescript.svg ├── vite.svg └── vue.svg ├── load-context.ts ├── package.json ├── pnpm-lock.yaml ├── pre-start.cjs ├── public ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico ├── favicon.svg ├── icons │ ├── Anthropic.svg │ ├── Cohere.svg │ ├── Deepseek.svg │ ├── Default.svg │ ├── Google.svg │ ├── Groq.svg │ ├── HuggingFace.svg │ ├── LMStudio.svg │ ├── Mistral.svg │ ├── Ollama.svg │ ├── OpenAI.svg │ ├── OpenAILike.svg │ ├── OpenRouter.svg │ ├── Perplexity.svg │ ├── Together.svg │ └── xAI.svg ├── logo-dark-styled.png ├── logo-dark.png ├── logo-light-styled.png ├── logo-light.png ├── logo.svg └── social_preview_index.jpg ├── tsconfig.json ├── types └── istextorbinary.d.ts ├── uno.config.ts ├── vite.config.ts ├── worker-configuration.d.ts └── wrangler.toml /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore Git and GitHub files 2 | .git 3 | .github/ 4 | 5 | # Ignore Husky configuration files 6 | .husky/ 7 | 8 | # Ignore documentation and metadata files 9 | CONTRIBUTING.md 10 | LICENSE 11 | README.md 12 | 13 | # Ignore environment examples and sensitive info 14 | .env 15 | *.local 16 | *.example 17 | 18 | # Ignore node modules, logs and cache files 19 | **/*.log 20 | **/node_modules 21 | **/dist 22 | **/build 23 | **/.cache 24 | logs 25 | dist-ssr 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 120 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "Bug report" 2 | description: Create a report to help us improve 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for reporting an issue :pray:. 8 | 9 | This issue tracker is for bugs and issues found with [Bolt.new](https://bolt.new). 10 | If you experience issues related to WebContainer, please file an issue in our [WebContainer repo](https://github.com/stackblitz/webcontainer-core), or file an issue in our [StackBlitz core repo](https://github.com/stackblitz/core) for issues with StackBlitz. 11 | 12 | The more information you fill in, the better we can help you. 13 | - type: textarea 14 | id: description 15 | attributes: 16 | label: Describe the bug 17 | description: Provide a clear and concise description of what you're running into. 18 | validations: 19 | required: true 20 | - type: input 21 | id: link 22 | attributes: 23 | label: Link to the Bolt URL that caused the error 24 | description: Please do not delete it after reporting! 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: steps 29 | attributes: 30 | label: Steps to reproduce 31 | description: Describe the steps we have to take to reproduce the behavior. 32 | placeholder: | 33 | 1. Go to '...' 34 | 2. Click on '....' 35 | 3. Scroll down to '....' 36 | 4. See error 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: expected 41 | attributes: 42 | label: Expected behavior 43 | description: Provide a clear and concise description of what you expected to happen. 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: screenshots 48 | attributes: 49 | label: Screen Recording / Screenshot 50 | description: If applicable, **please include a screen recording** (preferably) or screenshot showcasing the issue. This will assist us in resolving your issue quickly. 51 | - type: textarea 52 | id: platform 53 | attributes: 54 | label: Platform 55 | value: | 56 | - OS: [e.g. macOS, Windows, Linux] 57 | - Browser: [e.g. Chrome, Safari, Firefox] 58 | - Version: [e.g. 91.1] 59 | - type: input 60 | id: provider 61 | attributes: 62 | label: Provider Used 63 | description: Tell us the provider you are using. 64 | - type: input 65 | id: model 66 | attributes: 67 | label: Model Used 68 | description: Tell us the model you are using. 69 | - type: textarea 70 | id: additional 71 | attributes: 72 | label: Additional context 73 | description: Add any other context about the problem here. 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 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. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe:** 10 | 11 | 12 | 13 | **Describe the solution you'd like:** 14 | 15 | 16 | 17 | **Describe alternatives you've considered:** 18 | 19 | 20 | 21 | **Additional context:** 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/actions/setup-and-build/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup and Build 2 | description: Generic setup action 3 | inputs: 4 | pnpm-version: 5 | required: false 6 | type: string 7 | default: '9.4.0' 8 | node-version: 9 | required: false 10 | type: string 11 | default: '20.15.1' 12 | 13 | runs: 14 | using: composite 15 | 16 | steps: 17 | - uses: pnpm/action-setup@v4 18 | with: 19 | version: ${{ inputs.pnpm-version }} 20 | run_install: false 21 | 22 | - name: Set Node.js version to ${{ inputs.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ inputs.node-version }} 26 | cache: pnpm 27 | 28 | - name: Install dependencies and build project 29 | shell: bash 30 | run: | 31 | pnpm install 32 | pnpm run build 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup and Build 18 | uses: ./.github/actions/setup-and-build 19 | 20 | - name: Run type check 21 | run: pnpm run typecheck 22 | 23 | # - name: Run ESLint 24 | # run: pnpm run lint 25 | 26 | - name: Run tests 27 | run: pnpm run test 28 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Docs CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'docs/**' # This will only trigger the workflow when files in docs directory change 9 | permissions: 10 | contents: write 11 | jobs: 12 | build_docs: 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: ./docs 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Configure Git Credentials 20 | run: | 21 | git config user.name github-actions[bot] 22 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 23 | - uses: actions/setup-python@v5 24 | with: 25 | python-version: 3.x 26 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 27 | - uses: actions/cache@v4 28 | with: 29 | key: mkdocs-material-${{ env.cache_id }} 30 | path: .cache 31 | restore-keys: | 32 | mkdocs-material- 33 | 34 | - run: pip install mkdocs-material 35 | - run: mkdocs gh-deploy --force -------------------------------------------------------------------------------- /.github/workflows/pr-release-validation.yaml: -------------------------------------------------------------------------------- 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/semantic-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Semantic Pull Request 2 | on: 3 | pull_request_target: 4 | types: [opened, reopened, edited, synchronize] 5 | permissions: 6 | pull-requests: read 7 | jobs: 8 | main: 9 | name: Validate PR Title 10 | runs-on: ubuntu-latest 11 | steps: 12 | # https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.3 13 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | with: 17 | subjectPattern: ^(?![A-Z]).+$ 18 | subjectPatternError: | 19 | The subject "{subject}" found in the pull request title "{title}" 20 | didn't match the configured pattern. Please ensure that the subject 21 | doesn't start with an uppercase character. 22 | types: | 23 | fix 24 | feat 25 | chore 26 | build 27 | ci 28 | perf 29 | docs 30 | refactor 31 | revert 32 | test -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark Stale Issues and Pull Requests 2 | 3 | on: 4 | schedule: 5 | - cron: '0 2 * * *' # Runs daily at 2:00 AM UTC 6 | workflow_dispatch: # Allows manual triggering of the workflow 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Mark stale issues and pull requests 14 | uses: actions/stale@v8 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | stale-issue-message: "This issue has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days." 18 | stale-pr-message: "This pull request has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days." 19 | days-before-stale: 10 # Number of days before marking an issue or PR as stale 20 | days-before-close: 4 # Number of days after being marked stale before closing 21 | stale-issue-label: "stale" # Label to apply to stale issues 22 | stale-pr-label: "stale" # Label to apply to stale pull requests 23 | exempt-issue-labels: "pinned,important" # Issues with these labels won't be marked stale 24 | exempt-pr-labels: "pinned,important" # PRs with these labels won't be marked stale 25 | operations-per-run: 75 # Limits the number of actions per run to avoid API rate limits 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | lerna-debug.log* 8 | 9 | node_modules 10 | dist 11 | dist-ssr 12 | *.local 13 | 14 | .history 15 | .vscode/* 16 | .vscode/launch.json 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | /.history 27 | /.cache 28 | /build 29 | .env.local 30 | .env 31 | .dev.vars 32 | *.vars 33 | .wrangler 34 | _worker.bundle 35 | 36 | Modelfile 37 | modelfiles 38 | 39 | # docs ignore 40 | site 41 | 42 | # commit file ignore 43 | app/commit.json -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "🔍 Running pre-commit hook to check the code looks good... 🔍" 4 | 5 | # Load NVM if available (useful for managing Node.js versions) 6 | export NVM_DIR="$HOME/.nvm" 7 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 8 | 9 | # Ensure `pnpm` is available 10 | echo "Checking if pnpm is available..." 11 | if ! command -v pnpm >/dev/null 2>&1; then 12 | echo "❌ pnpm not found! Please ensure pnpm is installed and available in PATH." 13 | exit 1 14 | fi 15 | 16 | # Run typecheck 17 | echo "Running typecheck..." 18 | if ! pnpm typecheck; then 19 | echo "❌ Type checking failed! Please review TypeScript types." 20 | echo "Once you're done, don't forget to add your changes to the commit! 🚀" 21 | exit 1 22 | fi 23 | 24 | # Run lint 25 | echo "Running lint..." 26 | if ! pnpm lint; then 27 | echo "❌ Linting failed! Run 'pnpm lint:fix' to fix the easy issues." 28 | echo "Once you're done, don't forget to add your beautification to the commit! 🤩" 29 | exit 1 30 | fi 31 | 32 | echo "👍 All checks passed! Committing changes..." 33 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | .astro 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.15.1 2 | pnpm 9.4.0 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE=node:20.18.0 2 | FROM ${BASE} AS base 3 | 4 | WORKDIR /app 5 | 6 | # Install dependencies (this step is cached as long as the dependencies don't change) 7 | COPY package.json pnpm-lock.yaml ./ 8 | 9 | RUN corepack enable pnpm && pnpm install 10 | 11 | # Copy the rest of your app's source code 12 | COPY . . 13 | 14 | # Expose the port the app runs on 15 | EXPOSE 5173 16 | 17 | # Production image 18 | FROM base AS bolt-ai-production 19 | 20 | # Define environment variables with default values or let them be overridden 21 | ARG GROQ_API_KEY 22 | ARG HuggingFace_API_KEY 23 | ARG OPENAI_API_KEY 24 | ARG ANTHROPIC_API_KEY 25 | ARG OPEN_ROUTER_API_KEY 26 | ARG GOOGLE_GENERATIVE_AI_API_KEY 27 | ARG OLLAMA_API_BASE_URL 28 | ARG TOGETHER_API_KEY 29 | ARG TOGETHER_API_BASE_URL 30 | ARG VITE_LOG_LEVEL=debug 31 | ARG DEFAULT_NUM_CTX 32 | 33 | ENV WRANGLER_SEND_METRICS=false \ 34 | GROQ_API_KEY=${GROQ_API_KEY} \ 35 | HuggingFace_KEY=${HuggingFace_API_KEY} \ 36 | OPENAI_API_KEY=${OPENAI_API_KEY} \ 37 | ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \ 38 | OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \ 39 | GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \ 40 | OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \ 41 | TOGETHER_API_KEY=${TOGETHER_API_KEY} \ 42 | TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \ 43 | VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \ 44 | DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} 45 | 46 | # Pre-configure wrangler to disable metrics 47 | RUN mkdir -p /root/.config/.wrangler && \ 48 | echo '{"enabled":false}' > /root/.config/.wrangler/metrics.json 49 | 50 | RUN npm run build 51 | 52 | CMD [ "pnpm", "run", "dockerstart"] 53 | 54 | # Development image 55 | FROM base AS bolt-ai-development 56 | 57 | # Define the same environment variables for development 58 | ARG GROQ_API_KEY 59 | ARG HuggingFace 60 | ARG OPENAI_API_KEY 61 | ARG ANTHROPIC_API_KEY 62 | ARG OPEN_ROUTER_API_KEY 63 | ARG GOOGLE_GENERATIVE_AI_API_KEY 64 | ARG OLLAMA_API_BASE_URL 65 | ARG TOGETHER_API_KEY 66 | ARG TOGETHER_API_BASE_URL 67 | ARG VITE_LOG_LEVEL=debug 68 | ARG DEFAULT_NUM_CTX 69 | 70 | ENV GROQ_API_KEY=${GROQ_API_KEY} \ 71 | HuggingFace_API_KEY=${HuggingFace_API_KEY} \ 72 | OPENAI_API_KEY=${OPENAI_API_KEY} \ 73 | ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \ 74 | OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \ 75 | GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \ 76 | OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \ 77 | TOGETHER_API_KEY=${TOGETHER_API_KEY} \ 78 | TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \ 79 | VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \ 80 | DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} 81 | 82 | RUN mkdir -p ${WORKDIR}/run 83 | CMD pnpm run dev --host 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 StackBlitz, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a fork for my hosted version at 2 | https://bolt.myaibuilt.app/ 3 | With small changes I need for hosting it 4 | 5 | Main Repository is Bolt.diy here https://github.com/stackblitz-labs/bolt.diy as I joined efforts to contribute and work together with others there 6 | 7 | ![image](https://github.com/user-attachments/assets/fa3517a7-0f7f-460e-a19b-ef7d4617201f) 8 | -------------------------------------------------------------------------------- /app/commit.json: -------------------------------------------------------------------------------- 1 | { "commit": "32554202002999a7208e64863d62d5540563bbb7" } 2 | -------------------------------------------------------------------------------- /app/components/chat/APIKeyManager.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { IconButton } from '~/components/ui/IconButton'; 3 | import type { ProviderInfo } from '~/types/model'; 4 | 5 | interface APIKeyManagerProps { 6 | provider: ProviderInfo; 7 | apiKey: string; 8 | setApiKey: (key: string) => void; 9 | getApiKeyLink?: string; 10 | labelForGetApiKey?: string; 11 | } 12 | 13 | // eslint-disable-next-line @typescript-eslint/naming-convention 14 | export const APIKeyManager: React.FC = ({ provider, apiKey, setApiKey }) => { 15 | const [isEditing, setIsEditing] = useState(false); 16 | const [tempKey, setTempKey] = useState(apiKey); 17 | 18 | const handleSave = () => { 19 | setApiKey(tempKey); 20 | setIsEditing(false); 21 | }; 22 | 23 | return ( 24 |
25 |
26 | {provider?.name} API Key: 27 | {!isEditing && ( 28 |
29 | 30 | {apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'} 31 | 32 | setIsEditing(true)} title="Edit API Key"> 33 |
34 | 35 |
36 | )} 37 |
38 | 39 | {isEditing ? ( 40 |
41 | setTempKey(e.target.value)} 46 | className="flex-1 px-2 py-1 text-xs lg:text-sm rounded 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" 47 | /> 48 | 49 |
50 | 51 | setIsEditing(false)} title="Cancel"> 52 |
53 | 54 |
55 | ) : ( 56 | <> 57 | {provider?.getApiKeyLink && ( 58 | window.open(provider?.getApiKeyLink)} title="Edit API Key"> 59 | {provider?.labelForGetApiKey || 'Get API Key'} 60 |
61 | 62 | )} 63 | 64 | )} 65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /app/components/chat/AssistantMessage.tsx: -------------------------------------------------------------------------------- 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 |
23 | {usage && ( 24 |
25 | Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) 26 |
27 | )} 28 | {content} 29 |
30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /app/components/chat/BaseChat.module.scss: -------------------------------------------------------------------------------- 1 | .BaseChat { 2 | &[data-chat-visible='false'] { 3 | --workbench-inner-width: 100%; 4 | --workbench-left: 0; 5 | 6 | .Chat { 7 | --at-apply: bolt-ease-cubic-bezier; 8 | transition-property: transform, opacity; 9 | transition-duration: 0.3s; 10 | will-change: transform, opacity; 11 | transform: translateX(-50%); 12 | opacity: 0; 13 | } 14 | } 15 | } 16 | 17 | .Chat { 18 | opacity: 1; 19 | } 20 | 21 | .PromptEffectContainer { 22 | --prompt-container-offset: 50px; 23 | --prompt-line-stroke-width: 1px; 24 | position: absolute; 25 | pointer-events: none; 26 | inset: calc(var(--prompt-container-offset) / -2); 27 | width: calc(100% + var(--prompt-container-offset)); 28 | height: calc(100% + var(--prompt-container-offset)); 29 | } 30 | 31 | .PromptEffectLine { 32 | width: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width)); 33 | height: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width)); 34 | x: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2); 35 | y: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2); 36 | rx: calc(8px - var(--prompt-line-stroke-width)); 37 | fill: transparent; 38 | stroke-width: var(--prompt-line-stroke-width); 39 | stroke: url(#line-gradient); 40 | stroke-dasharray: 35px 65px; 41 | stroke-dashoffset: 10; 42 | } 43 | 44 | .PromptShine { 45 | fill: url(#shine-gradient); 46 | mix-blend-mode: overlay; 47 | } 48 | -------------------------------------------------------------------------------- /app/components/chat/CodeBlock.module.scss: -------------------------------------------------------------------------------- 1 | .CopyButtonContainer { 2 | button:before { 3 | content: 'Copied'; 4 | font-size: 12px; 5 | position: absolute; 6 | left: -53px; 7 | padding: 2px 6px; 8 | height: 30px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/components/chat/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect, useState } from 'react'; 2 | import { bundledLanguages, codeToHtml, isSpecialLang, type BundledLanguage, type SpecialLanguage } from 'shiki'; 3 | import { classNames } from '~/utils/classNames'; 4 | import { createScopedLogger } from '~/utils/logger'; 5 | 6 | import styles from './CodeBlock.module.scss'; 7 | 8 | const logger = createScopedLogger('CodeBlock'); 9 | 10 | interface CodeBlockProps { 11 | className?: string; 12 | code: string; 13 | language?: BundledLanguage | SpecialLanguage; 14 | theme?: 'light-plus' | 'dark-plus'; 15 | disableCopy?: boolean; 16 | } 17 | 18 | export const CodeBlock = memo( 19 | ({ className, code, language = 'plaintext', theme = 'dark-plus', disableCopy = false }: CodeBlockProps) => { 20 | const [html, setHTML] = useState(undefined); 21 | const [copied, setCopied] = useState(false); 22 | 23 | const copyToClipboard = () => { 24 | if (copied) { 25 | return; 26 | } 27 | 28 | navigator.clipboard.writeText(code); 29 | 30 | setCopied(true); 31 | 32 | setTimeout(() => { 33 | setCopied(false); 34 | }, 2000); 35 | }; 36 | 37 | useEffect(() => { 38 | if (language && !isSpecialLang(language) && !(language in bundledLanguages)) { 39 | logger.warn(`Unsupported language '${language}'`); 40 | } 41 | 42 | logger.trace(`Language = ${language}`); 43 | 44 | const processCode = async () => { 45 | setHTML(await codeToHtml(code, { lang: language, theme })); 46 | }; 47 | 48 | processCode(); 49 | }, [code]); 50 | 51 | return ( 52 |
53 |
62 | {!disableCopy && ( 63 | 76 | )} 77 |
78 |
79 |
80 | ); 81 | }, 82 | ); 83 | -------------------------------------------------------------------------------- /app/components/chat/ExamplePrompts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const EXAMPLE_PROMPTS = [ 4 | { text: 'Build a todo app in React using Tailwind' }, 5 | { text: 'Build a simple blog using Astro' }, 6 | { text: 'Create a cookie consent form using Material UI' }, 7 | { text: 'Make a space invaders game' }, 8 | { text: 'Make a Tic Tac Toe game in html, css and js only' }, 9 | ]; 10 | 11 | export function ExamplePrompts(sendMessage?: { (event: React.UIEvent, messageInput?: string): void | undefined }) { 12 | return ( 13 |
14 |
20 | {EXAMPLE_PROMPTS.map((examplePrompt, index: number) => { 21 | return ( 22 | 31 | ); 32 | })} 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/components/chat/FilePreview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface FilePreviewProps { 4 | files: File[]; 5 | imageDataList: string[]; 6 | onRemove: (index: number) => void; 7 | } 8 | 9 | const FilePreview: React.FC = ({ files, imageDataList, onRemove }) => { 10 | if (!files || files.length === 0) { 11 | return null; 12 | } 13 | 14 | return ( 15 |
16 | {files.map((file, index) => ( 17 |
18 | {imageDataList[index] && ( 19 |
20 | {file.name} 21 | 27 |
28 | )} 29 |
30 | ))} 31 |
32 | ); 33 | }; 34 | 35 | export default FilePreview; 36 | -------------------------------------------------------------------------------- /app/components/chat/Markdown.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { stripCodeFenceFromArtifact } from './Markdown'; 3 | 4 | describe('stripCodeFenceFromArtifact', () => { 5 | it('should remove code fences around artifact element', () => { 6 | const input = "```xml\n
\n```"; 7 | const expected = "\n
\n"; 8 | expect(stripCodeFenceFromArtifact(input)).toBe(expected); 9 | }); 10 | 11 | it('should handle code fence with language specification', () => { 12 | const input = "```typescript\n
\n```"; 13 | const expected = "\n
\n"; 14 | expect(stripCodeFenceFromArtifact(input)).toBe(expected); 15 | }); 16 | 17 | it('should not modify content without artifacts', () => { 18 | const input = '```\nregular code block\n```'; 19 | expect(stripCodeFenceFromArtifact(input)).toBe(input); 20 | }); 21 | 22 | it('should handle empty input', () => { 23 | expect(stripCodeFenceFromArtifact('')).toBe(''); 24 | }); 25 | 26 | it('should handle artifact without code fences', () => { 27 | const input = "
"; 28 | expect(stripCodeFenceFromArtifact(input)).toBe(input); 29 | }); 30 | 31 | it('should handle multiple artifacts but only remove fences around them', () => { 32 | const input = [ 33 | 'Some text', 34 | '```typescript', 35 | "
", 36 | '```', 37 | '```', 38 | 'regular code', 39 | '```', 40 | ].join('\n'); 41 | 42 | const expected = ['Some text', '', "
", '', '```', 'regular code', '```'].join( 43 | '\n', 44 | ); 45 | 46 | expect(stripCodeFenceFromArtifact(input)).toBe(expected); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /app/components/chat/ScreenshotStateManager.tsx: -------------------------------------------------------------------------------- 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 | }; 34 | -------------------------------------------------------------------------------- /app/components/chat/SendButton.client.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, cubicBezier, motion } from 'framer-motion'; 2 | 3 | interface SendButtonProps { 4 | show: boolean; 5 | isStreaming?: boolean; 6 | disabled?: boolean; 7 | onClick?: (event: React.MouseEvent) => void; 8 | onImagesSelected?: (images: File[]) => void; 9 | } 10 | 11 | const customEasingFn = cubicBezier(0.4, 0, 0.2, 1); 12 | 13 | export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonProps) => { 14 | return ( 15 | 16 | {show ? ( 17 | { 25 | event.preventDefault(); 26 | 27 | if (!disabled) { 28 | onClick?.(event); 29 | } 30 | }} 31 | > 32 |
33 | {!isStreaming ?
:
} 34 |
35 |
36 | ) : null} 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /app/components/chat/SpeechRecognition.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton } from '~/components/ui/IconButton'; 2 | import { classNames } from '~/utils/classNames'; 3 | import React from 'react'; 4 | 5 | export const SpeechRecognitionButton = ({ 6 | isListening, 7 | onStart, 8 | onStop, 9 | disabled, 10 | }: { 11 | isListening: boolean; 12 | onStart: () => void; 13 | onStop: () => void; 14 | disabled: boolean; 15 | }) => { 16 | return ( 17 | 25 | {isListening ?
:
} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /app/components/chat/StarterTemplates.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Template } from '~/types/template'; 3 | import { STARTER_TEMPLATES } from '~/utils/constants'; 4 | 5 | interface FrameworkLinkProps { 6 | template: Template; 7 | } 8 | 9 | const FrameworkLink: React.FC = ({ template }) => ( 10 | 16 |
19 | 20 | ); 21 | 22 | const StarterTemplates: React.FC = () => { 23 | return ( 24 |
25 | or start a blank app with your favorite stack 26 |
27 |
28 | {STARTER_TEMPLATES.map((template) => ( 29 | 30 | ))} 31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default StarterTemplates; 38 | -------------------------------------------------------------------------------- /app/components/chat/UserMessage.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @ts-nocheck 3 | * Preventing TS checks with files presented in the video for a better presentation. 4 | */ 5 | import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants'; 6 | import { Markdown } from './Markdown'; 7 | 8 | interface UserMessageProps { 9 | content: string | Array<{ type: string; text?: string; image?: string }>; 10 | } 11 | 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 |
20 |
21 | {textContent && {textContent}} 22 | {images.map((item, index) => ( 23 | {`Image 30 | ))} 31 |
32 |
33 | ); 34 | } 35 | 36 | const textContent = stripMetadata(content); 37 | 38 | return ( 39 |
40 | {textContent} 41 |
42 | ); 43 | } 44 | 45 | function stripMetadata(content: string) { 46 | return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''); 47 | } 48 | -------------------------------------------------------------------------------- /app/components/chat/chatExportAndImport/ExportChatButton.tsx: -------------------------------------------------------------------------------- 1 | import WithTooltip from '~/components/ui/Tooltip'; 2 | import { IconButton } from '~/components/ui/IconButton'; 3 | import React from 'react'; 4 | 5 | export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => { 6 | return ( 7 | 8 | exportChat?.()}> 9 |
10 |
11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /app/components/editor/codemirror/BinaryContent.tsx: -------------------------------------------------------------------------------- 1 | export function BinaryContent() { 2 | return ( 3 |
4 | File format cannot be displayed. 5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/components/editor/codemirror/indent.ts: -------------------------------------------------------------------------------- 1 | import { indentLess } from '@codemirror/commands'; 2 | import { indentUnit } from '@codemirror/language'; 3 | import { EditorSelection, EditorState, Line, type ChangeSpec } from '@codemirror/state'; 4 | import { EditorView, type KeyBinding } from '@codemirror/view'; 5 | 6 | export const indentKeyBinding: KeyBinding = { 7 | key: 'Tab', 8 | run: indentMore, 9 | shift: indentLess, 10 | }; 11 | 12 | function indentMore({ state, dispatch }: EditorView) { 13 | if (state.readOnly) { 14 | return false; 15 | } 16 | 17 | dispatch( 18 | state.update( 19 | changeBySelectedLine(state, (from, to, changes) => { 20 | changes.push({ from, to, insert: state.facet(indentUnit) }); 21 | }), 22 | { userEvent: 'input.indent' }, 23 | ), 24 | ); 25 | 26 | return true; 27 | } 28 | 29 | function changeBySelectedLine( 30 | state: EditorState, 31 | cb: (from: number, to: number | undefined, changes: ChangeSpec[], line: Line) => void, 32 | ) { 33 | return state.changeByRange((range) => { 34 | const changes: ChangeSpec[] = []; 35 | 36 | const line = state.doc.lineAt(range.from); 37 | 38 | // just insert single indent unit at the current cursor position 39 | if (range.from === range.to) { 40 | cb(range.from, undefined, changes, line); 41 | } 42 | // handle the case when multiple characters are selected in a single line 43 | else if (range.from < range.to && range.to <= line.to) { 44 | cb(range.from, range.to, changes, line); 45 | } else { 46 | let atLine = -1; 47 | 48 | // handle the case when selection spans multiple lines 49 | for (let pos = range.from; pos <= range.to; ) { 50 | const line = state.doc.lineAt(pos); 51 | 52 | if (line.number > atLine && (range.empty || range.to > line.from)) { 53 | cb(line.from, undefined, changes, line); 54 | atLine = line.number; 55 | } 56 | 57 | pos = line.to + 1; 58 | } 59 | } 60 | 61 | const changeSet = state.changes(changes); 62 | 63 | return { 64 | changes, 65 | range: EditorSelection.range(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1)), 66 | }; 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /app/components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from '@nanostores/react'; 2 | import { ClientOnly } from 'remix-utils/client-only'; 3 | import { chatStore } from '~/lib/stores/chat'; 4 | import { classNames } from '~/utils/classNames'; 5 | import { HeaderActionButtons } from './HeaderActionButtons.client'; 6 | import { ChatDescription } from '~/lib/persistence/ChatDescription.client'; 7 | 8 | export function Header() { 9 | const chat = useStore(chatStore); 10 | 11 | return ( 12 |
18 |
19 | 26 | {chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started. 27 | <> 28 | 29 | {() => } 30 | 31 | 32 | {() => ( 33 |
34 | 35 |
36 | )} 37 |
38 | 39 | )} 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/components/header/HeaderActionButtons.client.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from '@nanostores/react'; 2 | import useViewport from '~/lib/hooks'; 3 | import { chatStore } from '~/lib/stores/chat'; 4 | import { workbenchStore } from '~/lib/stores/workbench'; 5 | import { classNames } from '~/utils/classNames'; 6 | 7 | interface HeaderActionButtonsProps {} 8 | 9 | export function HeaderActionButtons({}: HeaderActionButtonsProps) { 10 | const showWorkbench = useStore(workbenchStore.showWorkbench); 11 | const { showChat } = useStore(chatStore); 12 | 13 | const isSmallViewport = useViewport(1024); 14 | 15 | const canHideChat = showWorkbench || !showChat; 16 | 17 | return ( 18 |
19 |
20 | 31 |
32 | 44 |
45 |
46 | ); 47 | } 48 | 49 | interface ButtonProps { 50 | active?: boolean; 51 | disabled?: boolean; 52 | children?: any; 53 | onClick?: VoidFunction; 54 | } 55 | 56 | function Button({ active = false, disabled = false, children, onClick }: ButtonProps) { 57 | return ( 58 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /app/components/settings/Settings.module.scss: -------------------------------------------------------------------------------- 1 | .settings-tabs { 2 | button { 3 | width: 100%; 4 | display: flex; 5 | align-items: center; 6 | gap: 0.5rem; 7 | padding: 0.75rem 1rem; 8 | border-radius: 0.5rem; 9 | text-align: left; 10 | font-size: 0.875rem; 11 | transition: all 0.2s; 12 | margin-bottom: 0.5rem; 13 | 14 | &.active { 15 | background: var(--bolt-elements-button-primary-background); 16 | color: var(--bolt-elements-textPrimary); 17 | } 18 | 19 | &:not(.active) { 20 | background: var(--bolt-elements-bg-depth-3); 21 | color: var(--bolt-elements-textPrimary); 22 | 23 | &:hover { 24 | background: var(--bolt-elements-button-primary-backgroundHover); 25 | } 26 | } 27 | } 28 | } 29 | 30 | .settings-button { 31 | background-color: var(--bolt-elements-button-primary-background); 32 | color: var(--bolt-elements-textPrimary); 33 | border-radius: 0.5rem; 34 | padding: 0.5rem 1rem; 35 | transition: background-color 0.2s; 36 | 37 | &:hover { 38 | background-color: var(--bolt-elements-button-primary-backgroundHover); 39 | } 40 | } 41 | 42 | .settings-danger-area { 43 | background-color: transparent; 44 | color: var(--bolt-elements-textPrimary); 45 | border-radius: 0.5rem; 46 | padding: 1rem; 47 | margin-bottom: 1rem; 48 | border-style: solid; 49 | border-color: var(--bolt-elements-button-danger-backgroundHover); 50 | border-width: thin; 51 | 52 | button { 53 | background-color: var(--bolt-elements-button-danger-background); 54 | color: var(--bolt-elements-button-danger-text); 55 | border-radius: 0.5rem; 56 | padding: 0.5rem 1rem; 57 | transition: background-color 0.2s; 58 | 59 | &:hover { 60 | background-color: var(--bolt-elements-button-danger-backgroundHover); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/components/sidebar/date-binning.ts: -------------------------------------------------------------------------------- 1 | import { format, isAfter, isThisWeek, isThisYear, isToday, isYesterday, subDays } from 'date-fns'; 2 | import type { ChatHistoryItem } from '~/lib/persistence'; 3 | 4 | type Bin = { category: string; items: ChatHistoryItem[] }; 5 | 6 | export function binDates(_list: ChatHistoryItem[]) { 7 | const list = _list.toSorted((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp)); 8 | 9 | const binLookup: Record = {}; 10 | const bins: Array = []; 11 | 12 | list.forEach((item) => { 13 | const category = dateCategory(new Date(item.timestamp)); 14 | 15 | if (!(category in binLookup)) { 16 | const bin = { 17 | category, 18 | items: [item], 19 | }; 20 | 21 | binLookup[category] = bin; 22 | 23 | bins.push(bin); 24 | } else { 25 | binLookup[category].items.push(item); 26 | } 27 | }); 28 | 29 | return bins; 30 | } 31 | 32 | function dateCategory(date: Date) { 33 | if (isToday(date)) { 34 | return 'Today'; 35 | } 36 | 37 | if (isYesterday(date)) { 38 | return 'Yesterday'; 39 | } 40 | 41 | if (isThisWeek(date)) { 42 | // e.g., "Monday" 43 | return format(date, 'eeee'); 44 | } 45 | 46 | const thirtyDaysAgo = subDays(new Date(), 30); 47 | 48 | if (isAfter(date, thirtyDaysAgo)) { 49 | return 'Last 30 Days'; 50 | } 51 | 52 | if (isThisYear(date)) { 53 | // e.g., "July" 54 | return format(date, 'MMMM'); 55 | } 56 | 57 | // e.g., "July 2023" 58 | return format(date, 'MMMM yyyy'); 59 | } 60 | -------------------------------------------------------------------------------- /app/components/ui/BackgroundRays/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss'; 2 | 3 | const BackgroundRays = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ); 16 | }; 17 | 18 | export default BackgroundRays; 19 | -------------------------------------------------------------------------------- /app/components/ui/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import { memo, forwardRef, type ForwardedRef } from 'react'; 2 | import { classNames } from '~/utils/classNames'; 3 | 4 | type IconSize = 'sm' | 'md' | 'lg' | 'xl' | 'xxl'; 5 | 6 | interface BaseIconButtonProps { 7 | size?: IconSize; 8 | className?: string; 9 | iconClassName?: string; 10 | disabledClassName?: string; 11 | title?: string; 12 | disabled?: boolean; 13 | onClick?: (event: React.MouseEvent) => void; 14 | } 15 | 16 | type IconButtonWithoutChildrenProps = { 17 | icon: string; 18 | children?: undefined; 19 | } & BaseIconButtonProps; 20 | 21 | type IconButtonWithChildrenProps = { 22 | icon?: undefined; 23 | children: string | JSX.Element | JSX.Element[]; 24 | } & BaseIconButtonProps; 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, 44 | ) => { 45 | return ( 46 | 67 | ); 68 | }, 69 | ), 70 | ); 71 | 72 | function getIconSize(size: IconSize) { 73 | if (size === 'sm') { 74 | return 'text-sm'; 75 | } else if (size === 'md') { 76 | return 'text-md'; 77 | } else if (size === 'lg') { 78 | return 'text-lg'; 79 | } else if (size === 'xl') { 80 | return 'text-xl'; 81 | } else { 82 | return 'text-2xl'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/components/ui/LoadingDots.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect, useState } from 'react'; 2 | 3 | interface LoadingDotsProps { 4 | text: string; 5 | } 6 | 7 | export const LoadingDots = memo(({ text }: LoadingDotsProps) => { 8 | const [dotCount, setDotCount] = useState(0); 9 | 10 | useEffect(() => { 11 | const interval = setInterval(() => { 12 | setDotCount((prevDotCount) => (prevDotCount + 1) % 4); 13 | }, 500); 14 | 15 | return () => clearInterval(interval); 16 | }, []); 17 | 18 | return ( 19 |
20 |
21 | {text} 22 | {'.'.repeat(dotCount)} 23 | ... 24 |
25 |
26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /app/components/ui/LoadingOverlay.tsx: -------------------------------------------------------------------------------- 1 | export const LoadingOverlay = ({ message = 'Loading...' }) => { 2 | return ( 3 |
4 | {/* Loading content */} 5 |
6 |
10 |

{message}

11 |
12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /app/components/ui/PanelHeader.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { classNames } from '~/utils/classNames'; 3 | 4 | interface PanelHeaderProps { 5 | className?: string; 6 | children: React.ReactNode; 7 | } 8 | 9 | export const PanelHeader = memo(({ className, children }: PanelHeaderProps) => { 10 | return ( 11 |
17 | {children} 18 |
19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /app/components/ui/PanelHeaderButton.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { classNames } from '~/utils/classNames'; 3 | 4 | interface PanelHeaderButtonProps { 5 | className?: string; 6 | disabledClassName?: string; 7 | disabled?: boolean; 8 | children: string | JSX.Element | Array; 9 | onClick?: (event: React.MouseEvent) => void; 10 | } 11 | 12 | export const PanelHeaderButton = memo( 13 | ({ className, disabledClassName, disabled = false, children, onClick }: PanelHeaderButtonProps) => { 14 | return ( 15 | 34 | ); 35 | }, 36 | ); 37 | -------------------------------------------------------------------------------- /app/components/ui/SettingsButton.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { IconButton } from '~/components/ui/IconButton'; 3 | interface SettingsButtonProps { 4 | onClick: () => void; 5 | } 6 | 7 | export const SettingsButton = memo(({ onClick }: SettingsButtonProps) => { 8 | return ( 9 | 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /app/components/ui/Slider.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { memo } from 'react'; 3 | import { classNames } from '~/utils/classNames'; 4 | import { cubicEasingFn } from '~/utils/easings'; 5 | import { genericMemo } from '~/utils/react'; 6 | 7 | interface SliderOption { 8 | value: T; 9 | text: string; 10 | } 11 | 12 | export interface SliderOptions { 13 | left: SliderOption; 14 | right: SliderOption; 15 | } 16 | 17 | interface SliderProps { 18 | selected: T; 19 | options: SliderOptions; 20 | setSelected?: (selected: T) => void; 21 | } 22 | 23 | export const Slider = genericMemo(({ selected, options, setSelected }: SliderProps) => { 24 | const isLeftSelected = selected === options.left.value; 25 | 26 | return ( 27 |
28 | setSelected?.(options.left.value)}> 29 | {options.left.text} 30 | 31 | setSelected?.(options.right.value)}> 32 | {options.right.text} 33 | 34 |
35 | ); 36 | }); 37 | 38 | interface SliderButtonProps { 39 | selected: boolean; 40 | children: string | JSX.Element | Array; 41 | setSelected: () => void; 42 | } 43 | 44 | const SliderButton = memo(({ selected, children, setSelected }: SliderButtonProps) => { 45 | return ( 46 | 64 | ); 65 | }); 66 | -------------------------------------------------------------------------------- /app/components/ui/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import * as SwitchPrimitive from '@radix-ui/react-switch'; 3 | import { classNames } from '~/utils/classNames'; 4 | 5 | interface SwitchProps { 6 | className?: string; 7 | checked?: boolean; 8 | onCheckedChange?: (event: boolean) => void; 9 | } 10 | 11 | export const Switch = memo(({ className, onCheckedChange, checked }: SwitchProps) => { 12 | return ( 13 | onCheckedChange?.(e)} 24 | > 25 | 35 | 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /app/components/ui/ThemeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from '@nanostores/react'; 2 | import { memo, useEffect, useState } from 'react'; 3 | import { themeStore, toggleTheme } from '~/lib/stores/theme'; 4 | import { IconButton } from './IconButton'; 5 | 6 | interface ThemeSwitchProps { 7 | className?: string; 8 | } 9 | 10 | export const ThemeSwitch = memo(({ className }: ThemeSwitchProps) => { 11 | const theme = useStore(themeStore); 12 | const [domLoaded, setDomLoaded] = useState(false); 13 | 14 | useEffect(() => { 15 | setDomLoaded(true); 16 | }, []); 17 | 18 | return ( 19 | domLoaded && ( 20 | 27 | ) 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /app/components/ui/Tooltip.tsx: -------------------------------------------------------------------------------- 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; 10 | tooltipStyle?: React.CSSProperties; 11 | position?: 'top' | 'bottom' | 'left' | 'right'; 12 | maxWidth?: number; 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, 30 | ) => { 31 | return ( 32 | 33 | {children} 34 | 35 | 63 |
{tooltip}
64 | 72 |
73 |
74 |
75 | ); 76 | }, 77 | ); 78 | 79 | export default WithTooltip; 80 | -------------------------------------------------------------------------------- /app/components/workbench/terminal/Terminal.tsx: -------------------------------------------------------------------------------- 1 | import { FitAddon } from '@xterm/addon-fit'; 2 | import { WebLinksAddon } from '@xterm/addon-web-links'; 3 | import { Terminal as XTerm } from '@xterm/xterm'; 4 | import { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react'; 5 | import type { Theme } from '~/lib/stores/theme'; 6 | import { createScopedLogger } from '~/utils/logger'; 7 | import { getTerminalTheme } from './theme'; 8 | 9 | const logger = createScopedLogger('Terminal'); 10 | 11 | export interface TerminalRef { 12 | reloadStyles: () => void; 13 | } 14 | 15 | export interface TerminalProps { 16 | className?: string; 17 | theme: Theme; 18 | readonly?: boolean; 19 | id: string; 20 | onTerminalReady?: (terminal: XTerm) => void; 21 | onTerminalResize?: (cols: number, rows: number) => void; 22 | } 23 | 24 | export const Terminal = memo( 25 | forwardRef( 26 | ({ className, theme, readonly, id, onTerminalReady, onTerminalResize }, ref) => { 27 | const terminalElementRef = useRef(null); 28 | const terminalRef = useRef(); 29 | 30 | useEffect(() => { 31 | const element = terminalElementRef.current!; 32 | 33 | const fitAddon = new FitAddon(); 34 | const webLinksAddon = new WebLinksAddon(); 35 | 36 | const terminal = new XTerm({ 37 | cursorBlink: true, 38 | convertEol: true, 39 | disableStdin: readonly, 40 | theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}), 41 | fontSize: 12, 42 | fontFamily: 'Menlo, courier-new, courier, monospace', 43 | }); 44 | 45 | terminalRef.current = terminal; 46 | 47 | terminal.loadAddon(fitAddon); 48 | terminal.loadAddon(webLinksAddon); 49 | terminal.open(element); 50 | 51 | const resizeObserver = new ResizeObserver(() => { 52 | fitAddon.fit(); 53 | onTerminalResize?.(terminal.cols, terminal.rows); 54 | }); 55 | 56 | resizeObserver.observe(element); 57 | 58 | logger.debug(`Attach [${id}]`); 59 | 60 | onTerminalReady?.(terminal); 61 | 62 | return () => { 63 | resizeObserver.disconnect(); 64 | terminal.dispose(); 65 | }; 66 | }, []); 67 | 68 | useEffect(() => { 69 | const terminal = terminalRef.current!; 70 | 71 | // we render a transparent cursor in case the terminal is readonly 72 | terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {}); 73 | 74 | terminal.options.disableStdin = readonly; 75 | }, [theme, readonly]); 76 | 77 | useImperativeHandle(ref, () => { 78 | return { 79 | reloadStyles: () => { 80 | const terminal = terminalRef.current!; 81 | terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {}); 82 | }, 83 | }; 84 | }, []); 85 | 86 | return
; 87 | }, 88 | ), 89 | ); 90 | -------------------------------------------------------------------------------- /app/components/workbench/terminal/theme.ts: -------------------------------------------------------------------------------- 1 | import type { ITheme } from '@xterm/xterm'; 2 | 3 | const style = getComputedStyle(document.documentElement); 4 | const cssVar = (token: string) => style.getPropertyValue(token) || undefined; 5 | 6 | export function getTerminalTheme(overrides?: ITheme): ITheme { 7 | return { 8 | cursor: cssVar('--bolt-elements-terminal-cursorColor'), 9 | cursorAccent: cssVar('--bolt-elements-terminal-cursorColorAccent'), 10 | foreground: cssVar('--bolt-elements-terminal-textColor'), 11 | background: cssVar('--bolt-elements-terminal-backgroundColor'), 12 | selectionBackground: cssVar('--bolt-elements-terminal-selection-backgroundColor'), 13 | selectionForeground: cssVar('--bolt-elements-terminal-selection-textColor'), 14 | selectionInactiveBackground: cssVar('--bolt-elements-terminal-selection-backgroundColorInactive'), 15 | 16 | // ansi escape code colors 17 | black: cssVar('--bolt-elements-terminal-color-black'), 18 | red: cssVar('--bolt-elements-terminal-color-red'), 19 | green: cssVar('--bolt-elements-terminal-color-green'), 20 | yellow: cssVar('--bolt-elements-terminal-color-yellow'), 21 | blue: cssVar('--bolt-elements-terminal-color-blue'), 22 | magenta: cssVar('--bolt-elements-terminal-color-magenta'), 23 | cyan: cssVar('--bolt-elements-terminal-color-cyan'), 24 | white: cssVar('--bolt-elements-terminal-color-white'), 25 | brightBlack: cssVar('--bolt-elements-terminal-color-brightBlack'), 26 | brightRed: cssVar('--bolt-elements-terminal-color-brightRed'), 27 | brightGreen: cssVar('--bolt-elements-terminal-color-brightGreen'), 28 | brightYellow: cssVar('--bolt-elements-terminal-color-brightYellow'), 29 | brightBlue: cssVar('--bolt-elements-terminal-color-brightBlue'), 30 | brightMagenta: cssVar('--bolt-elements-terminal-color-brightMagenta'), 31 | brightCyan: cssVar('--bolt-elements-terminal-color-brightCyan'), 32 | brightWhite: cssVar('--bolt-elements-terminal-color-brightWhite'), 33 | 34 | ...overrides, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from '@remix-run/react'; 2 | import { startTransition } from 'react'; 3 | import { hydrateRoot } from 'react-dom/client'; 4 | 5 | startTransition(() => { 6 | hydrateRoot(document.getElementById('root')!, ); 7 | }); 8 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare'; 2 | import { RemixServer } from '@remix-run/react'; 3 | import { isbot } from 'isbot'; 4 | import { renderToReadableStream } from 'react-dom/server'; 5 | import { renderHeadToString } from 'remix-island'; 6 | import { Head } from './root'; 7 | import { themeStore } from '~/lib/stores/theme'; 8 | import { initializeModelList } from '~/utils/constants'; 9 | 10 | export default async function handleRequest( 11 | request: Request, 12 | responseStatusCode: number, 13 | responseHeaders: Headers, 14 | remixContext: EntryContext, 15 | _loadContext: AppLoadContext, 16 | ) { 17 | await initializeModelList({}); 18 | 19 | const readable = await renderToReadableStream(, { 20 | signal: request.signal, 21 | onError(error: unknown) { 22 | console.error(error); 23 | responseStatusCode = 500; 24 | }, 25 | }); 26 | 27 | const body = new ReadableStream({ 28 | start(controller) { 29 | const head = renderHeadToString({ request, remixContext, Head }); 30 | 31 | controller.enqueue( 32 | new Uint8Array( 33 | new TextEncoder().encode( 34 | `${head}
`, 35 | ), 36 | ), 37 | ); 38 | 39 | const reader = readable.getReader(); 40 | 41 | function read() { 42 | reader 43 | .read() 44 | .then(({ done, value }) => { 45 | if (done) { 46 | controller.enqueue(new Uint8Array(new TextEncoder().encode('
'))); 47 | controller.close(); 48 | 49 | return; 50 | } 51 | 52 | controller.enqueue(value); 53 | read(); 54 | }) 55 | .catch((error) => { 56 | controller.error(error); 57 | readable.cancel(); 58 | }); 59 | } 60 | read(); 61 | }, 62 | 63 | cancel() { 64 | readable.cancel(); 65 | }, 66 | }); 67 | 68 | if (isbot(request.headers.get('user-agent') || '')) { 69 | await readable.allReady; 70 | } 71 | 72 | responseHeaders.set('Content-Type', 'text/html'); 73 | 74 | responseHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp'); 75 | responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin'); 76 | 77 | return new Response(body, { 78 | headers: responseHeaders, 79 | status: responseStatusCode, 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /app/lib/.server/llm/constants.ts: -------------------------------------------------------------------------------- 1 | // see https://docs.anthropic.com/en/docs/about-claude/models 2 | export const MAX_TOKENS = 8000; 3 | 4 | // limits the number of model responses that can be returned in a single request 5 | export const MAX_RESPONSE_SEGMENTS = 2; 6 | -------------------------------------------------------------------------------- /app/lib/.server/llm/switchable-stream.ts: -------------------------------------------------------------------------------- 1 | export default class SwitchableStream extends TransformStream { 2 | private _controller: TransformStreamDefaultController | null = null; 3 | private _currentReader: ReadableStreamDefaultReader | null = null; 4 | private _switches = 0; 5 | 6 | constructor() { 7 | let controllerRef: TransformStreamDefaultController | undefined; 8 | 9 | super({ 10 | start(controller) { 11 | controllerRef = controller; 12 | }, 13 | }); 14 | 15 | if (controllerRef === undefined) { 16 | throw new Error('Controller not properly initialized'); 17 | } 18 | 19 | this._controller = controllerRef; 20 | } 21 | 22 | async switchSource(newStream: ReadableStream) { 23 | if (this._currentReader) { 24 | await this._currentReader.cancel(); 25 | } 26 | 27 | this._currentReader = newStream.getReader(); 28 | 29 | this._pumpStream(); 30 | 31 | this._switches++; 32 | } 33 | 34 | private async _pumpStream() { 35 | if (!this._currentReader || !this._controller) { 36 | throw new Error('Stream is not properly initialized'); 37 | } 38 | 39 | try { 40 | while (true) { 41 | const { done, value } = await this._currentReader.read(); 42 | 43 | if (done) { 44 | break; 45 | } 46 | 47 | this._controller.enqueue(value); 48 | } 49 | } catch (error) { 50 | console.log(error); 51 | this._controller.error(error); 52 | } 53 | } 54 | 55 | close() { 56 | if (this._currentReader) { 57 | this._currentReader.cancel(); 58 | } 59 | 60 | this._controller?.terminate(); 61 | } 62 | 63 | get switches() { 64 | return this._switches; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/lib/common/prompt-library.ts: -------------------------------------------------------------------------------- 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 | } 50 | -------------------------------------------------------------------------------- /app/lib/crypto.ts: -------------------------------------------------------------------------------- 1 | const encoder = new TextEncoder(); 2 | const decoder = new TextDecoder(); 3 | const IV_LENGTH = 16; 4 | 5 | export async function encrypt(key: string, data: string) { 6 | const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); 7 | const cryptoKey = await getKey(key); 8 | 9 | const ciphertext = await crypto.subtle.encrypt( 10 | { 11 | name: 'AES-CBC', 12 | iv, 13 | }, 14 | cryptoKey, 15 | encoder.encode(data), 16 | ); 17 | 18 | const bundle = new Uint8Array(IV_LENGTH + ciphertext.byteLength); 19 | 20 | bundle.set(new Uint8Array(ciphertext)); 21 | bundle.set(iv, ciphertext.byteLength); 22 | 23 | return decodeBase64(bundle); 24 | } 25 | 26 | export async function decrypt(key: string, payload: string) { 27 | const bundle = encodeBase64(payload); 28 | 29 | const iv = new Uint8Array(bundle.buffer, bundle.byteLength - IV_LENGTH); 30 | const ciphertext = new Uint8Array(bundle.buffer, 0, bundle.byteLength - IV_LENGTH); 31 | 32 | const cryptoKey = await getKey(key); 33 | 34 | const plaintext = await crypto.subtle.decrypt( 35 | { 36 | name: 'AES-CBC', 37 | iv, 38 | }, 39 | cryptoKey, 40 | ciphertext, 41 | ); 42 | 43 | return decoder.decode(plaintext); 44 | } 45 | 46 | async function getKey(key: string) { 47 | return await crypto.subtle.importKey('raw', encodeBase64(key), { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']); 48 | } 49 | 50 | function decodeBase64(encoded: Uint8Array) { 51 | const byteChars = Array.from(encoded, (byte) => String.fromCodePoint(byte)); 52 | 53 | return btoa(byteChars.join('')); 54 | } 55 | 56 | function encodeBase64(data: string) { 57 | return Uint8Array.from(atob(data), (ch) => ch.codePointAt(0)!); 58 | } 59 | -------------------------------------------------------------------------------- /app/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | type CommonRequest = Omit & { body?: URLSearchParams }; 2 | 3 | export async function request(url: string, init?: CommonRequest) { 4 | if (import.meta.env.DEV) { 5 | const nodeFetch = await import('node-fetch'); 6 | const https = await import('node:https'); 7 | 8 | const agent = url.startsWith('https') ? new https.Agent({ rejectUnauthorized: false }) : undefined; 9 | 10 | return nodeFetch.default(url, { ...init, agent }); 11 | } 12 | 13 | return fetch(url, init); 14 | } 15 | -------------------------------------------------------------------------------- /app/lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useMessageParser'; 2 | export * from './usePromptEnhancer'; 3 | export * from './useShortcuts'; 4 | export * from './useSnapScroll'; 5 | export * from './useEditChatDescription'; 6 | export { default } from './useViewport'; 7 | -------------------------------------------------------------------------------- /app/lib/hooks/useMessageParser.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from 'ai'; 2 | import { useCallback, useState } from 'react'; 3 | import { StreamingMessageParser } from '~/lib/runtime/message-parser'; 4 | import { workbenchStore } from '~/lib/stores/workbench'; 5 | import { createScopedLogger } from '~/utils/logger'; 6 | 7 | const logger = createScopedLogger('useMessageParser'); 8 | 9 | const messageParser = new StreamingMessageParser({ 10 | callbacks: { 11 | onArtifactOpen: (data) => { 12 | logger.trace('onArtifactOpen', data); 13 | 14 | workbenchStore.showWorkbench.set(true); 15 | workbenchStore.addArtifact(data); 16 | }, 17 | onArtifactClose: (data) => { 18 | logger.trace('onArtifactClose'); 19 | 20 | workbenchStore.updateArtifact(data, { closed: true }); 21 | }, 22 | onActionOpen: (data) => { 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 | 37 | workbenchStore.runAction(data); 38 | }, 39 | onActionStream: (data) => { 40 | logger.trace('onActionStream', data.action); 41 | workbenchStore.runAction(data, true); 42 | }, 43 | }, 44 | }); 45 | 46 | export function useMessageParser() { 47 | const [parsedMessages, setParsedMessages] = useState<{ [key: number]: string }>({}); 48 | 49 | const parseMessages = useCallback((messages: Message[], isLoading: boolean) => { 50 | let reset = false; 51 | 52 | if (import.meta.env.DEV && !isLoading) { 53 | reset = true; 54 | messageParser.reset(); 55 | } 56 | 57 | for (const [index, message] of messages.entries()) { 58 | if (message.role === 'assistant') { 59 | const newParsedContent = messageParser.parse(message.id, message.content); 60 | 61 | setParsedMessages((prevParsed) => ({ 62 | ...prevParsed, 63 | [index]: !reset ? (prevParsed[index] || '') + newParsedContent : newParsedContent, 64 | })); 65 | } 66 | } 67 | }, []); 68 | 69 | return { parsedMessages, parseMessages }; 70 | } 71 | -------------------------------------------------------------------------------- /app/lib/hooks/usePromptEnhancer.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import type { ProviderInfo } from '~/types/model'; 3 | import { createScopedLogger } from '~/utils/logger'; 4 | 5 | const logger = createScopedLogger('usePromptEnhancement'); 6 | 7 | export function usePromptEnhancer() { 8 | const [enhancingPrompt, setEnhancingPrompt] = useState(false); 9 | const [promptEnhanced, setPromptEnhanced] = useState(false); 10 | 11 | const resetEnhancer = () => { 12 | setEnhancingPrompt(false); 13 | setPromptEnhanced(false); 14 | }; 15 | 16 | const enhancePrompt = async ( 17 | input: string, 18 | setInput: (value: string) => void, 19 | model: string, 20 | provider: ProviderInfo, 21 | apiKeys?: Record, 22 | ) => { 23 | setEnhancingPrompt(true); 24 | setPromptEnhanced(false); 25 | 26 | const requestBody: any = { 27 | message: input, 28 | model, 29 | provider, 30 | }; 31 | 32 | if (apiKeys) { 33 | requestBody.apiKeys = apiKeys; 34 | } 35 | 36 | const response = await fetch('/api/enhancer', { 37 | method: 'POST', 38 | body: JSON.stringify(requestBody), 39 | }); 40 | 41 | const reader = response.body?.getReader(); 42 | 43 | const originalInput = input; 44 | 45 | if (reader) { 46 | const decoder = new TextDecoder(); 47 | 48 | let _input = ''; 49 | let _error; 50 | 51 | try { 52 | setInput(''); 53 | 54 | while (true) { 55 | const { value, done } = await reader.read(); 56 | 57 | if (done) { 58 | break; 59 | } 60 | 61 | _input += decoder.decode(value); 62 | 63 | logger.trace('Set input', _input); 64 | 65 | setInput(_input); 66 | } 67 | } catch (error) { 68 | _error = error; 69 | setInput(originalInput); 70 | } finally { 71 | if (_error) { 72 | logger.error(_error); 73 | } 74 | 75 | setEnhancingPrompt(false); 76 | setPromptEnhanced(true); 77 | 78 | setTimeout(() => { 79 | setInput(_input); 80 | }); 81 | } 82 | } 83 | }; 84 | 85 | return { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer }; 86 | } 87 | -------------------------------------------------------------------------------- /app/lib/hooks/useSearchFilter.ts: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useCallback } from 'react'; 2 | import { debounce } from '~/utils/debounce'; 3 | import type { ChatHistoryItem } from '~/lib/persistence'; 4 | 5 | interface UseSearchFilterOptions { 6 | items: ChatHistoryItem[]; 7 | searchFields?: (keyof ChatHistoryItem)[]; 8 | debounceMs?: number; 9 | } 10 | 11 | export function useSearchFilter({ 12 | items = [], 13 | searchFields = ['description'], 14 | debounceMs = 300, 15 | }: UseSearchFilterOptions) { 16 | const [searchQuery, setSearchQuery] = useState(''); 17 | 18 | const debouncedSetSearch = useCallback(debounce(setSearchQuery, debounceMs), []); 19 | 20 | const handleSearchChange = useCallback( 21 | (event: React.ChangeEvent) => { 22 | debouncedSetSearch(event.target.value); 23 | }, 24 | [debouncedSetSearch], 25 | ); 26 | 27 | const filteredItems = useMemo(() => { 28 | if (!searchQuery.trim()) { 29 | return items; 30 | } 31 | 32 | const query = searchQuery.toLowerCase(); 33 | 34 | return items.filter((item) => 35 | searchFields.some((field) => { 36 | const value = item[field]; 37 | 38 | if (typeof value === 'string') { 39 | return value.toLowerCase().includes(query); 40 | } 41 | 42 | return false; 43 | }), 44 | ); 45 | }, [items, searchQuery, searchFields]); 46 | 47 | return { 48 | searchQuery, 49 | filteredItems, 50 | handleSearchChange, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /app/lib/hooks/useShortcuts.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from '@nanostores/react'; 2 | import { useEffect } from 'react'; 3 | import { shortcutsStore, type Shortcuts } from '~/lib/stores/settings'; 4 | 5 | class ShortcutEventEmitter { 6 | #emitter = new EventTarget(); 7 | 8 | dispatch(type: keyof Shortcuts) { 9 | this.#emitter.dispatchEvent(new Event(type)); 10 | } 11 | 12 | on(type: keyof Shortcuts, cb: VoidFunction) { 13 | this.#emitter.addEventListener(type, cb); 14 | 15 | return () => { 16 | this.#emitter.removeEventListener(type, cb); 17 | }; 18 | } 19 | } 20 | 21 | export const shortcutEventEmitter = new ShortcutEventEmitter(); 22 | 23 | export function useShortcuts(): void { 24 | const shortcuts = useStore(shortcutsStore); 25 | 26 | useEffect(() => { 27 | const handleKeyDown = (event: KeyboardEvent): void => { 28 | const { key, ctrlKey, shiftKey, altKey, metaKey } = event; 29 | 30 | for (const name in shortcuts) { 31 | const shortcut = shortcuts[name as keyof Shortcuts]; 32 | 33 | if ( 34 | shortcut.key.toLowerCase() === key.toLowerCase() && 35 | (shortcut.ctrlOrMetaKey 36 | ? ctrlKey || metaKey 37 | : (shortcut.ctrlKey === undefined || shortcut.ctrlKey === ctrlKey) && 38 | (shortcut.metaKey === undefined || shortcut.metaKey === metaKey)) && 39 | (shortcut.shiftKey === undefined || shortcut.shiftKey === shiftKey) && 40 | (shortcut.altKey === undefined || shortcut.altKey === altKey) 41 | ) { 42 | shortcutEventEmitter.dispatch(name as keyof Shortcuts); 43 | event.preventDefault(); 44 | event.stopPropagation(); 45 | 46 | shortcut.action(); 47 | 48 | break; 49 | } 50 | } 51 | }; 52 | 53 | window.addEventListener('keydown', handleKeyDown); 54 | 55 | return () => { 56 | window.removeEventListener('keydown', handleKeyDown); 57 | }; 58 | }, [shortcuts]); 59 | } 60 | -------------------------------------------------------------------------------- /app/lib/hooks/useSnapScroll.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback } from 'react'; 2 | 3 | export function useSnapScroll() { 4 | const autoScrollRef = useRef(true); 5 | const scrollNodeRef = useRef(); 6 | const onScrollRef = useRef<() => void>(); 7 | const observerRef = useRef(); 8 | 9 | const messageRef = useCallback((node: HTMLDivElement | null) => { 10 | if (node) { 11 | const observer = new ResizeObserver(() => { 12 | if (autoScrollRef.current && scrollNodeRef.current) { 13 | const { scrollHeight, clientHeight } = scrollNodeRef.current; 14 | const scrollTarget = scrollHeight - clientHeight; 15 | 16 | scrollNodeRef.current.scrollTo({ 17 | top: scrollTarget, 18 | }); 19 | } 20 | }); 21 | 22 | observer.observe(node); 23 | } else { 24 | observerRef.current?.disconnect(); 25 | observerRef.current = undefined; 26 | } 27 | }, []); 28 | 29 | const scrollRef = useCallback((node: HTMLDivElement | null) => { 30 | if (node) { 31 | onScrollRef.current = () => { 32 | const { scrollTop, scrollHeight, clientHeight } = node; 33 | const scrollTarget = scrollHeight - clientHeight; 34 | 35 | autoScrollRef.current = Math.abs(scrollTop - scrollTarget) <= 10; 36 | }; 37 | 38 | node.addEventListener('scroll', onScrollRef.current); 39 | 40 | scrollNodeRef.current = node; 41 | } else { 42 | if (onScrollRef.current) { 43 | scrollNodeRef.current?.removeEventListener('scroll', onScrollRef.current); 44 | } 45 | 46 | scrollNodeRef.current = undefined; 47 | onScrollRef.current = undefined; 48 | } 49 | }, []); 50 | 51 | return [messageRef, scrollRef]; 52 | } 53 | -------------------------------------------------------------------------------- /app/lib/hooks/useViewport.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useViewport = (threshold = 1024) => { 4 | const [isSmallViewport, setIsSmallViewport] = useState(window.innerWidth < threshold); 5 | 6 | useEffect(() => { 7 | const handleResize = () => setIsSmallViewport(window.innerWidth < threshold); 8 | window.addEventListener('resize', handleResize); 9 | 10 | return () => { 11 | window.removeEventListener('resize', handleResize); 12 | }; 13 | }, [threshold]); 14 | 15 | return isSmallViewport; 16 | }; 17 | 18 | export default useViewport; 19 | -------------------------------------------------------------------------------- /app/lib/modules/llm/base-provider.ts: -------------------------------------------------------------------------------- 1 | import type { LanguageModelV1 } from 'ai'; 2 | import type { ProviderInfo, ProviderConfig, ModelInfo } from './types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import { createOpenAI } from '@ai-sdk/openai'; 5 | import { LLMManager } from './manager'; 6 | 7 | export abstract class BaseProvider implements ProviderInfo { 8 | abstract name: string; 9 | abstract staticModels: ModelInfo[]; 10 | abstract config: ProviderConfig; 11 | 12 | getApiKeyLink?: string; 13 | labelForGetApiKey?: string; 14 | icon?: string; 15 | 16 | getProviderBaseUrlAndKey(options: { 17 | apiKeys?: Record; 18 | providerSettings?: IProviderSetting; 19 | serverEnv?: Record; 20 | defaultBaseUrlKey: string; 21 | defaultApiTokenKey: string; 22 | }) { 23 | const { apiKeys, providerSettings, serverEnv, defaultBaseUrlKey, defaultApiTokenKey } = options; 24 | let settingsBaseUrl = providerSettings?.baseUrl; 25 | const manager = LLMManager.getInstance(); 26 | 27 | if (settingsBaseUrl && settingsBaseUrl.length == 0) { 28 | settingsBaseUrl = undefined; 29 | } 30 | 31 | const baseUrlKey = this.config.baseUrlKey || defaultBaseUrlKey; 32 | let baseUrl = settingsBaseUrl || serverEnv?.[baseUrlKey] || process?.env?.[baseUrlKey] || manager.env?.[baseUrlKey]; 33 | 34 | if (baseUrl && baseUrl.endsWith('/')) { 35 | baseUrl = baseUrl.slice(0, -1); 36 | } 37 | 38 | const apiTokenKey = this.config.apiTokenKey || defaultApiTokenKey; 39 | const apiKey = 40 | apiKeys?.[this.name] || serverEnv?.[apiTokenKey] || process?.env?.[apiTokenKey] || manager.env?.[baseUrlKey]; 41 | 42 | return { 43 | baseUrl, 44 | apiKey, 45 | }; 46 | } 47 | 48 | // Declare the optional getDynamicModels method 49 | getDynamicModels?( 50 | apiKeys?: Record, 51 | settings?: IProviderSetting, 52 | serverEnv?: Record, 53 | ): Promise; 54 | 55 | abstract getModelInstance(options: { 56 | model: string; 57 | serverEnv: Env; 58 | apiKeys?: Record; 59 | providerSettings?: Record; 60 | }): LanguageModelV1; 61 | } 62 | 63 | type OptionalApiKey = string | undefined; 64 | 65 | export function getOpenAILikeModel(baseURL: string, apiKey: OptionalApiKey, model: string) { 66 | const openai = createOpenAI({ 67 | baseURL, 68 | apiKey, 69 | }); 70 | 71 | return openai(model); 72 | } 73 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/anthropic.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { LanguageModelV1 } from 'ai'; 4 | import type { IProviderSetting } from '~/types/model'; 5 | import { createAnthropic } from '@ai-sdk/anthropic'; 6 | 7 | export default class AnthropicProvider extends BaseProvider { 8 | name = 'Anthropic'; 9 | getApiKeyLink = 'https://console.anthropic.com/settings/keys'; 10 | 11 | config = { 12 | apiTokenKey: 'ANTHROPIC_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { 17 | name: 'claude-3-5-sonnet-latest', 18 | label: 'Claude 3.5 Sonnet (new)', 19 | provider: 'Anthropic', 20 | maxTokenAllowed: 8000, 21 | }, 22 | { 23 | name: 'claude-3-5-sonnet-20240620', 24 | label: 'Claude 3.5 Sonnet (old)', 25 | provider: 'Anthropic', 26 | maxTokenAllowed: 8000, 27 | }, 28 | { 29 | name: 'claude-3-5-haiku-latest', 30 | label: 'Claude 3.5 Haiku (new)', 31 | provider: 'Anthropic', 32 | maxTokenAllowed: 8000, 33 | }, 34 | { name: 'claude-3-opus-latest', label: 'Claude 3 Opus', provider: 'Anthropic', maxTokenAllowed: 8000 }, 35 | { name: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', provider: 'Anthropic', maxTokenAllowed: 8000 }, 36 | { name: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', provider: 'Anthropic', maxTokenAllowed: 8000 }, 37 | ]; 38 | getModelInstance: (options: { 39 | model: string; 40 | serverEnv: Env; 41 | apiKeys?: Record; 42 | providerSettings?: Record; 43 | }) => LanguageModelV1 = (options) => { 44 | const { apiKeys, providerSettings, serverEnv, model } = options; 45 | const { apiKey } = this.getProviderBaseUrlAndKey({ 46 | apiKeys, 47 | providerSettings, 48 | serverEnv: serverEnv as any, 49 | defaultBaseUrlKey: '', 50 | defaultApiTokenKey: 'ANTHROPIC_API_KEY', 51 | }); 52 | const anthropic = createAnthropic({ 53 | apiKey, 54 | }); 55 | 56 | return anthropic(model); 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/cohere.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | import { createCohere } from '@ai-sdk/cohere'; 6 | 7 | export default class CohereProvider extends BaseProvider { 8 | name = 'Cohere'; 9 | getApiKeyLink = 'https://dashboard.cohere.com/api-keys'; 10 | 11 | config = { 12 | apiTokenKey: 'COHERE_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { name: 'command-r-plus-08-2024', label: 'Command R plus Latest', provider: 'Cohere', maxTokenAllowed: 4096 }, 17 | { name: 'command-r-08-2024', label: 'Command R Latest', provider: 'Cohere', maxTokenAllowed: 4096 }, 18 | { name: 'command-r-plus', label: 'Command R plus', provider: 'Cohere', maxTokenAllowed: 4096 }, 19 | { name: 'command-r', label: 'Command R', provider: 'Cohere', maxTokenAllowed: 4096 }, 20 | { name: 'command', label: 'Command', provider: 'Cohere', maxTokenAllowed: 4096 }, 21 | { name: 'command-nightly', label: 'Command Nightly', provider: 'Cohere', maxTokenAllowed: 4096 }, 22 | { name: 'command-light', label: 'Command Light', provider: 'Cohere', maxTokenAllowed: 4096 }, 23 | { name: 'command-light-nightly', label: 'Command Light Nightly', provider: 'Cohere', maxTokenAllowed: 4096 }, 24 | { name: 'c4ai-aya-expanse-8b', label: 'c4AI Aya Expanse 8b', provider: 'Cohere', maxTokenAllowed: 4096 }, 25 | { name: 'c4ai-aya-expanse-32b', label: 'c4AI Aya Expanse 32b', provider: 'Cohere', maxTokenAllowed: 4096 }, 26 | ]; 27 | 28 | getModelInstance(options: { 29 | model: string; 30 | serverEnv: Env; 31 | apiKeys?: Record; 32 | providerSettings?: Record; 33 | }): LanguageModelV1 { 34 | const { model, serverEnv, apiKeys, providerSettings } = options; 35 | 36 | const { apiKey } = this.getProviderBaseUrlAndKey({ 37 | apiKeys, 38 | providerSettings: providerSettings?.[this.name], 39 | serverEnv: serverEnv as any, 40 | defaultBaseUrlKey: '', 41 | defaultApiTokenKey: 'COHERE_API_KEY', 42 | }); 43 | 44 | if (!apiKey) { 45 | throw new Error(`Missing API key for ${this.name} provider`); 46 | } 47 | 48 | const cohere = createCohere({ 49 | apiKey, 50 | }); 51 | 52 | return cohere(model); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/deepseek.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | import { createOpenAI } from '@ai-sdk/openai'; 6 | 7 | export default class DeepseekProvider extends BaseProvider { 8 | name = 'Deepseek'; 9 | getApiKeyLink = 'https://platform.deepseek.com/apiKeys'; 10 | 11 | config = { 12 | apiTokenKey: 'DEEPSEEK_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek', maxTokenAllowed: 8000 }, 17 | { name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek', maxTokenAllowed: 8000 }, 18 | ]; 19 | 20 | getModelInstance(options: { 21 | model: string; 22 | serverEnv: Env; 23 | apiKeys?: Record; 24 | providerSettings?: Record; 25 | }): LanguageModelV1 { 26 | const { model, serverEnv, apiKeys, providerSettings } = options; 27 | 28 | const { apiKey } = this.getProviderBaseUrlAndKey({ 29 | apiKeys, 30 | providerSettings: providerSettings?.[this.name], 31 | serverEnv: serverEnv as any, 32 | defaultBaseUrlKey: '', 33 | defaultApiTokenKey: 'DEEPSEEK_API_KEY', 34 | }); 35 | 36 | if (!apiKey) { 37 | throw new Error(`Missing API key for ${this.name} provider`); 38 | } 39 | 40 | const openai = createOpenAI({ 41 | baseURL: 'https://api.deepseek.com/beta', 42 | apiKey, 43 | }); 44 | 45 | return openai(model); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/google.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | import { createGoogleGenerativeAI } from '@ai-sdk/google'; 6 | 7 | export default class GoogleProvider extends BaseProvider { 8 | name = 'Google'; 9 | getApiKeyLink = 'https://aistudio.google.com/app/apikey'; 10 | 11 | config = { 12 | apiTokenKey: 'GOOGLE_GENERATIVE_AI_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google', maxTokenAllowed: 8192 }, 17 | { name: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash', provider: 'Google', maxTokenAllowed: 8192 }, 18 | { name: 'gemini-1.5-flash-002', label: 'Gemini 1.5 Flash-002', provider: 'Google', maxTokenAllowed: 8192 }, 19 | { name: 'gemini-1.5-flash-8b', label: 'Gemini 1.5 Flash-8b', provider: 'Google', maxTokenAllowed: 8192 }, 20 | { name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google', maxTokenAllowed: 8192 }, 21 | { name: 'gemini-1.5-pro-002', label: 'Gemini 1.5 Pro-002', provider: 'Google', maxTokenAllowed: 8192 }, 22 | { name: 'gemini-exp-1206', label: 'Gemini exp-1206', provider: 'Google', maxTokenAllowed: 8192 }, 23 | ]; 24 | 25 | getModelInstance(options: { 26 | model: string; 27 | serverEnv: any; 28 | apiKeys?: Record; 29 | providerSettings?: Record; 30 | }): LanguageModelV1 { 31 | const { model, serverEnv, apiKeys, providerSettings } = options; 32 | 33 | const { apiKey } = this.getProviderBaseUrlAndKey({ 34 | apiKeys, 35 | providerSettings: providerSettings?.[this.name], 36 | serverEnv: serverEnv as any, 37 | defaultBaseUrlKey: '', 38 | defaultApiTokenKey: 'GOOGLE_GENERATIVE_AI_API_KEY', 39 | }); 40 | 41 | if (!apiKey) { 42 | throw new Error(`Missing API key for ${this.name} provider`); 43 | } 44 | 45 | const google = createGoogleGenerativeAI({ 46 | apiKey, 47 | }); 48 | 49 | return google(model); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/groq.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | import { createOpenAI } from '@ai-sdk/openai'; 6 | 7 | export default class GroqProvider extends BaseProvider { 8 | name = 'Groq'; 9 | getApiKeyLink = 'https://console.groq.com/keys'; 10 | 11 | config = { 12 | apiTokenKey: 'GROQ_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, 17 | { name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, 18 | { name: 'llama-3.2-90b-vision-preview', label: 'Llama 3.2 90b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, 19 | { name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, 20 | { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, 21 | { name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, 22 | ]; 23 | 24 | getModelInstance(options: { 25 | model: string; 26 | serverEnv: Env; 27 | apiKeys?: Record; 28 | providerSettings?: Record; 29 | }): LanguageModelV1 { 30 | const { model, serverEnv, apiKeys, providerSettings } = options; 31 | 32 | const { apiKey } = this.getProviderBaseUrlAndKey({ 33 | apiKeys, 34 | providerSettings: providerSettings?.[this.name], 35 | serverEnv: serverEnv as any, 36 | defaultBaseUrlKey: '', 37 | defaultApiTokenKey: 'GROQ_API_KEY', 38 | }); 39 | 40 | if (!apiKey) { 41 | throw new Error(`Missing API key for ${this.name} provider`); 42 | } 43 | 44 | const openai = createOpenAI({ 45 | baseURL: 'https://api.groq.com/openai/v1', 46 | apiKey, 47 | }); 48 | 49 | return openai(model); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/huggingface.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | import { createOpenAI } from '@ai-sdk/openai'; 6 | 7 | export default class HuggingFaceProvider extends BaseProvider { 8 | name = 'HuggingFace'; 9 | getApiKeyLink = 'https://huggingface.co/settings/tokens'; 10 | 11 | config = { 12 | apiTokenKey: 'HuggingFace_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { 17 | name: 'Qwen/Qwen2.5-Coder-32B-Instruct', 18 | label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)', 19 | provider: 'HuggingFace', 20 | maxTokenAllowed: 8000, 21 | }, 22 | { 23 | name: '01-ai/Yi-1.5-34B-Chat', 24 | label: 'Yi-1.5-34B-Chat (HuggingFace)', 25 | provider: 'HuggingFace', 26 | maxTokenAllowed: 8000, 27 | }, 28 | { 29 | name: 'meta-llama/Llama-3.1-70B-Instruct', 30 | label: 'Llama-3.1-70B-Instruct (HuggingFace)', 31 | provider: 'HuggingFace', 32 | maxTokenAllowed: 8000, 33 | }, 34 | { 35 | name: 'meta-llama/Llama-3.1-405B', 36 | label: 'Llama-3.1-405B (HuggingFace)', 37 | provider: 'HuggingFace', 38 | maxTokenAllowed: 8000, 39 | }, 40 | ]; 41 | 42 | getModelInstance(options: { 43 | model: string; 44 | serverEnv: Env; 45 | apiKeys?: Record; 46 | providerSettings?: Record; 47 | }): LanguageModelV1 { 48 | const { model, serverEnv, apiKeys, providerSettings } = options; 49 | 50 | const { apiKey } = this.getProviderBaseUrlAndKey({ 51 | apiKeys, 52 | providerSettings: providerSettings?.[this.name], 53 | serverEnv: serverEnv as any, 54 | defaultBaseUrlKey: '', 55 | defaultApiTokenKey: 'HuggingFace_API_KEY', 56 | }); 57 | 58 | if (!apiKey) { 59 | throw new Error(`Missing API key for ${this.name} provider`); 60 | } 61 | 62 | const openai = createOpenAI({ 63 | baseURL: 'https://api-inference.huggingface.co/v1/', 64 | apiKey, 65 | }); 66 | 67 | return openai(model); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/lmstudio.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import { createOpenAI } from '@ai-sdk/openai'; 5 | import type { LanguageModelV1 } from 'ai'; 6 | 7 | export default class LMStudioProvider extends BaseProvider { 8 | name = 'LMStudio'; 9 | getApiKeyLink = 'https://lmstudio.ai/'; 10 | labelForGetApiKey = 'Get LMStudio'; 11 | icon = 'i-ph:cloud-arrow-down'; 12 | 13 | config = { 14 | baseUrlKey: 'LMSTUDIO_API_BASE_URL', 15 | }; 16 | 17 | staticModels: ModelInfo[] = []; 18 | 19 | async getDynamicModels( 20 | apiKeys?: Record, 21 | settings?: IProviderSetting, 22 | serverEnv: Record = {}, 23 | ): Promise { 24 | try { 25 | const { baseUrl } = this.getProviderBaseUrlAndKey({ 26 | apiKeys, 27 | providerSettings: settings, 28 | serverEnv, 29 | defaultBaseUrlKey: 'LMSTUDIO_API_BASE_URL', 30 | defaultApiTokenKey: '', 31 | }); 32 | 33 | if (!baseUrl) { 34 | return []; 35 | } 36 | 37 | const response = await fetch(`${baseUrl}/v1/models`); 38 | const data = (await response.json()) as { data: Array<{ id: string }> }; 39 | 40 | return data.data.map((model) => ({ 41 | name: model.id, 42 | label: model.id, 43 | provider: this.name, 44 | maxTokenAllowed: 8000, 45 | })); 46 | } catch (error: any) { 47 | console.log('Error getting LMStudio models:', error.message); 48 | 49 | return []; 50 | } 51 | } 52 | getModelInstance: (options: { 53 | model: string; 54 | serverEnv: Env; 55 | apiKeys?: Record; 56 | providerSettings?: Record; 57 | }) => LanguageModelV1 = (options) => { 58 | const { apiKeys, providerSettings, serverEnv, model } = options; 59 | const { baseUrl } = this.getProviderBaseUrlAndKey({ 60 | apiKeys, 61 | providerSettings, 62 | serverEnv: serverEnv as any, 63 | defaultBaseUrlKey: 'OLLAMA_API_BASE_URL', 64 | defaultApiTokenKey: '', 65 | }); 66 | const lmstudio = createOpenAI({ 67 | baseUrl: `${baseUrl}/v1`, 68 | apiKey: '', 69 | }); 70 | 71 | return lmstudio(model); 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/mistral.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | import { createMistral } from '@ai-sdk/mistral'; 6 | 7 | export default class MistralProvider extends BaseProvider { 8 | name = 'Mistral'; 9 | getApiKeyLink = 'https://console.mistral.ai/api-keys/'; 10 | 11 | config = { 12 | apiTokenKey: 'MISTRAL_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { name: 'open-mistral-7b', label: 'Mistral 7B', provider: 'Mistral', maxTokenAllowed: 8000 }, 17 | { name: 'open-mixtral-8x7b', label: 'Mistral 8x7B', provider: 'Mistral', maxTokenAllowed: 8000 }, 18 | { name: 'open-mixtral-8x22b', label: 'Mistral 8x22B', provider: 'Mistral', maxTokenAllowed: 8000 }, 19 | { name: 'open-codestral-mamba', label: 'Codestral Mamba', provider: 'Mistral', maxTokenAllowed: 8000 }, 20 | { name: 'open-mistral-nemo', label: 'Mistral Nemo', provider: 'Mistral', maxTokenAllowed: 8000 }, 21 | { name: 'ministral-8b-latest', label: 'Mistral 8B', provider: 'Mistral', maxTokenAllowed: 8000 }, 22 | { name: 'mistral-small-latest', label: 'Mistral Small', provider: 'Mistral', maxTokenAllowed: 8000 }, 23 | { name: 'codestral-latest', label: 'Codestral', provider: 'Mistral', maxTokenAllowed: 8000 }, 24 | { name: 'mistral-large-latest', label: 'Mistral Large Latest', provider: 'Mistral', maxTokenAllowed: 8000 }, 25 | ]; 26 | 27 | getModelInstance(options: { 28 | model: string; 29 | serverEnv: Env; 30 | apiKeys?: Record; 31 | providerSettings?: Record; 32 | }): LanguageModelV1 { 33 | const { model, serverEnv, apiKeys, providerSettings } = options; 34 | 35 | const { apiKey } = this.getProviderBaseUrlAndKey({ 36 | apiKeys, 37 | providerSettings: providerSettings?.[this.name], 38 | serverEnv: serverEnv as any, 39 | defaultBaseUrlKey: '', 40 | defaultApiTokenKey: 'MISTRAL_API_KEY', 41 | }); 42 | 43 | if (!apiKey) { 44 | throw new Error(`Missing API key for ${this.name} provider`); 45 | } 46 | 47 | const mistral = createMistral({ 48 | apiKey, 49 | }); 50 | 51 | return mistral(model); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/openai-like.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider, getOpenAILikeModel } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | 6 | export default class OpenAILikeProvider extends BaseProvider { 7 | name = 'OpenAILike'; 8 | getApiKeyLink = undefined; 9 | 10 | config = { 11 | baseUrlKey: 'OPENAI_LIKE_API_BASE_URL', 12 | apiTokenKey: 'OPENAI_LIKE_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = []; 16 | 17 | async getDynamicModels( 18 | apiKeys?: Record, 19 | settings?: IProviderSetting, 20 | serverEnv: Record = {}, 21 | ): Promise { 22 | try { 23 | const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({ 24 | apiKeys, 25 | providerSettings: settings, 26 | serverEnv, 27 | defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL', 28 | defaultApiTokenKey: 'OPENAI_LIKE_API_KEY', 29 | }); 30 | 31 | if (!baseUrl || !apiKey) { 32 | return []; 33 | } 34 | 35 | const response = await fetch(`${baseUrl}/models`, { 36 | headers: { 37 | Authorization: `Bearer ${apiKey}`, 38 | }, 39 | }); 40 | 41 | const res = (await response.json()) as any; 42 | 43 | return res.data.map((model: any) => ({ 44 | name: model.id, 45 | label: model.id, 46 | provider: this.name, 47 | maxTokenAllowed: 8000, 48 | })); 49 | } catch (error) { 50 | console.error('Error getting OpenAILike models:', error); 51 | return []; 52 | } 53 | } 54 | 55 | getModelInstance(options: { 56 | model: string; 57 | serverEnv: Env; 58 | apiKeys?: Record; 59 | providerSettings?: Record; 60 | }): LanguageModelV1 { 61 | const { model, serverEnv, apiKeys, providerSettings } = options; 62 | 63 | const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({ 64 | apiKeys, 65 | providerSettings: providerSettings?.[this.name], 66 | serverEnv: serverEnv as any, 67 | defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL', 68 | defaultApiTokenKey: 'OPENAI_LIKE_API_KEY', 69 | }); 70 | 71 | if (!baseUrl || !apiKey) { 72 | throw new Error(`Missing configuration for ${this.name} provider`); 73 | } 74 | 75 | return getOpenAILikeModel(baseUrl, apiKey, model); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/openai.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | import { createOpenAI } from '@ai-sdk/openai'; 6 | 7 | export default class OpenAIProvider extends BaseProvider { 8 | name = 'OpenAI'; 9 | getApiKeyLink = 'https://platform.openai.com/api-keys'; 10 | 11 | config = { 12 | apiTokenKey: 'OPENAI_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI', maxTokenAllowed: 8000 }, 17 | { name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 }, 18 | { name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI', maxTokenAllowed: 8000 }, 19 | { name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 }, 20 | ]; 21 | 22 | getModelInstance(options: { 23 | model: string; 24 | serverEnv: Env; 25 | apiKeys?: Record; 26 | providerSettings?: Record; 27 | }): LanguageModelV1 { 28 | const { model, serverEnv, apiKeys, providerSettings } = options; 29 | 30 | const { apiKey } = this.getProviderBaseUrlAndKey({ 31 | apiKeys, 32 | providerSettings: providerSettings?.[this.name], 33 | serverEnv: serverEnv as any, 34 | defaultBaseUrlKey: '', 35 | defaultApiTokenKey: 'OPENAI_API_KEY', 36 | }); 37 | 38 | if (!apiKey) { 39 | throw new Error(`Missing API key for ${this.name} provider`); 40 | } 41 | 42 | const openai = createOpenAI({ 43 | apiKey, 44 | }); 45 | 46 | return openai(model); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/perplexity.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | import { createOpenAI } from '@ai-sdk/openai'; 6 | 7 | export default class PerplexityProvider extends BaseProvider { 8 | name = 'Perplexity'; 9 | getApiKeyLink = 'https://www.perplexity.ai/settings/api'; 10 | 11 | config = { 12 | apiTokenKey: 'PERPLEXITY_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { 17 | name: 'llama-3.1-sonar-small-128k-online', 18 | label: 'Sonar Small Online', 19 | provider: 'Perplexity', 20 | maxTokenAllowed: 8192, 21 | }, 22 | { 23 | name: 'llama-3.1-sonar-large-128k-online', 24 | label: 'Sonar Large Online', 25 | provider: 'Perplexity', 26 | maxTokenAllowed: 8192, 27 | }, 28 | { 29 | name: 'llama-3.1-sonar-huge-128k-online', 30 | label: 'Sonar Huge Online', 31 | provider: 'Perplexity', 32 | maxTokenAllowed: 8192, 33 | }, 34 | ]; 35 | 36 | getModelInstance(options: { 37 | model: string; 38 | serverEnv: Env; 39 | apiKeys?: Record; 40 | providerSettings?: Record; 41 | }): LanguageModelV1 { 42 | const { model, serverEnv, apiKeys, providerSettings } = options; 43 | 44 | const { apiKey } = this.getProviderBaseUrlAndKey({ 45 | apiKeys, 46 | providerSettings: providerSettings?.[this.name], 47 | serverEnv: serverEnv as any, 48 | defaultBaseUrlKey: '', 49 | defaultApiTokenKey: 'PERPLEXITY_API_KEY', 50 | }); 51 | 52 | if (!apiKey) { 53 | throw new Error(`Missing API key for ${this.name} provider`); 54 | } 55 | 56 | const perplexity = createOpenAI({ 57 | baseURL: 'https://api.perplexity.ai/', 58 | apiKey, 59 | }); 60 | 61 | return perplexity(model); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/lib/modules/llm/providers/xai.ts: -------------------------------------------------------------------------------- 1 | import { BaseProvider } from '~/lib/modules/llm/base-provider'; 2 | import type { ModelInfo } from '~/lib/modules/llm/types'; 3 | import type { IProviderSetting } from '~/types/model'; 4 | import type { LanguageModelV1 } from 'ai'; 5 | import { createOpenAI } from '@ai-sdk/openai'; 6 | 7 | export default class XAIProvider extends BaseProvider { 8 | name = 'xAI'; 9 | getApiKeyLink = 'https://docs.x.ai/docs/quickstart#creating-an-api-key'; 10 | 11 | config = { 12 | apiTokenKey: 'XAI_API_KEY', 13 | }; 14 | 15 | staticModels: ModelInfo[] = [ 16 | { name: 'grok-beta', label: 'xAI Grok Beta', provider: 'xAI', maxTokenAllowed: 8000 }, 17 | { name: 'grok-2-1212', label: 'xAI Grok2 1212', provider: 'xAI', maxTokenAllowed: 8000 }, 18 | ]; 19 | 20 | getModelInstance(options: { 21 | model: string; 22 | serverEnv: Env; 23 | apiKeys?: Record; 24 | providerSettings?: Record; 25 | }): LanguageModelV1 { 26 | const { model, serverEnv, apiKeys, providerSettings } = options; 27 | 28 | const { apiKey } = this.getProviderBaseUrlAndKey({ 29 | apiKeys, 30 | providerSettings: providerSettings?.[this.name], 31 | serverEnv: serverEnv as any, 32 | defaultBaseUrlKey: '', 33 | defaultApiTokenKey: 'XAI_API_KEY', 34 | }); 35 | 36 | if (!apiKey) { 37 | throw new Error(`Missing API key for ${this.name} provider`); 38 | } 39 | 40 | const openai = createOpenAI({ 41 | baseURL: 'https://api.x.ai/v1', 42 | apiKey, 43 | }); 44 | 45 | return openai(model); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/lib/modules/llm/registry.ts: -------------------------------------------------------------------------------- 1 | import AnthropicProvider from './providers/anthropic'; 2 | import CohereProvider from './providers/cohere'; 3 | import DeepseekProvider from './providers/deepseek'; 4 | import GoogleProvider from './providers/google'; 5 | import GroqProvider from './providers/groq'; 6 | import HuggingFaceProvider from './providers/huggingface'; 7 | import LMStudioProvider from './providers/lmstudio'; 8 | import MistralProvider from './providers/mistral'; 9 | import OllamaProvider from './providers/ollama'; 10 | import OpenRouterProvider from './providers/open-router'; 11 | import OpenAILikeProvider from './providers/openai-like'; 12 | import OpenAIProvider from './providers/openai'; 13 | import PerplexityProvider from './providers/perplexity'; 14 | import TogetherProvider from './providers/together'; 15 | import XAIProvider from './providers/xai'; 16 | 17 | export { 18 | AnthropicProvider, 19 | CohereProvider, 20 | DeepseekProvider, 21 | GoogleProvider, 22 | GroqProvider, 23 | HuggingFaceProvider, 24 | MistralProvider, 25 | OllamaProvider, 26 | OpenAIProvider, 27 | OpenRouterProvider, 28 | OpenAILikeProvider, 29 | PerplexityProvider, 30 | XAIProvider, 31 | TogetherProvider, 32 | LMStudioProvider, 33 | }; 34 | -------------------------------------------------------------------------------- /app/lib/modules/llm/types.ts: -------------------------------------------------------------------------------- 1 | import type { LanguageModelV1 } from 'ai'; 2 | import type { IProviderSetting } from '~/types/model'; 3 | 4 | export interface ModelInfo { 5 | name: string; 6 | label: string; 7 | provider: string; 8 | maxTokenAllowed: number; 9 | } 10 | 11 | export interface ProviderInfo { 12 | name: string; 13 | staticModels: ModelInfo[]; 14 | getDynamicModels?: ( 15 | apiKeys?: Record, 16 | settings?: IProviderSetting, 17 | serverEnv?: Record, 18 | ) => Promise; 19 | getModelInstance: (options: { 20 | model: string; 21 | serverEnv: Env; 22 | apiKeys?: Record; 23 | providerSettings?: Record; 24 | }) => LanguageModelV1; 25 | getApiKeyLink?: string; 26 | labelForGetApiKey?: string; 27 | icon?: string; 28 | } 29 | export interface ProviderConfig { 30 | baseUrlKey?: string; 31 | apiTokenKey?: string; 32 | } 33 | -------------------------------------------------------------------------------- /app/lib/persistence/ChatDescription.client.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from '@nanostores/react'; 2 | import { TooltipProvider } from '@radix-ui/react-tooltip'; 3 | import WithTooltip from '~/components/ui/Tooltip'; 4 | import { useEditChatDescription } from '~/lib/hooks'; 5 | import { description as descriptionStore } from '~/lib/persistence'; 6 | 7 | export function ChatDescription() { 8 | const initialDescription = useStore(descriptionStore)!; 9 | 10 | const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } = 11 | useEditChatDescription({ 12 | initialDescription, 13 | syncWithGlobalStore: true, 14 | }); 15 | 16 | if (!initialDescription) { 17 | // doing this to prevent showing edit button until chat description is set 18 | return null; 19 | } 20 | 21 | return ( 22 |
23 | {editing ? ( 24 |
25 | 35 | 36 | 37 |
38 |
44 |
45 |
46 |
47 | ) : ( 48 | <> 49 | {currentDescription} 50 | 51 | 52 |
53 |
62 |
63 |
64 | 65 | )} 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /app/lib/persistence/index.ts: -------------------------------------------------------------------------------- 1 | export * from './db'; 2 | export * from './useChatHistory'; 3 | -------------------------------------------------------------------------------- /app/lib/stores/chat.ts: -------------------------------------------------------------------------------- 1 | import { map } from 'nanostores'; 2 | 3 | export const chatStore = map({ 4 | started: false, 5 | aborted: false, 6 | showChat: true, 7 | }); 8 | -------------------------------------------------------------------------------- /app/lib/stores/editor.ts: -------------------------------------------------------------------------------- 1 | import { atom, computed, map, type MapStore, type WritableAtom } from 'nanostores'; 2 | import type { EditorDocument, ScrollPosition } from '~/components/editor/codemirror/CodeMirrorEditor'; 3 | import type { FileMap, FilesStore } from './files'; 4 | 5 | export type EditorDocuments = Record; 6 | 7 | type SelectedFile = WritableAtom; 8 | 9 | export class EditorStore { 10 | #filesStore: FilesStore; 11 | 12 | selectedFile: SelectedFile = import.meta.hot?.data.selectedFile ?? atom(); 13 | documents: MapStore = import.meta.hot?.data.documents ?? map({}); 14 | 15 | currentDocument = computed([this.documents, this.selectedFile], (documents, selectedFile) => { 16 | if (!selectedFile) { 17 | return undefined; 18 | } 19 | 20 | return documents[selectedFile]; 21 | }); 22 | 23 | constructor(filesStore: FilesStore) { 24 | this.#filesStore = filesStore; 25 | 26 | if (import.meta.hot) { 27 | import.meta.hot.data.documents = this.documents; 28 | import.meta.hot.data.selectedFile = this.selectedFile; 29 | } 30 | } 31 | 32 | setDocuments(files: FileMap) { 33 | const previousDocuments = this.documents.value; 34 | 35 | this.documents.set( 36 | Object.fromEntries( 37 | Object.entries(files) 38 | .map(([filePath, dirent]) => { 39 | if (dirent === undefined || dirent.type === 'folder') { 40 | return undefined; 41 | } 42 | 43 | const previousDocument = previousDocuments?.[filePath]; 44 | 45 | return [ 46 | filePath, 47 | { 48 | value: dirent.content, 49 | filePath, 50 | scroll: previousDocument?.scroll, 51 | }, 52 | ] as [string, EditorDocument]; 53 | }) 54 | .filter(Boolean) as Array<[string, EditorDocument]>, 55 | ), 56 | ); 57 | } 58 | 59 | setSelectedFile(filePath: string | undefined) { 60 | this.selectedFile.set(filePath); 61 | } 62 | 63 | updateScrollPosition(filePath: string, position: ScrollPosition) { 64 | const documents = this.documents.get(); 65 | const documentState = documents[filePath]; 66 | 67 | if (!documentState) { 68 | return; 69 | } 70 | 71 | this.documents.setKey(filePath, { 72 | ...documentState, 73 | scroll: position, 74 | }); 75 | } 76 | 77 | updateFile(filePath: string, newContent: string) { 78 | const documents = this.documents.get(); 79 | const documentState = documents[filePath]; 80 | 81 | if (!documentState) { 82 | return; 83 | } 84 | 85 | const currentContent = documentState.value; 86 | const contentChanged = currentContent !== newContent; 87 | 88 | if (contentChanged) { 89 | this.documents.setKey(filePath, { 90 | ...documentState, 91 | value: newContent, 92 | }); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/lib/stores/previews.ts: -------------------------------------------------------------------------------- 1 | import type { WebContainer } from '@webcontainer/api'; 2 | import { atom } from 'nanostores'; 3 | 4 | export interface PreviewInfo { 5 | port: number; 6 | ready: boolean; 7 | baseUrl: string; 8 | } 9 | 10 | export class PreviewsStore { 11 | #availablePreviews = new Map(); 12 | #webcontainer: Promise; 13 | 14 | previews = atom([]); 15 | 16 | constructor(webcontainerPromise: Promise) { 17 | this.#webcontainer = webcontainerPromise; 18 | 19 | this.#init(); 20 | } 21 | 22 | async #init() { 23 | const webcontainer = await this.#webcontainer; 24 | 25 | webcontainer.on('port', (port, type, url) => { 26 | let previewInfo = this.#availablePreviews.get(port); 27 | 28 | if (type === 'close' && previewInfo) { 29 | this.#availablePreviews.delete(port); 30 | this.previews.set(this.previews.get().filter((preview) => preview.port !== port)); 31 | 32 | return; 33 | } 34 | 35 | const previews = this.previews.get(); 36 | 37 | if (!previewInfo) { 38 | previewInfo = { port, ready: type === 'open', baseUrl: url }; 39 | this.#availablePreviews.set(port, previewInfo); 40 | previews.push(previewInfo); 41 | } 42 | 43 | previewInfo.ready = type === 'open'; 44 | previewInfo.baseUrl = url; 45 | 46 | this.previews.set([...previews]); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/lib/stores/settings.ts: -------------------------------------------------------------------------------- 1 | import { atom, map } from 'nanostores'; 2 | import { workbenchStore } from './workbench'; 3 | import { PROVIDER_LIST } from '~/utils/constants'; 4 | import type { IProviderConfig } from '~/types/model'; 5 | 6 | export interface Shortcut { 7 | key: string; 8 | ctrlKey?: boolean; 9 | shiftKey?: boolean; 10 | altKey?: boolean; 11 | metaKey?: boolean; 12 | ctrlOrMetaKey?: boolean; 13 | action: () => void; 14 | } 15 | 16 | export interface Shortcuts { 17 | toggleTerminal: Shortcut; 18 | } 19 | 20 | export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike']; 21 | export const LOCAL_PROVIDERS = ['OpenAILike', 'LMStudio', 'Ollama']; 22 | 23 | export type ProviderSetting = Record; 24 | 25 | export const shortcutsStore = map({ 26 | toggleTerminal: { 27 | key: 'j', 28 | ctrlOrMetaKey: true, 29 | action: () => workbenchStore.toggleTerminal(), 30 | }, 31 | }); 32 | 33 | const initialProviderSettings: ProviderSetting = {}; 34 | PROVIDER_LIST.forEach((provider) => { 35 | initialProviderSettings[provider.name] = { 36 | ...provider, 37 | settings: { 38 | enabled: true, 39 | }, 40 | }; 41 | }); 42 | export const providersStore = map(initialProviderSettings); 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('default'); 51 | 52 | export const latestBranchStore = atom(false); 53 | -------------------------------------------------------------------------------- /app/lib/stores/terminal.ts: -------------------------------------------------------------------------------- 1 | import type { WebContainer, WebContainerProcess } from '@webcontainer/api'; 2 | import { atom, type WritableAtom } from 'nanostores'; 3 | import type { ITerminal } from '~/types/terminal'; 4 | import { newBoltShellProcess, newShellProcess } from '~/utils/shell'; 5 | import { coloredText } from '~/utils/terminal'; 6 | 7 | export class TerminalStore { 8 | #webcontainer: Promise; 9 | #terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = []; 10 | #boltTerminal = newBoltShellProcess(); 11 | 12 | showTerminal: WritableAtom = import.meta.hot?.data.showTerminal ?? atom(true); 13 | 14 | constructor(webcontainerPromise: Promise) { 15 | this.#webcontainer = webcontainerPromise; 16 | 17 | if (import.meta.hot) { 18 | import.meta.hot.data.showTerminal = this.showTerminal; 19 | } 20 | } 21 | get boltTerminal() { 22 | return this.#boltTerminal; 23 | } 24 | 25 | toggleTerminal(value?: boolean) { 26 | this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get()); 27 | } 28 | async attachBoltTerminal(terminal: ITerminal) { 29 | try { 30 | const wc = await this.#webcontainer; 31 | await this.#boltTerminal.init(wc, terminal); 32 | } catch (error: any) { 33 | terminal.write(coloredText.red('Failed to spawn bolt shell\n\n') + error.message); 34 | return; 35 | } 36 | } 37 | 38 | async attachTerminal(terminal: ITerminal) { 39 | try { 40 | const shellProcess = await newShellProcess(await this.#webcontainer, terminal); 41 | this.#terminals.push({ terminal, process: shellProcess }); 42 | } catch (error: any) { 43 | terminal.write(coloredText.red('Failed to spawn shell\n\n') + error.message); 44 | return; 45 | } 46 | } 47 | 48 | onTerminalResize(cols: number, rows: number) { 49 | for (const { process } of this.#terminals) { 50 | process.resize({ cols, rows }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/lib/stores/theme.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'nanostores'; 2 | import { logStore } from './logs'; 3 | 4 | export type Theme = 'dark' | 'light'; 5 | 6 | export const kTheme = 'bolt_theme'; 7 | 8 | export function themeIsDark() { 9 | return themeStore.get() === 'dark'; 10 | } 11 | 12 | export const DEFAULT_THEME = 'light'; 13 | 14 | export const themeStore = atom(initStore()); 15 | 16 | function initStore() { 17 | if (!import.meta.env.SSR) { 18 | const persistedTheme = localStorage.getItem(kTheme) as Theme | undefined; 19 | const themeAttribute = document.querySelector('html')?.getAttribute('data-theme'); 20 | 21 | return persistedTheme ?? (themeAttribute as Theme) ?? DEFAULT_THEME; 22 | } 23 | 24 | return DEFAULT_THEME; 25 | } 26 | 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 | } 35 | -------------------------------------------------------------------------------- /app/lib/webcontainer/auth.client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This client-only module that contains everything related to auth and is used 3 | * to avoid importing `@webcontainer/api` in the server bundle. 4 | */ 5 | 6 | export { auth, type AuthAPI } from '@webcontainer/api'; 7 | -------------------------------------------------------------------------------- /app/lib/webcontainer/index.ts: -------------------------------------------------------------------------------- 1 | import { WebContainer } from '@webcontainer/api'; 2 | import { WORK_DIR_NAME } from '~/utils/constants'; 3 | import { cleanStackTrace } from '~/utils/stacktrace'; 4 | 5 | interface WebContainerContext { 6 | loaded: boolean; 7 | } 8 | 9 | export const webcontainerContext: WebContainerContext = import.meta.hot?.data.webcontainerContext ?? { 10 | loaded: false, 11 | }; 12 | 13 | if (import.meta.hot) { 14 | import.meta.hot.data.webcontainerContext = webcontainerContext; 15 | } 16 | 17 | export let webcontainer: Promise = new Promise(() => { 18 | // noop for ssr 19 | }); 20 | 21 | if (!import.meta.env.SSR) { 22 | webcontainer = 23 | import.meta.hot?.data.webcontainer ?? 24 | Promise.resolve() 25 | .then(() => { 26 | return WebContainer.boot({ 27 | workdirName: WORK_DIR_NAME, 28 | forwardPreviewErrors: true, // Enable error forwarding from iframes 29 | }); 30 | }) 31 | .then(async (webcontainer) => { 32 | webcontainerContext.loaded = true; 33 | 34 | const { workbenchStore } = await import('~/lib/stores/workbench'); 35 | 36 | // Listen for preview errors 37 | webcontainer.on('preview-message', (message) => { 38 | console.log('WebContainer preview message:', message); 39 | 40 | // Handle both uncaught exceptions and unhandled promise rejections 41 | if (message.type === 'PREVIEW_UNCAUGHT_EXCEPTION' || message.type === 'PREVIEW_UNHANDLED_REJECTION') { 42 | const isPromise = message.type === 'PREVIEW_UNHANDLED_REJECTION'; 43 | workbenchStore.actionAlert.set({ 44 | type: 'preview', 45 | title: isPromise ? 'Unhandled Promise Rejection' : 'Uncaught Exception', 46 | description: message.message, 47 | content: `Error occurred at ${message.pathname}${message.search}${message.hash}\nPort: ${message.port}\n\nStack trace:\n${cleanStackTrace(message.stack || '')}`, 48 | source: 'preview', 49 | }); 50 | } 51 | }); 52 | 53 | return webcontainer; 54 | }); 55 | 56 | if (import.meta.hot) { 57 | import.meta.hot.data.webcontainer = webcontainer; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import { json, type MetaFunction } from '@remix-run/cloudflare'; 2 | import { ClientOnly } from 'remix-utils/client-only'; 3 | import { BaseChat } from '~/components/chat/BaseChat'; 4 | import { Chat } from '~/components/chat/Chat.client'; 5 | import { Header } from '~/components/header/Header'; 6 | import BackgroundRays from '~/components/ui/BackgroundRays'; 7 | 8 | export const meta: MetaFunction = () => { 9 | return [ 10 | { title: 'Bolt.diy: Open Source AI App Builder with Local & Hosted Free AI Models' }, 11 | { 12 | name: 'description', 13 | content: 14 | 'Bolt.diy is an open-source, community-driven platform for building AI-powered apps. Access local and hosted free AI models from a large list of options to suit your needs!', 15 | }, 16 | { 17 | name: 'keywords', 18 | content: 19 | 'open source AI app builder, Bolt, community-driven, free AI models, local models, hosted models, browser-based development, app development, full-stack development', 20 | }, 21 | 22 | // Open Graph tags 23 | { property: 'og:type', content: 'website' }, 24 | { property: 'og:title', content: 'Bolt.diy: Open Source AI App Builder' }, 25 | { 26 | property: 'og:description', 27 | content: 'Build AI-powered apps with Bolt.diy - Access local and hosted free AI models.', 28 | }, 29 | { property: 'og:site_name', content: 'Bolt.diy' }, 30 | 31 | // Twitter Card tags 32 | { name: 'twitter:card', content: 'summary_large_image' }, 33 | { name: 'twitter:title', content: 'Bolt.diy: Open Source AI App Builder' }, 34 | { 35 | name: 'twitter:description', 36 | content: 'Build AI-powered apps with Bolt.diy - Access local and hosted free AI models.', 37 | }, 38 | ]; 39 | }; 40 | 41 | export const loader = () => json({}); 42 | 43 | export default function Index() { 44 | return ( 45 |
46 | 47 |
48 | }>{() => } 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/routes/api.models.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@remix-run/cloudflare'; 2 | import { MODEL_LIST } from '~/utils/constants'; 3 | 4 | export async function loader() { 5 | return json(MODEL_LIST); 6 | } 7 | -------------------------------------------------------------------------------- /app/routes/chat.$id.tsx: -------------------------------------------------------------------------------- 1 | import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare'; 2 | import { default as IndexRoute } from './_index'; 3 | 4 | export async function loader(args: LoaderFunctionArgs) { 5 | return json({ id: args.params.id }); 6 | } 7 | 8 | export default IndexRoute; 9 | -------------------------------------------------------------------------------- /app/routes/git.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunctionArgs } from '@remix-run/cloudflare'; 2 | import { json, type MetaFunction } from '@remix-run/cloudflare'; 3 | import { ClientOnly } from 'remix-utils/client-only'; 4 | import { BaseChat } from '~/components/chat/BaseChat'; 5 | import { GitUrlImport } from '~/components/git/GitUrlImport.client'; 6 | import { Header } from '~/components/header/Header'; 7 | import BackgroundRays from '~/components/ui/BackgroundRays'; 8 | 9 | export const meta: MetaFunction = () => { 10 | return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }]; 11 | }; 12 | 13 | export async function loader(args: LoaderFunctionArgs) { 14 | return json({ url: args.params.url }); 15 | } 16 | 17 | export default function Index() { 18 | return ( 19 |
20 | 21 |
22 | }>{() => } 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/styles/animations.scss: -------------------------------------------------------------------------------- 1 | .animated { 2 | animation-fill-mode: both; 3 | animation-duration: var(--animate-duration, 0.2s); 4 | animation-timing-function: cubic-bezier(0, 0, 0.2, 1); 5 | 6 | &.fadeInRight { 7 | animation-name: fadeInRight; 8 | } 9 | 10 | &.fadeOutRight { 11 | animation-name: fadeOutRight; 12 | } 13 | } 14 | 15 | @keyframes fadeInRight { 16 | from { 17 | opacity: 0; 18 | transform: translate3d(100%, 0, 0); 19 | } 20 | 21 | to { 22 | opacity: 1; 23 | transform: translate3d(0, 0, 0); 24 | } 25 | } 26 | 27 | @keyframes fadeOutRight { 28 | from { 29 | opacity: 1; 30 | } 31 | 32 | to { 33 | opacity: 0; 34 | transform: translate3d(100%, 0, 0); 35 | } 36 | } 37 | 38 | .dropdown-animation { 39 | opacity: 0; 40 | animation: fadeMoveDown 0.15s forwards; 41 | animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 42 | } 43 | 44 | @keyframes fadeMoveDown { 45 | to { 46 | opacity: 1; 47 | transform: translateY(6px); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/styles/components/code.scss: -------------------------------------------------------------------------------- 1 | .actions .shiki { 2 | background-color: var(--bolt-elements-actions-code-background) !important; 3 | } 4 | 5 | .shiki { 6 | &:not(:has(.actions), .actions *) { 7 | background-color: var(--bolt-elements-messages-code-background) !important; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/styles/components/resize-handle.scss: -------------------------------------------------------------------------------- 1 | @use '../z-index'; 2 | 3 | [data-resize-handle] { 4 | position: relative; 5 | 6 | &[data-panel-group-direction='horizontal']:after { 7 | content: ''; 8 | position: absolute; 9 | top: 0; 10 | bottom: 0; 11 | left: -6px; 12 | right: -5px; 13 | z-index: z-index.$zIndexMax; 14 | } 15 | 16 | &[data-panel-group-direction='vertical']:after { 17 | content: ''; 18 | position: absolute; 19 | left: 0; 20 | right: 0; 21 | top: -5px; 22 | bottom: -6px; 23 | z-index: z-index.$zIndexMax; 24 | } 25 | 26 | &[data-resize-handle-state='hover']:after, 27 | &[data-resize-handle-state='drag']:after { 28 | background-color: #8882; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/styles/components/terminal.scss: -------------------------------------------------------------------------------- 1 | .xterm { 2 | padding: 1rem; 3 | } 4 | -------------------------------------------------------------------------------- /app/styles/components/toast.scss: -------------------------------------------------------------------------------- 1 | .Toastify__toast { 2 | --at-apply: shadow-md; 3 | 4 | background-color: var(--bolt-elements-bg-depth-2); 5 | color: var(--bolt-elements-textPrimary); 6 | border: 1px solid var(--bolt-elements-borderColor); 7 | } 8 | 9 | .Toastify__close-button { 10 | color: var(--bolt-elements-item-contentDefault); 11 | opacity: 1; 12 | transition: none; 13 | 14 | &:hover { 15 | color: var(--bolt-elements-item-contentActive); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use 'variables.scss'; 2 | @use 'z-index.scss'; 3 | @use 'animations.scss'; 4 | @use 'components/terminal.scss'; 5 | @use 'components/resize-handle.scss'; 6 | @use 'components/code.scss'; 7 | @use 'components/editor.scss'; 8 | @use 'components/toast.scss'; 9 | 10 | html, 11 | body { 12 | height: 100%; 13 | width: 100%; 14 | } 15 | 16 | :root { 17 | --gradient-opacity: 0.8; 18 | --primary-color: rgba(158, 117, 240, var(--gradient-opacity)); 19 | --secondary-color: rgba(138, 43, 226, var(--gradient-opacity)); 20 | --accent-color: rgba(128, 59, 239, var(--gradient-opacity)); 21 | // --primary-color: rgba(147, 112, 219, var(--gradient-opacity)); 22 | // --secondary-color: rgba(138, 43, 226, var(--gradient-opacity)); 23 | // --accent-color: rgba(180, 170, 220, var(--gradient-opacity)); 24 | } 25 | -------------------------------------------------------------------------------- /app/styles/z-index.scss: -------------------------------------------------------------------------------- 1 | $zIndexMax: 999; 2 | 3 | .z-logo { 4 | z-index: $zIndexMax - 1; 5 | } 6 | 7 | .z-sidebar { 8 | z-index: $zIndexMax - 2; 9 | } 10 | 11 | .z-port-dropdown { 12 | z-index: $zIndexMax - 3; 13 | } 14 | 15 | .z-iframe-overlay { 16 | z-index: $zIndexMax - 4; 17 | } 18 | 19 | .z-prompt { 20 | z-index: 2; 21 | } 22 | 23 | .z-workbench { 24 | z-index: 3; 25 | } 26 | 27 | .z-file-tree-breadcrumb { 28 | z-index: $zIndexMax - 1; 29 | } 30 | 31 | .z-max { 32 | z-index: $zIndexMax; 33 | } 34 | -------------------------------------------------------------------------------- /app/types/actions.ts: -------------------------------------------------------------------------------- 1 | export type ActionType = 'file' | 'shell'; 2 | 3 | export interface BaseAction { 4 | content: string; 5 | } 6 | 7 | export interface FileAction extends BaseAction { 8 | type: 'file'; 9 | filePath: string; 10 | } 11 | 12 | export interface ShellAction extends BaseAction { 13 | type: 'shell'; 14 | } 15 | 16 | export interface StartAction extends BaseAction { 17 | type: 'start'; 18 | } 19 | 20 | export type BoltAction = FileAction | ShellAction | StartAction; 21 | 22 | export type BoltActionData = BoltAction | BaseAction; 23 | 24 | export interface ActionAlert { 25 | type: string; 26 | title: string; 27 | description: string; 28 | content: string; 29 | source?: 'terminal' | 'preview'; // Add source to differentiate between terminal and preview errors 30 | } 31 | -------------------------------------------------------------------------------- /app/types/artifact.ts: -------------------------------------------------------------------------------- 1 | export interface BoltArtifactData { 2 | id: string; 3 | title: string; 4 | type?: string | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /app/types/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | showDirectoryPicker(): Promise; 3 | webkitSpeechRecognition: typeof SpeechRecognition; 4 | SpeechRecognition: typeof SpeechRecognition; 5 | } 6 | 7 | interface Performance { 8 | memory?: { 9 | jsHeapSizeLimit: number; 10 | totalJSHeapSize: number; 11 | usedJSHeapSize: number; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /app/types/model.ts: -------------------------------------------------------------------------------- 1 | import type { ModelInfo } from '~/lib/modules/llm/types'; 2 | 3 | export type ProviderInfo = { 4 | staticModels: ModelInfo[]; 5 | name: string; 6 | getDynamicModels?: ( 7 | providerName: string, 8 | apiKeys?: Record, 9 | providerSettings?: IProviderSetting, 10 | serverEnv?: Record, 11 | ) => Promise; 12 | getApiKeyLink?: string; 13 | labelForGetApiKey?: string; 14 | icon?: string; 15 | }; 16 | 17 | export interface IProviderSetting { 18 | enabled?: boolean; 19 | baseUrl?: string; 20 | } 21 | 22 | export type IProviderConfig = ProviderInfo & { 23 | settings: IProviderSetting; 24 | }; 25 | -------------------------------------------------------------------------------- /app/types/template.ts: -------------------------------------------------------------------------------- 1 | export interface Template { 2 | name: string; 3 | label: string; 4 | description: string; 5 | githubRepo: string; 6 | tags?: string[]; 7 | icon?: string; 8 | } 9 | -------------------------------------------------------------------------------- /app/types/terminal.ts: -------------------------------------------------------------------------------- 1 | export interface ITerminal { 2 | readonly cols?: number; 3 | readonly rows?: number; 4 | 5 | reset: () => void; 6 | write: (data: string) => void; 7 | onData: (cb: (data: string) => void) => void; 8 | input: (data: string) => void; 9 | } 10 | -------------------------------------------------------------------------------- /app/types/theme.ts: -------------------------------------------------------------------------------- 1 | export type Theme = 'dark' | 'light'; 2 | -------------------------------------------------------------------------------- /app/utils/buffer.ts: -------------------------------------------------------------------------------- 1 | export function bufferWatchEvents(timeInMs: number, cb: (events: T[]) => unknown) { 2 | let timeoutId: number | undefined; 3 | let events: T[] = []; 4 | 5 | // keep track of the processing of the previous batch so we can wait for it 6 | let processing: Promise = Promise.resolve(); 7 | 8 | const scheduleBufferTick = () => { 9 | timeoutId = self.setTimeout(async () => { 10 | // we wait until the previous batch is entirely processed so events are processed in order 11 | await processing; 12 | 13 | if (events.length > 0) { 14 | processing = Promise.resolve(cb(events)); 15 | } 16 | 17 | timeoutId = undefined; 18 | events = []; 19 | }, timeInMs); 20 | }; 21 | 22 | return (...args: T) => { 23 | events.push(args); 24 | 25 | if (!timeoutId) { 26 | scheduleBufferTick(); 27 | } 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /app/utils/classNames.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018 Jed Watson. 3 | * Licensed under the MIT License (MIT), see: 4 | * 5 | * @link http://jedwatson.github.io/classnames 6 | */ 7 | 8 | type ClassNamesArg = undefined | string | Record | ClassNamesArg[]; 9 | 10 | /** 11 | * A simple JavaScript utility for conditionally joining classNames together. 12 | * 13 | * @param args A series of classes or object with key that are class and values 14 | * that are interpreted as boolean to decide whether or not the class 15 | * should be included in the final class. 16 | */ 17 | export function classNames(...args: ClassNamesArg[]): string { 18 | let classes = ''; 19 | 20 | for (const arg of args) { 21 | classes = appendClass(classes, parseValue(arg)); 22 | } 23 | 24 | return classes; 25 | } 26 | 27 | function parseValue(arg: ClassNamesArg) { 28 | if (typeof arg === 'string' || typeof arg === 'number') { 29 | return arg; 30 | } 31 | 32 | if (typeof arg !== 'object') { 33 | return ''; 34 | } 35 | 36 | if (Array.isArray(arg)) { 37 | return classNames(...arg); 38 | } 39 | 40 | let classes = ''; 41 | 42 | for (const key in arg) { 43 | if (arg[key]) { 44 | classes = appendClass(classes, key); 45 | } 46 | } 47 | 48 | return classes; 49 | } 50 | 51 | function appendClass(value: string, newClass: string | undefined) { 52 | if (!newClass) { 53 | return value; 54 | } 55 | 56 | if (value) { 57 | return value + ' ' + newClass; 58 | } 59 | 60 | return value + newClass; 61 | } 62 | -------------------------------------------------------------------------------- /app/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce(fn: (...args: Args) => void, delay = 100) { 2 | if (delay === 0) { 3 | return fn; 4 | } 5 | 6 | let timer: number | undefined; 7 | 8 | return function (this: U, ...args: Args) { 9 | const context = this; 10 | 11 | clearTimeout(timer); 12 | 13 | timer = window.setTimeout(() => { 14 | fn.apply(context, args); 15 | }, delay); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /app/utils/diff.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { extractRelativePath } from './diff'; 3 | import { WORK_DIR } from './constants'; 4 | 5 | describe('Diff', () => { 6 | it('should strip out Work_dir', () => { 7 | const filePath = `${WORK_DIR}/index.js`; 8 | const result = extractRelativePath(filePath); 9 | expect(result).toBe('index.js'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/utils/easings.ts: -------------------------------------------------------------------------------- 1 | import { cubicBezier } from 'framer-motion'; 2 | 3 | export const cubicEasingFn = cubicBezier(0.4, 0, 0.2, 1); 4 | -------------------------------------------------------------------------------- /app/utils/folderImport.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from 'ai'; 2 | import { generateId } from './fileUtils'; 3 | import { detectProjectCommands, createCommandsMessage } from './projectCommands'; 4 | 5 | export const createChatFromFolder = async ( 6 | files: File[], 7 | binaryFiles: string[], 8 | folderName: string, 9 | ): Promise => { 10 | const fileArtifacts = await Promise.all( 11 | files.map(async (file) => { 12 | return new Promise<{ content: string; path: string }>((resolve, reject) => { 13 | const reader = new FileReader(); 14 | 15 | reader.onload = () => { 16 | const content = reader.result as string; 17 | const relativePath = file.webkitRelativePath.split('/').slice(1).join('/'); 18 | resolve({ 19 | content, 20 | path: relativePath, 21 | }); 22 | }; 23 | reader.onerror = reject; 24 | reader.readAsText(file); 25 | }); 26 | }), 27 | ); 28 | 29 | const commands = await detectProjectCommands(fileArtifacts); 30 | const commandsMessage = createCommandsMessage(commands); 31 | 32 | const binaryFilesMessage = 33 | binaryFiles.length > 0 34 | ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}` 35 | : ''; 36 | 37 | const filesMessage: Message = { 38 | role: 'assistant', 39 | content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage} 40 | 41 | 42 | ${fileArtifacts 43 | .map( 44 | (file) => ` 45 | ${file.content} 46 | `, 47 | ) 48 | .join('\n\n')} 49 | `, 50 | id: generateId(), 51 | createdAt: new Date(), 52 | }; 53 | 54 | const userMessage: Message = { 55 | role: 'user', 56 | id: generateId(), 57 | content: `Import the "${folderName}" folder`, 58 | createdAt: new Date(), 59 | }; 60 | 61 | const messages = [userMessage, filesMessage]; 62 | 63 | if (commandsMessage) { 64 | messages.push(commandsMessage); 65 | } 66 | 67 | return messages; 68 | }; 69 | -------------------------------------------------------------------------------- /app/utils/markdown.ts: -------------------------------------------------------------------------------- 1 | import rehypeRaw from 'rehype-raw'; 2 | import remarkGfm from 'remark-gfm'; 3 | import type { PluggableList, Plugin } from 'unified'; 4 | import rehypeSanitize, { defaultSchema, type Options as RehypeSanitizeOptions } from 'rehype-sanitize'; 5 | import { SKIP, visit } from 'unist-util-visit'; 6 | import type { UnistNode, UnistParent } from 'node_modules/unist-util-visit/lib'; 7 | 8 | export const allowedHTMLElements = [ 9 | 'a', 10 | 'b', 11 | 'blockquote', 12 | 'br', 13 | 'code', 14 | 'dd', 15 | 'del', 16 | 'details', 17 | 'div', 18 | 'dl', 19 | 'dt', 20 | 'em', 21 | 'h1', 22 | 'h2', 23 | 'h3', 24 | 'h4', 25 | 'h5', 26 | 'h6', 27 | 'hr', 28 | 'i', 29 | 'ins', 30 | 'kbd', 31 | 'li', 32 | 'ol', 33 | 'p', 34 | 'pre', 35 | 'q', 36 | 'rp', 37 | 'rt', 38 | 'ruby', 39 | 's', 40 | 'samp', 41 | 'source', 42 | 'span', 43 | 'strike', 44 | 'strong', 45 | 'sub', 46 | 'summary', 47 | 'sup', 48 | 'table', 49 | 'tbody', 50 | 'td', 51 | 'tfoot', 52 | 'th', 53 | 'thead', 54 | 'tr', 55 | 'ul', 56 | 'var', 57 | ]; 58 | 59 | const rehypeSanitizeOptions: RehypeSanitizeOptions = { 60 | ...defaultSchema, 61 | tagNames: allowedHTMLElements, 62 | attributes: { 63 | ...defaultSchema.attributes, 64 | div: [...(defaultSchema.attributes?.div ?? []), 'data*', ['className', '__boltArtifact__']], 65 | }, 66 | strip: [], 67 | }; 68 | 69 | export function remarkPlugins(limitedMarkdown: boolean) { 70 | const plugins: PluggableList = [remarkGfm]; 71 | 72 | if (limitedMarkdown) { 73 | plugins.unshift(limitedMarkdownPlugin); 74 | } 75 | 76 | return plugins; 77 | } 78 | 79 | export function rehypePlugins(html: boolean) { 80 | const plugins: PluggableList = []; 81 | 82 | if (html) { 83 | plugins.push(rehypeRaw, [rehypeSanitize, rehypeSanitizeOptions]); 84 | } 85 | 86 | return plugins; 87 | } 88 | 89 | const limitedMarkdownPlugin: Plugin = () => { 90 | return (tree, file) => { 91 | const contents = file.toString(); 92 | 93 | visit(tree, (node: UnistNode, index, parent: UnistParent) => { 94 | if ( 95 | index == null || 96 | ['paragraph', 'text', 'inlineCode', 'code', 'strong', 'emphasis'].includes(node.type) || 97 | !node.position 98 | ) { 99 | return true; 100 | } 101 | 102 | let value = contents.slice(node.position.start.offset, node.position.end.offset); 103 | 104 | if (node.type === 'heading') { 105 | value = `\n${value}`; 106 | } 107 | 108 | parent.children[index] = { 109 | type: 'text', 110 | value, 111 | } as any; 112 | 113 | return [SKIP, index] as const; 114 | }); 115 | }; 116 | }; 117 | -------------------------------------------------------------------------------- /app/utils/mobile.ts: -------------------------------------------------------------------------------- 1 | export function isMobile() { 2 | // we use sm: as the breakpoint for mobile. It's currently set to 640px 3 | return globalThis.innerWidth < 640; 4 | } 5 | -------------------------------------------------------------------------------- /app/utils/projectCommands.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from 'ai'; 2 | import { generateId } from './fileUtils'; 3 | 4 | export interface ProjectCommands { 5 | type: string; 6 | setupCommand: string; 7 | followupMessage: string; 8 | } 9 | 10 | interface FileContent { 11 | content: string; 12 | path: string; 13 | } 14 | 15 | export async function detectProjectCommands(files: FileContent[]): Promise { 16 | const hasFile = (name: string) => files.some((f) => f.path.endsWith(name)); 17 | 18 | if (hasFile('package.json')) { 19 | const packageJsonFile = files.find((f) => f.path.endsWith('package.json')); 20 | 21 | if (!packageJsonFile) { 22 | return { type: '', setupCommand: '', followupMessage: '' }; 23 | } 24 | 25 | try { 26 | const packageJson = JSON.parse(packageJsonFile.content); 27 | const scripts = packageJson?.scripts || {}; 28 | 29 | // Check for preferred commands in priority order 30 | const preferredCommands = ['dev', 'start', 'preview']; 31 | const availableCommand = preferredCommands.find((cmd) => scripts[cmd]); 32 | 33 | if (availableCommand) { 34 | return { 35 | type: 'Node.js', 36 | setupCommand: `npm install && npm run ${availableCommand}`, 37 | followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`, 38 | }; 39 | } 40 | 41 | return { 42 | type: 'Node.js', 43 | setupCommand: 'npm install', 44 | followupMessage: 45 | 'Would you like me to inspect package.json to determine the available scripts for running this project?', 46 | }; 47 | } catch (error) { 48 | console.error('Error parsing package.json:', error); 49 | return { type: '', setupCommand: '', followupMessage: '' }; 50 | } 51 | } 52 | 53 | if (hasFile('index.html')) { 54 | return { 55 | type: 'Static', 56 | setupCommand: 'npx --yes serve', 57 | followupMessage: '', 58 | }; 59 | } 60 | 61 | return { type: '', setupCommand: '', followupMessage: '' }; 62 | } 63 | 64 | export function createCommandsMessage(commands: ProjectCommands): Message | null { 65 | if (!commands.setupCommand) { 66 | return null; 67 | } 68 | 69 | return { 70 | role: 'assistant', 71 | content: ` 72 | 73 | 74 | ${commands.setupCommand} 75 | 76 | ${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}`, 77 | id: generateId(), 78 | createdAt: new Date(), 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /app/utils/promises.ts: -------------------------------------------------------------------------------- 1 | export function withResolvers(): PromiseWithResolvers { 2 | if (typeof Promise.withResolvers === 'function') { 3 | return Promise.withResolvers(); 4 | } 5 | 6 | let resolve!: (value: T | PromiseLike) => void; 7 | let reject!: (reason?: any) => void; 8 | 9 | const promise = new Promise((_resolve, _reject) => { 10 | resolve = _resolve; 11 | reject = _reject; 12 | }); 13 | 14 | return { 15 | resolve, 16 | reject, 17 | promise, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /app/utils/react.ts: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | export const genericMemo: >( 4 | component: T, 5 | propsAreEqual?: (prevProps: React.ComponentProps, nextProps: React.ComponentProps) => boolean, 6 | ) => T & { displayName?: string } = memo; 7 | -------------------------------------------------------------------------------- /app/utils/sampler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a function that samples calls at regular intervals and captures trailing calls. 3 | * - Drops calls that occur between sampling intervals 4 | * - Takes one call per sampling interval if available 5 | * - Captures the last call if no call was made during the interval 6 | * 7 | * @param fn The function to sample 8 | * @param sampleInterval How often to sample calls (in ms) 9 | * @returns The sampled function 10 | */ 11 | export function createSampler any>(fn: T, sampleInterval: number): T { 12 | let lastArgs: Parameters | null = null; 13 | let lastTime = 0; 14 | let timeout: NodeJS.Timeout | null = null; 15 | 16 | // Create a function with the same type as the input function 17 | const sampled = function (this: any, ...args: Parameters) { 18 | const now = Date.now(); 19 | lastArgs = args; 20 | 21 | // If we're within the sample interval, just store the args 22 | if (now - lastTime < sampleInterval) { 23 | // Set up trailing call if not already set 24 | if (!timeout) { 25 | timeout = setTimeout( 26 | () => { 27 | timeout = null; 28 | lastTime = Date.now(); 29 | 30 | if (lastArgs) { 31 | fn.apply(this, lastArgs); 32 | lastArgs = null; 33 | } 34 | }, 35 | sampleInterval - (now - lastTime), 36 | ); 37 | } 38 | 39 | return; 40 | } 41 | 42 | // If we're outside the interval, execute immediately 43 | lastTime = now; 44 | fn.apply(this, args); 45 | lastArgs = null; 46 | } as T; 47 | 48 | return sampled; 49 | } 50 | -------------------------------------------------------------------------------- /app/utils/stacktrace.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cleans webcontainer URLs from stack traces to show relative paths instead 3 | */ 4 | export function cleanStackTrace(stackTrace: string): string { 5 | // Function to clean a single URL 6 | const cleanUrl = (url: string): string => { 7 | const regex = /^https?:\/\/[^\/]+\.webcontainer-api\.io(\/.*)?$/; 8 | 9 | if (!regex.test(url)) { 10 | return url; 11 | } 12 | 13 | const pathRegex = /^https?:\/\/[^\/]+\.webcontainer-api\.io\/(.*?)$/; 14 | const match = url.match(pathRegex); 15 | 16 | return match?.[1] || ''; 17 | }; 18 | 19 | // Split the stack trace into lines and process each line 20 | return stackTrace 21 | .split('\n') 22 | .map((line) => { 23 | // Match any URL in the line that contains webcontainer-api.io 24 | return line.replace(/(https?:\/\/[^\/]+\.webcontainer-api\.io\/[^\s\)]+)/g, (match) => cleanUrl(match)); 25 | }) 26 | .join('\n'); 27 | } 28 | -------------------------------------------------------------------------------- /app/utils/stripIndent.ts: -------------------------------------------------------------------------------- 1 | export function stripIndents(value: string): string; 2 | export function stripIndents(strings: TemplateStringsArray, ...values: any[]): string; 3 | export function stripIndents(arg0: string | TemplateStringsArray, ...values: any[]) { 4 | if (typeof arg0 !== 'string') { 5 | const processedString = arg0.reduce((acc, curr, i) => { 6 | acc += curr + (values[i] ?? ''); 7 | return acc; 8 | }, ''); 9 | 10 | return _stripIndents(processedString); 11 | } 12 | 13 | return _stripIndents(arg0); 14 | } 15 | 16 | function _stripIndents(value: string) { 17 | return value 18 | .split('\n') 19 | .map((line) => line.trim()) 20 | .join('\n') 21 | .trimStart() 22 | .replace(/[\r\n]$/, ''); 23 | } 24 | -------------------------------------------------------------------------------- /app/utils/terminal.ts: -------------------------------------------------------------------------------- 1 | const reset = '\x1b[0m'; 2 | 3 | export const escapeCodes = { 4 | reset, 5 | clear: '\x1b[g', 6 | red: '\x1b[1;31m', 7 | }; 8 | 9 | export const coloredText = { 10 | red: (text: string) => `${escapeCodes.red}${text}${reset}`, 11 | }; 12 | -------------------------------------------------------------------------------- /app/utils/types.ts: -------------------------------------------------------------------------------- 1 | interface OllamaModelDetails { 2 | parent_model: string; 3 | format: string; 4 | family: string; 5 | families: string[]; 6 | parameter_size: string; 7 | quantization_level: string; 8 | } 9 | 10 | export interface OllamaModel { 11 | name: string; 12 | model: string; 13 | modified_at: string; 14 | size: number; 15 | digest: string; 16 | details: OllamaModelDetails; 17 | } 18 | 19 | export interface OllamaApiResponse { 20 | models: OllamaModel[]; 21 | } 22 | -------------------------------------------------------------------------------- /app/utils/unreachable.ts: -------------------------------------------------------------------------------- 1 | export function unreachable(message: string): never { 2 | throw new Error(`Unreachable: ${message}`); 3 | } 4 | -------------------------------------------------------------------------------- /app/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare const __COMMIT_HASH: string; 2 | declare const __APP_VERSION: string; 3 | -------------------------------------------------------------------------------- /bindings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bindings="" 4 | 5 | while IFS= read -r line || [ -n "$line" ]; do 6 | if [[ ! "$line" =~ ^# ]] && [[ -n "$line" ]]; then 7 | name=$(echo "$line" | cut -d '=' -f 1) 8 | value=$(echo "$line" | cut -d '=' -f 2-) 9 | value=$(echo $value | sed 's/^"\(.*\)"$/\1/') 10 | bindings+="--binding ${name}=${value} " 11 | fi 12 | done < .env.local 13 | 14 | bindings=$(echo $bindings | sed 's/[[:space:]]*$//') 15 | 16 | echo $bindings 17 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Release v0.0.3 2 | 3 | ### 🔄 Changes since v0.0.2 4 | 5 | #### 🐛 Bug Fixes 6 | 7 | - Prompt Enhance 8 | 9 | 10 | #### 📚 Documentation 11 | 12 | - miniflare error knowledge 13 | 14 | 15 | #### 🔧 Chores 16 | 17 | - adding back semantic pull pr check for better changelog system 18 | - update commit hash to 1e72d52278730f7d22448be9d5cf2daf12559486 19 | - update commit hash to 282beb96e2ee92ba8b1174aaaf9f270e03a288e8 20 | 21 | 22 | #### 🔍 Other Changes 23 | 24 | - Merge remote-tracking branch 'upstream/main' 25 | - Merge pull request #781 from thecodacus/semantic-pull-pr 26 | - miniflare and wrangler error 27 | - simplified the fix 28 | - Merge branch 'main' into fix/prompt-enhance 29 | 30 | 31 | **Full Changelog**: [`v0.0.2..v0.0.3`](https://github.com/stackblitz-labs/bolt.diy/compare/v0.0.2...v0.0.3) 32 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | app-prod: 3 | image: bolt-ai:production 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | target: bolt-ai-production 8 | ports: 9 | - "5173:5173" 10 | env_file: ".env.local" 11 | environment: 12 | - NODE_ENV=production 13 | - COMPOSE_PROFILES=production 14 | # No strictly needed but serving as hints for Coolify 15 | - PORT=5173 16 | - GROQ_API_KEY=${GROQ_API_KEY} 17 | - HuggingFace_API_KEY=${HuggingFace_API_KEY} 18 | - OPENAI_API_KEY=${OPENAI_API_KEY} 19 | - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} 20 | - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} 21 | - GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} 22 | - OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} 23 | - TOGETHER_API_KEY=${TOGETHER_API_KEY} 24 | - TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} 25 | - VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug} 26 | - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768} 27 | - RUNNING_IN_DOCKER=true 28 | extra_hosts: 29 | - "host.docker.internal:host-gateway" 30 | command: pnpm run dockerstart 31 | profiles: 32 | - production 33 | 34 | app-dev: 35 | image: bolt-ai:development 36 | build: 37 | target: bolt-ai-development 38 | environment: 39 | - NODE_ENV=development 40 | - VITE_HMR_PROTOCOL=ws 41 | - VITE_HMR_HOST=localhost 42 | - VITE_HMR_PORT=5173 43 | - CHOKIDAR_USEPOLLING=true 44 | - WATCHPACK_POLLING=true 45 | - PORT=5173 46 | - GROQ_API_KEY=${GROQ_API_KEY} 47 | - HuggingFace_API_KEY=${HuggingFace_API_KEY} 48 | - OPENAI_API_KEY=${OPENAI_API_KEY} 49 | - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} 50 | - OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} 51 | - GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} 52 | - OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} 53 | - TOGETHER_API_KEY=${TOGETHER_API_KEY} 54 | - TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} 55 | - VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug} 56 | - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768} 57 | - RUNNING_IN_DOCKER=true 58 | extra_hosts: 59 | - "host.docker.internal:host-gateway" 60 | volumes: 61 | - type: bind 62 | source: . 63 | target: /app 64 | consistency: cached 65 | - /app/node_modules 66 | ports: 67 | - "5173:5173" 68 | command: pnpm run dev --host 0.0.0.0 69 | profiles: ["development", "default"] 70 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | site/ -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/docs/README.md -------------------------------------------------------------------------------- /docs/images/api-key-ui-section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/docs/images/api-key-ui-section.png -------------------------------------------------------------------------------- /docs/images/bolt-settings-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/docs/images/bolt-settings-button.png -------------------------------------------------------------------------------- /docs/images/provider-base-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/docs/images/provider-base-url.png -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: bolt.diy Docs 2 | site_dir: ../site 3 | theme: 4 | name: material 5 | palette: 6 | - scheme: default 7 | toggle: 8 | icon: material/toggle-switch-off-outline 9 | name: Switch to dark mode 10 | - scheme: slate 11 | toggle: 12 | icon: material/toggle-switch 13 | name: Switch to light mode 14 | features: 15 | - navigation.tabs 16 | - navigation.sections 17 | - toc.follow 18 | - toc.integrate 19 | - navigation.top 20 | - search.suggest 21 | - search.highlight 22 | - content.tabs.link 23 | - content.code.annotation 24 | - content.code.copy 25 | # - navigation.instant 26 | # - navigation.tracking 27 | # - navigation.tabs.sticky 28 | # - navigation.expand 29 | # - content.code.annotate 30 | icon: 31 | repo: fontawesome/brands/github 32 | # logo: assets/logo.png 33 | # favicon: assets/logo.png 34 | repo_name: bolt.diy 35 | repo_url: https://github.com/stackblitz-labs/bolt.diy 36 | edit_uri: "" 37 | 38 | extra: 39 | generator: false 40 | social: 41 | - icon: fontawesome/brands/github 42 | link: https://github.com/stackblitz-labs/bolt.diy 43 | name: bolt.diy 44 | - icon: fontawesome/brands/discourse 45 | link: https://thinktank.ottomator.ai/ 46 | name: bolt.diy Discourse 47 | - icon: fontawesome/brands/x-twitter 48 | link: https://x.com/bolt_diy 49 | name: bolt.diy on X 50 | - icon: fontawesome/brands/bluesky 51 | link: https://bsky.app/profile/bolt.diy 52 | name: bolt.diy on Bluesky 53 | 54 | 55 | 56 | 57 | markdown_extensions: 58 | - pymdownx.highlight: 59 | anchor_linenums: true 60 | - pymdownx.inlinehilite 61 | - pymdownx.snippets 62 | - pymdownx.arithmatex: 63 | generic: true 64 | - footnotes 65 | - pymdownx.details 66 | - pymdownx.superfences 67 | - pymdownx.mark 68 | - attr_list -------------------------------------------------------------------------------- /docs/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "docs" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Anirban Kar "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | mkdocs-material = "^9.5.45" 11 | 12 | 13 | [build-system] 14 | requires = ["poetry-core"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import blitzPlugin from '@blitz/eslint-plugin'; 2 | import { jsFileExtensions } from '@blitz/eslint-plugin/dist/configs/javascript.js'; 3 | import { getNamingConventionRule, tsFileExtensions } from '@blitz/eslint-plugin/dist/configs/typescript.js'; 4 | 5 | export default [ 6 | { 7 | ignores: [ 8 | '**/dist', 9 | '**/node_modules', 10 | '**/.wrangler', 11 | '**/bolt/build', 12 | '**/.history', 13 | ], 14 | }, 15 | ...blitzPlugin.configs.recommended(), 16 | { 17 | rules: { 18 | '@blitz/catch-error-name': 'off', 19 | '@typescript-eslint/no-this-alias': 'off', 20 | '@typescript-eslint/no-empty-object-type': 'off', 21 | '@blitz/comment-syntax': 'off', 22 | '@blitz/block-scope-case': 'off', 23 | 'array-bracket-spacing': ["error", "never"], 24 | 'object-curly-newline': ["error", { "consistent": true }], 25 | 'keyword-spacing': ["error", { "before": true, "after": true }], 26 | 'consistent-return': "error", 27 | 'semi': ["error", "always"], 28 | 'curly': ["error"], 29 | 'no-eval': ["error"], 30 | 'linebreak-style': ["error", "unix"], 31 | 'arrow-spacing': ["error", { "before": true, "after": true }] 32 | }, 33 | }, 34 | { 35 | files: ['**/*.tsx'], 36 | rules: { 37 | ...getNamingConventionRule({}, true), 38 | }, 39 | }, 40 | { 41 | files: ['**/*.d.ts'], 42 | rules: { 43 | '@typescript-eslint/no-empty-object-type': 'off', 44 | }, 45 | }, 46 | { 47 | files: [...tsFileExtensions, ...jsFileExtensions, '**/*.tsx'], 48 | ignores: ['functions/*'], 49 | rules: { 50 | 'no-restricted-imports': [ 51 | 'error', 52 | { 53 | patterns: [ 54 | { 55 | group: ['../'], 56 | message: 'Relative imports are not allowed. Please use \'~/\' instead.', 57 | }, 58 | ], 59 | }, 60 | ], 61 | }, 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /functions/[[path]].ts: -------------------------------------------------------------------------------- 1 | import type { ServerBuild } from '@remix-run/cloudflare'; 2 | import { createPagesFunctionHandler } from '@remix-run/cloudflare-pages'; 3 | 4 | // @ts-ignore because the server build file is generated by `remix vite:build` 5 | import * as serverBuild from '../build/server'; 6 | 7 | export const onRequest = createPagesFunctionHandler({ 8 | build: serverBuild as unknown as ServerBuild, 9 | }); 10 | -------------------------------------------------------------------------------- /icons/angular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/astro.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/chat.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /icons/logo-text.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/nativescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/nextjs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/nuxt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/qwik.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/remix.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/remotion.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/slidev.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/stars.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /load-context.ts: -------------------------------------------------------------------------------- 1 | import { type PlatformProxy } from 'wrangler'; 2 | 3 | type Cloudflare = Omit, 'dispose'>; 4 | 5 | declare module '@remix-run/cloudflare' { 6 | interface AppLoadContext { 7 | cloudflare: Cloudflare; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pre-start.cjs: -------------------------------------------------------------------------------- 1 | const { execSync } =require('child_process'); 2 | 3 | // Get git hash with fallback 4 | const getGitHash = () => { 5 | try { 6 | return execSync('git rev-parse --short HEAD').toString().trim(); 7 | } catch { 8 | return 'no-git-info'; 9 | } 10 | }; 11 | 12 | let commitJson = { 13 | hash: JSON.stringify(getGitHash()), 14 | version: JSON.stringify(process.env.npm_package_version), 15 | }; 16 | 17 | console.log(` 18 | ★═══════════════════════════════════════★ 19 | B O L T . D I Y 20 | ⚡️ Welcome ⚡️ 21 | ★═══════════════════════════════════════★ 22 | `); 23 | console.log('📍 Current Version Tag:', `v${commitJson.version}`); 24 | console.log('📍 Current Commit Version:', commitJson.hash); 25 | console.log(' Please wait until the URL appears here'); 26 | console.log('★═══════════════════════════════════════★'); 27 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/Anthropic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/Cohere.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/Deepseek.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/icons/Default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/Google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/Groq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/HuggingFace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/LMStudio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/icons/Mistral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/Ollama.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/OpenAI.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/OpenAILike.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/OpenRouter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/Perplexity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/Together.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/xAI.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/logo-dark-styled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/public/logo-dark-styled.png -------------------------------------------------------------------------------- /public/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/public/logo-dark.png -------------------------------------------------------------------------------- /public/logo-light-styled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/public/logo-light-styled.png -------------------------------------------------------------------------------- /public/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/public/logo-light.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/social_preview_index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonderwhy-er/bolt.myaibuilt.app_hosted-Bolt.New-oTToDev/1321d26c5c19c61beec2901cc94759ece9d326d8/public/social_preview_index.jpg -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 4 | "types": ["@remix-run/cloudflare", "vite/client", "@cloudflare/workers-types/2023-07-01", "@types/dom-speech-recognition"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "module": "ESNext", 9 | "moduleResolution": "Bundler", 10 | "resolveJsonModule": true, 11 | "target": "ESNext", 12 | "strict": true, 13 | "allowJs": true, 14 | "skipLibCheck": true, 15 | "verbatimModuleSyntax": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "~/*": ["./app/*"] 20 | }, 21 | 22 | // vite takes care of building everything, not tsc 23 | "noEmit": true 24 | }, 25 | "include": [ 26 | "**/*.ts", 27 | "**/*.tsx", 28 | "**/.server/**/*.ts", 29 | "**/.server/**/*.tsx", 30 | "**/.client/**/*.ts", 31 | "**/.client/**/*.tsx" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /types/istextorbinary.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @note For some reason the types aren't picked up from node_modules so I declared the module here 3 | * with only the function that we use. 4 | */ 5 | declare module 'istextorbinary' { 6 | export interface EncodingOpts { 7 | /** Defaults to 24 */ 8 | chunkLength?: number; 9 | 10 | /** If not provided, will check the start, beginning, and end */ 11 | chunkBegin?: number; 12 | } 13 | 14 | export function getEncoding(buffer: Buffer | null, opts?: EncodingOpts): 'utf8' | 'binary' | null; 15 | } 16 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, vitePlugin as remixVitePlugin } from '@remix-run/dev'; 2 | import UnoCSS from 'unocss/vite'; 3 | import { defineConfig, type ViteDevServer } from 'vite'; 4 | import { nodePolyfills } from 'vite-plugin-node-polyfills'; 5 | import { optimizeCssModules } from 'vite-plugin-optimize-css-modules'; 6 | import tsconfigPaths from 'vite-tsconfig-paths'; 7 | 8 | import { execSync } from 'child_process'; 9 | 10 | // Get git hash with fallback 11 | const getGitHash = () => { 12 | try { 13 | return execSync('git rev-parse --short HEAD').toString().trim(); 14 | } catch { 15 | return 'no-git-info'; 16 | } 17 | }; 18 | 19 | 20 | export default defineConfig((config) => { 21 | return { 22 | define: { 23 | __COMMIT_HASH: JSON.stringify(getGitHash()), 24 | __APP_VERSION: JSON.stringify(process.env.npm_package_version), 25 | }, 26 | build: { 27 | target: 'esnext', 28 | }, 29 | plugins: [ 30 | nodePolyfills({ 31 | include: ['path', 'buffer'], 32 | }), 33 | config.mode !== 'test' && remixCloudflareDevProxy(), 34 | remixVitePlugin({ 35 | future: { 36 | v3_fetcherPersist: true, 37 | v3_relativeSplatPath: true, 38 | v3_throwAbortReason: true, 39 | v3_lazyRouteDiscovery: true 40 | }, 41 | }), 42 | UnoCSS(), 43 | tsconfigPaths(), 44 | chrome129IssuePlugin(), 45 | config.mode === 'production' && optimizeCssModules({ apply: 'build' }), 46 | ], 47 | envPrefix: ["VITE_","OPENAI_LIKE_API_BASE_URL", "OLLAMA_API_BASE_URL", "LMSTUDIO_API_BASE_URL","TOGETHER_API_BASE_URL"], 48 | css: { 49 | preprocessorOptions: { 50 | scss: { 51 | api: 'modern-compiler', 52 | }, 53 | }, 54 | }, 55 | }; 56 | }); 57 | 58 | function chrome129IssuePlugin() { 59 | return { 60 | name: 'chrome129IssuePlugin', 61 | configureServer(server: ViteDevServer) { 62 | server.middlewares.use((req, res, next) => { 63 | const raw = req.headers['user-agent']?.match(/Chrom(e|ium)\/([0-9]+)\./); 64 | 65 | if (raw) { 66 | const version = parseInt(raw[2], 10); 67 | 68 | if (version === 129) { 69 | res.setHeader('content-type', 'text/html'); 70 | res.end( 71 | '

Please use Chrome Canary for testing.

Chrome 129 has an issue with JavaScript modules & Vite local development, see for more information.

Note: This only impacts local development. `pnpm run build` and `pnpm run start` will work fine in this browser.

', 72 | ); 73 | 74 | return; 75 | } 76 | } 77 | 78 | next(); 79 | }); 80 | }, 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | interface Env { 2 | DEFAULT_NUM_CTX:Settings; 3 | ANTHROPIC_API_KEY: string; 4 | OPENAI_API_KEY: string; 5 | GROQ_API_KEY: string; 6 | HuggingFace_API_KEY: string; 7 | OPEN_ROUTER_API_KEY: string; 8 | OLLAMA_API_BASE_URL: string; 9 | OPENAI_LIKE_API_KEY: string; 10 | OPENAI_LIKE_API_BASE_URL: string; 11 | TOGETHER_API_KEY: string; 12 | TOGETHER_API_BASE_URL: string; 13 | DEEPSEEK_API_KEY: string; 14 | LMSTUDIO_API_BASE_URL: string; 15 | GOOGLE_GENERATIVE_AI_API_KEY: string; 16 | MISTRAL_API_KEY: string; 17 | XAI_API_KEY: string; 18 | PERPLEXITY_API_KEY: string; 19 | } 20 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | #:schema node_modules/wrangler/config-schema.json 2 | name = "bolt" 3 | compatibility_flags = ["nodejs_compat"] 4 | compatibility_date = "2024-07-01" 5 | pages_build_output_dir = "./build/client" 6 | send_metrics = false 7 | --------------------------------------------------------------------------------