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",
"copy-to-clipboard": "^3.3.3",
"html2canvas": "^1.4.1",
"nanoid": "^5.0.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",

View File

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

View File

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

View File

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

View File

@ -1,33 +1,29 @@
import {
History,
HistoryItem,
HistoryItemType,
RenderedHistoryItem,
} from "./history_types";
import { Commit, CommitHash } from "./history_types";
export function extractHistoryTree(
history: History,
version: number
export function extractHistory(
hash: CommitHash,
commits: Record<CommitHash, Commit>
): string[] {
const flatHistory: string[] = [];
let currentIndex: number | null = version;
while (currentIndex !== null) {
const item: HistoryItem = history[currentIndex];
let currentCommitHash: CommitHash | null = hash;
while (currentCommitHash !== null) {
const commit: Commit = commits[currentCommitHash];
if (item) {
if (item.type === "ai_create") {
if (commit) {
if (commit.type === "ai_create") {
// Don't include the image for ai_create
flatHistory.unshift(item.code);
} else if (item.type === "ai_edit") {
flatHistory.unshift(item.code);
flatHistory.unshift(item.inputs.prompt);
} else if (item.type === "code_create") {
flatHistory.unshift(item.code);
flatHistory.unshift(commit.variants[commit.selectedVariantIndex].code);
} else if (commit.type === "ai_edit") {
flatHistory.unshift(commit.variants[commit.selectedVariantIndex].code);
flatHistory.unshift(commit.inputs.prompt);
}
// } else if (item.type === "code_create") {
// flatHistory.unshift(item.code);
// }
// Move to the parent of the current item
currentIndex = item.parentIndex;
currentCommitHash = commit.parentHash;
} else {
throw new Error("Malformed history: missing parent index");
}
@ -36,61 +32,16 @@ export function extractHistoryTree(
return flatHistory;
}
function displayHistoryItemType(itemType: HistoryItemType) {
switch (itemType) {
export function summarizeHistoryItem(commit: Commit) {
const commitType = commit.type;
switch (commitType) {
case "ai_create":
return "Create";
case "ai_edit":
return "Edit";
case "code_create":
return "Imported from code";
return commit.inputs.prompt;
default: {
const exhaustiveCheck: never = itemType;
const exhaustiveCheck: never = commitType;
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) {
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 =
inputMode === "video" && appState === AppState.CODING
? extractHtml(generatedCode)
: generatedCode;
? extractHtml(currentCode)
: currentCode;
return (
<div className="ml-4">
@ -45,7 +50,7 @@ function PreviewPane({ doUpdate, reset, settings }: Props) {
Reset
</Button>
<Button
onClick={() => downloadCode(generatedCode)}
onClick={() => downloadCode(previewCode)}
variant="secondary"
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">
<CodeTab
code={previewCode}
setCode={setGeneratedCode}
// TODO*
setCode={() => {}}
settings={settings}
/>
</TabsContent>

View File

@ -37,15 +37,16 @@ function Sidebar({
setShouldIncludeResultImage,
} = useAppStore();
const {
inputMode,
generatedCode,
referenceImages,
executionConsoles,
currentVariantIndex,
} = useProjectStore();
const { inputMode, referenceImages, executionConsoles, head, commits } =
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
useEffect(() => {
@ -77,7 +78,7 @@ function Sidebar({
{executionConsole.slice(-1)[0]}
</div>
<CodePreview code={generatedCode} />
<CodePreview code={viewedCode} />
<div className="flex w-full">
<Button

View File

@ -1,45 +1,16 @@
import { useProjectStore } from "../../store/project-store";
function Variants() {
const {
// Inputs
referenceImages,
const { head, commits, updateSelectedVariantIndex } = useProjectStore();
// Outputs
variants,
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) {
// TODO: Is HEAD null right? And check variants.length === 0 ||
if (head === null) {
return null;
}
const variants = commits[head || ""].variants;
const selectedVariantIndex = commits[head || ""].selectedVariantIndex;
return (
<div className="mt-4 mb-4">
<div className="grid grid-cols-2 gap-2">
@ -47,11 +18,11 @@ function Variants() {
<div
key={index}
className={`p-2 border rounded-md cursor-pointer ${
index === currentVariantIndex
index === selectedVariantIndex
? "bg-blue-100 dark:bg-blue-900"
: "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>
</div>

View File

@ -1,5 +1,5 @@
import { create } from "zustand";
import { History } from "../components/history/history_types";
import { Commit, CommitHash } from "../components/history/history_types";
// Store for app-wide state
interface ProjectStore {
@ -11,32 +11,26 @@ interface ProjectStore {
referenceImages: string[];
setReferenceImages: (images: string[]) => void;
// Outputs and other state
generatedCode: string;
setGeneratedCode: (
updater: string | ((currentCode: string) => string)
) => void;
// Outputs
commits: Record<string, Commit>;
head: CommitHash | null;
variants: string[];
currentVariantIndex: number;
setCurrentVariantIndex: (index: number) => void;
setVariant: (code: string, index: number) => void;
appendToVariant: (newTokens: string, index: number) => void;
resetVariants: () => void;
addCommit: (commit: Commit) => void;
removeCommit: (hash: CommitHash) => void;
setHead: (hash: CommitHash) => void;
appendCommitCode: (
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[] };
appendExecutionConsole: (variantIndex: number, line: string) => 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) => ({
@ -48,39 +42,64 @@ export const useProjectStore = create<ProjectStore>((set) => ({
referenceImages: [],
setReferenceImages: (images) => set({ referenceImages: images }),
// Outputs and other state
generatedCode: "",
setGeneratedCode: (updater) =>
// Outputs
commits: {},
head: null,
addCommit: (commit: Commit) => {
set((state) => ({
generatedCode:
typeof updater === "function" ? updater(state.generatedCode) : updater,
commits: { ...state.commits, [commit.hash]: commit },
}));
},
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: {},
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) =>
set((state) => ({
executionConsoles: {
@ -92,13 +111,4 @@ export const useProjectStore = create<ProjectStore>((set) => ({
},
})),
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"
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:
version "1.4.0"
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"