feat: initial commit
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user