Dominic Elm commited on
Commit
6927c07
·
0 Parent(s):

feat: initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/workflows/ci.yaml +35 -0
  2. .github/workflows/semantic-pr.yaml +32 -0
  3. .gitignore +23 -0
  4. .husky/pre-commit +1 -0
  5. .prettierignore +2 -0
  6. .prettierrc +8 -0
  7. .tool-versions +2 -0
  8. .vscode/launch.json +16 -0
  9. README.md +49 -0
  10. eslint.config.mjs +26 -0
  11. package.json +40 -0
  12. packages/bolt/.gitignore +8 -0
  13. packages/bolt/README.md +78 -0
  14. packages/bolt/app/components/Header.tsx +12 -0
  15. packages/bolt/app/components/chat/Artifact.tsx +29 -0
  16. packages/bolt/app/components/chat/AssistantMessage.tsx +14 -0
  17. packages/bolt/app/components/chat/BaseChat.tsx +140 -0
  18. packages/bolt/app/components/chat/Chat.client.tsx +123 -0
  19. packages/bolt/app/components/chat/CodeBlock.module.scss +10 -0
  20. packages/bolt/app/components/chat/CodeBlock.tsx +82 -0
  21. packages/bolt/app/components/chat/Markdown.module.scss +136 -0
  22. packages/bolt/app/components/chat/Markdown.tsx +66 -0
  23. packages/bolt/app/components/chat/Messages.tsx +55 -0
  24. packages/bolt/app/components/chat/SendButton.client.tsx +30 -0
  25. packages/bolt/app/components/chat/UserMessage.tsx +13 -0
  26. packages/bolt/app/components/ui/IconButton.tsx +70 -0
  27. packages/bolt/app/components/workspace/WorkspacePanel.tsx +3 -0
  28. packages/bolt/app/entry.client.tsx +12 -0
  29. packages/bolt/app/entry.server.tsx +34 -0
  30. packages/bolt/app/lib/.server/llm/api-key.ts +9 -0
  31. packages/bolt/app/lib/.server/llm/model.ts +9 -0
  32. packages/bolt/app/lib/.server/llm/prompts.ts +185 -0
  33. packages/bolt/app/lib/hooks/index.ts +2 -0
  34. packages/bolt/app/lib/hooks/useMessageParser.ts +57 -0
  35. packages/bolt/app/lib/hooks/usePromptEnhancer.ts +71 -0
  36. packages/bolt/app/lib/runtime/message-parser.spec.ts +84 -0
  37. packages/bolt/app/lib/runtime/message-parser.ts +172 -0
  38. packages/bolt/app/lib/stores/workspace.ts +42 -0
  39. packages/bolt/app/lib/webcontainer/index.ts +31 -0
  40. packages/bolt/app/root.tsx +51 -0
  41. packages/bolt/app/routes/_index.tsx +18 -0
  42. packages/bolt/app/routes/api.chat.ts +43 -0
  43. packages/bolt/app/routes/api.enhancer.ts +65 -0
  44. packages/bolt/app/styles/index.scss +31 -0
  45. packages/bolt/app/styles/variables.scss +21 -0
  46. packages/bolt/app/utils/classNames.ts +61 -0
  47. packages/bolt/app/utils/logger.ts +87 -0
  48. packages/bolt/app/utils/markdown.ts +6 -0
  49. packages/bolt/app/utils/promises.ts +19 -0
  50. packages/bolt/app/utils/stripIndent.ts +23 -0
.github/workflows/ci.yaml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
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
+ strategy:
14
+ matrix:
15
+ node-version: [18.20.3]
16
+ steps:
17
+ - name: Setup
18
+ uses: pnpm/action-setup@v4
19
+ with:
20
+ version: 9.4.0
21
+
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Install dependencies
26
+ run: pnpm install
27
+
28
+ - name: Run type check
29
+ run: pnpm run typecheck
30
+
31
+ - name: Run ESLint
32
+ run: pnpm run lint
33
+
34
+ - name: Run tests
35
+ run: pnpm run test
.github/workflows/semantic-pr.yaml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
.gitignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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?
.husky/pre-commit ADDED
@@ -0,0 +1 @@
 
 
1
+ npm test
.prettierignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ pnpm-lock.yaml
2
+ .astro
.prettierrc ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "printWidth": 120,
3
+ "singleQuote": true,
4
+ "useTabs": false,
5
+ "tabWidth": 2,
6
+ "semi": true,
7
+ "bracketSpacing": true
8
+ }
.tool-versions ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ nodejs 18.20.3
2
+ pnpm 9.4.0
.vscode/launch.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "type": "node",
6
+ "request": "launch",
7
+ "name": "Debug Current Test File",
8
+ "autoAttachChildProcesses": true,
9
+ "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
10
+ "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
11
+ "args": ["run", "${relativeFile}"],
12
+ "smartStep": true,
13
+ "console": "integratedTerminal"
14
+ }
15
+ ]
16
+ }
README.md ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Bolt Monorepo
2
+
3
+ Welcome to the Bolt monorepo! This repository contains the codebase for Bolt, an AI assistant developed by StackBlitz.
4
+
5
+ ## Repository Structure
6
+
7
+ Currently, this monorepo contains a single package:
8
+
9
+ - [`bolt`](packages/bolt): The main package containing the UI interface for Bolt as well as the server components.
10
+
11
+ As the project grows, additional packages may be added to this workspace.
12
+
13
+ ## Getting Started
14
+
15
+ ### Prerequisites
16
+
17
+ - Node.js (v18.20.3)
18
+ - pnpm (v9.4.0)
19
+
20
+ ### Installation
21
+
22
+ 1. Clone the repository:
23
+
24
+ ```bash
25
+ git clone https://github.com/stackblitz/bolt.git
26
+ cd bolt
27
+ ```
28
+
29
+ 2. Install dependencies:
30
+
31
+ ```bash
32
+ pnpm i
33
+ ```
34
+
35
+ ### Development
36
+
37
+ To start developing the Bolt UI:
38
+
39
+ 1. Navigate to the bolt package:
40
+
41
+ ```bash
42
+ cd packages/bolt
43
+ ```
44
+
45
+ 2. Start the development server:
46
+
47
+ ```bash
48
+ pnpm run dev
49
+ ```
eslint.config.mjs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import blitzPlugin from '@blitz/eslint-plugin';
2
+ import { getNamingConventionRule } from '@blitz/eslint-plugin/dist/configs/typescript.js';
3
+
4
+ export default [
5
+ {
6
+ ignores: ['**/dist', '**/node_modules'],
7
+ },
8
+ ...blitzPlugin.configs.recommended(),
9
+ {
10
+ rules: {
11
+ '@blitz/catch-error-name': 'off',
12
+ },
13
+ },
14
+ {
15
+ files: ['**/*.tsx'],
16
+ rules: {
17
+ ...getNamingConventionRule({}, true),
18
+ },
19
+ },
20
+ {
21
+ files: ['**/*.d.ts'],
22
+ rules: {
23
+ '@typescript-eslint/no-empty-object-type': 'off',
24
+ },
25
+ },
26
+ ];
package.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "private": true,
3
+ "license": "MIT",
4
+ "packageManager": "[email protected]",
5
+ "scripts": {
6
+ "playground:dev": "pnpm run --filter=playground dev",
7
+ "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
8
+ "test": "pnpm run -r test",
9
+ "typecheck": "pnpm run -r typecheck",
10
+ "prepare": "husky"
11
+ },
12
+ "lint-staged": {},
13
+ "commitlint": {
14
+ "extends": [
15
+ "@commitlint/config-conventional"
16
+ ]
17
+ },
18
+ "husky": {
19
+ "hooks": {
20
+ "pre-commit": "lint-staged",
21
+ "commit-msg": "commitlint --edit $1"
22
+ }
23
+ },
24
+ "engines": {
25
+ "node": ">=18.18.0",
26
+ "pnpm": "9.4.0"
27
+ },
28
+ "devDependencies": {
29
+ "@blitz/eslint-plugin": "0.1.0",
30
+ "@commitlint/config-conventional": "^19.2.2",
31
+ "commitlint": "^19.3.0",
32
+ "husky": "^9.0.11",
33
+ "is-ci": "^3.0.1",
34
+ "prettier": "^3.3.2",
35
+ "vitest": "^2.0.1"
36
+ },
37
+ "resolutions": {
38
+ "@typescript-eslint/utils": "^8.0.0-alpha.30"
39
+ }
40
+ }
packages/bolt/.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+
3
+ /.cache
4
+ /build
5
+ .env
6
+ *.vars
7
+ .wrangler
8
+ _worker.bundle
packages/bolt/README.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Bolt
2
+
3
+ Bolt is an AI assistant developed by StackBlitz. This package contains the UI interface for Bolt as well as the server components, built using [Remix Run](https://remix.run/).
4
+
5
+ ## Prerequisites
6
+
7
+ Before you begin, ensure you have the following installed:
8
+
9
+ - Node.js (v18.20.3)
10
+ - pnpm (v9.4.0)
11
+
12
+ ## Setup
13
+
14
+ 1. Clone the repository (if you haven't already):
15
+
16
+ ```bash
17
+ git clone https://github.com/stackblitz/bolt.git
18
+ cd bolt
19
+ ```
20
+
21
+ 2. Install dependencies:
22
+
23
+ ```bash
24
+ pnpm install
25
+ ```
26
+
27
+ 3. Create a `.env.local` file in the root of the bolt package directory and add your Anthropic API key:
28
+
29
+ ```
30
+ ANTHROPIC_API_KEY=XXX
31
+ ```
32
+
33
+ Optionally, you an set the debug level:
34
+
35
+ ```
36
+ VITE_LOG_LEVEL=debug
37
+ ```
38
+
39
+ **Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
40
+
41
+ ## Available Scripts
42
+
43
+ - `pnpm run dev`: Starts the development server.
44
+ - `pnpm run build`: Builds the project.
45
+ - `pnpm run start`: Runs the built application locally using Wrangler Pages. This script uses `bindings.sh` to set up necessary bindings so you don't have to duplicate environment variables.
46
+ - `pnpm run preview`: Builds the project and then starts it locally, useful for testing the production build. Note, HTTP streaming currently doesn't work as expected with `wrangler pages dev`.
47
+ - `pnpm test:` Runs the test suite using Vitest.
48
+ - `pnpm run typecheck`: Runs TypeScript type checking.
49
+ - `pnpm run typegen`: Generates TypeScript types using Wrangler.
50
+ - `pnpm run deploy`: Builds the project and deploys it to Cloudflare Pages.
51
+
52
+ ## Development
53
+
54
+ To start the development server:
55
+
56
+ ```bash
57
+ pnpm run dev
58
+ ```
59
+
60
+ This will start the Remix Vite development server.
61
+
62
+ ## Testing
63
+
64
+ Run the test suite with:
65
+
66
+ ```bash
67
+ pnpm test
68
+ ```
69
+
70
+ ## Deployment
71
+
72
+ To deploy the application to Cloudflare Pages:
73
+
74
+ ```bash
75
+ pnpm run deploy
76
+ ```
77
+
78
+ Make sure you have the necessary permissions and Wrangler is correctly configured for your Cloudflare account.
packages/bolt/app/components/Header.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IconButton } from './ui/IconButton';
2
+
3
+ export function Header() {
4
+ return (
5
+ <header className="flex items-center bg-white p-4 border-b border-gray-200">
6
+ <div className="flex items-center gap-2">
7
+ <div className="text-2xl font-semibold text-accent">Bolt</div>
8
+ </div>
9
+ <IconButton icon="i-ph:gear-duotone" className="ml-auto" />
10
+ </header>
11
+ );
12
+ }
packages/bolt/app/components/chat/Artifact.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useStore } from '@nanostores/react';
2
+ import { workspaceStore } from '~/lib/stores/workspace';
3
+
4
+ interface ArtifactProps {
5
+ messageId: string;
6
+ onClick?: () => void;
7
+ }
8
+
9
+ export function Artifact({ messageId, onClick }: ArtifactProps) {
10
+ const artifacts = useStore(workspaceStore.artifacts);
11
+
12
+ const artifact = artifacts[messageId];
13
+
14
+ return (
15
+ <button className="flex border rounded-lg overflow-hidden items-stretch bg-gray-50/25 w-full" onClick={onClick}>
16
+ <div className="border-r flex items-center px-6 bg-gray-50">
17
+ {!artifact?.closed ? (
18
+ <div className="i-svg-spinners:90-ring-with-bg scale-130"></div>
19
+ ) : (
20
+ <div className="i-ph:code-bold scale-130 text-gray-600"></div>
21
+ )}
22
+ </div>
23
+ <div className="flex flex-col items-center px-4 p-2.5">
24
+ <div className="text-left w-full">{artifact?.title}</div>
25
+ <small className="w-full text-left">Click to open code</small>
26
+ </div>
27
+ </button>
28
+ );
29
+ }
packages/bolt/app/components/chat/AssistantMessage.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo } from 'react';
2
+ import { Markdown } from './Markdown';
3
+
4
+ interface AssistantMessageProps {
5
+ content: string;
6
+ }
7
+
8
+ export const AssistantMessage = memo(({ content }: AssistantMessageProps) => {
9
+ return (
10
+ <div className="overflow-hidden w-full">
11
+ <Markdown>{content}</Markdown>
12
+ </div>
13
+ );
14
+ });
packages/bolt/app/components/chat/BaseChat.tsx ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { LegacyRef } from 'react';
2
+ import React from 'react';
3
+ import { ClientOnly } from 'remix-utils/client-only';
4
+ import { IconButton } from '~/components/ui/IconButton';
5
+ import { classNames } from '~/utils/classNames';
6
+ import { SendButton } from './SendButton.client';
7
+
8
+ interface BaseChatProps {
9
+ textareaRef?: LegacyRef<HTMLTextAreaElement> | undefined;
10
+ messagesSlot?: React.ReactNode;
11
+ workspaceSlot?: React.ReactNode;
12
+ chatStarted?: boolean;
13
+ enhancingPrompt?: boolean;
14
+ promptEnhanced?: boolean;
15
+ input?: string;
16
+ sendMessage?: () => void;
17
+ handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
18
+ enhancePrompt?: () => void;
19
+ }
20
+
21
+ const EXAMPLES = [{ text: 'Example' }, { text: 'Example' }, { text: 'Example' }, { text: 'Example' }];
22
+
23
+ const TEXTAREA_MIN_HEIGHT = 72;
24
+
25
+ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
26
+ (
27
+ {
28
+ textareaRef,
29
+ chatStarted = false,
30
+ enhancingPrompt = false,
31
+ promptEnhanced = false,
32
+ messagesSlot,
33
+ workspaceSlot,
34
+ input = '',
35
+ sendMessage,
36
+ handleInputChange,
37
+ enhancePrompt,
38
+ },
39
+ ref,
40
+ ) => {
41
+ const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
42
+
43
+ return (
44
+ <div ref={ref} className="h-full flex w-full overflow-scroll px-6">
45
+ <div className="flex flex-col items-center w-full h-full">
46
+ <div id="chat" className="w-full">
47
+ {!chatStarted && (
48
+ <div id="intro" className="mt-[20vh] mb-14 max-w-2xl mx-auto">
49
+ <h2 className="text-4xl text-center font-bold text-slate-800 mb-2">Where ideas begin.</h2>
50
+ <p className="mb-14 text-center">Bring ideas to life in seconds or get help on existing projects.</p>
51
+ <div className="grid max-md:grid-cols-[repeat(2,1fr)] md:grid-cols-[repeat(2,minmax(200px,1fr))] gap-4">
52
+ {EXAMPLES.map((suggestion, index) => (
53
+ <button key={index} className="p-4 rounded-lg shadow-xs bg-white border border-gray-200 text-left">
54
+ {suggestion.text}
55
+ </button>
56
+ ))}
57
+ </div>
58
+ </div>
59
+ )}
60
+ {messagesSlot}
61
+ </div>
62
+ <div
63
+ className={classNames('w-full md:max-w-[720px] mx-auto', {
64
+ 'fixed bg-bolt-elements-app-backgroundColor bottom-0': chatStarted,
65
+ })}
66
+ >
67
+ <div
68
+ className={classNames(
69
+ 'relative shadow-sm border border-gray-200 md:mb-6 bg-white rounded-lg overflow-hidden',
70
+ {
71
+ 'max-md:rounded-none max-md:border-x-none': chatStarted,
72
+ },
73
+ )}
74
+ >
75
+ <textarea
76
+ ref={textareaRef}
77
+ onKeyDown={(event) => {
78
+ if (event.key === 'Enter') {
79
+ if (event.shiftKey) {
80
+ return;
81
+ }
82
+
83
+ event.preventDefault();
84
+
85
+ sendMessage?.();
86
+ }
87
+ }}
88
+ value={input}
89
+ onChange={(event) => {
90
+ handleInputChange?.(event);
91
+ }}
92
+ className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none`}
93
+ style={{
94
+ minHeight: TEXTAREA_MIN_HEIGHT,
95
+ maxHeight: TEXTAREA_MAX_HEIGHT,
96
+ }}
97
+ placeholder="How can Bolt help you today?"
98
+ translate="no"
99
+ />
100
+ <ClientOnly>{() => <SendButton show={input.length > 0} onClick={sendMessage} />}</ClientOnly>
101
+ <div className="flex justify-between text-sm p-4 pt-2">
102
+ <div className="flex gap-1 items-center">
103
+ <IconButton icon="i-ph:microphone-duotone" className="-ml-1" />
104
+ <IconButton icon="i-ph:plus-circle-duotone" />
105
+ <IconButton
106
+ disabled={input.length === 0 || enhancingPrompt}
107
+ className={classNames({
108
+ 'opacity-100!': enhancingPrompt,
109
+ 'text-accent! pr-1.5': promptEnhanced,
110
+ })}
111
+ onClick={() => enhancePrompt?.()}
112
+ >
113
+ {enhancingPrompt ? (
114
+ <>
115
+ <div className="i-svg-spinners:90-ring-with-bg text-black text-xl"></div>
116
+ <div className="ml-1.5">Enhancing prompt...</div>
117
+ </>
118
+ ) : (
119
+ <>
120
+ <div className="i-blitz:stars text-xl"></div>
121
+ {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
122
+ </>
123
+ )}
124
+ </IconButton>
125
+ </div>
126
+ {input.length > 3 ? (
127
+ <div className="text-xs">
128
+ Use <kbd className="bg-gray-100 p-1 rounded-md">Shift</kbd> +{' '}
129
+ <kbd className="bg-gray-100 p-1 rounded-md">Return</kbd> for a new line
130
+ </div>
131
+ ) : null}
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ {workspaceSlot}
137
+ </div>
138
+ );
139
+ },
140
+ );
packages/bolt/app/components/chat/Chat.client.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useChat } from 'ai/react';
2
+ import { cubicBezier, useAnimate } from 'framer-motion';
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { useMessageParser, usePromptEnhancer } from '~/lib/hooks';
5
+ import { createScopedLogger } from '~/utils/logger';
6
+ import { BaseChat } from './BaseChat';
7
+ import { Messages } from './Messages';
8
+
9
+ const logger = createScopedLogger('Chat');
10
+ const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
11
+
12
+ export function Chat() {
13
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
14
+
15
+ const [chatStarted, setChatStarted] = useState(false);
16
+
17
+ const [animationScope, animate] = useAnimate();
18
+
19
+ const { messages, isLoading, input, handleInputChange, setInput, handleSubmit } = useChat({
20
+ api: '/api/chat',
21
+ onError: (error) => {
22
+ logger.error(error);
23
+ },
24
+ onFinish: () => {
25
+ logger.debug('Finished streaming');
26
+ },
27
+ });
28
+
29
+ const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
30
+ const { parsedMessages, parseMessages } = useMessageParser();
31
+
32
+ const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
33
+
34
+ useEffect(() => {
35
+ parseMessages(messages, isLoading);
36
+ }, [messages, isLoading, parseMessages]);
37
+
38
+ const scrollTextArea = () => {
39
+ const textarea = textareaRef.current;
40
+
41
+ if (textarea) {
42
+ textarea.scrollTop = textarea.scrollHeight;
43
+ }
44
+ };
45
+
46
+ useEffect(() => {
47
+ const textarea = textareaRef.current;
48
+
49
+ if (textarea) {
50
+ textarea.style.height = 'auto';
51
+
52
+ const scrollHeight = textarea.scrollHeight;
53
+
54
+ textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`;
55
+ textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';
56
+ }
57
+ }, [input, textareaRef]);
58
+
59
+ const runAnimation = async () => {
60
+ if (chatStarted) {
61
+ return;
62
+ }
63
+
64
+ await Promise.all([
65
+ animate('#chat', { height: '100%' }, { duration: 0.3, ease: customEasingFn }),
66
+ animate('#intro', { opacity: 0, display: 'none' }, { duration: 0.15, ease: customEasingFn }),
67
+ ]);
68
+
69
+ setChatStarted(true);
70
+ };
71
+
72
+ const sendMessage = () => {
73
+ if (input.length === 0) {
74
+ return;
75
+ }
76
+
77
+ runAnimation();
78
+ handleSubmit();
79
+ resetEnhancer();
80
+
81
+ textareaRef.current?.blur();
82
+ };
83
+
84
+ return (
85
+ <BaseChat
86
+ ref={animationScope}
87
+ textareaRef={textareaRef}
88
+ input={input}
89
+ chatStarted={chatStarted}
90
+ enhancingPrompt={enhancingPrompt}
91
+ promptEnhanced={promptEnhanced}
92
+ sendMessage={sendMessage}
93
+ handleInputChange={handleInputChange}
94
+ messagesSlot={
95
+ chatStarted ? (
96
+ <Messages
97
+ classNames={{
98
+ root: 'h-full pt-10',
99
+ messagesContainer: 'max-w-2xl mx-auto max-md:pb-[calc(140px+1.5rem)] md:pb-[calc(140px+3rem)]',
100
+ }}
101
+ messages={messages.map((message, i) => {
102
+ if (message.role === 'user') {
103
+ return message;
104
+ }
105
+
106
+ return {
107
+ ...message,
108
+ content: parsedMessages[i] || '',
109
+ };
110
+ })}
111
+ isLoading={isLoading}
112
+ />
113
+ ) : null
114
+ }
115
+ enhancePrompt={() => {
116
+ enhancePrompt(input, (input) => {
117
+ setInput(input);
118
+ scrollTextArea();
119
+ });
120
+ }}
121
+ ></BaseChat>
122
+ );
123
+ }
packages/bolt/app/components/chat/CodeBlock.module.scss ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
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
+ }
packages/bolt/app/components/chat/CodeBlock.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo, useEffect, useState } from 'react';
2
+ import {
3
+ bundledLanguages,
4
+ codeToHtml,
5
+ isSpecialLang,
6
+ type BundledLanguage,
7
+ type BundledTheme,
8
+ type SpecialLanguage,
9
+ } from 'shiki';
10
+ import { classNames } from '~/utils/classNames';
11
+ import { createScopedLogger } from '~/utils/logger';
12
+ import styles from './CodeBlock.module.scss';
13
+
14
+ const logger = createScopedLogger('CodeBlock');
15
+
16
+ interface CodeBlockProps {
17
+ code: string;
18
+ language?: BundledLanguage;
19
+ theme?: BundledTheme | SpecialLanguage;
20
+ }
21
+
22
+ export const CodeBlock = memo(({ code, language, theme }: CodeBlockProps) => {
23
+ const [html, setHTML] = useState<string | undefined>(undefined);
24
+ const [copied, setCopied] = useState(false);
25
+
26
+ const copyToClipboard = () => {
27
+ if (copied) {
28
+ return;
29
+ }
30
+
31
+ navigator.clipboard.writeText(code);
32
+
33
+ setCopied(true);
34
+
35
+ setTimeout(() => {
36
+ setCopied(false);
37
+ }, 2000);
38
+ };
39
+
40
+ useEffect(() => {
41
+ if (language && !isSpecialLang(language) && !(language in bundledLanguages)) {
42
+ logger.warn(`Unsupported language '${language}'`);
43
+ }
44
+
45
+ logger.trace(`Language = ${language}`);
46
+
47
+ const processCode = async () => {
48
+ setHTML(await codeToHtml(code, { lang: language ?? 'plaintext', theme: theme ?? 'dark-plus' }));
49
+ };
50
+
51
+ processCode();
52
+ }, [code]);
53
+
54
+ return (
55
+ <div className="relative group">
56
+ <div
57
+ className={classNames(
58
+ styles.CopyButtonContainer,
59
+ 'bg-white absolute top-[10px] right-[10px] rounded-md z-10 text-lg flex items-center justify-center opacity-0 group-hover:opacity-100',
60
+ {
61
+ 'rounded-l-0 opacity-100': copied,
62
+ },
63
+ )}
64
+ >
65
+ <button
66
+ className={classNames(
67
+ 'flex items-center p-[6px] justify-center before:bg-white before:rounded-l-md before:text-gray-500 before:border-r before:border-gray-300',
68
+ {
69
+ 'before:opacity-0': !copied,
70
+ 'before:opacity-100': copied,
71
+ },
72
+ )}
73
+ title="Copy Code"
74
+ onClick={() => copyToClipboard()}
75
+ >
76
+ <div className="i-ph:clipboard-text-duotone"></div>
77
+ </button>
78
+ </div>
79
+ <div dangerouslySetInnerHTML={{ __html: html ?? '' }}></div>
80
+ </div>
81
+ );
82
+ });
packages/bolt/app/components/chat/Markdown.module.scss ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ $font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace,
2
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
3
+ $color-text: #333;
4
+ $color-heading: #2c3e50;
5
+ $color-link: #3498db;
6
+ $color-code-bg: #f8f8f8;
7
+ $color-blockquote-border: #dfe2e5;
8
+
9
+ .MarkdownContent {
10
+ line-height: 1.6;
11
+ color: $color-text;
12
+
13
+ > *:not(:last-child) {
14
+ margin-bottom: 16px;
15
+ }
16
+
17
+ :is(h1, h2, h3, h4, h5, h6) {
18
+ margin-top: 24px;
19
+ margin-bottom: 16px;
20
+ font-weight: 600;
21
+ line-height: 1.25;
22
+ color: $color-heading;
23
+ }
24
+
25
+ h1 {
26
+ font-size: 2em;
27
+ border-bottom: 1px solid #eaecef;
28
+ padding-bottom: 0.3em;
29
+ }
30
+
31
+ h2 {
32
+ font-size: 1.5em;
33
+ border-bottom: 1px solid #eaecef;
34
+ padding-bottom: 0.3em;
35
+ }
36
+
37
+ h3 {
38
+ font-size: 1.25em;
39
+ }
40
+
41
+ h4 {
42
+ font-size: 1em;
43
+ }
44
+
45
+ h5 {
46
+ font-size: 0.875em;
47
+ }
48
+
49
+ h6 {
50
+ font-size: 0.85em;
51
+ color: #6a737d;
52
+ }
53
+
54
+ p:not(:last-of-type) {
55
+ margin-top: 0;
56
+ margin-bottom: 16px;
57
+ }
58
+
59
+ a {
60
+ color: $color-link;
61
+ text-decoration: none;
62
+
63
+ &:hover {
64
+ text-decoration: underline;
65
+ }
66
+ }
67
+
68
+ :not(pre) > code {
69
+ font-family: $font-mono;
70
+ font-size: 14px;
71
+ background-color: $color-code-bg;
72
+ border-radius: 6px;
73
+ padding: 0.2em 0.4em;
74
+ }
75
+
76
+ pre {
77
+ padding: 20px 16px;
78
+ border-radius: 6px;
79
+ }
80
+
81
+ pre:has(> code) {
82
+ font-family: $font-mono;
83
+ font-size: 14px;
84
+ background: transparent;
85
+ overflow-x: auto;
86
+ }
87
+
88
+ blockquote {
89
+ margin: 0;
90
+ padding: 0 1em;
91
+ color: #6a737d;
92
+ border-left: 0.25em solid $color-blockquote-border;
93
+ }
94
+
95
+ :is(ul, ol) {
96
+ padding-left: 2em;
97
+ margin-top: 0;
98
+ margin-bottom: 16px;
99
+ }
100
+
101
+ ul {
102
+ list-style-type: disc;
103
+ }
104
+
105
+ ol {
106
+ list-style-type: decimal;
107
+ }
108
+
109
+ img {
110
+ max-width: 100%;
111
+ box-sizing: border-box;
112
+ }
113
+
114
+ hr {
115
+ height: 0.25em;
116
+ padding: 0;
117
+ margin: 24px 0;
118
+ background-color: #e1e4e8;
119
+ border: 0;
120
+ }
121
+
122
+ table {
123
+ border-collapse: collapse;
124
+ width: 100%;
125
+ margin-bottom: 16px;
126
+
127
+ :is(th, td) {
128
+ padding: 6px 13px;
129
+ border: 1px solid #dfe2e5;
130
+ }
131
+
132
+ tr:nth-child(2n) {
133
+ background-color: #f6f8fa;
134
+ }
135
+ }
136
+ }
packages/bolt/app/components/chat/Markdown.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import type { BundledLanguage } from 'shiki';
4
+ import { createScopedLogger } from '~/utils/logger';
5
+ import { rehypePlugins, remarkPlugins } from '~/utils/markdown';
6
+ import { Artifact } from './Artifact';
7
+ import { CodeBlock } from './CodeBlock';
8
+ import styles from './Markdown.module.scss';
9
+
10
+ const logger = createScopedLogger('MarkdownComponent');
11
+
12
+ interface MarkdownProps {
13
+ children: string;
14
+ }
15
+
16
+ export const Markdown = memo(({ children }: MarkdownProps) => {
17
+ logger.trace('Render');
18
+
19
+ return (
20
+ <ReactMarkdown
21
+ className={styles.MarkdownContent}
22
+ components={{
23
+ div: ({ className, children, node, ...props }) => {
24
+ if (className?.includes('__boltArtifact__')) {
25
+ const messageId = node?.properties.dataMessageId as string;
26
+
27
+ if (!messageId) {
28
+ logger.warn(`Invalud message id ${messageId}`);
29
+ }
30
+
31
+ return <Artifact messageId={messageId} />;
32
+ }
33
+
34
+ return (
35
+ <div className={className} {...props}>
36
+ {children}
37
+ </div>
38
+ );
39
+ },
40
+ pre: (props) => {
41
+ const { children, node, ...rest } = props;
42
+
43
+ const [firstChild] = node?.children ?? [];
44
+
45
+ if (
46
+ firstChild &&
47
+ firstChild.type === 'element' &&
48
+ firstChild.tagName === 'code' &&
49
+ firstChild.children[0].type === 'text'
50
+ ) {
51
+ const { className, ...rest } = firstChild.properties;
52
+ const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? [];
53
+
54
+ return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;
55
+ }
56
+
57
+ return <pre {...rest}>{children}</pre>;
58
+ },
59
+ }}
60
+ remarkPlugins={remarkPlugins}
61
+ rehypePlugins={rehypePlugins}
62
+ >
63
+ {children}
64
+ </ReactMarkdown>
65
+ );
66
+ });
packages/bolt/app/components/chat/Messages.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from 'ai';
2
+ import { useRef } from 'react';
3
+ import { classNames } from '~/utils/classNames';
4
+ import { AssistantMessage } from './AssistantMessage';
5
+ import { UserMessage } from './UserMessage';
6
+
7
+ interface MessagesProps {
8
+ id?: string;
9
+ classNames?: { root?: string; messagesContainer?: string };
10
+ isLoading?: boolean;
11
+ messages?: Message[];
12
+ }
13
+
14
+ export function Messages(props: MessagesProps) {
15
+ const { id, isLoading, messages = [] } = props;
16
+
17
+ const containerRef = useRef<HTMLDivElement>(null);
18
+
19
+ return (
20
+ <div id={id} ref={containerRef} className={props.classNames?.root}>
21
+ <div className={classNames('flex flex-col', props.classNames?.messagesContainer)}>
22
+ {messages.length > 0
23
+ ? messages.map((message, i) => {
24
+ const { role, content } = message;
25
+ const isUser = role === 'user';
26
+ const isFirst = i === 0;
27
+
28
+ return (
29
+ <div
30
+ key={message.id}
31
+ className={classNames('flex gap-4 border rounded-md p-6 bg-white/80 backdrop-blur-sm', {
32
+ 'mt-4': !isFirst,
33
+ })}
34
+ >
35
+ <div
36
+ className={classNames(
37
+ 'flex items-center justify-center min-w-[34px] min-h-[34px] text-gray-600 rounded-md p-1 self-start',
38
+ {
39
+ 'bg-gray-100': role === 'user',
40
+ 'bg-accent text-xl': role === 'assistant',
41
+ },
42
+ )}
43
+ >
44
+ <div className={role === 'user' ? 'i-ph:user-fill text-xl' : 'i-blitz:logo'}></div>
45
+ </div>
46
+ {isUser ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
47
+ </div>
48
+ );
49
+ })
50
+ : null}
51
+ {isLoading && <div className="text-center w-full i-svg-spinners:3-dots-fade text-4xl mt-4"></div>}
52
+ </div>
53
+ </div>
54
+ );
55
+ }
packages/bolt/app/components/chat/SendButton.client.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AnimatePresence, cubicBezier, motion } from 'framer-motion';
2
+
3
+ interface SendButtonProps {
4
+ show: boolean;
5
+ onClick?: VoidFunction;
6
+ }
7
+
8
+ const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
9
+
10
+ export function SendButton({ show, onClick }: SendButtonProps) {
11
+ return (
12
+ <AnimatePresence>
13
+ {show ? (
14
+ <motion.button
15
+ className="absolute flex justify-center items-center top-[18px] right-[22px] p-1 bg-accent hover:brightness-110 color-white rounded-md w-[34px] h-[34px]"
16
+ transition={{ ease: customEasingFn, duration: 0.17 }}
17
+ initial={{ opacity: 0, y: 10 }}
18
+ animate={{ opacity: 1, y: 0 }}
19
+ exit={{ opacity: 0, y: 10 }}
20
+ onClick={(event) => {
21
+ event.preventDefault();
22
+ onClick?.();
23
+ }}
24
+ >
25
+ <div className="i-ph:arrow-right text-xl"></div>
26
+ </motion.button>
27
+ ) : null}
28
+ </AnimatePresence>
29
+ );
30
+ }
packages/bolt/app/components/chat/UserMessage.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Markdown } from './Markdown';
2
+
3
+ interface UserMessageProps {
4
+ content: string;
5
+ }
6
+
7
+ export function UserMessage({ content }: UserMessageProps) {
8
+ return (
9
+ <div className="overflow-hidden">
10
+ <Markdown>{content}</Markdown>
11
+ </div>
12
+ );
13
+ }
packages/bolt/app/components/ui/IconButton.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo } from 'react';
2
+ import { classNames } from '~/utils/classNames';
3
+
4
+ type IconSize = 'sm' | 'md' | 'xl';
5
+
6
+ interface BaseIconButtonProps {
7
+ size?: IconSize;
8
+ className?: string;
9
+ iconClassName?: string;
10
+ disabledClassName?: string;
11
+ disabled?: boolean;
12
+ onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
13
+ }
14
+
15
+ type IconButtonWithoutChildrenProps = {
16
+ icon: string;
17
+ children?: undefined;
18
+ } & BaseIconButtonProps;
19
+
20
+ type IconButtonWithChildrenProps = {
21
+ icon?: undefined;
22
+ children: string | JSX.Element | JSX.Element[];
23
+ } & BaseIconButtonProps;
24
+
25
+ type IconButtonProps = IconButtonWithoutChildrenProps | IconButtonWithChildrenProps;
26
+
27
+ export const IconButton = memo(
28
+ ({
29
+ icon,
30
+ size = 'xl',
31
+ className,
32
+ iconClassName,
33
+ disabledClassName,
34
+ disabled = false,
35
+ onClick,
36
+ children,
37
+ }: IconButtonProps) => {
38
+ return (
39
+ <button
40
+ className={classNames(
41
+ 'flex items-center text-gray-600 enabled:hover:text-gray-900 rounded-md p-1 enabled:hover:bg-gray-200/80 disabled:cursor-not-allowed',
42
+ {
43
+ [classNames('opacity-30', disabledClassName)]: disabled,
44
+ },
45
+ className,
46
+ )}
47
+ disabled={disabled}
48
+ onClick={(event) => {
49
+ if (disabled) {
50
+ return;
51
+ }
52
+
53
+ onClick?.(event);
54
+ }}
55
+ >
56
+ {children ? children : <div className={classNames(icon, getIconSize(size), iconClassName)}></div>}
57
+ </button>
58
+ );
59
+ },
60
+ );
61
+
62
+ function getIconSize(size: IconSize) {
63
+ if (size === 'sm') {
64
+ return 'text-sm';
65
+ } else if (size === 'md') {
66
+ return 'text-md';
67
+ } else {
68
+ return 'text-xl';
69
+ }
70
+ }
packages/bolt/app/components/workspace/WorkspacePanel.tsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export function Workspace() {
2
+ return <div>WORKSPACE PANEL</div>;
3
+ }
packages/bolt/app/entry.client.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RemixBrowser } from '@remix-run/react';
2
+ import { startTransition, StrictMode } from 'react';
3
+ import { hydrateRoot } from 'react-dom/client';
4
+
5
+ startTransition(() => {
6
+ hydrateRoot(
7
+ document,
8
+ <StrictMode>
9
+ <RemixBrowser />
10
+ </StrictMode>,
11
+ );
12
+ });
packages/bolt/app/entry.server.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare';
2
+ import { RemixServer } from '@remix-run/react';
3
+ import { isbot } from 'isbot';
4
+ import { renderToReadableStream } from 'react-dom/server';
5
+
6
+ export default async function handleRequest(
7
+ request: Request,
8
+ responseStatusCode: number,
9
+ responseHeaders: Headers,
10
+ remixContext: EntryContext,
11
+ _loadContext: AppLoadContext,
12
+ ) {
13
+ const body = await renderToReadableStream(<RemixServer context={remixContext} url={request.url} />, {
14
+ signal: request.signal,
15
+ onError(error: unknown) {
16
+ console.error(error);
17
+ responseStatusCode = 500;
18
+ },
19
+ });
20
+
21
+ if (isbot(request.headers.get('user-agent') || '')) {
22
+ await body.allReady;
23
+ }
24
+
25
+ responseHeaders.set('Content-Type', 'text/html');
26
+
27
+ responseHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
28
+ responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
29
+
30
+ return new Response(body, {
31
+ headers: responseHeaders,
32
+ status: responseStatusCode,
33
+ });
34
+ }
packages/bolt/app/lib/.server/llm/api-key.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { env } from 'node:process';
2
+
3
+ export function getAPIKey(cloudflareEnv: Env) {
4
+ /**
5
+ * The `cloudflareEnv` is only used when deployed or when previewing locally.
6
+ * In development the environment variables are available through `env`.
7
+ */
8
+ return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
9
+ }
packages/bolt/app/lib/.server/llm/model.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { createAnthropic } from '@ai-sdk/anthropic';
2
+
3
+ export function getAnthropicModel(apiKey: string) {
4
+ const anthropic = createAnthropic({
5
+ apiKey,
6
+ });
7
+
8
+ return anthropic('claude-3-5-sonnet-20240620');
9
+ }
packages/bolt/app/lib/.server/llm/prompts.ts ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const systemPrompt = `
2
+ You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.
3
+
4
+ <system_constraints>
5
+ You are operating in an environment called WebContainer, an in-browser Node.js runtime that emulates a Linux system to some degree. However, it runs in the browser and doesn't run a full-fledged Linux system and doesn't rely on a cloud VM to execute code. All code is executed in the browser. It does come with a shell that emulates zsh. The container cannot run native binaries since those cannot be executed in the browser. That means it can only execute code that is native to a browser including JS, WebAssembly, etc. The shell comes with a \`python\` binary but it CANNOT rely on 3rd party dependencies and doesn't have \`pip\` support nor networking support. It's LIMITED TO VANILLA Python. The assistant should keep that in mind.
6
+
7
+ WebContainer has the ability to run a web server but requires to use an npm package (e.g., Vite, servor, serve, http-server) or use the Node.js APIs to implement a web server.
8
+
9
+ IMPORTANT: Prefer using Vite instead of implementing a custom web server.
10
+
11
+ IMPORTANT: Git is NOT available.
12
+
13
+ Available shell commands: ['cat','chmod','cp','echo','hostname','kill','ln','ls','mkdir','mv','ps','pwd','rm','rmdir','xxd','alias','cd','clear','curl','env','false','getconf','head','sort','tail','touch','true','uptime','which','code','jq','loadenv','node','python3','wasm','xdg-open','command','exit','export','source']
14
+ </system_constraints>
15
+
16
+ <code_formatting_info>
17
+ Use 2 spaces for code indentation
18
+ </code_formatting_info>
19
+
20
+ <artifact_info>
21
+ Bolt creates a SINGLE, comprehensive artifact for each project. The artifact contains all necessary steps and components, including:
22
+
23
+ - Shell commands to run including dependencies to install using a package manager (NPM)
24
+ - Files to create and their contents
25
+
26
+ <artifact_instructions>
27
+ 1. Think BEFORE creating an artifact.
28
+
29
+ 2. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
30
+
31
+ 3. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
32
+
33
+ 3. Use \`<boltAction>\` tags to define specific actions to perform.
34
+
35
+ 4. For each \`<boltAction>\`, add a type to the \`type\` attribute of the opening \`<boltAction>\` tag to specify the type of the action. Assign one of the following values to the \`type\` attribute:
36
+
37
+ - shell: For running shell commands. When Using \`npx\`, ALWAYS provide the \`--yes\` flag!
38
+
39
+ - file: For writing new files or updating existing files. For each file add a \`path\` attribute to the opening \`<boltArtifact>\` tag to specify the file path. The content of the the file artifact is the file contents.
40
+
41
+ 4. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
42
+
43
+ 5. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
44
+
45
+ IMPORTANT: Add all required dependencies to the \`package.json\` already and try to avoid \`npm i <pkg>\` if possible!
46
+
47
+ 5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
48
+
49
+ 6. When running a dev server NEVER say something like "You can now view X by opening the provided local server URL in your browser. The preview will be opened automatically or by the user manually!
50
+ </artifact_instructions>
51
+ </artifact_info>
52
+
53
+ NEVER use the word "artifact". For example:
54
+ - DO NOT SAY: "This artifact sets up a simple Snake game using HTML, CSS, and JavaScript."
55
+ - INSTEAD SAY: "We set up a simple Snake game using HTML, CSS, and JavaScript."
56
+
57
+ IMPORTANT: Use valid markdown only for all your responses and DO NOT use HTML tags except for artifacts!
58
+
59
+ ULTRA IMPORTANT: Do NOT be verbose and DO NOT explain anything unless the user is asking for more information. That is VERY important.
60
+
61
+ ULTRA IMPORTANT: Think first and reply with the artifact that contains all necessary steps to set up the project, files, shell commands to run. It is SUPER IMPORTANT to respond with this first.
62
+
63
+ Here are some examples of correct usage of artifacts:
64
+
65
+ <examples>
66
+ <example>
67
+ <user_query>Can you help me create a JavaScript function to calculate the factorial of a number?</user_query>
68
+
69
+ <assistant_response>
70
+ Certainly, I can help you create a JavaScript function to calculate the factorial of a number.
71
+
72
+ <boltArtifact title="JavaScript Factorial Function">
73
+ <boltAction type="file" path="index.js">
74
+ function factorial(n) {
75
+ if (n === 0 || n === 1) {
76
+ return 1;
77
+ } else if (n < 0) {
78
+ return "Factorial is not defined for negative numbers";
79
+ } else {
80
+ return n * factorial(n - 1);
81
+ }
82
+ }
83
+
84
+ ...
85
+ </boltAction>
86
+
87
+ <boltAction type="shell">
88
+ node index.js
89
+ </boltAction>
90
+ </boltArtifact>
91
+ </assistant_response>
92
+ </example>
93
+
94
+ <example>
95
+ <user_query>Build a snake game</user_query>
96
+
97
+ <assistant_response>
98
+ Certainly! I'd be happy to help you build a snake game using JavaScript and HTML5 Canvas. This will be a basic implementation that you can later expand upon. Let's create the game step by step.
99
+
100
+ <boltArtifact title="Snake Game in HTML and JavaScript">
101
+ <boltAction type="file" path="package.json">
102
+ {
103
+ "name": "snake",
104
+ "scripts": {
105
+ "dev": "vite"
106
+ }
107
+ ...
108
+ }
109
+ </boltAction>
110
+
111
+ <boltAction type="shell">
112
+ npm install --save-dev vite
113
+ </boltAction>
114
+
115
+ <boltAction type="file" path="index.html">
116
+ ...
117
+ </boltAction>
118
+
119
+ <boltAction type="shell">
120
+ npm run dev
121
+ </boltAction>
122
+ </boltArtifact>
123
+
124
+ Now you can play the Snake game by opening the provided local server URL in your browser. Use the arrow keys to control the snake. Eat the red food to grow and increase your score. The game ends if you hit the wall or your own tail.
125
+ </assistant_response>
126
+ </example>
127
+
128
+ <example>
129
+ <user_query>Make a bouncing ball with real gravity using React</user_query>
130
+
131
+ <assistant_response>
132
+ Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.
133
+
134
+ <boltArtifact title="Bouncing Ball with Gravity in React">
135
+ <boltAction type="file" path="package.json">
136
+ {
137
+ "name": "bouncing-ball",
138
+ "private": true,
139
+ "version": "0.0.0",
140
+ "type": "module",
141
+ "scripts": {
142
+ "dev": "vite",
143
+ "build": "vite build",
144
+ "preview": "vite preview"
145
+ },
146
+ "dependencies": {
147
+ "react": "^18.2.0",
148
+ "react-dom": "^18.2.0",
149
+ "react-spring": "^9.7.1"
150
+ },
151
+ "devDependencies": {
152
+ "@types/react": "^18.0.28",
153
+ "@types/react-dom": "^18.0.11",
154
+ "@vitejs/plugin-react": "^3.1.0",
155
+ "vite": "^4.2.0"
156
+ }
157
+ }
158
+ </boltAction>
159
+
160
+ <boltAction type="file" path="index.html">
161
+ ...
162
+ </boltAction>
163
+
164
+ <boltAction type="file" path="src/main.jsx">
165
+ ...
166
+ </boltAction>
167
+
168
+ <boltAction type="file" path="src/index.css">
169
+ ...
170
+ </boltAction>
171
+
172
+ <boltAction type="file" path="src/App.jsx">
173
+ ...
174
+ </boltAction>
175
+
176
+ <boltAction type="shell">
177
+ npm run dev
178
+ </boltAction>
179
+ </boltArtifact>
180
+
181
+ You can now view the bouncing ball animation in the preview. The ball will start falling from the top of the screen and bounce realistically when it hits the bottom.
182
+ </assistant_response>
183
+ </example>
184
+ </examples>
185
+ `;
packages/bolt/app/lib/hooks/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export * from './useMessageParser';
2
+ export * from './usePromptEnhancer';
packages/bolt/app/lib/hooks/useMessageParser.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from 'ai';
2
+ import { useCallback, useState } from 'react';
3
+ import { StreamingMessageParser } from '~/lib/runtime/message-parser';
4
+ import { workspaceStore } from '~/lib/stores/workspace';
5
+ import { createScopedLogger } from '~/utils/logger';
6
+
7
+ const logger = createScopedLogger('useMessageParser');
8
+
9
+ const messageParser = new StreamingMessageParser({
10
+ callbacks: {
11
+ onArtifactOpen: (messageId, { title }) => {
12
+ logger.debug('onArtifactOpen', title);
13
+ workspaceStore.updateArtifact(messageId, { title, closed: false });
14
+ },
15
+ onArtifactClose: (messageId) => {
16
+ logger.debug('onArtifactClose');
17
+ workspaceStore.updateArtifact(messageId, { closed: true });
18
+ },
19
+ onAction: (messageId, { type, path, content }) => {
20
+ console.log('ACTION', messageId, { type, path, content });
21
+ },
22
+ },
23
+ });
24
+
25
+ export function useMessageParser() {
26
+ const [parsedMessages, setParsedMessages] = useState<{ [key: number]: string }>({});
27
+
28
+ const parseMessages = useCallback((messages: Message[], isLoading: boolean) => {
29
+ let reset = false;
30
+
31
+ if (import.meta.env.DEV && !isLoading) {
32
+ reset = true;
33
+ messageParser.reset();
34
+ }
35
+
36
+ for (const [index, message] of messages.entries()) {
37
+ if (message.role === 'assistant') {
38
+ /**
39
+ * In production, we only parse the last assistant message since previous messages can't change.
40
+ * During development they can change, e.g., if the parser gets modified.
41
+ */
42
+ if (import.meta.env.PROD && index < messages.length - 1) {
43
+ continue;
44
+ }
45
+
46
+ const newParsedContent = messageParser.parse(message.id, message.content);
47
+
48
+ setParsedMessages((prevParsed) => ({
49
+ ...prevParsed,
50
+ [index]: !reset ? (prevParsed[index] || '') + newParsedContent : newParsedContent,
51
+ }));
52
+ }
53
+ }
54
+ }, []);
55
+
56
+ return { parsedMessages, parseMessages };
57
+ }
packages/bolt/app/lib/hooks/usePromptEnhancer.ts ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { createScopedLogger } from '~/utils/logger';
3
+
4
+ const logger = createScopedLogger('usePromptEnhancement');
5
+
6
+ export function usePromptEnhancer() {
7
+ const [enhancingPrompt, setEnhancingPrompt] = useState(false);
8
+ const [promptEnhanced, setPromptEnhanced] = useState(false);
9
+
10
+ const resetEnhancer = () => {
11
+ setEnhancingPrompt(false);
12
+ setPromptEnhanced(false);
13
+ };
14
+
15
+ const enhancePrompt = async (input: string, setInput: (value: string) => void) => {
16
+ setEnhancingPrompt(true);
17
+ setPromptEnhanced(false);
18
+
19
+ const response = await fetch('/api/enhancer', {
20
+ method: 'POST',
21
+ body: JSON.stringify({
22
+ message: input,
23
+ }),
24
+ });
25
+
26
+ const reader = response.body?.getReader();
27
+
28
+ const originalInput = input;
29
+
30
+ if (reader) {
31
+ const decoder = new TextDecoder();
32
+
33
+ let _input = '';
34
+ let _error;
35
+
36
+ try {
37
+ setInput('');
38
+
39
+ while (true) {
40
+ const { value, done } = await reader.read();
41
+
42
+ if (done) {
43
+ break;
44
+ }
45
+
46
+ _input += decoder.decode(value);
47
+
48
+ logger.trace('Set input', _input);
49
+
50
+ setInput(_input);
51
+ }
52
+ } catch (error) {
53
+ _error = error;
54
+ setInput(originalInput);
55
+ } finally {
56
+ if (_error) {
57
+ logger.error(_error);
58
+ }
59
+
60
+ setEnhancingPrompt(false);
61
+ setPromptEnhanced(true);
62
+
63
+ setTimeout(() => {
64
+ setInput(_input);
65
+ });
66
+ }
67
+ }
68
+ };
69
+
70
+ return { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer };
71
+ }
packages/bolt/app/lib/runtime/message-parser.spec.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import { StreamingMessageParser } from './message-parser';
3
+
4
+ describe('StreamingMessageParser', () => {
5
+ it('should pass through normal text', () => {
6
+ const parser = new StreamingMessageParser();
7
+ expect(parser.parse('test_id', 'Hello, world!')).toBe('Hello, world!');
8
+ });
9
+
10
+ it('should allow normal HTML tags', () => {
11
+ const parser = new StreamingMessageParser();
12
+ expect(parser.parse('test_id', 'Hello <strong>world</strong>!')).toBe('Hello <strong>world</strong>!');
13
+ });
14
+
15
+ it.each([
16
+ ['Foo bar', 'Foo bar'],
17
+ ['Foo bar <', 'Foo bar '],
18
+ ['Foo bar <p', 'Foo bar <p'],
19
+ ['Foo bar <b', 'Foo bar '],
20
+ ['Foo bar <ba', 'Foo bar <ba'],
21
+ ['Foo bar <bol', 'Foo bar '],
22
+ ['Foo bar <bolt', 'Foo bar '],
23
+ ['Foo bar <bolta', 'Foo bar <bolta'],
24
+ ['Foo bar <boltA', 'Foo bar '],
25
+ ['Some text before <boltArtifact>foo</boltArtifact> Some more text', 'Some text before Some more text'],
26
+ [['Some text before <boltArti', 'fact>foo</boltArtifact> Some more text'], 'Some text before Some more text'],
27
+ [['Some text before <boltArti', 'fac', 't>foo</boltArtifact> Some more text'], 'Some text before Some more text'],
28
+ [['Some text before <boltArti', 'fact>fo', 'o</boltArtifact> Some more text'], 'Some text before Some more text'],
29
+ [
30
+ ['Some text before <boltArti', 'fact>fo', 'o', '<', '/boltArtifact> Some more text'],
31
+ 'Some text before Some more text',
32
+ ],
33
+ [
34
+ ['Some text before <boltArti', 'fact>fo', 'o<', '/boltArtifact> Some more text'],
35
+ 'Some text before Some more text',
36
+ ],
37
+ ['Before <oltArtfiact>foo</boltArtifact> After', 'Before <oltArtfiact>foo</boltArtifact> After'],
38
+ ['Before <boltArtifactt>foo</boltArtifact> After', 'Before <boltArtifactt>foo</boltArtifact> After'],
39
+ ['Before <boltArtifact title="Some title">foo</boltArtifact> After', 'Before After'],
40
+ [
41
+ 'Before <boltArtifact title="Some title"><boltAction type="shell">npm install</boltAction></boltArtifact> After',
42
+ 'Before After',
43
+ [{ type: 'shell', content: 'npm install' }],
44
+ ],
45
+ [
46
+ 'Before <boltArtifact title="Some title"><boltAction type="shell">npm install</boltAction><boltAction type="file" path="index.js">some content</boltAction></boltArtifact> After',
47
+ 'Before After',
48
+ [
49
+ { type: 'shell', content: 'npm install' },
50
+ { type: 'file', path: 'index.js', content: 'some content\n' },
51
+ ],
52
+ ],
53
+ ])('should correctly parse chunks and strip out bolt artifacts', (input, expected, expectedActions = []) => {
54
+ let actionCounter = 0;
55
+
56
+ const testId = 'test_id';
57
+
58
+ const parser = new StreamingMessageParser({
59
+ artifactElement: '',
60
+ callbacks: {
61
+ onAction: (id, action) => {
62
+ expect(testId).toBe(id);
63
+ expect(action).toEqual(expectedActions[actionCounter]);
64
+ actionCounter++;
65
+ },
66
+ },
67
+ });
68
+
69
+ let message = '';
70
+
71
+ let result = '';
72
+
73
+ const chunks = Array.isArray(input) ? input : input.split('');
74
+
75
+ for (const chunk of chunks) {
76
+ message += chunk;
77
+
78
+ result += parser.parse(testId, message);
79
+ }
80
+
81
+ expect(actionCounter).toBe(expectedActions.length);
82
+ expect(result).toEqual(expected);
83
+ });
84
+ });
packages/bolt/app/lib/runtime/message-parser.ts ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const ARTIFACT_TAG_OPEN = '<boltArtifact';
2
+ const ARTIFACT_TAG_CLOSE = '</boltArtifact>';
3
+ const ARTIFACT_ACTION_TAG_OPEN = '<boltAction';
4
+ const ARTIFACT_ACTION_TAG_CLOSE = '</boltAction>';
5
+
6
+ interface BoltArtifact {
7
+ title: string;
8
+ }
9
+
10
+ type ArtifactOpenCallback = (messageId: string, artifact: BoltArtifact) => void;
11
+ type ArtifactCloseCallback = (messageId: string) => void;
12
+ type ActionCallback = (messageId: string, action: BoltActionData) => void;
13
+
14
+ type ActionType = 'file' | 'shell';
15
+
16
+ export interface BoltActionData {
17
+ type?: ActionType;
18
+ path?: string;
19
+ content: string;
20
+ }
21
+
22
+ interface Callbacks {
23
+ onArtifactOpen?: ArtifactOpenCallback;
24
+ onArtifactClose?: ArtifactCloseCallback;
25
+ onAction?: ActionCallback;
26
+ }
27
+
28
+ type ElementFactory = () => string;
29
+
30
+ interface StreamingMessageParserOptions {
31
+ callbacks?: Callbacks;
32
+ artifactElement?: string | ElementFactory;
33
+ }
34
+
35
+ export class StreamingMessageParser {
36
+ #lastPositions = new Map<string, number>();
37
+ #insideArtifact = false;
38
+ #insideAction = false;
39
+ #currentAction: BoltActionData = { content: '' };
40
+
41
+ constructor(private _options: StreamingMessageParserOptions = {}) {}
42
+
43
+ parse(id: string, input: string) {
44
+ let output = '';
45
+ let i = this.#lastPositions.get(id) ?? 0;
46
+ let earlyBreak = false;
47
+
48
+ while (i < input.length) {
49
+ if (this.#insideArtifact) {
50
+ if (this.#insideAction) {
51
+ const closeIndex = input.indexOf(ARTIFACT_ACTION_TAG_CLOSE, i);
52
+
53
+ if (closeIndex !== -1) {
54
+ this.#currentAction.content += input.slice(i, closeIndex);
55
+
56
+ let content = this.#currentAction.content.trim();
57
+
58
+ if (this.#currentAction.type === 'file') {
59
+ content += '\n';
60
+ }
61
+
62
+ this.#currentAction.content = content;
63
+
64
+ this._options.callbacks?.onAction?.(id, this.#currentAction);
65
+
66
+ this.#insideAction = false;
67
+ this.#currentAction = { content: '' };
68
+
69
+ i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
70
+ } else {
71
+ break;
72
+ }
73
+ } else {
74
+ const actionOpenIndex = input.indexOf(ARTIFACT_ACTION_TAG_OPEN, i);
75
+ const artifactCloseIndex = input.indexOf(ARTIFACT_TAG_CLOSE, i);
76
+
77
+ if (actionOpenIndex !== -1 && (artifactCloseIndex === -1 || actionOpenIndex < artifactCloseIndex)) {
78
+ const actionEndIndex = input.indexOf('>', actionOpenIndex);
79
+
80
+ if (actionEndIndex !== -1) {
81
+ const actionTag = input.slice(actionOpenIndex, actionEndIndex + 1);
82
+ this.#currentAction.type = this.#extractAttribute(actionTag, 'type') as ActionType;
83
+ this.#currentAction.path = this.#extractAttribute(actionTag, 'path');
84
+ this.#insideAction = true;
85
+ i = actionEndIndex + 1;
86
+ } else {
87
+ break;
88
+ }
89
+ } else if (artifactCloseIndex !== -1) {
90
+ this.#insideArtifact = false;
91
+
92
+ this._options.callbacks?.onArtifactClose?.(id);
93
+
94
+ i = artifactCloseIndex + ARTIFACT_TAG_CLOSE.length;
95
+ } else {
96
+ break;
97
+ }
98
+ }
99
+ } else if (input[i] === '<' && input[i + 1] !== '/') {
100
+ let j = i;
101
+ let potentialTag = '';
102
+
103
+ while (j < input.length && potentialTag.length < ARTIFACT_TAG_OPEN.length) {
104
+ potentialTag += input[j];
105
+
106
+ if (potentialTag === ARTIFACT_TAG_OPEN) {
107
+ const nextChar = input[j + 1];
108
+
109
+ if (nextChar && nextChar !== '>' && nextChar !== ' ') {
110
+ output += input.slice(i, j + 1);
111
+ i = j + 1;
112
+ break;
113
+ }
114
+
115
+ const openTagEnd = input.indexOf('>', j);
116
+
117
+ if (openTagEnd !== -1) {
118
+ const artifactTag = input.slice(i, openTagEnd + 1);
119
+
120
+ const artifactTitle = this.#extractAttribute(artifactTag, 'title') as string;
121
+
122
+ this.#insideArtifact = true;
123
+
124
+ this._options.callbacks?.onArtifactOpen?.(id, { title: artifactTitle });
125
+
126
+ output += this._options.artifactElement ?? `<div class="__boltArtifact__" data-message-id="${id}"></div>`;
127
+
128
+ i = openTagEnd + 1;
129
+ } else {
130
+ earlyBreak = true;
131
+ }
132
+
133
+ break;
134
+ } else if (!ARTIFACT_TAG_OPEN.startsWith(potentialTag)) {
135
+ output += input.slice(i, j + 1);
136
+ i = j + 1;
137
+ break;
138
+ }
139
+
140
+ j++;
141
+ }
142
+
143
+ if (j === input.length && ARTIFACT_TAG_OPEN.startsWith(potentialTag)) {
144
+ break;
145
+ }
146
+ } else {
147
+ output += input[i];
148
+ i++;
149
+ }
150
+
151
+ if (earlyBreak) {
152
+ break;
153
+ }
154
+ }
155
+
156
+ this.#lastPositions.set(id, i);
157
+
158
+ return output;
159
+ }
160
+
161
+ reset() {
162
+ this.#lastPositions.clear();
163
+ this.#insideArtifact = false;
164
+ this.#insideAction = false;
165
+ this.#currentAction = { content: '' };
166
+ }
167
+
168
+ #extractAttribute(tag: string, attributeName: string): string | undefined {
169
+ const match = tag.match(new RegExp(`${attributeName}="([^"]*)"`, 'i'));
170
+ return match ? match[1] : undefined;
171
+ }
172
+ }
packages/bolt/app/lib/stores/workspace.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { WebContainer } from '@webcontainer/api';
2
+ import { atom, map, type MapStore, type WritableAtom } from 'nanostores';
3
+ import { webcontainer } from '~/lib/webcontainer';
4
+
5
+ interface WorkspaceStoreOptions {
6
+ webcontainer: Promise<WebContainer>;
7
+ }
8
+
9
+ interface ArtifactState {
10
+ title: string;
11
+ closed: boolean;
12
+ actions: any /* TODO */;
13
+ }
14
+
15
+ export class WorkspaceStore {
16
+ #webcontainer: Promise<WebContainer>;
17
+
18
+ artifacts: MapStore<Record<string, ArtifactState>> = import.meta.hot?.data.artifacts ?? map({});
19
+ showWorkspace: WritableAtom<boolean> = import.meta.hot?.data.showWorkspace ?? atom(false);
20
+
21
+ constructor({ webcontainer }: WorkspaceStoreOptions) {
22
+ this.#webcontainer = webcontainer;
23
+ }
24
+
25
+ updateArtifact(id: string, state: Partial<ArtifactState>) {
26
+ const artifacts = this.artifacts.get();
27
+ const artifact = artifacts[id];
28
+
29
+ this.artifacts.setKey(id, { ...artifact, ...state });
30
+ }
31
+
32
+ runAction() {
33
+ // TODO
34
+ }
35
+ }
36
+
37
+ export const workspaceStore = new WorkspaceStore({ webcontainer });
38
+
39
+ if (import.meta.hot) {
40
+ import.meta.hot.data.artifacts = workspaceStore.artifacts;
41
+ import.meta.hot.data.showWorkspace = workspaceStore.showWorkspace;
42
+ }
packages/bolt/app/lib/webcontainer/index.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { WebContainer } from '@webcontainer/api';
2
+
3
+ interface WebContainerContext {
4
+ loaded: boolean;
5
+ }
6
+
7
+ export const webcontainerContext: WebContainerContext = import.meta.hot?.data.webcontainerContext ?? {
8
+ loaded: false,
9
+ };
10
+
11
+ if (import.meta.hot) {
12
+ import.meta.hot.data.webcontainerContext = webcontainerContext;
13
+ }
14
+
15
+ export let webcontainer: Promise<WebContainer> = new Promise(() => {
16
+ // noop for ssr
17
+ });
18
+
19
+ if (!import.meta.env.SSR) {
20
+ webcontainer =
21
+ import.meta.hot?.data.webcontainer ??
22
+ Promise.resolve()
23
+ .then(() => WebContainer.boot({ workdirName: 'project' }))
24
+ .then(() => {
25
+ webcontainerContext.loaded = true;
26
+ });
27
+
28
+ if (import.meta.hot) {
29
+ import.meta.hot.data.webcontainer = webcontainer;
30
+ }
31
+ }
packages/bolt/app/root.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { LinksFunction } from '@remix-run/cloudflare';
2
+ import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
3
+ import reset from '@unocss/reset/tailwind.css?url';
4
+ import globalStyles from '~/styles/index.scss?url';
5
+
6
+ import 'virtual:uno.css';
7
+
8
+ export const links: LinksFunction = () => [
9
+ {
10
+ rel: 'icon',
11
+ href: '/favicon.svg',
12
+ type: 'image/svg+xml',
13
+ },
14
+ { rel: 'stylesheet', href: reset },
15
+ { rel: 'stylesheet', href: globalStyles },
16
+ {
17
+ rel: 'preconnect',
18
+ href: 'https://fonts.googleapis.com',
19
+ },
20
+ {
21
+ rel: 'preconnect',
22
+ href: 'https://fonts.gstatic.com',
23
+ crossOrigin: 'anonymous',
24
+ },
25
+ {
26
+ rel: 'stylesheet',
27
+ href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
28
+ },
29
+ ];
30
+
31
+ export function Layout({ children }: { children: React.ReactNode }) {
32
+ return (
33
+ <html lang="en">
34
+ <head>
35
+ <meta charSet="utf-8" />
36
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
37
+ <Meta />
38
+ <Links />
39
+ </head>
40
+ <body>
41
+ {children}
42
+ <ScrollRestoration />
43
+ <Scripts />
44
+ </body>
45
+ </html>
46
+ );
47
+ }
48
+
49
+ export default function App() {
50
+ return <Outlet />;
51
+ }
packages/bolt/app/routes/_index.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 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';
6
+
7
+ export const meta: MetaFunction = () => {
8
+ return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
9
+ };
10
+
11
+ export default function Index() {
12
+ return (
13
+ <div className="flex flex-col h-full w-full">
14
+ <Header />
15
+ <ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
16
+ </div>
17
+ );
18
+ }
packages/bolt/app/routes/api.chat.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2
+ import { convertToCoreMessages, streamText } from 'ai';
3
+ import { getAPIKey } from '~/lib/.server/llm/api-key';
4
+ import { getAnthropicModel } from '~/lib/.server/llm/model';
5
+ import { systemPrompt } from '~/lib/.server/llm/prompts';
6
+
7
+ interface ToolResult<Name extends string, Args, Result> {
8
+ toolCallId: string;
9
+ toolName: Name;
10
+ args: Args;
11
+ result: Result;
12
+ }
13
+
14
+ interface Message {
15
+ role: 'user' | 'assistant';
16
+ content: string;
17
+ toolInvocations?: ToolResult<string, unknown, unknown>[];
18
+ }
19
+
20
+ export async function action({ context, request }: ActionFunctionArgs) {
21
+ const { messages } = await request.json<{ messages: Message[] }>();
22
+
23
+ try {
24
+ const result = await streamText({
25
+ model: getAnthropicModel(getAPIKey(context.cloudflare.env)),
26
+ messages: convertToCoreMessages(messages),
27
+ toolChoice: 'none',
28
+ onFinish: ({ finishReason, usage, warnings }) => {
29
+ console.log({ finishReason, usage, warnings });
30
+ },
31
+ system: systemPrompt,
32
+ });
33
+
34
+ return result.toAIStreamResponse();
35
+ } catch (error) {
36
+ console.log(error);
37
+
38
+ throw new Response(null, {
39
+ status: 500,
40
+ statusText: 'Internal Server Error',
41
+ });
42
+ }
43
+ }
packages/bolt/app/routes/api.enhancer.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2
+ import { StreamingTextResponse, convertToCoreMessages, parseStreamPart, streamText } from 'ai';
3
+ import { getAPIKey } from '~/lib/.server/llm/api-key';
4
+ import { getAnthropicModel } from '~/lib/.server/llm/model';
5
+ import { systemPrompt } from '~/lib/.server/llm/prompts';
6
+ import { stripIndents } from '~/utils/stripIndent';
7
+
8
+ const encoder = new TextEncoder();
9
+ const decoder = new TextDecoder();
10
+
11
+ export async function action({ context, request }: ActionFunctionArgs) {
12
+ const { message } = await request.json<{ message: string }>();
13
+
14
+ try {
15
+ const result = await streamText({
16
+ model: getAnthropicModel(getAPIKey(context.cloudflare.env)),
17
+ system: systemPrompt,
18
+ messages: convertToCoreMessages([
19
+ {
20
+ role: 'user',
21
+ content: stripIndents`
22
+ I want you to improve the following prompt.
23
+
24
+ IMPORTANT: Only respond with the improved prompt and nothing else!
25
+
26
+ <original_prompt>
27
+ ${message}
28
+ </original_prompt>
29
+ `,
30
+ },
31
+ ]),
32
+ });
33
+
34
+ if (import.meta.env.DEV) {
35
+ result.usage.then((usage) => {
36
+ console.log('Usage', usage);
37
+ });
38
+ }
39
+
40
+ const transformStream = new TransformStream({
41
+ transform(chunk, controller) {
42
+ const processedChunk = decoder
43
+ .decode(chunk)
44
+ .split('\n')
45
+ .filter((line) => line !== '')
46
+ .map(parseStreamPart)
47
+ .map((part) => part.value)
48
+ .join('');
49
+
50
+ controller.enqueue(encoder.encode(processedChunk));
51
+ },
52
+ });
53
+
54
+ const transformedStream = result.toAIStream().pipeThrough(transformStream);
55
+
56
+ return new StreamingTextResponse(transformedStream);
57
+ } catch (error) {
58
+ console.log(error);
59
+
60
+ throw new Response(null, {
61
+ status: 500,
62
+ statusText: 'Internal Server Error',
63
+ });
64
+ }
65
+ }
packages/bolt/app/styles/index.scss ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import './variables.scss';
2
+
3
+ body {
4
+ --at-apply: bg-bolt-elements-app-backgroundColor;
5
+
6
+ font-family: 'Inter', sans-serif;
7
+
8
+ &:before {
9
+ --line: color-mix(in lch, canvasText, transparent 93%);
10
+ --size: 50px;
11
+
12
+ content: '';
13
+ height: 100vh;
14
+ mask: linear-gradient(-25deg, transparent 60%, white);
15
+ pointer-events: none;
16
+ position: fixed;
17
+ top: 0;
18
+ transform-style: flat;
19
+ width: 100vw;
20
+ z-index: -1;
21
+ background:
22
+ linear-gradient(90deg, var(--line) 1px, transparent 1px var(--size)) 50% 50% / var(--size) var(--size),
23
+ linear-gradient(var(--line) 1px, transparent 1px var(--size)) 50% 50% / var(--size) var(--size);
24
+ }
25
+ }
26
+
27
+ html,
28
+ body {
29
+ height: 100%;
30
+ width: 100%;
31
+ }
packages/bolt/app/styles/variables.scss ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root,
2
+ :root[data-theme='light'] {
3
+ /* Color Tokens */
4
+ --bolt-background-primary: theme('colors.gray.50');
5
+ }
6
+
7
+ :root,
8
+ :root[data-theme='dark'] {
9
+ /* Color Tokens */
10
+ --bolt-background-primary: theme('colors.gray.50');
11
+ }
12
+
13
+ /*
14
+ * Element Tokens
15
+ *
16
+ * Hierarchy: Element Token -> (Element Token | Color Tokens) -> Primitives
17
+ */
18
+ :root {
19
+ /* App */
20
+ --bolt-elements-app-backgroundColor: var(--bolt-background-primary);
21
+ }
packages/bolt/app/utils/classNames.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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<string, boolean> | 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
+ }
packages/bolt/app/utils/logger.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
2
+
3
+ type LoggerFunction = (...messages: any[]) => void;
4
+
5
+ interface Logger {
6
+ trace: LoggerFunction;
7
+ debug: LoggerFunction;
8
+ info: LoggerFunction;
9
+ warn: LoggerFunction;
10
+ error: LoggerFunction;
11
+ setLevel: (level: DebugLevel) => void;
12
+ }
13
+
14
+ let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? 'warn';
15
+
16
+ export const logger: Logger = {
17
+ trace: (...messages: any[]) => log('trace', undefined, messages),
18
+ debug: (...messages: any[]) => log('debug', undefined, messages),
19
+ info: (...messages: any[]) => log('info', undefined, messages),
20
+ warn: (...messages: any[]) => log('warn', undefined, messages),
21
+ error: (...messages: any[]) => log('error', undefined, messages),
22
+ setLevel,
23
+ };
24
+
25
+ export function createScopedLogger(scope: string): Logger {
26
+ return {
27
+ trace: (...messages: any[]) => log('trace', scope, messages),
28
+ debug: (...messages: any[]) => log('debug', scope, messages),
29
+ info: (...messages: any[]) => log('info', scope, messages),
30
+ warn: (...messages: any[]) => log('warn', scope, messages),
31
+ error: (...messages: any[]) => log('error', scope, messages),
32
+ setLevel,
33
+ };
34
+ }
35
+
36
+ function setLevel(level: DebugLevel) {
37
+ if ((level === 'trace' || level === 'debug') && import.meta.env.PROD) {
38
+ return;
39
+ }
40
+
41
+ currentLevel = level;
42
+ }
43
+
44
+ function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
45
+ const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error'];
46
+
47
+ if (levelOrder.indexOf(level) >= levelOrder.indexOf(currentLevel)) {
48
+ const labelBackgroundColor = getColorForLevel(level);
49
+ const labelTextColor = level === 'warn' ? 'black' : 'white';
50
+
51
+ const labelStyles = getLabelStyles(labelBackgroundColor, labelTextColor);
52
+ const scopeStyles = getLabelStyles('#77828D', 'white');
53
+
54
+ const styles = [labelStyles];
55
+
56
+ if (typeof scope === 'string') {
57
+ styles.push('', scopeStyles);
58
+ }
59
+
60
+ console.log(`%c${level.toUpperCase()}${scope ? `%c %c${scope}` : ''}`, ...styles, ...messages);
61
+ }
62
+ }
63
+
64
+ function getLabelStyles(color: string, textColor: string) {
65
+ return `background-color: ${color}; color: white; border: 4px solid ${color}; color: ${textColor};`;
66
+ }
67
+
68
+ function getColorForLevel(level: DebugLevel): string {
69
+ switch (level) {
70
+ case 'trace':
71
+ case 'debug': {
72
+ return '#77828D';
73
+ }
74
+ case 'info': {
75
+ return '#1389FD';
76
+ }
77
+ case 'warn': {
78
+ return '#FFDB6C';
79
+ }
80
+ case 'error': {
81
+ return '#EE4744';
82
+ }
83
+ default: {
84
+ return 'black';
85
+ }
86
+ }
87
+ }
packages/bolt/app/utils/markdown.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import rehypeRaw from 'rehype-raw';
2
+ import remarkGfm from 'remark-gfm';
3
+ import type { PluggableList } from 'unified';
4
+
5
+ export const remarkPlugins = [remarkGfm] satisfies PluggableList;
6
+ export const rehypePlugins = [rehypeRaw] satisfies PluggableList;
packages/bolt/app/utils/promises.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function withResolvers<T>(): PromiseWithResolvers<T> {
2
+ if (typeof Promise.withResolvers === 'function') {
3
+ return Promise.withResolvers();
4
+ }
5
+
6
+ let resolve!: (value: T | PromiseLike<T>) => void;
7
+ let reject!: (reason?: any) => void;
8
+
9
+ const promise = new Promise<T>((_resolve, _reject) => {
10
+ resolve = _resolve;
11
+ reject = _reject;
12
+ });
13
+
14
+ return {
15
+ resolve,
16
+ reject,
17
+ promise,
18
+ };
19
+ }
packages/bolt/app/utils/stripIndent.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }