Skip to content

Commit

Permalink
Update backend APIs for doing simulation based prompting (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
bhackett1024 authored Jan 20, 2025
1 parent 8e27657 commit a5c5d39
Show file tree
Hide file tree
Showing 14 changed files with 511 additions and 136 deletions.
6 changes: 3 additions & 3 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
{!chatStarted && (
<div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">
<h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
Where ideas begin
Get unstuck
</h1>
<p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
Bring ideas to life in seconds or get help on existing projects.
Fix tough bugs and get your app working right.
</p>
</div>
)}
Expand Down Expand Up @@ -369,7 +369,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
minHeight: TEXTAREA_MIN_HEIGHT,
maxHeight: TEXTAREA_MAX_HEIGHT,
}}
placeholder="How can Bolt help you today?"
placeholder={chatStarted ? "How can we help you?" : "What do you want to build?"}
translate="no"
/>
<ClientOnly>
Expand Down
2 changes: 1 addition & 1 deletion app/components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function Header() {
<div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
<div className="i-ph:sidebar-simple-duotone text-xl" />
<a href="/" className="text-2xl font-semibold text-accent flex items-center">
<img src="/logo-styled.svg" alt="logo" className="w-[45px] inline-block rotate-90" />
<img src="/logo-styled.svg" alt="logo" className="w-[40px] inline-block rotate-90" />
</a>
</div>
{chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
Expand Down
260 changes: 219 additions & 41 deletions app/lib/replay/SimulationPrompt.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Core logic for prompting the AI developer with the repository state and simulation data.
// Core logic for using simulation data from remote recording to enhance
// the AI developer prompt.

// Currently the simulation prompt is sent from the server.

import { type SimulationData, type MouseData } from './Recording';
import { sendCommandDedicatedClient } from './ReplayProtocolClient';
import { type ChatFileChange } from '~/utils/chatStreamController';
import { assert, ProtocolClient, sendCommandDedicatedClient } from './ReplayProtocolClient';
import JSZip from 'jszip';

// Data supplied by the client for a simulation prompt, separate from the chat input.
export interface SimulationPromptClientData {
Expand All @@ -13,51 +14,228 @@ export interface SimulationPromptClientData {
mouseData?: MouseData;
}

export interface SimulationChatMessage {
role: "user" | "assistant";
content: string;
interface RerecordGenerateParams {
rerecordData: SimulationData;
repositoryContents: string;
}

// Params format for the simulationPrompt command.
interface SimulationPrompt {
simulationData: SimulationData;
repositoryContents: string; // base64 encoded zip file
userPrompt: string;
chatHistory: SimulationChatMessage[];
mouseData?: MouseData;
anthropicAPIKey: string;
}

// Result format for the simulationPrompt command.
interface SimulationPromptResult {
message: string;
fileChanges: ChatFileChange[];
}

export async function performSimulationPrompt(
simulationClientData: SimulationPromptClientData,
userPrompt: string,
chatHistory: SimulationChatMessage[],
anthropicAPIKey: string,
): Promise<SimulationPromptResult> {
const { simulationData, repositoryContents, mouseData } = simulationClientData;

const prompt: SimulationPrompt = {
simulationData,
export async function getSimulationRecording(
simulationData: SimulationData,
repositoryContents: string
): Promise<string> {
const params: RerecordGenerateParams = {
rerecordData: simulationData,
repositoryContents,
userPrompt,
chatHistory,
mouseData,
anthropicAPIKey,
};

const simulationRval = await sendCommandDedicatedClient({
const rv = await sendCommandDedicatedClient({
method: "Recording.globalExperimentalCommand",
params: {
name: "simulationPrompt",
params: prompt,
name: "rerecordGenerate",
params,
},
});

return (simulationRval as { rval: SimulationPromptResult }).rval;
return (rv as { rval: { rerecordedRecordingId: string } }).rval.rerecordedRecordingId;
}

type ProtocolExecutionPoint = string;

export interface URLLocation {
sourceId: string;
line: number;
column: number;
url: string;
}

// A location within a recording and associated source contents.
export interface URLLocationWithSource extends URLLocation {
// Text from the application source indicating the location.
source: string;
}

interface ExecutionDataEntry {
// Value from the application source which is being described.
value?: string;

// Description of the contents of the value. If |value| is omitted
// this describes a control dependency for the location.
contents: string;

// Any associated execution point.
associatedPoint?: ProtocolExecutionPoint;

// Location in the recording of the associated execution point.
associatedLocation?: URLLocationWithSource;

// Any expression for the value at the associated point which flows to this one.
associatedValue?: string;

// Description of how data flows from the associated point to this one.
associatedDataflow?: string;
}

interface ExecutionDataPoint {
// Associated point.
point: ProtocolExecutionPoint;

// Location in the recording being described.
location: URLLocationWithSource;

// Entries describing the point.
entries: ExecutionDataEntry[];
}

// Initial point for analysis that is an uncaught exception thrown
// from application code called by React, causing the app to unmount.
interface ExecutionDataInitialPointReactException {
kind: "ReactException";
errorText: string;

// Whether the exception was thrown by library code called at the point.
calleeFrame: boolean;
}

// Initial point for analysis that is an exception logged to the console.
interface ExecutionDataInitialPointConsoleError {
kind: "ConsoleError";
errorText: string;
}

type BaseExecutionDataInitialPoint =
| ExecutionDataInitialPointReactException
| ExecutionDataInitialPointConsoleError;

export type ExecutionDataInitialPoint = {
point: ProtocolExecutionPoint;
} & BaseExecutionDataInitialPoint;

export interface ExecutionDataAnalysisResult {
// Points which were described.
points: ExecutionDataPoint[];

// If an expression was specified, the dataflow steps for that expression.
dataflow?: string[];

// The initial point which was analyzed. If no point was originally specified,
// another point will be picked based on any comments or other data in the recording.
point?: ProtocolExecutionPoint;

// Any comment text associated with the point.
commentText?: string;

// If the comment is on a React component, the name of the component.
reactComponentName?: string;

// If no point or comment was available, describes the failure associated with the
// initial point of the analysis.
failureData?: ExecutionDataInitialPoint;
}

function trimFileName(url: string): string {
const lastSlash = url.lastIndexOf('/');
return url.slice(lastSlash + 1);
}

async function getSourceText(repositoryContents: string, fileName: string): Promise<string> {
const zip = new JSZip();
const binaryData = Buffer.from(repositoryContents, 'base64');
await zip.loadAsync(binaryData as any /* TS complains but JSZip works */);
for (const [path, file] of Object.entries(zip.files)) {
if (trimFileName(path) === fileName) {
return await file.async('string');
}
}
for (const path of Object.keys(zip.files)) {
console.log("RepositoryPath", path);
}
throw new Error(`File ${fileName} not found in repository`);
}

async function annotateSource(repositoryContents: string, fileName: string, source: string, annotation: string): Promise<string> {
const sourceText = await getSourceText(repositoryContents, fileName);
const sourceLines = sourceText.split('\n');
const lineIndex = sourceLines.findIndex(line => line.includes(source));
if (lineIndex === -1) {
throw new Error(`Source text ${source} not found in ${fileName}`);
}

let rv = "";
for (let i = lineIndex - 3; i < lineIndex + 3; i++) {
if (i < 0 || i >= sourceLines.length) {
continue;
}
if (i === lineIndex) {
const leadingSpaces = sourceLines[i].match(/^\s*/)![0];
rv += `${leadingSpaces}// ${annotation}\n`;
}
rv += `${sourceLines[i]}\n`;
}
return rv;
}

async function enhancePromptFromFailureData(
failurePoint: ExecutionDataPoint,
failureData: ExecutionDataInitialPoint,
repositoryContents: string
): Promise<string> {
const pointText = failurePoint.location.source.trim();
const fileName = trimFileName(failurePoint.location.url);

let prompt = "";
let annotation;

switch (failureData.kind) {
case "ReactException":
prompt += "An exception was thrown which causes React to unmount the application.\n";
if (failureData.calleeFrame) {
annotation = `A function called from here is throwing the exception "${failureData.errorText}"`;
} else {
annotation = `This line is throwing the exception "${failureData.errorText}"`;
}
break;
case "ConsoleError":
prompt += "An exception was thrown and later logged to the console.\n";
annotation = `This line is throwing the exception "${failureData.errorText}"`;
break;
default:
throw new Error(`Unknown failure kind: ${(failureData as any).kind}`);
}

const annotatedSource = await annotateSource(repositoryContents, fileName, pointText, annotation);

prompt += `Here is the affected code, in ${fileName}:\n\n`;
prompt += "```\n" + annotatedSource + "```\n";
return prompt;
}

export async function getSimulationEnhancedPrompt(recordingId: string, repositoryContents: string): Promise<string> {
const client = new ProtocolClient();
await client.initialize();
try {
const createSessionRval = await client.sendCommand({ method: "Recording.createSession", params: { recordingId } });
const sessionId = (createSessionRval as { sessionId: string }).sessionId;

const { rval } = await client.sendCommand({
method: "Session.experimentalCommand",
params: {
name: "analyzeExecutionPoint",
params: {},
},
sessionId,
}) as { rval: ExecutionDataAnalysisResult };;

const { points, failureData } = rval;
assert(failureData, "No failure data");

const failurePoint = points.find(p => p.point === failureData.point);
assert(failurePoint, "No failure point");

console.log("FailureData", JSON.stringify(failureData, null, 2));

const prompt = await enhancePromptFromFailureData(failurePoint, failureData, repositoryContents);
console.log("Enhanced prompt", prompt);
return prompt;
} finally {
client.close();
}
}
2 changes: 1 addition & 1 deletion app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Header } from '~/components/header/Header';
import BackgroundRays from '~/components/ui/BackgroundRays';

export const meta: MetaFunction = () => {
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
return [{ title: 'Nut' }];
};

export const loader = () => json({});
Expand Down
Loading

0 comments on commit a5c5d39

Please sign in to comment.