Fix assorted problems (#27)

This commit is contained in:
Brian Hackett 2025-02-14 14:27:20 -08:00 committed by GitHub
parent b2efa3b56d
commit 20bb97c0c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 29 deletions

View File

@ -424,8 +424,10 @@ export const ChatImpl = memo(
// The project contents are associated with the last message present when // The project contents are associated with the last message present when
// the user message is added. // the user message is added.
const lastMessage = messages[messages.length - 1]; const lastMessage = messages[messages.length - 1];
const { contentBase64 } = await workbenchStore.generateZipBase64(); if (lastMessage) {
saveProjectContents(lastMessage.id, { content: contentBase64 }); const { contentBase64 } = await workbenchStore.generateZipBase64();
saveProjectContents(lastMessage.id, { content: contentBase64 });
}
}; };
const onRewind = async (messageId: string, contents: string) => { const onRewind = async (messageId: string, contents: string) => {

View File

@ -113,7 +113,6 @@ async function restorePartialFile(
existingContent: string, existingContent: string,
newContent: string, newContent: string,
apiKey: string, apiKey: string,
mainResponseText: string,
responseDescription: string responseDescription: string
) { ) {
const systemPrompt = ` const systemPrompt = `
@ -171,24 +170,20 @@ ${responseDescription}
const closeTag = restoreCall.responseText.indexOf(CloseTag); const closeTag = restoreCall.responseText.indexOf(CloseTag);
if (openTag === -1 || closeTag === -1) { if (openTag === -1 || closeTag === -1) {
console.error("Invalid restored content"); console.error("Invalid restored content", restoreCall.responseText);
return { restoreCall, newResponseText: mainResponseText }; return { restoreCall, restoredContent: newContent };
} }
const restoredContent = restoreCall.responseText.substring(openTag + OpenTag.length, closeTag); const restoredContent = restoreCall.responseText.substring(openTag + OpenTag.length, closeTag);
const newContentIndex = mainResponseText.indexOf(newContent);
if (newContentIndex === -1) { // Sometimes the model ignores its instructions and doesn't return the content if it hasn't
console.error("New content not found in response"); // made any modifications. In this case we use the unmodified new content.
return { restoreCall, newResponseText: mainResponseText }; if (restoredContent.length < existingContent.length && restoredContent.length < newContent.length) {
console.error("Restored content too short", restoredContent);
return { restoreCall, restoredContent: newContent };
} }
const newResponseText = return { restoreCall, restoredContent };
mainResponseText.substring(0, newContentIndex) +
restoredContent +
mainResponseText.substring(newContentIndex + newContent.length);
return { restoreCall, newResponseText };
} }
// Return the english description in a model response, skipping over any artifacts. // Return the english description in a model response, skipping over any artifacts.
@ -214,12 +209,67 @@ function getMessageDescription(responseText: string): string {
return responseText; 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 { interface FileContents {
filePath: string; filePath: string;
content: 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 fileContents: FileContents[] = [];
const messageParser = new StreamingMessageParser({ const messageParser = new StreamingMessageParser({
@ -240,20 +290,23 @@ async function restorePartialFiles(files: FileMap, apiKey: string, responseText:
const responseDescription = getMessageDescription(responseText); const responseDescription = getMessageDescription(responseText);
const restoreCalls: AnthropicCall[] = []; const restoreCalls: AnthropicCall[] = [];
for (const file of fileContents) { for (const { filePath, content: newContent } of fileContents) {
const existingContent = getFileContents(files, file.filePath); const existingContent = getFileContents(files, filePath);
const newContent = file.content;
if (shouldRestorePartialFile(existingContent, newContent)) { if (shouldRestorePartialFile(existingContent, newContent)) {
const { restoreCall, newResponseText } = await restorePartialFile( const { restoreCall, restoredContent } = await restorePartialFile(
existingContent, existingContent,
newContent, newContent,
apiKey, apiKey,
responseText,
responseDescription responseDescription
); );
restoreCalls.push(restoreCall); 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 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); chatController.writeText(responseText);

View File

@ -37,7 +37,7 @@ export function useChatHistory() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [initialMessages, setInitialMessages] = useState<Message[]>([]); const [initialMessages, setInitialMessages] = useState<Message[]>([]);
const [ready, setReady] = useState<boolean>(false); const [ready, setReady] = useState<boolean>(!mixedId && !problemId);
const [urlId, setUrlId] = useState<string | undefined>(); const [urlId, setUrlId] = useState<string | undefined>();
const importChat = async (description: string, messages: Message[]) => { const importChat = async (description: string, messages: Message[]) => {
@ -100,7 +100,7 @@ export function useChatHistory() {
}, []); }, []);
return { return {
ready: ready || (!mixedId && !problemId), ready,
initialMessages, initialMessages,
storeMessageHistory: async (messages: Message[]) => { storeMessageHistory: async (messages: Message[]) => {
if (!db || messages.length === 0) { if (!db || messages.length === 0) {

View File

@ -37,7 +37,9 @@ function sendIframeRequest<K extends keyof RequestMap>(
iframe: HTMLIFrameElement, iframe: HTMLIFrameElement,
request: Extract<Request, { request: K }>, request: Extract<Request, { request: K }>,
) { ) {
assert(iframe.contentWindow); if (!iframe.contentWindow) {
return undefined;
}
const target = iframe.contentWindow; const target = iframe.contentWindow;
const requestId = ++lastRequestId; const requestId = ++lastRequestId;
@ -56,10 +58,12 @@ function sendIframeRequest<K extends keyof RequestMap>(
}); });
} }
let gMessageCount = 0;
export async function getIFrameSimulationData(iframe: HTMLIFrameElement): Promise<SimulationData> { export async function getIFrameSimulationData(iframe: HTMLIFrameElement): Promise<SimulationData> {
const buffer = await sendIframeRequest(iframe, { request: 'recording-data' }); const buffer = await sendIframeRequest(iframe, { request: 'recording-data' });
if (!buffer) {
return [];
}
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const jsonString = decoder.decode(new Uint8Array(buffer)); 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<MouseData> { export async function getMouseData(iframe: HTMLIFrameElement, position: { x: number; y: number }): Promise<MouseData> {
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. // Add handlers to the current iframe's window.