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

View File

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

View File

@ -37,7 +37,7 @@ export function useChatHistory() {
const [searchParams] = useSearchParams();
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 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) {

View File

@ -37,7 +37,9 @@ function sendIframeRequest<K extends keyof RequestMap>(
iframe: HTMLIFrameElement,
request: Extract<Request, { request: K }>,
) {
assert(iframe.contentWindow);
if (!iframe.contentWindow) {
return undefined;
}
const target = iframe.contentWindow;
const requestId = ++lastRequestId;
@ -56,10 +58,12 @@ function sendIframeRequest<K extends keyof RequestMap>(
});
}
let gMessageCount = 0;
export async function getIFrameSimulationData(iframe: HTMLIFrameElement): Promise<SimulationData> {
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<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.