Compare commits

...
Author SHA1 Message Date
Michael Chung c8a9f0bc31 Merge branch 'main' into feature/undo-redo 2025-04-14 22:53:23 -04:00
Brandon Thomas d7365ea045 remove more entries from package-lock.json 2025-04-14 19:26:57 -04:00
Brandon Thomas 3386247858 remove pnpm-lock.yaml to unblock CI - it's causing new failures 2025-04-14 19:10:44 -04:00
Brandon Thomas 7ea9c96d77 remove dead references from editor3d package 2025-04-14 19:08:49 -04:00
Michael Chung 4afa753212 branch: undo-redo 2025-04-14 19:06:26 -04:00
Brandon Thomas 059f4b4e38 add pnpm to solve build issues 2025-04-14 18:56:05 -04:00
Brandon Thomas 3c36e041ca remove "workspace:*" URL. Not working on all machines? 2025-04-14 18:40:23 -04:00
Tanish MankuandTanish Manku f027fcb29a Write Gallery component with test data (#508)
Co-authored-by: Tanish Manku <-h>
2025-04-14 10:49:35 -07:00
BFlat 6f2ed9e61e dismiss onboarding helper and local storage 2025-04-14 14:57:33 +07:00
Brandon Thomas 2b40b2f8d6 add index on jobs anonymous visitor tokens 2025-04-14 02:58:49 -04:00
Brandon Thomas 9996b41bc2 Add nx library component and import it 2025-04-14 02:24:29 -04:00
ArEnScandMichael Chung b9a5768679 Feature/polling (#505)
add text back and toasters

Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-13 23:54:20 -04:00
Brandon Thomas 614a5edc55 cleanup netlify.toml files 2025-04-13 21:17:09 -04:00
Brandon Thomas 01468020bd cleanup build script 2025-04-13 21:06:24 -04:00
Brandon Thomas 289ef6f352 debug branch build (#507) 2025-04-13 21:01:09 -04:00
45 changed files with 2419 additions and 2561 deletions
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP INDEX `index_maybe_creator_anonymous_visitor_token`
ON `generic_inference_jobs`;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD INDEX index_maybe_creator_anonymous_visitor_token (maybe_creator_anonymous_visitor_token),
ALGORITHM=INPLACE, LOCK=NONE;
@@ -297,6 +297,7 @@ CREATE TABLE generic_inference_jobs (
KEY fk_maybe_input_source_token_and_type (maybe_input_source_token, maybe_input_source_token_type),
KEY fk_on_success_result_entity_token (on_success_result_entity_token),
KEY fk_maybe_creator_user_token (maybe_creator_user_token),
KEY index_maybe_creator_anonymous_visitor_token (maybe_creator_anonymous_visitor_token),
KEY index_creator_ip_address (creator_ip_address),
KEY index_priority_level (priority_level),
KEY index_is_debug_request (is_debug_request),
+1
View File
@@ -0,0 +1 @@
node_modules
+56
View File
@@ -0,0 +1,56 @@
{
"root": true,
"ignorePatterns": [
"**/*"
],
"plugins": [
"@nx"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": [
"*"
]
}
]
}
]
}
},
{
"files": [
"*.ts",
"*.tsx"
],
"extends": [
"plugin:@nx/typescript"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"extends": [
"plugin:@nx/javascript"
],
"rules": {}
}
]
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
}
+1 -2
View File
@@ -1,5 +1,5 @@
[build]
#command = "./script/netlify_build.sh"
command = "./apps/editor2d/script/netlify_build.sh"
publish = "apps/editor2d/dist/"
#[build.environment]
@@ -16,7 +16,6 @@ publish = "apps/editor2d/dist/"
for = "/build/*"
[headers.values]
"Cache-Control" = "public, max-age=31560000, immutable"
# [[redirects]]
@@ -15,6 +15,6 @@ cp _redirects dist/
echo "Copy netlify 404.html page into dist"
cp "404.html" dist/
echo "List files after build"
echo "List final files in build"
find dist/
@@ -0,0 +1,122 @@
import { faFolder } from "@fortawesome/pro-solid-svg-icons"
import { useEffect, useState } from "react";
import { Button } from "~/components/ui"
const testUrl = "https://api.fakeyou.com/v1/media_files/list/user/hanashi?include_user_uploads=true&page_size=24&filter_media_classes=unknown";
const getTestData = (callback: (data: any) => void) => {
const netCall = async () => {
// GET the test url
const testResults = await fetch(testUrl);
const testJson = await testResults.json();
console.log("testResults", testJson);
return testJson;
}
netCall().then(callback);
}
export const GalleryRootComponent = ({
className
}: {
className?: string
}) => {
const [testData, setTestData] = useState<any>();
useEffect(() => {
getTestData(setTestData);
}, [])
const aggregatedData = aggregateGalleryData({ data: testData });
return (
<div className={className}>
<div className="m-32">
<div className="flex w-fit gap-x-6 items-center">
<span className="text-white font-bold text-5xl">My Gallery</span>
<Button variant="secondary" icon={faFolder} className="h-8">
Open Folder
</Button>
</div>
<div className="overflow-y-auto mt-12 flex flex-col gap-y-8">
{Array.from(aggregatedData.entries()).map(([dateEpoch, results]) => (<GallerySection dateEpoch={dateEpoch} results={results} />))}
</div>
</div>
</div>
)
}
const GallerySection = ({
dateEpoch,
results
}: {
dateEpoch: number,
results: any[]
}) => {
const dateObj = new Date(dateEpoch);
const title = dateObj.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
return (
<div className="w-full">
<span className="text-lg">{title}</span>
<div className="flex gap-4 flex-wrap mt-4">
{results.map((result) => (<GalleryCard result={result} />))}
</div>
</div>
)
}
const GalleryCard = ({ result }: {
result: {
media_links: {
cdn_url: string,
maybe_thumbnail_template: string,
},
}
}) => {
return (
<div className="rounded-md size-48 flex items-center justify-center overflow-hidden">
<img src={result.media_links.cdn_url} alt="Thumbnail" className="h-full w-full object-cover" />
</div>
)
}
const aggregateGalleryData = ({
data
}: {
data?: {
results: any[]
}
}) => {
const aggregateMap = new Map<number, any[]>();
if (!data) {
return aggregateMap;
}
data.results.forEach((result) => {
if (result.media_type !== "image") {
return;
}
const dateString = result.updated_at as string;
const dateObj = new Date(dateString);
// Move it the start of the day for comparison
dateObj.setUTCHours(0, 0, 0, 0);
const dateEpoch = dateObj.getTime();
if (!aggregateMap.has(dateEpoch)) {
aggregateMap.set(dateEpoch, []);
}
aggregateMap.get(dateEpoch)!.push(result);
})
return aggregateMap;
}
@@ -0,0 +1 @@
export * from "./GalleryRootComponent";
+36 -19
View File
@@ -148,7 +148,6 @@ export class ApiManager {
formData.append("file", blob);
}
// console.log(`HELLO${formData}`);
return this.fetchMultipartFormData<T>(endpoint, {
method: "POST",
headers: {
@@ -327,7 +326,10 @@ export class Api extends ApiManager {
return result;
}
public async pollJobSession(jobToken: string): Promise<
public async pollJobSession(
jobToken: string,
thumbnailWidth: number = 256,
): Promise<
ApiResponse<{
job_token: string;
request: {
@@ -338,6 +340,9 @@ export class Api extends ApiManager {
progress_percentage: number;
};
result: {
image_url: string;
thumbnail_url: string;
public_bucket_path: string;
generated_images?: string[];
error?: string;
};
@@ -345,41 +350,53 @@ export class Api extends ApiManager {
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/job/${jobToken}`;
const response = await this.get<{
success?: boolean;
status?: string;
result?: {
generated_images?: string[];
error?: string;
};
BadInput?: string;
}>({
const response = await this.get<any>({
endpoint,
});
console.log("Job Polling Response:");
console.log(response);
let image_url = response.state.maybe_result?.media_links?.cdn_url;
let public_bucket_path =
response.state.maybe_result?.maybe_public_bucket_media_path;
let thumbnail_url =
response.state.maybe_result?.media_links?.maybe_thumbnail_template?.replace(
"{WIDTH}",
thumbnailWidth.toString(),
);
let progress_percentage = response.state.status.progress_percentage;
let status_string = response.state.status.status;
console.log("Image URL:", image_url);
console.log("Thumbnail URL:", thumbnail_url);
console.log("Progress Percentage:", progress_percentage);
console.log("Status", status_string);
console.log("response FROM JOBS");
console.log(response);
const success = response.success ?? false;
const status = response.status ?? "";
const result = response.result ?? {
generated_images: [],
error: undefined,
};
const errorMessage = response.BadInput;
return {
success,
data: {
result,
result: {
image_url: image_url || "",
thumbnail_url: thumbnail_url || "",
public_bucket_path: public_bucket_path || "",
error: undefined,
},
job_token: jobToken,
request: {
maybe_model_title: "Image Generation",
},
status: {
status: status.toLowerCase(),
progress_percentage: 0,
status: status_string.toLowerCase(),
progress_percentage: progress_percentage,
},
},
errorMessage,
@@ -55,7 +55,7 @@ export class Engine {
private uiLayer: Konva.Layer;
private previewLayer: Konva.Layer;
private offScreenCanvas: OffscreenCanvas;
private realTimeDrawEngine: RealTimeDrawEngine;
public realTimeDrawEngine: RealTimeDrawEngine;
private nodesManager: NodesManager;
private nodeIsolator: NodeIsolator;
private nodeTransformer: NodeTransformer;
@@ -66,8 +66,8 @@ export class Engine {
private backgroundNode: ShapeNode | null = null;
private sceneManager: SceneManager;
private undoStackManager: UndoStackManager;
private commandManager: CommandManager;
public undoStackManager: UndoStackManager;
public commandManager: CommandManager;
private videoExtractionHandler: VideoExtractionHandler;
public segmentationButtonCanBePressed: boolean = true;
@@ -1,14 +1,14 @@
import { useCallback, useRef } from "react";
import { UndoRedo } from "~/components/reusable/UndoRedoController/UndoRedo";
// Components
import { KonvaCanvasContainer } from "./KonvaCanvasContainer";
import { ContextualButtonRetry } from "./ContextualButtonRetry";
import { ContextualToolbarNode } from "./ContextualToolbarNode";
import { ContextualLoadingBar } from "./ContextualLoadingBar";
import { SignaledCanvasDragDropFiles } from "./SignaledCanvasDragDropFiles";
import { SignaledDialogs } from "./SignaledDialogs";
import { SignaledToolbarMain } from "./SignaledToolbarMain";
import { SignaledToolbarVideoExtraction } from "./SignaledToolbarVideoExtraction";
import { PromptBox } from "~/components/PromptBox";
// The KonvaApp is the root of the Konva stage
// and only entry point for anything in Konva JS
@@ -22,9 +22,6 @@ import { useLayoutContext } from "./contextSignals/layout";
// common hooks
import { useRenderCounter } from "~/hooks/useRenderCounter";
import { useNavigate } from "react-router-dom";
import { SignaledMagicBox } from "./SignaledMagicBox";
import { SignaledPromptSlider } from "./SignaledPromptSlider";
import { SignaledPromptText } from "./SignaledPromptText";
export const KonvaRootComponent = ({
className,
@@ -53,11 +50,13 @@ export const KonvaRootComponent = ({
return (
<>
<UndoRedo>
<KonvaCanvasContainer
ref={konvaContainerCallbackRef}
className={className}
// retreive the classNames from the parent for sizing/styling
/>
{/* <SignaledMagicBox /> */}
<SignaledCanvasDragDropFiles
openAddImage={appUiContext.openAddImage}
@@ -77,6 +76,7 @@ export const KonvaRootComponent = ({
{/* <ContextualLoadingBar /> */}
<ContextualToolbarNode />
{/* <ContextualButtonRetry /> */}
</UndoRedo>
</>
);
};
@@ -17,6 +17,7 @@ import { EditModeRootComponent } from "~/EditModeRootComponent/EditModeRootCompo
import { useRef } from "react";
import { EngineType } from "~/KonvaApp";
import { GenerationEngine } from "~/KonvaApp/GenerationEngine";
import { GalleryRootComponent } from "~/GalleryRootComponent";
export const Main = withProtectionRoute(() => {
// This is a hook that will log the number of times the component has rerendered
@@ -29,7 +30,6 @@ export const Main = withProtectionRoute(() => {
useSignals();
const realtimeEngineRef = useRef<EngineType | null>(null);
const generationEngineRef = useRef<GenerationEngine | null>(null);
const appModeValue = appMode.value;
@@ -59,8 +59,10 @@ export const Main = withProtectionRoute(() => {
}
return (
<div className="fixed grid h-full w-full grid-cols-12 grid-rows-12">
{childView}
<div className="fixed h-full w-full">
<div className="fixed grid h-full w-full grid-cols-12 grid-rows-12">
{childView}
</div>
<div className="absolute top-0 w-full p-3.5">
<div className="relative flex w-full">
<div className="absolute left-0">
@@ -74,6 +76,8 @@ export const Main = withProtectionRoute(() => {
</div>
</div>
</div>
{appModeValue === "gallery" &&
(<GalleryRootComponent className="fixed h-full w-full z-1 bg-black" />)}
</div>
);
});
@@ -1,8 +1,10 @@
import React, { createContext, useContext, useState } from "react";
interface JobContextType {
jobToken: string | null;
setJobToken: (token: string | null) => void;
jobTokens: string[];
addJobToken: (token: string) => void;
removeJobToken: (token: string) => void;
clearJobTokens: () => void;
}
const JobContext = createContext<JobContextType | undefined>(undefined);
@@ -10,10 +12,24 @@ const JobContext = createContext<JobContextType | undefined>(undefined);
export const JobProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [jobToken, setJobToken] = useState<string | null>(null);
const [jobTokens, setJobTokens] = useState<string[]>([]);
const addJobToken = (token: string) => {
setJobTokens((prevTokens) => [...prevTokens, token]);
};
const removeJobToken = (token: string) => {
setJobTokens((prevTokens) => prevTokens.filter((t) => t !== token));
};
const clearJobTokens = () => {
setJobTokens([]);
};
return (
<JobContext.Provider value={{ jobToken, setJobToken }}>
<JobContext.Provider
value={{ jobTokens, addJobToken, removeJobToken, clearJobTokens }}
>
{children}
</JobContext.Provider>
);
@@ -47,7 +47,8 @@ export const PromptBox = () => {
const isDesktopApp = window.navigator.userAgent.includes("Tauri");
console.log("Is this a desktop app?", isDesktopApp);
const { jobToken, setJobToken } = useJobContext();
const { jobTokens, addJobToken, removeJobToken, clearJobTokens } =
useJobContext();
//const { lastRenderedBitmap } = useCanvasSignal();
const [prompt, setPrompt] = useState("");
@@ -119,7 +120,7 @@ export const PromptBox = () => {
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId),
);
console.log("Reference image added:", referenceImage);
toast.success("Reference image added.");
} else if (
newState.status === UploaderStates.assetError ||
newState.status === UploaderStates.imageCreateError
@@ -127,7 +128,8 @@ export const PromptBox = () => {
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId),
);
console.error("Upload failed");
toast.error("Upload failed. Please try again.");
}
},
});
@@ -206,7 +208,7 @@ export const PromptBox = () => {
const api = new Api();
let image = getCanvasRenderBitmap();
if (image === undefined) {
console.error(
toast.error(
"Error: Unable to generate image. Please check the input and try again.",
);
return;
@@ -229,7 +231,9 @@ export const PromptBox = () => {
});
if (snapshotMediaToken.data === undefined) {
console.error("Error: Unable to upload scene snapshot.");
toast.error(
"Error: Unable to upload scene snapshot Please try again.",
);
return;
}
@@ -242,17 +246,13 @@ export const PromptBox = () => {
});
if (response.success === true) {
console.error("Successfully enqueued image generation.");
toast.success("Please wait while we process your image.");
if (response.data) {
console.log("Response:", response.data);
setJobToken(response.data);
addJobToken(response.data);
}
return;
} else {
console.log("Image generation Not Successful.");
toast.error("Failed to generate image. Please try again.");
toast.error("Failed to enqueue image generation. Please try again.");
}
}
} catch (error) {
@@ -63,12 +63,11 @@ export const ToolbarMain = ({
buttonProps={buttonProps.SELECT}
tooltip="Select"
/>
{/* <ToolbarButton
<ToolbarButton
icon={faText}
buttonProps={buttonProps.ADD_TEXT}
tooltip="Add Text"
/> */}
/>
<Popover className="relative">
{({ close }) => (
<>
@@ -1,4 +1,5 @@
import { twMerge } from "tailwind-merge";
import { Login } from "~/app/pages/login";
import { Activity } from "~/components/ui/Activity/Activity";
// import { paperWrapperStyles } from "~/components/styles";
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
@@ -0,0 +1,58 @@
import { useRef } from "react";
import { useCallback, useContext, ReactNode } from "react";
import { EngineType } from "~/KonvaApp";
import { Engine } from "~/KonvaApp/Engine";
interface UndoRedoProps {
children: ReactNode;
}
/**
* A transparent component that provides undo/redo functionality
* with keyboard shortcuts for macOS (Command+Z, Command+Shift+Z)
*/
export const UndoRedo = ({
children,
}: UndoRedoProps) => {
const engineRef = useRef<EngineType | null>(null);
// Set up keyboard shortcuts for macOS
useCallback(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const engine = engineRef.current;
if (!engine) {
console.error('Engine reference is not available for undo/redo operations');
return;
}
// Command+Z for Undo
// Ctrl+Shift+Z for Redo (Windows/Linux alternative)
if (e.ctrlKey && e.shiftKey && e.key === 'z') {
e.preventDefault();
}
// Delete key handling
if ((e.key === 'Delete' || e.key === 'Backspace')) {
e.preventDefault();
engine.commandManager.deleteNodes();
}
if (e.metaKey && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
engine.undoStackManager.undo();
}
// Command+Shift+Z for Redo
if (e.metaKey && e.key === 'z' && e.shiftKey) {
e.preventDefault();
engine.undoStackManager.undo();
engine.realTimeDrawEngine.render();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [engineRef]);
return children;
};
@@ -6,6 +6,8 @@ import { CompletedCard } from "./CompletedCard";
import { InProgressCard } from "./InProgressCard";
import { useJobContext } from "~/components/JobContext";
import { Api } from "~/KonvaApp/Api";
import { toast } from "sonner";
// Dummy data types
interface CompletedItem {
token: string;
@@ -65,90 +67,72 @@ const dummyCompletedItems: CompletedItem[] = [
// ];
export function Activity() {
const { jobToken, setJobToken } = useJobContext();
const { jobTokens, addJobToken, removeJobToken, clearJobTokens } =
useJobContext();
const [completedItems, setCompletedItems] = useState<CompletedItem[]>([]);
const [jobs, setJobs] = useState<ActiveJob[]>([]);
const [loading, setLoading] = useState(true);
// Dummy polling function
useEffect(() => {
const pollData = async () => {
if (jobToken) {
console.log("Current Job Token:", jobToken);
if (jobTokens.length > 0) {
console.log("Current Job Tokens:", jobTokens);
const api = new Api();
const job = await api.pollJobSession(jobToken);
console.log("Job response:", job);
if (job.success && job.data) {
const { status, progress_percentage } = job.data.status;
const { job_token } = job.data;
// Create a map to store the latest job data
const updatedJobs = new Map<string, ActiveJob>();
console.log("Job status:", status);
console.log("Progress percentage:", progress_percentage);
console.log("Job token:", job_token);
for (const jobToken of jobTokens) {
const job = await api.pollJobSession(jobToken);
// Update jobs with real job data
if (
status.toLowerCase() === "completed" ||
status.toLowerCase() === "failed"
) {
// If job is completed or failed, remove it from active jobs
setJobs((prevJobs) =>
prevJobs.filter((j) => j.job_token !== job_token),
);
if (job.success && job.data) {
const { status, progress_percentage } = job.data.status;
const { job_token } = job.data;
// Add to completed items if it was successful
if (
status.toLowerCase() === "completed" &&
job.data.result.generated_images
status.toLowerCase() === "complete_success" ||
status.toLowerCase() === "failed"
) {
const newCompletedItem: CompletedItem = {
id: job_token,
title: job.data.request.maybe_model_title || "Image Generation",
timestamp: new Date().toISOString(),
type: "image",
imageUrl: job.data.result.generated_images[0] || "",
};
// If job is complete_success or failed, we'll remove it later
removeJobToken(job_token);
setCompletedItems((prev) => [newCompletedItem, ...prev]);
// Add to complete_success items if it was successful
if (status.toLowerCase() === "complete_success") {
const newCompletedItem: CompletedItem = {
token: job_token,
maybe_title:
job.data.request.maybe_model_title || "Image Generation",
public_bucket_path: job.data.result.image_url || "",
updated_at: new Date().toISOString(),
};
setCompletedItems((prev) => [newCompletedItem, ...prev]);
toast.success("Done Processing Image");
}
} else {
// Store the job in our map
const updatedJob: ActiveJob = {
job_token,
request: {
maybe_model_title:
job.data.request.maybe_model_title || "Image Generation",
},
status: {
status: status.toUpperCase(),
progress_percentage: progress_percentage || 0,
},
};
updatedJobs.set(job_token, updatedJob);
}
} else {
// Update or add the job to the active jobs list
const updatedJob: ActiveJob = {
job_token,
request: {
maybe_model_title:
job.data.request.maybe_model_title || "Image Generation",
},
status: {
status: status.toUpperCase(),
progress_percentage: progress_percentage || 0,
},
};
setJobs((prevJobs) => {
const existingJobIndex = prevJobs.findIndex(
(j) => j.job_token === job_token,
);
if (existingJobIndex >= 0) {
// Update existing job
const newJobs = [...prevJobs];
newJobs[existingJobIndex] = updatedJob;
return newJobs;
} else {
// Add new job
return [updatedJob, ...prevJobs];
}
});
console.error("Failed to fetch job status:", job.errorMessage);
}
} else {
console.error("Failed to fetch job status:", job.errorMessage);
}
} else {
console.log("No Job Token available.");
}
if (jobs.length === 0) {
// Update jobs state with the deduplicated jobs
setJobs(Array.from(updatedJobs.values()));
}
if (jobTokens.length === 0) {
setJobs([]);
setLoading(false);
}
};
@@ -163,7 +147,7 @@ export function Activity() {
}, 5000);
return () => clearInterval(interval);
}, [jobToken]);
}, [jobTokens, removeJobToken]);
return (
<PopoverMenu
@@ -17,8 +17,10 @@ interface Props {
export function CompletedCard({ job }: Props) {
const [loadError, setLoadError] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const imageUrl = job.public_bucket_path + "-thumb.gif";
const imageUrl = job.public_bucket_path;
const fullImageUrl = job.public_bucket_path;
console.log("I am hjere");
console.log(job);
return (
<>
@@ -15,7 +15,7 @@ export const CloseButton = ({
}: CloseButtonProps) => {
const sizeClasses = {
sm: "h-5 w-5 text-sm",
md: "h-7 w-7 text-lg",
md: "h-7 w-7 text-md",
lg: "h-9 w-9 text-xl",
};
@@ -1,10 +1,41 @@
import { useState, useEffect } from "react";
import { Transition } from "@headlessui/react";
import { Key, KeyGroup, Mouse, Plus } from "./ControlsTopButtons/Help/Help";
import { CloseButton } from "~/components";
const STORAGE_KEY = "onboarding_helper_dismissed";
const EXPIRATION_DAYS = 3;
export const OnboardingHelper = () => {
const [actionPerformed, setActionPerformed] = useState(false);
const [keyPressed, setKeyPressed] = useState(false);
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
// Check if the helper was dismissed
const dismissedData = localStorage.getItem(STORAGE_KEY);
if (dismissedData) {
const { timestamp } = JSON.parse(dismissedData);
const expirationTime = timestamp + EXPIRATION_DAYS * 24 * 60 * 60 * 1000; // 3 days in milliseconds
if (Date.now() < expirationTime) {
setIsVisible(false);
} else {
// Clear expired data
localStorage.removeItem(STORAGE_KEY);
}
}
}, []);
const handleClose = () => {
setIsVisible(false);
// Store dismissal timestamp
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
timestamp: Date.now(),
})
);
};
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -30,7 +61,7 @@ export const OnboardingHelper = () => {
return (
<Transition
show={!(actionPerformed && keyPressed)}
show={isVisible && !(actionPerformed && keyPressed)}
enter="transition-opacity duration-500"
enterFrom="opacity-0"
enterTo="opacity-100"
@@ -38,7 +69,7 @@ export const OnboardingHelper = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="glass pointer-events-none absolute left-1/2 top-48 z-10 flex -translate-x-1/2 transform items-center justify-center gap-2 rounded-2xl border-2 border-brand-primary !bg-brand-primary/10 px-5 py-4">
<div className="glass pointer-events-none absolute left-1/2 top-48 z-10 flex -translate-x-1/2 transform items-center justify-center gap-2 rounded-2xl border-2 border-brand-primary !bg-brand-primary/10 px-5 py-4">
<div className="flex items-center">
<p className="pr-3 text-lg font-semibold">Hold</p>
<Mouse button="left" />
@@ -56,6 +87,9 @@ export const OnboardingHelper = () => {
</div>
</div>
<p className="text-lg font-semibold">to move around the scene</p>
<div className="pointer-events-auto ml-2">
<CloseButton onClick={handleClose} />
</div>
</div>
</Transition>
);
+3
View File
@@ -16,6 +16,7 @@ import normalizeCss from "./styles/normalize.css?url";
import tailwindCss from "./styles/tailwind.css?url";
import baseCss from "./styles/base.css?url";
import { Environment, Configs } from "./configs";
import { api } from "@storyteller/api";
// The following import prevents a Font Awesome icon server-side rendering bug,
// where the icons flash from a very large icon down to a properly sized one:
@@ -71,6 +72,8 @@ export async function loader() {
uploadApiVideo = "http://localhost:12345";
break;
}
console.log(">>>> API LIBRARY IMPORT TEST", api());
const env = {
// @ts-expect-error ProvessEnv is correct
+4 -6
View File
@@ -1,15 +1,14 @@
[build]
#command = "./script/netlify_build.sh"
#publish = "build/client"
command = "./apps/editor3d/script/netlify_build.sh"
publish = "apps/editor3d/dist/"
[build.environment]
REACT_APP_PUBLIC_POSTHOG_KEY = "phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
REACT_APP_PUBLIC_POSTHOG_UI = "https://us.i.posthog.com"
[dev]
command = "npm run dev"
framework = "vite"
#[dev]
#command = "npm run dev"
#framework = "vite"
# Set immutable caching for static files, because they have fingerprinted filenames
@@ -17,7 +16,6 @@ framework = "vite"
for = "/build/*"
[headers.values]
"Cache-Control" = "public, max-age=31560000, immutable"
[[redirects]]
+1 -2
View File
@@ -21,6 +21,7 @@
"test": "jest"
},
"dependencies": {
"@storyteller/api": "file:../../libs/api",
"@awesome.me/kit-fde2be5eb0": "^1.0.7",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
@@ -44,14 +45,12 @@
"@zip.js/zip.js": "^2.7.51",
"ammo.js": "^0.0.10",
"ccapture.js": "github:aceslowman/ccapture.js#npm-fix",
"common": "file:common",
"csstype": "^3.1.3",
"dayjs": "^1.11.10",
"deep-equal": "^2.2.3",
"detect-gpu": "^5.0.38",
"http-proxy-middleware": "^3.0.0",
"isbot": "^4.1.0",
"kalidokit": "file:./vendor/kalidokit",
"lodash.debounce": "^4.0.8",
"posthog-js": "^1.141.0",
"react": "^18.2.0",
+10 -32
View File
@@ -2,48 +2,26 @@
set -euxo pipefail
# echo "Run build script"
# npm install
# npm run build
#
# #echo "Emit built files."
# #pwd
# #find .
#
# # echo "Copy test files into dist"
# # cp -r test/* dist/
# #
# # echo "Copy netlify configs into dist"
# # cp _headers dist/
# # cp _redirects dist/
# #
# # echo "Copy netlify 404.html page into dist"
# # cp "404.html" dist/
# #
# # echo "List files after build"
# # find dist/
echo "Run build script (TODO: Make strict)"
echo "Build Editor 3D"
nx build editor3d
echo "Change to project dir"
pushd apps/editor3d/
echo "List directory files"
pwd
ls -lA .
echo "List build directory files"
pwd
ls -lA build/
#echo "Copy netlify configs into dist"
#cp _headers dist/
#cp _redirects dist/
echo "Make output directory"
mkdir dist/
#echo "Copy netlify 404.html page into dist"
echo "Copy files to putput directory"
cp -r build/server/* dist/
cp -r build/client/* dist/
cp _headers dist/
#cp _redirects dist/
#cp "404.html" dist/
echo "List files after build"
echo "List final files in build"
find dist/
+120
View File
@@ -0,0 +1,120 @@
{
"success": true,
"jobs": [
{
"created_at": "2024-03-20T10:15:30Z",
"job_token": "job_123456789",
"maybe_result": {
"entity_token": "entity_987654321",
"entity_type": "image",
"maybe_public_bucket_media_path": "https://storage.example.com/media/image123.jpg",
"maybe_successfully_completed_at": "2024-03-20T10:20:45Z",
"media_links": {
"cdn_url": "https://cdn.example.com/media/image123.jpg",
"maybe_thumbnail_template": "https://cdn.example.com/media/image123_{WIDTH}.jpg",
"maybe_video_previews": null
}
},
"request": {
"inference_category": "image_generation",
"maybe_model_title": "Stable Diffusion v2.1",
"maybe_model_token": "model_abc123",
"maybe_model_type": "diffusion",
"maybe_raw_inference_text": null,
"maybe_style_name": "anime"
},
"status": {
"attempt_count": 1,
"maybe_assigned_cluster": "cluster-1",
"maybe_assigned_worker": "worker-xyz",
"maybe_current_execution_duration_seconds": 315,
"maybe_extra_status_description": "Processing at 85%",
"maybe_failure_category": null,
"maybe_first_started_at": "2024-03-20T10:15:35Z",
"progress_percentage": 85,
"requires_keepalive": false,
"status": "running"
},
"updated_at": "2024-03-20T10:20:45Z"
},
{
"created_at": "2024-03-20T09:30:15Z",
"job_token": "job_987654321",
"maybe_result": {
"entity_token": "entity_123456789",
"entity_type": "video",
"maybe_public_bucket_media_path": "https://storage.example.com/media/video456.mp4",
"maybe_successfully_completed_at": "2024-03-20T09:45:30Z",
"media_links": {
"cdn_url": "https://cdn.example.com/media/video456.mp4",
"maybe_thumbnail_template": null,
"maybe_video_previews": {
"animated": {
"url": "https://cdn.example.com/previews/video456_animated.mp4"
},
"animated_thumbnail_template": [
"https://cdn.example.com/previews/video456_animated_{WIDTH}.jpg"
],
"still": {
"url": "https://cdn.example.com/previews/video456_still.jpg"
},
"still_thumbnail_template": [
"https://cdn.example.com/previews/video456_still_{WIDTH}.jpg"
]
}
}
},
"request": {
"inference_category": "video_generation",
"maybe_lipsync_details": {
"face_driver_media_file_token": "media_face123",
"source_media_file_token": "media_source456"
},
"maybe_model_title": "Video Diffusion Model",
"maybe_model_token": "model_def456",
"maybe_model_type": "video_diffusion",
"maybe_raw_inference_text": null,
"maybe_style_name": "realistic"
},
"status": {
"attempt_count": 1,
"maybe_assigned_cluster": "cluster-2",
"maybe_assigned_worker": "worker-abc",
"maybe_current_execution_duration_seconds": null,
"maybe_extra_status_description": null,
"maybe_failure_category": null,
"maybe_first_started_at": "2024-03-20T09:30:20Z",
"progress_percentage": 100,
"requires_keepalive": false,
"status": "completed"
},
"updated_at": "2024-03-20T09:45:30Z"
},
{
"created_at": "2024-03-20T11:00:00Z",
"job_token": "job_456789123",
"maybe_result": null,
"request": {
"inference_category": "text_to_speech",
"maybe_model_title": "TTS Model v3",
"maybe_model_token": "model_ghi789",
"maybe_model_type": "tts",
"maybe_raw_inference_text": "Hello, this is a sample text for TTS conversion.",
"maybe_style_name": null
},
"status": {
"attempt_count": 2,
"maybe_assigned_cluster": "cluster-3",
"maybe_assigned_worker": null,
"maybe_current_execution_duration_seconds": null,
"maybe_extra_status_description": "Failed to process audio",
"maybe_failure_category": "processing_error",
"maybe_first_started_at": "2024-03-20T11:00:05Z",
"progress_percentage": 0,
"requires_keepalive": true,
"status": "failed"
},
"updated_at": "2024-03-20T11:15:00Z"
}
]
}
+50
View File
@@ -0,0 +1,50 @@
{
"extends": [
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.json"
],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
"{projectRoot}/vite.config.{js,ts,mjs,mts}"
]
}
]
}
}
]
}
+11
View File
@@ -0,0 +1,11 @@
# api
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build api` to build the library.
## Running unit tests
Run `nx test api` to execute the unit tests via [Vitest](https://vitest.dev/).
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@storyteller/api",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"dependencies": {}
}
+1
View File
@@ -0,0 +1 @@
export * from './lib/api.js';
+7
View File
@@ -0,0 +1,7 @@
import { api } from './api.js';
describe('api', () => {
it('should work', () => {
expect(api()).toEqual('api');
})
})
+3
View File
@@ -0,0 +1,3 @@
export function api(): string {
return 'api library built and imported!';
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+35
View File
@@ -0,0 +1,35 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"emitDeclarationOnly": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"forceConsistentCasingInFileNames": true,
"types": [
"node",
"vite/client"
]
},
"include": [
"src/**/*.ts"
],
"references": [],
"exclude": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
]
}
+36
View File
@@ -0,0 +1,36 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"module": "nodenext",
"moduleResolution": "nodenext",
"forceConsistentCasingInFileNames": true
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
+48
View File
@@ -0,0 +1,48 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/api',
plugins: [dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@frontend/api',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: []
},
},
test: {
watch: false,
globals: true,
environment: 'node',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+17 -1
View File
@@ -1,5 +1,6 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"npmScope": "storyteller",
"namedInputs": {
"default": [
"{projectRoot}/**/*",
@@ -9,7 +10,9 @@
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/src/test-setup.[jt]s"
"!{projectRoot}/src/test-setup.[jt]s",
"!{projectRoot}/.eslintrc.json",
"!{projectRoot}/eslint.config.mjs"
],
"sharedGlobals": []
},
@@ -65,6 +68,12 @@
"watchDepsTargetName": "watch-deps",
"staticServeTargetName": "static-serve"
}
},
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "eslint:lint"
}
}
],
"generators": {
@@ -83,5 +92,12 @@
"linter": "none"
}
}
},
"targetDefaults": {
"test": {
"dependsOn": [
"^build"
]
}
}
}
+1587 -2393
View File
File diff suppressed because it is too large Load Diff
+12 -1
View File
@@ -7,11 +7,14 @@
"dependencies": {
"@preact/signals-react": "^3.0.1",
"@remix-run/serve": "^2.15.0",
"pnpm": "^10.8.1",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@netlify/remix-edge-adapter": "^3.4.3",
"@nx/eslint": "20.7.2",
"@nx/eslint-plugin": "20.7.2",
"@nx/js": "20.7.2",
"@nx/react": "20.7.2",
"@nx/remix": "^20.7.2",
@@ -26,20 +29,28 @@
"@types/node": "^20.0.0",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@vitejs/plugin-react": "^4.2.0",
"@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.0",
"eslint": "~8.57.0",
"eslint-config-prettier": "^10.0.0",
"jiti": "2.4.2",
"jsdom": "~22.1.0",
"jsonc-eslint-parser": "^2.1.0",
"nx": "20.7.2",
"tslib": "^2.3.0",
"typescript": "~5.7.2",
"vite": "^6.0.0",
"vite-plugin-banner": "^0.8.0",
"vite-plugin-dts": "~4.5.0",
"vite-plugin-static-copy": "^2.3.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.0"
},
"workspaces": [
"apps/*"
"apps/*",
"libs/*"
]
}
+4
View File
@@ -0,0 +1,4 @@
packages:
- 'apps/*'
- 'libs/*'
+3
View File
@@ -8,6 +8,9 @@
},
{
"path": "./apps/editor3d"
},
{
"path": "./libs/api"
}
]
}
+1
View File
@@ -0,0 +1 @@
export default ['**/vite.config.{mjs,js,ts,mts}', '**/vitest.config.{mjs,js,ts,mts}'];