427 lines
12 KiB
TypeScript
427 lines
12 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import { generateCode } from "./generateCode";
|
|
import SettingsDialog from "./components/settings/SettingsDialog";
|
|
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
|
|
import { IS_RUNNING_ON_CLOUD } from "./config";
|
|
import { PicoBadge } from "./components/messages/PicoBadge";
|
|
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 { extractHistory } from "./components/history/utils";
|
|
import toast from "react-hot-toast";
|
|
import { Stack } from "./lib/stacks";
|
|
import { CodeGenerationModel } from "./lib/models";
|
|
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator";
|
|
import TipLink from "./components/messages/TipLink";
|
|
import { useAppStore } from "./store/app-store";
|
|
import { useProjectStore } from "./store/project-store";
|
|
import Sidebar from "./components/sidebar/Sidebar";
|
|
import PreviewPane from "./components/preview/PreviewPane";
|
|
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 {
|
|
// Inputs
|
|
inputMode,
|
|
setInputMode,
|
|
isImportedFromCode,
|
|
setIsImportedFromCode,
|
|
referenceImages,
|
|
setReferenceImages,
|
|
|
|
head,
|
|
commits,
|
|
addCommit,
|
|
removeCommit,
|
|
setHead,
|
|
appendCommitCode,
|
|
setCommitCode,
|
|
resetCommits,
|
|
|
|
// Outputs
|
|
appendExecutionConsole,
|
|
resetExecutionConsoles,
|
|
} = useProjectStore();
|
|
|
|
const {
|
|
disableInSelectAndEditMode,
|
|
setUpdateInstruction,
|
|
appState,
|
|
setAppState,
|
|
shouldIncludeResultImage,
|
|
setShouldIncludeResultImage,
|
|
} = useAppStore();
|
|
|
|
// Settings
|
|
const [settings, setSettings] = usePersistedState<Settings>(
|
|
{
|
|
openAiApiKey: null,
|
|
openAiBaseURL: null,
|
|
anthropicApiKey: null,
|
|
screenshotOneApiKey: null,
|
|
isImageGenerationEnabled: true,
|
|
editorTheme: EditorTheme.COBALT,
|
|
generatedCodeConfig: Stack.HTML_TAILWIND,
|
|
codeGenerationModel: CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20,
|
|
// Only relevant for hosted version
|
|
isTermOfServiceAccepted: false,
|
|
},
|
|
"setting"
|
|
);
|
|
|
|
const wsRef = useRef<WebSocket>(null);
|
|
|
|
// Code generation model from local storage or the default value
|
|
const model =
|
|
settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION;
|
|
|
|
const showBetterModelMessage =
|
|
model !== CodeGenerationModel.GPT_4O_2024_05_13 &&
|
|
model !== CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20 &&
|
|
appState === AppState.INITIAL;
|
|
|
|
const showSelectAndEditFeature =
|
|
(model === CodeGenerationModel.GPT_4O_2024_05_13 ||
|
|
model === CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20) &&
|
|
(settings.generatedCodeConfig === Stack.HTML_TAILWIND ||
|
|
settings.generatedCodeConfig === Stack.HTML_CSS);
|
|
|
|
// Indicate coding state using the browser tab's favicon and title
|
|
useBrowserTabIndicator(appState === AppState.CODING);
|
|
|
|
// When the user already has the settings in local storage, newly added keys
|
|
// do not get added to the settings so if it's falsy, we populate it with the default
|
|
// value
|
|
useEffect(() => {
|
|
if (!settings.generatedCodeConfig) {
|
|
setSettings((prev) => ({
|
|
...prev,
|
|
generatedCodeConfig: Stack.HTML_TAILWIND,
|
|
}));
|
|
}
|
|
}, [settings.generatedCodeConfig, setSettings]);
|
|
|
|
// Functions
|
|
|
|
const reset = () => {
|
|
setAppState(AppState.INITIAL);
|
|
setShouldIncludeResultImage(false);
|
|
setUpdateInstruction("");
|
|
disableInSelectAndEditMode();
|
|
resetExecutionConsoles();
|
|
|
|
resetCommits();
|
|
|
|
// Inputs
|
|
setInputMode("image");
|
|
setReferenceImages([]);
|
|
setIsImportedFromCode(false);
|
|
};
|
|
|
|
const regenerate = () => {
|
|
// 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 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);
|
|
};
|
|
|
|
// Used when the user cancels the code generation
|
|
const cancelCodeGeneration = () => {
|
|
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE);
|
|
// make sure stop can correct the state even if the websocket is already closed
|
|
// TODO: Look into this
|
|
// cancelCodeGenerationAndReset();
|
|
};
|
|
|
|
// Used for code generation failure as well
|
|
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, 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) {
|
|
// Reset the execution console
|
|
resetExecutionConsoles();
|
|
|
|
// Set the app state
|
|
setAppState(AppState.CODING);
|
|
|
|
// 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) => {
|
|
appendCommitCode(commit.hash, variant, token);
|
|
},
|
|
// On set code
|
|
(code, variant) => {
|
|
setCommitCode(commit.hash, variant, code);
|
|
},
|
|
// On status update
|
|
(line, variant) => appendExecutionConsole(variant, line),
|
|
// On cancel
|
|
() => {
|
|
cancelCodeGenerationAndReset(commit);
|
|
},
|
|
// On complete
|
|
() => {
|
|
setAppState(AppState.CODE_READY);
|
|
}
|
|
);
|
|
}
|
|
|
|
// Initial version creation
|
|
function doCreate(referenceImages: string[], inputMode: "image" | "video") {
|
|
// Reset any existing state
|
|
reset();
|
|
|
|
// Set the input states
|
|
setReferenceImages(referenceImages);
|
|
setInputMode(inputMode);
|
|
|
|
// Kick off the code generation
|
|
if (referenceImages.length > 0) {
|
|
doGenerateCode({
|
|
generationType: "create",
|
|
image: referenceImages[0],
|
|
inputMode,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Subsequent updates
|
|
async function doUpdate(
|
|
updateInstruction: string,
|
|
selectedElement?: HTMLElement
|
|
) {
|
|
if (updateInstruction.trim() === "") {
|
|
toast.error("Please include some instructions for AI on what to update.");
|
|
return;
|
|
}
|
|
|
|
// if (currentVersion === null) {
|
|
// toast.error(
|
|
// "No current version set. Contact support or open a Github issue."
|
|
// );
|
|
// return;
|
|
// }
|
|
|
|
let historyTree;
|
|
try {
|
|
// 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."
|
|
);
|
|
return;
|
|
}
|
|
|
|
let modifiedUpdateInstruction = updateInstruction;
|
|
|
|
// Send in a reference to the selected element if it exists
|
|
if (selectedElement) {
|
|
modifiedUpdateInstruction =
|
|
updateInstruction +
|
|
" referring to this element specifically: " +
|
|
selectedElement.outerHTML;
|
|
}
|
|
|
|
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,
|
|
});
|
|
} else {
|
|
doGenerateCode({
|
|
generationType: "update",
|
|
inputMode,
|
|
image: referenceImages[0],
|
|
history: updatedHistory,
|
|
isImportedFromCode,
|
|
});
|
|
}
|
|
|
|
setUpdateInstruction("");
|
|
}
|
|
|
|
const handleTermDialogOpenChange = (open: boolean) => {
|
|
setSettings((s) => ({
|
|
...s,
|
|
isTermOfServiceAccepted: !open,
|
|
}));
|
|
};
|
|
|
|
function setStack(stack: Stack) {
|
|
setSettings((prev) => ({
|
|
...prev,
|
|
generatedCodeConfig: stack,
|
|
}));
|
|
}
|
|
|
|
function importFromCode(code: string, stack: Stack) {
|
|
// Set input state
|
|
setIsImportedFromCode(true);
|
|
|
|
console.log(code);
|
|
|
|
// Set up this project
|
|
// TODO*
|
|
// setGeneratedCode(code);
|
|
setStack(stack);
|
|
// 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);
|
|
}
|
|
|
|
return (
|
|
<div className="mt-2 dark:bg-black dark:text-white">
|
|
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
|
{IS_RUNNING_ON_CLOUD && (
|
|
<TermsOfServiceDialog
|
|
open={!settings.isTermOfServiceAccepted}
|
|
onOpenChange={handleTermDialogOpenChange}
|
|
/>
|
|
)}
|
|
<div className="lg:fixed lg:inset-y-0 lg:z-40 lg:flex lg:w-96 lg:flex-col">
|
|
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6 dark:bg-zinc-950 dark:text-white">
|
|
{/* Header with access to settings */}
|
|
<div className="flex items-center justify-between mt-10 mb-2">
|
|
<h1 className="text-2xl ">Screenshot to Code</h1>
|
|
<SettingsDialog settings={settings} setSettings={setSettings} />
|
|
</div>
|
|
|
|
{/* Generation settings like stack and model */}
|
|
<GenerationSettings
|
|
settings={settings}
|
|
setSettings={setSettings}
|
|
selectedCodeGenerationModel={model}
|
|
/>
|
|
|
|
{/* Show auto updated message when older models are choosen */}
|
|
{showBetterModelMessage && <DeprecationMessage />}
|
|
|
|
{/* Show tip link until coding is complete */}
|
|
{appState !== AppState.CODE_READY && <TipLink />}
|
|
|
|
{IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />}
|
|
|
|
{/* Rest of the sidebar when we're not in the initial state */}
|
|
{(appState === AppState.CODING ||
|
|
appState === AppState.CODE_READY) && (
|
|
<Sidebar
|
|
showSelectAndEditFeature={showSelectAndEditFeature}
|
|
doUpdate={doUpdate}
|
|
regenerate={regenerate}
|
|
cancelCodeGeneration={cancelCodeGeneration}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<main className="py-2 lg:pl-96">
|
|
{appState === AppState.INITIAL && (
|
|
<StartPane
|
|
doCreate={doCreate}
|
|
importFromCode={importFromCode}
|
|
settings={settings}
|
|
/>
|
|
)}
|
|
|
|
{(appState === AppState.CODING || appState === AppState.CODE_READY) && (
|
|
<PreviewPane doUpdate={doUpdate} reset={reset} settings={settings} />
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|