mirror of
https://github.com/coleam00/bolt.new-any-llm
synced 2024-12-27 14:22:32 +00:00
feat: initial commit
This commit is contained in:
commit
6927c07451
35
.github/workflows/ci.yaml
vendored
Normal file
35
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.20.3]
|
||||
steps:
|
||||
- name: Setup
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.4.0
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run type check
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Run ESLint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm run test
|
32
.github/workflows/semantic-pr.yaml
vendored
Normal file
32
.github/workflows/semantic-pr.yaml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: Semantic Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
permissions:
|
||||
pull-requests: read
|
||||
jobs:
|
||||
main:
|
||||
name: Validate PR Title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.3
|
||||
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
subjectPattern: ^(?![A-Z]).+$
|
||||
subjectPatternError: |
|
||||
The subject "{subject}" found in the pull request title "{title}"
|
||||
didn't match the configured pattern. Please ensure that the subject
|
||||
doesn't start with an uppercase character.
|
||||
types: |
|
||||
fix
|
||||
feat
|
||||
chore
|
||||
build
|
||||
ci
|
||||
perf
|
||||
docs
|
||||
refactor
|
||||
revert
|
||||
test
|
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
||||
npm test
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
||||
pnpm-lock.yaml
|
||||
.astro
|
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"bracketSpacing": true
|
||||
}
|
2
.tool-versions
Normal file
2
.tool-versions
Normal file
@ -0,0 +1,2 @@
|
||||
nodejs 18.20.3
|
||||
pnpm 9.4.0
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Current Test File",
|
||||
"autoAttachChildProcesses": true,
|
||||
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
|
||||
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
|
||||
"args": ["run", "${relativeFile}"],
|
||||
"smartStep": true,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
49
README.md
Normal file
49
README.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Bolt Monorepo
|
||||
|
||||
Welcome to the Bolt monorepo! This repository contains the codebase for Bolt, an AI assistant developed by StackBlitz.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
Currently, this monorepo contains a single package:
|
||||
|
||||
- [`bolt`](packages/bolt): The main package containing the UI interface for Bolt as well as the server components.
|
||||
|
||||
As the project grows, additional packages may be added to this workspace.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (v18.20.3)
|
||||
- pnpm (v9.4.0)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stackblitz/bolt.git
|
||||
cd bolt
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
To start developing the Bolt UI:
|
||||
|
||||
1. Navigate to the bolt package:
|
||||
|
||||
```bash
|
||||
cd packages/bolt
|
||||
```
|
||||
|
||||
2. Start the development server:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
26
eslint.config.mjs
Normal file
26
eslint.config.mjs
Normal file
@ -0,0 +1,26 @@
|
||||
import blitzPlugin from '@blitz/eslint-plugin';
|
||||
import { getNamingConventionRule } from '@blitz/eslint-plugin/dist/configs/typescript.js';
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/dist', '**/node_modules'],
|
||||
},
|
||||
...blitzPlugin.configs.recommended(),
|
||||
{
|
||||
rules: {
|
||||
'@blitz/catch-error-name': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.tsx'],
|
||||
rules: {
|
||||
...getNamingConventionRule({}, true),
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
},
|
||||
},
|
||||
];
|
40
package.json
Normal file
40
package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@9.4.0",
|
||||
"scripts": {
|
||||
"playground:dev": "pnpm run --filter=playground dev",
|
||||
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
||||
"test": "pnpm run -r test",
|
||||
"typecheck": "pnpm run -r typecheck",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"lint-staged": {},
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
"commit-msg": "commitlint --edit $1"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0",
|
||||
"pnpm": "9.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blitz/eslint-plugin": "0.1.0",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
"commitlint": "^19.3.0",
|
||||
"husky": "^9.0.11",
|
||||
"is-ci": "^3.0.1",
|
||||
"prettier": "^3.3.2",
|
||||
"vitest": "^2.0.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/utils": "^8.0.0-alpha.30"
|
||||
}
|
||||
}
|
8
packages/bolt/.gitignore
vendored
Normal file
8
packages/bolt/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
|
||||
/.cache
|
||||
/build
|
||||
.env
|
||||
*.vars
|
||||
.wrangler
|
||||
_worker.bundle
|
78
packages/bolt/README.md
Normal file
78
packages/bolt/README.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Bolt
|
||||
|
||||
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/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following installed:
|
||||
|
||||
- Node.js (v18.20.3)
|
||||
- pnpm (v9.4.0)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone the repository (if you haven't already):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stackblitz/bolt.git
|
||||
cd bolt
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. Create a `.env.local` file in the root of the bolt package directory and add your Anthropic API key:
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY=XXX
|
||||
```
|
||||
|
||||
Optionally, you an set the debug level:
|
||||
|
||||
```
|
||||
VITE_LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
- `pnpm run dev`: Starts the development server.
|
||||
- `pnpm run build`: Builds the project.
|
||||
- `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.
|
||||
- `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`.
|
||||
- `pnpm test:` Runs the test suite using Vitest.
|
||||
- `pnpm run typecheck`: Runs TypeScript type checking.
|
||||
- `pnpm run typegen`: Generates TypeScript types using Wrangler.
|
||||
- `pnpm run deploy`: Builds the project and deploys it to Cloudflare Pages.
|
||||
|
||||
## Development
|
||||
|
||||
To start the development server:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
This will start the Remix Vite development server.
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite with:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
To deploy the application to Cloudflare Pages:
|
||||
|
||||
```bash
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
Make sure you have the necessary permissions and Wrangler is correctly configured for your Cloudflare account.
|
12
packages/bolt/app/components/Header.tsx
Normal file
12
packages/bolt/app/components/Header.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { IconButton } from './ui/IconButton';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="flex items-center bg-white p-4 border-b border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-2xl font-semibold text-accent">Bolt</div>
|
||||
</div>
|
||||
<IconButton icon="i-ph:gear-duotone" className="ml-auto" />
|
||||
</header>
|
||||
);
|
||||
}
|
29
packages/bolt/app/components/chat/Artifact.tsx
Normal file
29
packages/bolt/app/components/chat/Artifact.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { workspaceStore } from '~/lib/stores/workspace';
|
||||
|
||||
interface ArtifactProps {
|
||||
messageId: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function Artifact({ messageId, onClick }: ArtifactProps) {
|
||||
const artifacts = useStore(workspaceStore.artifacts);
|
||||
|
||||
const artifact = artifacts[messageId];
|
||||
|
||||
return (
|
||||
<button className="flex border rounded-lg overflow-hidden items-stretch bg-gray-50/25 w-full" onClick={onClick}>
|
||||
<div className="border-r flex items-center px-6 bg-gray-50">
|
||||
{!artifact?.closed ? (
|
||||
<div className="i-svg-spinners:90-ring-with-bg scale-130"></div>
|
||||
) : (
|
||||
<div className="i-ph:code-bold scale-130 text-gray-600"></div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-center px-4 p-2.5">
|
||||
<div className="text-left w-full">{artifact?.title}</div>
|
||||
<small className="w-full text-left">Click to open code</small>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
14
packages/bolt/app/components/chat/AssistantMessage.tsx
Normal file
14
packages/bolt/app/components/chat/AssistantMessage.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { memo } from 'react';
|
||||
import { Markdown } from './Markdown';
|
||||
|
||||
interface AssistantMessageProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const AssistantMessage = memo(({ content }: AssistantMessageProps) => {
|
||||
return (
|
||||
<div className="overflow-hidden w-full">
|
||||
<Markdown>{content}</Markdown>
|
||||
</div>
|
||||
);
|
||||
});
|
140
packages/bolt/app/components/chat/BaseChat.tsx
Normal file
140
packages/bolt/app/components/chat/BaseChat.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import type { LegacyRef } from 'react';
|
||||
import React from 'react';
|
||||
import { ClientOnly } from 'remix-utils/client-only';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { SendButton } from './SendButton.client';
|
||||
|
||||
interface BaseChatProps {
|
||||
textareaRef?: LegacyRef<HTMLTextAreaElement> | undefined;
|
||||
messagesSlot?: React.ReactNode;
|
||||
workspaceSlot?: React.ReactNode;
|
||||
chatStarted?: boolean;
|
||||
enhancingPrompt?: boolean;
|
||||
promptEnhanced?: boolean;
|
||||
input?: string;
|
||||
sendMessage?: () => void;
|
||||
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
enhancePrompt?: () => void;
|
||||
}
|
||||
|
||||
const EXAMPLES = [{ text: 'Example' }, { text: 'Example' }, { text: 'Example' }, { text: 'Example' }];
|
||||
|
||||
const TEXTAREA_MIN_HEIGHT = 72;
|
||||
|
||||
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
(
|
||||
{
|
||||
textareaRef,
|
||||
chatStarted = false,
|
||||
enhancingPrompt = false,
|
||||
promptEnhanced = false,
|
||||
messagesSlot,
|
||||
workspaceSlot,
|
||||
input = '',
|
||||
sendMessage,
|
||||
handleInputChange,
|
||||
enhancePrompt,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
||||
|
||||
return (
|
||||
<div ref={ref} className="h-full flex w-full overflow-scroll px-6">
|
||||
<div className="flex flex-col items-center w-full h-full">
|
||||
<div id="chat" className="w-full">
|
||||
{!chatStarted && (
|
||||
<div id="intro" className="mt-[20vh] mb-14 max-w-2xl mx-auto">
|
||||
<h2 className="text-4xl text-center font-bold text-slate-800 mb-2">Where ideas begin.</h2>
|
||||
<p className="mb-14 text-center">Bring ideas to life in seconds or get help on existing projects.</p>
|
||||
<div className="grid max-md:grid-cols-[repeat(2,1fr)] md:grid-cols-[repeat(2,minmax(200px,1fr))] gap-4">
|
||||
{EXAMPLES.map((suggestion, index) => (
|
||||
<button key={index} className="p-4 rounded-lg shadow-xs bg-white border border-gray-200 text-left">
|
||||
{suggestion.text}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{messagesSlot}
|
||||
</div>
|
||||
<div
|
||||
className={classNames('w-full md:max-w-[720px] mx-auto', {
|
||||
'fixed bg-bolt-elements-app-backgroundColor bottom-0': chatStarted,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative shadow-sm border border-gray-200 md:mb-6 bg-white rounded-lg overflow-hidden',
|
||||
{
|
||||
'max-md:rounded-none max-md:border-x-none': chatStarted,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
sendMessage?.();
|
||||
}
|
||||
}}
|
||||
value={input}
|
||||
onChange={(event) => {
|
||||
handleInputChange?.(event);
|
||||
}}
|
||||
className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none`}
|
||||
style={{
|
||||
minHeight: TEXTAREA_MIN_HEIGHT,
|
||||
maxHeight: TEXTAREA_MAX_HEIGHT,
|
||||
}}
|
||||
placeholder="How can Bolt help you today?"
|
||||
translate="no"
|
||||
/>
|
||||
<ClientOnly>{() => <SendButton show={input.length > 0} onClick={sendMessage} />}</ClientOnly>
|
||||
<div className="flex justify-between text-sm p-4 pt-2">
|
||||
<div className="flex gap-1 items-center">
|
||||
<IconButton icon="i-ph:microphone-duotone" className="-ml-1" />
|
||||
<IconButton icon="i-ph:plus-circle-duotone" />
|
||||
<IconButton
|
||||
disabled={input.length === 0 || enhancingPrompt}
|
||||
className={classNames({
|
||||
'opacity-100!': enhancingPrompt,
|
||||
'text-accent! pr-1.5': promptEnhanced,
|
||||
})}
|
||||
onClick={() => enhancePrompt?.()}
|
||||
>
|
||||
{enhancingPrompt ? (
|
||||
<>
|
||||
<div className="i-svg-spinners:90-ring-with-bg text-black text-xl"></div>
|
||||
<div className="ml-1.5">Enhancing prompt...</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="i-blitz:stars text-xl"></div>
|
||||
{promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
|
||||
</>
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
{input.length > 3 ? (
|
||||
<div className="text-xs">
|
||||
Use <kbd className="bg-gray-100 p-1 rounded-md">Shift</kbd> +{' '}
|
||||
<kbd className="bg-gray-100 p-1 rounded-md">Return</kbd> for a new line
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{workspaceSlot}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
123
packages/bolt/app/components/chat/Chat.client.tsx
Normal file
123
packages/bolt/app/components/chat/Chat.client.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import { useChat } from 'ai/react';
|
||||
import { cubicBezier, useAnimate } from 'framer-motion';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useMessageParser, usePromptEnhancer } from '~/lib/hooks';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { BaseChat } from './BaseChat';
|
||||
import { Messages } from './Messages';
|
||||
|
||||
const logger = createScopedLogger('Chat');
|
||||
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
|
||||
|
||||
export function Chat() {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const [chatStarted, setChatStarted] = useState(false);
|
||||
|
||||
const [animationScope, animate] = useAnimate();
|
||||
|
||||
const { messages, isLoading, input, handleInputChange, setInput, handleSubmit } = useChat({
|
||||
api: '/api/chat',
|
||||
onError: (error) => {
|
||||
logger.error(error);
|
||||
},
|
||||
onFinish: () => {
|
||||
logger.debug('Finished streaming');
|
||||
},
|
||||
});
|
||||
|
||||
const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
|
||||
const { parsedMessages, parseMessages } = useMessageParser();
|
||||
|
||||
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
||||
|
||||
useEffect(() => {
|
||||
parseMessages(messages, isLoading);
|
||||
}, [messages, isLoading, parseMessages]);
|
||||
|
||||
const scrollTextArea = () => {
|
||||
const textarea = textareaRef.current;
|
||||
|
||||
if (textarea) {
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
|
||||
const scrollHeight = textarea.scrollHeight;
|
||||
|
||||
textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`;
|
||||
textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';
|
||||
}
|
||||
}, [input, textareaRef]);
|
||||
|
||||
const runAnimation = async () => {
|
||||
if (chatStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
animate('#chat', { height: '100%' }, { duration: 0.3, ease: customEasingFn }),
|
||||
animate('#intro', { opacity: 0, display: 'none' }, { duration: 0.15, ease: customEasingFn }),
|
||||
]);
|
||||
|
||||
setChatStarted(true);
|
||||
};
|
||||
|
||||
const sendMessage = () => {
|
||||
if (input.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
runAnimation();
|
||||
handleSubmit();
|
||||
resetEnhancer();
|
||||
|
||||
textareaRef.current?.blur();
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseChat
|
||||
ref={animationScope}
|
||||
textareaRef={textareaRef}
|
||||
input={input}
|
||||
chatStarted={chatStarted}
|
||||
enhancingPrompt={enhancingPrompt}
|
||||
promptEnhanced={promptEnhanced}
|
||||
sendMessage={sendMessage}
|
||||
handleInputChange={handleInputChange}
|
||||
messagesSlot={
|
||||
chatStarted ? (
|
||||
<Messages
|
||||
classNames={{
|
||||
root: 'h-full pt-10',
|
||||
messagesContainer: 'max-w-2xl mx-auto max-md:pb-[calc(140px+1.5rem)] md:pb-[calc(140px+3rem)]',
|
||||
}}
|
||||
messages={messages.map((message, i) => {
|
||||
if (message.role === 'user') {
|
||||
return message;
|
||||
}
|
||||
|
||||
return {
|
||||
...message,
|
||||
content: parsedMessages[i] || '',
|
||||
};
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
enhancePrompt={() => {
|
||||
enhancePrompt(input, (input) => {
|
||||
setInput(input);
|
||||
scrollTextArea();
|
||||
});
|
||||
}}
|
||||
></BaseChat>
|
||||
);
|
||||
}
|
10
packages/bolt/app/components/chat/CodeBlock.module.scss
Normal file
10
packages/bolt/app/components/chat/CodeBlock.module.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.CopyButtonContainer {
|
||||
button:before {
|
||||
content: 'Copied';
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
left: -53px;
|
||||
padding: 2px 6px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
82
packages/bolt/app/components/chat/CodeBlock.tsx
Normal file
82
packages/bolt/app/components/chat/CodeBlock.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import {
|
||||
bundledLanguages,
|
||||
codeToHtml,
|
||||
isSpecialLang,
|
||||
type BundledLanguage,
|
||||
type BundledTheme,
|
||||
type SpecialLanguage,
|
||||
} from 'shiki';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import styles from './CodeBlock.module.scss';
|
||||
|
||||
const logger = createScopedLogger('CodeBlock');
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
language?: BundledLanguage;
|
||||
theme?: BundledTheme | SpecialLanguage;
|
||||
}
|
||||
|
||||
export const CodeBlock = memo(({ code, language, theme }: CodeBlockProps) => {
|
||||
const [html, setHTML] = useState<string | undefined>(undefined);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
if (copied) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(code);
|
||||
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (language && !isSpecialLang(language) && !(language in bundledLanguages)) {
|
||||
logger.warn(`Unsupported language '${language}'`);
|
||||
}
|
||||
|
||||
logger.trace(`Language = ${language}`);
|
||||
|
||||
const processCode = async () => {
|
||||
setHTML(await codeToHtml(code, { lang: language ?? 'plaintext', theme: theme ?? 'dark-plus' }));
|
||||
};
|
||||
|
||||
processCode();
|
||||
}, [code]);
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div
|
||||
className={classNames(
|
||||
styles.CopyButtonContainer,
|
||||
'bg-white absolute top-[10px] right-[10px] rounded-md z-10 text-lg flex items-center justify-center opacity-0 group-hover:opacity-100',
|
||||
{
|
||||
'rounded-l-0 opacity-100': copied,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className={classNames(
|
||||
'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',
|
||||
{
|
||||
'before:opacity-0': !copied,
|
||||
'before:opacity-100': copied,
|
||||
},
|
||||
)}
|
||||
title="Copy Code"
|
||||
onClick={() => copyToClipboard()}
|
||||
>
|
||||
<div className="i-ph:clipboard-text-duotone"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div dangerouslySetInnerHTML={{ __html: html ?? '' }}></div>
|
||||
</div>
|
||||
);
|
||||
});
|
136
packages/bolt/app/components/chat/Markdown.module.scss
Normal file
136
packages/bolt/app/components/chat/Markdown.module.scss
Normal file
@ -0,0 +1,136 @@
|
||||
$font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace,
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
$color-text: #333;
|
||||
$color-heading: #2c3e50;
|
||||
$color-link: #3498db;
|
||||
$color-code-bg: #f8f8f8;
|
||||
$color-blockquote-border: #dfe2e5;
|
||||
|
||||
.MarkdownContent {
|
||||
line-height: 1.6;
|
||||
color: $color-text;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: $color-heading;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.85em;
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
p:not(:last-of-type) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-link;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
font-family: $font-mono;
|
||||
font-size: 14px;
|
||||
background-color: $color-code-bg;
|
||||
border-radius: 6px;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 20px 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
pre:has(> code) {
|
||||
font-family: $font-mono;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
color: #6a737d;
|
||||
border-left: 0.25em solid $color-blockquote-border;
|
||||
}
|
||||
|
||||
:is(ul, ol) {
|
||||
padding-left: 2em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: #e1e4e8;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
:is(th, td) {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
}
|
||||
}
|
66
packages/bolt/app/components/chat/Markdown.tsx
Normal file
66
packages/bolt/app/components/chat/Markdown.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { memo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import type { BundledLanguage } from 'shiki';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { rehypePlugins, remarkPlugins } from '~/utils/markdown';
|
||||
import { Artifact } from './Artifact';
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
import styles from './Markdown.module.scss';
|
||||
|
||||
const logger = createScopedLogger('MarkdownComponent');
|
||||
|
||||
interface MarkdownProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
export const Markdown = memo(({ children }: MarkdownProps) => {
|
||||
logger.trace('Render');
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={styles.MarkdownContent}
|
||||
components={{
|
||||
div: ({ className, children, node, ...props }) => {
|
||||
if (className?.includes('__boltArtifact__')) {
|
||||
const messageId = node?.properties.dataMessageId as string;
|
||||
|
||||
if (!messageId) {
|
||||
logger.warn(`Invalud message id ${messageId}`);
|
||||
}
|
||||
|
||||
return <Artifact messageId={messageId} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
pre: (props) => {
|
||||
const { children, node, ...rest } = props;
|
||||
|
||||
const [firstChild] = node?.children ?? [];
|
||||
|
||||
if (
|
||||
firstChild &&
|
||||
firstChild.type === 'element' &&
|
||||
firstChild.tagName === 'code' &&
|
||||
firstChild.children[0].type === 'text'
|
||||
) {
|
||||
const { className, ...rest } = firstChild.properties;
|
||||
const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? [];
|
||||
|
||||
return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;
|
||||
}
|
||||
|
||||
return <pre {...rest}>{children}</pre>;
|
||||
},
|
||||
}}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
});
|
55
packages/bolt/app/components/chat/Messages.tsx
Normal file
55
packages/bolt/app/components/chat/Messages.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import type { Message } from 'ai';
|
||||
import { useRef } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { AssistantMessage } from './AssistantMessage';
|
||||
import { UserMessage } from './UserMessage';
|
||||
|
||||
interface MessagesProps {
|
||||
id?: string;
|
||||
classNames?: { root?: string; messagesContainer?: string };
|
||||
isLoading?: boolean;
|
||||
messages?: Message[];
|
||||
}
|
||||
|
||||
export function Messages(props: MessagesProps) {
|
||||
const { id, isLoading, messages = [] } = props;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div id={id} ref={containerRef} className={props.classNames?.root}>
|
||||
<div className={classNames('flex flex-col', props.classNames?.messagesContainer)}>
|
||||
{messages.length > 0
|
||||
? messages.map((message, i) => {
|
||||
const { role, content } = message;
|
||||
const isUser = role === 'user';
|
||||
const isFirst = i === 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className={classNames('flex gap-4 border rounded-md p-6 bg-white/80 backdrop-blur-sm', {
|
||||
'mt-4': !isFirst,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex items-center justify-center min-w-[34px] min-h-[34px] text-gray-600 rounded-md p-1 self-start',
|
||||
{
|
||||
'bg-gray-100': role === 'user',
|
||||
'bg-accent text-xl': role === 'assistant',
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className={role === 'user' ? 'i-ph:user-fill text-xl' : 'i-blitz:logo'}></div>
|
||||
</div>
|
||||
{isUser ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
{isLoading && <div className="text-center w-full i-svg-spinners:3-dots-fade text-4xl mt-4"></div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
30
packages/bolt/app/components/chat/SendButton.client.tsx
Normal file
30
packages/bolt/app/components/chat/SendButton.client.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { AnimatePresence, cubicBezier, motion } from 'framer-motion';
|
||||
|
||||
interface SendButtonProps {
|
||||
show: boolean;
|
||||
onClick?: VoidFunction;
|
||||
}
|
||||
|
||||
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
|
||||
|
||||
export function SendButton({ show, onClick }: SendButtonProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{show ? (
|
||||
<motion.button
|
||||
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]"
|
||||
transition={{ ease: customEasingFn, duration: 0.17 }}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onClick?.();
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:arrow-right text-xl"></div>
|
||||
</motion.button>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
13
packages/bolt/app/components/chat/UserMessage.tsx
Normal file
13
packages/bolt/app/components/chat/UserMessage.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Markdown } from './Markdown';
|
||||
|
||||
interface UserMessageProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function UserMessage({ content }: UserMessageProps) {
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
<Markdown>{content}</Markdown>
|
||||
</div>
|
||||
);
|
||||
}
|
70
packages/bolt/app/components/ui/IconButton.tsx
Normal file
70
packages/bolt/app/components/ui/IconButton.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { memo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
|
||||
type IconSize = 'sm' | 'md' | 'xl';
|
||||
|
||||
interface BaseIconButtonProps {
|
||||
size?: IconSize;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
disabledClassName?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
type IconButtonWithoutChildrenProps = {
|
||||
icon: string;
|
||||
children?: undefined;
|
||||
} & BaseIconButtonProps;
|
||||
|
||||
type IconButtonWithChildrenProps = {
|
||||
icon?: undefined;
|
||||
children: string | JSX.Element | JSX.Element[];
|
||||
} & BaseIconButtonProps;
|
||||
|
||||
type IconButtonProps = IconButtonWithoutChildrenProps | IconButtonWithChildrenProps;
|
||||
|
||||
export const IconButton = memo(
|
||||
({
|
||||
icon,
|
||||
size = 'xl',
|
||||
className,
|
||||
iconClassName,
|
||||
disabledClassName,
|
||||
disabled = false,
|
||||
onClick,
|
||||
children,
|
||||
}: IconButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'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',
|
||||
{
|
||||
[classNames('opacity-30', disabledClassName)]: disabled,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={(event) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.(event);
|
||||
}}
|
||||
>
|
||||
{children ? children : <div className={classNames(icon, getIconSize(size), iconClassName)}></div>}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function getIconSize(size: IconSize) {
|
||||
if (size === 'sm') {
|
||||
return 'text-sm';
|
||||
} else if (size === 'md') {
|
||||
return 'text-md';
|
||||
} else {
|
||||
return 'text-xl';
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export function Workspace() {
|
||||
return <div>WORKSPACE PANEL</div>;
|
||||
}
|
12
packages/bolt/app/entry.client.tsx
Normal file
12
packages/bolt/app/entry.client.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { RemixBrowser } from '@remix-run/react';
|
||||
import { startTransition, StrictMode } from 'react';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<RemixBrowser />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
34
packages/bolt/app/entry.server.tsx
Normal file
34
packages/bolt/app/entry.server.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare';
|
||||
import { RemixServer } from '@remix-run/react';
|
||||
import { isbot } from 'isbot';
|
||||
import { renderToReadableStream } from 'react-dom/server';
|
||||
|
||||
export default async function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
_loadContext: AppLoadContext,
|
||||
) {
|
||||
const body = await renderToReadableStream(<RemixServer context={remixContext} url={request.url} />, {
|
||||
signal: request.signal,
|
||||
onError(error: unknown) {
|
||||
console.error(error);
|
||||
responseStatusCode = 500;
|
||||
},
|
||||
});
|
||||
|
||||
if (isbot(request.headers.get('user-agent') || '')) {
|
||||
await body.allReady;
|
||||
}
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html');
|
||||
|
||||
responseHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||
responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
|
||||
return new Response(body, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
});
|
||||
}
|
9
packages/bolt/app/lib/.server/llm/api-key.ts
Normal file
9
packages/bolt/app/lib/.server/llm/api-key.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { env } from 'node:process';
|
||||
|
||||
export function getAPIKey(cloudflareEnv: Env) {
|
||||
/**
|
||||
* The `cloudflareEnv` is only used when deployed or when previewing locally.
|
||||
* In development the environment variables are available through `env`.
|
||||
*/
|
||||
return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
|
||||
}
|
9
packages/bolt/app/lib/.server/llm/model.ts
Normal file
9
packages/bolt/app/lib/.server/llm/model.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||
|
||||
export function getAnthropicModel(apiKey: string) {
|
||||
const anthropic = createAnthropic({
|
||||
apiKey,
|
||||
});
|
||||
|
||||
return anthropic('claude-3-5-sonnet-20240620');
|
||||
}
|
185
packages/bolt/app/lib/.server/llm/prompts.ts
Normal file
185
packages/bolt/app/lib/.server/llm/prompts.ts
Normal file
@ -0,0 +1,185 @@
|
||||
export const systemPrompt = `
|
||||
You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices.
|
||||
|
||||
<system_constraints>
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
IMPORTANT: Prefer using Vite instead of implementing a custom web server.
|
||||
|
||||
IMPORTANT: Git is NOT available.
|
||||
|
||||
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']
|
||||
</system_constraints>
|
||||
|
||||
<code_formatting_info>
|
||||
Use 2 spaces for code indentation
|
||||
</code_formatting_info>
|
||||
|
||||
<artifact_info>
|
||||
Bolt creates a SINGLE, comprehensive artifact for each project. The artifact contains all necessary steps and components, including:
|
||||
|
||||
- Shell commands to run including dependencies to install using a package manager (NPM)
|
||||
- Files to create and their contents
|
||||
|
||||
<artifact_instructions>
|
||||
1. Think BEFORE creating an artifact.
|
||||
|
||||
2. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
|
||||
|
||||
3. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
|
||||
|
||||
3. Use \`<boltAction>\` tags to define specific actions to perform.
|
||||
|
||||
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:
|
||||
|
||||
- shell: For running shell commands. When Using \`npx\`, ALWAYS provide the \`--yes\` flag!
|
||||
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
5. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
|
||||
|
||||
IMPORTANT: Add all required dependencies to the \`package.json\` already and try to avoid \`npm i <pkg>\` if possible!
|
||||
|
||||
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...".
|
||||
|
||||
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!
|
||||
</artifact_instructions>
|
||||
</artifact_info>
|
||||
|
||||
NEVER use the word "artifact". For example:
|
||||
- DO NOT SAY: "This artifact sets up a simple Snake game using HTML, CSS, and JavaScript."
|
||||
- INSTEAD SAY: "We set up a simple Snake game using HTML, CSS, and JavaScript."
|
||||
|
||||
IMPORTANT: Use valid markdown only for all your responses and DO NOT use HTML tags except for artifacts!
|
||||
|
||||
ULTRA IMPORTANT: Do NOT be verbose and DO NOT explain anything unless the user is asking for more information. That is VERY important.
|
||||
|
||||
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.
|
||||
|
||||
Here are some examples of correct usage of artifacts:
|
||||
|
||||
<examples>
|
||||
<example>
|
||||
<user_query>Can you help me create a JavaScript function to calculate the factorial of a number?</user_query>
|
||||
|
||||
<assistant_response>
|
||||
Certainly, I can help you create a JavaScript function to calculate the factorial of a number.
|
||||
|
||||
<boltArtifact title="JavaScript Factorial Function">
|
||||
<boltAction type="file" path="index.js">
|
||||
function factorial(n) {
|
||||
if (n === 0 || n === 1) {
|
||||
return 1;
|
||||
} else if (n < 0) {
|
||||
return "Factorial is not defined for negative numbers";
|
||||
} else {
|
||||
return n * factorial(n - 1);
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="shell">
|
||||
node index.js
|
||||
</boltAction>
|
||||
</boltArtifact>
|
||||
</assistant_response>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<user_query>Build a snake game</user_query>
|
||||
|
||||
<assistant_response>
|
||||
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.
|
||||
|
||||
<boltArtifact title="Snake Game in HTML and JavaScript">
|
||||
<boltAction type="file" path="package.json">
|
||||
{
|
||||
"name": "snake",
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
}
|
||||
...
|
||||
}
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="shell">
|
||||
npm install --save-dev vite
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="file" path="index.html">
|
||||
...
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="shell">
|
||||
npm run dev
|
||||
</boltAction>
|
||||
</boltArtifact>
|
||||
|
||||
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.
|
||||
</assistant_response>
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<user_query>Make a bouncing ball with real gravity using React</user_query>
|
||||
|
||||
<assistant_response>
|
||||
Certainly! I'll create a bouncing ball with real gravity using React. We'll use the react-spring library for physics-based animations.
|
||||
|
||||
<boltArtifact title="Bouncing Ball with Gravity in React">
|
||||
<boltAction type="file" path="package.json">
|
||||
{
|
||||
"name": "bouncing-ball",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-spring": "^9.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="file" path="index.html">
|
||||
...
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="file" path="src/main.jsx">
|
||||
...
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="file" path="src/index.css">
|
||||
...
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="file" path="src/App.jsx">
|
||||
...
|
||||
</boltAction>
|
||||
|
||||
<boltAction type="shell">
|
||||
npm run dev
|
||||
</boltAction>
|
||||
</boltArtifact>
|
||||
|
||||
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.
|
||||
</assistant_response>
|
||||
</example>
|
||||
</examples>
|
||||
`;
|
2
packages/bolt/app/lib/hooks/index.ts
Normal file
2
packages/bolt/app/lib/hooks/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './useMessageParser';
|
||||
export * from './usePromptEnhancer';
|
57
packages/bolt/app/lib/hooks/useMessageParser.ts
Normal file
57
packages/bolt/app/lib/hooks/useMessageParser.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import type { Message } from 'ai';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { StreamingMessageParser } from '~/lib/runtime/message-parser';
|
||||
import { workspaceStore } from '~/lib/stores/workspace';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('useMessageParser');
|
||||
|
||||
const messageParser = new StreamingMessageParser({
|
||||
callbacks: {
|
||||
onArtifactOpen: (messageId, { title }) => {
|
||||
logger.debug('onArtifactOpen', title);
|
||||
workspaceStore.updateArtifact(messageId, { title, closed: false });
|
||||
},
|
||||
onArtifactClose: (messageId) => {
|
||||
logger.debug('onArtifactClose');
|
||||
workspaceStore.updateArtifact(messageId, { closed: true });
|
||||
},
|
||||
onAction: (messageId, { type, path, content }) => {
|
||||
console.log('ACTION', messageId, { type, path, content });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function useMessageParser() {
|
||||
const [parsedMessages, setParsedMessages] = useState<{ [key: number]: string }>({});
|
||||
|
||||
const parseMessages = useCallback((messages: Message[], isLoading: boolean) => {
|
||||
let reset = false;
|
||||
|
||||
if (import.meta.env.DEV && !isLoading) {
|
||||
reset = true;
|
||||
messageParser.reset();
|
||||
}
|
||||
|
||||
for (const [index, message] of messages.entries()) {
|
||||
if (message.role === 'assistant') {
|
||||
/**
|
||||
* In production, we only parse the last assistant message since previous messages can't change.
|
||||
* During development they can change, e.g., if the parser gets modified.
|
||||
*/
|
||||
if (import.meta.env.PROD && index < messages.length - 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newParsedContent = messageParser.parse(message.id, message.content);
|
||||
|
||||
setParsedMessages((prevParsed) => ({
|
||||
...prevParsed,
|
||||
[index]: !reset ? (prevParsed[index] || '') + newParsedContent : newParsedContent,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { parsedMessages, parseMessages };
|
||||
}
|
71
packages/bolt/app/lib/hooks/usePromptEnhancer.ts
Normal file
71
packages/bolt/app/lib/hooks/usePromptEnhancer.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { useState } from 'react';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('usePromptEnhancement');
|
||||
|
||||
export function usePromptEnhancer() {
|
||||
const [enhancingPrompt, setEnhancingPrompt] = useState(false);
|
||||
const [promptEnhanced, setPromptEnhanced] = useState(false);
|
||||
|
||||
const resetEnhancer = () => {
|
||||
setEnhancingPrompt(false);
|
||||
setPromptEnhanced(false);
|
||||
};
|
||||
|
||||
const enhancePrompt = async (input: string, setInput: (value: string) => void) => {
|
||||
setEnhancingPrompt(true);
|
||||
setPromptEnhanced(false);
|
||||
|
||||
const response = await fetch('/api/enhancer', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
message: input,
|
||||
}),
|
||||
});
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
const originalInput = input;
|
||||
|
||||
if (reader) {
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let _input = '';
|
||||
let _error;
|
||||
|
||||
try {
|
||||
setInput('');
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
_input += decoder.decode(value);
|
||||
|
||||
logger.trace('Set input', _input);
|
||||
|
||||
setInput(_input);
|
||||
}
|
||||
} catch (error) {
|
||||
_error = error;
|
||||
setInput(originalInput);
|
||||
} finally {
|
||||
if (_error) {
|
||||
logger.error(_error);
|
||||
}
|
||||
|
||||
setEnhancingPrompt(false);
|
||||
setPromptEnhanced(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setInput(_input);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer };
|
||||
}
|
84
packages/bolt/app/lib/runtime/message-parser.spec.ts
Normal file
84
packages/bolt/app/lib/runtime/message-parser.spec.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { StreamingMessageParser } from './message-parser';
|
||||
|
||||
describe('StreamingMessageParser', () => {
|
||||
it('should pass through normal text', () => {
|
||||
const parser = new StreamingMessageParser();
|
||||
expect(parser.parse('test_id', 'Hello, world!')).toBe('Hello, world!');
|
||||
});
|
||||
|
||||
it('should allow normal HTML tags', () => {
|
||||
const parser = new StreamingMessageParser();
|
||||
expect(parser.parse('test_id', 'Hello <strong>world</strong>!')).toBe('Hello <strong>world</strong>!');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['Foo bar', 'Foo bar'],
|
||||
['Foo bar <', 'Foo bar '],
|
||||
['Foo bar <p', 'Foo bar <p'],
|
||||
['Foo bar <b', 'Foo bar '],
|
||||
['Foo bar <ba', 'Foo bar <ba'],
|
||||
['Foo bar <bol', 'Foo bar '],
|
||||
['Foo bar <bolt', 'Foo bar '],
|
||||
['Foo bar <bolta', 'Foo bar <bolta'],
|
||||
['Foo bar <boltA', 'Foo bar '],
|
||||
['Some text before <boltArtifact>foo</boltArtifact> Some more text', 'Some text before Some more text'],
|
||||
[['Some text before <boltArti', 'fact>foo</boltArtifact> Some more text'], 'Some text before Some more text'],
|
||||
[['Some text before <boltArti', 'fac', 't>foo</boltArtifact> Some more text'], 'Some text before Some more text'],
|
||||
[['Some text before <boltArti', 'fact>fo', 'o</boltArtifact> Some more text'], 'Some text before Some more text'],
|
||||
[
|
||||
['Some text before <boltArti', 'fact>fo', 'o', '<', '/boltArtifact> Some more text'],
|
||||
'Some text before Some more text',
|
||||
],
|
||||
[
|
||||
['Some text before <boltArti', 'fact>fo', 'o<', '/boltArtifact> Some more text'],
|
||||
'Some text before Some more text',
|
||||
],
|
||||
['Before <oltArtfiact>foo</boltArtifact> After', 'Before <oltArtfiact>foo</boltArtifact> After'],
|
||||
['Before <boltArtifactt>foo</boltArtifact> After', 'Before <boltArtifactt>foo</boltArtifact> After'],
|
||||
['Before <boltArtifact title="Some title">foo</boltArtifact> After', 'Before After'],
|
||||
[
|
||||
'Before <boltArtifact title="Some title"><boltAction type="shell">npm install</boltAction></boltArtifact> After',
|
||||
'Before After',
|
||||
[{ type: 'shell', content: 'npm install' }],
|
||||
],
|
||||
[
|
||||
'Before <boltArtifact title="Some title"><boltAction type="shell">npm install</boltAction><boltAction type="file" path="index.js">some content</boltAction></boltArtifact> After',
|
||||
'Before After',
|
||||
[
|
||||
{ type: 'shell', content: 'npm install' },
|
||||
{ type: 'file', path: 'index.js', content: 'some content\n' },
|
||||
],
|
||||
],
|
||||
])('should correctly parse chunks and strip out bolt artifacts', (input, expected, expectedActions = []) => {
|
||||
let actionCounter = 0;
|
||||
|
||||
const testId = 'test_id';
|
||||
|
||||
const parser = new StreamingMessageParser({
|
||||
artifactElement: '',
|
||||
callbacks: {
|
||||
onAction: (id, action) => {
|
||||
expect(testId).toBe(id);
|
||||
expect(action).toEqual(expectedActions[actionCounter]);
|
||||
actionCounter++;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let message = '';
|
||||
|
||||
let result = '';
|
||||
|
||||
const chunks = Array.isArray(input) ? input : input.split('');
|
||||
|
||||
for (const chunk of chunks) {
|
||||
message += chunk;
|
||||
|
||||
result += parser.parse(testId, message);
|
||||
}
|
||||
|
||||
expect(actionCounter).toBe(expectedActions.length);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
172
packages/bolt/app/lib/runtime/message-parser.ts
Normal file
172
packages/bolt/app/lib/runtime/message-parser.ts
Normal file
@ -0,0 +1,172 @@
|
||||
const ARTIFACT_TAG_OPEN = '<boltArtifact';
|
||||
const ARTIFACT_TAG_CLOSE = '</boltArtifact>';
|
||||
const ARTIFACT_ACTION_TAG_OPEN = '<boltAction';
|
||||
const ARTIFACT_ACTION_TAG_CLOSE = '</boltAction>';
|
||||
|
||||
interface BoltArtifact {
|
||||
title: string;
|
||||
}
|
||||
|
||||
type ArtifactOpenCallback = (messageId: string, artifact: BoltArtifact) => void;
|
||||
type ArtifactCloseCallback = (messageId: string) => void;
|
||||
type ActionCallback = (messageId: string, action: BoltActionData) => void;
|
||||
|
||||
type ActionType = 'file' | 'shell';
|
||||
|
||||
export interface BoltActionData {
|
||||
type?: ActionType;
|
||||
path?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
onArtifactOpen?: ArtifactOpenCallback;
|
||||
onArtifactClose?: ArtifactCloseCallback;
|
||||
onAction?: ActionCallback;
|
||||
}
|
||||
|
||||
type ElementFactory = () => string;
|
||||
|
||||
interface StreamingMessageParserOptions {
|
||||
callbacks?: Callbacks;
|
||||
artifactElement?: string | ElementFactory;
|
||||
}
|
||||
|
||||
export class StreamingMessageParser {
|
||||
#lastPositions = new Map<string, number>();
|
||||
#insideArtifact = false;
|
||||
#insideAction = false;
|
||||
#currentAction: BoltActionData = { content: '' };
|
||||
|
||||
constructor(private _options: StreamingMessageParserOptions = {}) {}
|
||||
|
||||
parse(id: string, input: string) {
|
||||
let output = '';
|
||||
let i = this.#lastPositions.get(id) ?? 0;
|
||||
let earlyBreak = false;
|
||||
|
||||
while (i < input.length) {
|
||||
if (this.#insideArtifact) {
|
||||
if (this.#insideAction) {
|
||||
const closeIndex = input.indexOf(ARTIFACT_ACTION_TAG_CLOSE, i);
|
||||
|
||||
if (closeIndex !== -1) {
|
||||
this.#currentAction.content += input.slice(i, closeIndex);
|
||||
|
||||
let content = this.#currentAction.content.trim();
|
||||
|
||||
if (this.#currentAction.type === 'file') {
|
||||
content += '\n';
|
||||
}
|
||||
|
||||
this.#currentAction.content = content;
|
||||
|
||||
this._options.callbacks?.onAction?.(id, this.#currentAction);
|
||||
|
||||
this.#insideAction = false;
|
||||
this.#currentAction = { content: '' };
|
||||
|
||||
i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const actionOpenIndex = input.indexOf(ARTIFACT_ACTION_TAG_OPEN, i);
|
||||
const artifactCloseIndex = input.indexOf(ARTIFACT_TAG_CLOSE, i);
|
||||
|
||||
if (actionOpenIndex !== -1 && (artifactCloseIndex === -1 || actionOpenIndex < artifactCloseIndex)) {
|
||||
const actionEndIndex = input.indexOf('>', actionOpenIndex);
|
||||
|
||||
if (actionEndIndex !== -1) {
|
||||
const actionTag = input.slice(actionOpenIndex, actionEndIndex + 1);
|
||||
this.#currentAction.type = this.#extractAttribute(actionTag, 'type') as ActionType;
|
||||
this.#currentAction.path = this.#extractAttribute(actionTag, 'path');
|
||||
this.#insideAction = true;
|
||||
i = actionEndIndex + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (artifactCloseIndex !== -1) {
|
||||
this.#insideArtifact = false;
|
||||
|
||||
this._options.callbacks?.onArtifactClose?.(id);
|
||||
|
||||
i = artifactCloseIndex + ARTIFACT_TAG_CLOSE.length;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (input[i] === '<' && input[i + 1] !== '/') {
|
||||
let j = i;
|
||||
let potentialTag = '';
|
||||
|
||||
while (j < input.length && potentialTag.length < ARTIFACT_TAG_OPEN.length) {
|
||||
potentialTag += input[j];
|
||||
|
||||
if (potentialTag === ARTIFACT_TAG_OPEN) {
|
||||
const nextChar = input[j + 1];
|
||||
|
||||
if (nextChar && nextChar !== '>' && nextChar !== ' ') {
|
||||
output += input.slice(i, j + 1);
|
||||
i = j + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
const openTagEnd = input.indexOf('>', j);
|
||||
|
||||
if (openTagEnd !== -1) {
|
||||
const artifactTag = input.slice(i, openTagEnd + 1);
|
||||
|
||||
const artifactTitle = this.#extractAttribute(artifactTag, 'title') as string;
|
||||
|
||||
this.#insideArtifact = true;
|
||||
|
||||
this._options.callbacks?.onArtifactOpen?.(id, { title: artifactTitle });
|
||||
|
||||
output += this._options.artifactElement ?? `<div class="__boltArtifact__" data-message-id="${id}"></div>`;
|
||||
|
||||
i = openTagEnd + 1;
|
||||
} else {
|
||||
earlyBreak = true;
|
||||
}
|
||||
|
||||
break;
|
||||
} else if (!ARTIFACT_TAG_OPEN.startsWith(potentialTag)) {
|
||||
output += input.slice(i, j + 1);
|
||||
i = j + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
j++;
|
||||
}
|
||||
|
||||
if (j === input.length && ARTIFACT_TAG_OPEN.startsWith(potentialTag)) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
output += input[i];
|
||||
i++;
|
||||
}
|
||||
|
||||
if (earlyBreak) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.#lastPositions.set(id, i);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#lastPositions.clear();
|
||||
this.#insideArtifact = false;
|
||||
this.#insideAction = false;
|
||||
this.#currentAction = { content: '' };
|
||||
}
|
||||
|
||||
#extractAttribute(tag: string, attributeName: string): string | undefined {
|
||||
const match = tag.match(new RegExp(`${attributeName}="([^"]*)"`, 'i'));
|
||||
return match ? match[1] : undefined;
|
||||
}
|
||||
}
|
42
packages/bolt/app/lib/stores/workspace.ts
Normal file
42
packages/bolt/app/lib/stores/workspace.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { WebContainer } from '@webcontainer/api';
|
||||
import { atom, map, type MapStore, type WritableAtom } from 'nanostores';
|
||||
import { webcontainer } from '~/lib/webcontainer';
|
||||
|
||||
interface WorkspaceStoreOptions {
|
||||
webcontainer: Promise<WebContainer>;
|
||||
}
|
||||
|
||||
interface ArtifactState {
|
||||
title: string;
|
||||
closed: boolean;
|
||||
actions: any /* TODO */;
|
||||
}
|
||||
|
||||
export class WorkspaceStore {
|
||||
#webcontainer: Promise<WebContainer>;
|
||||
|
||||
artifacts: MapStore<Record<string, ArtifactState>> = import.meta.hot?.data.artifacts ?? map({});
|
||||
showWorkspace: WritableAtom<boolean> = import.meta.hot?.data.showWorkspace ?? atom(false);
|
||||
|
||||
constructor({ webcontainer }: WorkspaceStoreOptions) {
|
||||
this.#webcontainer = webcontainer;
|
||||
}
|
||||
|
||||
updateArtifact(id: string, state: Partial<ArtifactState>) {
|
||||
const artifacts = this.artifacts.get();
|
||||
const artifact = artifacts[id];
|
||||
|
||||
this.artifacts.setKey(id, { ...artifact, ...state });
|
||||
}
|
||||
|
||||
runAction() {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
export const workspaceStore = new WorkspaceStore({ webcontainer });
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.artifacts = workspaceStore.artifacts;
|
||||
import.meta.hot.data.showWorkspace = workspaceStore.showWorkspace;
|
||||
}
|
31
packages/bolt/app/lib/webcontainer/index.ts
Normal file
31
packages/bolt/app/lib/webcontainer/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { WebContainer } from '@webcontainer/api';
|
||||
|
||||
interface WebContainerContext {
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
export const webcontainerContext: WebContainerContext = import.meta.hot?.data.webcontainerContext ?? {
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.webcontainerContext = webcontainerContext;
|
||||
}
|
||||
|
||||
export let webcontainer: Promise<WebContainer> = new Promise(() => {
|
||||
// noop for ssr
|
||||
});
|
||||
|
||||
if (!import.meta.env.SSR) {
|
||||
webcontainer =
|
||||
import.meta.hot?.data.webcontainer ??
|
||||
Promise.resolve()
|
||||
.then(() => WebContainer.boot({ workdirName: 'project' }))
|
||||
.then(() => {
|
||||
webcontainerContext.loaded = true;
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.webcontainer = webcontainer;
|
||||
}
|
||||
}
|
51
packages/bolt/app/root.tsx
Normal file
51
packages/bolt/app/root.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import type { LinksFunction } from '@remix-run/cloudflare';
|
||||
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
|
||||
import reset from '@unocss/reset/tailwind.css?url';
|
||||
import globalStyles from '~/styles/index.scss?url';
|
||||
|
||||
import 'virtual:uno.css';
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
{
|
||||
rel: 'icon',
|
||||
href: '/favicon.svg',
|
||||
type: 'image/svg+xml',
|
||||
},
|
||||
{ rel: 'stylesheet', href: reset },
|
||||
{ rel: 'stylesheet', href: globalStyles },
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://fonts.googleapis.com',
|
||||
},
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://fonts.gstatic.com',
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
|
||||
},
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
18
packages/bolt/app/routes/_index.tsx
Normal file
18
packages/bolt/app/routes/_index.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import type { MetaFunction } from '@remix-run/cloudflare';
|
||||
import { ClientOnly } from 'remix-utils/client-only';
|
||||
import { BaseChat } from '~/components/chat/BaseChat';
|
||||
import { Chat } from '~/components/chat/Chat.client';
|
||||
import { Header } from '~/components/Header';
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<Header />
|
||||
<ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
|
||||
</div>
|
||||
);
|
||||
}
|
43
packages/bolt/app/routes/api.chat.ts
Normal file
43
packages/bolt/app/routes/api.chat.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { convertToCoreMessages, streamText } from 'ai';
|
||||
import { getAPIKey } from '~/lib/.server/llm/api-key';
|
||||
import { getAnthropicModel } from '~/lib/.server/llm/model';
|
||||
import { systemPrompt } from '~/lib/.server/llm/prompts';
|
||||
|
||||
interface ToolResult<Name extends string, Args, Result> {
|
||||
toolCallId: string;
|
||||
toolName: Name;
|
||||
args: Args;
|
||||
result: Result;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
toolInvocations?: ToolResult<string, unknown, unknown>[];
|
||||
}
|
||||
|
||||
export async function action({ context, request }: ActionFunctionArgs) {
|
||||
const { messages } = await request.json<{ messages: Message[] }>();
|
||||
|
||||
try {
|
||||
const result = await streamText({
|
||||
model: getAnthropicModel(getAPIKey(context.cloudflare.env)),
|
||||
messages: convertToCoreMessages(messages),
|
||||
toolChoice: 'none',
|
||||
onFinish: ({ finishReason, usage, warnings }) => {
|
||||
console.log({ finishReason, usage, warnings });
|
||||
},
|
||||
system: systemPrompt,
|
||||
});
|
||||
|
||||
return result.toAIStreamResponse();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
});
|
||||
}
|
||||
}
|
65
packages/bolt/app/routes/api.enhancer.ts
Normal file
65
packages/bolt/app/routes/api.enhancer.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { StreamingTextResponse, convertToCoreMessages, parseStreamPart, streamText } from 'ai';
|
||||
import { getAPIKey } from '~/lib/.server/llm/api-key';
|
||||
import { getAnthropicModel } from '~/lib/.server/llm/model';
|
||||
import { systemPrompt } from '~/lib/.server/llm/prompts';
|
||||
import { stripIndents } from '~/utils/stripIndent';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export async function action({ context, request }: ActionFunctionArgs) {
|
||||
const { message } = await request.json<{ message: string }>();
|
||||
|
||||
try {
|
||||
const result = await streamText({
|
||||
model: getAnthropicModel(getAPIKey(context.cloudflare.env)),
|
||||
system: systemPrompt,
|
||||
messages: convertToCoreMessages([
|
||||
{
|
||||
role: 'user',
|
||||
content: stripIndents`
|
||||
I want you to improve the following prompt.
|
||||
|
||||
IMPORTANT: Only respond with the improved prompt and nothing else!
|
||||
|
||||
<original_prompt>
|
||||
${message}
|
||||
</original_prompt>
|
||||
`,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
result.usage.then((usage) => {
|
||||
console.log('Usage', usage);
|
||||
});
|
||||
}
|
||||
|
||||
const transformStream = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
const processedChunk = decoder
|
||||
.decode(chunk)
|
||||
.split('\n')
|
||||
.filter((line) => line !== '')
|
||||
.map(parseStreamPart)
|
||||
.map((part) => part.value)
|
||||
.join('');
|
||||
|
||||
controller.enqueue(encoder.encode(processedChunk));
|
||||
},
|
||||
});
|
||||
|
||||
const transformedStream = result.toAIStream().pipeThrough(transformStream);
|
||||
|
||||
return new StreamingTextResponse(transformedStream);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
});
|
||||
}
|
||||
}
|
31
packages/bolt/app/styles/index.scss
Normal file
31
packages/bolt/app/styles/index.scss
Normal file
@ -0,0 +1,31 @@
|
||||
@import './variables.scss';
|
||||
|
||||
body {
|
||||
--at-apply: bg-bolt-elements-app-backgroundColor;
|
||||
|
||||
font-family: 'Inter', sans-serif;
|
||||
|
||||
&:before {
|
||||
--line: color-mix(in lch, canvasText, transparent 93%);
|
||||
--size: 50px;
|
||||
|
||||
content: '';
|
||||
height: 100vh;
|
||||
mask: linear-gradient(-25deg, transparent 60%, white);
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
transform-style: flat;
|
||||
width: 100vw;
|
||||
z-index: -1;
|
||||
background:
|
||||
linear-gradient(90deg, var(--line) 1px, transparent 1px var(--size)) 50% 50% / var(--size) var(--size),
|
||||
linear-gradient(var(--line) 1px, transparent 1px var(--size)) 50% 50% / var(--size) var(--size);
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
21
packages/bolt/app/styles/variables.scss
Normal file
21
packages/bolt/app/styles/variables.scss
Normal file
@ -0,0 +1,21 @@
|
||||
:root,
|
||||
:root[data-theme='light'] {
|
||||
/* Color Tokens */
|
||||
--bolt-background-primary: theme('colors.gray.50');
|
||||
}
|
||||
|
||||
:root,
|
||||
:root[data-theme='dark'] {
|
||||
/* Color Tokens */
|
||||
--bolt-background-primary: theme('colors.gray.50');
|
||||
}
|
||||
|
||||
/*
|
||||
* Element Tokens
|
||||
*
|
||||
* Hierarchy: Element Token -> (Element Token | Color Tokens) -> Primitives
|
||||
*/
|
||||
:root {
|
||||
/* App */
|
||||
--bolt-elements-app-backgroundColor: var(--bolt-background-primary);
|
||||
}
|
61
packages/bolt/app/utils/classNames.ts
Normal file
61
packages/bolt/app/utils/classNames.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2018 Jed Watson.
|
||||
* Licensed under the MIT License (MIT), see:
|
||||
*
|
||||
* @link http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
type ClassNamesArg = undefined | string | Record<string, boolean> | ClassNamesArg[];
|
||||
|
||||
/**
|
||||
* A simple JavaScript utility for conditionally joining classNames together.
|
||||
*
|
||||
* @param args A series of classes or object with key that are class and values
|
||||
* that are interpreted as boolean to decide whether or not the class
|
||||
* should be included in the final class.
|
||||
*/
|
||||
export function classNames(...args: ClassNamesArg[]): string {
|
||||
let classes = '';
|
||||
|
||||
for (const arg of args) {
|
||||
classes = appendClass(classes, parseValue(arg));
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
function parseValue(arg: ClassNamesArg) {
|
||||
if (typeof arg === 'string' || typeof arg === 'number') {
|
||||
return arg;
|
||||
}
|
||||
|
||||
if (typeof arg !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Array.isArray(arg)) {
|
||||
return classNames(...arg);
|
||||
}
|
||||
|
||||
let classes = '';
|
||||
|
||||
for (const key in arg) {
|
||||
if (arg[key]) {
|
||||
classes = appendClass(classes, key);
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
function appendClass(value: string, newClass: string | undefined) {
|
||||
if (!newClass) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
return value + ' ' + newClass;
|
||||
}
|
||||
|
||||
return value + newClass;
|
||||
}
|
87
packages/bolt/app/utils/logger.ts
Normal file
87
packages/bolt/app/utils/logger.ts
Normal file
@ -0,0 +1,87 @@
|
||||
export type DebugLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
type LoggerFunction = (...messages: any[]) => void;
|
||||
|
||||
interface Logger {
|
||||
trace: LoggerFunction;
|
||||
debug: LoggerFunction;
|
||||
info: LoggerFunction;
|
||||
warn: LoggerFunction;
|
||||
error: LoggerFunction;
|
||||
setLevel: (level: DebugLevel) => void;
|
||||
}
|
||||
|
||||
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? 'warn';
|
||||
|
||||
export const logger: Logger = {
|
||||
trace: (...messages: any[]) => log('trace', undefined, messages),
|
||||
debug: (...messages: any[]) => log('debug', undefined, messages),
|
||||
info: (...messages: any[]) => log('info', undefined, messages),
|
||||
warn: (...messages: any[]) => log('warn', undefined, messages),
|
||||
error: (...messages: any[]) => log('error', undefined, messages),
|
||||
setLevel,
|
||||
};
|
||||
|
||||
export function createScopedLogger(scope: string): Logger {
|
||||
return {
|
||||
trace: (...messages: any[]) => log('trace', scope, messages),
|
||||
debug: (...messages: any[]) => log('debug', scope, messages),
|
||||
info: (...messages: any[]) => log('info', scope, messages),
|
||||
warn: (...messages: any[]) => log('warn', scope, messages),
|
||||
error: (...messages: any[]) => log('error', scope, messages),
|
||||
setLevel,
|
||||
};
|
||||
}
|
||||
|
||||
function setLevel(level: DebugLevel) {
|
||||
if ((level === 'trace' || level === 'debug') && import.meta.env.PROD) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentLevel = level;
|
||||
}
|
||||
|
||||
function log(level: DebugLevel, scope: string | undefined, messages: any[]) {
|
||||
const levelOrder: DebugLevel[] = ['trace', 'debug', 'info', 'warn', 'error'];
|
||||
|
||||
if (levelOrder.indexOf(level) >= levelOrder.indexOf(currentLevel)) {
|
||||
const labelBackgroundColor = getColorForLevel(level);
|
||||
const labelTextColor = level === 'warn' ? 'black' : 'white';
|
||||
|
||||
const labelStyles = getLabelStyles(labelBackgroundColor, labelTextColor);
|
||||
const scopeStyles = getLabelStyles('#77828D', 'white');
|
||||
|
||||
const styles = [labelStyles];
|
||||
|
||||
if (typeof scope === 'string') {
|
||||
styles.push('', scopeStyles);
|
||||
}
|
||||
|
||||
console.log(`%c${level.toUpperCase()}${scope ? `%c %c${scope}` : ''}`, ...styles, ...messages);
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelStyles(color: string, textColor: string) {
|
||||
return `background-color: ${color}; color: white; border: 4px solid ${color}; color: ${textColor};`;
|
||||
}
|
||||
|
||||
function getColorForLevel(level: DebugLevel): string {
|
||||
switch (level) {
|
||||
case 'trace':
|
||||
case 'debug': {
|
||||
return '#77828D';
|
||||
}
|
||||
case 'info': {
|
||||
return '#1389FD';
|
||||
}
|
||||
case 'warn': {
|
||||
return '#FFDB6C';
|
||||
}
|
||||
case 'error': {
|
||||
return '#EE4744';
|
||||
}
|
||||
default: {
|
||||
return 'black';
|
||||
}
|
||||
}
|
||||
}
|
6
packages/bolt/app/utils/markdown.ts
Normal file
6
packages/bolt/app/utils/markdown.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import type { PluggableList } from 'unified';
|
||||
|
||||
export const remarkPlugins = [remarkGfm] satisfies PluggableList;
|
||||
export const rehypePlugins = [rehypeRaw] satisfies PluggableList;
|
19
packages/bolt/app/utils/promises.ts
Normal file
19
packages/bolt/app/utils/promises.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export function withResolvers<T>(): PromiseWithResolvers<T> {
|
||||
if (typeof Promise.withResolvers === 'function') {
|
||||
return Promise.withResolvers();
|
||||
}
|
||||
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: any) => void;
|
||||
|
||||
const promise = new Promise<T>((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
|
||||
return {
|
||||
resolve,
|
||||
reject,
|
||||
promise,
|
||||
};
|
||||
}
|
23
packages/bolt/app/utils/stripIndent.ts
Normal file
23
packages/bolt/app/utils/stripIndent.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export function stripIndents(value: string): string;
|
||||
export function stripIndents(strings: TemplateStringsArray, ...values: any[]): string;
|
||||
export function stripIndents(arg0: string | TemplateStringsArray, ...values: any[]) {
|
||||
if (typeof arg0 !== 'string') {
|
||||
const processedString = arg0.reduce((acc, curr, i) => {
|
||||
acc += curr + (values[i] ?? '');
|
||||
return acc;
|
||||
}, '');
|
||||
|
||||
return _stripIndents(processedString);
|
||||
}
|
||||
|
||||
return _stripIndents(arg0);
|
||||
}
|
||||
|
||||
function _stripIndents(value: string) {
|
||||
return value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.join('\n')
|
||||
.trimStart()
|
||||
.replace(/[\r\n]$/, '');
|
||||
}
|
16
packages/bolt/bindings.sh
Executable file
16
packages/bolt/bindings.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
bindings=""
|
||||
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
if [[ ! "$line" =~ ^# ]] && [[ -n "$line" ]]; then
|
||||
name=$(echo "$line" | cut -d '=' -f 1)
|
||||
value=$(echo "$line" | cut -d '=' -f 2-)
|
||||
value=$(echo $value | sed 's/^"\(.*\)"$/\1/')
|
||||
bindings+="--binding ${name}=${value} "
|
||||
fi
|
||||
done < .env.local
|
||||
|
||||
bindings=$(echo $bindings | sed 's/[[:space:]]*$//')
|
||||
|
||||
echo $bindings
|
9
packages/bolt/functions/[[path]].ts
Normal file
9
packages/bolt/functions/[[path]].ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { ServerBuild } from '@remix-run/cloudflare';
|
||||
import { createPagesFunctionHandler } from '@remix-run/cloudflare-pages';
|
||||
|
||||
// @ts-ignore because the server build file is generated by `remix vite:build`
|
||||
import * as serverBuild from '../build/server';
|
||||
|
||||
export const onRequest = createPagesFunctionHandler({
|
||||
build: serverBuild as unknown as ServerBuild,
|
||||
});
|
4
packages/bolt/icons/logo.svg
Normal file
4
packages/bolt/icons/logo.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
||||
<rect width="16" height="16" rx="2" fill="#1389fd" />
|
||||
<path d="M7.398 9.091h-3.58L10.364 2 8.602 6.909h3.58L5.636 14l1.762-4.909Z" fill="#fff" />
|
||||
</svg>
|
After Width: | Height: | Size: 241 B |
1
packages/bolt/icons/stars.svg
Normal file
1
packages/bolt/icons/stars.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M11.5 6a.5.5 0 0 1-.475-.342l-.263-.789a1 1 0 0 0-.632-.632l-.789-.263a.5.5 0 0 1 0-.949l.789-.263a1 1 0 0 0 .632-.632l.264-.79a.5.5 0 0 1 .948 0l.264.79a1 1 0 0 0 .632.632l.789.263a.5.5 0 0 1 0 .949l-.789.263a1 1 0 0 0-.632.632l-.264.789A.5.5 0 0 1 11.5 6ZM6 13a.667.667 0 0 1-.647-.505l-.23-.92a2.337 2.337 0 0 0-1.696-1.698l-.92-.23a.666.666 0 0 1 0-1.293l.92-.23a2.337 2.337 0 0 0 1.697-1.697l.23-.92a.667.667 0 0 1 1.293 0l.23.92a2.333 2.333 0 0 0 1.696 1.697l.92.23a.666.666 0 0 1 0 1.294l-.92.229a2.332 2.332 0 0 0-1.697 1.697l-.23.921A.667.667 0 0 1 6 13ZM13.5 23a.75.75 0 0 1-.72-.544l-.813-2.846a3.75 3.75 0 0 0-2.576-2.576l-2.846-.813a.75.75 0 0 1 0-1.442l2.846-.813a3.75 3.75 0 0 0 2.575-2.576l.814-2.846a.75.75 0 0 1 1.442 0l.813 2.846a3.75 3.75 0 0 0 2.575 2.576l2.846.813a.75.75 0 0 1 0 1.442l-2.846.813a3.749 3.749 0 0 0-2.575 2.576l-.813 2.846A.75.75 0 0 1 13.5 23Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
9
packages/bolt/load-context.ts
Normal file
9
packages/bolt/load-context.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { type PlatformProxy } from 'wrangler';
|
||||
|
||||
type Cloudflare = Omit<PlatformProxy<Env>, 'dispose'>;
|
||||
|
||||
declare module '@remix-run/cloudflare' {
|
||||
interface AppLoadContext {
|
||||
cloudflare: Cloudflare;
|
||||
}
|
||||
}
|
59
packages/bolt/package.json
Normal file
59
packages/bolt/package.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "bolt",
|
||||
"description": "StackBlitz AI Agent",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"deploy": "npm run build && wrangler pages deploy",
|
||||
"build": "remix vite:build",
|
||||
"dev": "remix vite:dev",
|
||||
"test": "vitest --run",
|
||||
"start": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings",
|
||||
"typecheck": "tsc",
|
||||
"typegen": "wrangler types",
|
||||
"preview": "pnpm run build && pnpm run start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^0.0.27",
|
||||
"@iconify-json/ph": "^1.1.13",
|
||||
"@iconify-json/svg-spinners": "^1.1.2",
|
||||
"@nanostores/react": "^0.7.2",
|
||||
"@remix-run/cloudflare": "^2.10.2",
|
||||
"@remix-run/cloudflare-pages": "^2.10.2",
|
||||
"@remix-run/react": "^2.10.2",
|
||||
"@unocss/reset": "^0.61.0",
|
||||
"@webcontainer/api": "^1.2.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"ai": "^3.2.16",
|
||||
"framer-motion": "^11.2.12",
|
||||
"isbot": "^4.1.0",
|
||||
"nanostores": "^0.10.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remix-utils": "^7.6.0",
|
||||
"shiki": "^1.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"@remix-run/dev": "^2.10.0",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"fast-glob": "^3.3.2",
|
||||
"typescript": "^5.5.2",
|
||||
"unified": "^11.0.5",
|
||||
"unocss": "^0.61.0",
|
||||
"vite": "^5.3.1",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"wrangler": "^3.63.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
}
|
4
packages/bolt/public/favicon.svg
Normal file
4
packages/bolt/public/favicon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
||||
<rect width="16" height="16" rx="2" fill="#1389fd" />
|
||||
<path d="M7.398 9.091h-3.58L10.364 2 8.602 6.909h3.58L5.636 14l1.762-4.909Z" fill="#fff" />
|
||||
</svg>
|
After Width: | Height: | Size: 241 B |
33
packages/bolt/tsconfig.json
Normal file
33
packages/bolt/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"types": ["@remix-run/cloudflare", "vite/client", "@cloudflare/workers-types/2023-07-01"],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"target": "ESNext",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
|
||||
// vite takes care of building everything, not tsc
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/.server/**/*.ts",
|
||||
"**/.server/**/*.tsx",
|
||||
"**/.client/**/*.ts",
|
||||
"**/.client/**/*.tsx"
|
||||
]
|
||||
}
|
134
packages/bolt/uno.config.ts
Normal file
134
packages/bolt/uno.config.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { globSync } from 'fast-glob';
|
||||
import fs from 'node:fs/promises';
|
||||
import { basename } from 'node:path';
|
||||
import { defineConfig, presetIcons, presetUno, transformerDirectives } from 'unocss';
|
||||
|
||||
const iconPaths = globSync('./icons/*.svg');
|
||||
|
||||
const collectionName = 'blitz';
|
||||
|
||||
const customIconCollection = iconPaths.reduce(
|
||||
(acc, iconPath) => {
|
||||
const [iconName] = basename(iconPath).split('.');
|
||||
|
||||
acc[collectionName] ??= {};
|
||||
acc[collectionName][iconName] = async () => fs.readFile(iconPath, 'utf8');
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Record<string, () => Promise<string>>>,
|
||||
);
|
||||
|
||||
const COLOR_PRIMITIVES = {
|
||||
accent: {
|
||||
DEFAULT: '#1389FD',
|
||||
50: '#EEF9FF',
|
||||
100: '#D8F1FF',
|
||||
200: '#B9E7FF',
|
||||
300: '#89DBFF',
|
||||
400: '#52C5FF',
|
||||
500: '#2AA7FF',
|
||||
600: '#1389FD',
|
||||
700: '#0C70E9',
|
||||
800: '#115ABC',
|
||||
900: '#144D94',
|
||||
950: '#11305A',
|
||||
},
|
||||
gray: {
|
||||
0: '#FFFFFF',
|
||||
50: '#F6F8F9',
|
||||
100: '#EEF0F1',
|
||||
200: '#E4E6E9',
|
||||
300: '#D2D5D9',
|
||||
400: '#AAAFB6',
|
||||
500: '#7C8085',
|
||||
600: '#565A64',
|
||||
700: '#414349',
|
||||
800: '#31343B',
|
||||
900: '#2B2D35',
|
||||
950: '#232429',
|
||||
1000: '#000000',
|
||||
},
|
||||
positive: {
|
||||
50: '#EDFCF6',
|
||||
100: '#CEFDEB',
|
||||
200: '#A1F9DC',
|
||||
300: '#64F1CB',
|
||||
400: '#24E0B3',
|
||||
500: '#02C79F',
|
||||
600: '#00A282',
|
||||
700: '#00826B',
|
||||
800: '#006656',
|
||||
900: '#005449',
|
||||
950: '#223533',
|
||||
},
|
||||
negative: {
|
||||
50: '#FEF2F3',
|
||||
100: '#FDE6E7',
|
||||
200: '#FBD0D4',
|
||||
300: '#F7AAB1',
|
||||
400: '#F06A78',
|
||||
500: '#E84B60',
|
||||
600: '#D42A48',
|
||||
700: '#B21E3C',
|
||||
800: '#951C38',
|
||||
900: '#801B36',
|
||||
950: '#45212A',
|
||||
},
|
||||
info: {
|
||||
50: '#EFF9FF',
|
||||
100: '#E5F6FF',
|
||||
200: '#B6E9FF',
|
||||
300: '#75DAFF',
|
||||
400: '#2CC8FF',
|
||||
500: '#00AEF2',
|
||||
600: '#008ED4',
|
||||
700: '#0071AB',
|
||||
800: '#005F8D',
|
||||
900: '#064F74',
|
||||
950: '#17374A',
|
||||
},
|
||||
warning: {
|
||||
50: '#FEFAEC',
|
||||
100: '#FCF4D9',
|
||||
200: '#F9E08E',
|
||||
300: '#F6CA53',
|
||||
400: '#ED9413',
|
||||
500: '#D2700D',
|
||||
600: '#AE4E0F',
|
||||
700: '#AE4E0F',
|
||||
800: '#8E3D12',
|
||||
900: '#753212',
|
||||
950: '#402C22',
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
colors: {
|
||||
...COLOR_PRIMITIVES,
|
||||
bolt: {
|
||||
elements: {
|
||||
app: {
|
||||
backgroundColor: 'var(--bolt-elements-app-backgroundColor)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
transformers: [transformerDirectives()],
|
||||
presets: [
|
||||
presetUno({
|
||||
dark: {
|
||||
light: '[data-theme="light"]',
|
||||
dark: '[data-theme="dark"]',
|
||||
},
|
||||
}),
|
||||
presetIcons({
|
||||
warn: true,
|
||||
collections: {
|
||||
...customIconCollection,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
21
packages/bolt/vite.config.ts
Normal file
21
packages/bolt/vite.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, vitePlugin as remixVitePlugin } from '@remix-run/dev';
|
||||
import UnoCSS from 'unocss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig((config) => {
|
||||
return {
|
||||
plugins: [
|
||||
config.mode !== 'test' && remixCloudflareDevProxy(),
|
||||
remixVitePlugin({
|
||||
future: {
|
||||
v3_fetcherPersist: true,
|
||||
v3_relativeSplatPath: true,
|
||||
v3_throwAbortReason: true,
|
||||
},
|
||||
}),
|
||||
UnoCSS(),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
};
|
||||
});
|
3
packages/bolt/worker-configuration.d.ts
vendored
Normal file
3
packages/bolt/worker-configuration.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
interface Env {
|
||||
ANTHROPIC_API_KEY: string;
|
||||
}
|
5
packages/bolt/wrangler.toml
Normal file
5
packages/bolt/wrangler.toml
Normal file
@ -0,0 +1,5 @@
|
||||
#:schema node_modules/wrangler/config-schema.json
|
||||
name = "bolt"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
compatibility_date = "2024-07-01"
|
||||
pages_build_output_dir = "./build/client"
|
9794
pnpm-lock.yaml
Normal file
9794
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- 'packages/*'
|
Loading…
Reference in New Issue
Block a user