diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..5274ff0128c0fd56a256e7226df1c5e0e65a4284 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 120 +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..d5cfc5cb32adfc110f50c2d1e99d396acd6e4026 --- /dev/null +++ b/.env.example @@ -0,0 +1,44 @@ +# Rename this file to .env.local once you have filled in the below environment variables! + +# Get your GROQ API Key here - +# https://console.groq.com/keys +# You only need this environment variable set if you want to use Groq models +GROQ_API_KEY= + +# Get your Open AI API Key by following these instructions - +# https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key +# You only need this environment variable set if you want to use GPT models +OPENAI_API_KEY= + +# Get your Anthropic API Key in your account settings - +# https://console.anthropic.com/settings/keys +# You only need this environment variable set if you want to use Claude models +ANTHROPIC_API_KEY= + +# Get your OpenRouter API Key in your account settings - +# https://openrouter.ai/settings/keys +# You only need this environment variable set if you want to use OpenRouter models +OPEN_ROUTER_API_KEY= + +# Get your Google Generative AI API Key by following these instructions - +# https://console.cloud.google.com/apis/credentials +# You only need this environment variable set if you want to use Google Generative AI models +GOOGLE_GENERATIVE_AI_API_KEY= + +# You only need this environment variable set if you want to use oLLAMA models +# EXAMPLE http://localhost:11434 +OLLAMA_API_BASE_URL= + +# You only need this environment variable set if you want to use OpenAI Like models +OPENAI_LIKE_API_BASE_URL= + +# Get your OpenAI Like API Key +OPENAI_LIKE_API_KEY= + +# Get your Mistral API Key by following these instructions - +# https://console.mistral.ai/api-keys/ +# You only need this environment variable set if you want to use Mistral models +MISTRAL_API_KEY= + +# Include this environment variable if you want more logging for debugging locally +VITE_LOG_LEVEL=debug diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000000000000000000000000000000000..a594bc8724f9e6decde8da702d6e4cbb6a0850cf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,63 @@ +name: "Bug report" +description: Create a report to help us improve +body: + - type: markdown + attributes: + value: | + Thank you for reporting an issue :pray:. + + This issue tracker is for bugs and issues found with [Bolt.new](https://bolt.new). + 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. + + The more information you fill in, the better we can help you. + - type: textarea + id: description + attributes: + label: Describe the bug + description: Provide a clear and concise description of what you're running into. + validations: + required: true + - type: input + id: link + attributes: + label: Link to the Bolt URL that caused the error + description: Please do not delete it after reporting! + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Describe the steps we have to take to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: Provide a clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screen Recording / Screenshot + description: If applicable, **please include a screen recording** (preferably) or screenshot showcasing the issue. This will assist us in resolving your issue quickly. + - type: textarea + id: platform + attributes: + label: Platform + value: | + - OS: [e.g. macOS, Windows, Linux] + - Browser: [e.g. Chrome, Safari, Firefox] + - Version: [e.g. 91.1] + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000000000000000000000000000000000..6cd1e7471ee097b93a2da26b9f5c0f80df3ad550 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' +--- + +**Is your feature request related to a problem? Please describe:** + + + +**Describe the solution you'd like:** + + + +**Describe alternatives you've considered:** + + + +**Additional context:** + + diff --git a/.github/actions/setup-and-build/action.yaml b/.github/actions/setup-and-build/action.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b27bc6fb8e30115679ea27e2d1600cbe8826d380 --- /dev/null +++ b/.github/actions/setup-and-build/action.yaml @@ -0,0 +1,32 @@ +name: Setup and Build +description: Generic setup action +inputs: + pnpm-version: + required: false + type: string + default: '9.4.0' + node-version: + required: false + type: string + default: '20.15.1' + +runs: + using: composite + + steps: + - uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.pnpm-version }} + run_install: false + + - name: Set Node.js version to ${{ inputs.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: pnpm + + - name: Install dependencies and build project + shell: bash + run: | + pnpm install + pnpm run build diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8ab236d587c6bdd122a48ea7f89cf49665845a99 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,27 @@ +name: CI/CD + +on: + push: + branches: + - master + pull_request: + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup and Build + uses: ./.github/actions/setup-and-build + + - name: Run type check + run: pnpm run typecheck + + # - name: Run ESLint + # run: pnpm run lint + + - name: Run tests + run: pnpm run test diff --git a/.github/workflows/github-build-push.yml b/.github/workflows/github-build-push.yml new file mode 100644 index 0000000000000000000000000000000000000000..4d4db05d8714c93787e4a764dd86ac4f798d48d1 --- /dev/null +++ b/.github/workflows/github-build-push.yml @@ -0,0 +1,39 @@ +name: Build and Push Container + +on: + push: + branches: + - main + # paths: + # - 'Dockerfile' + workflow_dispatch: +jobs: + build-and-push: + runs-on: [ubuntu-latest] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push Containers + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} diff --git a/.github/workflows/semantic-pr.yaml b/.github/workflows/semantic-pr.yaml new file mode 100644 index 0000000000000000000000000000000000000000..503b045524e650bc8a980fcd7238f6e296e58d89 --- /dev/null +++ b/.github/workflows/semantic-pr.yaml @@ -0,0 +1,32 @@ +name: Semantic Pull Request +on: + pull_request_target: + types: [opened, reopened, edited, synchronize] +permissions: + pull-requests: read +jobs: + main: + name: Validate PR Title + runs-on: ubuntu-latest + steps: + # https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.3 + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + subjectPattern: ^(?![A-Z]).+$ + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the subject + doesn't start with an uppercase character. + types: | + fix + feat + chore + build + ci + perf + docs + refactor + revert + test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f141cc0ef5a4b28b6805ff8fe76dd4cf840477a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +.vscode/* +.vscode/launch.json +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +/.cache +/build +.env.local +.env +*.vars +.wrangler +_worker.bundle diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000000000000000000000000000000000000..d821bbc58de2bc4e96ebb214d7fb895d3b0c12a2 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +. "$(dirname "$0")/_/husky.sh" + +npx commitlint --edit $1 + +exit 0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000000000000000000000000000000000..3a08d6e2b8cd539347e0ece9923808c6a05c99f6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +pnpm-lock.yaml +.astro diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..8d3dfb047c8c168b52ced5119a342a3ff2339806 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 120, + "singleQuote": true, + "useTabs": false, + "tabWidth": 2, + "semi": true, + "bracketSpacing": true +} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000000000000000000000000000000000000..427253d38b7e6af4c3f46184b762d21bfc69153b --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +nodejs 20.15.1 +pnpm 9.4.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..ad3b1951ac4b541c49b9a5b68dc897d4176eaad1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,98 @@ +# Contributing to Bolt.new Fork + +First off, thank you for considering contributing to Bolt.new! This fork aims to expand the capabilities of the original project by integrating multiple LLM providers and enhancing functionality. Every contribution helps make Bolt.new a better tool for developers worldwide. + +## 📋 Table of Contents +- [Code of Conduct](#code-of-conduct) +- [How Can I Contribute?](#how-can-i-contribute) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Coding Standards](#coding-standards) +- [Development Setup](#development-setup) +- [Project Structure](#project-structure) + +## Code of Conduct + +This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers. + +## How Can I Contribute? + +### 🐞 Reporting Bugs and Feature Requests +- Check the issue tracker to avoid duplicates +- Use the issue templates when available +- Include as much relevant information as possible +- For bugs, add steps to reproduce the issue + +### 🔧 Code Contributions +1. Fork the repository +2. Create a new branch for your feature/fix +3. Write your code +4. Submit a pull request + +### ✨ Becoming a Core Contributor +We're looking for dedicated contributors to help maintain and grow this project. If you're interested in becoming a core contributor, please fill out our [Contributor Application Form](https://forms.gle/TBSteXSDCtBDwr5m7). + +## Pull Request Guidelines + +### 📝 PR Checklist +- [ ] Branch from the main branch +- [ ] Update documentation if needed +- [ ] Manually verify all new functionality works as expected +- [ ] Keep PRs focused and atomic + +### 👀 Review Process +1. Manually test the changes +2. At least one maintainer review required +3. Address all review comments +4. Maintain clean commit history + +## Coding Standards + +### 💻 General Guidelines +- Follow existing code style +- Comment complex logic +- Keep functions focused and small +- Use meaningful variable names + +## Development Setup + +### 🔄 Initial Setup +1. Clone the repository: +```bash +git clone https://github.com/coleam00/bolt.new-any-llm.git +``` + +2. Install dependencies: +```bash +pnpm install +``` + +3. Set up environment variables: + - Rename `.env.example` to `.env.local` + - Add your LLM API keys (only set the ones you plan to use): +```bash +GROQ_API_KEY=XXX +OPENAI_API_KEY=XXX +ANTHROPIC_API_KEY=XXX +... +``` + - Optionally set debug level: +```bash +VITE_LOG_LEVEL=debug +``` +**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore. + +### 🚀 Running the Development Server +```bash +pnpm run dev +``` + +**Note**: You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway. + +## Questions? + +For any questions about contributing, please: +1. Check existing documentation +2. Search through issues +3. Create a new issue with the question label + +Thank you for contributing to Bolt.new! 🚀 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..de88d11e93f60f0fb007079b5b068198a71c8381 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Use an official Node.js runtime as the base image +FROM node:20.15.1 + +# Set the working directory in the container +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm@9.4.0 + +# Copy package.json and pnpm-lock.yaml (if available) +COPY package.json pnpm-lock.yaml* ./ + +# Install dependencies +RUN pnpm install + +# Copy the rest of the application code +COPY . . + +# Build the application +RUN pnpm run build + +# Make sure bindings.sh is executable +RUN chmod +x bindings.sh + +# Expose the port the app runs on (adjust if you specified a different port) +EXPOSE 3000 + +# Start the application +CMD ["pnpm", "run", "start"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..79290241f979e4079f5fd7d6fdc9acac6614de2b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 StackBlitz, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/components/chat/Artifact.tsx b/app/components/chat/Artifact.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9de52dd94a3aae0a31b57cec4ae5030f6dc56541 --- /dev/null +++ b/app/components/chat/Artifact.tsx @@ -0,0 +1,213 @@ +import { useStore } from '@nanostores/react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { computed } from 'nanostores'; +import { memo, useEffect, useRef, useState } from 'react'; +import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki'; +import type { ActionState } from '~/lib/runtime/action-runner'; +import { workbenchStore } from '~/lib/stores/workbench'; +import { classNames } from '~/utils/classNames'; +import { cubicEasingFn } from '~/utils/easings'; + +const highlighterOptions = { + langs: ['shell'], + themes: ['light-plus', 'dark-plus'], +}; + +const shellHighlighter: HighlighterGeneric = + import.meta.hot?.data.shellHighlighter ?? (await createHighlighter(highlighterOptions)); + +if (import.meta.hot) { + import.meta.hot.data.shellHighlighter = shellHighlighter; +} + +interface ArtifactProps { + messageId: string; +} + +export const Artifact = memo(({ messageId }: ArtifactProps) => { + const userToggledActions = useRef(false); + const [showActions, setShowActions] = useState(false); + + const artifacts = useStore(workbenchStore.artifacts); + const artifact = artifacts[messageId]; + + const actions = useStore( + computed(artifact.runner.actions, (actions) => { + return Object.values(actions); + }), + ); + + const toggleActions = () => { + userToggledActions.current = true; + setShowActions(!showActions); + }; + + useEffect(() => { + if (actions.length && !showActions && !userToggledActions.current) { + setShowActions(true); + } + }, [actions]); + + return ( +
+
+ +
+ + {actions.length && ( + +
+
+
+
+ )} +
+
+ + {showActions && actions.length > 0 && ( + +
+
+ +
+ + )} + +
+ ); +}); + +interface ShellCodeBlockProps { + classsName?: string; + code: string; +} + +function ShellCodeBlock({ classsName, code }: ShellCodeBlockProps) { + return ( +
+ ); +} + +interface ActionListProps { + actions: ActionState[]; +} + +const actionVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 }, +}; + +const ActionList = memo(({ actions }: ActionListProps) => { + return ( + +
    + {actions.map((action, index) => { + const { status, type, content } = action; + const isLast = index === actions.length - 1; + + return ( + +
    +
    + {status === 'running' ? ( +
    + ) : status === 'pending' ? ( +
    + ) : status === 'complete' ? ( +
    + ) : status === 'failed' || status === 'aborted' ? ( +
    + ) : null} +
    + {type === 'file' ? ( +
    + Create{' '} + + {action.filePath} + +
    + ) : type === 'shell' ? ( +
    + Run command +
    + ) : null} +
    + {type === 'shell' && ( + + )} +
    + ); + })} +
+
+ ); +}); + +function getIconColor(status: ActionState['status']) { + switch (status) { + case 'pending': { + return 'text-bolt-elements-textTertiary'; + } + case 'running': { + return 'text-bolt-elements-loader-progress'; + } + case 'complete': { + return 'text-bolt-elements-icon-success'; + } + case 'aborted': { + return 'text-bolt-elements-textSecondary'; + } + case 'failed': { + return 'text-bolt-elements-icon-error'; + } + default: { + return undefined; + } + } +} diff --git a/app/components/chat/AssistantMessage.tsx b/app/components/chat/AssistantMessage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a5698e9756cf692027952b1184464940c3b473fb --- /dev/null +++ b/app/components/chat/AssistantMessage.tsx @@ -0,0 +1,14 @@ +import { memo } from 'react'; +import { Markdown } from './Markdown'; + +interface AssistantMessageProps { + content: string; +} + +export const AssistantMessage = memo(({ content }: AssistantMessageProps) => { + return ( +
+ {content} +
+ ); +}); diff --git a/app/components/chat/BaseChat.module.scss b/app/components/chat/BaseChat.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..3d6ed4c8617bfaa86a42bdf85fbb9c8dbe885147 --- /dev/null +++ b/app/components/chat/BaseChat.module.scss @@ -0,0 +1,19 @@ +.BaseChat { + &[data-chat-visible='false'] { + --workbench-inner-width: 100%; + --workbench-left: 0; + + .Chat { + --at-apply: bolt-ease-cubic-bezier; + transition-property: transform, opacity; + transition-duration: 0.3s; + will-change: transform, opacity; + transform: translateX(-50%); + opacity: 0; + } + } +} + +.Chat { + opacity: 1; +} diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c1175f700cdabf5e957325922901cedba5b22a53 --- /dev/null +++ b/app/components/chat/BaseChat.tsx @@ -0,0 +1,269 @@ +// @ts-nocheck +// Preventing TS checks with files presented in the video for a better presentation. +import type { Message } from 'ai'; +import React, { type RefCallback } from 'react'; +import { ClientOnly } from 'remix-utils/client-only'; +import { Menu } from '~/components/sidebar/Menu.client'; +import { IconButton } from '~/components/ui/IconButton'; +import { Workbench } from '~/components/workbench/Workbench.client'; +import { classNames } from '~/utils/classNames'; +import { MODEL_LIST, DEFAULT_PROVIDER } from '~/utils/constants'; +import { Messages } from './Messages.client'; +import { SendButton } from './SendButton.client'; +import { useState } from 'react'; + +import styles from './BaseChat.module.scss'; + +const EXAMPLE_PROMPTS = [ + { text: 'Build a todo app in React using Tailwind' }, + { text: 'Build a simple blog using Astro' }, + { text: 'Create a cookie consent form using Material UI' }, + { text: 'Make a space invaders game' }, + { text: 'How do I center a div?' }, +]; + +const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))] + +const ModelSelector = ({ model, setModel, modelList, providerList }) => { + const [provider, setProvider] = useState(DEFAULT_PROVIDER); + return ( +
+ + +
+ ); +}; + +const TEXTAREA_MIN_HEIGHT = 76; + +interface BaseChatProps { + textareaRef?: React.RefObject | undefined; + messageRef?: RefCallback | undefined; + scrollRef?: RefCallback | undefined; + showChat?: boolean; + chatStarted?: boolean; + isStreaming?: boolean; + messages?: Message[]; + enhancingPrompt?: boolean; + promptEnhanced?: boolean; + input?: string; + model: string; + setModel: (model: string) => void; + handleStop?: () => void; + sendMessage?: (event: React.UIEvent, messageInput?: string) => void; + handleInputChange?: (event: React.ChangeEvent) => void; + enhancePrompt?: () => void; +} + +export const BaseChat = React.forwardRef( + ( + { + textareaRef, + messageRef, + scrollRef, + showChat = true, + chatStarted = false, + isStreaming = false, + enhancingPrompt = false, + promptEnhanced = false, + messages, + input = '', + model, + setModel, + sendMessage, + handleInputChange, + enhancePrompt, + handleStop, + }, + ref, + ) => { + const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; + + return ( +
+ {() => } +
+
+ {!chatStarted && ( +
+

+ Where ideas begin +

+

+ Bring ideas to life in seconds or get help on existing projects. +

+
+ )} +
+ + {() => { + return chatStarted ? ( + + ) : null; + }} + +
+ +
+