mirror of
https://github.com/coleam00/bolt.new-any-llm
synced 2024-12-27 22:33:03 +00:00
feat: submit file changes to the llm (#11)
This commit is contained in:
parent
a5ed695cb3
commit
2cb3f09947
@ -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;
|
||||
}
|
||||
@ -103,7 +103,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
sendMessage?.();
|
||||
sendMessage?.(event);
|
||||
}
|
||||
}}
|
||||
value={input}
|
||||
@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -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';
|
||||
@ -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);
|
||||
@ -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();
|
||||
|
@ -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);
|
||||
@ -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">
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { modificationsRegex } from '~/utils/diff';
|
||||
import { Markdown } from './Markdown';
|
||||
|
||||
interface UserMessageProps {
|
||||
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
@ -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.
|
||||
@ -198,7 +197,7 @@ export const CodeMirrorEditor = memo(
|
||||
return;
|
||||
}
|
||||
|
||||
if (doc.value instanceof Uint8Array) {
|
||||
if (doc.isBinary) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
@ -343,7 +342,7 @@ function setEditorDocument(
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
effects: [editableStateEffect.of(editable)],
|
||||
effects: [editableStateEffect.of(editable && !doc.isBinary)],
|
||||
});
|
||||
|
||||
getLanguage(doc.filePath).then((languageSupport) => {
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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) => `
|
||||
@ -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:
|
||||
|
||||
@ -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. The current working directory is \`${cwd}\`.
|
||||
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.
|
||||
|
||||
3. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
|
||||
3. The current working directory is \`${cwd}\`.
|
||||
|
||||
4. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
|
||||
4. Wrap the content in opening and closing \`<boltArtifact>\` tags. These tags contain more specific \`<boltAction>\` elements.
|
||||
|
||||
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.
|
||||
5. Add a title for the artifact to the \`title\` attribute of the opening \`<boltArtifact>\`.
|
||||
|
||||
6. Use \`<boltAction>\` tags to define specific actions to perform.
|
||||
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.
|
||||
|
||||
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:
|
||||
7. Use \`<boltAction>\` tags to define specific actions to perform.
|
||||
|
||||
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.
|
||||
|
||||
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
@ -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];
|
||||
|
||||
|
@ -1,17 +1,22 @@
|
||||
import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';
|
||||
import { getEncoding } from 'istextorbinary';
|
||||
import { map, type MapStore } from 'nanostores';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import * as nodePath from 'node:path';
|
||||
import { bufferWatchEvents } from '~/utils/buffer';
|
||||
import { WORK_DIR } from '~/utils/constants';
|
||||
import { computeFileModifications } from '~/utils/diff';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { unreachable } from '~/utils/unreachable';
|
||||
|
||||
const logger = createScopedLogger('FilesStore');
|
||||
|
||||
const textDecoder = new TextDecoder('utf8', { fatal: true });
|
||||
const utf8TextDecoder = new TextDecoder('utf8', { fatal: true });
|
||||
|
||||
export interface File {
|
||||
type: 'file';
|
||||
content: string | Uint8Array;
|
||||
content: string;
|
||||
isBinary: boolean;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
@ -30,6 +35,16 @@ export class FilesStore {
|
||||
*/
|
||||
#size = 0;
|
||||
|
||||
/**
|
||||
* @note Keeps track all modified files with their original content since the last user message.
|
||||
* Needs to be reset when the user sends another message and all changes have to be submitted
|
||||
* for the model to be aware of the changes.
|
||||
*/
|
||||
#modifiedFiles: Map<string, string> = import.meta.hot?.data.modifiedFiles ?? new Map();
|
||||
|
||||
/**
|
||||
* Map of files that matches the state of WebContainer.
|
||||
*/
|
||||
files: MapStore<FileMap> = import.meta.hot?.data.files ?? map({});
|
||||
|
||||
get filesCount() {
|
||||
@ -41,6 +56,7 @@ export class FilesStore {
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.files = this.files;
|
||||
import.meta.hot.data.modifiedFiles = this.#modifiedFiles;
|
||||
}
|
||||
|
||||
this.#init();
|
||||
@ -56,7 +72,15 @@ export class FilesStore {
|
||||
return dirent;
|
||||
}
|
||||
|
||||
async saveFile(filePath: string, content: string | Uint8Array) {
|
||||
getFileModifications() {
|
||||
return computeFileModifications(this.files.get(), this.#modifiedFiles);
|
||||
}
|
||||
|
||||
resetFileModifications() {
|
||||
this.#modifiedFiles.clear();
|
||||
}
|
||||
|
||||
async saveFile(filePath: string, content: string) {
|
||||
const webcontainer = await this.#webcontainer;
|
||||
|
||||
try {
|
||||
@ -66,9 +90,20 @@ export class FilesStore {
|
||||
throw new Error(`EINVAL: invalid file path, write '${relativePath}'`);
|
||||
}
|
||||
|
||||
const oldContent = this.getFile(filePath)?.content;
|
||||
|
||||
if (!oldContent) {
|
||||
unreachable('Expected content to be defined');
|
||||
}
|
||||
|
||||
await webcontainer.fs.writeFile(relativePath, content);
|
||||
|
||||
this.files.setKey(filePath, { type: 'file', content });
|
||||
if (!this.#modifiedFiles.has(filePath)) {
|
||||
this.#modifiedFiles.set(filePath, oldContent);
|
||||
}
|
||||
|
||||
// we immediately update the file and don't rely on the `change` event coming from the watcher
|
||||
this.files.setKey(filePath, { type: 'file', content, isBinary: false });
|
||||
|
||||
logger.info('File updated');
|
||||
} catch (error) {
|
||||
@ -117,7 +152,21 @@ export class FilesStore {
|
||||
this.#size++;
|
||||
}
|
||||
|
||||
this.files.setKey(sanitizedPath, { type: 'file', content: this.#decodeFileContent(buffer) });
|
||||
let content = '';
|
||||
|
||||
/**
|
||||
* @note This check is purely for the editor. The way we detect this is not
|
||||
* bullet-proof and it's a best guess so there might be false-positives.
|
||||
* The reason we do this is because we don't want to display binary files
|
||||
* in the editor nor allow to edit them.
|
||||
*/
|
||||
const isBinary = isBinaryFile(buffer);
|
||||
|
||||
if (!isBinary) {
|
||||
content = this.#decodeFileContent(buffer);
|
||||
}
|
||||
|
||||
this.files.setKey(sanitizedPath, { type: 'file', content, isBinary });
|
||||
|
||||
break;
|
||||
}
|
||||
@ -140,10 +189,32 @@ export class FilesStore {
|
||||
}
|
||||
|
||||
try {
|
||||
return textDecoder.decode(buffer);
|
||||
return utf8TextDecoder.decode(buffer);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isBinaryFile(buffer: Uint8Array | undefined) {
|
||||
if (buffer === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getEncoding(convertToBuffer(buffer), { chunkLength: 100 }) === 'binary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a `Uint8Array` into a Node.js `Buffer` by copying the prototype.
|
||||
* The goal is to avoid expensive copies. It does create a new typed array
|
||||
* but that's generally cheap as long as it uses the same underlying
|
||||
* array buffer.
|
||||
*/
|
||||
function convertToBuffer(view: Uint8Array): Buffer {
|
||||
const buffer = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
||||
|
||||
Object.setPrototypeOf(buffer, Buffer.prototype);
|
||||
|
||||
return buffer as Buffer;
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ export class WorkbenchStore {
|
||||
this.showWorkbench.set(show);
|
||||
}
|
||||
|
||||
setCurrentDocumentContent(newContent: string | Uint8Array) {
|
||||
setCurrentDocumentContent(newContent: string) {
|
||||
const filePath = this.currentDocument.get()?.filePath;
|
||||
|
||||
if (!filePath) {
|
||||
@ -119,6 +119,22 @@ export class WorkbenchStore {
|
||||
this.#editorStore.setSelectedFile(filePath);
|
||||
}
|
||||
|
||||
async saveFile(filePath: string) {
|
||||
const documents = this.#editorStore.documents.get();
|
||||
const document = documents[filePath];
|
||||
|
||||
if (document === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.#filesStore.saveFile(filePath, document.value);
|
||||
|
||||
const newUnsavedFiles = new Set(this.unsavedFiles.get());
|
||||
newUnsavedFiles.delete(filePath);
|
||||
|
||||
this.unsavedFiles.set(newUnsavedFiles);
|
||||
}
|
||||
|
||||
async saveCurrentDocument() {
|
||||
const currentDocument = this.currentDocument.get();
|
||||
|
||||
@ -126,14 +142,7 @@ export class WorkbenchStore {
|
||||
return;
|
||||
}
|
||||
|
||||
const { filePath } = currentDocument;
|
||||
|
||||
await this.#filesStore.saveFile(filePath, currentDocument.value);
|
||||
|
||||
const newUnsavedFiles = new Set(this.unsavedFiles.get());
|
||||
newUnsavedFiles.delete(filePath);
|
||||
|
||||
this.unsavedFiles.set(newUnsavedFiles);
|
||||
await this.saveFile(currentDocument.filePath);
|
||||
}
|
||||
|
||||
resetCurrentDocument() {
|
||||
@ -153,6 +162,20 @@ export class WorkbenchStore {
|
||||
this.setCurrentDocumentContent(file.content);
|
||||
}
|
||||
|
||||
async saveAllFiles() {
|
||||
for (const filePath of this.unsavedFiles.get()) {
|
||||
await this.saveFile(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
getFileModifcations() {
|
||||
return this.#filesStore.getFileModifications();
|
||||
}
|
||||
|
||||
resetAllFileModifications() {
|
||||
this.#filesStore.resetFileModifications();
|
||||
}
|
||||
|
||||
abortAllActions() {
|
||||
// TODO: what do we wanna do and how do we wanna recover from this?
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import SwitchableStream from '~/lib/.server/llm/switchable-stream';
|
||||
|
||||
export async function action({ context, request }: ActionFunctionArgs) {
|
||||
const { messages } = await request.json<{ messages: Messages }>();
|
||||
|
||||
const stream = new SwitchableStream();
|
||||
|
||||
try {
|
||||
|
@ -1,2 +1,3 @@
|
||||
export const WORK_DIR_NAME = 'project';
|
||||
export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
|
||||
export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
|
||||
|
108
packages/bolt/app/utils/diff.ts
Normal file
108
packages/bolt/app/utils/diff.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { createTwoFilesPatch } from 'diff';
|
||||
import type { FileMap } from '~/lib/stores/files';
|
||||
import { MODIFICATIONS_TAG_NAME } from './constants';
|
||||
|
||||
export const modificationsRegex = new RegExp(
|
||||
`^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
|
||||
'g',
|
||||
);
|
||||
|
||||
interface ModifiedFile {
|
||||
type: 'diff' | 'file';
|
||||
content: string;
|
||||
}
|
||||
|
||||
type FileModifications = Record<string, ModifiedFile>;
|
||||
|
||||
export function computeFileModifications(files: FileMap, modifiedFiles: Map<string, string>) {
|
||||
const modifications: FileModifications = {};
|
||||
|
||||
let hasModifiedFiles = false;
|
||||
|
||||
for (const [filePath, originalContent] of modifiedFiles) {
|
||||
const file = files[filePath];
|
||||
|
||||
if (file?.type !== 'file') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const unifiedDiff = diffFiles(filePath, originalContent, file.content);
|
||||
|
||||
if (!unifiedDiff) {
|
||||
// files are identical
|
||||
continue;
|
||||
}
|
||||
|
||||
hasModifiedFiles = true;
|
||||
|
||||
if (unifiedDiff.length > file.content.length) {
|
||||
// if there are lots of changes we simply grab the current file content since it's smaller than the diff
|
||||
modifications[filePath] = { type: 'file', content: file.content };
|
||||
} else {
|
||||
// otherwise we use the diff since it's smaller
|
||||
modifications[filePath] = { type: 'diff', content: unifiedDiff };
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasModifiedFiles) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return modifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a diff in the unified format. The only difference is that the header is omitted
|
||||
* because it will always assume that you're comparing two versions of the same file and
|
||||
* it allows us to avoid the extra characters we send back to the llm.
|
||||
*
|
||||
* @see https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html
|
||||
*/
|
||||
export function diffFiles(fileName: string, oldFileContent: string, newFileContent: string) {
|
||||
let unifiedDiff = createTwoFilesPatch(fileName, fileName, oldFileContent, newFileContent);
|
||||
|
||||
const patchHeaderEnd = `--- ${fileName}\n+++ ${fileName}\n`;
|
||||
const headerEndIndex = unifiedDiff.indexOf(patchHeaderEnd);
|
||||
|
||||
if (headerEndIndex >= 0) {
|
||||
unifiedDiff = unifiedDiff.slice(headerEndIndex + patchHeaderEnd.length);
|
||||
}
|
||||
|
||||
if (unifiedDiff === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return unifiedDiff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the unified diff to HTML.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```html
|
||||
* <bolt_file_modifications>
|
||||
* <diff path="/home/project/index.js">
|
||||
* - console.log('Hello, World!');
|
||||
* + console.log('Hello, Bolt!');
|
||||
* </diff>
|
||||
* </bolt_file_modifications>
|
||||
* ```
|
||||
*/
|
||||
export function fileModificationsToHTML(modifications: FileModifications) {
|
||||
const entries = Object.entries(modifications);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: string[] = [`<${MODIFICATIONS_TAG_NAME}>`];
|
||||
|
||||
for (const [filePath, { type, content }] of entries) {
|
||||
result.push(`<${type} path=${JSON.stringify(filePath)}>`, content, `</${type}>`);
|
||||
}
|
||||
|
||||
result.push(`</${MODIFICATIONS_TAG_NAME}>`);
|
||||
|
||||
return result.join('\n');
|
||||
}
|
@ -43,8 +43,10 @@
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"ai": "^3.2.27",
|
||||
"diff": "^5.2.0",
|
||||
"framer-motion": "^11.2.12",
|
||||
"isbot": "^4.1.0",
|
||||
"istextorbinary": "^9.5.0",
|
||||
"nanostores": "^0.10.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -59,6 +61,7 @@
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"@remix-run/dev": "^2.10.0",
|
||||
"@types/diff": "^5.2.1",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"fast-glob": "^3.3.2",
|
||||
|
15
packages/bolt/types/istextorbinary.d.ts
vendored
Normal file
15
packages/bolt/types/istextorbinary.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @note For some reason the types aren't picked up from node_modules so I declared the module here
|
||||
* with only the function that we use.
|
||||
*/
|
||||
declare module 'istextorbinary' {
|
||||
export interface EncodingOpts {
|
||||
/** Defaults to 24 */
|
||||
chunkLength?: number;
|
||||
|
||||
/** If not provided, will check the start, beginning, and end */
|
||||
chunkBegin?: number;
|
||||
}
|
||||
|
||||
export function getEncoding(buffer: Buffer | null, opts?: EncodingOpts): 'utf8' | 'binary' | null;
|
||||
}
|
@ -12,7 +12,7 @@ export default defineConfig((config) => {
|
||||
},
|
||||
plugins: [
|
||||
nodePolyfills({
|
||||
include: ['path'],
|
||||
include: ['path', 'buffer'],
|
||||
}),
|
||||
config.mode !== 'test' && remixCloudflareDevProxy(),
|
||||
remixVitePlugin({
|
||||
|
@ -116,12 +116,18 @@ importers:
|
||||
ai:
|
||||
specifier: ^3.2.27
|
||||
version: 3.2.27(react@18.3.1)(svelte@4.2.18)(vue@3.4.30(typescript@5.5.2))(zod@3.23.8)
|
||||
diff:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
framer-motion:
|
||||
specifier: ^11.2.12
|
||||
version: 11.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
isbot:
|
||||
specifier: ^4.1.0
|
||||
version: 4.4.0
|
||||
istextorbinary:
|
||||
specifier: ^9.5.0
|
||||
version: 9.5.0
|
||||
nanostores:
|
||||
specifier: ^0.10.3
|
||||
version: 0.10.3
|
||||
@ -159,6 +165,9 @@ importers:
|
||||
'@remix-run/dev':
|
||||
specifier: ^2.10.0
|
||||
version: 2.10.0(@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2))(@remix-run/serve@2.10.0(typescript@5.5.2))(@types/node@20.14.9)(sass@1.77.6)(typescript@5.5.2)(vite@5.3.1(@types/node@20.14.9)(sass@1.77.6))(wrangler@3.63.2(@cloudflare/workers-types@4.20240620.0))
|
||||
'@types/diff':
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
'@types/react':
|
||||
specifier: ^18.2.20
|
||||
version: 18.3.3
|
||||
@ -1445,6 +1454,9 @@ packages:
|
||||
'@types/diff-match-patch@1.0.36':
|
||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||
|
||||
'@types/diff@5.2.1':
|
||||
resolution: {integrity: sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==}
|
||||
|
||||
'@types/eslint@8.56.10':
|
||||
resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
|
||||
|
||||
@ -1865,6 +1877,10 @@ packages:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
binaryextensions@6.11.0:
|
||||
resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
@ -2328,6 +2344,10 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
editions@6.21.0:
|
||||
resolution: {integrity: sha512-ofkXJtn7z0urokN62DI3SBo/5xAtF0rR7tn+S/bSYV79Ka8pTajIIl+fFQ1q88DQEImymmo97M4azY3WX/nUdg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
@ -3040,6 +3060,10 @@ packages:
|
||||
resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istextorbinary@9.5.0:
|
||||
resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
jackspeak@3.4.0:
|
||||
resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==}
|
||||
engines: {node: '>=14'}
|
||||
@ -4522,6 +4546,10 @@ packages:
|
||||
text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
textextensions@6.11.0:
|
||||
resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
through2@2.0.5:
|
||||
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
||||
|
||||
@ -4766,6 +4794,10 @@ packages:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
version-range@4.14.0:
|
||||
resolution: {integrity: sha512-gjb0ARm9qlcBAonU4zPwkl9ecKkas+tC2CGwFfptTCWWIVTWY1YUbT2zZKsOAF1jR/tNxxyLwwG0cb42XlYcTg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
vfile-location@5.0.2:
|
||||
resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==}
|
||||
|
||||
@ -6344,6 +6376,8 @@ snapshots:
|
||||
|
||||
'@types/diff-match-patch@1.0.36': {}
|
||||
|
||||
'@types/diff@5.2.1': {}
|
||||
|
||||
'@types/eslint@8.56.10':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
@ -6933,6 +6967,10 @@ snapshots:
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
binaryextensions@6.11.0:
|
||||
dependencies:
|
||||
editions: 6.21.0
|
||||
|
||||
bl@4.1.0:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
@ -7435,6 +7473,10 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
editions@6.21.0:
|
||||
dependencies:
|
||||
version-range: 4.14.0
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
electron-to-chromium@1.4.812: {}
|
||||
@ -8284,6 +8326,12 @@ snapshots:
|
||||
|
||||
isomorphic-timers-promises@1.0.1: {}
|
||||
|
||||
istextorbinary@9.5.0:
|
||||
dependencies:
|
||||
binaryextensions: 6.11.0
|
||||
editions: 6.21.0
|
||||
textextensions: 6.11.0
|
||||
|
||||
jackspeak@3.4.0:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
@ -10228,6 +10276,10 @@ snapshots:
|
||||
|
||||
text-table@0.2.0: {}
|
||||
|
||||
textextensions@6.11.0:
|
||||
dependencies:
|
||||
editions: 6.21.0
|
||||
|
||||
through2@2.0.5:
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
@ -10502,6 +10554,8 @@ snapshots:
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
version-range@4.14.0: {}
|
||||
|
||||
vfile-location@5.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.2
|
||||
|
Loading…
Reference in New Issue
Block a user