feat: submit file changes to the llm (#11)

This commit is contained in:
Dominic Elm 2024-07-25 17:28:23 +02:00 committed by GitHub
parent a5ed695cb3
commit 2cb3f09947
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 415 additions and 57 deletions

View File

@ -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);
}}
/>
)}

View File

@ -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();

View File

@ -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">

View File

@ -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();
}

View File

@ -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) => {

View File

@ -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,
},

View File

@ -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.

View File

@ -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

View File

@ -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];

View File

@ -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;
}

View File

@ -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?
}

View File

@ -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 {

View File

@ -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';

View 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');
}

View File

@ -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
View 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;
}

View File

@ -12,7 +12,7 @@ export default defineConfig((config) => {
},
plugins: [
nodePolyfills({
include: ['path'],
include: ['path', 'buffer'],
}),
config.mode !== 'test' && remixCloudflareDevProxy(),
remixVitePlugin({

View File

@ -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