From 20bb97c0c5aca08d00bf0035021695c33b9c33f4 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Fri, 14 Feb 2025 14:27:20 -0800 Subject: [PATCH] Fix assorted problems (#27) --- app/components/chat/Chat.client.tsx | 6 +- app/lib/.server/llm/chat-anthropic.ts | 95 +++++++++++++++++++++------ app/lib/persistence/useChatHistory.ts | 4 +- app/lib/replay/Recording.ts | 14 ++-- 4 files changed, 90 insertions(+), 29 deletions(-) diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 9eb9ffd9..4af7b094 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -424,8 +424,10 @@ export const ChatImpl = memo( // The project contents are associated with the last message present when // the user message is added. const lastMessage = messages[messages.length - 1]; - const { contentBase64 } = await workbenchStore.generateZipBase64(); - saveProjectContents(lastMessage.id, { content: contentBase64 }); + if (lastMessage) { + const { contentBase64 } = await workbenchStore.generateZipBase64(); + saveProjectContents(lastMessage.id, { content: contentBase64 }); + } }; const onRewind = async (messageId: string, contents: string) => { diff --git a/app/lib/.server/llm/chat-anthropic.ts b/app/lib/.server/llm/chat-anthropic.ts index c7a75357..6d90ae62 100644 --- a/app/lib/.server/llm/chat-anthropic.ts +++ b/app/lib/.server/llm/chat-anthropic.ts @@ -113,7 +113,6 @@ async function restorePartialFile( existingContent: string, newContent: string, apiKey: string, - mainResponseText: string, responseDescription: string ) { const systemPrompt = ` @@ -171,24 +170,20 @@ ${responseDescription} const closeTag = restoreCall.responseText.indexOf(CloseTag); if (openTag === -1 || closeTag === -1) { - console.error("Invalid restored content"); - return { restoreCall, newResponseText: mainResponseText }; + console.error("Invalid restored content", restoreCall.responseText); + return { restoreCall, restoredContent: newContent }; } const restoredContent = restoreCall.responseText.substring(openTag + OpenTag.length, closeTag); - const newContentIndex = mainResponseText.indexOf(newContent); - if (newContentIndex === -1) { - console.error("New content not found in response"); - return { restoreCall, newResponseText: mainResponseText }; + // Sometimes the model ignores its instructions and doesn't return the content if it hasn't + // made any modifications. In this case we use the unmodified new content. + if (restoredContent.length < existingContent.length && restoredContent.length < newContent.length) { + console.error("Restored content too short", restoredContent); + return { restoreCall, restoredContent: newContent }; } - const newResponseText = - mainResponseText.substring(0, newContentIndex) + - restoredContent + - mainResponseText.substring(newContentIndex + newContent.length); - - return { restoreCall, newResponseText }; + return { restoreCall, restoredContent }; } // Return the english description in a model response, skipping over any artifacts. @@ -214,12 +209,67 @@ function getMessageDescription(responseText: string): string { return responseText; } +async function getLatestPackageVersion(packageName: string) { + try { + const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`); + const data = await response.json() as any; + if (typeof data.version == "string") { + return data.version; + } + } catch (e) { + console.error("Error getting latest package version", packageName, e); + } + return undefined; +} + +function ignorePackageUpgrade(packageName: string) { + // Don't upgrade react, our support for react 19 isn't complete yet. + return packageName.startsWith("react"); +} + +// Upgrade dependencies in package.json to the latest version, instead of the random +// and sometimes ancient versions that the AI picks. +async function upgradePackageJSON(content: string) { + try { + const packageJSON = JSON.parse(content); + for (const key of Object.keys(packageJSON.dependencies)) { + if (!ignorePackageUpgrade(key)) { + const version = await getLatestPackageVersion(key); + if (version) { + packageJSON.dependencies[key] = version; + } + } + } + return JSON.stringify(packageJSON, null, 2); + } catch (e) { + console.error("Error upgrading package.json", e); + return content; + } +} + +function replaceFileContents(responseText: string, oldContent: string, newContent: string) { + let contentIndex = responseText.indexOf(oldContent); + + if (contentIndex === -1) { + // The old content may have a trailing newline which wasn't originally present in the response. + oldContent = oldContent.trim(); + contentIndex = responseText.indexOf(oldContent); + + console.error("Old content not found in response", JSON.stringify({ responseText, oldContent })); + return responseText; + } + + return responseText.substring(0, contentIndex) + + newContent + + responseText.substring(contentIndex + oldContent.length); +} + interface FileContents { filePath: string; content: string; } -async function restorePartialFiles(files: FileMap, apiKey: string, responseText: string) { +async function fixupResponseFiles(files: FileMap, apiKey: string, responseText: string) { const fileContents: FileContents[] = []; const messageParser = new StreamingMessageParser({ @@ -240,20 +290,23 @@ async function restorePartialFiles(files: FileMap, apiKey: string, responseText: const responseDescription = getMessageDescription(responseText); const restoreCalls: AnthropicCall[] = []; - for (const file of fileContents) { - const existingContent = getFileContents(files, file.filePath); - const newContent = file.content; + for (const { filePath, content: newContent } of fileContents) { + const existingContent = getFileContents(files, filePath); if (shouldRestorePartialFile(existingContent, newContent)) { - const { restoreCall, newResponseText } = await restorePartialFile( + const { restoreCall, restoredContent } = await restorePartialFile( existingContent, newContent, apiKey, - responseText, responseDescription ); restoreCalls.push(restoreCall); - responseText = newResponseText; + responseText = replaceFileContents(responseText, newContent, restoredContent); + } + + if (filePath.includes("package.json")) { + const newPackageJSON = await upgradePackageJSON(newContent); + responseText = replaceFileContents(responseText, newContent, newPackageJSON); } } @@ -274,7 +327,7 @@ export async function chatAnthropic(chatController: ChatStreamController, files: const mainCall = await callAnthropic(apiKey, systemPrompt, messageParams); - const { responseText, restoreCalls } = await restorePartialFiles(files, apiKey, mainCall.responseText); + const { responseText, restoreCalls } = await fixupResponseFiles(files, apiKey, mainCall.responseText); chatController.writeText(responseText); diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index 92279746..2b296e6c 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -37,7 +37,7 @@ export function useChatHistory() { const [searchParams] = useSearchParams(); const [initialMessages, setInitialMessages] = useState([]); - const [ready, setReady] = useState(false); + const [ready, setReady] = useState(!mixedId && !problemId); const [urlId, setUrlId] = useState(); const importChat = async (description: string, messages: Message[]) => { @@ -100,7 +100,7 @@ export function useChatHistory() { }, []); return { - ready: ready || (!mixedId && !problemId), + ready, initialMessages, storeMessageHistory: async (messages: Message[]) => { if (!db || messages.length === 0) { diff --git a/app/lib/replay/Recording.ts b/app/lib/replay/Recording.ts index 9d535ecd..9b80082c 100644 --- a/app/lib/replay/Recording.ts +++ b/app/lib/replay/Recording.ts @@ -37,7 +37,9 @@ function sendIframeRequest( iframe: HTMLIFrameElement, request: Extract, ) { - assert(iframe.contentWindow); + if (!iframe.contentWindow) { + return undefined; + } const target = iframe.contentWindow; const requestId = ++lastRequestId; @@ -56,10 +58,12 @@ function sendIframeRequest( }); } -let gMessageCount = 0; - export async function getIFrameSimulationData(iframe: HTMLIFrameElement): Promise { const buffer = await sendIframeRequest(iframe, { request: 'recording-data' }); + if (!buffer) { + return []; + } + const decoder = new TextDecoder(); const jsonString = decoder.decode(new Uint8Array(buffer)); @@ -75,7 +79,9 @@ export interface MouseData { } export async function getMouseData(iframe: HTMLIFrameElement, position: { x: number; y: number }): Promise { - return sendIframeRequest(iframe, { request: 'mouse-data', payload: position }); + const mouseData = await sendIframeRequest(iframe, { request: 'mouse-data', payload: position }); + assert(mouseData, "Expected to have mouse data"); + return mouseData; } // Add handlers to the current iframe's window.