intermediate changes towards multiple generations

This commit is contained in:
Abi Raja 2024-08-22 13:26:42 -04:00
parent 5f6dd08411
commit 8e8f0b4b64
11 changed files with 538 additions and 567 deletions

View File

@ -36,6 +36,7 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"nanoid": "^5.0.7",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",

View File

@ -8,8 +8,7 @@ import { OnboardingNote } from "./components/messages/OnboardingNote";
import { usePersistedState } from "./hooks/usePersistedState"; import { usePersistedState } from "./hooks/usePersistedState";
import TermsOfServiceDialog from "./components/TermsOfServiceDialog"; import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants"; import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
import { History } from "./components/history/history_types"; import { extractHistory } from "./components/history/utils";
import { extractHistoryTree } from "./components/history/utils";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Stack } from "./lib/stacks"; import { Stack } from "./lib/stacks";
import { CodeGenerationModel } from "./lib/models"; import { CodeGenerationModel } from "./lib/models";
@ -23,6 +22,7 @@ import DeprecationMessage from "./components/messages/DeprecationMessage";
import { GenerationSettings } from "./components/settings/GenerationSettings"; import { GenerationSettings } from "./components/settings/GenerationSettings";
import StartPane from "./components/start-pane/StartPane"; import StartPane from "./components/start-pane/StartPane";
import { takeScreenshot } from "./lib/takeScreenshot"; import { takeScreenshot } from "./lib/takeScreenshot";
import { Commit, createCommit } from "./components/history/history_types";
function App() { function App() {
const { const {
@ -34,18 +34,18 @@ function App() {
referenceImages, referenceImages,
setReferenceImages, setReferenceImages,
head,
commits,
addCommit,
removeCommit,
setHead,
appendCommitCode,
setCommitCode,
resetCommits,
// Outputs // Outputs
setGeneratedCode,
currentVariantIndex,
setVariant,
appendToVariant,
resetVariants,
appendExecutionConsole, appendExecutionConsole,
resetExecutionConsoles, resetExecutionConsoles,
currentVersion,
setCurrentVersion,
appHistory,
setAppHistory,
} = useProjectStore(); } = useProjectStore();
const { const {
@ -113,34 +113,30 @@ function App() {
setShouldIncludeResultImage(false); setShouldIncludeResultImage(false);
setUpdateInstruction(""); setUpdateInstruction("");
disableInSelectAndEditMode(); disableInSelectAndEditMode();
setGeneratedCode("");
resetVariants();
resetExecutionConsoles(); resetExecutionConsoles();
resetCommits();
// Inputs // Inputs
setInputMode("image"); setInputMode("image");
setReferenceImages([]); setReferenceImages([]);
setIsImportedFromCode(false); setIsImportedFromCode(false);
setAppHistory([]);
setCurrentVersion(null);
}; };
const regenerate = () => { const regenerate = () => {
if (currentVersion === null) { // TODO: post to Sentry
if (head === null) {
toast.error( toast.error(
"No current version set. Please open a Github issue as this shouldn't happen." "No current version set. Please open a Github issue as this shouldn't happen."
); );
return; return;
} }
// Retrieve the previous command // Retrieve the previous command
const previousCommand = appHistory[currentVersion]; const currentCommit = commits[head];
if (previousCommand.type !== "ai_create") { if (currentCommit.type !== "ai_create") {
toast.error("Only the first version can be regenerated."); toast.error("Only the first version can be regenerated.");
return; return;
} }
// Re-run the create // Re-run the create
doCreate(referenceImages, inputMode); doCreate(referenceImages, inputMode);
}; };
@ -149,25 +145,32 @@ function App() {
const cancelCodeGeneration = () => { const cancelCodeGeneration = () => {
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE); wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE);
// make sure stop can correct the state even if the websocket is already closed // make sure stop can correct the state even if the websocket is already closed
cancelCodeGenerationAndReset(); // TODO: Look into this
// cancelCodeGenerationAndReset();
}; };
// Used for code generation failure as well // Used for code generation failure as well
const cancelCodeGenerationAndReset = () => { const cancelCodeGenerationAndReset = (commit: Commit) => {
// When this is the first version, reset the entire app state // When the current commit is the first version, reset the entire app state
if (currentVersion === null) { if (commit.type === "ai_create") {
reset(); reset();
} else { } else {
// Otherwise, revert to the last version // Otherwise, remove current commit from commits
setGeneratedCode(appHistory[currentVersion].code); removeCommit(commit.hash);
// Revert to parent commit
const parentCommitHash = commit.parentHash;
if (parentCommitHash) {
setHead(parentCommitHash);
} else {
// TODO: Hit Sentry
}
setAppState(AppState.CODE_READY); setAppState(AppState.CODE_READY);
} }
}; };
function doGenerateCode( function doGenerateCode(params: CodeGenerationParams) {
params: CodeGenerationParams,
parentVersion: number | null
) {
// Reset the execution console // Reset the execution console
resetExecutionConsoles(); resetExecutionConsoles();
@ -177,69 +180,51 @@ function App() {
// Merge settings with params // Merge settings with params
const updatedParams = { ...params, ...settings }; const updatedParams = { ...params, ...settings };
const baseCommitObject = {
date_created: new Date(),
variants: [{ code: "" }, { code: "" }],
selectedVariantIndex: 0,
};
const commitInputObject =
params.generationType === "create"
? {
...baseCommitObject,
type: "ai_create" as const,
parentHash: null,
inputs: { image_url: referenceImages[0] },
}
: {
...baseCommitObject,
type: "ai_edit" as const,
parentHash: head,
inputs: {
prompt: params.history
? params.history[params.history.length - 1]
: "",
},
};
const commit = createCommit(commitInputObject);
addCommit(commit);
setHead(commit.hash);
generateCode( generateCode(
wsRef, wsRef,
updatedParams, updatedParams,
// On change // On change
(token, variant) => { (token, variant) => {
if (variant === currentVariantIndex) { appendCommitCode(commit.hash, variant, token);
setGeneratedCode((prev) => prev + token);
}
appendToVariant(token, variant);
}, },
// On set code // On set code
(code, variant) => { (code, variant) => {
if (variant === currentVariantIndex) { setCommitCode(commit.hash, variant, code);
setGeneratedCode(code);
}
setVariant(code, variant);
// TODO: How to deal with variants?
if (params.generationType === "create") {
setAppHistory([
{
type: "ai_create",
parentIndex: null,
code,
inputs: { image_url: referenceImages[0] },
},
]);
setCurrentVersion(0);
} else {
setAppHistory((prev) => {
// Validate parent version
if (parentVersion === null) {
toast.error(
"No parent version set. Contact support or open a Github issue."
);
return prev;
}
const newHistory: History = [
...prev,
{
type: "ai_edit",
parentIndex: parentVersion,
code,
inputs: {
prompt: params.history
? params.history[params.history.length - 1]
: "", // History should never be empty when performing an edit
},
},
];
setCurrentVersion(newHistory.length - 1);
return newHistory;
});
}
}, },
// On status update // On status update
(line, variant) => appendExecutionConsole(variant, line), (line, variant) => appendExecutionConsole(variant, line),
// On cancel // On cancel
() => { () => {
cancelCodeGenerationAndReset(); cancelCodeGenerationAndReset(commit);
}, },
// On complete // On complete
() => { () => {
@ -259,14 +244,11 @@ function App() {
// Kick off the code generation // Kick off the code generation
if (referenceImages.length > 0) { if (referenceImages.length > 0) {
doGenerateCode( doGenerateCode({
{
generationType: "create", generationType: "create",
image: referenceImages[0], image: referenceImages[0],
inputMode, inputMode,
}, });
currentVersion
);
} }
} }
@ -280,16 +262,17 @@ function App() {
return; return;
} }
if (currentVersion === null) { // if (currentVersion === null) {
toast.error( // toast.error(
"No current version set. Contact support or open a Github issue." // "No current version set. Contact support or open a Github issue."
); // );
return; // return;
} // }
let historyTree; let historyTree;
try { try {
historyTree = extractHistoryTree(appHistory, currentVersion); // TODO: Fix head being null
historyTree = extractHistory(head || "", commits);
} catch { } catch {
toast.error( toast.error(
"Version history is invalid. This shouldn't happen. Please contact support or open a Github issue." "Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
@ -309,34 +292,28 @@ function App() {
const updatedHistory = [...historyTree, modifiedUpdateInstruction]; const updatedHistory = [...historyTree, modifiedUpdateInstruction];
console.log(updatedHistory);
if (shouldIncludeResultImage) { if (shouldIncludeResultImage) {
const resultImage = await takeScreenshot(); const resultImage = await takeScreenshot();
doGenerateCode( doGenerateCode({
{
generationType: "update", generationType: "update",
inputMode, inputMode,
image: referenceImages[0], image: referenceImages[0],
resultImage: resultImage, resultImage: resultImage,
history: updatedHistory, history: updatedHistory,
isImportedFromCode, isImportedFromCode,
}, });
currentVersion
);
} else { } else {
doGenerateCode( doGenerateCode({
{
generationType: "update", generationType: "update",
inputMode, inputMode,
image: referenceImages[0], image: referenceImages[0],
history: updatedHistory, history: updatedHistory,
isImportedFromCode, isImportedFromCode,
}, });
currentVersion
);
} }
setGeneratedCode("");
resetVariants();
setUpdateInstruction(""); setUpdateInstruction("");
} }
@ -358,18 +335,27 @@ function App() {
// Set input state // Set input state
setIsImportedFromCode(true); setIsImportedFromCode(true);
console.log(code);
// Set up this project // Set up this project
setGeneratedCode(code); // TODO*
// setGeneratedCode(code);
setStack(stack); setStack(stack);
setAppHistory([ // setAppHistory([
{ // {
type: "code_create", // type: "code_create",
parentIndex: null, // parentIndex: null,
code, // code,
inputs: { code }, // inputs: { code },
}, // },
]); // ]);
setCurrentVersion(0); // setVariant(0, {
// type: "code_create",
// parentIndex: null,
// code,
// });
// setCurrentVariantIndex(0);
// setCurrentVersion(0);
// Set the app state // Set the app state
setAppState(AppState.CODE_READY); setAppState(AppState.CODE_READY);

View File

@ -2,7 +2,7 @@ import toast from "react-hot-toast";
import classNames from "classnames"; import classNames from "classnames";
import { Badge } from "../ui/badge"; import { Badge } from "../ui/badge";
import { renderHistory } from "./utils"; import { summarizeHistoryItem } from "./utils";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
@ -17,25 +17,58 @@ interface Props {
} }
export default function HistoryDisplay({ shouldDisableReverts }: Props) { export default function HistoryDisplay({ shouldDisableReverts }: Props) {
const { const { commits, head, setHead } = useProjectStore();
appHistory: history,
currentVersion,
setCurrentVersion,
setGeneratedCode,
} = useProjectStore();
const renderedHistory = renderHistory(history, currentVersion);
const revertToVersion = (index: number) => { // TODO: Clean this up
if (index < 0 || index >= history.length || !history[index]) return;
setCurrentVersion(index); const newHistory = Object.values(commits).flatMap((commit) => {
setGeneratedCode(history[index].code); if (commit.type === "ai_create" || commit.type === "ai_edit") {
return {
type: commit.type,
hash: commit.hash,
summary: summarizeHistoryItem(commit),
parentHash: commit.parentHash,
code: commit.variants[commit.selectedVariantIndex].code,
inputs: commit.inputs,
date_created: commit.date_created,
};
}
return [];
});
// Sort by date created
newHistory.sort(
(a, b) =>
new Date(a.date_created).getTime() - new Date(b.date_created).getTime()
);
const setParentVersion = (
parentHash: string | null,
currentHash: string | null
) => {
if (!parentHash) return null;
const parentIndex = newHistory.findIndex(
(item) => item.hash === parentHash
);
const currentIndex = newHistory.findIndex(
(item) => item.hash === currentHash
);
return parentIndex !== -1 && parentIndex != currentIndex - 1
? parentIndex + 1
: null;
}; };
return renderedHistory.length === 0 ? null : ( // Update newHistory to include the parent version
const updatedHistory = newHistory.map((item) => ({
...item,
parentVersion: setParentVersion(item.parentHash, item.hash),
}));
return updatedHistory.length === 0 ? null : (
<div className="flex flex-col h-screen"> <div className="flex flex-col h-screen">
<h1 className="font-bold mb-2">Versions</h1> <h1 className="font-bold mb-2">Versions</h1>
<ul className="space-y-0 flex flex-col-reverse"> <ul className="space-y-0 flex flex-col-reverse">
{renderedHistory.map((item, index) => ( {updatedHistory.map((item, index) => (
<li key={index}> <li key={index}>
<Collapsible> <Collapsible>
<div <div
@ -43,8 +76,8 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
"flex items-center justify-between space-x-2 w-full pr-2", "flex items-center justify-between space-x-2 w-full pr-2",
"border-b cursor-pointer", "border-b cursor-pointer",
{ {
" hover:bg-black hover:text-white": !item.isActive, " hover:bg-black hover:text-white": item.hash === head,
"bg-slate-500 text-white": item.isActive, "bg-slate-500 text-white": item.hash === head,
} }
)} )}
> >
@ -55,14 +88,14 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
? toast.error( ? toast.error(
"Please wait for code generation to complete before viewing an older version." "Please wait for code generation to complete before viewing an older version."
) )
: revertToVersion(index) : setHead(item.hash)
} }
> >
<div className="flex gap-x-1 truncate"> <div className="flex gap-x-1 truncate">
<h2 className="text-sm truncate">{item.summary}</h2> <h2 className="text-sm truncate">{item.summary}</h2>
{item.parentVersion !== null && ( {item.parentVersion !== null && (
<h2 className="text-sm"> <h2 className="text-sm">
(parent: {item.parentVersion}) (parent: v{item.parentVersion})
</h2> </h2>
)} )}
</div> </div>

View File

@ -1,37 +1,44 @@
export type HistoryItemType = "ai_create" | "ai_edit" | "code_create"; export type CommitType = "ai_create" | "ai_edit" | "code_create";
type CommonHistoryItem = { export type CommitHash = string;
parentIndex: null | number;
export type Variant = {
code: string; code: string;
}; };
export type HistoryItem = export type BaseCommit = {
| ({ hash: CommitHash;
parentHash: CommitHash | null;
date_created: Date;
variants: Variant[];
selectedVariantIndex: number;
};
import { nanoid } from "nanoid";
// TODO: Move to a different file
export function createCommit(
commit: Omit<AiCreateCommit, "hash"> | Omit<AiEditCommit, "hash">
): Commit {
const hash = nanoid();
return { ...commit, hash };
}
export type AiCreateCommit = BaseCommit & {
type: "ai_create"; type: "ai_create";
inputs: AiCreateInputs; inputs: {
} & CommonHistoryItem)
| ({
type: "ai_edit";
inputs: AiEditInputs;
} & CommonHistoryItem)
| ({
type: "code_create";
inputs: CodeCreateInputs;
} & CommonHistoryItem);
export type AiCreateInputs = {
image_url: string; image_url: string;
};
}; };
export type AiEditInputs = { export type AiEditCommit = BaseCommit & {
type: "ai_edit";
inputs: {
prompt: string; prompt: string;
};
}; };
export type CodeCreateInputs = { export type Commit = AiCreateCommit | AiEditCommit;
code: string;
};
export type History = HistoryItem[];
export type RenderedHistoryItem = { export type RenderedHistoryItem = {
type: string; type: string;

View File

@ -1,231 +1,231 @@
import { extractHistoryTree, renderHistory } from "./utils"; // import { extractHistoryTree, renderHistory } from "./utils";
import type { History } from "./history_types"; // import type { History } from "./history_types";
const basicLinearHistory: History = [ // const basicLinearHistory: History = [
{ // {
type: "ai_create", // type: "ai_create",
parentIndex: null, // parentIndex: null,
code: "<html>1. create</html>", // code: "<html>1. create</html>",
inputs: { // inputs: {
image_url: "", // image_url: "",
}, // },
}, // },
{ // {
type: "ai_edit", // type: "ai_edit",
parentIndex: 0, // parentIndex: 0,
code: "<html>2. edit with better icons</html>", // code: "<html>2. edit with better icons</html>",
inputs: { // inputs: {
prompt: "use better icons", // prompt: "use better icons",
}, // },
}, // },
{ // {
type: "ai_edit", // type: "ai_edit",
parentIndex: 1, // parentIndex: 1,
code: "<html>3. edit with better icons and red text</html>", // code: "<html>3. edit with better icons and red text</html>",
inputs: { // inputs: {
prompt: "make text red", // prompt: "make text red",
}, // },
}, // },
]; // ];
const basicLinearHistoryWithCode: History = [ // const basicLinearHistoryWithCode: History = [
{ // {
type: "code_create", // type: "code_create",
parentIndex: null, // parentIndex: null,
code: "<html>1. create</html>", // code: "<html>1. create</html>",
inputs: { // inputs: {
code: "<html>1. create</html>", // code: "<html>1. create</html>",
}, // },
}, // },
...basicLinearHistory.slice(1), // ...basicLinearHistory.slice(1),
]; // ];
const basicBranchingHistory: History = [ // const basicBranchingHistory: History = [
...basicLinearHistory, // ...basicLinearHistory,
{ // {
type: "ai_edit", // type: "ai_edit",
parentIndex: 1, // parentIndex: 1,
code: "<html>4. edit with better icons and green text</html>", // code: "<html>4. edit with better icons and green text</html>",
inputs: { // inputs: {
prompt: "make text green", // prompt: "make text green",
}, // },
}, // },
]; // ];
const longerBranchingHistory: History = [ // const longerBranchingHistory: History = [
...basicBranchingHistory, // ...basicBranchingHistory,
{ // {
type: "ai_edit", // type: "ai_edit",
parentIndex: 3, // parentIndex: 3,
code: "<html>5. edit with better icons and green, bold text</html>", // code: "<html>5. edit with better icons and green, bold text</html>",
inputs: { // inputs: {
prompt: "make text bold", // prompt: "make text bold",
}, // },
}, // },
]; // ];
const basicBadHistory: History = [ // const basicBadHistory: History = [
{ // {
type: "ai_create", // type: "ai_create",
parentIndex: null, // parentIndex: null,
code: "<html>1. create</html>", // code: "<html>1. create</html>",
inputs: { // inputs: {
image_url: "", // image_url: "",
}, // },
}, // },
{ // {
type: "ai_edit", // type: "ai_edit",
parentIndex: 2, // <- Bad parent index // parentIndex: 2, // <- Bad parent index
code: "<html>2. edit with better icons</html>", // code: "<html>2. edit with better icons</html>",
inputs: { // inputs: {
prompt: "use better icons", // prompt: "use better icons",
}, // },
}, // },
]; // ];
describe("History Utils", () => { // describe("History Utils", () => {
test("should correctly extract the history tree", () => { // test("should correctly extract the history tree", () => {
expect(extractHistoryTree(basicLinearHistory, 2)).toEqual([ // expect(extractHistoryTree(basicLinearHistory, 2)).toEqual([
"<html>1. create</html>", // "<html>1. create</html>",
"use better icons", // "use better icons",
"<html>2. edit with better icons</html>", // "<html>2. edit with better icons</html>",
"make text red", // "make text red",
"<html>3. edit with better icons and red text</html>", // "<html>3. edit with better icons and red text</html>",
]); // ]);
expect(extractHistoryTree(basicLinearHistory, 0)).toEqual([ // expect(extractHistoryTree(basicLinearHistory, 0)).toEqual([
"<html>1. create</html>", // "<html>1. create</html>",
]); // ]);
// Test branching // // Test branching
expect(extractHistoryTree(basicBranchingHistory, 3)).toEqual([ // expect(extractHistoryTree(basicBranchingHistory, 3)).toEqual([
"<html>1. create</html>", // "<html>1. create</html>",
"use better icons", // "use better icons",
"<html>2. edit with better icons</html>", // "<html>2. edit with better icons</html>",
"make text green", // "make text green",
"<html>4. edit with better icons and green text</html>", // "<html>4. edit with better icons and green text</html>",
]); // ]);
expect(extractHistoryTree(longerBranchingHistory, 4)).toEqual([ // expect(extractHistoryTree(longerBranchingHistory, 4)).toEqual([
"<html>1. create</html>", // "<html>1. create</html>",
"use better icons", // "use better icons",
"<html>2. edit with better icons</html>", // "<html>2. edit with better icons</html>",
"make text green", // "make text green",
"<html>4. edit with better icons and green text</html>", // "<html>4. edit with better icons and green text</html>",
"make text bold", // "make text bold",
"<html>5. edit with better icons and green, bold text</html>", // "<html>5. edit with better icons and green, bold text</html>",
]); // ]);
expect(extractHistoryTree(longerBranchingHistory, 2)).toEqual([ // expect(extractHistoryTree(longerBranchingHistory, 2)).toEqual([
"<html>1. create</html>", // "<html>1. create</html>",
"use better icons", // "use better icons",
"<html>2. edit with better icons</html>", // "<html>2. edit with better icons</html>",
"make text red", // "make text red",
"<html>3. edit with better icons and red text</html>", // "<html>3. edit with better icons and red text</html>",
]); // ]);
// Errors // // Errors
// Bad index // // Bad index
expect(() => extractHistoryTree(basicLinearHistory, 100)).toThrow(); // expect(() => extractHistoryTree(basicLinearHistory, 100)).toThrow();
expect(() => extractHistoryTree(basicLinearHistory, -2)).toThrow(); // expect(() => extractHistoryTree(basicLinearHistory, -2)).toThrow();
// Bad tree // // Bad tree
expect(() => extractHistoryTree(basicBadHistory, 1)).toThrow(); // expect(() => extractHistoryTree(basicBadHistory, 1)).toThrow();
}); // });
test("should correctly render the history tree", () => { // test("should correctly render the history tree", () => {
expect(renderHistory(basicLinearHistory, 2)).toEqual([ // expect(renderHistory(basicLinearHistory, 2)).toEqual([
{ // {
isActive: false, // isActive: false,
parentVersion: null, // parentVersion: null,
summary: "Create", // summary: "Create",
type: "Create", // type: "Create",
}, // },
{ // {
isActive: false, // isActive: false,
parentVersion: null, // parentVersion: null,
summary: "use better icons", // summary: "use better icons",
type: "Edit", // type: "Edit",
}, // },
{ // {
isActive: true, // isActive: true,
parentVersion: null, // parentVersion: null,
summary: "make text red", // summary: "make text red",
type: "Edit", // type: "Edit",
}, // },
]); // ]);
// Current version is the first version // // Current version is the first version
expect(renderHistory(basicLinearHistory, 0)).toEqual([ // expect(renderHistory(basicLinearHistory, 0)).toEqual([
{ // {
isActive: true, // isActive: true,
parentVersion: null, // parentVersion: null,
summary: "Create", // summary: "Create",
type: "Create", // type: "Create",
}, // },
{ // {
isActive: false, // isActive: false,
parentVersion: null, // parentVersion: null,
summary: "use better icons", // summary: "use better icons",
type: "Edit", // type: "Edit",
}, // },
{ // {
isActive: false, // isActive: false,
parentVersion: null, // parentVersion: null,
summary: "make text red", // summary: "make text red",
type: "Edit", // type: "Edit",
}, // },
]); // ]);
// Render a history with code // // Render a history with code
expect(renderHistory(basicLinearHistoryWithCode, 0)).toEqual([ // expect(renderHistory(basicLinearHistoryWithCode, 0)).toEqual([
{ // {
isActive: true, // isActive: true,
parentVersion: null, // parentVersion: null,
summary: "Imported from code", // summary: "Imported from code",
type: "Imported from code", // type: "Imported from code",
}, // },
{ // {
isActive: false, // isActive: false,
parentVersion: null, // parentVersion: null,
summary: "use better icons", // summary: "use better icons",
type: "Edit", // type: "Edit",
}, // },
{ // {
isActive: false, // isActive: false,
parentVersion: null, // parentVersion: null,
summary: "make text red", // summary: "make text red",
type: "Edit", // type: "Edit",
}, // },
]); // ]);
// Render a non-linear history // // Render a non-linear history
expect(renderHistory(basicBranchingHistory, 3)).toEqual([ // expect(renderHistory(basicBranchingHistory, 3)).toEqual([
{ // {
isActive: false, // isActive: false,
parentVersion: null, // parentVersion: null,
summary: "Create", // summary: "Create",
type: "Create", // type: "Create",
}, // },
{ // {
isActive: false, // isActive: false,
parentVersion: null, // parentVersion: null,
summary: "use better icons", // summary: "use better icons",
type: "Edit", // type: "Edit",
}, // },
{ // {
isActive: false, // isActive: false,
parentVersion: null, // parentVersion: null,
summary: "make text red", // summary: "make text red",
type: "Edit", // type: "Edit",
}, // },
{ // {
isActive: true, // isActive: true,
parentVersion: "v2", // parentVersion: "v2",
summary: "make text green", // summary: "make text green",
type: "Edit", // type: "Edit",
}, // },
]); // ]);
}); // });
}); // });

View File

@ -1,33 +1,29 @@
import { import { Commit, CommitHash } from "./history_types";
History,
HistoryItem,
HistoryItemType,
RenderedHistoryItem,
} from "./history_types";
export function extractHistoryTree( export function extractHistory(
history: History, hash: CommitHash,
version: number commits: Record<CommitHash, Commit>
): string[] { ): string[] {
const flatHistory: string[] = []; const flatHistory: string[] = [];
let currentIndex: number | null = version; let currentCommitHash: CommitHash | null = hash;
while (currentIndex !== null) { while (currentCommitHash !== null) {
const item: HistoryItem = history[currentIndex]; const commit: Commit = commits[currentCommitHash];
if (item) { if (commit) {
if (item.type === "ai_create") { if (commit.type === "ai_create") {
// Don't include the image for ai_create // Don't include the image for ai_create
flatHistory.unshift(item.code); flatHistory.unshift(commit.variants[commit.selectedVariantIndex].code);
} else if (item.type === "ai_edit") { } else if (commit.type === "ai_edit") {
flatHistory.unshift(item.code); flatHistory.unshift(commit.variants[commit.selectedVariantIndex].code);
flatHistory.unshift(item.inputs.prompt); flatHistory.unshift(commit.inputs.prompt);
} else if (item.type === "code_create") {
flatHistory.unshift(item.code);
} }
// } else if (item.type === "code_create") {
// flatHistory.unshift(item.code);
// }
// Move to the parent of the current item // Move to the parent of the current item
currentIndex = item.parentIndex; currentCommitHash = commit.parentHash;
} else { } else {
throw new Error("Malformed history: missing parent index"); throw new Error("Malformed history: missing parent index");
} }
@ -36,61 +32,16 @@ export function extractHistoryTree(
return flatHistory; return flatHistory;
} }
function displayHistoryItemType(itemType: HistoryItemType) { export function summarizeHistoryItem(commit: Commit) {
switch (itemType) { const commitType = commit.type;
switch (commitType) {
case "ai_create": case "ai_create":
return "Create"; return "Create";
case "ai_edit": case "ai_edit":
return "Edit"; return commit.inputs.prompt;
case "code_create":
return "Imported from code";
default: { default: {
const exhaustiveCheck: never = itemType; const exhaustiveCheck: never = commitType;
throw new Error(`Unhandled case: ${exhaustiveCheck}`); throw new Error(`Unhandled case: ${exhaustiveCheck}`);
} }
} }
} }
function summarizeHistoryItem(item: HistoryItem) {
const itemType = item.type;
switch (itemType) {
case "ai_create":
return "Create";
case "ai_edit":
return item.inputs.prompt;
case "code_create":
return "Imported from code";
default: {
const exhaustiveCheck: never = itemType;
throw new Error(`Unhandled case: ${exhaustiveCheck}`);
}
}
}
export const renderHistory = (
history: History,
currentVersion: number | null
) => {
const renderedHistory: RenderedHistoryItem[] = [];
for (let i = 0; i < history.length; i++) {
const item = history[i];
// Only show the parent version if it's not the previous version
// (i.e. it's the branching point) and if it's not the first version
const parentVersion =
item.parentIndex !== null && item.parentIndex !== i - 1
? `v${(item.parentIndex || 0) + 1}`
: null;
const type = displayHistoryItemType(item.type);
const isActive = i === currentVersion;
const summary = summarizeHistoryItem(item);
renderedHistory.push({
isActive,
summary: summary,
parentVersion,
type,
});
}
return renderedHistory;
};

View File

@ -23,12 +23,17 @@ interface Props {
function PreviewPane({ doUpdate, reset, settings }: Props) { function PreviewPane({ doUpdate, reset, settings }: Props) {
const { appState } = useAppStore(); const { appState } = useAppStore();
const { inputMode, generatedCode, setGeneratedCode } = useProjectStore(); const { inputMode, head, commits } = useProjectStore();
const currentCommit = head && commits[head] ? commits[head] : "";
const currentCode = currentCommit
? currentCommit.variants[currentCommit.selectedVariantIndex].code
: "";
const previewCode = const previewCode =
inputMode === "video" && appState === AppState.CODING inputMode === "video" && appState === AppState.CODING
? extractHtml(generatedCode) ? extractHtml(currentCode)
: generatedCode; : currentCode;
return ( return (
<div className="ml-4"> <div className="ml-4">
@ -45,7 +50,7 @@ function PreviewPane({ doUpdate, reset, settings }: Props) {
Reset Reset
</Button> </Button>
<Button <Button
onClick={() => downloadCode(generatedCode)} onClick={() => downloadCode(previewCode)}
variant="secondary" variant="secondary"
className="flex items-center gap-x-2 mr-4 dark:text-white dark:bg-gray-700 download-btn" className="flex items-center gap-x-2 mr-4 dark:text-white dark:bg-gray-700 download-btn"
> >
@ -86,7 +91,8 @@ function PreviewPane({ doUpdate, reset, settings }: Props) {
<TabsContent value="code"> <TabsContent value="code">
<CodeTab <CodeTab
code={previewCode} code={previewCode}
setCode={setGeneratedCode} // TODO*
setCode={() => {}}
settings={settings} settings={settings}
/> />
</TabsContent> </TabsContent>

View File

@ -37,15 +37,16 @@ function Sidebar({
setShouldIncludeResultImage, setShouldIncludeResultImage,
} = useAppStore(); } = useAppStore();
const { const { inputMode, referenceImages, executionConsoles, head, commits } =
inputMode, useProjectStore();
generatedCode,
referenceImages,
executionConsoles,
currentVariantIndex,
} = useProjectStore();
const executionConsole = executionConsoles[currentVariantIndex] || []; const viewedCode =
head && commits[head]
? commits[head].variants[commits[head].selectedVariantIndex].code
: "";
const executionConsole =
(head && executionConsoles[commits[head].selectedVariantIndex]) || [];
// When coding is complete, focus on the update instruction textarea // When coding is complete, focus on the update instruction textarea
useEffect(() => { useEffect(() => {
@ -77,7 +78,7 @@ function Sidebar({
{executionConsole.slice(-1)[0]} {executionConsole.slice(-1)[0]}
</div> </div>
<CodePreview code={generatedCode} /> <CodePreview code={viewedCode} />
<div className="flex w-full"> <div className="flex w-full">
<Button <Button

View File

@ -1,45 +1,16 @@
import { useProjectStore } from "../../store/project-store"; import { useProjectStore } from "../../store/project-store";
function Variants() { function Variants() {
const { const { head, commits, updateSelectedVariantIndex } = useProjectStore();
// Inputs
referenceImages,
// Outputs // TODO: Is HEAD null right? And check variants.length === 0 ||
variants, if (head === null) {
currentVariantIndex,
setCurrentVariantIndex,
setGeneratedCode,
appHistory,
setAppHistory,
} = useProjectStore();
function switchVariant(index: number) {
const variant = variants[index];
setCurrentVariantIndex(index);
setGeneratedCode(variant);
if (appHistory.length === 1) {
setAppHistory([
{
type: "ai_create",
parentIndex: null,
code: variant,
inputs: { image_url: referenceImages[0] },
},
]);
} else {
setAppHistory((prev) => {
const newHistory = [...prev];
newHistory[newHistory.length - 1].code = variant;
return newHistory;
});
}
}
if (variants.length === 0) {
return null; return null;
} }
const variants = commits[head || ""].variants;
const selectedVariantIndex = commits[head || ""].selectedVariantIndex;
return ( return (
<div className="mt-4 mb-4"> <div className="mt-4 mb-4">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@ -47,11 +18,11 @@ function Variants() {
<div <div
key={index} key={index}
className={`p-2 border rounded-md cursor-pointer ${ className={`p-2 border rounded-md cursor-pointer ${
index === currentVariantIndex index === selectedVariantIndex
? "bg-blue-100 dark:bg-blue-900" ? "bg-blue-100 dark:bg-blue-900"
: "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700" : "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700"
}`} }`}
onClick={() => switchVariant(index)} onClick={() => updateSelectedVariantIndex(head, index)}
> >
<h3 className="font-medium mb-1">Option {index + 1}</h3> <h3 className="font-medium mb-1">Option {index + 1}</h3>
</div> </div>

View File

@ -1,5 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import { History } from "../components/history/history_types"; import { Commit, CommitHash } from "../components/history/history_types";
// Store for app-wide state // Store for app-wide state
interface ProjectStore { interface ProjectStore {
@ -11,32 +11,26 @@ interface ProjectStore {
referenceImages: string[]; referenceImages: string[];
setReferenceImages: (images: string[]) => void; setReferenceImages: (images: string[]) => void;
// Outputs and other state // Outputs
generatedCode: string; commits: Record<string, Commit>;
setGeneratedCode: ( head: CommitHash | null;
updater: string | ((currentCode: string) => string)
) => void;
variants: string[]; addCommit: (commit: Commit) => void;
currentVariantIndex: number; removeCommit: (hash: CommitHash) => void;
setCurrentVariantIndex: (index: number) => void;
setVariant: (code: string, index: number) => void; setHead: (hash: CommitHash) => void;
appendToVariant: (newTokens: string, index: number) => void; appendCommitCode: (
resetVariants: () => void; hash: CommitHash,
numVariant: number,
code: string
) => void;
setCommitCode: (hash: CommitHash, numVariant: number, code: string) => void;
updateSelectedVariantIndex: (hash: CommitHash, index: number) => void;
resetCommits: () => void;
executionConsoles: { [key: number]: string[] }; executionConsoles: { [key: number]: string[] };
appendExecutionConsole: (variantIndex: number, line: string) => void; appendExecutionConsole: (variantIndex: number, line: string) => void;
resetExecutionConsoles: () => void; resetExecutionConsoles: () => void;
// Tracks the currently shown version from app history
// TODO: might want to move to appStore
currentVersion: number | null;
setCurrentVersion: (version: number | null) => void;
appHistory: History;
setAppHistory: (
updater: History | ((currentHistory: History) => History)
) => void;
} }
export const useProjectStore = create<ProjectStore>((set) => ({ export const useProjectStore = create<ProjectStore>((set) => ({
@ -48,39 +42,64 @@ export const useProjectStore = create<ProjectStore>((set) => ({
referenceImages: [], referenceImages: [],
setReferenceImages: (images) => set({ referenceImages: images }), setReferenceImages: (images) => set({ referenceImages: images }),
// Outputs and other state // Outputs
generatedCode: "", commits: {},
setGeneratedCode: (updater) => head: null,
addCommit: (commit: Commit) => {
set((state) => ({ set((state) => ({
generatedCode: commits: { ...state.commits, [commit.hash]: commit },
typeof updater === "function" ? updater(state.generatedCode) : updater, }));
},
removeCommit: (hash: CommitHash) => {
set((state) => {
const newCommits = { ...state.commits };
delete newCommits[hash];
return { commits: newCommits };
});
},
setHead: (hash: CommitHash) => set({ head: hash }),
appendCommitCode: (hash: CommitHash, numVariant: number, code: string) =>
set((state) => ({
commits: {
...state.commits,
[hash]: {
...state.commits[hash],
variants: state.commits[hash].variants.map((variant, index) =>
index === numVariant
? { ...variant, code: variant.code + code }
: variant
),
},
},
})), })),
setCommitCode: (hash: CommitHash, numVariant: number, code: string) =>
set((state) => ({
commits: {
...state.commits,
[hash]: {
...state.commits[hash],
variants: state.commits[hash].variants.map((variant, index) =>
index === numVariant ? { ...variant, code } : variant
),
},
},
})),
updateSelectedVariantIndex: (hash: CommitHash, index: number) =>
set((state) => ({
commits: {
...state.commits,
[hash]: {
...state.commits[hash],
selectedVariantIndex: index,
},
},
})),
resetCommits: () => set({ commits: {}, head: null }),
// TODO: Reset heads
currentVariantIndex: 0,
variants: [],
executionConsoles: {}, executionConsoles: {},
setCurrentVariantIndex: (index) => set({ currentVariantIndex: index }),
setVariant: (code: string, index: number) =>
set((state) => {
const newVariants = [...state.variants];
while (newVariants.length <= index) {
newVariants.push("");
}
newVariants[index] = code;
return { variants: newVariants };
}),
appendToVariant: (newTokens: string, index: number) =>
set((state) => {
const newVariants = [...state.variants];
while (newVariants.length <= index) {
newVariants.push("");
}
newVariants[index] += newTokens;
return { variants: newVariants };
}),
resetVariants: () => set({ variants: [], currentVariantIndex: 0 }),
appendExecutionConsole: (variantIndex: number, line: string) => appendExecutionConsole: (variantIndex: number, line: string) =>
set((state) => ({ set((state) => ({
executionConsoles: { executionConsoles: {
@ -92,13 +111,4 @@ export const useProjectStore = create<ProjectStore>((set) => ({
}, },
})), })),
resetExecutionConsoles: () => set({ executionConsoles: {} }), resetExecutionConsoles: () => set({ executionConsoles: {} }),
currentVersion: null,
setCurrentVersion: (version) => set({ currentVersion: version }),
appHistory: [],
setAppHistory: (updater) =>
set((state) => ({
appHistory:
typeof updater === "function" ? updater(state.appHistory) : updater,
})),
})); }));

View File

@ -4441,6 +4441,11 @@ nanoid@^3.3.6, nanoid@^3.3.7:
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
nanoid@^5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.7.tgz#6452e8c5a816861fd9d2b898399f7e5fd6944cc6"
integrity sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"