├── docs ├── README.md ├── .python-version ├── .gitignore ├── images │ ├── api-key-ui-section.png │ ├── provider-base-url.png │ └── bolt-settings-button.png ├── pyproject.toml └── mkdocs.yml ├── .prettierignore ├── app ├── types │ ├── theme.ts │ ├── artifact.ts │ ├── template.ts │ ├── terminal.ts │ ├── global.d.ts │ ├── context.ts │ ├── model.ts │ ├── supabase.ts │ ├── netlify.ts │ └── actions.ts ├── styles │ ├── components │ │ ├── terminal.scss │ │ ├── code.scss │ │ ├── toast.scss │ │ └── resize-handle.scss │ ├── z-index.scss │ ├── index.scss │ ├── animations.scss │ └── diff-view.css ├── routes │ ├── api.models.$provider.ts │ ├── api.health.ts │ ├── chat.$id.tsx │ ├── api.update.ts │ ├── git.tsx │ ├── _index.tsx │ ├── api.supabase.variables.ts │ ├── api.check-env-key.ts │ ├── api.system.git-info.ts │ └── api.supabase.ts ├── vite-env.d.ts ├── lib │ ├── persistence │ │ ├── index.ts │ │ ├── types.ts │ │ └── localStorage.ts │ ├── stores │ │ ├── streaming.ts │ │ ├── chat.ts │ │ ├── profile.ts │ │ ├── tabConfigurationStore.ts │ │ ├── theme.ts │ │ ├── terminal.ts │ │ └── netlify.ts │ ├── webcontainer │ │ └── auth.client.ts │ ├── fetch.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useViewport.ts │ │ ├── useLocalProviders.ts │ │ ├── useSearchFilter.ts │ │ ├── useNotifications.ts │ │ ├── useUpdateCheck.ts │ │ └── useConnectionStatus.ts │ ├── .server │ │ └── llm │ │ │ ├── constants.ts │ │ │ └── switchable-stream.ts │ ├── modules │ │ └── llm │ │ │ ├── types.ts │ │ │ ├── registry.ts │ │ │ └── providers │ │ │ ├── xai.ts │ │ │ ├── deepseek.ts │ │ │ ├── perplexity.ts │ │ │ └── github.ts │ ├── api │ │ ├── features.ts │ │ ├── cookies.ts │ │ ├── connection.ts │ │ └── notifications.ts │ ├── common │ │ └── prompt-library.ts │ └── crypto.ts ├── utils │ ├── easings.ts │ ├── unreachable.ts │ ├── mobile.ts │ ├── terminal.ts │ ├── react.ts │ ├── formatSize.ts │ ├── os.ts │ ├── diff.spec.ts │ ├── debounce.ts │ ├── types.ts │ ├── promises.ts │ ├── getLanguageFromExtension.ts │ ├── stripIndent.ts │ ├── buffer.ts │ ├── stacktrace.ts │ ├── path.ts │ ├── classNames.ts │ └── sampler.ts ├── components │ ├── chat │ │ ├── CodeBlock.module.scss │ │ ├── chatExportAndImport │ │ │ └── ExportChatButton.tsx │ │ ├── SpeechRecognition.tsx │ │ ├── ScreenshotStateManager.tsx │ │ ├── FilePreview.tsx │ │ ├── ThoughtBox.tsx │ │ ├── ExamplePrompts.tsx │ │ ├── BaseChat.module.scss │ │ ├── SendButton.client.tsx │ │ ├── StarterTemplates.tsx │ │ ├── UserMessage.tsx │ │ ├── Markdown.spec.ts │ │ └── NetlifyDeploymentLink.client.tsx │ ├── editor │ │ └── codemirror │ │ │ └── BinaryContent.tsx │ ├── ui │ │ ├── Collapsible.tsx │ │ ├── SettingsButton.tsx │ │ ├── PanelHeader.tsx │ │ ├── Separator.tsx │ │ ├── Label.tsx │ │ ├── Progress.tsx │ │ ├── BackgroundRays │ │ │ └── index.tsx │ │ ├── LoadingDots.tsx │ │ ├── ThemeSwitch.tsx │ │ ├── Popover.tsx │ │ ├── Input.tsx │ │ ├── use-toast.ts │ │ ├── PanelHeaderButton.tsx │ │ ├── Badge.tsx │ │ ├── LoadingOverlay.tsx │ │ ├── Switch.tsx │ │ ├── ScrollArea.tsx │ │ ├── Button.tsx │ │ └── Dropdown.tsx │ ├── @settings │ │ ├── index.ts │ │ ├── utils │ │ │ └── animations.ts │ │ └── tabs │ │ │ ├── providers │ │ │ └── service-status │ │ │ │ ├── types.ts │ │ │ │ └── providers │ │ │ │ ├── xai.ts │ │ │ │ ├── deepseek.ts │ │ │ │ └── hyperbolic.ts │ │ │ └── connections │ │ │ └── ConnectionsTab.tsx │ ├── sidebar │ │ └── date-binning.ts │ ├── header │ │ └── Header.tsx │ └── workbench │ │ └── terminal │ │ └── theme.ts └── entry.client.tsx ├── public ├── favicon.ico ├── logo-dark.png ├── logo-light.png ├── apple-touch-icon.png ├── logo-dark-styled.png ├── logo-light-styled.png ├── social_preview_index.jpg ├── apple-touch-icon-precomposed.png ├── icons │ ├── Anthropic.svg │ ├── xAI.svg │ ├── Groq.svg │ ├── OpenRouter.svg │ ├── Mistral.svg │ ├── Cohere.svg │ ├── LMStudio.svg │ ├── Default.svg │ ├── Ollama.svg │ ├── Perplexity.svg │ ├── Google.svg │ ├── Together.svg │ ├── Deepseek.svg │ ├── HuggingFace.svg │ ├── OpenAILike.svg │ ├── OpenAI.svg │ └── AmazonBedrock.svg └── favicon.svg ├── assets ├── icons │ ├── icon.ico │ ├── icon.png │ └── icon.icns └── entitlements.mac.plist ├── electron-update.yml ├── electron ├── main │ ├── utils │ │ ├── store.ts │ │ ├── constants.ts │ │ ├── reload.ts │ │ ├── cookie.ts │ │ └── vite-server.ts │ ├── ui │ │ ├── menu.ts │ │ └── window.ts │ ├── tsconfig.json │ └── vite.config.ts └── preload │ ├── tsconfig.json │ ├── index.ts │ └── vite.config.ts ├── .prettierrc ├── wrangler.toml ├── icons ├── vue.svg ├── logo.svg ├── angular.svg ├── vite.svg ├── remix.svg ├── logo-text.svg ├── nextjs.svg ├── nuxt.svg ├── astro.svg ├── nativescript.svg ├── stars.svg ├── slidev.svg ├── qwik.svg ├── typescript.svg ├── svelte.svg ├── remotion.svg └── chat.svg ├── load-context.ts ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ ├── epic.md │ └── feature.md ├── workflows │ ├── ci.yaml │ ├── docs.yaml │ ├── semantic-pr.yaml │ ├── pr-release-validation.yaml │ ├── stale.yml │ └── docker.yaml └── actions │ └── setup-and-build │ └── action.yaml ├── scripts ├── update-imports.sh ├── clean.js └── update.sh ├── functions └── [[path]].ts ├── .dockerignore ├── types └── istextorbinary.d.ts ├── .gitignore ├── worker-configuration.d.ts ├── pre-start.cjs ├── tsconfig.json ├── notarize.cjs ├── bindings.sh ├── .husky └── pre-commit ├── LICENSE ├── electron-builder.yml └── eslint.config.mjs /docs/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.python-version: -------------------------------------------------------------------------------- 1 | 3.12.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | .astro 3 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | site/ 3 | .python-version -------------------------------------------------------------------------------- /app/types/theme.ts: -------------------------------------------------------------------------------- 1 | export type Theme = 'dark' | 'light'; 2 | -------------------------------------------------------------------------------- /app/styles/components/terminal.scss: -------------------------------------------------------------------------------- 1 | .xterm { 2 | padding: 1rem; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/public/favicon.ico -------------------------------------------------------------------------------- /assets/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/assets/icons/icon.ico -------------------------------------------------------------------------------- /assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/assets/icons/icon.png -------------------------------------------------------------------------------- /public/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/public/logo-dark.png -------------------------------------------------------------------------------- /public/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/public/logo-light.png -------------------------------------------------------------------------------- /app/routes/api.models.$provider.ts: -------------------------------------------------------------------------------- 1 | import { loader } from './api.models'; 2 | export { loader }; 3 | -------------------------------------------------------------------------------- /app/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare const __COMMIT_HASH: string; 2 | declare const __APP_VERSION: string; 3 | -------------------------------------------------------------------------------- /assets/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/assets/icons/icon.icns -------------------------------------------------------------------------------- /electron-update.yml: -------------------------------------------------------------------------------- 1 | owner: stackblitz-labs 2 | repo: bolt.diy 3 | provider: github 4 | private: false 5 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/logo-dark-styled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/public/logo-dark-styled.png -------------------------------------------------------------------------------- /public/logo-light-styled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/public/logo-light-styled.png -------------------------------------------------------------------------------- /public/social_preview_index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/public/social_preview_index.jpg -------------------------------------------------------------------------------- /docs/images/api-key-ui-section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/docs/images/api-key-ui-section.png -------------------------------------------------------------------------------- /docs/images/provider-base-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/docs/images/provider-base-url.png -------------------------------------------------------------------------------- /app/lib/persistence/index.ts: -------------------------------------------------------------------------------- 1 | export * from './localStorage'; 2 | export * from './db'; 3 | export * from './useChatHistory'; 4 | -------------------------------------------------------------------------------- /app/lib/stores/streaming.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'nanostores'; 2 | 3 | export const streamingState = atom(false); 4 | -------------------------------------------------------------------------------- /docs/images/bolt-settings-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/docs/images/bolt-settings-button.png -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/bolt.diy/main/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /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/unreachable.ts: -------------------------------------------------------------------------------- 1 | export function unreachable(message: string): never { 2 | throw new Error(`Unreachable: ${message}`); 3 | } 4 | -------------------------------------------------------------------------------- /app/types/artifact.ts: -------------------------------------------------------------------------------- 1 | export interface BoltArtifactData { 2 | id: string; 3 | title: string; 4 | type?: string | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /electron/main/utils/store.ts: -------------------------------------------------------------------------------- 1 | import ElectronStore from 'electron-store'; 2 | 3 | export const store = new ElectronStore({ encryptionKey: 'something' }); 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /electron/preload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../main/tsconfig.json", 3 | "include": ["./**/*.ts"], 4 | "compilerOptions": { 5 | "rootDir": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/lib/persistence/types.ts: -------------------------------------------------------------------------------- 1 | import type { FileMap } from '~/lib/stores/files'; 2 | 3 | export interface Snapshot { 4 | chatIndex: string; 5 | files: FileMap; 6 | summary?: string; 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /electron/main/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | 3 | export const isDev = !(global.process.env.NODE_ENV === 'production' || app.isPackaged); 4 | export const DEFAULT_PORT = 5173; 5 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /icons/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icons/Anthropic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/routes/api.health.ts: -------------------------------------------------------------------------------- 1 | import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare'; 2 | 3 | export const loader = async ({ request: _request }: LoaderFunctionArgs) => { 4 | return json({ 5 | status: 'healthy', 6 | timestamp: new Date().toISOString(), 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /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/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/components/editor/codemirror/BinaryContent.tsx: -------------------------------------------------------------------------------- 1 | export function BinaryContent() { 2 | return ( 3 |
4 | File format cannot be displayed. 5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icons/xAI.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/utils/formatSize.ts: -------------------------------------------------------------------------------- 1 | export function formatSize(bytes: number): string { 2 | const units = ['B', 'KB', 'MB', 'GB', 'TB']; 3 | let size = bytes; 4 | let unitIndex = 0; 5 | 6 | while (size >= 1024 && unitIndex < units.length - 1) { 7 | size /= 1024; 8 | unitIndex++; 9 | } 10 | 11 | return `${size.toFixed(1)} ${units[unitIndex]}`; 12 | } 13 | -------------------------------------------------------------------------------- /icons/angular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icons/Groq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/components/ui/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | const CollapsibleTrigger = CollapsiblePrimitive.Trigger; 7 | const CollapsibleContent = CollapsiblePrimitive.Content; 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icons/OpenRouter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/utils/os.ts: -------------------------------------------------------------------------------- 1 | // Helper to detect OS 2 | export const isMac = typeof navigator !== 'undefined' ? navigator.platform.toLowerCase().includes('mac') : false; 3 | export const isWindows = typeof navigator !== 'undefined' ? navigator.platform.toLowerCase().includes('win') : false; 4 | export const isLinux = typeof navigator !== 'undefined' ? navigator.platform.toLowerCase().includes('linux') : false; 5 | -------------------------------------------------------------------------------- /scripts/update-imports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Update imports in TypeScript files 4 | find app -type f -name "*.ts" -o -name "*.tsx" | xargs sed -i '' 's|~/components/settings/settings.types|~/components/@settings/core/types|g' 5 | 6 | # Update imports for specific components 7 | find app -type f -name "*.ts" -o -name "*.tsx" | xargs sed -i '' 's|~/components/settings/|~/components/@settings/tabs/|g' -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/types/context.ts: -------------------------------------------------------------------------------- 1 | export type ContextAnnotation = 2 | | { 3 | type: 'codeContext'; 4 | files: string[]; 5 | } 6 | | { 7 | type: 'chatSummary'; 8 | summary: string; 9 | chatId: string; 10 | }; 11 | 12 | export type ProgressAnnotation = { 13 | type: 'progress'; 14 | label: string; 15 | status: 'in-progress' | 'complete'; 16 | order: number; 17 | message: string; 18 | }; 19 | -------------------------------------------------------------------------------- /app/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce any>(func: T, wait: number): (...args: Parameters) => void { 2 | let timeout: NodeJS.Timeout; 3 | 4 | return function executedFunction(...args: Parameters) { 5 | const later = () => { 6 | clearTimeout(timeout); 7 | func(...args); 8 | }; 9 | 10 | clearTimeout(timeout); 11 | timeout = setTimeout(later, wait); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /public/icons/Mistral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icons/Cohere.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/LMStudio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /public/icons/Default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/Ollama.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icons/Perplexity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | export { useUpdateCheck } from './useUpdateCheck'; 8 | export { useFeatures } from './useFeatures'; 9 | export { useNotifications } from './useNotifications'; 10 | export { useConnectionStatus } from './useConnectionStatus'; 11 | export { useDebugStatus } from './useDebugStatus'; 12 | -------------------------------------------------------------------------------- /public/icons/Google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icons/Together.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/components/@settings/index.ts: -------------------------------------------------------------------------------- 1 | // Core exports 2 | export { ControlPanel } from './core/ControlPanel'; 3 | export type { TabType, TabVisibilityConfig } from './core/types'; 4 | 5 | // Constants 6 | export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constants'; 7 | 8 | // Shared components 9 | export { TabTile } from './shared/components/TabTile'; 10 | export { TabManagement } from './shared/components/TabManagement'; 11 | 12 | // Utils 13 | export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers'; 14 | export * from './utils/animations'; 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icons/Deepseek.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/utils/getLanguageFromExtension.ts: -------------------------------------------------------------------------------- 1 | export const getLanguageFromExtension = (ext: string): string => { 2 | const map: Record = { 3 | js: 'javascript', 4 | jsx: 'jsx', 5 | ts: 'typescript', 6 | tsx: 'tsx', 7 | json: 'json', 8 | html: 'html', 9 | css: 'css', 10 | py: 'python', 11 | java: 'java', 12 | rb: 'ruby', 13 | cpp: 'cpp', 14 | c: 'c', 15 | cs: 'csharp', 16 | go: 'go', 17 | rs: 'rust', 18 | php: 'php', 19 | swift: 'swift', 20 | md: 'plaintext', 21 | sh: 'bash', 22 | }; 23 | return map[ext] || 'typescript'; 24 | }; 25 | -------------------------------------------------------------------------------- /icons/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/remix.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | .vscode/* 15 | .vscode/launch.json 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | /.history 26 | /.cache 27 | /build 28 | .env.local 29 | .env 30 | .dev.vars 31 | *.vars 32 | .wrangler 33 | _worker.bundle 34 | 35 | Modelfile 36 | modelfiles 37 | 38 | # docs ignore 39 | site 40 | 41 | # commit file ignore 42 | app/commit.json 43 | changelogUI.md 44 | docs/instructions/Roadmap.md 45 | .cursorrules 46 | *.md 47 | -------------------------------------------------------------------------------- /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/supabase.ts: -------------------------------------------------------------------------------- 1 | export interface SupabaseUser { 2 | id: string; 3 | email: string; 4 | role: string; 5 | created_at: string; 6 | last_sign_in_at: string; 7 | } 8 | 9 | export interface SupabaseProject { 10 | id: string; 11 | name: string; 12 | organization_id: string; 13 | region: string; 14 | created_at: string; 15 | status: string; 16 | } 17 | 18 | export interface SupabaseStats { 19 | projects: SupabaseProject[]; 20 | totalProjects: number; 21 | } 22 | 23 | export interface SupabaseApiKey { 24 | name: string; 25 | api_key: string; 26 | } 27 | 28 | export interface SupabaseCredentials { 29 | anonKey?: string; 30 | supabaseUrl?: string; 31 | } 32 | -------------------------------------------------------------------------------- /icons/logo-text.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/components/ui/Separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 2 | import { classNames } from '~/utils/classNames'; 3 | 4 | interface SeparatorProps { 5 | className?: string; 6 | orientation?: 'horizontal' | 'vertical'; 7 | } 8 | 9 | export const Separator = ({ className, orientation = 'horizontal' }: SeparatorProps) => { 10 | return ( 11 | 19 | ); 20 | }; 21 | 22 | export default Separator; 23 | -------------------------------------------------------------------------------- /worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | interface Env { 2 | RUNNING_IN_DOCKER: Settings; 3 | DEFAULT_NUM_CTX: Settings; 4 | ANTHROPIC_API_KEY: string; 5 | OPENAI_API_KEY: string; 6 | GROQ_API_KEY: string; 7 | HuggingFace_API_KEY: string; 8 | OPEN_ROUTER_API_KEY: string; 9 | OLLAMA_API_BASE_URL: string; 10 | OPENAI_LIKE_API_KEY: string; 11 | OPENAI_LIKE_API_BASE_URL: string; 12 | TOGETHER_API_KEY: string; 13 | TOGETHER_API_BASE_URL: string; 14 | DEEPSEEK_API_KEY: string; 15 | LMSTUDIO_API_BASE_URL: string; 16 | GOOGLE_GENERATIVE_AI_API_KEY: string; 17 | MISTRAL_API_KEY: string; 18 | XAI_API_KEY: string; 19 | PERPLEXITY_API_KEY: string; 20 | AWS_BEDROCK_CONFIG: string; 21 | } 22 | -------------------------------------------------------------------------------- /app/routes/api.update.ts: -------------------------------------------------------------------------------- 1 | import { json, type ActionFunction } from '@remix-run/cloudflare'; 2 | 3 | export const action: ActionFunction = async ({ request }) => { 4 | if (request.method !== 'POST') { 5 | return json({ error: 'Method not allowed' }, { status: 405 }); 6 | } 7 | 8 | return json( 9 | { 10 | error: 'Updates must be performed manually in a server environment', 11 | instructions: [ 12 | '1. Navigate to the project directory', 13 | '2. Run: git fetch upstream', 14 | '3. Run: git pull upstream main', 15 | '4. Run: pnpm install', 16 | '5. Run: pnpm run build', 17 | ], 18 | }, 19 | { status: 400 }, 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /icons/nextjs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/ui/Label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as LabelPrimitive from '@radix-ui/react-label'; 3 | import { classNames } from '~/utils/classNames'; 4 | 5 | const Label = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 17 | )); 18 | Label.displayName = LabelPrimitive.Root.displayName; 19 | 20 | export { Label }; 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /icons/nuxt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /icons/astro.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/ui/Progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { classNames } from '~/utils/classNames'; 3 | 4 | interface ProgressProps extends React.HTMLAttributes { 5 | value?: number; 6 | } 7 | 8 | const Progress = React.forwardRef(({ className, value, ...props }, ref) => ( 9 |
14 |
18 |
19 | )); 20 | Progress.displayName = 'Progress'; 21 | 22 | export { Progress }; 23 | -------------------------------------------------------------------------------- /electron/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, contextBridge, type IpcRendererEvent } from 'electron'; 2 | 3 | console.debug('start preload.', ipcRenderer); 4 | 5 | const ipc = { 6 | invoke(...args: any[]) { 7 | return ipcRenderer.invoke('ipcTest', ...args); 8 | }, 9 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 10 | on(channel: string, func: Function) { 11 | const f = (event: IpcRendererEvent, ...args: any[]) => func(...[event, ...args]); 12 | console.debug('register listener', channel, f); 13 | ipcRenderer.on(channel, f); 14 | 15 | return () => { 16 | console.debug('remove listener', channel, f); 17 | ipcRenderer.removeListener(channel, f); 18 | }; 19 | }, 20 | }; 21 | 22 | contextBridge.exposeInMainWorld('ipc', ipc); 23 | -------------------------------------------------------------------------------- /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/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/lib/hooks/useLocalProviders.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import type { IProviderConfig } from '~/types/model'; 3 | 4 | export interface UseLocalProvidersReturn { 5 | localProviders: IProviderConfig[]; 6 | refreshLocalProviders: () => void; 7 | } 8 | 9 | export function useLocalProviders(): UseLocalProvidersReturn { 10 | const [localProviders, setLocalProviders] = useState([]); 11 | 12 | const refreshLocalProviders = useCallback(() => { 13 | /* 14 | * Refresh logic for local providers 15 | * This would typically involve checking the status of Ollama and LMStudio 16 | * For now, we'll just return an empty array 17 | */ 18 | setLocalProviders([]); 19 | }, []); 20 | 21 | return { 22 | localProviders, 23 | refreshLocalProviders, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /app/lib/persistence/localStorage.ts: -------------------------------------------------------------------------------- 1 | // Client-side storage utilities 2 | const isClient = typeof window !== 'undefined' && typeof localStorage !== 'undefined'; 3 | 4 | export function getLocalStorage(key: string): any | null { 5 | if (!isClient) { 6 | return null; 7 | } 8 | 9 | try { 10 | const item = localStorage.getItem(key); 11 | return item ? JSON.parse(item) : null; 12 | } catch (error) { 13 | console.error(`Error reading from localStorage key "${key}":`, error); 14 | return null; 15 | } 16 | } 17 | 18 | export function setLocalStorage(key: string, value: any): void { 19 | if (!isClient) { 20 | return; 21 | } 22 | 23 | try { 24 | localStorage.setItem(key, JSON.stringify(value)); 25 | } catch (error) { 26 | console.error(`Error writing to localStorage key "${key}":`, error); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /electron/main/ui/menu.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, Menu } from 'electron'; 2 | 3 | export function setupMenu(win: BrowserWindow): void { 4 | const app = Menu.getApplicationMenu(); 5 | Menu.setApplicationMenu( 6 | Menu.buildFromTemplate([ 7 | ...(app ? app.items : []), 8 | { 9 | label: 'Go', 10 | submenu: [ 11 | { 12 | label: 'Back', 13 | accelerator: 'CmdOrCtrl+[', 14 | click: () => { 15 | win?.webContents.navigationHistory.goBack(); 16 | }, 17 | }, 18 | { 19 | label: 'Forward', 20 | accelerator: 'CmdOrCtrl+]', 21 | click: () => { 22 | win?.webContents.navigationHistory.goForward(); 23 | }, 24 | }, 25 | ], 26 | }, 27 | ]), 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /electron/preload/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: resolve('electron/preload/index.ts'), 8 | formats: ['cjs'], 9 | }, 10 | rollupOptions: { 11 | external: ['electron'], 12 | output: { 13 | dir: 'build/electron', 14 | 15 | /* 16 | * preload must be cjs format. 17 | * if mjs, it will be error: 18 | * - Unable to load preload script. 19 | * - SyntaxError: Cannot use import statement outside a module. 20 | */ 21 | entryFileNames: 'preload/[name].cjs', 22 | format: 'cjs', 23 | }, 24 | }, 25 | minify: false, 26 | emptyOutDir: false, 27 | }, 28 | esbuild: { 29 | platform: 'node', 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /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/icons/HuggingFace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/lib/stores/profile.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'nanostores'; 2 | 3 | interface Profile { 4 | username: string; 5 | bio: string; 6 | avatar: string; 7 | } 8 | 9 | // Initialize with stored profile or defaults 10 | const storedProfile = typeof window !== 'undefined' ? localStorage.getItem('bolt_profile') : null; 11 | const initialProfile: Profile = storedProfile 12 | ? JSON.parse(storedProfile) 13 | : { 14 | username: '', 15 | bio: '', 16 | avatar: '', 17 | }; 18 | 19 | export const profileStore = atom(initialProfile); 20 | 21 | export const updateProfile = (updates: Partial) => { 22 | profileStore.set({ ...profileStore.get(), ...updates }); 23 | 24 | // Persist to localStorage 25 | if (typeof window !== 'undefined') { 26 | localStorage.setItem('bolt_profile', JSON.stringify(profileStore.get())); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /app/types/netlify.ts: -------------------------------------------------------------------------------- 1 | export interface NetlifySite { 2 | id: string; 3 | name: string; 4 | url: string; 5 | admin_url: string; 6 | build_settings: { 7 | provider: string; 8 | repo_url: string; 9 | cmd: string; 10 | }; 11 | published_deploy: { 12 | published_at: string; 13 | deploy_time: number; 14 | }; 15 | } 16 | 17 | export interface NetlifyUser { 18 | id: string; 19 | slug: string; 20 | email: string; 21 | full_name: string; 22 | avatar_url: string; 23 | } 24 | 25 | export interface NetlifyStats { 26 | sites: NetlifySite[]; 27 | totalSites: number; 28 | } 29 | 30 | export interface NetlifyConnection { 31 | user: NetlifyUser | null; 32 | token: string; 33 | stats?: NetlifyStats; 34 | } 35 | 36 | export interface NetlifySiteInfo { 37 | id: string; 38 | name: string; 39 | url: string; 40 | chatId: string; 41 | } 42 | -------------------------------------------------------------------------------- /electron/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["."], 3 | "compilerOptions": { 4 | "lib": ["ESNext"], 5 | "jsx": "preserve", 6 | "target": "ESNext", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "useDefineForClassFields": true, 10 | 11 | /* modules */ 12 | "moduleResolution": "Bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "module": "ESNext", 16 | "isolatedModules": true, 17 | "emitDeclarationOnly": true, 18 | "declaration": true, 19 | "declarationDir": "./dist", 20 | 21 | /* type checking */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "noImplicitReturns": true, 27 | "verbatimModuleSyntax": true, 28 | "forceConsistentCasingInFileNames": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/epic.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Epic 3 | about: Epics define long-term vision and capabilities of the software. They will never be finished but serve as umbrella for features. 4 | title: '' 5 | labels: 6 | - epic 7 | assignees: '' 8 | --- 9 | 10 | # Strategic Impact 11 | 12 | 13 | 14 | # Target Audience 15 | 16 | 19 | 20 | # Capabilities 21 | 22 | 24 | -------------------------------------------------------------------------------- /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/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/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/lib/stores/tabConfigurationStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | export interface TabConfig { 4 | id: string; 5 | visible: boolean; 6 | window: 'developer' | 'user'; 7 | order: number; 8 | locked?: boolean; 9 | } 10 | 11 | interface TabConfigurationStore { 12 | userTabs: TabConfig[]; 13 | developerTabs: TabConfig[]; 14 | get: () => { userTabs: TabConfig[]; developerTabs: TabConfig[] }; 15 | set: (config: { userTabs: TabConfig[]; developerTabs: TabConfig[] }) => void; 16 | reset: () => void; 17 | } 18 | 19 | const DEFAULT_CONFIG = { 20 | userTabs: [], 21 | developerTabs: [], 22 | }; 23 | 24 | export const tabConfigurationStore = create((set, get) => ({ 25 | ...DEFAULT_CONFIG, 26 | get: () => ({ 27 | userTabs: get().userTabs, 28 | developerTabs: get().developerTabs, 29 | }), 30 | set: (config) => set(config), 31 | reset: () => set(DEFAULT_CONFIG), 32 | })); 33 | -------------------------------------------------------------------------------- /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 | 7 | export interface File { 8 | type: 'file'; 9 | content: string; 10 | isBinary: boolean; 11 | } 12 | 13 | export interface Folder { 14 | type: 'folder'; 15 | } 16 | 17 | type Dirent = File | Folder; 18 | 19 | export type FileMap = Record; 20 | 21 | export const IGNORE_PATTERNS = [ 22 | 'node_modules/**', 23 | '.git/**', 24 | 'dist/**', 25 | 'build/**', 26 | '.next/**', 27 | 'coverage/**', 28 | '.cache/**', 29 | '.vscode/**', 30 | '.idea/**', 31 | '**/*.log', 32 | '**/.DS_Store', 33 | '**/npm-debug.log*', 34 | '**/yarn-debug.log*', 35 | '**/yarn-error.log*', 36 | '**/*lock.json', 37 | '**/*lock.yml', 38 | ]; 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /electron/main/utils/reload.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import path from 'node:path'; 3 | import { promises as fs } from 'node:fs'; 4 | 5 | // Reload on change. 6 | let isQuited = false; 7 | 8 | const abort = new AbortController(); 9 | const { signal } = abort; 10 | 11 | export async function reloadOnChange() { 12 | const dir = path.join(app.getAppPath(), 'build', 'electron'); 13 | 14 | try { 15 | const watcher = fs.watch(dir, { signal, recursive: true }); 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | for await (const _event of watcher) { 19 | if (!isQuited) { 20 | isQuited = true; 21 | app.relaunch(); 22 | app.quit(); 23 | } 24 | } 25 | } catch (err) { 26 | if (!(err instanceof Error)) { 27 | throw err; 28 | } 29 | 30 | if (err.name === 'AbortError') { 31 | console.log('abort watching:', dir); 32 | return; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/components/ui/Popover.tsx: -------------------------------------------------------------------------------- 1 | import * as Popover from '@radix-ui/react-popover'; 2 | import type { PropsWithChildren, ReactNode } from 'react'; 3 | 4 | export default ({ 5 | children, 6 | trigger, 7 | side, 8 | align, 9 | }: PropsWithChildren<{ 10 | trigger: ReactNode; 11 | side: 'top' | 'right' | 'bottom' | 'left' | undefined; 12 | align: 'center' | 'start' | 'end' | undefined; 13 | }>) => ( 14 | 15 | {trigger} 16 | 17 | 18 | 24 | {children} 25 | 26 | 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /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 | baseUrl?: string; 32 | apiTokenKey?: string; 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: A pretty vague description of how a capability of our software can be added or improved. 4 | title: '' 5 | labels: 6 | - feature 7 | assignees: '' 8 | --- 9 | 10 | # Motivation 11 | 12 | 13 | 14 | # Scope 15 | 16 | 19 | 20 | # Options 21 | 22 | 25 | 26 | # Related 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/components/ui/Input.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import { classNames } from '~/utils/classNames'; 3 | 4 | export interface InputProps extends React.InputHTMLAttributes {} 5 | 6 | const Input = forwardRef(({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }); 19 | 20 | Input.displayName = 'Input'; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /icons/nativescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/lib/api/features.ts: -------------------------------------------------------------------------------- 1 | export interface Feature { 2 | id: string; 3 | name: string; 4 | description: string; 5 | viewed: boolean; 6 | releaseDate: string; 7 | } 8 | 9 | export const getFeatureFlags = async (): Promise => { 10 | /* 11 | * TODO: Implement actual feature flags logic 12 | * This is a mock implementation 13 | */ 14 | return [ 15 | { 16 | id: 'feature-1', 17 | name: 'Dark Mode', 18 | description: 'Enable dark mode for better night viewing', 19 | viewed: true, 20 | releaseDate: '2024-03-15', 21 | }, 22 | { 23 | id: 'feature-2', 24 | name: 'Tab Management', 25 | description: 'Customize your tab layout', 26 | viewed: false, 27 | releaseDate: '2024-03-20', 28 | }, 29 | ]; 30 | }; 31 | 32 | export const markFeatureViewed = async (featureId: string): Promise => { 33 | /* TODO: Implement actual feature viewed logic */ 34 | console.log(`Marking feature ${featureId} as viewed`); 35 | }; 36 | -------------------------------------------------------------------------------- /icons/stars.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /icons/slidev.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/utils/path.ts: -------------------------------------------------------------------------------- 1 | // Browser-compatible path utilities 2 | import type { ParsedPath } from 'path'; 3 | import pathBrowserify from 'path-browserify'; 4 | 5 | /** 6 | * A browser-compatible path utility that mimics Node's path module 7 | * Using path-browserify for consistent behavior in browser environments 8 | */ 9 | export const path = { 10 | join: (...paths: string[]): string => pathBrowserify.join(...paths), 11 | dirname: (path: string): string => pathBrowserify.dirname(path), 12 | basename: (path: string, ext?: string): string => pathBrowserify.basename(path, ext), 13 | extname: (path: string): string => pathBrowserify.extname(path), 14 | relative: (from: string, to: string): string => pathBrowserify.relative(from, to), 15 | isAbsolute: (path: string): boolean => pathBrowserify.isAbsolute(path), 16 | normalize: (path: string): string => pathBrowserify.normalize(path), 17 | parse: (path: string): ParsedPath => pathBrowserify.parse(path), 18 | format: (pathObject: ParsedPath): string => pathBrowserify.format(pathObject), 19 | } as const; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 4 | "types": [ 5 | "@remix-run/cloudflare", 6 | "vite/client", 7 | "@cloudflare/workers-types/2023-07-01", 8 | "@types/dom-speech-recognition", 9 | "electron" 10 | ], 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "jsx": "react-jsx", 14 | "module": "ESNext", 15 | "moduleResolution": "Bundler", 16 | "resolveJsonModule": true, 17 | "target": "ESNext", 18 | "strict": true, 19 | "allowJs": true, 20 | "skipLibCheck": true, 21 | "verbatimModuleSyntax": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "baseUrl": ".", 24 | "paths": { 25 | "~/*": ["./app/*"] 26 | }, 27 | // vite takes care of building everything, not tsc 28 | "noEmit": true 29 | }, 30 | "include": [ 31 | "**/*.ts", 32 | "**/*.tsx", 33 | "**/.server/**/*.ts", 34 | "**/.server/**/*.tsx", 35 | "**/.client/**/*.ts", 36 | "**/.client/**/*.tsx" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /notarize.cjs: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize'); 2 | 3 | exports.default = async function notarizing(context) { 4 | const { electronPlatformName, appOutDir } = context; 5 | 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | // Skip notarization when identity is null (development build) 11 | if (!context.packager.config.mac || context.packager.config.mac.identity === null) { 12 | console.log('Skipping notarization: identity is null'); 13 | return; 14 | } 15 | 16 | const appName = context.packager.appInfo.productFilename; 17 | const appBundleId = context.packager.config.appId; 18 | 19 | try { 20 | console.log(`Notarizing ${appBundleId} found at ${appOutDir}/${appName}.app`); 21 | await notarize({ 22 | tool: 'notarytool', 23 | appPath: `${appOutDir}/${appName}.app`, 24 | teamId: process.env.APPLE_TEAM_ID, 25 | }); 26 | console.log(`Done notarizing ${appBundleId}`); 27 | } catch (error) { 28 | console.error('Notarization failed:', error); 29 | throw error; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /bindings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bindings="" 4 | 5 | # Function to extract variable names from the TypeScript interface 6 | extract_env_vars() { 7 | grep -o '[A-Z_]\+:' worker-configuration.d.ts | sed 's/://' 8 | } 9 | 10 | # First try to read from .env.local if it exists 11 | if [ -f ".env.local" ]; then 12 | while IFS= read -r line || [ -n "$line" ]; do 13 | if [[ ! "$line" =~ ^# ]] && [[ -n "$line" ]]; then 14 | name=$(echo "$line" | cut -d '=' -f 1) 15 | value=$(echo "$line" | cut -d '=' -f 2-) 16 | value=$(echo $value | sed 's/^"\(.*\)"$/\1/') 17 | bindings+="--binding ${name}=${value} " 18 | fi 19 | done < .env.local 20 | else 21 | # If .env.local doesn't exist, use environment variables defined in .d.ts 22 | env_vars=($(extract_env_vars)) 23 | # Generate bindings for each environment variable if it exists 24 | for var in "${env_vars[@]}"; do 25 | if [ -n "${!var}" ]; then 26 | bindings+="--binding ${var}=${!var} " 27 | fi 28 | done 29 | fi 30 | 31 | bindings=$(echo $bindings | sed 's/[[:space:]]*$//') 32 | 33 | echo $bindings -------------------------------------------------------------------------------- /public/icons/OpenAILike.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | com.apple.security.cs.allow-jit 7 | 8 | 9 | 10 | com.apple.security.cs.allow-unsigned-executable-memory 11 | 12 | 13 | 14 | com.apple.security.network.client 15 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | -------------------------------------------------------------------------------- /icons/qwik.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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 36 | -------------------------------------------------------------------------------- /icons/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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 33 | -------------------------------------------------------------------------------- /electron/main/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: resolve('electron/main/index.ts'), 8 | formats: ['es'], 9 | }, 10 | rollupOptions: { 11 | external: [ 12 | 'vite', 13 | 'electron', 14 | ...[ 15 | 'electron-log', 16 | 17 | // electron-log uses fs internally 18 | 'fs', 19 | 'util', 20 | ], 21 | 22 | // Add all Node.js built-in modules as external 23 | 'node:fs', 24 | 'node:path', 25 | 'node:url', 26 | 'node:util', 27 | 'node:stream', 28 | 'node:events', 29 | 'electron-store', 30 | '@remix-run/node', 31 | 32 | // "mime", // NOTE: don't enable. not working if it's external. 33 | 'electron-updater', 34 | ], 35 | output: { 36 | dir: 'build/electron', 37 | entryFileNames: 'main/[name].mjs', 38 | format: 'esm', 39 | }, 40 | }, 41 | minify: false, 42 | emptyOutDir: false, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 StackBlitz, Inc. and bolt.diy contributors 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 | -------------------------------------------------------------------------------- /.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 32 | -------------------------------------------------------------------------------- /app/components/@settings/utils/animations.ts: -------------------------------------------------------------------------------- 1 | import type { Variants } from 'framer-motion'; 2 | 3 | export const fadeIn: Variants = { 4 | initial: { opacity: 0 }, 5 | animate: { opacity: 1 }, 6 | exit: { opacity: 0 }, 7 | }; 8 | 9 | export const slideIn: Variants = { 10 | initial: { opacity: 0, y: 20 }, 11 | animate: { opacity: 1, y: 0 }, 12 | exit: { opacity: 0, y: -20 }, 13 | }; 14 | 15 | export const scaleIn: Variants = { 16 | initial: { opacity: 0, scale: 0.8 }, 17 | animate: { opacity: 1, scale: 1 }, 18 | exit: { opacity: 0, scale: 0.8 }, 19 | }; 20 | 21 | export const tabAnimation: Variants = { 22 | initial: { opacity: 0, scale: 0.8, y: 20 }, 23 | animate: { opacity: 1, scale: 1, y: 0 }, 24 | exit: { opacity: 0, scale: 0.8, y: -20 }, 25 | }; 26 | 27 | export const overlayAnimation: Variants = { 28 | initial: { opacity: 0 }, 29 | animate: { opacity: 1 }, 30 | exit: { opacity: 0 }, 31 | }; 32 | 33 | export const modalAnimation: Variants = { 34 | initial: { opacity: 0, scale: 0.95, y: 20 }, 35 | animate: { opacity: 1, scale: 1, y: 0 }, 36 | exit: { opacity: 0, scale: 0.95, y: 20 }, 37 | }; 38 | 39 | export const transition = { 40 | duration: 0.2, 41 | }; 42 | -------------------------------------------------------------------------------- /app/components/ui/use-toast.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { toast as toastify } from 'react-toastify'; 3 | 4 | interface ToastOptions { 5 | type?: 'success' | 'error' | 'info' | 'warning'; 6 | duration?: number; 7 | } 8 | 9 | export function useToast() { 10 | const toast = useCallback((message: string, options: ToastOptions = {}) => { 11 | const { type = 'info', duration = 3000 } = options; 12 | 13 | toastify[type](message, { 14 | position: 'bottom-right', 15 | autoClose: duration, 16 | hideProgressBar: false, 17 | closeOnClick: true, 18 | pauseOnHover: true, 19 | draggable: true, 20 | progress: undefined, 21 | theme: 'dark', 22 | }); 23 | }, []); 24 | 25 | const success = useCallback( 26 | (message: string, options: Omit = {}) => { 27 | toast(message, { ...options, type: 'success' }); 28 | }, 29 | [toast], 30 | ); 31 | 32 | const error = useCallback( 33 | (message: string, options: Omit = {}) => { 34 | toast(message, { ...options, type: 'error' }); 35 | }, 36 | [toast], 37 | ); 38 | 39 | return { toast, success, error }; 40 | } 41 | -------------------------------------------------------------------------------- /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 [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }]; 10 | }; 11 | 12 | export const loader = () => json({}); 13 | 14 | /** 15 | * Landing page component for Bolt 16 | * Note: Settings functionality should ONLY be accessed through the sidebar menu. 17 | * Do not add settings button/panel to this landing page as it was intentionally removed 18 | * to keep the UI clean and consistent with the design system. 19 | */ 20 | export default function Index() { 21 | return ( 22 |
23 | 24 |
25 | }>{() => } 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /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/lib/api/cookies.ts: -------------------------------------------------------------------------------- 1 | export function parseCookies(cookieHeader: string | null) { 2 | const cookies: Record = {}; 3 | 4 | if (!cookieHeader) { 5 | return cookies; 6 | } 7 | 8 | // Split the cookie string by semicolons and spaces 9 | const items = cookieHeader.split(';').map((cookie) => cookie.trim()); 10 | 11 | items.forEach((item) => { 12 | const [name, ...rest] = item.split('='); 13 | 14 | if (name && rest.length > 0) { 15 | // Decode the name and value, and join value parts in case it contains '=' 16 | const decodedName = decodeURIComponent(name.trim()); 17 | const decodedValue = decodeURIComponent(rest.join('=').trim()); 18 | cookies[decodedName] = decodedValue; 19 | } 20 | }); 21 | 22 | return cookies; 23 | } 24 | 25 | export function getApiKeysFromCookie(cookieHeader: string | null): Record { 26 | const cookies = parseCookies(cookieHeader); 27 | return cookies.apiKeys ? JSON.parse(cookies.apiKeys) : {}; 28 | } 29 | 30 | export function getProviderSettingsFromCookie(cookieHeader: string | null): Record { 31 | const cookies = parseCookies(cookieHeader); 32 | return cookies.providers ? JSON.parse(cookies.providers) : {}; 33 | } 34 | -------------------------------------------------------------------------------- /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/@settings/tabs/providers/service-status/types.ts: -------------------------------------------------------------------------------- 1 | import type { IconType } from 'react-icons'; 2 | 3 | export type ProviderName = 4 | | 'AmazonBedrock' 5 | | 'Cohere' 6 | | 'Deepseek' 7 | | 'Google' 8 | | 'Groq' 9 | | 'HuggingFace' 10 | | 'Hyperbolic' 11 | | 'Mistral' 12 | | 'OpenRouter' 13 | | 'Perplexity' 14 | | 'Together' 15 | | 'XAI'; 16 | 17 | export type ServiceStatus = { 18 | provider: ProviderName; 19 | status: 'operational' | 'degraded' | 'down'; 20 | lastChecked: string; 21 | statusUrl?: string; 22 | icon?: IconType; 23 | message?: string; 24 | responseTime?: number; 25 | incidents?: string[]; 26 | }; 27 | 28 | export interface ProviderConfig { 29 | statusUrl: string; 30 | apiUrl: string; 31 | headers: Record; 32 | testModel: string; 33 | } 34 | 35 | export type ApiResponse = { 36 | error?: { 37 | message: string; 38 | }; 39 | message?: string; 40 | model?: string; 41 | models?: Array<{ 42 | id?: string; 43 | name?: string; 44 | }>; 45 | data?: Array<{ 46 | id?: string; 47 | name?: string; 48 | }>; 49 | }; 50 | 51 | export type StatusCheckResult = { 52 | status: 'operational' | 'degraded' | 'down'; 53 | message: string; 54 | incidents: string[]; 55 | }; 56 | -------------------------------------------------------------------------------- /app/routes/api.supabase.variables.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@remix-run/node'; 2 | import type { ActionFunctionArgs } from '@remix-run/node'; 3 | 4 | export async function action({ request }: ActionFunctionArgs) { 5 | try { 6 | // Add proper type assertion for the request body 7 | const body = (await request.json()) as { projectId?: string; token?: string }; 8 | const { projectId, token } = body; 9 | 10 | if (!projectId || !token) { 11 | return json({ error: 'Project ID and token are required' }, { status: 400 }); 12 | } 13 | 14 | const response = await fetch(`https://api.supabase.com/v1/projects/${projectId}/api-keys`, { 15 | method: 'GET', 16 | headers: { 17 | Authorization: `Bearer ${token}`, 18 | 'Content-Type': 'application/json', 19 | }, 20 | }); 21 | 22 | if (!response.ok) { 23 | return json({ error: `Failed to fetch API keys: ${response.statusText}` }, { status: response.status }); 24 | } 25 | 26 | const apiKeys = await response.json(); 27 | 28 | return json({ apiKeys }); 29 | } catch (error) { 30 | console.error('Error fetching project API keys:', error); 31 | return json({ error: error instanceof Error ? error.message : 'Unknown error occurred' }, { status: 500 }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /electron/main/utils/cookie.ts: -------------------------------------------------------------------------------- 1 | import { session } from 'electron'; 2 | import { DEFAULT_PORT } from './constants'; 3 | import { store } from './store'; 4 | 5 | /** 6 | * On app startup: read any existing cookies from store and set it as a cookie. 7 | */ 8 | export async function initCookies() { 9 | await loadStoredCookies(); 10 | } 11 | 12 | // Function to store all cookies 13 | export async function storeCookies(cookies: Electron.Cookie[]) { 14 | for (const cookie of cookies) { 15 | store.set(`cookie:${cookie.name}`, cookie); 16 | } 17 | } 18 | 19 | // Function to load stored cookies 20 | async function loadStoredCookies() { 21 | // Get all keys that start with 'cookie:' 22 | const cookieKeys = store.store ? Object.keys(store.store).filter((key) => key.startsWith('cookie:')) : []; 23 | 24 | for (const key of cookieKeys) { 25 | const cookie = store.get(key); 26 | 27 | if (cookie) { 28 | try { 29 | // Add default URL if not present 30 | const cookieWithUrl = { 31 | ...cookie, 32 | url: cookie.url || `http://localhost:${DEFAULT_PORT}`, 33 | }; 34 | await session.defaultSession.cookies.set(cookieWithUrl); 35 | } catch (error) { 36 | console.error(`Failed to set cookie ${key}:`, error); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/components/ui/Badge.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { cva, type VariantProps } from 'class-variance-authority'; 5 | import { classNames } from '~/utils/classNames'; 6 | 7 | const badgeVariants = cva( 8 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-bolt-elements-ring focus:ring-offset-2', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'border-transparent bg-bolt-elements-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background/80', 14 | secondary: 15 | 'border-transparent bg-bolt-elements-background text-bolt-elements-textSecondary hover:bg-bolt-elements-background/80', 16 | destructive: 'border-transparent bg-red-500/10 text-red-500 hover:bg-red-500/20', 17 | outline: 'text-bolt-elements-textPrimary', 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: 'default', 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps extends React.HTMLAttributes, VariantProps {} 27 | 28 | function Badge({ className, variant, ...props }: BadgeProps) { 29 | return
; 30 | } 31 | 32 | export { Badge, badgeVariants }; 33 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/components/ui/LoadingOverlay.tsx: -------------------------------------------------------------------------------- 1 | export const LoadingOverlay = ({ 2 | message = 'Loading...', 3 | progress, 4 | progressText, 5 | }: { 6 | message?: string; 7 | progress?: number; 8 | progressText?: string; 9 | }) => { 10 | return ( 11 |
12 |
13 |
17 |

{message}

18 | {progress !== undefined && ( 19 |
20 |
21 |
25 |
26 | {progressText &&

{progressText}

} 27 |
28 | )} 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /app/components/chat/ThoughtBox.tsx: -------------------------------------------------------------------------------- 1 | import { useState, type PropsWithChildren } from 'react'; 2 | 3 | const ThoughtBox = ({ title, children }: PropsWithChildren<{ title: string }>) => { 4 | const [isExpanded, setIsExpanded] = useState(false); 5 | 6 | return ( 7 |
setIsExpanded(!isExpanded)} 9 | className={` 10 | bg-bolt-elements-background-depth-2 11 | shadow-md 12 | rounded-lg 13 | cursor-pointer 14 | transition-all 15 | duration-300 16 | ${isExpanded ? 'max-h-96' : 'max-h-13'} 17 | overflow-auto 18 | border border-bolt-elements-borderColor 19 | `} 20 | > 21 |
22 |
23 |
24 | {title}{' '} 25 | {!isExpanded && - Click to expand} 26 |
27 |
28 |
37 | {children} 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default ThoughtBox; 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /electron/main/utils/vite-server.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import type { ViteDevServer } from 'vite'; 3 | 4 | let viteServer: ViteDevServer | undefined; 5 | 6 | // Conditionally import Vite only in development 7 | export async function initViteServer() { 8 | if (!(global.process.env.NODE_ENV === 'production' || app.isPackaged)) { 9 | const vite = await import('vite'); 10 | viteServer = await vite.createServer({ 11 | root: '.', 12 | envDir: process.cwd(), // load .env files from the root directory. 13 | }); 14 | } 15 | } 16 | 17 | /* 18 | * 19 | * take care of vite-dev-server. 20 | * 21 | */ 22 | app.on('before-quit', async (_event) => { 23 | if (!viteServer) { 24 | return; 25 | } 26 | 27 | /* 28 | * ref: https://stackoverflow.com/questions/68750716/electron-app-throwing-quit-unexpectedly-error-message-on-mac-when-quitting-the-a 29 | * event.preventDefault(); 30 | */ 31 | try { 32 | console.log('will close vite-dev-server.'); 33 | await viteServer.close(); 34 | console.log('closed vite-dev-server.'); 35 | 36 | // app.quit(); // Not working. causes recursively 'before-quit' events. 37 | app.exit(); // Not working expectedly SOMETIMES. Still throws exception and macOS shows dialog. 38 | // global.process.exit(0); // Not working well... I still see exceptional dialog. 39 | } catch (err) { 40 | console.log('failed to close Vite server:', err); 41 | } 42 | }); 43 | 44 | export { viteServer }; 45 | -------------------------------------------------------------------------------- /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 | import HyperbolicProvider from './providers/hyperbolic'; 17 | import AmazonBedrockProvider from './providers/amazon-bedrock'; 18 | import GithubProvider from './providers/github'; 19 | 20 | export { 21 | AnthropicProvider, 22 | CohereProvider, 23 | DeepseekProvider, 24 | GoogleProvider, 25 | GroqProvider, 26 | HuggingFaceProvider, 27 | HyperbolicProvider, 28 | MistralProvider, 29 | OllamaProvider, 30 | OpenAIProvider, 31 | OpenRouterProvider, 32 | OpenAILikeProvider, 33 | PerplexityProvider, 34 | XAIProvider, 35 | TogetherProvider, 36 | LMStudioProvider, 37 | AmazonBedrockProvider, 38 | GithubProvider, 39 | }; 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/clean.js: -------------------------------------------------------------------------------- 1 | import { rm, existsSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { execSync } from 'child_process'; 4 | import { fileURLToPath } from 'url'; 5 | import { dirname } from 'path'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | const dirsToRemove = ['node_modules/.vite', 'node_modules/.cache', '.cache', 'dist']; 11 | 12 | console.log('🧹 Cleaning project...'); 13 | 14 | // Remove directories 15 | for (const dir of dirsToRemove) { 16 | const fullPath = join(__dirname, '..', dir); 17 | 18 | try { 19 | if (existsSync(fullPath)) { 20 | console.log(`Removing ${dir}...`); 21 | rm(fullPath, { recursive: true, force: true }, (err) => { 22 | if (err) { 23 | console.error(`Error removing ${dir}:`, err.message); 24 | } 25 | }); 26 | } 27 | } catch (err) { 28 | console.error(`Error removing ${dir}:`, err.message); 29 | } 30 | } 31 | 32 | // Run pnpm commands 33 | console.log('\n📦 Reinstalling dependencies...'); 34 | 35 | try { 36 | execSync('pnpm install', { stdio: 'inherit' }); 37 | console.log('\n🗑️ Clearing pnpm cache...'); 38 | execSync('pnpm cache clean', { stdio: 'inherit' }); 39 | console.log('\n🏗️ Rebuilding project...'); 40 | execSync('pnpm build', { stdio: 'inherit' }); 41 | console.log('\n✨ Clean completed! You can now run pnpm dev'); 42 | } catch (err) { 43 | console.error('\n❌ Error during cleanup:', err.message); 44 | process.exit(1); 45 | } 46 | -------------------------------------------------------------------------------- /app/routes/api.check-env-key.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from '@remix-run/cloudflare'; 2 | import { LLMManager } from '~/lib/modules/llm/manager'; 3 | import { getApiKeysFromCookie } from '~/lib/api/cookies'; 4 | 5 | export const loader: LoaderFunction = async ({ context, request }) => { 6 | const url = new URL(request.url); 7 | const provider = url.searchParams.get('provider'); 8 | 9 | if (!provider) { 10 | return Response.json({ isSet: false }); 11 | } 12 | 13 | const llmManager = LLMManager.getInstance(context?.cloudflare?.env as any); 14 | const providerInstance = llmManager.getProvider(provider); 15 | 16 | if (!providerInstance || !providerInstance.config.apiTokenKey) { 17 | return Response.json({ isSet: false }); 18 | } 19 | 20 | const envVarName = providerInstance.config.apiTokenKey; 21 | 22 | // Get API keys from cookie 23 | const cookieHeader = request.headers.get('Cookie'); 24 | const apiKeys = getApiKeysFromCookie(cookieHeader); 25 | 26 | /* 27 | * Check API key in order of precedence: 28 | * 1. Client-side API keys (from cookies) 29 | * 2. Server environment variables (from Cloudflare env) 30 | * 3. Process environment variables (from .env.local) 31 | * 4. LLMManager environment variables 32 | */ 33 | const isSet = !!( 34 | apiKeys?.[provider] || 35 | (context?.cloudflare?.env as Record)?.[envVarName] || 36 | process.env[envVarName] || 37 | llmManager.env[envVarName] 38 | ); 39 | 40 | return Response.json({ isSet }); 41 | }; 42 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /electron/main/ui/window.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import path from 'node:path'; 3 | import { isDev } from '../utils/constants'; 4 | import { store } from '../utils/store'; 5 | 6 | export function createWindow(rendererURL: string) { 7 | console.log('Creating window with URL:', rendererURL); 8 | 9 | const bounds = store.get('bounds'); 10 | console.log('restored bounds:', bounds); 11 | 12 | const win = new BrowserWindow({ 13 | ...{ 14 | width: 1200, 15 | height: 800, 16 | ...bounds, 17 | }, 18 | vibrancy: 'under-window', 19 | visualEffectState: 'active', 20 | webPreferences: { 21 | preload: path.join(app.getAppPath(), 'build', 'electron', 'preload', 'index.cjs'), 22 | }, 23 | }); 24 | 25 | console.log('Window created, loading URL...'); 26 | win.loadURL(rendererURL).catch((err) => { 27 | console.log('Failed to load URL:', err); 28 | }); 29 | 30 | win.webContents.on('did-fail-load', (_, errorCode, errorDescription) => { 31 | console.log('Failed to load:', errorCode, errorDescription); 32 | }); 33 | 34 | win.webContents.on('did-finish-load', () => { 35 | console.log('Window finished loading'); 36 | }); 37 | 38 | // Open devtools in development 39 | if (isDev) { 40 | win.webContents.openDevTools(); 41 | } 42 | 43 | const boundsListener = () => { 44 | const bounds = win.getBounds(); 45 | store.set('bounds', bounds); 46 | }; 47 | win.on('moved', boundsListener); 48 | win.on('resized', boundsListener); 49 | 50 | return win; 51 | } 52 | -------------------------------------------------------------------------------- /scripts/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on any error 4 | set -e 5 | 6 | echo "Starting Bolt.DIY update process..." 7 | 8 | # Get the current directory 9 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 10 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 11 | 12 | # Store current version 13 | CURRENT_VERSION=$(cat "$PROJECT_ROOT/package.json" | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]') 14 | 15 | echo "Current version: $CURRENT_VERSION" 16 | echo "Fetching latest version..." 17 | 18 | # Create temp directory 19 | TMP_DIR=$(mktemp -d) 20 | cd "$TMP_DIR" 21 | 22 | # Download latest release 23 | LATEST_RELEASE_URL=$(curl -s https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d \") 24 | if [ -z "$LATEST_RELEASE_URL" ]; then 25 | echo "Error: Could not find latest release download URL" 26 | exit 1 27 | fi 28 | 29 | echo "Downloading latest release..." 30 | curl -L -o latest.zip "$LATEST_RELEASE_URL" 31 | 32 | echo "Extracting update..." 33 | unzip -q latest.zip 34 | 35 | # Backup current installation 36 | echo "Creating backup..." 37 | BACKUP_DIR="$PROJECT_ROOT/backup_$(date +%Y%m%d_%H%M%S)" 38 | mkdir -p "$BACKUP_DIR" 39 | cp -r "$PROJECT_ROOT"/* "$BACKUP_DIR/" 40 | 41 | # Install update 42 | echo "Installing update..." 43 | cp -r ./* "$PROJECT_ROOT/" 44 | 45 | # Clean up 46 | cd "$PROJECT_ROOT" 47 | rm -rf "$TMP_DIR" 48 | 49 | echo "Update completed successfully!" 50 | echo "Please restart the application to apply the changes." 51 | 52 | exit 0 53 | -------------------------------------------------------------------------------- /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., "Mon" instead of "Monday" 43 | return format(date, 'EEE'); 44 | } 45 | 46 | const thirtyDaysAgo = subDays(new Date(), 30); 47 | 48 | if (isAfter(date, thirtyDaysAgo)) { 49 | return 'Past 30 Days'; 50 | } 51 | 52 | if (isThisYear(date)) { 53 | // e.g., "Jan" instead of "January" 54 | return format(date, 'LLL'); 55 | } 56 | 57 | // e.g., "Jan 2023" instead of "January 2023" 58 | return format(date, 'LLL yyyy'); 59 | } 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icons/OpenAI.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /icons/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/types/actions.ts: -------------------------------------------------------------------------------- 1 | import type { Change } from 'diff'; 2 | 3 | export type ActionType = 'file' | 'shell' | 'supabase'; 4 | 5 | export interface BaseAction { 6 | content: string; 7 | } 8 | 9 | export interface FileAction extends BaseAction { 10 | type: 'file'; 11 | filePath: string; 12 | } 13 | 14 | export interface ShellAction extends BaseAction { 15 | type: 'shell'; 16 | } 17 | 18 | export interface StartAction extends BaseAction { 19 | type: 'start'; 20 | } 21 | 22 | export interface BuildAction extends BaseAction { 23 | type: 'build'; 24 | } 25 | 26 | export interface SupabaseAction extends BaseAction { 27 | type: 'supabase'; 28 | operation: 'migration' | 'query'; 29 | filePath?: string; 30 | projectId?: string; 31 | } 32 | 33 | export type BoltAction = FileAction | ShellAction | StartAction | BuildAction | SupabaseAction; 34 | 35 | export type BoltActionData = BoltAction | BaseAction; 36 | 37 | export interface ActionAlert { 38 | type: string; 39 | title: string; 40 | description: string; 41 | content: string; 42 | source?: 'terminal' | 'preview'; // Add source to differentiate between terminal and preview errors 43 | } 44 | 45 | export interface SupabaseAlert { 46 | type: string; 47 | title: string; 48 | description: string; 49 | content: string; 50 | source?: 'supabase'; 51 | } 52 | 53 | export interface FileHistory { 54 | originalContent: string; 55 | lastModified: number; 56 | changes: Change[]; 57 | versions: { 58 | timestamp: number; 59 | content: string; 60 | }[]; 61 | 62 | // Novo campo para rastrear a origem das mudanças 63 | changeSource?: 'user' | 'auto-save' | 'external'; 64 | } 65 | -------------------------------------------------------------------------------- /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 | supabase?: { 9 | isConnected: boolean; 10 | hasSelectedProject: boolean; 11 | credentials?: { 12 | anonKey?: string; 13 | supabaseUrl?: string; 14 | }; 15 | }; 16 | } 17 | 18 | export class PromptLibrary { 19 | static library: Record< 20 | string, 21 | { 22 | label: string; 23 | description: string; 24 | get: (options: PromptOptions) => string; 25 | } 26 | > = { 27 | default: { 28 | label: 'Default Prompt', 29 | description: 'This is the battle tested default system Prompt', 30 | get: (options) => getSystemPrompt(options.cwd, options.supabase), 31 | }, 32 | optimized: { 33 | label: 'Optimized Prompt (experimental)', 34 | description: 'an Experimental version of the prompt for lower token usage', 35 | get: (options) => optimized(options), 36 | }, 37 | }; 38 | static getList() { 39 | return Object.entries(this.library).map(([key, value]) => { 40 | const { label, description } = value; 41 | return { 42 | id: key, 43 | label, 44 | description, 45 | }; 46 | }); 47 | } 48 | static getPropmtFromLibrary(promptId: string, options: PromptOptions) { 49 | const prompt = this.library[promptId]; 50 | 51 | if (!prompt) { 52 | throw 'Prompt Now Found'; 53 | } 54 | 55 | return this.library[promptId]?.get(options); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.stackblitz.bolt.diy 2 | productName: Bolt Local 3 | directories: 4 | buildResources: build 5 | output: dist 6 | files: 7 | - build/**/* 8 | - package.json 9 | - node_modules/**/* 10 | - icons/** 11 | - electron-update.yml 12 | extraMetadata: 13 | main: build/electron/main/index.mjs 14 | asarUnpack: 15 | - resources/** 16 | - build/client/**/* 17 | - build/server/**/* 18 | - electron-update.yml 19 | 20 | mac: 21 | icon: assets/icons/icon.icns 22 | target: 23 | - dmg 24 | identity: "Xinzhe Wang (RDQSC33B2X)" 25 | category: "public.app-category.developer-tools" 26 | type: "distribution" 27 | hardenedRuntime: true 28 | entitlements: "assets/entitlements.mac.plist" 29 | entitlementsInherit: "assets/entitlements.mac.plist" 30 | gatekeeperAssess: false 31 | 32 | win: 33 | icon: assets/icons/icon.ico 34 | target: 35 | - nsis 36 | signDlls: false 37 | artifactName: ${name}-${version}-${os}-${arch}.${ext} 38 | 39 | linux: 40 | icon: assets/icons/icon.png 41 | target: 42 | - AppImage 43 | - deb 44 | artifactName: ${name}-${version}-${os}-${arch}.${ext} 45 | category: Development 46 | 47 | nsis: 48 | oneClick: false 49 | allowToChangeInstallationDirectory: true 50 | createDesktopShortcut: true 51 | createStartMenuShortcut: true 52 | shortcutName: ${productName} 53 | artifactName: ${name}-${version}-${os}-${arch}-setup.${ext} 54 | 55 | npmRebuild: false 56 | 57 | publish: 58 | provider: github 59 | owner: Derek-X-Wang 60 | repo: bolt.local 61 | private: true 62 | releaseType: release 63 | 64 | electronDownload: 65 | mirror: https://npmmirror.com/mirrors/electron/ 66 | -------------------------------------------------------------------------------- /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 | 31 | // Update the theme store 32 | themeStore.set(newTheme); 33 | 34 | // Update localStorage 35 | localStorage.setItem(kTheme, newTheme); 36 | 37 | // Update the HTML attribute 38 | document.querySelector('html')?.setAttribute('data-theme', newTheme); 39 | 40 | // Update user profile if it exists 41 | try { 42 | const userProfile = localStorage.getItem('bolt_user_profile'); 43 | 44 | if (userProfile) { 45 | const profile = JSON.parse(userProfile); 46 | profile.theme = newTheme; 47 | localStorage.setItem('bolt_user_profile', JSON.stringify(profile)); 48 | } 49 | } catch (error) { 50 | console.error('Error updating user profile theme:', error); 51 | } 52 | 53 | logStore.logSystem(`Theme changed to ${newTheme} mode`); 54 | } 55 | -------------------------------------------------------------------------------- /app/components/@settings/tabs/providers/service-status/providers/xai.ts: -------------------------------------------------------------------------------- 1 | import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider'; 2 | import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types'; 3 | 4 | export class XAIStatusChecker extends BaseProviderChecker { 5 | async checkStatus(): Promise { 6 | try { 7 | /* 8 | * Check API endpoint directly since XAI is a newer provider 9 | * and may not have a public status page yet 10 | */ 11 | const apiEndpoint = 'https://api.xai.com/v1/models'; 12 | const apiStatus = await this.checkEndpoint(apiEndpoint); 13 | 14 | // Check their website as a secondary indicator 15 | const websiteStatus = await this.checkEndpoint('https://x.ai'); 16 | 17 | let status: StatusCheckResult['status'] = 'operational'; 18 | let message = 'All systems operational'; 19 | 20 | if (apiStatus !== 'reachable' || websiteStatus !== 'reachable') { 21 | status = apiStatus !== 'reachable' ? 'down' : 'degraded'; 22 | message = apiStatus !== 'reachable' ? 'API appears to be down' : 'Service may be experiencing issues'; 23 | } 24 | 25 | return { 26 | status, 27 | message, 28 | incidents: [], // No public incident tracking available yet 29 | }; 30 | } catch (error) { 31 | console.error('Error checking XAI status:', error); 32 | 33 | return { 34 | status: 'degraded', 35 | message: 'Unable to determine service status', 36 | incidents: ['Note: Limited status information available'], 37 | }; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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 |
20 | 21 | ); 22 | 23 | const StarterTemplates: React.FC = () => { 24 | // Debug: Log available templates and their icons 25 | React.useEffect(() => { 26 | console.log( 27 | 'Available templates:', 28 | STARTER_TEMPLATES.map((t) => ({ name: t.name, icon: t.icon })), 29 | ); 30 | }, []); 31 | 32 | return ( 33 |
34 | or start a blank app with your favorite stack 35 |
36 |
37 | {STARTER_TEMPLATES.map((template) => ( 38 | 39 | ))} 40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | export default StarterTemplates; 47 | -------------------------------------------------------------------------------- /app/routes/api.system.git-info.ts: -------------------------------------------------------------------------------- 1 | import { json, type LoaderFunction } from '@remix-run/cloudflare'; 2 | 3 | interface GitInfo { 4 | local: { 5 | commitHash: string; 6 | branch: string; 7 | commitTime: string; 8 | author: string; 9 | email: string; 10 | remoteUrl: string; 11 | repoName: string; 12 | }; 13 | github?: { 14 | currentRepo?: { 15 | fullName: string; 16 | defaultBranch: string; 17 | stars: number; 18 | forks: number; 19 | openIssues?: number; 20 | }; 21 | }; 22 | isForked?: boolean; 23 | } 24 | 25 | // These values will be replaced at build time 26 | declare const __COMMIT_HASH: string; 27 | declare const __GIT_BRANCH: string; 28 | declare const __GIT_COMMIT_TIME: string; 29 | declare const __GIT_AUTHOR: string; 30 | declare const __GIT_EMAIL: string; 31 | declare const __GIT_REMOTE_URL: string; 32 | declare const __GIT_REPO_NAME: string; 33 | 34 | export const loader: LoaderFunction = async () => { 35 | const gitInfo: GitInfo = { 36 | local: { 37 | commitHash: typeof __COMMIT_HASH !== 'undefined' ? __COMMIT_HASH : 'development', 38 | branch: typeof __GIT_BRANCH !== 'undefined' ? __GIT_BRANCH : 'main', 39 | commitTime: typeof __GIT_COMMIT_TIME !== 'undefined' ? __GIT_COMMIT_TIME : new Date().toISOString(), 40 | author: typeof __GIT_AUTHOR !== 'undefined' ? __GIT_AUTHOR : 'development', 41 | email: typeof __GIT_EMAIL !== 'undefined' ? __GIT_EMAIL : 'development@local', 42 | remoteUrl: typeof __GIT_REMOTE_URL !== 'undefined' ? __GIT_REMOTE_URL : 'local', 43 | repoName: typeof __GIT_REPO_NAME !== 'undefined' ? __GIT_REPO_NAME : 'bolt.diy', 44 | }, 45 | }; 46 | 47 | return json(gitInfo); 48 | }; 49 | -------------------------------------------------------------------------------- /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/api/connection.ts: -------------------------------------------------------------------------------- 1 | export interface ConnectionStatus { 2 | connected: boolean; 3 | latency: number; 4 | lastChecked: string; 5 | } 6 | 7 | export const checkConnection = async (): Promise => { 8 | try { 9 | // Check if we have network connectivity 10 | const online = navigator.onLine; 11 | 12 | if (!online) { 13 | return { 14 | connected: false, 15 | latency: 0, 16 | lastChecked: new Date().toISOString(), 17 | }; 18 | } 19 | 20 | // Try multiple endpoints in case one fails 21 | const endpoints = [ 22 | '/api/health', 23 | '/', // Fallback to root route 24 | '/favicon.ico', // Another common fallback 25 | ]; 26 | 27 | let latency = 0; 28 | let connected = false; 29 | 30 | for (const endpoint of endpoints) { 31 | try { 32 | const start = performance.now(); 33 | const response = await fetch(endpoint, { 34 | method: 'HEAD', 35 | cache: 'no-cache', 36 | }); 37 | const end = performance.now(); 38 | 39 | if (response.ok) { 40 | latency = Math.round(end - start); 41 | connected = true; 42 | break; 43 | } 44 | } catch (endpointError) { 45 | console.debug(`Failed to connect to ${endpoint}:`, endpointError); 46 | continue; 47 | } 48 | } 49 | 50 | return { 51 | connected, 52 | latency, 53 | lastChecked: new Date().toISOString(), 54 | }; 55 | } catch (error) { 56 | console.error('Connection check failed:', error); 57 | return { 58 | connected: false, 59 | latency: 0, 60 | lastChecked: new Date().toISOString(), 61 | }; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /app/components/@settings/tabs/providers/service-status/providers/deepseek.ts: -------------------------------------------------------------------------------- 1 | import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider'; 2 | import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types'; 3 | 4 | export class DeepseekStatusChecker extends BaseProviderChecker { 5 | async checkStatus(): Promise { 6 | try { 7 | /* 8 | * Check status page - Note: Deepseek doesn't have a public status page yet 9 | * so we'll check their API endpoint directly 10 | */ 11 | const apiEndpoint = 'https://api.deepseek.com/v1/models'; 12 | const apiStatus = await this.checkEndpoint(apiEndpoint); 13 | 14 | // Check their website as a secondary indicator 15 | const websiteStatus = await this.checkEndpoint('https://deepseek.com'); 16 | 17 | let status: StatusCheckResult['status'] = 'operational'; 18 | let message = 'All systems operational'; 19 | 20 | if (apiStatus !== 'reachable' || websiteStatus !== 'reachable') { 21 | status = apiStatus !== 'reachable' ? 'down' : 'degraded'; 22 | message = apiStatus !== 'reachable' ? 'API appears to be down' : 'Service may be experiencing issues'; 23 | } 24 | 25 | return { 26 | status, 27 | message, 28 | incidents: [], // No public incident tracking available yet 29 | }; 30 | } catch (error) { 31 | console.error('Error checking Deepseek status:', error); 32 | 33 | return { 34 | status: 'degraded', 35 | message: 'Unable to determine service status', 36 | incidents: ['Note: Limited status information available'], 37 | }; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/components/@settings/tabs/providers/service-status/providers/hyperbolic.ts: -------------------------------------------------------------------------------- 1 | import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider'; 2 | import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types'; 3 | 4 | export class HyperbolicStatusChecker extends BaseProviderChecker { 5 | async checkStatus(): Promise { 6 | try { 7 | /* 8 | * Check API endpoint directly since Hyperbolic is a newer provider 9 | * and may not have a public status page yet 10 | */ 11 | const apiEndpoint = 'https://api.hyperbolic.ai/v1/models'; 12 | const apiStatus = await this.checkEndpoint(apiEndpoint); 13 | 14 | // Check their website as a secondary indicator 15 | const websiteStatus = await this.checkEndpoint('https://hyperbolic.ai'); 16 | 17 | let status: StatusCheckResult['status'] = 'operational'; 18 | let message = 'All systems operational'; 19 | 20 | if (apiStatus !== 'reachable' || websiteStatus !== 'reachable') { 21 | status = apiStatus !== 'reachable' ? 'down' : 'degraded'; 22 | message = apiStatus !== 'reachable' ? 'API appears to be down' : 'Service may be experiencing issues'; 23 | } 24 | 25 | return { 26 | status, 27 | message, 28 | incidents: [], // No public incident tracking available yet 29 | }; 30 | } catch (error) { 31 | console.error('Error checking Hyperbolic status:', error); 32 | 33 | return { 34 | status: 'degraded', 35 | message: 'Unable to determine service status', 36 | incidents: ['Note: Limited status information available'], 37 | }; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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/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 | const artifactRegex = /]*>[\s\S]*?<\/boltArtifact>/gm; 47 | return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '').replace(artifactRegex, ''); 48 | } 49 | -------------------------------------------------------------------------------- /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 { createDeepSeek } from '@ai-sdk/deepseek'; 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 | { name: 'deepseek-reasoner', label: 'Deepseek-Reasoner', provider: 'Deepseek', maxTokenAllowed: 8000 }, 19 | ]; 20 | 21 | getModelInstance(options: { 22 | model: string; 23 | serverEnv: Env; 24 | apiKeys?: Record; 25 | providerSettings?: Record; 26 | }): LanguageModelV1 { 27 | const { model, serverEnv, apiKeys, providerSettings } = options; 28 | 29 | const { apiKey } = this.getProviderBaseUrlAndKey({ 30 | apiKeys, 31 | providerSettings: providerSettings?.[this.name], 32 | serverEnv: serverEnv as any, 33 | defaultBaseUrlKey: '', 34 | defaultApiTokenKey: 'DEEPSEEK_API_KEY', 35 | }); 36 | 37 | if (!apiKey) { 38 | throw new Error(`Missing API key for ${this.name} provider`); 39 | } 40 | 41 | const deepseek = createDeepSeek({ 42 | apiKey, 43 | }); 44 | 45 | return deepseek(model, { 46 | // simulateStreaming: true, 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/components/@settings/tabs/connections/ConnectionsTab.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import React, { Suspense } from 'react'; 3 | 4 | // Use React.lazy for dynamic imports 5 | const GithubConnection = React.lazy(() => import('./GithubConnection')); 6 | const NetlifyConnection = React.lazy(() => import('./NetlifyConnection')); 7 | 8 | // Loading fallback component 9 | const LoadingFallback = () => ( 10 |
11 |
12 |
13 | Loading connection... 14 |
15 |
16 | ); 17 | 18 | export default function ConnectionsTab() { 19 | return ( 20 |
21 | {/* Header */} 22 | 28 |
29 |

Connection Settings

30 | 31 |

32 | Manage your external service connections and integrations 33 |

34 | 35 |
36 | }> 37 | 38 | 39 | }> 40 | 41 | 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/components/ui/ScrollArea.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; 5 | import { classNames } from '~/utils/classNames'; 6 | 7 | const ScrollArea = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, children, ...props }, ref) => ( 11 | 12 | {children} 13 | 14 | 15 | 16 | )); 17 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 18 | 19 | const ScrollBar = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, orientation = 'vertical', ...props }, ref) => ( 23 | 36 | 37 | 38 | )); 39 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 40 | 41 | export { ScrollArea, ScrollBar }; 42 | -------------------------------------------------------------------------------- /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/lib/api/notifications.ts: -------------------------------------------------------------------------------- 1 | import { logStore } from '~/lib/stores/logs'; 2 | import type { LogEntry } from '~/lib/stores/logs'; 3 | 4 | export interface Notification { 5 | id: string; 6 | title: string; 7 | message: string; 8 | type: 'info' | 'warning' | 'error' | 'success'; 9 | timestamp: string; 10 | read: boolean; 11 | details?: Record; 12 | } 13 | 14 | export interface LogEntryWithRead extends LogEntry { 15 | read: boolean; 16 | } 17 | 18 | export const getNotifications = async (): Promise => { 19 | // Get notifications from the log store 20 | const logs = Object.values(logStore.logs.get()); 21 | 22 | return logs 23 | .filter((log) => log.category !== 'system') // Filter out system logs 24 | .map((log) => ({ 25 | id: log.id, 26 | title: (log.details?.title as string) || log.message.split('\n')[0], 27 | message: log.message, 28 | type: log.level as 'info' | 'warning' | 'error' | 'success', 29 | timestamp: log.timestamp, 30 | read: logStore.isRead(log.id), 31 | details: log.details, 32 | })) 33 | .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); 34 | }; 35 | 36 | export const markNotificationRead = async (notificationId: string): Promise => { 37 | logStore.markAsRead(notificationId); 38 | }; 39 | 40 | export const clearNotifications = async (): Promise => { 41 | logStore.clearLogs(); 42 | }; 43 | 44 | export const getUnreadCount = (): number => { 45 | const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[]; 46 | 47 | return logs.filter((log) => { 48 | if (!logStore.isRead(log.id)) { 49 | if (log.details?.type === 'update') { 50 | return true; 51 | } 52 | 53 | return log.level === 'error' || log.level === 'warning'; 54 | } 55 | 56 | return false; 57 | }).length; 58 | }; 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build', '**/.history'], 8 | }, 9 | ...blitzPlugin.configs.recommended(), 10 | { 11 | rules: { 12 | '@blitz/catch-error-name': 'off', 13 | '@typescript-eslint/no-this-alias': 'off', 14 | '@typescript-eslint/no-empty-object-type': 'off', 15 | '@blitz/comment-syntax': 'off', 16 | '@blitz/block-scope-case': 'off', 17 | 'array-bracket-spacing': ['error', 'never'], 18 | 'object-curly-newline': ['error', { consistent: true }], 19 | 'keyword-spacing': ['error', { before: true, after: true }], 20 | 'consistent-return': 'error', 21 | semi: ['error', 'always'], 22 | curly: ['error'], 23 | 'no-eval': ['error'], 24 | 'linebreak-style': ['error', 'unix'], 25 | 'arrow-spacing': ['error', { before: true, after: true }], 26 | }, 27 | }, 28 | { 29 | files: ['**/*.tsx'], 30 | rules: { 31 | ...getNamingConventionRule({}, true), 32 | }, 33 | }, 34 | { 35 | files: ['**/*.d.ts'], 36 | rules: { 37 | '@typescript-eslint/no-empty-object-type': 'off', 38 | }, 39 | }, 40 | { 41 | files: [...tsFileExtensions, ...jsFileExtensions, '**/*.tsx'], 42 | ignores: ['functions/*', 'electron/**/*'], 43 | rules: { 44 | 'no-restricted-imports': [ 45 | 'error', 46 | { 47 | patterns: [ 48 | { 49 | group: ['../'], 50 | message: "Relative imports are not allowed. Please use '~/' instead.", 51 | }, 52 | ], 53 | }, 54 | ], 55 | }, 56 | }, 57 | ]; 58 | -------------------------------------------------------------------------------- /app/routes/api.supabase.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@remix-run/node'; 2 | import type { ActionFunction } from '@remix-run/node'; 3 | import type { SupabaseProject } from '~/types/supabase'; 4 | 5 | export const action: ActionFunction = async ({ request }) => { 6 | if (request.method !== 'POST') { 7 | return json({ error: 'Method not allowed' }, { status: 405 }); 8 | } 9 | 10 | try { 11 | const { token } = (await request.json()) as any; 12 | 13 | const projectsResponse = await fetch('https://api.supabase.com/v1/projects', { 14 | headers: { 15 | Authorization: `Bearer ${token}`, 16 | 'Content-Type': 'application/json', 17 | }, 18 | }); 19 | 20 | if (!projectsResponse.ok) { 21 | const errorText = await projectsResponse.text(); 22 | console.error('Projects fetch failed:', errorText); 23 | 24 | return json({ error: 'Failed to fetch projects' }, { status: 401 }); 25 | } 26 | 27 | const projects = (await projectsResponse.json()) as SupabaseProject[]; 28 | 29 | const uniqueProjectsMap = new Map(); 30 | 31 | for (const project of projects) { 32 | if (!uniqueProjectsMap.has(project.id)) { 33 | uniqueProjectsMap.set(project.id, project); 34 | } 35 | } 36 | 37 | const uniqueProjects = Array.from(uniqueProjectsMap.values()); 38 | 39 | uniqueProjects.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); 40 | 41 | return json({ 42 | user: { email: 'Connected', role: 'Admin' }, 43 | stats: { 44 | projects: uniqueProjects, 45 | totalProjects: uniqueProjects.length, 46 | }, 47 | }); 48 | } catch (error) { 49 | console.error('Supabase API error:', error); 50 | return json( 51 | { 52 | error: error instanceof Error ? error.message : 'Authentication failed', 53 | }, 54 | { status: 401 }, 55 | ); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /public/icons/AmazonBedrock.svg: -------------------------------------------------------------------------------- 1 | Bedrock -------------------------------------------------------------------------------- /app/lib/hooks/useNotifications.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications'; 3 | import { logStore } from '~/lib/stores/logs'; 4 | import { useStore } from '@nanostores/react'; 5 | 6 | export const useNotifications = () => { 7 | const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false); 8 | const [unreadNotifications, setUnreadNotifications] = useState([]); 9 | const logs = useStore(logStore.logs); 10 | 11 | const checkNotifications = async () => { 12 | try { 13 | const notifications = await getNotifications(); 14 | const unread = notifications.filter((n) => !logStore.isRead(n.id)); 15 | setUnreadNotifications(unread); 16 | setHasUnreadNotifications(unread.length > 0); 17 | } catch (error) { 18 | console.error('Failed to check notifications:', error); 19 | } 20 | }; 21 | 22 | useEffect(() => { 23 | // Check immediately and then every minute 24 | checkNotifications(); 25 | 26 | const interval = setInterval(checkNotifications, 60 * 1000); 27 | 28 | return () => clearInterval(interval); 29 | }, [logs]); // Re-run when logs change 30 | 31 | const markAsRead = async (notificationId: string) => { 32 | try { 33 | await markNotificationRead(notificationId); 34 | await checkNotifications(); 35 | } catch (error) { 36 | console.error('Failed to mark notification as read:', error); 37 | } 38 | }; 39 | 40 | const markAllAsRead = async () => { 41 | try { 42 | const notifications = await getNotifications(); 43 | await Promise.all(notifications.map((n) => markNotificationRead(n.id))); 44 | await checkNotifications(); 45 | } catch (error) { 46 | console.error('Failed to mark all notifications as read:', error); 47 | } 48 | }; 49 | 50 | return { hasUnreadNotifications, unreadNotifications, markAsRead, markAllAsRead }; 51 | }; 52 | -------------------------------------------------------------------------------- /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/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/components/chat/NetlifyDeploymentLink.client.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from '@nanostores/react'; 2 | import { netlifyConnection, fetchNetlifyStats } from '~/lib/stores/netlify'; 3 | import { chatId } from '~/lib/persistence/useChatHistory'; 4 | import * as Tooltip from '@radix-ui/react-tooltip'; 5 | import { useEffect } from 'react'; 6 | 7 | export function NetlifyDeploymentLink() { 8 | const connection = useStore(netlifyConnection); 9 | const currentChatId = useStore(chatId); 10 | 11 | useEffect(() => { 12 | if (connection.token && currentChatId) { 13 | fetchNetlifyStats(connection.token); 14 | } 15 | }, [connection.token, currentChatId]); 16 | 17 | const deployedSite = connection.stats?.sites?.find((site) => site.name.includes(`bolt-diy-${currentChatId}`)); 18 | 19 | if (!deployedSite) { 20 | return null; 21 | } 22 | 23 | return ( 24 | 25 | 26 | 27 | { 33 | e.stopPropagation(); // Add this to prevent click from bubbling up 34 | }} 35 | > 36 |
37 | 38 | 39 | 40 | 44 | {deployedSite.url} 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /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/components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | import { classNames } from '~/utils/classNames'; 4 | 5 | const buttonVariants = cva( 6 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-bolt-elements-borderColor disabled:pointer-events-none disabled:opacity-50', 7 | { 8 | variants: { 9 | variant: { 10 | default: 'bg-bolt-elements-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2', 11 | destructive: 'bg-red-500 text-white hover:bg-red-600', 12 | outline: 13 | 'border border-input bg-transparent hover:bg-bolt-elements-background-depth-2 hover:text-bolt-elements-textPrimary', 14 | secondary: 15 | 'bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2', 16 | ghost: 'hover:bg-bolt-elements-background-depth-1 hover:text-bolt-elements-textPrimary', 17 | link: 'text-bolt-elements-textPrimary underline-offset-4 hover:underline', 18 | }, 19 | size: { 20 | default: 'h-9 px-4 py-2', 21 | sm: 'h-8 rounded-md px-3 text-xs', 22 | lg: 'h-10 rounded-md px-8', 23 | icon: 'h-9 w-9', 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: 'default', 28 | size: 'default', 29 | }, 30 | }, 31 | ); 32 | 33 | export interface ButtonProps 34 | extends React.ButtonHTMLAttributes, 35 | VariantProps { 36 | _asChild?: boolean; 37 | } 38 | 39 | const Button = React.forwardRef( 40 | ({ className, variant, size, _asChild = false, ...props }, ref) => { 41 | return