abstract into more components
This commit is contained in:
parent
deb2375146
commit
993ff88e2b
@ -1,21 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import ImageUpload from "./components/ImageUpload";
|
import ImageUpload from "./components/ImageUpload";
|
||||||
import CodePreview from "./components/CodePreview";
|
|
||||||
import Preview from "./components/Preview";
|
|
||||||
import { generateCode } from "./generateCode";
|
import { generateCode } from "./generateCode";
|
||||||
import Spinner from "./components/Spinner";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import {
|
|
||||||
FaCode,
|
|
||||||
FaDesktop,
|
|
||||||
FaDownload,
|
|
||||||
FaMobile,
|
|
||||||
FaUndo,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import { Switch } from "./components/ui/switch";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs";
|
|
||||||
import SettingsDialog from "./components/SettingsDialog";
|
import SettingsDialog from "./components/SettingsDialog";
|
||||||
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
|
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
|
||||||
import { IS_RUNNING_ON_CLOUD } from "./config";
|
import { IS_RUNNING_ON_CLOUD } from "./config";
|
||||||
@ -26,7 +11,6 @@ import { UrlInputSection } from "./components/UrlInputSection";
|
|||||||
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
|
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
|
||||||
import html2canvas from "html2canvas";
|
import html2canvas from "html2canvas";
|
||||||
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
|
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
|
||||||
import CodeTab from "./components/CodeTab";
|
|
||||||
import OutputSettingsSection from "./components/OutputSettingsSection";
|
import OutputSettingsSection from "./components/OutputSettingsSection";
|
||||||
import { History } from "./components/history/history_types";
|
import { History } from "./components/history/history_types";
|
||||||
import HistoryDisplay from "./components/history/HistoryDisplay";
|
import HistoryDisplay from "./components/history/HistoryDisplay";
|
||||||
@ -36,17 +20,14 @@ import ImportCodeSection from "./components/ImportCodeSection";
|
|||||||
import { Stack } from "./lib/stacks";
|
import { Stack } from "./lib/stacks";
|
||||||
import { CodeGenerationModel } from "./lib/models";
|
import { CodeGenerationModel } from "./lib/models";
|
||||||
import ModelSettingsSection from "./components/ModelSettingsSection";
|
import ModelSettingsSection from "./components/ModelSettingsSection";
|
||||||
import { extractHtml } from "./components/preview/extractHtml";
|
|
||||||
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator";
|
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator";
|
||||||
import TipLink from "./components/core/TipLink";
|
import TipLink from "./components/core/TipLink";
|
||||||
import SelectAndEditModeToggleButton from "./components/select-and-edit/SelectAndEditModeToggleButton";
|
|
||||||
import { useAppStore } from "./store/app-store";
|
import { useAppStore } from "./store/app-store";
|
||||||
import KeyboardShortcutBadge from "./components/core/KeyboardShortcutBadge";
|
|
||||||
import { useProjectStore } from "./store/project-store";
|
import { useProjectStore } from "./store/project-store";
|
||||||
|
import Sidebar from "./components/sidebar/Sidebar";
|
||||||
|
import Preview from "./components/preview/Preview";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [appState, setAppState] = useState<AppState>(AppState.INITIAL);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
// Inputs
|
// Inputs
|
||||||
inputMode,
|
inputMode,
|
||||||
@ -57,20 +38,22 @@ function App() {
|
|||||||
setReferenceImages,
|
setReferenceImages,
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
generatedCode,
|
|
||||||
setGeneratedCode,
|
setGeneratedCode,
|
||||||
|
setExecutionConsole,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
setCurrentVersion,
|
setCurrentVersion,
|
||||||
appHistory,
|
appHistory,
|
||||||
setAppHistory,
|
setAppHistory,
|
||||||
} = useProjectStore();
|
} = useProjectStore();
|
||||||
|
|
||||||
const [executionConsole, setExecutionConsole] = useState<string[]>([]);
|
const {
|
||||||
const [updateInstruction, setUpdateInstruction] = useState("");
|
disableInSelectAndEditMode,
|
||||||
|
setUpdateInstruction,
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
appState,
|
||||||
|
setAppState,
|
||||||
const { disableInSelectAndEditMode } = useAppStore();
|
shouldIncludeResultImage,
|
||||||
|
setShouldIncludeResultImage,
|
||||||
|
} = useAppStore();
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
const [settings, setSettings] = usePersistedState<Settings>(
|
const [settings, setSettings] = usePersistedState<Settings>(
|
||||||
@ -93,9 +76,6 @@ function App() {
|
|||||||
const selectedCodeGenerationModel =
|
const selectedCodeGenerationModel =
|
||||||
settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION;
|
settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION;
|
||||||
|
|
||||||
const [shouldIncludeResultImage, setShouldIncludeResultImage] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
const wsRef = useRef<WebSocket>(null);
|
const wsRef = useRef<WebSocket>(null);
|
||||||
|
|
||||||
const showBetterModelMessage =
|
const showBetterModelMessage =
|
||||||
@ -139,23 +119,6 @@ function App() {
|
|||||||
return png;
|
return png;
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadCode = () => {
|
|
||||||
// Create a blob from the generated code
|
|
||||||
const blob = new Blob([generatedCode], { type: "text/html" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
// Create an anchor element and set properties for download
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download = "index.html"; // Set the file name for download
|
|
||||||
document.body.appendChild(a); // Append to the document
|
|
||||||
a.click(); // Programmatically click the anchor to trigger download
|
|
||||||
|
|
||||||
// Clean up by removing the anchor and revoking the Blob URL
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setAppState(AppState.INITIAL);
|
setAppState(AppState.INITIAL);
|
||||||
setGeneratedCode("");
|
setGeneratedCode("");
|
||||||
@ -194,11 +157,6 @@ function App() {
|
|||||||
cancelCodeGenerationAndReset();
|
cancelCodeGenerationAndReset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const previewCode =
|
|
||||||
inputMode === "video" && appState === AppState.CODING
|
|
||||||
? extractHtml(generatedCode)
|
|
||||||
: generatedCode;
|
|
||||||
|
|
||||||
const cancelCodeGenerationAndReset = () => {
|
const cancelCodeGenerationAndReset = () => {
|
||||||
// When this is the first version, reset the entire app state
|
// When this is the first version, reset the entire app state
|
||||||
if (currentVersion === null) {
|
if (currentVersion === null) {
|
||||||
@ -257,7 +215,7 @@ function App() {
|
|||||||
inputs: {
|
inputs: {
|
||||||
prompt: params.history
|
prompt: params.history
|
||||||
? params.history[params.history.length - 1]
|
? params.history[params.history.length - 1]
|
||||||
: updateInstruction,
|
: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -407,13 +365,6 @@ function App() {
|
|||||||
setAppState(AppState.CODE_READY);
|
setAppState(AppState.CODE_READY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When coding is complete, focus on the update instruction textarea
|
|
||||||
useEffect(() => {
|
|
||||||
if (appState === AppState.CODE_READY && textareaRef.current) {
|
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [appState]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 dark:bg-black dark:text-white">
|
<div className="mt-2 dark:bg-black dark:text-white">
|
||||||
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
||||||
@ -461,135 +412,12 @@ function App() {
|
|||||||
|
|
||||||
{(appState === AppState.CODING ||
|
{(appState === AppState.CODING ||
|
||||||
appState === AppState.CODE_READY) && (
|
appState === AppState.CODE_READY) && (
|
||||||
<>
|
<Sidebar
|
||||||
{/* Show code preview only when coding */}
|
showSelectAndEditFeature={showSelectAndEditFeature}
|
||||||
{appState === AppState.CODING && (
|
doUpdate={doUpdate}
|
||||||
<div className="flex flex-col">
|
regenerate={regenerate}
|
||||||
{/* Speed disclaimer for video mode */}
|
cancelCodeGeneration={cancelCodeGeneration}
|
||||||
{inputMode === "video" && (
|
|
||||||
<div
|
|
||||||
className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700
|
|
||||||
p-2 text-xs mb-4 mt-1"
|
|
||||||
>
|
|
||||||
Code generation from videos can take 3-4 minutes. We do
|
|
||||||
multiple passes to get the best result. Please be patient.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center gap-x-1">
|
|
||||||
<Spinner />
|
|
||||||
{executionConsole.slice(-1)[0]}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CodePreview code={generatedCode} />
|
|
||||||
|
|
||||||
<div className="flex w-full">
|
|
||||||
<Button
|
|
||||||
onClick={cancelCodeGeneration}
|
|
||||||
className="w-full dark:text-white dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{appState === AppState.CODE_READY && (
|
|
||||||
<div>
|
|
||||||
<div className="grid w-full gap-2">
|
|
||||||
<Textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
placeholder="Tell the AI what to change..."
|
|
||||||
onChange={(e) => setUpdateInstruction(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
|
||||||
doUpdate(updateInstruction);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={updateInstruction}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between items-center gap-x-2">
|
|
||||||
<div className="font-500 text-xs text-slate-700 dark:text-white">
|
|
||||||
Include screenshot of current version?
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
checked={shouldIncludeResultImage}
|
|
||||||
onCheckedChange={setShouldIncludeResultImage}
|
|
||||||
className="dark:bg-gray-700"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => doUpdate(updateInstruction)}
|
|
||||||
className="dark:text-white dark:bg-gray-700 update-btn"
|
|
||||||
>
|
|
||||||
Update <KeyboardShortcutBadge letter="enter" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end gap-x-2 mt-2">
|
|
||||||
<Button
|
|
||||||
onClick={regenerate}
|
|
||||||
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700 regenerate-btn"
|
|
||||||
>
|
|
||||||
🔄 Regenerate
|
|
||||||
</Button>
|
|
||||||
{showSelectAndEditFeature && (
|
|
||||||
<SelectAndEditModeToggleButton />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end items-center mt-2">
|
|
||||||
<TipLink />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Reference image display */}
|
|
||||||
<div className="flex gap-x-2 mt-2">
|
|
||||||
{referenceImages.length > 0 && (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div
|
|
||||||
className={classNames({
|
|
||||||
"scanning relative": appState === AppState.CODING,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{inputMode === "image" && (
|
|
||||||
<img
|
|
||||||
className="w-[340px] border border-gray-200 rounded-md"
|
|
||||||
src={referenceImages[0]}
|
|
||||||
alt="Reference"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{inputMode === "video" && (
|
|
||||||
<video
|
|
||||||
muted
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
className="w-[340px] border border-gray-200 rounded-md"
|
|
||||||
src={referenceImages[0]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-400 uppercase text-sm text-center mt-1">
|
|
||||||
{inputMode === "video"
|
|
||||||
? "Original Video"
|
|
||||||
: "Original Screenshot"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="bg-gray-400 px-4 py-2 rounded text-sm">
|
|
||||||
<h2 className="text-lg mb-4 border-b border-gray-800">
|
|
||||||
Console
|
|
||||||
</h2>
|
|
||||||
{executionConsole.map((line, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="border-b border-gray-400 mb-2 text-gray-600 font-mono"
|
|
||||||
>
|
|
||||||
{line}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{
|
{
|
||||||
<HistoryDisplay
|
<HistoryDisplay
|
||||||
@ -612,68 +440,7 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{(appState === AppState.CODING || appState === AppState.CODE_READY) && (
|
{(appState === AppState.CODING || appState === AppState.CODE_READY) && (
|
||||||
// Right side preview and code
|
<Preview doUpdate={doUpdate} reset={reset} settings={settings} />
|
||||||
<div className="ml-4">
|
|
||||||
<Tabs defaultValue="desktop">
|
|
||||||
<div className="flex justify-between mr-8 mb-4">
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
{appState === AppState.CODE_READY && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={reset}
|
|
||||||
className="flex items-center ml-4 gap-x-2 dark:text-white dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
<FaUndo />
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={downloadCode}
|
|
||||||
variant="secondary"
|
|
||||||
className="flex items-center gap-x-2 mr-4 dark:text-white dark:bg-gray-700 download-btn"
|
|
||||||
>
|
|
||||||
<FaDownload /> Download
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="desktop" className="flex gap-x-2">
|
|
||||||
<FaDesktop /> Desktop
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="mobile" className="flex gap-x-2">
|
|
||||||
<FaMobile /> Mobile
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="code" className="flex gap-x-2">
|
|
||||||
<FaCode />
|
|
||||||
Code
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TabsContent value="desktop">
|
|
||||||
<Preview
|
|
||||||
code={previewCode}
|
|
||||||
device="desktop"
|
|
||||||
doUpdate={doUpdate}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="mobile">
|
|
||||||
<Preview
|
|
||||||
code={previewCode}
|
|
||||||
device="mobile"
|
|
||||||
doUpdate={doUpdate}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="code">
|
|
||||||
<CodeTab
|
|
||||||
code={previewCode}
|
|
||||||
setCode={setGeneratedCode}
|
|
||||||
settings={settings}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
114
frontend/src/components/preview/Preview.tsx
Normal file
114
frontend/src/components/preview/Preview.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
||||||
|
import {
|
||||||
|
FaUndo,
|
||||||
|
FaDownload,
|
||||||
|
FaDesktop,
|
||||||
|
FaMobile,
|
||||||
|
FaCode,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import { AppState, Settings } from "../../types";
|
||||||
|
import CodeTab from "../CodeTab";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { useAppStore } from "../../store/app-store";
|
||||||
|
import { useProjectStore } from "../../store/project-store";
|
||||||
|
import { extractHtml } from "./extractHtml";
|
||||||
|
import PreviewComponent from "./PreviewComponent";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
doUpdate: (instruction: string) => void;
|
||||||
|
reset: () => void;
|
||||||
|
settings: Settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Preview({ doUpdate, reset, settings }: Props) {
|
||||||
|
const downloadCode = (code: string) => {
|
||||||
|
// Create a blob from the generated code
|
||||||
|
const blob = new Blob([code], { type: "text/html" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create an anchor element and set properties for download
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "index.html"; // Set the file name for download
|
||||||
|
document.body.appendChild(a); // Append to the document
|
||||||
|
a.click(); // Programmatically click the anchor to trigger download
|
||||||
|
|
||||||
|
// Clean up by removing the anchor and revoking the Blob URL
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { appState } = useAppStore();
|
||||||
|
const { inputMode, generatedCode, setGeneratedCode } = useProjectStore();
|
||||||
|
|
||||||
|
const previewCode =
|
||||||
|
inputMode === "video" && appState === AppState.CODING
|
||||||
|
? extractHtml(generatedCode)
|
||||||
|
: generatedCode;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ml-4">
|
||||||
|
<Tabs defaultValue="desktop">
|
||||||
|
<div className="flex justify-between mr-8 mb-4">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
{appState === AppState.CODE_READY && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={reset}
|
||||||
|
className="flex items-center ml-4 gap-x-2 dark:text-white dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<FaUndo />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => downloadCode(generatedCode)}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-x-2 mr-4 dark:text-white dark:bg-gray-700 download-btn"
|
||||||
|
>
|
||||||
|
<FaDownload /> Download
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="desktop" className="flex gap-x-2">
|
||||||
|
<FaDesktop /> Desktop
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="mobile" className="flex gap-x-2">
|
||||||
|
<FaMobile /> Mobile
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="code" className="flex gap-x-2">
|
||||||
|
<FaCode />
|
||||||
|
Code
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TabsContent value="desktop">
|
||||||
|
<PreviewComponent
|
||||||
|
code={previewCode}
|
||||||
|
device="desktop"
|
||||||
|
doUpdate={doUpdate}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="mobile">
|
||||||
|
<PreviewComponent
|
||||||
|
code={previewCode}
|
||||||
|
device="mobile"
|
||||||
|
doUpdate={doUpdate}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="code">
|
||||||
|
<CodeTab
|
||||||
|
code={previewCode}
|
||||||
|
setCode={setGeneratedCode}
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Preview;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import useThrottle from "../hooks/useThrottle";
|
import useThrottle from "../../hooks/useThrottle";
|
||||||
import EditPopup from "./select-and-edit/EditPopup";
|
import EditPopup from "../select-and-edit/EditPopup";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
code: string;
|
code: string;
|
||||||
@ -9,7 +9,7 @@ interface Props {
|
|||||||
doUpdate: (updateInstruction: string, selectedElement?: HTMLElement) => void;
|
doUpdate: (updateInstruction: string, selectedElement?: HTMLElement) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Preview({ code, device, doUpdate }: Props) {
|
function PreviewComponent({ code, device, doUpdate }: Props) {
|
||||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||||
|
|
||||||
// Don't update code more often than every 200ms.
|
// Don't update code more often than every 200ms.
|
||||||
@ -53,4 +53,4 @@ function Preview({ code, device, doUpdate }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Preview;
|
export default PreviewComponent;
|
||||||
174
frontend/src/components/sidebar/Sidebar.tsx
Normal file
174
frontend/src/components/sidebar/Sidebar.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { Switch } from "@radix-ui/react-switch";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useAppStore } from "../../store/app-store";
|
||||||
|
import { useProjectStore } from "../../store/project-store";
|
||||||
|
import { AppState } from "../../types";
|
||||||
|
import CodePreview from "../CodePreview";
|
||||||
|
import Spinner from "../Spinner";
|
||||||
|
import KeyboardShortcutBadge from "../core/KeyboardShortcutBadge";
|
||||||
|
import TipLink from "../core/TipLink";
|
||||||
|
import SelectAndEditModeToggleButton from "../select-and-edit/SelectAndEditModeToggleButton";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
showSelectAndEditFeature: boolean;
|
||||||
|
doUpdate: (instruction: string) => void;
|
||||||
|
regenerate: () => void;
|
||||||
|
cancelCodeGeneration: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sidebar({
|
||||||
|
showSelectAndEditFeature,
|
||||||
|
doUpdate,
|
||||||
|
regenerate,
|
||||||
|
cancelCodeGeneration,
|
||||||
|
}: SidebarProps) {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
appState,
|
||||||
|
updateInstruction,
|
||||||
|
setUpdateInstruction,
|
||||||
|
shouldIncludeResultImage,
|
||||||
|
setShouldIncludeResultImage,
|
||||||
|
} = useAppStore();
|
||||||
|
const { inputMode, generatedCode, referenceImages, executionConsole } =
|
||||||
|
useProjectStore();
|
||||||
|
|
||||||
|
// When coding is complete, focus on the update instruction textarea
|
||||||
|
useEffect(() => {
|
||||||
|
if (appState === AppState.CODE_READY && textareaRef.current) {
|
||||||
|
textareaRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [appState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Show code preview only when coding */}
|
||||||
|
{appState === AppState.CODING && (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{/* Speed disclaimer for video mode */}
|
||||||
|
{inputMode === "video" && (
|
||||||
|
<div
|
||||||
|
className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700
|
||||||
|
p-2 text-xs mb-4 mt-1"
|
||||||
|
>
|
||||||
|
Code generation from videos can take 3-4 minutes. We do multiple
|
||||||
|
passes to get the best result. Please be patient.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
<Spinner />
|
||||||
|
{executionConsole.slice(-1)[0]}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CodePreview code={generatedCode} />
|
||||||
|
|
||||||
|
<div className="flex w-full">
|
||||||
|
<Button
|
||||||
|
onClick={cancelCodeGeneration}
|
||||||
|
className="w-full dark:text-white dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{appState === AppState.CODE_READY && (
|
||||||
|
<div>
|
||||||
|
<div className="grid w-full gap-2">
|
||||||
|
<Textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
placeholder="Tell the AI what to change..."
|
||||||
|
onChange={(e) => setUpdateInstruction(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
doUpdate(updateInstruction);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={updateInstruction}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between items-center gap-x-2">
|
||||||
|
<div className="font-500 text-xs text-slate-700 dark:text-white">
|
||||||
|
Include screenshot of current version?
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={shouldIncludeResultImage}
|
||||||
|
onCheckedChange={setShouldIncludeResultImage}
|
||||||
|
className="dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => doUpdate(updateInstruction)}
|
||||||
|
className="dark:text-white dark:bg-gray-700 update-btn"
|
||||||
|
>
|
||||||
|
Update <KeyboardShortcutBadge letter="enter" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-x-2 mt-2">
|
||||||
|
<Button
|
||||||
|
onClick={regenerate}
|
||||||
|
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700 regenerate-btn"
|
||||||
|
>
|
||||||
|
🔄 Regenerate
|
||||||
|
</Button>
|
||||||
|
{showSelectAndEditFeature && <SelectAndEditModeToggleButton />}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end items-center mt-2">
|
||||||
|
<TipLink />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reference image display */}
|
||||||
|
<div className="flex gap-x-2 mt-2">
|
||||||
|
{referenceImages.length > 0 && (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"scanning relative": appState === AppState.CODING,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{inputMode === "image" && (
|
||||||
|
<img
|
||||||
|
className="w-[340px] border border-gray-200 rounded-md"
|
||||||
|
src={referenceImages[0]}
|
||||||
|
alt="Reference"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{inputMode === "video" && (
|
||||||
|
<video
|
||||||
|
muted
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
className="w-[340px] border border-gray-200 rounded-md"
|
||||||
|
src={referenceImages[0]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 uppercase text-sm text-center mt-1">
|
||||||
|
{inputMode === "video" ? "Original Video" : "Original Screenshot"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="bg-gray-400 px-4 py-2 rounded text-sm">
|
||||||
|
<h2 className="text-lg mb-4 border-b border-gray-800">Console</h2>
|
||||||
|
{executionConsole.map((line, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border-b border-gray-400 mb-2 text-gray-600 font-mono"
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
@ -1,13 +1,34 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
|
||||||
// Store for app-wide state
|
// Store for app-wide state
|
||||||
interface AppStore {
|
interface AppStore {
|
||||||
|
appState: AppState;
|
||||||
|
setAppState: (state: AppState) => void;
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
updateInstruction: string;
|
||||||
|
setUpdateInstruction: (instruction: string) => void;
|
||||||
|
shouldIncludeResultImage: boolean;
|
||||||
|
setShouldIncludeResultImage: (shouldInclude: boolean) => void;
|
||||||
|
|
||||||
inSelectAndEditMode: boolean;
|
inSelectAndEditMode: boolean;
|
||||||
toggleInSelectAndEditMode: () => void;
|
toggleInSelectAndEditMode: () => void;
|
||||||
disableInSelectAndEditMode: () => void;
|
disableInSelectAndEditMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAppStore = create<AppStore>((set) => ({
|
export const useAppStore = create<AppStore>((set) => ({
|
||||||
|
appState: AppState.INITIAL,
|
||||||
|
setAppState: (state: AppState) => set({ appState: state }),
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
updateInstruction: "",
|
||||||
|
setUpdateInstruction: (instruction: string) =>
|
||||||
|
set({ updateInstruction: instruction }),
|
||||||
|
shouldIncludeResultImage: true,
|
||||||
|
setShouldIncludeResultImage: (shouldInclude: boolean) =>
|
||||||
|
set({ shouldIncludeResultImage: shouldInclude }),
|
||||||
|
|
||||||
inSelectAndEditMode: false,
|
inSelectAndEditMode: false,
|
||||||
toggleInSelectAndEditMode: () =>
|
toggleInSelectAndEditMode: () =>
|
||||||
set((state) => ({ inSelectAndEditMode: !state.inSelectAndEditMode })),
|
set((state) => ({ inSelectAndEditMode: !state.inSelectAndEditMode })),
|
||||||
|
|||||||
@ -16,6 +16,10 @@ interface ProjectStore {
|
|||||||
setGeneratedCode: (
|
setGeneratedCode: (
|
||||||
updater: string | ((currentCode: string) => string)
|
updater: string | ((currentCode: string) => string)
|
||||||
) => void;
|
) => void;
|
||||||
|
executionConsole: string[];
|
||||||
|
setExecutionConsole: (
|
||||||
|
updater: string[] | ((currentConsole: string[]) => string[])
|
||||||
|
) => void;
|
||||||
|
|
||||||
// Tracks the currently shown version from app history
|
// Tracks the currently shown version from app history
|
||||||
// TODO: might want to move to appStore
|
// TODO: might want to move to appStore
|
||||||
@ -44,6 +48,15 @@ export const useProjectStore = create<ProjectStore>((set) => ({
|
|||||||
generatedCode:
|
generatedCode:
|
||||||
typeof updater === "function" ? updater(state.generatedCode) : updater,
|
typeof updater === "function" ? updater(state.generatedCode) : updater,
|
||||||
})),
|
})),
|
||||||
|
executionConsole: [],
|
||||||
|
setExecutionConsole: (updater) =>
|
||||||
|
set((state) => ({
|
||||||
|
executionConsole:
|
||||||
|
typeof updater === "function"
|
||||||
|
? updater(state.executionConsole)
|
||||||
|
: updater,
|
||||||
|
})),
|
||||||
|
|
||||||
currentVersion: null,
|
currentVersion: null,
|
||||||
setCurrentVersion: (version) => set({ currentVersion: version }),
|
setCurrentVersion: (version) => set({ currentVersion: version }),
|
||||||
appHistory: [],
|
appHistory: [],
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user