Skip to content

Commit

Permalink
feat: submit file changes to the llm (stackblitz#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
d3lm authored Jul 25, 2024
1 parent a5ed695 commit 2cb3f09
Show file tree
Hide file tree
Showing 18 changed files with 413 additions and 55 deletions.
8 changes: 4 additions & 4 deletions packages/bolt/app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface BaseChatProps {
promptEnhanced?: boolean;
input?: string;
handleStop?: () => void;
sendMessage?: () => void;
sendMessage?: (event: React.UIEvent) => void;
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
enhancePrompt?: () => void;
}
Expand Down Expand Up @@ -103,7 +103,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(

event.preventDefault();

sendMessage?.();
sendMessage?.(event);
}
}}
value={input}
Expand All @@ -122,13 +122,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
<SendButton
show={input.length > 0 || isStreaming}
isStreaming={isStreaming}
onClick={() => {
onClick={(event) => {
if (isStreaming) {
handleStop?.();
return;
}

sendMessage?.();
sendMessage?.(event);
}}
/>
)}
Expand Down
45 changes: 40 additions & 5 deletions packages/bolt/app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import type { Message } from 'ai';
import { useChat } from 'ai/react';
import { useAnimate } from 'framer-motion';
import { useEffect, useRef, useState } from 'react';
import { toast, ToastContainer, cssTransition } from 'react-toastify';
import { cssTransition, toast, ToastContainer } from 'react-toastify';
import { useMessageParser, usePromptEnhancer, useSnapScroll } from '~/lib/hooks';
import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench';
import { fileModificationsToHTML } from '~/utils/diff';
import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat';
Expand Down Expand Up @@ -41,7 +42,7 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {

const [animationScope, animate] = useAnimate();

const { messages, isLoading, input, handleInputChange, setInput, handleSubmit, stop } = useChat({
const { messages, isLoading, input, handleInputChange, setInput, handleSubmit, stop, append } = useChat({
api: '/api/chat',
onError: (error) => {
logger.error(error);
Expand Down Expand Up @@ -100,15 +101,49 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
setChatStarted(true);
};

const sendMessage = () => {
if (input.length === 0) {
const sendMessage = async (event: React.UIEvent) => {
if (input.length === 0 || isLoading) {
return;
}

/**
* @note (delm) Usually saving files shouldn't take long but it may take longer if there
* many unsaved files. In that case we need to block user input and show an indicator
* of some kind so the user is aware that something is happening. But I consider the
* happy case to be no unsaved files and I would expect users to save their changes
* before they send another message.
*/
await workbenchStore.saveAllFiles();

const fileModifications = workbenchStore.getFileModifcations();

chatStore.setKey('aborted', false);

runAnimation();
handleSubmit();

if (fileModifications !== undefined) {
const diff = fileModificationsToHTML(fileModifications);

/**
* If we have file modifications we append a new user message manually since we have to prefix
* the user input with the file modifications and we don't want the new user input to appear
* in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to
* manually reset the input and we'd have to manually pass in file attachments. However, those
* aren't relevant here.
*/
append({ role: 'user', content: `${diff}\n\n${input}` });

setInput('');

/**
* After sending a new message we reset all modifications since the model
* should now be aware of all the changes.
*/
workbenchStore.resetAllFileModifications();
} else {
handleSubmit(event);
}

resetEnhancer();

textareaRef.current?.blur();
Expand Down
4 changes: 2 additions & 2 deletions packages/bolt/app/components/chat/SendButton.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AnimatePresence, cubicBezier, motion } from 'framer-motion';
interface SendButtonProps {
show: boolean;
isStreaming?: boolean;
onClick?: VoidFunction;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}

const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
Expand All @@ -20,7 +20,7 @@ export function SendButton({ show, isStreaming, onClick }: SendButtonProps) {
exit={{ opacity: 0, y: 10 }}
onClick={(event) => {
event.preventDefault();
onClick?.();
onClick?.(event);
}}
>
<div className="text-lg">
Expand Down
7 changes: 6 additions & 1 deletion packages/bolt/app/components/chat/UserMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { modificationsRegex } from '~/utils/diff';
import { Markdown } from './Markdown';

interface UserMessageProps {
Expand All @@ -7,7 +8,11 @@ interface UserMessageProps {
export function UserMessage({ content }: UserMessageProps) {
return (
<div className="overflow-hidden">
<Markdown>{content}</Markdown>
<Markdown>{sanitizeUserMessage(content)}</Markdown>
</div>
);
}

function sanitizeUserMessage(content: string) {
return content.replace(modificationsRegex, '').trim();
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import { getLanguage } from './languages';
const logger = createScopedLogger('CodeMirrorEditor');

export interface EditorDocument {
value: string | Uint8Array;
value: string;
isBinary: boolean;
filePath: string;
scroll?: ScrollPosition;
}
Expand Down Expand Up @@ -116,8 +117,6 @@ export const CodeMirrorEditor = memo(
const onChangeRef = useRef(onChange);
const onSaveRef = useRef(onSave);

const isBinaryFile = doc?.value instanceof Uint8Array;

/**
* This effect is used to avoid side effects directly in the render function
* and instead the refs are updated after each render.
Expand Down Expand Up @@ -198,7 +197,7 @@ export const CodeMirrorEditor = memo(
return;
}

if (doc.value instanceof Uint8Array) {
if (doc.isBinary) {
return;
}

Expand Down Expand Up @@ -230,7 +229,7 @@ export const CodeMirrorEditor = memo(

return (
<div className={classNames('relative h-full', className)}>
{isBinaryFile && <BinaryContent />}
{doc?.isBinary && <BinaryContent />}
<div className="h-full overflow-hidden" ref={containerRef} />
</div>
);
Expand Down Expand Up @@ -343,7 +342,7 @@ function setEditorDocument(
}

view.dispatch({
effects: [editableStateEffect.of(editable)],
effects: [editableStateEffect.of(editable && !doc.isBinary)],
});

getLanguage(doc.filePath).then((languageSupport) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/bolt/app/components/ui/PanelHeaderButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const PanelHeaderButton = memo(
return (
<button
className={classNames(
'flex items-center gap-1.5 px-1.5 rounded-lg py-0.5 bg-transparent hover:bg-white disabled:cursor-not-allowed',
'flex items-center gap-1.5 px-1.5 rounded-md py-0.5 bg-transparent hover:bg-white disabled:cursor-not-allowed',
{
[classNames('opacity-30', disabledClassName)]: disabled,
},
Expand Down
74 changes: 60 additions & 14 deletions packages/bolt/app/lib/.server/llm/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WORK_DIR } from '~/utils/constants';
import { MODIFICATIONS_TAG_NAME, WORK_DIR } from '~/utils/constants';
import { stripIndents } from '~/utils/stripIndent';

export const getSystemPrompt = (cwd: string = WORK_DIR) => `
Expand All @@ -20,6 +20,50 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
Use 2 spaces for code indentation
</code_formatting_info>
<diff_spec>
For user-made file modifications, a \`<${MODIFICATIONS_TAG_NAME}>\` section will appear at the start of the user message. It will contain either \`<diff>\` or \`<file>\` elements for each modified file:
- \`<diff path="/some/file/path.ext">\`: Contains GNU unified diff format changes
- \`<file path="/some/file/path.ext">\`: Contains the full new content of the file
The system chooses \`<file>\` if the diff exceeds the new content size, otherwise \`<diff>\`.
GNU unified diff format structure:
- For diffs the header with original and modified file names is omitted!
- Changed sections start with @@ -X,Y +A,B @@ where:
- X: Original file starting line
- Y: Original file line count
- A: Modified file starting line
- B: Modified file line count
- (-) lines: Removed from original
- (+) lines: Added in modified version
- Unmarked lines: Unchanged context
Example:
<${MODIFICATIONS_TAG_NAME}>
<diff path="/home/project/src/main.js">
@@ -2,7 +2,10 @@
return a + b;
}
-console.log('Hello, World!');
+console.log('Hello, Bolt!');
+
function greet() {
- return 'Greetings!';
+ return 'Greetings!!';
}
+
+console.log('The End');
</diff>
<file path="/home/project/package.json">
// full file content here
</file>
</${MODIFICATIONS_TAG_NAME}>
</diff_spec>
<artifact_info>
Bolt creates a SINGLE, comprehensive artifact for each project. The artifact contains all necessary steps and components, including:
Expand All @@ -28,19 +72,21 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
- Folders to create if necessary
<artifact_instructions>
1. Think BEFORE creating an artifact
1. Think BEFORE creating an artifact.
2. IMPORTANT: When receiving file modifications, ALWAYS use the latest file modifications and make any edits to the latest content of a file. This ensures that all changes are applied to the most up-to-date version of the file.
2. The current working directory is \`${cwd}\`.
3. The current working directory is \`${cwd}\`.
3. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
4. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
4. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
5. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
5. Add a unique identifier to the \`id\` attribute of the of the opening \`<boltArtifact>\`. For updates, reuse the prior identifier. The identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
6. Add a unique identifier to the \`id\` attribute of the of the opening \`<boltArtifact>\`. For updates, reuse the prior identifier. The identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
6. Use \`<boltAction>\` tags to define specific actions to perform.
7. Use \`<boltAction>\` tags to define specific actions to perform.
7. 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:
8. 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.
Expand All @@ -50,19 +96,19 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
- file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory.
8. 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.
9. 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.
9. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
10. 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!
10. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
11. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
11. 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!
12. 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!
12. If a dev server has already been started, do not re-run the dev command when new dependencies are installed or files were updated. Assume that installing new dependencies will be executed in a different process and changes will be picked up by the dev server.
13. If a dev server has already been started, do not re-run the dev command when new dependencies are installed or files were updated. Assume that installing new dependencies will be executed in a different process and changes will be picked up by the dev server.
13. ULTRA IMPORTANT: Use coding best practices and split functionality into smaller modules instead of putting everything in a single gigantic file. Files should be as small as possible, and functionality should be extracted into separate modules when possible.
14. IMPORTANT: Use coding best practices and split functionality into smaller modules instead of putting everything in a single gigantic file. Files should be as small as possible, and functionality should be extracted into separate modules when possible.
- Ensure code is clean, readable, and maintainable.
- Adhere to proper naming conventions and consistent formatting.
Expand Down
9 changes: 3 additions & 6 deletions packages/bolt/app/lib/runtime/action-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,17 @@ export class ActionRunner {
#webcontainer: Promise<WebContainer>;
#currentExecutionPromise: Promise<void> = Promise.resolve();

actions: ActionsMap = import.meta.hot?.data.actions ?? map({});
actions: ActionsMap = map({});

constructor(webcontainerPromise: Promise<WebContainer>) {
this.#webcontainer = webcontainerPromise;

if (import.meta.hot) {
import.meta.hot.data.actions = this.actions;
}
}

addAction(data: ActionCallbackData) {
const { actionId } = data;

const action = this.actions.get()[actionId];
const actions = this.actions.get();
const action = actions[actionId];

if (action) {
// action already added
Expand Down
4 changes: 2 additions & 2 deletions packages/bolt/app/lib/stores/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class EditorStore {
#filesStore: FilesStore;

selectedFile: SelectedFile = import.meta.hot?.data.selectedFile ?? atom<string | undefined>();
documents: MapStore<EditorDocuments> = import.meta.hot?.data.documents ?? map<EditorDocuments>({});
documents: MapStore<EditorDocuments> = import.meta.hot?.data.documents ?? map({});

currentDocument = computed([this.documents, this.selectedFile], (documents, selectedFile) => {
if (!selectedFile) {
Expand Down Expand Up @@ -74,7 +74,7 @@ export class EditorStore {
});
}

updateFile(filePath: string, newContent: string | Uint8Array) {
updateFile(filePath: string, newContent: string) {
const documents = this.documents.get();
const documentState = documents[filePath];

Expand Down
Loading

0 comments on commit 2cb3f09

Please sign in to comment.