From 2cb3f09947dfc69e9557c7fe896e892b85e737e0 Mon Sep 17 00:00:00 2001 From: Dominic Elm Date: Thu, 25 Jul 2024 17:28:23 +0200 Subject: [PATCH] feat: submit file changes to the llm (#11) --- .../bolt/app/components/chat/BaseChat.tsx | 8 +- .../bolt/app/components/chat/Chat.client.tsx | 45 +++++++- .../app/components/chat/SendButton.client.tsx | 4 +- .../bolt/app/components/chat/UserMessage.tsx | 7 +- .../editor/codemirror/CodeMirrorEditor.tsx | 11 +- .../app/components/ui/PanelHeaderButton.tsx | 2 +- packages/bolt/app/lib/.server/llm/prompts.ts | 74 +++++++++--- .../bolt/app/lib/runtime/action-runner.ts | 9 +- packages/bolt/app/lib/stores/editor.ts | 4 +- packages/bolt/app/lib/stores/files.ts | 83 +++++++++++++- packages/bolt/app/lib/stores/workbench.ts | 41 +++++-- packages/bolt/app/routes/api.chat.ts | 1 + packages/bolt/app/utils/constants.ts | 1 + packages/bolt/app/utils/diff.ts | 108 ++++++++++++++++++ packages/bolt/package.json | 3 + packages/bolt/types/istextorbinary.d.ts | 15 +++ packages/bolt/vite.config.ts | 2 +- pnpm-lock.yaml | 54 +++++++++ 18 files changed, 415 insertions(+), 57 deletions(-) create mode 100644 packages/bolt/app/utils/diff.ts create mode 100644 packages/bolt/types/istextorbinary.d.ts diff --git a/packages/bolt/app/components/chat/BaseChat.tsx b/packages/bolt/app/components/chat/BaseChat.tsx index a1f39f75..6e79dee1 100644 --- a/packages/bolt/app/components/chat/BaseChat.tsx +++ b/packages/bolt/app/components/chat/BaseChat.tsx @@ -18,7 +18,7 @@ interface BaseChatProps { promptEnhanced?: boolean; input?: string; handleStop?: () => void; - sendMessage?: () => void; + sendMessage?: (event: React.UIEvent) => void; handleInputChange?: (event: React.ChangeEvent) => void; enhancePrompt?: () => void; } @@ -103,7 +103,7 @@ export const BaseChat = React.forwardRef( event.preventDefault(); - sendMessage?.(); + sendMessage?.(event); } }} value={input} @@ -122,13 +122,13 @@ export const BaseChat = React.forwardRef( 0 || isStreaming} isStreaming={isStreaming} - onClick={() => { + onClick={(event) => { if (isStreaming) { handleStop?.(); return; } - sendMessage?.(); + sendMessage?.(event); }} /> )} diff --git a/packages/bolt/app/components/chat/Chat.client.tsx b/packages/bolt/app/components/chat/Chat.client.tsx index d4ec41f1..a12128fa 100644 --- a/packages/bolt/app/components/chat/Chat.client.tsx +++ b/packages/bolt/app/components/chat/Chat.client.tsx @@ -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(); diff --git a/packages/bolt/app/components/chat/SendButton.client.tsx b/packages/bolt/app/components/chat/SendButton.client.tsx index cf77eb89..10b8a863 100644 --- a/packages/bolt/app/components/chat/SendButton.client.tsx +++ b/packages/bolt/app/components/chat/SendButton.client.tsx @@ -3,7 +3,7 @@ import { AnimatePresence, cubicBezier, motion } from 'framer-motion'; interface SendButtonProps { show: boolean; isStreaming?: boolean; - onClick?: VoidFunction; + onClick?: (event: React.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); }} >
diff --git a/packages/bolt/app/components/chat/UserMessage.tsx b/packages/bolt/app/components/chat/UserMessage.tsx index 5b7863fa..e1c0d3f0 100644 --- a/packages/bolt/app/components/chat/UserMessage.tsx +++ b/packages/bolt/app/components/chat/UserMessage.tsx @@ -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 (
- {content} + {sanitizeUserMessage(content)}
); } + +function sanitizeUserMessage(content: string) { + return content.replace(modificationsRegex, '').trim(); +} diff --git a/packages/bolt/app/components/editor/codemirror/CodeMirrorEditor.tsx b/packages/bolt/app/components/editor/codemirror/CodeMirrorEditor.tsx index ff072198..b57ba3a4 100644 --- a/packages/bolt/app/components/editor/codemirror/CodeMirrorEditor.tsx +++ b/packages/bolt/app/components/editor/codemirror/CodeMirrorEditor.tsx @@ -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 (
- {isBinaryFile && } + {doc?.isBinary && }
); @@ -343,7 +342,7 @@ function setEditorDocument( } view.dispatch({ - effects: [editableStateEffect.of(editable)], + effects: [editableStateEffect.of(editable && !doc.isBinary)], }); getLanguage(doc.filePath).then((languageSupport) => { diff --git a/packages/bolt/app/components/ui/PanelHeaderButton.tsx b/packages/bolt/app/components/ui/PanelHeaderButton.tsx index df254144..73af1e56 100644 --- a/packages/bolt/app/components/ui/PanelHeaderButton.tsx +++ b/packages/bolt/app/components/ui/PanelHeaderButton.tsx @@ -14,7 +14,7 @@ export const PanelHeaderButton = memo( return (