Merge branch 'main' of github.com:storytold/storyteller-board into main

This commit is contained in:
Michael Chung committed 2024-09-17 17:52:51 -04:00
commit d5fc2d8145
23 files changed
+788 -166

No files matched your search

+8 -2
View File
@@ -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";
@@ -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"
@@ -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) => (
@@ -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;
};
}
+4 -1
View File
@@ -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";
}
+81
View File
@@ -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>
);
};
+131
View File
@@ -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>
);
};
+2
View File
@@ -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";
+5 -2
View File
@@ -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,
};
},
+38 -14
View File
@@ -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,
};
+3 -3
View File
@@ -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,