mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 10:11:31 +00:00
revised moderation job control page/TempSelect
This commit is contained in:
1 parent
0c4c90075f
commit
2012e5ccf2
9 files changed
+195
-188
No files matched your search
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"WeightTypes": {
|
||||
"all": "All weights",
|
||||
"hifigan_tt2": "HiFi-GAN TT2",
|
||||
"rvc_v2": "RVCv2",
|
||||
"sd_1": "SD 1.0",
|
||||
"sd_1.5": "SD 1.5",
|
||||
"sdxl": "SDXL",
|
||||
"so_vits_svc": "so_vits_svc",
|
||||
"tt2": "TT2",
|
||||
"lora": "loRA",
|
||||
"vall_e": "vall_e"
|
||||
},
|
||||
"MediaTypes": {
|
||||
"all": "All media",
|
||||
"audio": "Audio",
|
||||
"image": "Image",
|
||||
"video": "Video",
|
||||
"bvh": "BVH",
|
||||
"glb": "GLB",
|
||||
"gltf": "gltf",
|
||||
"fbx": "FBX"
|
||||
},
|
||||
}
|
||||
@@ -3,16 +3,38 @@
|
||||
.fy-checkbox-frame {
|
||||
margin-bottom: .5rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
label {
|
||||
margin-left: .5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.fy-checkbox-primary-unchecked, .fy-checkbox-primary-checked {
|
||||
.fy-checkbox {
|
||||
background-color: $primary;
|
||||
}
|
||||
&:hover {
|
||||
.fy-checkbox {
|
||||
background-color: lighten($primary, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fy-checkbox-secondary-unchecked, .fy-checkbox-secondary-checked {
|
||||
.fy-checkbox {
|
||||
background-color: $secondary;
|
||||
}
|
||||
&:hover {
|
||||
.fy-checkbox {
|
||||
background-color: $secondary-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fy-checkbox {
|
||||
background-color: $primary;
|
||||
border-radius: .5rem;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 2rem;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { a, useSpring } from "@react-spring/web";
|
||||
import { Check } from "components/svg";
|
||||
import { useHover, useId } from "hooks";
|
||||
import { useId } from "hooks";
|
||||
import makeClass from "resources/makeClass";
|
||||
import "./Checkbox.scss";
|
||||
|
||||
@@ -11,20 +10,18 @@ interface Props {
|
||||
label?: string;
|
||||
name?: string;
|
||||
onChange?: any;
|
||||
variant?: "primary" | "secondary";
|
||||
}
|
||||
|
||||
export default function Checkbox({ checked, className = "", label = "", name = "", onChange }: Props) {
|
||||
const [hover, hoverEvents = {}] = useHover({});
|
||||
const style = useSpring({
|
||||
config: { tension: 120, friction: 14 },
|
||||
backgroundColor: checked ? hover ? "#bb5a5d" : "#e66462" : hover ? "#bb5a5d" : "#39394c"
|
||||
});
|
||||
export default function Checkbox({ checked, className = "", label = "", name = "", onChange, variant = "secondary" }: Props) {
|
||||
const onClick = ({ target }: any) => onChange({ target: { checked: !checked, name, type: 'checkbox' } });
|
||||
const id = "checkbox-" + useId();
|
||||
return <div {...{ ...makeClass("fy-checkbox-frame",className), }}>
|
||||
<a.svg {...{ className: "fy-checkbox", onClick, style, ...hoverEvents }}>
|
||||
return <div {...{
|
||||
...makeClass(`fy-checkbox-frame fy-checkbox-${ variant }-${ checked ? "checked" : "unchecked" }`,className),
|
||||
}}>
|
||||
<svg {...{ className: `fy-checkbox`, onClick }}>
|
||||
<Check {...{ checked }}/>
|
||||
</a.svg>
|
||||
</svg>
|
||||
{ label && <label {...{ className: "form-check-label", htmlFor: id, onClick }}>{ label }</label> }
|
||||
</div>
|
||||
};
|
||||
@@ -11,10 +11,6 @@ interface Props {
|
||||
required?: boolean;
|
||||
step?: number;
|
||||
thumbTip?: string;
|
||||
// name, value and setter for onChange has to be in this particular format
|
||||
// name = state variable's name as a stringh
|
||||
// value = the state variable's reference
|
||||
// onChange = the statevariable's setter function's reference
|
||||
name?: string;
|
||||
value?: number;
|
||||
onChange?: any;
|
||||
@@ -29,20 +25,14 @@ const renderTrack = ({ props: { style, ...props }, children }: any) => (
|
||||
const thumb =
|
||||
(thumbTip = "") =>
|
||||
({ props: { style, ...props } }: any) => {
|
||||
return (
|
||||
<Tippy
|
||||
{...{
|
||||
arrow: false,
|
||||
content: thumbTip,
|
||||
placement: "bottom",
|
||||
theme: "range-slider",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
{...{ ...props, className: "fy-number-slider-thumb", style }}
|
||||
></div>
|
||||
</Tippy>
|
||||
);
|
||||
return thumbTip ? <Tippy {...{
|
||||
arrow: false,
|
||||
content: thumbTip,
|
||||
placement: "bottom",
|
||||
theme: "range-slider",
|
||||
}}>
|
||||
<div {...{ ...props, className: "fy-number-slider-thumb", style }}></div>
|
||||
</Tippy> : <div{...{ ...props, className: "fy-number-slider-thumb", style }}></div>;
|
||||
};
|
||||
|
||||
export default function NumberSlider({
|
||||
|
||||
@@ -8,6 +8,21 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.select-option {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.select-option.select__option--is-selected {
|
||||
background-color: $secondary-lighter;
|
||||
&.select__option--is-focused {
|
||||
background-color: $secondary-very-light;
|
||||
}
|
||||
}
|
||||
|
||||
.select__value-container--is-multi {
|
||||
height: min-content;
|
||||
// background: red;
|
||||
|
||||
@@ -6,11 +6,15 @@ import "./TempSelect.scss";
|
||||
|
||||
type OptionValue = string | number | boolean;
|
||||
|
||||
export interface Option {
|
||||
value: OptionValue;
|
||||
export interface BaseOption {
|
||||
value?: OptionValue;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Option extends BaseOption {
|
||||
options?: BaseOption[];
|
||||
}
|
||||
|
||||
export interface SelectProps extends ReactSelectProps {
|
||||
icon?: IconDefinition;
|
||||
label?: string;
|
||||
@@ -34,8 +38,21 @@ export default function Select({
|
||||
...rest
|
||||
}: SelectProps) {
|
||||
const isMulti = Array.isArray(value);
|
||||
const valueLabel =
|
||||
options.find((option: any) => option.value === value)?.label || "";
|
||||
const findVal = (opts: Option[], nest = 0): Option | undefined => {
|
||||
let val: Option | undefined;
|
||||
opts.forEach((option) => {
|
||||
if (!val) {
|
||||
if (option.options) {
|
||||
val = findVal(option.options,++nest);
|
||||
} else if (option.value === value) {
|
||||
val = option
|
||||
}
|
||||
}
|
||||
});
|
||||
return val;
|
||||
};
|
||||
|
||||
const valueLabel = findVal(options)?.label || "";
|
||||
const onChange = (option: any, x: any) => {
|
||||
if (Array.isArray(option)) {
|
||||
inChange({ target: {value: option.map(({ value = "" }) => value), name, type: "select" } });
|
||||
@@ -80,9 +97,9 @@ export default function Select({
|
||||
name,
|
||||
onChange,
|
||||
options,
|
||||
...isMulti ? { value: Array.isArray(value) ?
|
||||
value.map((val: any) => ({ label: val, value: val })) : [] } :
|
||||
{ value: { label: valueLabel, value } },
|
||||
...value && (isMulti ? {
|
||||
value: Array.isArray(value) ? value.map((val: any) => ({ label: val, value: val })) : []
|
||||
} : { value: { label: valueLabel, value } }),
|
||||
...rest,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -132,4 +132,9 @@ export default function EntityInput({ accept: inAccept, aspectRatio = "square",
|
||||
}
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
};
|
||||
|
||||
// const handleTargetChange = (ev: React.FormEvent<HTMLSelectElement>) => {
|
||||
// const value = (ev.target as HTMLSelectElement).value;
|
||||
// setTarget(value);
|
||||
// };
|
||||
@@ -29,10 +29,19 @@ export enum WeightsFilters {
|
||||
vall_e
|
||||
}
|
||||
|
||||
export enum WeightsCategories {
|
||||
all,
|
||||
faceAnimation,
|
||||
tts,
|
||||
voiceConversion
|
||||
}
|
||||
|
||||
export type EntityModeProp = keyof typeof EntityInputMode;
|
||||
export type MediaFilterProp = keyof typeof MediaFilters;
|
||||
export type WeightFilterProp = keyof typeof WeightsFilters;
|
||||
export type WeightCategoriesProp = keyof typeof WeightsCategories;
|
||||
export type AcceptTypes = MediaFilterProp | WeightFilterProp;
|
||||
export type JobSelection = WeightCategoriesProp | WeightFilterProp;
|
||||
|
||||
export enum EntityType {
|
||||
unknown,
|
||||
|
||||
+77
-149
@@ -1,23 +1,35 @@
|
||||
import React, { useState } from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { Checkbox, Container, NumberSlider, Panel, TempSelect as Select } from "components/common";
|
||||
import { WebUrl } from "../../../../../common/WebUrl";
|
||||
import { BackLink } from "../../../_common/BackLink";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBomb } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
KillJobs,
|
||||
} from "@storyteller/components/src/api/moderation/queues/KillJobs";
|
||||
import { WeightsCategories, WeightsFilters } from "components/entities/EntityTypes";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
function ModerationJobControlPage(props: Props) {
|
||||
const [filter, filterSet] = useState<string | undefined>();
|
||||
const objNumKeys = (obj = {}) => Object.keys(obj)
|
||||
.filter((val: any) => isNaN(Number(val)))
|
||||
.filter((val: any) => val !== "all");
|
||||
const weightCategories = objNumKeys(WeightsCategories);
|
||||
const weightTypes = objNumKeys(WeightsFilters);
|
||||
const makeOptions = (filters: string[]) => filters.map((filter,i) => ({ label: filter, value: filter }));
|
||||
const categoryOpts = makeOptions(weightCategories);
|
||||
const typeOpts = makeOptions(weightTypes);
|
||||
const groupedOpts = [{ label: "ALL JOBS", value: "all_jobs" },{ label: "Categories", options: categoryOpts },{ label: "Types", options: typeOpts }];
|
||||
const isCategory = weightCategories.indexOf(filter || "") >= 0;
|
||||
const [killPendingJobs, setKillPendingJobs] = useState<boolean>(true);
|
||||
const [killFailedJobs, setKillFailedJobs] = useState<boolean>(true);
|
||||
const [killStartedJobs, setKillStartedJobs] = useState<boolean>(true);
|
||||
const [priorityLevel, setPriorityLevel] = useState<number>(0);
|
||||
const [target, setTarget] = useState<string>('all_jobs');
|
||||
|
||||
const killJobs= async () => {
|
||||
let jobStatuses = [];
|
||||
@@ -26,30 +38,15 @@ function ModerationJobControlPage(props: Props) {
|
||||
if (killFailedJobs) { jobStatuses.push('failed') }
|
||||
if (killStartedJobs) { jobStatuses.push('started') }
|
||||
|
||||
// NB: The backend uses parameterized enums, so the request shape here is unusual
|
||||
let requestTarget: any = 'all_jobs';
|
||||
|
||||
switch (target) {
|
||||
case 'all_jobs':
|
||||
break;
|
||||
case 'lipsync_animation':
|
||||
case 'text_to_speech':
|
||||
case 'voice_conversion':
|
||||
requestTarget = { 'category': target };
|
||||
break;
|
||||
case 'rvc_v2':
|
||||
case 'so_vits_svc':
|
||||
case 'tacotron2':
|
||||
requestTarget = { 'model_type': target };
|
||||
break;
|
||||
}
|
||||
|
||||
const request = {
|
||||
job_statuses: jobStatuses,
|
||||
target: requestTarget,
|
||||
target: filter === "all_jobs" ? filter :
|
||||
{ [ isCategory ? "category" : "model_type" ]: filter }, // *
|
||||
maybe_priority_or_lower: priorityLevel,
|
||||
};
|
||||
|
||||
// * NB: The backend uses parameterized enums, so the request shape here is unusual
|
||||
|
||||
await KillJobs(request);
|
||||
};
|
||||
|
||||
@@ -59,146 +56,77 @@ function ModerationJobControlPage(props: Props) {
|
||||
return (ev.target as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
const handlePriorityChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const level = (ev.target as HTMLInputElement).value;
|
||||
let numericLevel = parseInt(level.trim());
|
||||
if (isNaN(numericLevel)) {
|
||||
numericLevel = 0;
|
||||
} else if (numericLevel < 0) {
|
||||
numericLevel = 0;
|
||||
}
|
||||
setPriorityLevel(numericLevel);
|
||||
};
|
||||
|
||||
const handleTargetChange = (ev: React.FormEvent<HTMLSelectElement>) => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
setTarget(value);
|
||||
};
|
||||
|
||||
if (!props.sessionWrapper.canBanUsers()) {
|
||||
return <h1>Unauthorized</h1>;
|
||||
}
|
||||
|
||||
const priorityGroups = ["logged out users","free users","loyalty users","premium users"];
|
||||
let groupList = [...priorityGroups];
|
||||
|
||||
groupList.length = priorityLevel + 1;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="container py-5">
|
||||
<h1 className=" fw-bold">Job Stats</h1>
|
||||
<div className="pt-3">
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
<Container type="panel" className="mb-5">
|
||||
<PageHeader {...{
|
||||
back: { to: WebUrl.moderationMain(), label: "Back to moderation" },
|
||||
title: "Kill Jobs",
|
||||
subText: "(Only do this in emergencies!)"
|
||||
}}/>
|
||||
<Panel {...{ padding: true }}>
|
||||
<Select {...{
|
||||
className: "abc",
|
||||
label: "Kill a category or weight type",
|
||||
options: groupedOpts,
|
||||
onChange: ({ target }) => { filterSet(target.value) },
|
||||
placeholder: "Select a category or type",
|
||||
value: filter
|
||||
}}/>
|
||||
<h6>With these job statuses</h6>
|
||||
<div {...{ className: "d-flex gap-3" }}>
|
||||
<Checkbox {...{
|
||||
checked: killFailedJobs,
|
||||
label: "Failed",
|
||||
onChange: (ev: any) => setKillFailedJobs(checkboxEventStatus(ev))
|
||||
}}/>
|
||||
<Checkbox {...{
|
||||
checked: killPendingJobs,
|
||||
label: "Pending",
|
||||
onChange: (ev: any) => setKillPendingJobs(checkboxEventStatus(ev))
|
||||
}}/>
|
||||
<Checkbox {...{
|
||||
checked: killStartedJobs,
|
||||
label: "Started",
|
||||
onChange: (ev: any) => setKillStartedJobs(checkboxEventStatus(ev))
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container-panel pt-3 pb-5">
|
||||
<div className="panel p-3 p-lg-4">
|
||||
<h2 className="panel-title fw-bold">Kill Jobs (Danger Zone)</h2>
|
||||
<h5 className="fw-semibold mb-4">(Only do this in emergencies!)</h5>
|
||||
|
||||
<h5 className="fw-semibold mb-4">Job Types / Categories</h5>
|
||||
|
||||
<div>
|
||||
<label className="sub-title">Category Type</label>
|
||||
|
||||
<div className="form-group">
|
||||
<select
|
||||
onChange={handleTargetChange}
|
||||
className="form-select"
|
||||
value={target}
|
||||
>
|
||||
<option value="all_jobs">ALL JOBS !!!</option>
|
||||
<option value="lipsync_animation">Category: Lipsync Animation</option>
|
||||
<option value="text_to_speech">Category: Text-to-Speech (TT2, Voice Designer, etc.)</option>
|
||||
<option value="voice_conversion">Category: Voice Conversion (RVC, SVC, etc.)</option>
|
||||
<option value="rvc_v2">Model type: RVCv2</option>
|
||||
<option value="so_vits_svc">Model type: SVC</option>
|
||||
<option value="tacotron2">Model type: TT2</option>
|
||||
</select>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<h5 className="fw-semibold mb-4">Job Statuses</h5>
|
||||
<NumberSlider {...{
|
||||
label: "For these user priority levels or below",
|
||||
onChange: ({ target }: { target: any }) => {
|
||||
setPriorityLevel(target.value);
|
||||
},
|
||||
value: priorityLevel,
|
||||
min: 0,
|
||||
max: 3
|
||||
}}/>
|
||||
<p {...{ className: "mt-3" }}>
|
||||
This will kill jobs for { groupList.join(", ") }
|
||||
</p>
|
||||
|
||||
<div className="py-6">
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<label className="form-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={killFailedJobs}
|
||||
onChange={(ev) => setKillFailedJobs(checkboxEventStatus(ev))}
|
||||
className="form-check-input"
|
||||
/>
|
||||
Kill Failed Jobs
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<label className="form-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={killPendingJobs}
|
||||
onChange={(ev) => setKillPendingJobs(checkboxEventStatus(ev))}
|
||||
className="form-check-input"
|
||||
/>
|
||||
Kill Pending Jobs
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<label className="form-check-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={killStartedJobs}
|
||||
onChange={(ev) => setKillStartedJobs(checkboxEventStatus(ev))}
|
||||
className="form-check-input"
|
||||
/>
|
||||
Kill Started Jobs
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<h5 className="fw-semibold mb-4">Job Priorities</h5>
|
||||
|
||||
<div>
|
||||
<label className="sub-title">Kill Priority Level(s) and Below</label>
|
||||
<div className="form-group">
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder="Priority Level"
|
||||
value={priorityLevel}
|
||||
onChange={handlePriorityChange}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div>eg. setting this to "3" will kill all priorities 3 and below (3, 2, 1, and 0).</div>
|
||||
<br />
|
||||
<ul>
|
||||
<li>Level 0 = logged out</li>
|
||||
<li>Level 1 = free account</li>
|
||||
<li>Level 2+ = premium or loyalty user</li>
|
||||
</ul>
|
||||
<br />
|
||||
<div>Try not to kill premium user workloads unless things get bad.</div>
|
||||
</div>
|
||||
|
||||
<div className="py-6">
|
||||
<div className="d-flex flex-column gap-3">
|
||||
|
||||
<button
|
||||
className="btn btn-destructive w-100"
|
||||
onClick={() => killJobs()}
|
||||
>
|
||||
Kill Jobs
|
||||
<FontAwesomeIcon icon={faBomb} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-destructive w-100"
|
||||
onClick={() => killJobs()}
|
||||
>
|
||||
Kill Jobs
|
||||
<FontAwesomeIcon icon={faBomb} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container pb-5">
|
||||
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user