mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 01:18:34 +00:00
Merge branch 'main' of github.com:storytold/storyteller-board into main
This commit is contained in:
commit
d5fc2d8145
23 files changed
+788
-166
No files matched your search
@@ -168,8 +168,14 @@ export class Engine {
|
||||
this.addVideo(video.url);
|
||||
});
|
||||
|
||||
uiEvents.onRequestAiStylize((data) => {
|
||||
uiEvents.aiStylize.onRequest((data) => {
|
||||
console.log("Engine heard AI Stylize request: ", data);
|
||||
console.log(
|
||||
"you can also get initial values via uiEvents.aiStylize.getInitialValues",
|
||||
);
|
||||
console.log(
|
||||
"if you missed the event, the current values can be accessed via uiEvents.aiStylize.getCurrentValues",
|
||||
);
|
||||
});
|
||||
// TODO: You may listen to all the image toolbar events here
|
||||
uiEvents.toolbarImage.MOVE.onClick(() => {
|
||||
@@ -185,7 +191,7 @@ export class Engine {
|
||||
//this.onRenderingSystemReceived(undefined);
|
||||
});
|
||||
|
||||
uiEvents.onRequestAiStylize(async () => {
|
||||
uiEvents.aiStylize.onRequest(async () => {
|
||||
// uiAccess.toolbarMain.changeButtonState(
|
||||
// ToolbarMainButtonNames.AI_STYLIZE,
|
||||
// { disabled: true },
|
||||
|
||||
@@ -99,6 +99,13 @@ export const SignaledToolbarMain = () => {
|
||||
/>
|
||||
<DialogAiStylize
|
||||
isOpen={state.isAiStylizeOpen ?? false}
|
||||
onRequestAIStylize={(data) => {
|
||||
const { selectedArtStyle: artstyle, ...rest } = data;
|
||||
dispatchUiEvents.aiStylize.dispatchRequest({
|
||||
artstyle,
|
||||
...rest,
|
||||
});
|
||||
}}
|
||||
closeCallback={closeAll}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
|
||||
import {
|
||||
faWandSparkles,
|
||||
faChevronLeft,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button } from "~/components/ui";
|
||||
import {
|
||||
dialogBackgroundStyles,
|
||||
paperWrapperStyles,
|
||||
} from "~/components/styles";
|
||||
|
||||
import { ArtStyleNames, SubPanelNames } from "./enums";
|
||||
import {
|
||||
generateRandomTextPositive,
|
||||
generateRandomTextNegative,
|
||||
AIStylizeProps,
|
||||
initialValues,
|
||||
} from "./utilities";
|
||||
|
||||
import { SubPanelBasic } from "./SubPanelBasic";
|
||||
import { SubPanelAdvance } from "./SubPanelAdvance";
|
||||
|
||||
export const DialogAiStylize = ({
|
||||
isOpen,
|
||||
closeCallback,
|
||||
onRequestAIStylize,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
closeCallback: () => void;
|
||||
onRequestAIStylize: (data: AIStylizeProps) => void;
|
||||
}) => {
|
||||
const [state, setState] = useState<
|
||||
AIStylizeProps & { panelState: SubPanelNames }
|
||||
>({
|
||||
...initialValues,
|
||||
panelState: SubPanelNames.BASIC,
|
||||
});
|
||||
const { panelState, ...aiStylizeProps } = state;
|
||||
const { selectedArtStyle, positivePrompt, negativePrompt } = aiStylizeProps;
|
||||
|
||||
function handleGenerate() {
|
||||
onRequestAIStylize(aiStylizeProps);
|
||||
closeCallback();
|
||||
}
|
||||
const onChangePanel = useCallback((newP: SubPanelNames) => {
|
||||
setState((curr) => ({ ...curr, panelState: newP }));
|
||||
}, []);
|
||||
const setStylizeOptions = useCallback(
|
||||
(newOptions: Partial<AIStylizeProps>) => {
|
||||
console.log("setStylizeOptions", newOptions);
|
||||
setState((curr) => ({ ...curr, ...newOptions }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
const onSelectedArtStyle = useCallback((newArtstyle: ArtStyleNames) => {
|
||||
setStylizeOptions({
|
||||
selectedArtStyle: newArtstyle,
|
||||
positivePrompt: generateRandomTextPositive(newArtstyle),
|
||||
negativePrompt: generateRandomTextNegative(newArtstyle),
|
||||
});
|
||||
}, []);
|
||||
const onChangeNegativePrompt = useCallback((newPrompt: string) => {
|
||||
setStylizeOptions({ negativePrompt: newPrompt });
|
||||
}, []);
|
||||
const onChangePositivePrompt = useCallback((newPrompt: string) => {
|
||||
setStylizeOptions({ positivePrompt: newPrompt });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setState({ ...initialValues, panelState: SubPanelNames.BASIC });
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={closeCallback} className="relative z-50">
|
||||
<div className={dialogBackgroundStyles}>
|
||||
<DialogPanel
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex w-full max-w-5xl flex-col justify-between gap-4 px-6 pb-6 pt-4",
|
||||
)}
|
||||
style={{ height: "calc(100vh - 200px)" }}
|
||||
>
|
||||
{panelState === SubPanelNames.BASIC && (
|
||||
<>
|
||||
<DialogTitle className="font-bold">
|
||||
Use AI to Stylize{" "}
|
||||
</DialogTitle>
|
||||
<SubPanelBasic
|
||||
selectedArtStyle={selectedArtStyle}
|
||||
positivePrompt={positivePrompt}
|
||||
negativePrompt={negativePrompt}
|
||||
onSelectedArtStyle={onSelectedArtStyle}
|
||||
onChangeNegativePrompt={onChangeNegativePrompt}
|
||||
onChangePositivePrompt={onChangePositivePrompt}
|
||||
onChangePanel={onChangePanel}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{panelState === SubPanelNames.ADVANCED && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => onChangePanel(SubPanelNames.BASIC)}
|
||||
variant="tertiary"
|
||||
className="my-1 w-fit"
|
||||
icon={faChevronLeft}
|
||||
>
|
||||
Back to Basic Options
|
||||
</Button>
|
||||
<SubPanelAdvance
|
||||
aiStylizeProps={aiStylizeProps}
|
||||
onStylizeOptionsChanged={setStylizeOptions}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="flex w-full justify-center gap-4">
|
||||
<Button onClick={closeCallback} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="hover:animate-pulse"
|
||||
icon={faWandSparkles}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
import { Field, Label, Switch } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { AIStylizeProps } from "../utilities";
|
||||
|
||||
export const StyleOptionSwitches = ({
|
||||
faceDetail,
|
||||
upscale,
|
||||
lipSync,
|
||||
cinematic,
|
||||
enginePreProcessing,
|
||||
onStylizeOptionsChanged,
|
||||
}: {
|
||||
cinematic: boolean;
|
||||
enginePreProcessing: boolean;
|
||||
faceDetail: boolean;
|
||||
lipSync: boolean;
|
||||
upscale: boolean;
|
||||
onStylizeOptionsChanged: (newOptions: Partial<AIStylizeProps>) => void;
|
||||
}) => {
|
||||
const handleCinematicChange = () => {
|
||||
onStylizeOptionsChanged({
|
||||
cinematic: !cinematic,
|
||||
upscale: !cinematic === true ? false : upscale,
|
||||
});
|
||||
};
|
||||
|
||||
const enginePreProcessingChange = () => {
|
||||
onStylizeOptionsChanged({
|
||||
enginePreProcessing: !enginePreProcessing,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpscaleChange = () => {
|
||||
onStylizeOptionsChanged({
|
||||
upscale: !upscale,
|
||||
cinematic: !upscale === true ? false : cinematic,
|
||||
});
|
||||
};
|
||||
|
||||
const handleLipsyncChange = () => {
|
||||
onStylizeOptionsChanged({
|
||||
lipSync: !lipSync,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFaceDetailerChange = () => {
|
||||
onStylizeOptionsChanged({
|
||||
faceDetail: !faceDetail,
|
||||
});
|
||||
};
|
||||
|
||||
const switchContainerBaseStyle =
|
||||
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-0 focus:ring-indigo-500 focus:ring-offset-0";
|
||||
const switchContainerStateStyle = (state: boolean) =>
|
||||
state ? "bg-primary hover:bg-primary-400" : "bg-gray-500 hover:bg-gray-400";
|
||||
const switchButtonBaseStyle =
|
||||
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform";
|
||||
const switchButtonStateStyle = (state: boolean) =>
|
||||
state ? "translate-x-6" : "translate-x-1";
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-4 rounded-b-lg bg-ui-panel">
|
||||
<Field className="flex items-center">
|
||||
<Label className="mr-3 grow text-sm font-medium transition-opacity">
|
||||
Sync Lips with Speech
|
||||
</Label>
|
||||
<Switch
|
||||
checked={lipSync}
|
||||
onChange={handleLipsyncChange}
|
||||
className={twMerge(
|
||||
switchContainerBaseStyle,
|
||||
switchContainerStateStyle(lipSync),
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
switchButtonBaseStyle,
|
||||
switchButtonStateStyle(lipSync),
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Field>
|
||||
|
||||
<Field className="flex items-center">
|
||||
<Label className="mr-3 grow text-sm font-medium transition-opacity">
|
||||
Face Detailer
|
||||
</Label>
|
||||
<Switch
|
||||
checked={faceDetail}
|
||||
onChange={handleFaceDetailerChange}
|
||||
className={twMerge(
|
||||
switchContainerBaseStyle,
|
||||
switchContainerStateStyle(faceDetail),
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
switchButtonBaseStyle,
|
||||
switchButtonStateStyle(faceDetail),
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Field>
|
||||
|
||||
<Field className="flex items-center">
|
||||
<Label
|
||||
className={twMerge(
|
||||
"mr-3 grow text-sm font-medium transition-opacity",
|
||||
cinematic ? "opacity-50" : "",
|
||||
)}
|
||||
>
|
||||
Upscale
|
||||
</Label>
|
||||
<Switch
|
||||
checked={upscale}
|
||||
onChange={handleUpscaleChange}
|
||||
className={twMerge(
|
||||
switchContainerBaseStyle,
|
||||
switchContainerStateStyle(upscale),
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
switchButtonBaseStyle,
|
||||
switchButtonStateStyle(upscale),
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Field>
|
||||
|
||||
<Field className="flex items-center">
|
||||
<Label
|
||||
className={twMerge(
|
||||
"mr-3 grow text-sm font-medium transition-opacity",
|
||||
upscale ? "opacity-50" : "",
|
||||
)}
|
||||
>
|
||||
Use Cinematic
|
||||
</Label>
|
||||
<Switch
|
||||
checked={cinematic}
|
||||
onChange={handleCinematicChange}
|
||||
className={twMerge(
|
||||
switchContainerBaseStyle,
|
||||
switchContainerStateStyle(cinematic),
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
switchButtonBaseStyle,
|
||||
switchButtonStateStyle(cinematic),
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Field>
|
||||
|
||||
<Field className="flex items-center">
|
||||
<Label className="mr-3 grow text-sm font-medium transition-opacity">
|
||||
Engine Preprocessing
|
||||
</Label>
|
||||
<Switch
|
||||
checked={enginePreProcessing}
|
||||
onChange={enginePreProcessingChange}
|
||||
className={twMerge(
|
||||
switchContainerBaseStyle,
|
||||
switchContainerStateStyle(enginePreProcessing),
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
switchButtonBaseStyle,
|
||||
switchButtonStateStyle(enginePreProcessing),
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { InputNumber, Slider } from "~/components/ui";
|
||||
import { AIStylizeProps } from "../utilities";
|
||||
|
||||
export function StyleStrengthSlider({
|
||||
styleStrength,
|
||||
onStylizeOptionsChanged,
|
||||
}: {
|
||||
styleStrength: number;
|
||||
onStylizeOptionsChanged: (newOptions: Partial<AIStylizeProps>) => void;
|
||||
}) {
|
||||
const onChanged = (value: number | number[]) => {
|
||||
onStylizeOptionsChanged({
|
||||
styleStrength: (value as number) / 100,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col rounded-b-lg bg-ui-panel">
|
||||
<label className="font-bold leading-tight">
|
||||
Set the Style Strength (%)
|
||||
</label>
|
||||
<p className="mt-1 text-xs">
|
||||
(The higher the value the more the style will be applied, the lower the
|
||||
value the closer to source.)
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-3.5">
|
||||
<InputNumber value={styleStrength * 100} onChange={onChanged} />
|
||||
<Slider
|
||||
value={styleStrength * 100}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={onChanged}
|
||||
showTooltip={true}
|
||||
suffix="%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { StyleOptionSwitches } from "./StyleOptionSwitches";
|
||||
import { AIStylizeProps } from "../utilities";
|
||||
import { StyleStrengthSlider } from "./StyleStrengthSlider";
|
||||
|
||||
export const SubPanelAdvance = ({
|
||||
aiStylizeProps,
|
||||
onStylizeOptionsChanged,
|
||||
}: {
|
||||
aiStylizeProps: AIStylizeProps;
|
||||
onStylizeOptionsChanged: (newOptions: Partial<AIStylizeProps>) => void;
|
||||
}) => {
|
||||
const {
|
||||
cinematic,
|
||||
enginePreProcessing,
|
||||
faceDetail,
|
||||
lipSync,
|
||||
upscale,
|
||||
styleStrength,
|
||||
} = aiStylizeProps;
|
||||
|
||||
return (
|
||||
<div className="flex w-full grow gap-2">
|
||||
<div className="flex w-2/3 flex-col">
|
||||
<h6>IP Adapter</h6>
|
||||
</div>
|
||||
<div className="flex w-1/3 flex-col gap-4">
|
||||
<h4>Advanced Options</h4>
|
||||
<StyleOptionSwitches
|
||||
faceDetail={faceDetail}
|
||||
upscale={upscale}
|
||||
lipSync={lipSync}
|
||||
cinematic={cinematic}
|
||||
enginePreProcessing={enginePreProcessing}
|
||||
onStylizeOptionsChanged={onStylizeOptionsChanged}
|
||||
/>
|
||||
<br />
|
||||
<StyleStrengthSlider
|
||||
styleStrength={styleStrength}
|
||||
onStylizeOptionsChanged={onStylizeOptionsChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SubPanelAdvance";
|
||||
+9
-19
@@ -1,6 +1,7 @@
|
||||
import { ImgHTMLAttributes, useEffect, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ArtStyleNames } from "./enums";
|
||||
import { ArtStyleNames } from "../enums";
|
||||
import { transitionTimingStyles } from "~/components/styles";
|
||||
|
||||
interface ArtStyleItemProps extends ImgHTMLAttributes<HTMLImageElement> {
|
||||
label: string;
|
||||
@@ -18,8 +19,6 @@ export const ArtStyleItem = ({
|
||||
defaultImg = "/resources/placeholders/style_placeholder.png",
|
||||
src = defaultImg,
|
||||
onSelected,
|
||||
// width,
|
||||
// height,
|
||||
className,
|
||||
...imgProps
|
||||
}: ArtStyleItemProps) => {
|
||||
@@ -39,18 +38,13 @@ export const ArtStyleItem = ({
|
||||
return (
|
||||
<button
|
||||
className={twMerge(
|
||||
"relative cursor-pointer overflow-hidden rounded-lg border-2 transition-colors ease-in-out",
|
||||
"relative cursor-pointer overflow-hidden rounded-lg border-2 transition-colors",
|
||||
transitionTimingStyles,
|
||||
selected
|
||||
? "border-primary"
|
||||
: "border-ui-border hover:border-primary-300",
|
||||
className,
|
||||
)}
|
||||
// style={{
|
||||
// minWidth: (width as number) + 4,
|
||||
// minHeight: (height as number) + 4,
|
||||
// maxWidth: (width as number) + 4,
|
||||
// maxHeight: (height as number) + 4,
|
||||
// }}
|
||||
onClick={handleSelected}
|
||||
>
|
||||
<img
|
||||
@@ -58,12 +52,6 @@ export const ArtStyleItem = ({
|
||||
src={imageSrc}
|
||||
{...imgProps}
|
||||
alt={label}
|
||||
// style={{
|
||||
// minWidth: width,
|
||||
// minHeight: height,
|
||||
// maxWidth: width,
|
||||
// maxHeight: height,
|
||||
// }}
|
||||
/>
|
||||
<div className="absolute bottom-0 left-0 h-1/2 w-full bg-gradient-to-t from-ui-panel" />
|
||||
<h4 className="absolute bottom-0 left-1 truncate text-start text-sm drop-shadow-md">
|
||||
@@ -72,9 +60,11 @@ export const ArtStyleItem = ({
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
className={`absolute right-2 top-2 size-5 shadow-xl transition-opacity duration-200 ease-in-out ${
|
||||
selected ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
className={twMerge(
|
||||
"absolute right-2 top-2 size-5 shadow-xl transition-opacity",
|
||||
transitionTimingStyles,
|
||||
selected ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
<path
|
||||
opacity="1"
|
||||
+3
-6
@@ -1,6 +1,6 @@
|
||||
import { styleList } from "./data/styeList";
|
||||
import { styleList } from "../data/styeList";
|
||||
import { ArtStyleItem } from "./ArtStyleItem";
|
||||
import { ArtStyleNames } from "./enums";
|
||||
import { ArtStyleNames } from "../enums";
|
||||
|
||||
export const ArtStyleSelector = ({
|
||||
selectedArtStyle,
|
||||
@@ -10,10 +10,7 @@ export const ArtStyleSelector = ({
|
||||
onSelectedArtStyle: (newArtStyle: ArtStyleNames) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-4 overflow-hidden rounded-t-lg bg-ui-panel"
|
||||
style={{ height: "calc(100vh - 500px)" }}
|
||||
>
|
||||
<div className="flex h-0 flex-auto overflow-hidden">
|
||||
<div className="overflow-y-auto">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{styleList.map((style) => (
|
||||
+2
-2
@@ -6,8 +6,8 @@ import { Textarea } from "~/components/ui";
|
||||
import {
|
||||
generateRandomTextPositive,
|
||||
generateRandomTextNegative,
|
||||
} from "./utilities";
|
||||
import { ArtStyleNames } from "./enums";
|
||||
} from "../utilities";
|
||||
import { ArtStyleNames } from "../enums";
|
||||
|
||||
export const Prompts = ({
|
||||
selectedArtStyle,
|
||||
@@ -0,0 +1,55 @@
|
||||
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ArtStyleSelector } from "./ArtStyleSelector";
|
||||
import { Prompts } from "./Prompts";
|
||||
|
||||
import { Button } from "~/components/ui";
|
||||
import { ArtStyleNames, SubPanelNames } from "../enums";
|
||||
|
||||
export const SubPanelBasic = ({
|
||||
selectedArtStyle,
|
||||
positivePrompt,
|
||||
negativePrompt,
|
||||
onSelectedArtStyle,
|
||||
onChangePositivePrompt,
|
||||
onChangeNegativePrompt,
|
||||
onChangePanel,
|
||||
}: {
|
||||
selectedArtStyle: ArtStyleNames;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
onSelectedArtStyle: (artStyle: ArtStyleNames) => void;
|
||||
onChangePositivePrompt: (newPrompt: string) => void;
|
||||
onChangeNegativePrompt: (newPrompt: string) => void;
|
||||
onChangePanel: (newP: SubPanelNames) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full grow gap-2">
|
||||
<div className="flex w-2/3 flex-col">
|
||||
<h4>Pick a Style</h4>
|
||||
<ArtStyleSelector
|
||||
onSelectedArtStyle={onSelectedArtStyle}
|
||||
selectedArtStyle={selectedArtStyle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-1/3 flex-col justify-between">
|
||||
<Prompts
|
||||
selectedArtStyle={selectedArtStyle}
|
||||
positivePrompt={positivePrompt}
|
||||
negativePrompt={negativePrompt}
|
||||
onChangePositivePrompt={onChangePositivePrompt}
|
||||
onChangeNegativePrompt={onChangeNegativePrompt}
|
||||
/>
|
||||
<span className="grow" />
|
||||
<Button
|
||||
onClick={() => onChangePanel(SubPanelNames.ADVANCED)}
|
||||
variant="tertiary"
|
||||
className="w-fit self-end"
|
||||
>
|
||||
Advanced Options
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SubPanelBaisc";
|
||||
@@ -39,3 +39,8 @@ export enum Visibility {
|
||||
Hidden = "hidden",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
export enum SubPanelNames {
|
||||
BASIC = "basic",
|
||||
ADVANCED = "advanced",
|
||||
}
|
||||
@@ -1,110 +1 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
|
||||
import { faWandSparkles } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Button } from "~/components/ui";
|
||||
import {
|
||||
dialogBackgroundStyles,
|
||||
paperWrapperStyles,
|
||||
} from "~/components/styles";
|
||||
|
||||
import { ArtStyleNames } from "./enums";
|
||||
import {
|
||||
generateRandomTextPositive,
|
||||
generateRandomTextNegative,
|
||||
} from "./utilities";
|
||||
|
||||
import { Prompts } from "./Prompts";
|
||||
import { ArtStyleSelector } from "./ArtStyleSelector";
|
||||
import { dispatchUiEvents } from "~/signals";
|
||||
|
||||
export const DialogAiStylize = ({
|
||||
isOpen,
|
||||
closeCallback,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
closeCallback: () => void;
|
||||
}) => {
|
||||
const [{ selectedArtStyle, positivePrompt, negativePrompt }, setState] =
|
||||
useState<{
|
||||
selectedArtStyle: ArtStyleNames;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
}>({
|
||||
selectedArtStyle: ArtStyleNames.Anime2DFlat,
|
||||
positivePrompt: generateRandomTextPositive(ArtStyleNames.Anime2DFlat),
|
||||
negativePrompt: generateRandomTextNegative(ArtStyleNames.Anime2DFlat),
|
||||
});
|
||||
|
||||
function handleClose() {
|
||||
closeCallback();
|
||||
}
|
||||
|
||||
function handleGenerate() {
|
||||
dispatchUiEvents.requestAiStylize({
|
||||
artstyle: selectedArtStyle,
|
||||
positivePrompt: positivePrompt,
|
||||
negativePrompt: negativePrompt,
|
||||
});
|
||||
handleClose();
|
||||
}
|
||||
const onSelectedArtStyle = useCallback((newArtstyle: ArtStyleNames) => {
|
||||
setState({
|
||||
selectedArtStyle: newArtstyle,
|
||||
positivePrompt: generateRandomTextPositive(newArtstyle),
|
||||
negativePrompt: generateRandomTextNegative(newArtstyle),
|
||||
});
|
||||
}, []);
|
||||
const onChangeNegativePrompt = useCallback((newPrompt: string) => {
|
||||
setState((curr) => ({ ...curr, negativePrompt: newPrompt }));
|
||||
}, []);
|
||||
const onChangePositivePrompt = useCallback((newPrompt: string) => {
|
||||
setState((curr) => ({ ...curr, positivePrompt: newPrompt }));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={closeCallback} className="relative z-50">
|
||||
<div className={dialogBackgroundStyles}>
|
||||
<DialogPanel
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex w-full max-w-5xl flex-col gap-4 px-6 py-4",
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="font-bold">Use AI to Stylize</DialogTitle>
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="w-2/3">
|
||||
<h4>Pick a Style</h4>
|
||||
<ArtStyleSelector
|
||||
onSelectedArtStyle={onSelectedArtStyle}
|
||||
selectedArtStyle={selectedArtStyle}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
<Prompts
|
||||
selectedArtStyle={selectedArtStyle}
|
||||
positivePrompt={positivePrompt}
|
||||
negativePrompt={negativePrompt}
|
||||
onChangePositivePrompt={onChangePositivePrompt}
|
||||
onChangeNegativePrompt={onChangeNegativePrompt}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-end gap-4">
|
||||
<Button onClick={handleClose} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="hover:animate-pulse"
|
||||
icon={faWandSparkles}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export * from "./DialogAiStylize";
|
||||
@@ -4,18 +4,40 @@ import {
|
||||
RandomTextsNegative,
|
||||
} from "./data/randomTextList";
|
||||
|
||||
export const generateRandomTextPositive = (artStyleName: ArtStyleNames) => {
|
||||
export type AIStylizeProps = {
|
||||
selectedArtStyle: ArtStyleNames;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
cinematic: boolean;
|
||||
enginePreProcessing: boolean;
|
||||
faceDetail: boolean;
|
||||
lipSync: boolean;
|
||||
upscale: boolean;
|
||||
styleStrength: number;
|
||||
};
|
||||
export const initialValues = {
|
||||
selectedArtStyle: ArtStyleNames.Anime2DFlat,
|
||||
positivePrompt: generateRandomTextPositive(ArtStyleNames.Anime2DFlat),
|
||||
negativePrompt: generateRandomTextNegative(ArtStyleNames.Anime2DFlat),
|
||||
cinematic: false,
|
||||
enginePreProcessing: false,
|
||||
faceDetail: false,
|
||||
lipSync: false,
|
||||
upscale: false,
|
||||
styleStrength: 0.8,
|
||||
};
|
||||
export function generateRandomTextPositive(artStyleName: ArtStyleNames) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * RandomTextsPositive[artStyleName].length,
|
||||
);
|
||||
const randomText = RandomTextsPositive[artStyleName][randomIndex];
|
||||
return randomText;
|
||||
};
|
||||
}
|
||||
|
||||
export const generateRandomTextNegative = (artStyleName: ArtStyleNames) => {
|
||||
export function generateRandomTextNegative(artStyleName: ArtStyleNames) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * RandomTextsNegative[artStyleName].length,
|
||||
);
|
||||
const randomText = RandomTextsNegative[artStyleName][randomIndex];
|
||||
return randomText;
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
iconClassName?: string;
|
||||
iconFlip?: boolean;
|
||||
htmlFor?: string;
|
||||
variant?: "primary" | "secondary" | "action";
|
||||
variant?: "primary" | "secondary" | "tertiary" | "action";
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ export const Button = ({
|
||||
}: ButtonProps) => {
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "tertiary": {
|
||||
return "bg-tertiary hover:bg-tertiary-400 text-white focus-visible:outline-tertiary-600";
|
||||
}
|
||||
case "secondary": {
|
||||
return "bg-secondary hover:bg-secondary-900 text-white focus-visible:outline-secondary";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Input } from "./Input";
|
||||
|
||||
interface NumberInputProps {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export const InputNumber = ({ value, onChange }: NumberInputProps) => {
|
||||
const [inputValue, setInputValue] = useState<string>(value.toString());
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(Math.round(value * 100) / 100 + "");
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = event.target.value;
|
||||
if (
|
||||
newValue === "" ||
|
||||
(!isNaN(Number(newValue)) && Number(newValue) <= 100)
|
||||
) {
|
||||
setInputValue(newValue);
|
||||
const numericValue = parseFloat(newValue);
|
||||
if (newValue !== "") {
|
||||
onChange(Math.round(numericValue * 100) / 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
if (inputValue === "") {
|
||||
setInputValue(Math.round(value * 100) / 100 + "");
|
||||
}
|
||||
};
|
||||
|
||||
const handleIncrement = () => {
|
||||
const newValue = Math.min(Number(inputValue) + 1, 100);
|
||||
setInputValue(Math.round(newValue * 100) / 100 + "");
|
||||
onChange(Math.round(newValue * 100) / 100);
|
||||
};
|
||||
|
||||
const handleDecrement = () => {
|
||||
const newValue = Math.max(Number(inputValue) - 1, 0);
|
||||
setInputValue(Math.round(newValue * 100) / 100 + "");
|
||||
onChange(Math.round(newValue * 100) / 100);
|
||||
};
|
||||
|
||||
const incrementButtonStyle =
|
||||
"flex h-4 w-6 items-center justify-center text-xs text-white bg-ui-border hover:bg-secondary-500 outline-none focus:outline-none transition-all active:outline-none";
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
inputClassName="w-16 h-7 rounded-md text-sm"
|
||||
/>
|
||||
|
||||
<div className="absolute right-0 top-0 flex h-7 flex-col rounded-r-md border-l">
|
||||
<button
|
||||
className={twMerge(incrementButtonStyle, "rounded-tr-md")}
|
||||
onClick={handleIncrement}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronUp} />
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(incrementButtonStyle, "rounded-br-md")}
|
||||
onClick={handleDecrement}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import React, { useState, useRef, useEffect, ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface SliderProps {
|
||||
min: number;
|
||||
max: number;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
step: number;
|
||||
className?: string;
|
||||
showTooltip?: boolean;
|
||||
tooltipContent?: ReactNode;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
export const Slider = ({
|
||||
min,
|
||||
max,
|
||||
value,
|
||||
onChange,
|
||||
step,
|
||||
className,
|
||||
showTooltip,
|
||||
tooltipContent,
|
||||
suffix,
|
||||
}: SliderProps) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [showHandleTooltip, setShowHandleTooltip] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const updatePosition = (x: number) => {
|
||||
if (!sliderRef.current) return;
|
||||
const rect = sliderRef.current.getBoundingClientRect();
|
||||
const fraction = (x - rect.left) / rect.width;
|
||||
const newValue = fraction * (max - min) + min;
|
||||
const roundedValue = Math.round(newValue / step) * step;
|
||||
const clampedValue = Math.min(Math.max(roundedValue, min), max);
|
||||
setLocalValue(clampedValue);
|
||||
onChange(clampedValue);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
setShowHandleTooltip(true);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
updatePosition(e.clientX);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
updatePosition(e.clientX);
|
||||
};
|
||||
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
setShowHandleTooltip(false);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isDragging) {
|
||||
setShowHandleTooltip(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!isDragging) {
|
||||
setShowHandleTooltip(false);
|
||||
}
|
||||
};
|
||||
|
||||
const percentage = ((localValue - min) / (max - min)) * 100;
|
||||
const displayValue =
|
||||
localValue.toFixed(step.toString().split(".")[1]?.length || 0) +
|
||||
(suffix || "");
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={twMerge(
|
||||
"relative h-2.5 w-full cursor-pointer rounded-full bg-gray-500",
|
||||
className,
|
||||
)}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div
|
||||
className="absolute h-2.5 rounded-full bg-primary"
|
||||
style={{ width: `${percentage}%` }}
|
||||
></div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute -top-1 h-[18px] w-[18px] -translate-x-1/2 transform rounded-full bg-gray-200 transition-colors",
|
||||
isDragging ? "bg-white" : "bg-gray-300 hover:bg-white",
|
||||
)}
|
||||
style={{ left: `${percentage}%` }}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
{showTooltip && (
|
||||
<Transition
|
||||
as={"div"}
|
||||
show={showHandleTooltip}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="absolute -top-8 z-10 rounded-md bg-ui-panel px-2 py-1 text-xs text-black shadow-lg"
|
||||
style={{ left: `${percentage}%`, transform: "translateX(-50%)" }}
|
||||
>
|
||||
{tooltipContent ? tooltipContent : displayValue}
|
||||
</div>
|
||||
</Transition>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
export * from "./Button";
|
||||
export * from "./Input";
|
||||
export * from "./InputNumber";
|
||||
export * from "./LoadingBar";
|
||||
export * from "./LoadingSpinner";
|
||||
export * from "./PanelBottom";
|
||||
export * from "./PanelRight";
|
||||
export * from "./Slider";
|
||||
export * from "./Spinner";
|
||||
export * from "./TextArea";
|
||||
export * from "./Tooltip";
|
||||
@@ -53,7 +53,7 @@ export const loadingBar = {
|
||||
status,
|
||||
};
|
||||
},
|
||||
show(props: Omit<ContextualLoadingBarProps, "isShowing">) {
|
||||
show(props?: Omit<ContextualLoadingBarProps, "isShowing">) {
|
||||
if (loadingBarSignal.value.isShowing) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(
|
||||
@@ -63,8 +63,11 @@ export const loadingBar = {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const mergedProps = props
|
||||
? { ...props, ...loadingBarSignal.value }
|
||||
: loadingBarSignal.value;
|
||||
loadingBarSignal.value = {
|
||||
...props,
|
||||
...mergedProps,
|
||||
isShowing: true,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -38,10 +38,12 @@ export const loadingBar = {
|
||||
status,
|
||||
};
|
||||
},
|
||||
show: (props: Omit<LoadingParInterface, "isShowing">) => {
|
||||
show: (props?: Omit<LoadingParInterface, "isShowing">) => {
|
||||
const mergedProps = props
|
||||
? { ...props, ...loadingBarSignal.value }
|
||||
: loadingBarSignal.value;
|
||||
loadingBarSignal.value = {
|
||||
...loadingBarSignal.value,
|
||||
...props,
|
||||
...mergedProps,
|
||||
isShowing: true,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
import { signal, effect } from "@preact/signals-react";
|
||||
import {
|
||||
RandomTextsNegative,
|
||||
RandomTextsPositive,
|
||||
} from "~/components/features/DialogAiStylize/data/randomTextList";
|
||||
|
||||
import { ArtStyleNames } from "~/components/features/DialogAiStylize/enums";
|
||||
|
||||
const stagedAiStylizeRequest = signal<{
|
||||
interface AiStylizeSingalType {
|
||||
artstyle: ArtStyleNames;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
} | null>(null);
|
||||
cinematic: boolean;
|
||||
enginePreProcessing: boolean;
|
||||
faceDetail: boolean;
|
||||
lipSync: boolean;
|
||||
upscale: boolean;
|
||||
styleStrength: number;
|
||||
}
|
||||
const initialValues = {
|
||||
artstyle: ArtStyleNames.Anime2DFlat,
|
||||
positivePrompt: RandomTextsPositive[ArtStyleNames.Anime2DFlat][0],
|
||||
negativePrompt: RandomTextsNegative[ArtStyleNames.Anime2DFlat][0],
|
||||
cinematic: false,
|
||||
enginePreProcessing: false,
|
||||
faceDetail: false,
|
||||
lipSync: false,
|
||||
upscale: false,
|
||||
styleStrength: 0.8,
|
||||
};
|
||||
const stagedAiStylizeRequest = signal<AiStylizeSingalType>(initialValues);
|
||||
|
||||
export const requestAiStylize = (data: {
|
||||
artstyle: ArtStyleNames;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
}) => {
|
||||
const dispatchRequest = (data: AiStylizeSingalType) => {
|
||||
stagedAiStylizeRequest.value = data;
|
||||
};
|
||||
|
||||
export const onRequestAiStylize = (
|
||||
callback: (data: {
|
||||
artstyle: ArtStyleNames;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
}) => void,
|
||||
) => {
|
||||
const onRequest = (callback: (data: AiStylizeSingalType) => void) => {
|
||||
effect(() => {
|
||||
if (stagedAiStylizeRequest.value) {
|
||||
callback(stagedAiStylizeRequest.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getCurrentValues = stagedAiStylizeRequest.value;
|
||||
const getInitialValues = initialValues;
|
||||
|
||||
export const aiStylizeDispatchers = {
|
||||
dispatchRequest,
|
||||
};
|
||||
export const aiStylizeEvents = {
|
||||
onRequest,
|
||||
getCurrentValues,
|
||||
getInitialValues,
|
||||
};
|
||||
@@ -22,11 +22,11 @@ import {
|
||||
dispatcher as buttonRetryDispatcher,
|
||||
eventsHandler as buttonRetryEvent,
|
||||
} from "./buttonRetry";
|
||||
import { requestAiStylize, onRequestAiStylize } from "./aiStylize";
|
||||
import { aiStylizeDispatchers, aiStylizeEvents } from "./aiStylize";
|
||||
|
||||
export const uiEvents = {
|
||||
...addMediaEvents,
|
||||
onRequestAiStylize,
|
||||
aiStylize: aiStylizeEvents,
|
||||
buttonRetry: buttonRetryEvent,
|
||||
buttonTest: buttonTestEvent,
|
||||
toolbarImage: toolbarImageEvents,
|
||||
@@ -35,7 +35,7 @@ export const uiEvents = {
|
||||
};
|
||||
export const dispatchUiEvents = {
|
||||
...addMediaDispatchers,
|
||||
requestAiStylize,
|
||||
aiStylize: aiStylizeDispatchers,
|
||||
buttonRetry: buttonRetryDispatcher,
|
||||
buttonTest: buttonTestDispatcher,
|
||||
toolbarImage: toolbarImageDispatchers,
|
||||
|
||||
Reference in new issue
Block a user