Compare commits

...
Author SHA1 Message Date
bflatastic 2b6890fa12 activity popover for generations 2025-04-04 18:28:38 +07:00
10 changed files with 162 additions and 32 deletions
@@ -28,7 +28,7 @@ export const Button = ({
function getVariantClassNames(variant: string) {
switch (variant) {
case "secondary": {
return "bg-brand-secondary hover:bg-brand-secondary-900 text-white focus-visible:outline-brand-secondary";
return "bg-brand-secondary hover:bg-brand-secondary-700 text-white focus-visible:outline-brand-secondary";
}
case "action": {
return "bg-action hover:bg-action-900 text-white focus-visible:outline-action";
@@ -25,6 +25,8 @@ interface PopoverMenuProps {
panelClassName?: string;
onAction?: (action: string) => void;
panelTitle?: string;
position?: "top" | "bottom";
align?: "start" | "center" | "end";
}
export const PopoverMenu = ({
@@ -38,8 +40,11 @@ export const PopoverMenu = ({
triggerLabel,
children,
buttonClassName,
panelClassName,
onAction,
panelTitle,
position = "top",
align = "start",
}: PopoverMenuProps) => {
const selectedItem = items.find((item) => item.selected);
@@ -61,6 +66,17 @@ export const PopoverMenu = ({
buttonClassName,
);
const positionClasses = {
top: "bottom-full",
bottom: "top-full",
};
const alignClasses = {
start: "left-0",
center: "left-1/2 -translate-x-1/2",
end: "right-0",
};
return (
<Popover className="relative">
{({ close }) => (
@@ -71,19 +87,40 @@ export const PopoverMenu = ({
{mode === "default" && triggerLabel ? triggerLabel : null}
</Popover.Button>
<div className="absolute bottom-full left-0 transform-gpu">
<div
className={twMerge(
"absolute transform-gpu",
positionClasses[position],
alignClasses[align],
)}
>
<Transition
enter="transition duration-100 ease-out"
enterFrom="translate-y-2 opacity-0"
enterFrom={
position === "bottom"
? "translate-y-2 opacity-0"
: "-translate-y-2 opacity-0"
}
enterTo="translate-y-0 opacity-100"
leave="transition duration-100 ease-in"
leaveFrom="translate-y-0 opacity-100"
leaveTo="translate-y-2 opacity-0"
leaveTo={
position === "bottom"
? "translate-y-2 opacity-0"
: "-translate-y-2 opacity-0"
}
>
<Popover.Panel className="origin-bottom">
<Popover.Panel
className={twMerge(
position === "bottom" ? "origin-top" : "origin-bottom",
)}
>
<div
className={twMerge(
"z-10 mb-2 min-w-48 rounded-lg bg-[#46464B] p-1.5 shadow-lg",
"z-10 min-w-48 rounded-lg bg-[#46464B] p-1.5 shadow-lg",
position === "top" && "mb-2",
position === "bottom" && "mt-2",
panelClassName,
)}
>
{panelTitle && (
@@ -1,10 +1,10 @@
import { useLocation, useParams } from "@remix-run/react";
import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons";
import { ButtonLink } from "~/components";
import { faChevronLeft, faImages } from "@fortawesome/pro-solid-svg-icons";
import { Button, ButtonLink } from "~/components";
import { AuthButtons } from "./AuthButtons";
import { SceneTitleInput } from "./SceneTitleInput";
import { MyMoviesButton } from "./MyMoviesButton";
import { getCurrentLocationWithoutParams } from "~/utilities";
import { Activity } from "~/pages/PageEnigma/comps/GenerateModals/Activity";
function isEditorPath(path: string) {
if (path === "/") return true;
@@ -47,8 +47,13 @@ export const TopBar = ({ pageName }: Props) => {
<SceneTitleInput pageName={pageName} />
</div>
<div className="flex justify-end gap-2.5">
<MyMoviesButton />
<div className="flex justify-end gap-5">
<div className="flex gap-2">
<Button variant="secondary" icon={faImages} disabled>
My Library
</Button>
<Activity />
</div>
<div className="flex justify-end gap-2">
<AuthButtons />
</div>
+1
View File
@@ -13,4 +13,5 @@ export enum JobType {
TextToSpeech = "text_to_speech",
VoiceConversion = "voice_conversion",
VideoStyleTransfer = "workflow",
ImageGeneration = "image_generation",
}
@@ -0,0 +1,75 @@
import { useSignals } from "@preact/signals-react/runtime";
import { faBell, faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PopoverMenu } from "~/components/reusable/Popover/Popover";
import { CompletedCard } from "./CompletedCard";
import { InProgressCard } from "./InProgressCard";
import { H3, P, Tooltip } from "~/components";
import { activeImageGenerationJobs, userMovies } from "~/signals";
export function Activity() {
useSignals();
return (
<Tooltip content="Activity" position="bottom">
<PopoverMenu
mode="default"
buttonClassName="h-[38px] w-[38px] !p-0"
panelClassName="w-[360px] p-2"
position="bottom"
align="end"
triggerIcon={
<div>
<FontAwesomeIcon icon={faBell} />
{activeImageGenerationJobs.value &&
activeImageGenerationJobs.value.length > 0 && (
<div className="bg-blue-500 absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full" />
)}
</div>
}
>
<div className="max-h-[480px] overflow-y-auto">
{/* In progress */}
{activeImageGenerationJobs.value &&
activeImageGenerationJobs.value.length > 0 && (
<div>
{activeImageGenerationJobs.value.map((movieJob) => (
<InProgressCard key={movieJob.job_token} movie={movieJob} />
))}
</div>
)}
{!userMovies.value && !activeImageGenerationJobs.value ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<FontAwesomeIcon
icon={faSpinnerThird}
spin
size="2x"
className="text-gray-400"
/>
<H3 className="text-gray-300">Retrieving Activities</H3>
</div>
) : userMovies.value &&
userMovies.value.length === 0 &&
!activeImageGenerationJobs.value ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<H3 className="text-gray-300">No activities yet</H3>
<P className="text-gray-400">
Try generating something new from the featured scenes
</P>
</div>
) : (
<div>
{/* Completed */}
<div className="flex flex-col">
{userMovies.value?.map((movie) => (
<CompletedCard key={movie.token} movie={movie} />
))}
</div>
</div>
)}
</div>
</PopoverMenu>
</Tooltip>
);
}
@@ -19,7 +19,7 @@ export function getStyleName(typeInput: string) {
interface Props {
movie: MediaInfo;
setMovieId: (id: string) => void;
setMovieId?: (id: string) => void;
}
export function CompletedCard({ movie, setMovieId }: Props) {
@@ -37,20 +37,23 @@ export function CompletedCard({ movie, setMovieId }: Props) {
);
const styleName =
movie.maybe_style_name && getStyleName(movie.maybe_style_name);
const handleClick = () => {
setMovieId?.(movie.token);
};
return (
<div
className="flex w-full items-center justify-between px-5 py-3 text-start transition-all duration-150 hover:cursor-pointer hover:bg-brand-secondary/40"
onClick={() => {
setMovieId(movie.token);
}}
className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:cursor-pointer hover:bg-white/10"
onClick={handleClick}
>
<div className="flex gap-4">
<div className="flex h-32 w-32 justify-center overflow-hidden rounded-lg bg-black/60">
<div className="flex aspect-square h-14 w-14 justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
<img
src={
loadError ? "/resources/images/movie-placeholder.png" : imageUrl
}
className="h-full object-contain"
className="h-full w-full object-cover"
alt={movie.maybe_title ?? "unknown"}
crossOrigin="anonymous"
onError={() => setLoadError(true)}
@@ -60,16 +63,16 @@ export function CompletedCard({ movie, setMovieId }: Props) {
<div className="flex flex-col justify-center gap-1">
<div className="font-medium">{movie.maybe_title || "Untitled"}</div>
<div>
{styleName && (
{/* {styleName && (
<div className="text-sm text-white/60">{styleName}</div>
)}
)} */}
<div className="text-sm text-white/60">
{dayjs(movie.updated_at).format("MMM D, YYYY HH:mm:ss")}
</div>
</div>
</div>
</div>
<div className="pr-5">
{/* <div className="pr-5">
<button
onClick={(event) => {
event.preventDefault();
@@ -83,7 +86,7 @@ export function CompletedCard({ movie, setMovieId }: Props) {
<FontAwesomeIcon icon={faArrowDownToLine} className="mr-2" />
Download
</button>
</div>
</div> */}
</div>
);
}
@@ -58,19 +58,19 @@ export function InProgressCard({ movie }: Props) {
}, []);
return (
<div className="flex w-full items-center justify-between px-5 py-3 text-start transition-all duration-150">
<div className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:bg-white/10">
<div className="flex gap-4">
<div className="flex h-32 w-32 items-center justify-center rounded-lg bg-white/10">
<div className="flex aspect-square h-14 w-14 items-center justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
{(movie.status.status === JobState.STARTED ||
movie.status.status === JobState.PENDING) && (
<FontAwesomeIcon icon={faSpinnerThird} spin size={"2x"} />
<FontAwesomeIcon icon={faSpinnerThird} spin size={"lg"} />
)}
{(movie.status.status === JobState.COMPLETE_FAILURE ||
movie.status.status === JobState.ATTEMPT_FAILED) && (
<FontAwesomeIcon icon={faCircleExclamation} size={"2x"} />
<FontAwesomeIcon icon={faCircleExclamation} size={"lg"} />
)}
</div>
<div className="flex flex-col justify-center gap-2">
<div className="flex flex-col justify-center gap-1">
<div className="font-medium">
{movie.request.maybe_model_title || "Untitled"}
</div>
@@ -94,7 +94,7 @@ export function InProgressCard({ movie }: Props) {
<Tooltip content="Cancel" position="top">
<button
onClick={() => deleteJob(movie)}
className="text-xl text-white/50 transition-all duration-150 hover:text-white/90"
className="text-[15px] font-medium text-white/50 transition-all duration-150 hover:text-white/100"
>
<FontAwesomeIcon icon={faClose} />
</button>
@@ -27,7 +27,7 @@ export const SidePanel = () => {
<>
<div
className={[
"fixed z-30 flex border-l border-[#363636] bg-ui-panel transition-all duration-100",
"fixed flex border-l border-[#363636] bg-ui-panel transition-all duration-100",
].join(" ")}
style={{
top: 64,
@@ -30,7 +30,7 @@ export const SidePanelMenu = ({
return (
<div
className={twMerge(
"fixed z-30 bg-assets-background",
"fixed bg-assets-background",
"right-0 top-[64px] w-[84px] overflow-auto border-l px-2 py-2",
sidePanelVisible.value ? "border-transparent" : "border-[#363636]",
)}
@@ -49,9 +49,9 @@ export const SidePanelMenu = ({
? "border-[#363636] bg-brand-secondary-900/60 opacity-100 hover:bg-brand-secondary-900/60"
: "opacity-60",
tab.title === TabTitles.STYLIZE &&
"bg-brand-primary font-medium opacity-90 hover:border-white/25 hover:bg-brand-primary hover:opacity-100",
"bg-brand-primary font-medium opacity-90 hover:border-white/25 hover:bg-brand-primary hover:opacity-100",
tab.title === selectedTab.title &&
tab.title === TabTitles.STYLIZE
tab.title === TabTitles.STYLIZE
? "border-white/50 opacity-100 hover:border-white/50"
: "",
])}
@@ -33,6 +33,15 @@ export const activeWorkflowJobs = computed(() => {
});
});
export const activeImageGenerationJobs = computed(() => {
if (!activeJobs.value) {
return undefined;
}
return activeJobs.value.filter((job) => {
return job.request.inference_category === JobType.ImageGeneration;
});
});
export const activeAudioJobs = computed(() => {
if (!activeJobs.value) {
return undefined;