Compare commits

...
7 changed files with 202 additions and 98 deletions
@@ -1,12 +1,7 @@
import { useEffect, useState } from "react";
import {
TransitionDialogue,
LoadingDots,
Label,
CloseButton,
} from "~/components";
import { LoadingDots } from "~/components";
import { Modal } from "@storyteller/ui-modal";
import { Label } from "@storyteller/ui-label";
import { UploadAssetError } from "../UploadModal/UploadAssetError";
import { UploadSuccess } from "../UploadModal/UploadSuccess";
import { UploadFiles3D } from "./UploadFiles3D";
@@ -19,18 +14,21 @@ import {
CHARACTER_MIXAMO_FILE_TYPE,
IMAGEPLANE_FILE_TYPE,
} from "~/enums";
import { assetModalVisible } from "~/pages/PageEnigma/signals";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
interface Props {
onClose: () => void;
onSuccess: () => void;
onSuccess: (category: FilterEngineCategories) => void;
isOpen: boolean;
title: string;
titleIcon: IconDefinition;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
preselectedCategory?: FilterEngineCategories;
isSelectVisible: boolean;
}
// Map categories to their allowed file types
@@ -43,16 +41,6 @@ const categoryFileTypes: Partial<Record<FilterEngineCategories, string[]>> = {
[FilterEngineCategories.LOCATION]: Object.values(OBJECT_FILE_TYPE),
[FilterEngineCategories.CREATURE]: Object.values(OBJECT_FILE_TYPE),
};
// Map engine categories to asset modal tabs
const categoryToAssetTab: Partial<Record<FilterEngineCategories, string>> = {
[FilterEngineCategories.OBJECT]: "objects",
[FilterEngineCategories.CHARACTER]: "character",
[FilterEngineCategories.IMAGE_PLANE]: "image-planes",
[FilterEngineCategories.LOCATION]: "sets",
[FilterEngineCategories.CREATURE]: "creatures",
};
// Excluded category options for the upload modal
const categoryOptions = Object.entries(FilterEngineCategories)
.filter(
@@ -82,12 +70,19 @@ export function UploadModal3D({
onClose,
onSuccess,
title,
titleIcon,
options,
}: Props) {
preselectedCategory,
isSelectVisible,
}: Props & { preselectedCategory?: FilterEngineCategories }) {
const [uploaderState, setUploaderState] =
useState<UploaderState>(initialUploaderState);
const [selectedCategory, setSelectedCategory] =
useState<FilterEngineCategories>(FilterEngineCategories.OBJECT);
useState<FilterEngineCategories>(
preselectedCategory !== undefined
? preselectedCategory
: FilterEngineCategories.OBJECT,
);
const updateUploaderState = (newState: UploaderState) => {
setUploaderState(newState);
@@ -103,30 +98,51 @@ export function UploadModal3D({
}
}, [isOpen]);
useEffect(() => {
if (preselectedCategory !== undefined) {
setSelectedCategory(preselectedCategory);
}
}, [preselectedCategory]);
useEffect(() => {
console.log("Preselected category:", preselectedCategory);
console.log("Initial selected category:", selectedCategory);
}, [preselectedCategory, selectedCategory]);
useEffect(() => {
if (uploaderState.status === UploaderStates.success) {
onSuccess(selectedCategory);
onClose();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uploaderState.status]);
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderStates.ready:
return (
<div className="space-y-4">
<div className="flex w-full flex-col">
<Label>Category</Label>
<Select
id="category-select"
options={categoryOptions}
value={selectedCategory}
onChange={(value) => {
if (
typeof value === "string" &&
Object.values(FilterEngineCategories).includes(
value as FilterEngineCategories,
)
) {
setSelectedCategory(value as FilterEngineCategories);
}
}}
placeholder="Select a category"
/>
</div>
{isSelectVisible && (
<div className="flex w-full flex-col">
<Label className="mb-1">Category</Label>
<Select
id="category-select"
options={categoryOptions}
value={selectedCategory}
onChange={(value) => {
if (
typeof value === "string" &&
Object.values(FilterEngineCategories).includes(
value as FilterEngineCategories,
)
) {
setSelectedCategory(value as FilterEngineCategories);
}
}}
placeholder="Select a category"
/>
</div>
)}
<UploadFiles3D
title={title}
engineCategory={selectedCategory}
@@ -146,31 +162,17 @@ export function UploadModal3D({
<div className="w-100 text-center opacity-50">Uploading...</div>
</>
);
case UploaderStates.success:
case UploaderStates.success: {
return (
<UploadSuccess
title="3D model"
onOk={() => {
onSuccess();
onClose();
// Store the selected category and tab in sessionStorage
const assetTab = categoryToAssetTab[selectedCategory];
if (assetTab) {
sessionStorage.setItem(
"lastUploadedCategory",
selectedCategory,
);
sessionStorage.setItem("lastUploadedTab", assetTab);
// Reopen the asset modal
assetModalVisible.value = true;
} else {
// Clear any existing stored values if we don't have a valid tab
sessionStorage.removeItem("lastUploadedCategory");
sessionStorage.removeItem("lastUploadedTab");
}
onSuccess(selectedCategory);
}}
/>
);
}
case UploaderStates.assetError:
return (
<UploadAssetError
@@ -199,15 +201,15 @@ export function UploadModal3D({
};
return (
<TransitionDialogue
<Modal
isOpen={isOpen}
onClose={onClose}
titleIcon={titleIcon}
title={title}
className="max-w-xl"
showClose={false}
showClose={true}
>
<CloseButton className="absolute right-4 top-4" onClick={onClose} />
<UploaderModalContent />
</TransitionDialogue>
</Modal>
);
}
@@ -29,8 +29,8 @@ interface Props {
}
const appTabs: TabItem[] = [
{ id: "2D", label: "2D" },
{ id: "3D", label: "3D" },
{ id: "2D", label: "2D" },
];
export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
@@ -171,12 +171,12 @@ export const PageEditor = () => {
}}
/> */}
<LoginModal
{/* <LoginModal
onClose={() => {}}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
openAiLogo="/resources/images/openai-logo.png"
/>
/> */}
</div>
);
};
@@ -1,7 +1,6 @@
import { Modal } from "@storyteller/ui-modal";
import { UploadModal3D } from "~/components/reusable/UploadModal3D";
import {
faPlus,
faSearch,
faChevronLeft,
faLayerGroup,
@@ -12,13 +11,13 @@ import {
faMountainCity,
faDog,
faImage,
faUpFromLine,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Button } from "@storyteller/ui-button";
import { CloseButton } from "@storyteller/ui-close-button";
import { Input } from "@storyteller/ui-input";
import { TabSelector } from "~/components/reusable/TabSelector";
import { TabSelector } from "@storyteller/ui-tab-selector";
import React, {
useState,
useEffect,
@@ -45,6 +44,7 @@ import { isAnyStatusFetching } from "../SidePanelTabs/utilities";
type AssetTab = {
id: string;
label: string;
labelSingle?: string;
icon: typeof faLayerGroup;
engineCategory?: FilterEngineCategories;
items: MediaItem[];
@@ -81,6 +81,21 @@ const AllTabSection = ({
</div>
);
const categoryToTabIdMap: Record<FilterEngineCategories, string> = {
[FilterEngineCategories.CHARACTER]: "characters",
[FilterEngineCategories.CREATURE]: "creatures",
[FilterEngineCategories.IMAGE_PLANE]: "image-planes",
[FilterEngineCategories.LOCATION]: "sets",
[FilterEngineCategories.OBJECT]: "objects",
[FilterEngineCategories.SKYBOX]: "skybox",
[FilterEngineCategories.ANIMATION]: "all",
[FilterEngineCategories.AUDIO]: "all",
[FilterEngineCategories.EXPRESSION]: "all",
[FilterEngineCategories.SCENE]: "all",
[FilterEngineCategories.SET_DRESSING]: "all",
[FilterEngineCategories.VIDEO_PLANE]: "all",
};
export const AssetModal = () => {
useSignals();
const [activeLibraryTab, setActiveLibraryTab] = useState("library");
@@ -88,6 +103,9 @@ export const AssetModal = () => {
const [searchTerm, setSearchTerm] = useState("");
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
const [selectedCategory, setSelectedCategory] =
useState<FilterEngineCategories | null>(null);
const [isSelectVisible, setIsSelectVisible] = useState(true);
const handleReopenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.checked;
@@ -101,6 +119,10 @@ export const AssetModal = () => {
assetModalVisible.value = false;
};
const handleOpen = () => {
assetModalVisible.value = true;
};
useEffect(() => {
if (assetModalVisible.value) {
// Check for stored category and tab from upload
@@ -116,7 +138,10 @@ export const AssetModal = () => {
const timer = setTimeout(() => {
searchInputRef.current?.focus();
}, 100);
return () => clearTimeout(timer);
return () => {
clearTimeout(timer);
// Ensure no side effects remain
};
}
return undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -232,7 +257,8 @@ export const AssetModal = () => {
{ id: "all", label: "All", icon: faLayerGroup, items: [] },
{
id: "character",
label: "Character",
label: "Characters",
labelSingle: "Character",
icon: faUser,
engineCategory: FilterEngineCategories.CHARACTER,
items:
@@ -243,6 +269,7 @@ export const AssetModal = () => {
{
id: "objects",
label: "Objects",
labelSingle: "Object",
icon: faCube,
engineCategory: FilterEngineCategories.OBJECT,
items:
@@ -253,6 +280,7 @@ export const AssetModal = () => {
{
id: "sets",
label: "Sets",
labelSingle: "Set",
icon: faMountainCity,
engineCategory: FilterEngineCategories.LOCATION,
items:
@@ -263,6 +291,7 @@ export const AssetModal = () => {
{
id: "creatures",
label: "Creatures",
labelSingle: "Creature",
icon: faDog,
engineCategory: FilterEngineCategories.CREATURE,
items:
@@ -273,12 +302,14 @@ export const AssetModal = () => {
{
id: "skybox",
label: "Skybox",
labelSingle: "Skybox",
icon: faSun,
items: activeLibraryTab === "library" ? demoSkyboxItems.value : [],
},
{
id: "image-planes",
label: "Image Planes",
labelSingle: "Image Plane",
icon: faImage,
engineCategory: FilterEngineCategories.IMAGE_PLANE,
items:
@@ -397,11 +428,20 @@ export const AssetModal = () => {
};
const handleAddAsset = () => {
assetModalVisible.value = false;
handleClose();
setIsUploadModalOpen(true);
setSelectedCategory(null);
setIsSelectVisible(true);
};
const handleUploadSuccess = () => {
const handleAddSpecificAsset = (category: FilterEngineCategories) => {
handleClose();
setIsUploadModalOpen(true);
setSelectedCategory(category);
setIsSelectVisible(false);
};
const handleUploadSuccess = (category: FilterEngineCategories) => {
if (reopenAfterDragSignal.value) {
// Small delay to allow the modal to close and reopen
setTimeout(() => {
@@ -410,10 +450,32 @@ export const AssetModal = () => {
} else {
handleClose();
}
// Use the provided category to find the AssetTab ID
const lastUploadedTabId = categoryToTabIdMap[category] || "all";
// Close the upload modal
setTimeout(() => {
sessionStorage.setItem("lastUploadedTab", lastUploadedTabId);
console.log("AssetModal: lastUploadedTabId set to:", lastUploadedTabId);
setIsUploadModalOpen(false);
handleOpen();
}, 300);
fetchUserCharacters();
fetchUserObjects();
fetchUserSets();
fetchUserCreatures();
fetchUserImagePlanes();
};
const clearSearch = () => setSearchTerm("");
const getArticle = (word: string | undefined) => {
if (!word) return "a";
return /^[aeiou]/i.test(word) ? "an" : "a";
};
return (
<>
<Modal
@@ -429,17 +491,31 @@ export const AssetModal = () => {
<h2 className="text-[18px] font-semibold opacity-80">
3D Assets
</h2>
<Tooltip content="Upload model" position="top" delay={200}>
{/* <Tooltip content="Upload model" position="top" delay={200}>
<Button
className="h-6 w-6 rounded-full border-none bg-transparent text-white/70 transition-colors hover:bg-transparent hover:text-white/100"
className="flex h-7 w-7 items-center justify-center rounded-md border-none bg-primary/50 text-white/70 transition-colors hover:bg-primary/70 hover:text-white/100"
onClick={handleAddAsset}
>
<FontAwesomeIcon icon={faPlus} className="text-xl" />
<FontAwesomeIcon icon={faPlus} className="text-lg" />
</Button>
</Tooltip>
</Tooltip> */}
</div>
<hr className="my-2 w-full border-white/10" />
<div className="space-y-2">
<div className="flex h-full flex-col space-y-2">
<Button
variant="primary"
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
"border-primary bg-primary/70 hover:bg-primary",
)}
onClick={handleAddAsset}
>
<FontAwesomeIcon
icon={faUpFromLine}
className="mr-2 text-lg opacity-70"
/>
Upload an asset
</Button>
{assetTabs.map((tab) => (
<Button
key={tab.id}
@@ -447,7 +523,7 @@ export const AssetModal = () => {
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
activeAssetTab === tab.id &&
"border-brand-primary bg-brand-primary/10 hover:bg-brand-primary/10",
"border-primary bg-primary/10 hover:bg-primary/10",
)}
onClick={() => setActiveAssetTab(tab.id)}
>
@@ -479,7 +555,7 @@ export const AssetModal = () => {
id="reopen-after-add"
checked={reopenAfterDragSignal.value}
onChange={handleReopenChange}
className="h-4 w-4 cursor-pointer rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary"
className="h-4 w-4 cursor-pointer rounded-lg border-gray-300 bg-gray-700 text-primary focus:ring-primary"
/>
<label
htmlFor="reopen-after-add"
@@ -529,18 +605,36 @@ export const AssetModal = () => {
)}
>
{activeAssetTab !== "all" && !searchTerm && (
<div className="mb-2 flex items-center font-semibold">
<Button
variant="secondary"
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
onClick={() => setActiveAssetTab("all")}
>
<FontAwesomeIcon
icon={faChevronLeft}
className="text-sm font-semibold opacity-70"
/>
</Button>
{currentTab.label}
<div className="mb-2 flex items-center justify-between font-semibold">
<div className="flex items-center">
<Button
variant="secondary"
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
onClick={() => setActiveAssetTab("all")}
>
<FontAwesomeIcon
icon={faChevronLeft}
className="text-sm font-semibold opacity-70"
/>
</Button>
{currentTab.label}
</div>
{activeAssetTab !== "skybox" &&
activeAssetTab !== "all" && (
<Button
icon={faUpFromLine}
onClick={() =>
handleAddSpecificAsset(
currentTab.engineCategory ||
FilterEngineCategories.OBJECT,
)
}
className="border-primary bg-primary/70 px-2 py-1 text-xs transition-colors hover:bg-primary"
>
Upload {getArticle(currentTab.labelSingle)}{" "}
{currentTab.labelSingle}
</Button>
)}
</div>
)}
{renderContent()}
@@ -551,10 +645,15 @@ export const AssetModal = () => {
</div>
</Modal>
<UploadModal3D
onClose={() => setIsUploadModalOpen(false)}
onSuccess={handleUploadSuccess}
onClose={() => {
setIsUploadModalOpen(false);
}}
onSuccess={(category) => handleUploadSuccess(category)}
isOpen={isUploadModalOpen}
title="Upload 3D Asset"
title={`Upload ${selectedCategory ? (selectedCategory.toLowerCase().replace(/_/g, " ") === "location" ? "sets" : selectedCategory.toLowerCase().replace(/_/g, " ")) : "3D Asset"}`}
titleIcon={!isSelectVisible ? currentTab.icon : faUpFromLine}
preselectedCategory={selectedCategory || FilterEngineCategories.OBJECT}
isSelectVisible={isSelectVisible}
/>
</>
);
@@ -23,7 +23,8 @@ export const Controls3D = () => {
useEffect(() => {
// Check if scene is empty and onboarding helper is not visible
const checkSceneEmpty = () => {
const isSceneEmpty = outlinerState.items.value.length === 0;
const isSceneEmpty =
outlinerState.items.value.length === 0 && !assetModalVisible.value;
setShowEmptySceneTooltip(isSceneEmpty);
};
@@ -37,7 +38,8 @@ export const Controls3D = () => {
return () => {
unsubscribe();
};
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assetModalVisible.value]);
const handleModeChange = (value: string) => {
selectedMode.value = value;
@@ -119,7 +121,7 @@ export const Controls3D = () => {
<Tooltip
content="Add 3D asset to scene (B)"
position="bottom"
delay={100}
delay={300}
closeOnClick
className={twMerge(
showEmptySceneTooltip ? "hidden" : "block",
@@ -209,7 +209,7 @@ export function LoginModal({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 cursor-pointer bg-black/60" />
<div className="fixed inset-0 cursor-pointer bg-black/80" />
</TransitionChild>
<div className="fixed inset-0 flex items-center justify-center p-4">
<TransitionChild
@@ -82,6 +82,7 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
className={({ selected }) =>
twMerge(
"relative z-20 mx-0.5 min-w-max rounded-md border-2 border-transparent px-4 py-0.5 text-center text-sm font-semibold transition-all duration-200 ease-in-out",
"focus-visible:outline-none focus-visible:ring-0",
selected ? "text-white" : "text-gray-300 hover:text-white",
disabled ? "cursor-not-allowed opacity-60" : ""
)