tts config pages

This commit is contained in:
BFlat committed 2022-11-29 21:42:37 +07:00
1 parent c388ac1407
commit 9dcd247671
15 files changed
+1058 -853

No files matched your search

@@ -94,6 +94,10 @@ p {
color: rgba(255, 255, 255, 0.85);
}
ul {
margin-bottom: 0;
}
.fw-semibold {
font-weight: 500;
}
@@ -1,47 +1,56 @@
import React, { useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
import { TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/GetTwitchEventRule';
import { Link, useHistory, useParams } from 'react-router-dom';
import { TwitchEventCategory, TWITCH_EVENT_CATEGORY_BY_STRING } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory';
import { EventResponse } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse';
import { EventMatchPredicate } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate';
import { CreateTwitchEventRule, CreateTwitchEventRuleRequest } from '@storyteller/components/src/api/storyteller/twitch_event_rules/CreateTwitchEventRule';
import { EventMatchPredicateBuilderComponent } from './event_match_predicate_builder/EventMatchPredicateBuilderComponent';
import { EventResponseComponent } from './event_response_builder/EventResponseComponent';
import { TwitchEventRuleElement } from './rule_cards/TwitchEventRuleElement';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAngleLeft, faSave } from '@fortawesome/free-solid-svg-icons';
import React, { useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { TwitchEventRule } from "@storyteller/components/src/api/storyteller/twitch_event_rules/GetTwitchEventRule";
import { Link, useHistory, useParams } from "react-router-dom";
import {
TwitchEventCategory,
TWITCH_EVENT_CATEGORY_BY_STRING,
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory";
import { EventResponse } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse";
import { EventMatchPredicate } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate";
import {
CreateTwitchEventRule,
CreateTwitchEventRuleRequest,
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/CreateTwitchEventRule";
import { EventMatchPredicateBuilderComponent } from "./event_match_predicate_builder/EventMatchPredicateBuilderComponent";
import { EventResponseComponent } from "./event_response_builder/EventResponseComponent";
import { TwitchEventRuleElement } from "./rule_cards/TwitchEventRuleElement";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleLeft, faSave } from "@fortawesome/free-solid-svg-icons";
interface Props {
sessionWrapper: SessionWrapper,
allTtsModels: TtsModelListItem[],
allTtsModelsByToken: Map<string, TtsModelListItem>,
sessionWrapper: SessionWrapper;
allTtsModels: TtsModelListItem[];
allTtsModelsByToken: Map<string, TtsModelListItem>;
}
function TtsConfigsCreateRulePage(props: Props) {
const { event_category } = useParams() as { event_category : string };
const { event_category } = useParams() as { event_category: string };
const history = useHistory();
// TODO: Use centralized configs
const indexLink = '/tts_configs';
const indexLink = "/tts_configs";
// ========== In-Progress Model Edits ==========
// Sent to sub-components to initialize UI
// NB: This is a HACK. The subcomponent views were originally created for the "Edit" UI
// and this had to be done to get them to work for "Create" UI. Sending empty dictionaries
// NB: This is a HACK. The subcomponent views were originally created for the "Edit" UI
// and this had to be done to get them to work for "Create" UI. Sending empty dictionaries
// or statically initialized values froze the UI.
const [serverEventMatchPredicate] = useState<EventMatchPredicate>({});
const [serverEventResponse] = useState<EventResponse>({});
const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] = useState<EventMatchPredicate>({});
const [modifiedEventResponse, setModifiedEventResponse] = useState<EventResponse>({});
const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] =
useState<EventMatchPredicate>({});
const [modifiedEventResponse, setModifiedEventResponse] =
useState<EventResponse>({});
const [ruleIsDisabled] = useState(false);
let maybeTwitchEventCategory = TWITCH_EVENT_CATEGORY_BY_STRING.get(event_category);
let maybeTwitchEventCategory =
TWITCH_EVENT_CATEGORY_BY_STRING.get(event_category);
if (maybeTwitchEventCategory === undefined) {
history.push(indexLink);
@@ -51,15 +60,19 @@ function TtsConfigsCreateRulePage(props: Props) {
// NB: To satisfy the type system and not be a union on 'undefined'
const twitchEventCategory = maybeTwitchEventCategory;
const updateModifiedEventMatchPredicate = (predicate: EventMatchPredicate) => {
const updateModifiedEventMatchPredicate = (
predicate: EventMatchPredicate
) => {
setModifiedEventMatchPredicate(predicate);
}
};
const updateModifiedEventResponse = (response: EventResponse) => {
setModifiedEventResponse(response);
}
};
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
const handleFormSubmit = async (
ev: React.FormEvent<HTMLFormElement>
): Promise<boolean> => {
ev.preventDefault();
// TODO: Check for errors.
@@ -69,7 +82,7 @@ function TtsConfigsCreateRulePage(props: Props) {
// TODO: UUID idempotency token that updates on every model change.
const request : CreateTwitchEventRuleRequest = {
const request: CreateTwitchEventRuleRequest = {
idempotency_token: uuidv4(),
event_category: twitchEventCategory,
event_match_predicate: newEventMatchPredicate,
@@ -84,16 +97,16 @@ function TtsConfigsCreateRulePage(props: Props) {
}
return false;
}
};
if (!props.sessionWrapper.isLoggedIn()) {
return <h1>Must Log In</h1>;
}
// NB: This is a hypothetical version of what we'll update to
let renderRule : TwitchEventRule = {
let renderRule: TwitchEventRule = {
// Not yet saved, so fake values
token: 'virtual',
token: "virtual",
user_specified_rule_order: 1000,
created_at: new Date(),
updated_at: new Date(),
@@ -106,62 +119,57 @@ function TtsConfigsCreateRulePage(props: Props) {
};
const titlesByCategory = new Map<TwitchEventCategory, string>([
[TwitchEventCategory.Bits, 'Create New Bits / Cheers Rule'],
[TwitchEventCategory.ChannelPoints, 'Create New Channel Points Rule'],
[TwitchEventCategory.Bits, "Create New Bits Rule"],
[TwitchEventCategory.ChannelPoints, "Create New Channel Points Rule"],
]);
const pageTitle = titlesByCategory.get(twitchEventCategory);
return (
<>
<div className="section">
<h1 className="title"> {pageTitle} </h1>
<div className="pt-5 container pb-2">
<h1 className="fw-bold mt-5 pt-lg-5">
<span className="word">{pageTitle}</span>
</h1>
</div>
<br />
<br />
<form onSubmit={handleFormSubmit}>
<EventMatchPredicateBuilderComponent
twitchEventCategory={twitchEventCategory}
serverEventMatchPredicate={serverEventMatchPredicate}
updateModifiedEventMatchPredicate={updateModifiedEventMatchPredicate}
<div className="container mt-5">
<form onSubmit={handleFormSubmit} className="d-flex flex-column gap-4">
<EventMatchPredicateBuilderComponent
twitchEventCategory={twitchEventCategory}
serverEventMatchPredicate={serverEventMatchPredicate}
updateModifiedEventMatchPredicate={
updateModifiedEventMatchPredicate
}
/>
<br />
<br />
<EventResponseComponent
serverEventResponse={serverEventResponse}
updateModifiedEventResponse={updateModifiedEventResponse}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
<h2 className="title is-4">This is the rule:</h2>
<div className="content">
<TwitchEventRuleElement
rule={renderRule}
hideButtons={true}
<EventResponseComponent
serverEventResponse={serverEventResponse}
updateModifiedEventResponse={updateModifiedEventResponse}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
</div>
/>
<button className="button is-large is-fullwidth is-primary">
Save New Rule&nbsp;<FontAwesomeIcon icon={faSave} />
</button>
</form>
<br />
<h2 className="fw-bold mt-4">This is the rule:</h2>
<Link to={indexLink} className="button is-large is-fullwidth is-info is-outlined">
<FontAwesomeIcon icon={faAngleLeft} />&nbsp;Cancel / Go Back
</Link>
<TwitchEventRuleElement
rule={renderRule}
hideButtons={true}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
<button className="btn btn-primary">
<FontAwesomeIcon icon={faSave} className="me-2" />
Save New Rule
</button>
</form>
<Link to={indexLink} className="btn btn-secondary mt-3 mb-5">
<FontAwesomeIcon icon={faAngleLeft} className="me-2" />
Cancel / Go Back
</Link>
</div>
</>
)
);
}
export { TtsConfigsCreateRulePage }
export { TtsConfigsCreateRulePage };
@@ -1,43 +1,58 @@
import React, { useCallback, useEffect, useState } from 'react';
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
import { GetTwitchEventRule, GetTwitchEventRuleIsError, GetTwitchEventRuleIsOk, TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/GetTwitchEventRule';
import { EditTwitchEventRule, EditTwitchEventRuleRequest } from '@storyteller/components/src/api/storyteller/twitch_event_rules/EditTwitchEventRule';
import { TwitchEventRuleElement } from './rule_cards/TwitchEventRuleElement';
import { Link, useHistory, useParams } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAngleLeft, faSave } from '@fortawesome/free-solid-svg-icons';
import { EventMatchPredicate } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate';
import { EventResponse } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse';
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
import { EventResponseComponent } from './event_response_builder/EventResponseComponent';
import { EventMatchPredicateBuilderComponent } from './event_match_predicate_builder/EventMatchPredicateBuilderComponent';
import React, { useCallback, useEffect, useState } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import {
GetTwitchEventRule,
GetTwitchEventRuleIsError,
GetTwitchEventRuleIsOk,
TwitchEventRule,
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/GetTwitchEventRule";
import {
EditTwitchEventRule,
EditTwitchEventRuleRequest,
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/EditTwitchEventRule";
import { TwitchEventRuleElement } from "./rule_cards/TwitchEventRuleElement";
import { Link, useHistory, useParams } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleLeft, faSave } from "@fortawesome/free-solid-svg-icons";
import { EventMatchPredicate } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate";
import { EventResponse } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { EventResponseComponent } from "./event_response_builder/EventResponseComponent";
import { EventMatchPredicateBuilderComponent } from "./event_match_predicate_builder/EventMatchPredicateBuilderComponent";
interface Props {
sessionWrapper: SessionWrapper,
allTtsModels: TtsModelListItem[],
allTtsModelsByToken: Map<string, TtsModelListItem>,
sessionWrapper: SessionWrapper;
allTtsModels: TtsModelListItem[];
allTtsModelsByToken: Map<string, TtsModelListItem>;
}
function TtsConfigsEditRulePage(props: Props) {
const { token } : { token : string } = useParams();
const { token }: { token: string } = useParams();
// TODO: Use centralized URL configs
const indexLink = '/tts_configs';
const indexLink = "/tts_configs";
const history = useHistory();
// ========== Initial Load of Server State ==========
const [twitchEventRule, setTwitchEventRule] = useState<TwitchEventRule|undefined>(undefined);
const [twitchEventRule, setTwitchEventRule] = useState<
TwitchEventRule | undefined
>(undefined);
// Sent to sub-components to initialize UI
const [serverEventMatchPredicate, setServerEventMatchPredicate] = useState<EventMatchPredicate>({});
const [serverEventResponse, setServerEventResponse] = useState<EventResponse>({});
const [serverEventMatchPredicate, setServerEventMatchPredicate] =
useState<EventMatchPredicate>({});
const [serverEventResponse, setServerEventResponse] = useState<EventResponse>(
{}
);
// ========== In-Progress Model Edits ==========
const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] = useState<EventMatchPredicate>({});
const [modifiedEventResponse, setModifiedEventResponse] = useState<EventResponse>({});
const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] =
useState<EventMatchPredicate>({});
const [modifiedEventResponse, setModifiedEventResponse] =
useState<EventResponse>({});
const [ruleIsDisabled, setRuleIsDisabled] = useState(false);
const getTwitchEventRule = useCallback(async (token: string) => {
@@ -46,15 +61,18 @@ function TtsConfigsEditRulePage(props: Props) {
if (GetTwitchEventRuleIsOk(response)) {
// Source of truth
setTwitchEventRule(response.twitch_event_rule);
setServerEventMatchPredicate(response.twitch_event_rule.event_match_predicate);
setServerEventMatchPredicate(
response.twitch_event_rule.event_match_predicate
);
setServerEventResponse(response.twitch_event_rule.event_response);
// In-progress modifications
setModifiedEventMatchPredicate(response.twitch_event_rule.event_match_predicate);
setModifiedEventMatchPredicate(
response.twitch_event_rule.event_match_predicate
);
setModifiedEventResponse(response.twitch_event_rule.event_response);
setRuleIsDisabled(response.twitch_event_rule.rule_is_disabled);
} else if (GetTwitchEventRuleIsError(response)) {
} else if (GetTwitchEventRuleIsError(response)) {
// TODO
}
}, []);
@@ -63,15 +81,19 @@ function TtsConfigsEditRulePage(props: Props) {
getTwitchEventRule(token);
}, [getTwitchEventRule, token]);
const updateModifiedEventMatchPredicate = (predicate: EventMatchPredicate) => {
const updateModifiedEventMatchPredicate = (
predicate: EventMatchPredicate
) => {
setModifiedEventMatchPredicate(predicate);
}
};
const updateModifiedEventResponse = (response: EventResponse) => {
setModifiedEventResponse(response);
}
};
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
const handleFormSubmit = async (
ev: React.FormEvent<HTMLFormElement>
): Promise<boolean> => {
ev.preventDefault();
// TODO: Check for errors.
@@ -79,7 +101,7 @@ function TtsConfigsEditRulePage(props: Props) {
let newEventMatchPredicate = modifiedEventMatchPredicate;
let newEventResponse = modifiedEventResponse;
const request : EditTwitchEventRuleRequest = {
const request: EditTwitchEventRuleRequest = {
event_match_predicate: newEventMatchPredicate,
event_response: newEventResponse,
rule_is_disabled: ruleIsDisabled,
@@ -91,7 +113,7 @@ function TtsConfigsEditRulePage(props: Props) {
}
return false;
}
};
if (!props.sessionWrapper.isLoggedIn()) {
return <h1>Must Log In</h1>;
@@ -102,7 +124,7 @@ function TtsConfigsEditRulePage(props: Props) {
}
// NB: This is a hypothetical version of what we'll update to
let renderRule : TwitchEventRule = {
let renderRule: TwitchEventRule = {
// Unchanged
token: twitchEventRule.token,
event_category: twitchEventRule.event_category,
@@ -118,54 +140,52 @@ function TtsConfigsEditRulePage(props: Props) {
return (
<>
<div className="section">
<h1 className="title"> Edit Rule </h1>
<div className="pt-5 container">
<h1 className="fw-bold mt-5 pt-lg-5">
<span className="word">Edit Rule</span>
</h1>
</div>
<br />
<br />
<form onSubmit={handleFormSubmit}>
<EventMatchPredicateBuilderComponent
twitchEventCategory={twitchEventRule.event_category}
serverEventMatchPredicate={serverEventMatchPredicate}
updateModifiedEventMatchPredicate={updateModifiedEventMatchPredicate}
<div className="container mt-5">
<form onSubmit={handleFormSubmit} className="d-flex flex-column gap-4">
<EventMatchPredicateBuilderComponent
twitchEventCategory={twitchEventRule.event_category}
serverEventMatchPredicate={serverEventMatchPredicate}
updateModifiedEventMatchPredicate={
updateModifiedEventMatchPredicate
}
/>
<br />
<br />
<EventResponseComponent
serverEventResponse={serverEventResponse}
updateModifiedEventResponse={updateModifiedEventResponse}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
<h2 className="title is-4">This is the rule:</h2>
<div className="content">
<TwitchEventRuleElement
rule={renderRule}
hideButtons={true}
<EventResponseComponent
serverEventResponse={serverEventResponse}
updateModifiedEventResponse={updateModifiedEventResponse}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
<h2 className="fw-bold mt-4">This is the rule:</h2>
<div className="content">
<TwitchEventRuleElement
rule={renderRule}
hideButtons={true}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
</div>
</div>
<button className="button is-large is-fullwidth is-primary">
Save Changes&nbsp;<FontAwesomeIcon icon={faSave} />
</button>
</form>
<br />
<Link to={indexLink} className="button is-large is-fullwidth is-info is-outlined">
<FontAwesomeIcon icon={faAngleLeft} />&nbsp;Cancel / Go Back
</Link>
<button className="btn btn-primary">
<FontAwesomeIcon icon={faSave} className="me-2" />
Save Changes
</button>
</form>
<Link to={indexLink} className="btn btn-secondary mt-3 mb-5">
<FontAwesomeIcon icon={faAngleLeft} className="me-2" />
Cancel / Go Back
</Link>
</div>
</>
)
);
}
export { TtsConfigsEditRulePage }
export { TtsConfigsEditRulePage };
@@ -8,20 +8,12 @@ import {
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules";
import { TwitchEventRuleElement } from "./rule_cards/TwitchEventRuleElement";
import { TwitchEventCategory } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory";
import { DiscordLink } from "@storyteller/components/src/elements/DiscordLink";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faBox,
faDonate,
faFilm,
faGem,
faHeart,
faLightbulb,
faMeteor,
faPlus,
faSort,
faTerminal,
faUsers,
} from "@fortawesome/free-solid-svg-icons";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { Link } from "react-router-dom";
@@ -1,60 +1,80 @@
import React, { useCallback, useEffect, useState } from 'react';
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
import { ListTwitchEventRules, ListTwitchEventRulesIsError, ListTwitchEventRulesIsOk, TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules';
import { ReorderTwitchEventRules, ReorderTwitchEventRulesRequest } from '@storyteller/components/src/api/storyteller/twitch_event_rules/ReorderTwitchEventRules';
import { TwitchEventCategory, TWITCH_EVENT_CATEGORY_BY_STRING } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSave } from '@fortawesome/free-solid-svg-icons';
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
import { useHistory, useParams } from 'react-router-dom';
import { ReorderableTwitchEventRuleElement } from './rule_cards/ReorderableTwitchEventRuleElement';
import React, { useCallback, useEffect, useState } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import {
ListTwitchEventRules,
ListTwitchEventRulesIsError,
ListTwitchEventRulesIsOk,
TwitchEventRule,
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules";
import {
ReorderTwitchEventRules,
ReorderTwitchEventRulesRequest,
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/ReorderTwitchEventRules";
import {
TwitchEventCategory,
TWITCH_EVENT_CATEGORY_BY_STRING,
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSave } from "@fortawesome/free-solid-svg-icons";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { useHistory, useParams } from "react-router-dom";
import { ReorderableTwitchEventRuleElement } from "./rule_cards/ReorderableTwitchEventRuleElement";
interface Props {
sessionWrapper: SessionWrapper,
allTtsModelsByToken: Map<string, TtsModelListItem>,
sessionWrapper: SessionWrapper;
allTtsModelsByToken: Map<string, TtsModelListItem>;
}
function TtsConfigsReorderPage(props: Props) {
const { event_category } = useParams() as { event_category : string };
const { event_category } = useParams() as { event_category: string };
const history = useHistory();
// TODO: Use centralized configs
const indexLink = '/tts_configs';
const indexLink = "/tts_configs";
const [twitchEventRules, setTwitchEventRules] = useState<TwitchEventRule[]>([]);
const [twitchEventRules, setTwitchEventRules] = useState<TwitchEventRule[]>(
[]
);
const listTwitchEventRules = useCallback(async (twitchEventCategory: TwitchEventCategory) => {
const response = await ListTwitchEventRules();
const listTwitchEventRules = useCallback(
async (twitchEventCategory: TwitchEventCategory) => {
const response = await ListTwitchEventRules();
if (ListTwitchEventRulesIsOk(response)) {
const allRules = response.twitch_event_rules;
const releventRules = allRules.filter(rule => rule.event_category === twitchEventCategory);
if (ListTwitchEventRulesIsOk(response)) {
const allRules = response.twitch_event_rules;
const releventRules = allRules.filter(
(rule) => rule.event_category === twitchEventCategory
);
setTwitchEventRules(releventRules);
} else if (ListTwitchEventRulesIsError(response)) {
// TODO
}
}, []);
setTwitchEventRules(releventRules);
} else if (ListTwitchEventRulesIsError(response)) {
// TODO
}
},
[]
);
useEffect(() => {
console.log('useEffect');
let maybeTwitchEventCategory = TWITCH_EVENT_CATEGORY_BY_STRING.get(event_category);
console.log("useEffect");
let maybeTwitchEventCategory =
TWITCH_EVENT_CATEGORY_BY_STRING.get(event_category);
if (maybeTwitchEventCategory === undefined) {
history.push(indexLink);
} else {
listTwitchEventRules(maybeTwitchEventCategory);
}
}, [listTwitchEventRules, event_category, history]);
const handleMoveUp = (ruleIndex: number) => {
let newEventRules = [...twitchEventRules];
if (ruleIndex >= newEventRules.length || // Walk off end
ruleIndex === 0) { // Can't move up.
if (
ruleIndex >= newEventRules.length || // Walk off end
ruleIndex === 0
) {
// Can't move up.
return false;
}
@@ -65,13 +85,16 @@ function TtsConfigsReorderPage(props: Props) {
newEventRules[ruleIndex] = aboveRule;
setTwitchEventRules(newEventRules);
}
};
const handleMoveDown = (ruleIndex: number) => {
let newEventRules = [...twitchEventRules];
if (ruleIndex < 0 || // Invalid
ruleIndex >= newEventRules.length - 1) { // Can't move down.
if (
ruleIndex < 0 || // Invalid
ruleIndex >= newEventRules.length - 1
) {
// Can't move down.
return false;
}
@@ -85,18 +108,19 @@ function TtsConfigsReorderPage(props: Props) {
newEventRules[ruleIndex] = belowRule;
setTwitchEventRules(newEventRules);
}
const handleSaveOrderings = async (ev: React.FormEvent<HTMLButtonElement>) : Promise<boolean> => {
};
const handleSaveOrderings = async (
ev: React.FormEvent<HTMLButtonElement>
): Promise<boolean> => {
const pairs = twitchEventRules.map((rule, index) => {
return {
rule_token: rule.token,
position: index,
}
})
};
});
const request : ReorderTwitchEventRulesRequest = {
const request: ReorderTwitchEventRulesRequest = {
rule_token_position_pairs: pairs,
};
@@ -106,7 +130,7 @@ function TtsConfigsReorderPage(props: Props) {
}
return false;
}
};
if (!props.sessionWrapper.isLoggedIn()) {
return <h1>Must Log In</h1>;
@@ -114,44 +138,51 @@ function TtsConfigsReorderPage(props: Props) {
return (
<>
<div className="section">
<h1 className="title"> Reorder Rules </h1>
<h2 className="subtitle"> This affects match priority. Rules at the top come first. </h2>
<div className="container pt-5">
<h1 className="fw-bold mt-5 pt-lg-5 mb-4">
<span className="word">Reorder Rules</span>
</h1>
<h2 className="fw-bold">
{" "}
This affects match priority. Rules at the top come first.{" "}
</h2>
</div>
<div className="content">
<article className="message is-dark">
<div className="message-body">
You can create rules for matching <em>different categories of events</em>: cheers (bits),
channel point rewards, etc.
When an event occurs in your stream, we compare it against the rules in its
category. <strong>The first matched rule</strong> (from the top) is selected, and the action
that it specifies is taken. For now this is limited to FakeYou text to speech, but we'll be
adding lots of new capabilities soon.
</div>
</article>
<div className="container">
<p className="pb-5">
You can create rules for matching{" "}
<em>different categories of events</em>: cheers (bits), channel point
rewards, etc. When an event occurs in your stream, we compare it
against the rules in its category.{" "}
<strong>The first matched rule</strong> (from the top) is selected,
and the action that it specifies is taken. For now this is limited to
FakeYou text to speech, but we'll be adding lots of new capabilities
soon.
</p>
{twitchEventRules.map((rule, index) => {
return <ReorderableTwitchEventRuleElement
key={rule.token}
rule={rule}
ruleIndex={index}
handleMoveUp={handleMoveUp}
handleMoveDown={handleMoveDown}
allTtsModelsByToken={props.allTtsModelsByToken}
return (
<ReorderableTwitchEventRuleElement
key={rule.token}
rule={rule}
ruleIndex={index}
handleMoveUp={handleMoveUp}
handleMoveDown={handleMoveDown}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
);
})}
</div>
<button
className="button is-large is-fullwidth is-primary"
onClick={handleSaveOrderings}
<button
className="btn btn-primary w-100 my-5"
onClick={handleSaveOrderings}
>
Save Orderings &nbsp;<FontAwesomeIcon icon={faSave} />
</button>
<FontAwesomeIcon icon={faSave} className="me-2" />
Save Orderings
</button>
</div>
</>
)
);
}
export { TtsConfigsReorderPage }
export { TtsConfigsReorderPage };
@@ -1,36 +1,49 @@
import React, { useEffect, useState } from 'react';
import { BitsRuleType } from './types/BitsRuleType';
import { TwitchEventCategory } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory';
import { EventMatchPredicate } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate';
import { BitsCheermoteNameExactMatchForm } from './subforms/BitsCheermoteNameExactMatchForm';
import { BitsCheermotePrefixSpendThresholdForm } from './subforms/BitsCheermotePrefixSpendThresholdForm';
import { BitsSpendThresholdForm } from './subforms/BitsSpendThresholdForm';
import { ChannelPointsRewardNameExactMatchForm } from './subforms/ChannelPointsRewardNameExactMatchForm';
import { ChannelPointsRuleType } from './types/ChannelPointsRuleType';
import { CheerUtil } from '../../../twitch/CheerUtil';
import { CheerState, CheerStateIsCustom, CheerStateIsOfficial, cheerStateToPredicate, predicateToCheerState } from './CheerState';
import { CHEER_LOOKUP_MAP } from '../../../twitch/Cheers';
import React, { useEffect, useState } from "react";
import { BitsRuleType } from "./types/BitsRuleType";
import { TwitchEventCategory } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory";
import { EventMatchPredicate } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate";
import { BitsCheermoteNameExactMatchForm } from "./subforms/BitsCheermoteNameExactMatchForm";
import { BitsCheermotePrefixSpendThresholdForm } from "./subforms/BitsCheermotePrefixSpendThresholdForm";
import { BitsSpendThresholdForm } from "./subforms/BitsSpendThresholdForm";
import { ChannelPointsRewardNameExactMatchForm } from "./subforms/ChannelPointsRewardNameExactMatchForm";
import { ChannelPointsRuleType } from "./types/ChannelPointsRuleType";
import { CheerUtil } from "../../../twitch/CheerUtil";
import {
CheerState,
CheerStateIsCustom,
CheerStateIsOfficial,
cheerStateToPredicate,
predicateToCheerState,
} from "./CheerState";
import { CHEER_LOOKUP_MAP } from "../../../twitch/Cheers";
// NB: This was created for "Edit" view, but we've forced it to work for "Create" view by hardcoding
// NB: This was created for "Edit" view, but we've forced it to work for "Create" view by hardcoding
// values that don't actually come from the server.
interface EventMatchPredicateBuilderComponentProps {
// CANNOT BE CHANGED AFTER CREATION
twitchEventCategory: TwitchEventCategory,
twitchEventCategory: TwitchEventCategory;
// Original event match predicate from the server
// Original event match predicate from the server
// (also immutable for sanity and because we lose information on UI changes)
serverEventMatchPredicate: EventMatchPredicate,
serverEventMatchPredicate: EventMatchPredicate;
// Updates sent back up the tree
updateModifiedEventMatchPredicate: (predicate: EventMatchPredicate) => void,
};
updateModifiedEventMatchPredicate: (predicate: EventMatchPredicate) => void;
}
function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderComponentProps) {
function EventMatchPredicateBuilderComponent(
props: EventMatchPredicateBuilderComponentProps
) {
// ========== Core UI flow ==========
const [bitsRuleType, setBitsRuleType] = useState<BitsRuleType>(BitsRuleType.BitsCheermoteNameExactMatch);
const [channelPointsRuleType, setChannelPointsRuleType] = useState<ChannelPointsRuleType>(ChannelPointsRuleType.ChannelPointsRewardNameExactMatch);
const [bitsRuleType, setBitsRuleType] = useState<BitsRuleType>(
BitsRuleType.BitsCheermoteNameExactMatch
);
const [channelPointsRuleType, setChannelPointsRuleType] =
useState<ChannelPointsRuleType>(
ChannelPointsRuleType.ChannelPointsRewardNameExactMatch
);
// ========== Cached Values for Editing ==========
@@ -41,15 +54,16 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
// Used in:
// ChannelPointsRewardNameExactMatch
const [rewardName, setRewardName] = useState('');
const [rewardName, setRewardName] = useState("");
// NB: useState is not always setting from props correctly (after several re-renders)
// The following answers suggests using useEffect:
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
// https://stackoverflow.com/a/62982753
useEffect(() => {
let newBitsRuleType = BitsRuleType.BitsCheermoteNameExactMatch;
let newChannelPointsRuleType = ChannelPointsRuleType.ChannelPointsRewardNameExactMatch;
let newBitsRuleType = BitsRuleType.BitsCheermoteNameExactMatch;
let newChannelPointsRuleType =
ChannelPointsRuleType.ChannelPointsRewardNameExactMatch;
let newCheerState = predicateToCheerState(props.serverEventMatchPredicate);
@@ -57,16 +71,26 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
case TwitchEventCategory.Bits:
if (!!props.serverEventMatchPredicate.bits_cheermote_name_exact_match) {
newBitsRuleType = BitsRuleType.BitsCheermoteNameExactMatch;
} else if (!!props.serverEventMatchPredicate.bits_cheermote_prefix_spend_threshold) {
} else if (
!!props.serverEventMatchPredicate
.bits_cheermote_prefix_spend_threshold
) {
newBitsRuleType = BitsRuleType.BitsCheermotePrefixSpendThreshold;
} else if (!!props.serverEventMatchPredicate.bits_spend_threshold) {
newBitsRuleType = BitsRuleType.BitsSpendThreshold;
}
break;
case TwitchEventCategory.ChannelPoints: // NB: Only one rule type
if (!!props.serverEventMatchPredicate.channel_points_reward_name_exact_match) {
setRewardName(props.serverEventMatchPredicate.channel_points_reward_name_exact_match.reward_name);
newChannelPointsRuleType = ChannelPointsRuleType.ChannelPointsRewardNameExactMatch;
if (
!!props.serverEventMatchPredicate
.channel_points_reward_name_exact_match
) {
setRewardName(
props.serverEventMatchPredicate
.channel_points_reward_name_exact_match.reward_name
);
newChannelPointsRuleType =
ChannelPointsRuleType.ChannelPointsRewardNameExactMatch;
}
break;
case TwitchEventCategory.ChatCommand: // TODO: Not yet supported
@@ -78,82 +102,90 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
setBitsRuleType(newBitsRuleType);
setChannelPointsRuleType(newChannelPointsRuleType);
}, [props.twitchEventCategory, props.serverEventMatchPredicate]);
// TODO: We need to recalculate the model.
const handleChangedBitsRuleType = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const handleChangedBitsRuleType = (
ev: React.FormEvent<HTMLSelectElement>
): boolean => {
const value = (ev.target as HTMLSelectElement).value;
const newRuleType = value as BitsRuleType;
let predicate = cheerStateToPredicate(cheerState, newRuleType);
setBitsRuleType(newRuleType);
props.updateModifiedEventMatchPredicate(predicate);
return true;
}
};
const handleChangedChannelPointsRuleType = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const handleChangedChannelPointsRuleType = (
ev: React.FormEvent<HTMLSelectElement>
): boolean => {
const value = (ev.target as HTMLSelectElement).value;
const ruleType = value as ChannelPointsRuleType;
setChannelPointsRuleType(ruleType);
return true;
}
};
const handleChangedCheerPrefix = (newPrefix: string) => {
// 1) Update Cheer State
// 2) Update NewEventMatchPredicate
let newCheerState : CheerState = {};
let newCheerState: CheerState = {};
let maybeCheerPrefix = CHEER_LOOKUP_MAP.get(newPrefix || '');
let maybeCheerPrefix = CHEER_LOOKUP_MAP.get(newPrefix || "");
if (!!maybeCheerPrefix) {
newCheerState = {
cheerPrefix: maybeCheerPrefix,
bits: cheerState.bits, // Unchanged
}
};
} else {
newCheerState = {
cheerFull: newPrefix,
bits: cheerState.bits, // Unchanged
}
};
}
let predicate = cheerStateToPredicate(newCheerState, bitsRuleType);
setCheerState(newCheerState);
props.updateModifiedEventMatchPredicate(predicate);
}
};
const handleChangedCheerName = (name: string) => {
// 1) Update Cheer State
// 2) Update NewEventMatchPredicate
let newCheerState : CheerState = {};
let newCheerState: CheerState = {};
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(name);
let newBitValue = bitValue;
let maybeCheerPrefix = CHEER_LOOKUP_MAP.get(cheerPrefix || '');
let maybeCheerPrefix = CHEER_LOOKUP_MAP.get(cheerPrefix || "");
if (!!maybeCheerPrefix) {
// Official prefix.
newCheerState = {
cheerPrefix: maybeCheerPrefix,
bits: newBitValue,
}
};
} else if (!!cheerPrefix) {
// Custom prefix.
// Test if hypothetically we should retain an independent of name bit value.
// Hypothetically, Sus13 -> Sus12, but the bit value is 5000 in a separate field.
// Hypothetically, Sus13 -> Sus12, but the bit value is 5000 in a separate field.
// We don't want to repalace with 13.
if (CheerStateIsCustom(cheerState)) {
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(cheerState.cheerFull || '');
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(
cheerState.cheerFull || ""
);
if ((!!cheerPrefix && !!bitValue) // We have a complete name
&& (bitValue !== cheerState.bits)) // The bit value is not independent of the cheer name, as in eg. (Foobar1000, 1000)
{
if (
!!cheerPrefix &&
!!bitValue && // We have a complete name
bitValue !== cheerState.bits
) {
// The bit value is not independent of the cheer name, as in eg. (Foobar1000, 1000)
// Keep the old value.
newBitValue = cheerState.bits;
}
@@ -161,15 +193,15 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
newCheerState = {
cheerFull: name, // Should always be the full value
bits: newBitValue,
}
}
bits: newBitValue,
};
}
let predicate = cheerStateToPredicate(newCheerState, bitsRuleType);
setCheerState(newCheerState);
props.updateModifiedEventMatchPredicate(predicate);
}
};
const handleChangedMinimumBitsSpent = (minimumSpent: number) => {
// 1) Update Cheer State
@@ -180,10 +212,12 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
newCheerState = {
cheerPrefix: cheerState.cheerPrefix, // Unchanged
bits: minimumSpent,
}
};
} else if (CheerStateIsCustom(cheerState)) {
// Test if the cheer name includes numbers, eg. 'Sus12', or '1984', that differ from bit spend
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(cheerState.cheerFull || '');
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(
cheerState.cheerFull || ""
);
let cheerFull = cheerState.cheerFull;
if (!!cheerPrefix && !!bitValue && bitValue === cheerState.bits) {
@@ -194,118 +228,125 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
newCheerState = {
cheerFull: cheerFull,
bits: minimumSpent,
}
};
} else {
newCheerState = {
bits: minimumSpent,
}
};
}
let predicate = cheerStateToPredicate(newCheerState, bitsRuleType);
setCheerState(newCheerState);
props.updateModifiedEventMatchPredicate(predicate);
}
};
// TODO
const handleChangedRewardName = (name: string) => {
let predicate : EventMatchPredicate = {};
let predicate: EventMatchPredicate = {};
switch (channelPointsRuleType) {
case ChannelPointsRuleType.ChannelPointsRewardNameExactMatch:
predicate.channel_points_reward_name_exact_match = {
reward_name: name, // New value
}
};
break;
}
props.updateModifiedEventMatchPredicate(predicate);
}
};
let ruleTypeSelect = <></>
let matchingRulesForm = <></>
let ruleTypeSelect = <></>;
let matchingRulesForm = <></>;
if (props.twitchEventCategory === TwitchEventCategory.Bits) {
ruleTypeSelect = (
<>
<select
<select
value={bitsRuleType}
onChange={handleChangedBitsRuleType}>
<option
value={BitsRuleType.BitsSpendThreshold}
>Bits Spend Threshold</option>
<option
value={BitsRuleType.BitsCheermoteNameExactMatch}
>Cheermote Name (Exact Match)</option>
<option
value={BitsRuleType.BitsCheermotePrefixSpendThreshold}
>Cheermote Prefix and Bits Spend Threshold</option>
onChange={handleChangedBitsRuleType}
className="form-select"
>
<option value={BitsRuleType.BitsSpendThreshold}>
Bits Spend Threshold
</option>
<option value={BitsRuleType.BitsCheermoteNameExactMatch}>
Cheermote Name (Exact Match)
</option>
<option value={BitsRuleType.BitsCheermotePrefixSpendThreshold}>
Cheermote Prefix and Bits Spend Threshold
</option>
</select>
</>
);
switch (bitsRuleType) {
case BitsRuleType.BitsCheermoteNameExactMatch:
matchingRulesForm = <BitsCheermoteNameExactMatchForm
cheerState={cheerState}
updateCheerName={handleChangedCheerName}
/>;
matchingRulesForm = (
<BitsCheermoteNameExactMatchForm
cheerState={cheerState}
updateCheerName={handleChangedCheerName}
/>
);
break;
case BitsRuleType.BitsCheermotePrefixSpendThreshold:
matchingRulesForm = <BitsCheermotePrefixSpendThresholdForm
cheerState={cheerState}
updateCheerPrefix={handleChangedCheerPrefix}
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
matchingRulesForm = (
<BitsCheermotePrefixSpendThresholdForm
cheerState={cheerState}
updateCheerPrefix={handleChangedCheerPrefix}
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
/>
);
break;
case BitsRuleType.BitsSpendThreshold:
matchingRulesForm = <BitsSpendThresholdForm
cheerState={cheerState}
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
/>;
matchingRulesForm = (
<BitsSpendThresholdForm
cheerState={cheerState}
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
/>
);
break;
}
} else if (props.twitchEventCategory === TwitchEventCategory.ChannelPoints) {
ruleTypeSelect = (
<>
<select
<select
value={channelPointsRuleType}
onChange={handleChangedChannelPointsRuleType}>
<option
onChange={handleChangedChannelPointsRuleType}
className="form-select"
>
<option
value={ChannelPointsRuleType.ChannelPointsRewardNameExactMatch}
>Channel Points Reward Name (exact match)</option>
>
Channel Points Reward Name (exact match)
</option>
</select>
</>
);
matchingRulesForm = <ChannelPointsRewardNameExactMatchForm
rewardName={rewardName}
updateRewardName={handleChangedRewardName}
/>;
matchingRulesForm = (
<ChannelPointsRewardNameExactMatchForm
rewardName={rewardName}
updateRewardName={handleChangedRewardName}
/>
);
}
return (
<>
<h2 className="title is-4">1) Pick what to match on</h2>
<div className="d-flex flex-column gap-4">
<div className="d-flex flex-column panel p-3 p-lg-4 gap-2">
<h2 className="fw-bold pb-3">1) Pick what to match on</h2>
<div className="form-group">{ruleTypeSelect}</div>
</div>
<div className="field">
<div className="control">
<div className="select is-medium is-fullwidth">
{ruleTypeSelect}
</div>
<div className="d-flex flex-column panel p-3 p-lg-4 gap-2">
<h2 className="fw-bold pb-3">2) Configure the matching</h2>
<div className="form-group">{matchingRulesForm}</div>
</div>
</div>
<br />
<br />
<h2 className="title is-4">2) Configure the matching</h2>
{matchingRulesForm}
</>
)
);
}
export { EventMatchPredicateBuilderComponent }
export { EventMatchPredicateBuilderComponent };
@@ -1,68 +1,83 @@
import React, { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faGem } from '@fortawesome/free-solid-svg-icons';
import { CHEER_BIT_LEVELS, CHEER_PREFIXES, CHEER_PREFIX_TO_STRING_MAP } from '../../../../twitch/Cheers';
import { CheerState, CheerStateIsCustom, CheerStateIsOfficial } from '../CheerState';
import React, { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGem } from "@fortawesome/free-solid-svg-icons";
import {
CHEER_BIT_LEVELS,
CHEER_PREFIXES,
CHEER_PREFIX_TO_STRING_MAP,
} from "../../../../twitch/Cheers";
import {
CheerState,
CheerStateIsCustom,
CheerStateIsOfficial,
} from "../CheerState";
// TODO: Don't duplicate
const CHEER_REGEX = /^([A-Za-z]+)(\d+)?$/;
interface BitsCheermoteNameExactMatchProps {
cheerState: CheerState,
updateCheerName: (cheerNameOrPrefix: string) => void,
};
cheerState: CheerState;
updateCheerName: (cheerNameOrPrefix: string) => void;
}
function BitsCheermoteNameExactMatchForm(props: BitsCheermoteNameExactMatchProps) {
function BitsCheermoteNameExactMatchForm(
props: BitsCheermoteNameExactMatchProps
) {
// The two dropdowns
const [cheerPrefix, setCheerPrefix] = useState<string>('');
const [cheerPrefix, setCheerPrefix] = useState<string>("");
const [bitsValue, setBitsValue] = useState<number>(1);
// The freeform text input
const [manualCheerValue, setManualCheerValue] = useState<string>('');
const [manualCheerValue, setManualCheerValue] = useState<string>("");
// NB: useState is not always setting from props correctly (after several re-renders)
// The following answers suggests using useEffect:
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
// https://stackoverflow.com/a/62982753
useEffect(() => {
let newPrefix = '';
let newFullName = '';
let newPrefix = "";
let newFullName = "";
let newBits = 1;
let hasBits = false;
if (!!props.cheerState.bits && !isNaN(props.cheerState.bits) && props.cheerState.bits > 0) {
if (
!!props.cheerState.bits &&
!isNaN(props.cheerState.bits) &&
props.cheerState.bits > 0
) {
newBits = props.cheerState.bits;
hasBits = true;
}
if (CheerStateIsOfficial(props.cheerState)) {
if (!!props.cheerState.cheerPrefix) {
newPrefix = CHEER_PREFIX_TO_STRING_MAP.get((props.cheerState.cheerPrefix)) || '';
newPrefix =
CHEER_PREFIX_TO_STRING_MAP.get(props.cheerState.cheerPrefix) || "";
}
if (hasBits) {
newFullName = `${newPrefix}${newBits}`;
} else {
newFullName = newPrefix;
}
} else if (CheerStateIsCustom(props.cheerState)) {
newFullName = props.cheerState.cheerFull || '';
newFullName = props.cheerState.cheerFull || "";
}
setCheerPrefix(newPrefix);
setBitsValue(newBits);
setManualCheerValue(newFullName);
}, [props.cheerState]);
const updateCheerPrefix = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const updateCheerPrefix = (
ev: React.FormEvent<HTMLSelectElement>
): boolean => {
const value = (ev.target as HTMLSelectElement).value;
setCheerPrefix(value);
recalcuateFieldValue(value, bitsValue);
return true;
}
};
const updateBitsValue = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const updateBitsValue = (ev: React.FormEvent<HTMLSelectElement>): boolean => {
const value = (ev.target as HTMLSelectElement).value;
const numericValue = parseInt(value);
if (!isNaN(numericValue)) {
@@ -71,13 +86,15 @@ function BitsCheermoteNameExactMatchForm(props: BitsCheermoteNameExactMatchProps
recalcuateFieldValue(cheerPrefix, numericValue);
}
return true;
}
};
const updateTextCheerValue = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
const updateTextCheerValue = (
ev: React.FormEvent<HTMLInputElement>
): boolean => {
const value = (ev.target as HTMLInputElement).value;
// Cheer name is the full value, eg. 'Corgo100'
const matches = value.trim().match(CHEER_REGEX)
const matches = value.trim().match(CHEER_REGEX);
if (!!matches && matches.length > 1) {
setCheerPrefix(matches[1]); // First match group
@@ -93,77 +110,73 @@ function BitsCheermoteNameExactMatchForm(props: BitsCheermoteNameExactMatchProps
setManualCheerValue(value);
props.updateCheerName(value);
return true;
}
};
// When the dropdowns are used, replace any manual text entry.
const recalcuateFieldValue = (prefix: string, bits: number) => {
const cheerValue = `${prefix}${bits}`;
setManualCheerValue(cheerValue);
props.updateCheerName(cheerValue);
}
};
return (
<>
<div className="field is-grouped">
<div className="control">
<label className="label">Pick the cheer</label>
<div className="select is-medium">
<select
onChange={updateCheerPrefix}
value={cheerPrefix}
>
<option
key={`option-*`}
value=""
>Select cheer...</option>
{CHEER_PREFIXES.map(cheerPrefix => {
return (
<option
key={`option-${cheerPrefix}`}
value={cheerPrefix}
>{cheerPrefix}</option>
);
})}
</select>
</div>
<div className="d-flex flex-column gap-4">
<div className="form-group">
<label className="sub-title">Pick the cheer</label>
<select
onChange={updateCheerPrefix}
value={cheerPrefix}
className="form-select"
>
<option key={`option-*`} value="">
Select cheer...
</option>
{CHEER_PREFIXES.map((cheerPrefix) => {
return (
<option key={`option-${cheerPrefix}`} value={cheerPrefix}>
{cheerPrefix}
</option>
);
})}
</select>
</div>
<div className="control">
<label className="label">Then the bit value</label>
<div className="control">
<div className="select is-medium">
<select
onChange={updateBitsValue}
value={bitsValue}
>
{CHEER_BIT_LEVELS.map(level => {
return (
<option
key={`option-${cheerPrefix}-${level}`}
value={level}
>{level}</option>
);
})}
</select>
</div>
</div>
<div className="form-group">
<label className="sub-title">Then the bit value</label>
<select
onChange={updateBitsValue}
value={bitsValue}
className="form-select"
>
{CHEER_BIT_LEVELS.map((level) => {
return (
<option key={`option-${cheerPrefix}-${level}`} value={level}>
{level}
</option>
);
})}
</select>
</div>
<div className="control is-expanded">
<label className="label">To match against this (or set something custom)</label>
<p className="control has-icons-left is-large">
<input
value={manualCheerValue}
onChange={updateTextCheerValue}
className="input is-medium is-primary"
type="text"
placeholder="Cheermote full name (including bit value)" />
<span className="icon is-small is-left">
<label className="sub-title">
To match against this (or set something custom)
</label>
<div className="input-icon">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faGem} />
</span>
</p>
<input
value={manualCheerValue}
onChange={updateTextCheerValue}
className="form-control"
type="text"
placeholder="Cheermote full name (including bit value)"
/>
</div>
</div>
</div>
</>
)
);
}
export { BitsCheermoteNameExactMatchForm }
export { BitsCheermoteNameExactMatchForm };
@@ -1,19 +1,29 @@
import React, { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faGem } from '@fortawesome/free-solid-svg-icons';
import { CHEER_BIT_LEVELS, CHEER_PREFIXES, CHEER_PREFIX_TO_STRING_MAP } from '../../../../twitch/Cheers';
import { CheerState, CheerStateIsCustom, CheerStateIsOfficial } from '../CheerState';
import React, { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGem } from "@fortawesome/free-solid-svg-icons";
import {
CHEER_BIT_LEVELS,
CHEER_PREFIXES,
CHEER_PREFIX_TO_STRING_MAP,
} from "../../../../twitch/Cheers";
import {
CheerState,
CheerStateIsCustom,
CheerStateIsOfficial,
} from "../CheerState";
interface BitsCheermotePrefixSpendThresholdProps {
cheerState: CheerState,
cheerState: CheerState;
updateCheerPrefix: (cheerNameOrPrefix: string) => void,
updateMinimumBitsSpent: (minimumSpent: number) => void,
};
updateCheerPrefix: (cheerNameOrPrefix: string) => void;
updateMinimumBitsSpent: (minimumSpent: number) => void;
}
function BitsCheermotePrefixSpendThresholdForm(props: BitsCheermotePrefixSpendThresholdProps) {
const [cheerPrefix, setCheerPrefix] = useState<string>('');
const [customCheerPrefix, setCustomCheerPrefix] = useState<string>('');
function BitsCheermotePrefixSpendThresholdForm(
props: BitsCheermotePrefixSpendThresholdProps
) {
const [cheerPrefix, setCheerPrefix] = useState<string>("");
const [customCheerPrefix, setCustomCheerPrefix] = useState<string>("");
const [bitsValue, setBitsValue] = useState<number>(1);
const [customBitsValue, setCustomBitsValue] = useState<number>(1);
@@ -24,172 +34,170 @@ function BitsCheermotePrefixSpendThresholdForm(props: BitsCheermotePrefixSpendTh
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
// https://stackoverflow.com/a/62982753
useEffect(() => {
let newPrefix = '';
let newCustomPrefix = '';
let newPrefix = "";
let newCustomPrefix = "";
let newBits = 1;
let newCustomBits = 1;
if (!!props.cheerState.bits && !isNaN(props.cheerState.bits) && props.cheerState.bits > 0) {
if (
!!props.cheerState.bits &&
!isNaN(props.cheerState.bits) &&
props.cheerState.bits > 0
) {
newBits = props.cheerState.bits;
newCustomBits = props.cheerState.bits;
}
if (CheerStateIsOfficial(props.cheerState)) {
if (!!props.cheerState.cheerPrefix) {
newPrefix = CHEER_PREFIX_TO_STRING_MAP.get((props.cheerState.cheerPrefix)) || '';
newCustomPrefix = CHEER_PREFIX_TO_STRING_MAP.get((props.cheerState.cheerPrefix)) || '';
newPrefix =
CHEER_PREFIX_TO_STRING_MAP.get(props.cheerState.cheerPrefix) || "";
newCustomPrefix =
CHEER_PREFIX_TO_STRING_MAP.get(props.cheerState.cheerPrefix) || "";
}
} else if (CheerStateIsCustom(props.cheerState)) {
newCustomPrefix = props.cheerState.cheerFull || '';
newCustomPrefix = props.cheerState.cheerFull || "";
}
setCheerPrefix(newPrefix);
setCustomCheerPrefix(newCustomPrefix);
setBitsValue(newBits);
setCustomBitsValue(newCustomBits);
}, [props.cheerState]);
const updateCheerPrefix = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const updateCheerPrefix = (
ev: React.FormEvent<HTMLSelectElement>
): boolean => {
const value = (ev.target as HTMLSelectElement).value;
setCheerPrefix(value);
setCustomCheerPrefix(value);
props.updateCheerPrefix(value);
return true;
}
};
const updateTextCheerValue = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
const updateTextCheerValue = (
ev: React.FormEvent<HTMLInputElement>
): boolean => {
const value = (ev.target as HTMLInputElement).value;
setCheerPrefix(value);
setCustomCheerPrefix(value);
props.updateCheerPrefix(value);
return true;
}
};
const handleBitSelect = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const handleBitSelect = (ev: React.FormEvent<HTMLSelectElement>): boolean => {
const value = (ev.target as HTMLSelectElement).value;
const numericValue = parseInt(value);
setBitsValue(numericValue);
setCustomBitsValue(numericValue);
props.updateMinimumBitsSpent(numericValue);
return true;
}
const handleCustomBitsValueUpdate = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
};
const handleCustomBitsValueUpdate = (
ev: React.FormEvent<HTMLInputElement>
): boolean => {
const value = (ev.target as HTMLInputElement).value;
const numericValue = parseInt(value);
setCustomBitsValue(numericValue);
props.updateMinimumBitsSpent(numericValue);
return true;
}
};
return (
<>
<article className="message is-warning">
<div className="message-body">
This rule type requires <strong>both</strong> the cheermote
to match and the bits spent to be equal or greater than the amount specified.
</div>
</article>
<div className="alert alert-warning">
This rule type requires <strong>both</strong> the cheermote to match and
the bits spent to be equal or greater than the amount specified.
</div>
<h1 className="title is-5">Cheermote matching</h1>
<div className="field is-grouped">
<div className="control">
<label className="label">Pick the cheer prefix</label>
<div className="select is-medium">
<select
onChange={updateCheerPrefix}
value={cheerPrefix}
>
<option
key={`option-*`}
value=""
>Select cheer...</option>
{CHEER_PREFIXES.map(cheerPrefix => {
return (
<option
key={`option-${cheerPrefix}`}
value={cheerPrefix}
>{cheerPrefix}</option>
);
})}
</select>
</div>
<div className="d-flex flex-column gap-4">
<h2 className="mt-3">Cheermote matching</h2>
<div className="form-group">
<label className="sub-title">Pick the cheer prefix</label>
<select
onChange={updateCheerPrefix}
value={cheerPrefix}
className="form-select"
>
<option key={`option-*`} value="">
Select cheer...
</option>
{CHEER_PREFIXES.map((cheerPrefix) => {
return (
<option key={`option-${cheerPrefix}`} value={cheerPrefix}>
{cheerPrefix}
</option>
);
})}
</select>
</div>
<div className="control is-expanded">
<label className="label">To match against this (or set something custom)</label>
<p className="control has-icons-left is-large">
<input
<div className="form-group">
<label className="sub-title">
To match against this (or set something custom)
</label>
<div className="input-icon">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faGem} />
</span>
<input
value={customCheerPrefix}
onChange={updateTextCheerValue}
className="input is-medium is-primary"
type="text"
placeholder="Cheermote full name (including bit value)" />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faGem} />
</span>
</p>
</div>
</div>
<article className="message">
<div className="message-body">
The "Cheer prefix" can be an actual cheer prefix, such as "SeemsGood", but it can also be an arbitrary
string value. So if you make it "ManBearPig", that will be searched for in cheers.
Cheermotes will be removed from the words used in TTS, but currently custom strings will not (though
this may change in the future).
</div>
</article>
<h1 className="title is-5">Bits spent</h1>
<div className="field is-grouped">
<div className="control">
<label className="label">Preset thresholds</label>
<div className="control">
<div className="select is-medium">
<select
onChange={handleBitSelect}
value={bitsValue}
>
{CHEER_BIT_LEVELS.map(level => {
return (
<option
key={`option-${level}`}
value={level}
>Spend at least {level} bits</option>
);
})}
</select>
</div>
className="form-control"
type="text"
placeholder="Cheermote full name (including bit value)"
/>
</div>
</div>
<div className="control is-expanded">
<label className="label">Or a custom minimum amount.</label>
<p className="control has-icons-left is-large">
<input
value={customBitsValue}
onChange={handleCustomBitsValueUpdate}
className="input is-medium is-primary"
type="text"
placeholder="Cheermote full name (including bit value)" />
<span className="icon is-small is-left">
<p>
The "Cheer prefix" can be an actual cheer prefix, such as "SeemsGood",
but it can also be an arbitrary string value. So if you make it
"ManBearPig", that will be searched for in cheers. Cheermotes will be
removed from the words used in TTS, but currently custom strings will
not (though this may change in the future).
</p>
<h2 className="mt-3">Bits spent</h2>
<div className="form-group">
<label className="sub-title">Preset thresholds</label>
<select
onChange={handleBitSelect}
value={bitsValue}
className="form-select"
>
{CHEER_BIT_LEVELS.map((level) => {
return (
<option key={`option-${level}`} value={level}>
Spend at least {level} bits
</option>
);
})}
</select>
</div>
<div className="form-group">
<label className="sub-title">Or a custom minimum amount.</label>
<div className="input-icon">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faGem} />
</span>
</p>
<input
value={customBitsValue}
onChange={handleCustomBitsValueUpdate}
className="form-control"
type="text"
placeholder="Cheermote full name (including bit value)"
/>
</div>
</div>
<p>
Remember that all Cheermotes in a single message add up, eg.{" "}
<em>&ldquo;Cheer100 + Cheer100 + Party100&rdquo;</em> is 300 bits. A
custom amount should give you flexibility to charge, say, 300 or 500
bits. Or 420. Whatever you want.
</p>
</div>
<article className="message">
<div className="message-body">
Remember that all Cheermotes in a single message add up,
eg. <em>&ldquo;Cheer100 + Cheer100 + Party100&rdquo;</em> is 300 bits.
A custom amount should give you flexibility to charge, say, 300 or 500 bits.
Or 420. Whatever you want.
</div>
</article>
</>
)
);
}
export { BitsCheermotePrefixSpendThresholdForm }
export { BitsCheermotePrefixSpendThresholdForm };
@@ -1,13 +1,13 @@
import React, { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faGem } from '@fortawesome/free-solid-svg-icons';
import { CHEER_BIT_LEVELS } from '../../../../twitch/Cheers';
import { CheerState } from '../CheerState';
import React, { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGem } from "@fortawesome/free-solid-svg-icons";
import { CHEER_BIT_LEVELS } from "../../../../twitch/Cheers";
import { CheerState } from "../CheerState";
interface BitsSpendThresholdFormProps {
cheerState: CheerState,
updateMinimumBitsSpent: (minimumSpent: number) => void,
};
cheerState: CheerState;
updateMinimumBitsSpent: (minimumSpent: number) => void;
}
function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) {
// The dropdown
@@ -19,13 +19,17 @@ function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) {
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
// https://stackoverflow.com/a/62982753
useEffect(() => {
if (!!props.cheerState.bits && !isNaN(props.cheerState.bits) && props.cheerState.bits > 0) {
if (
!!props.cheerState.bits &&
!isNaN(props.cheerState.bits) &&
props.cheerState.bits > 0
) {
setBitsValue(props.cheerState.bits);
setCustomAmount(props.cheerState.bits);
}
}, [props.cheerState]);
const handleBitSelect = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const handleBitSelect = (ev: React.FormEvent<HTMLSelectElement>): boolean => {
const value = (ev.target as HTMLSelectElement).value;
const numericValue = parseInt(value);
@@ -37,9 +41,11 @@ function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) {
}
return true;
}
};
const handleCustomAmountUpdate = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
const handleCustomAmountUpdate = (
ev: React.FormEvent<HTMLInputElement>
): boolean => {
const value = (ev.target as HTMLInputElement).value;
const numericValue = parseInt(value);
@@ -51,60 +57,54 @@ function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) {
}
return true;
}
};
return (
<>
<div className="field is-grouped">
<div className="control">
<label className="label">Preset thresholds</label>
<div className="control">
<div className="select is-medium">
<select
onChange={handleBitSelect}
value={bitsValue}
>
<option
key={`option-*`}
value=""
>Choose or type...</option>
{CHEER_BIT_LEVELS.map(level => {
return (
<option
key={`option-${level}`}
value={level}
>Spend at least {level} bits</option>
);
})}
</select>
</div>
</div>
<div className="d-flex flex-column gap-4">
<div className="form-group">
<label className="sub-title">Preset thresholds</label>
<select
onChange={handleBitSelect}
value={bitsValue}
className="form-select"
>
<option key={`option-*`} value="">
Choose or type...
</option>
{CHEER_BIT_LEVELS.map((level) => {
return (
<option key={`option-${level}`} value={level}>
Spend at least {level} bits
</option>
);
})}
</select>
</div>
<div className="control is-expanded">
<label className="label">Or a custom minimum amount.</label>
<p className="control has-icons-left is-large">
<input
value={customAmount}
onChange={handleCustomAmountUpdate}
className="input is-medium is-primary"
type="text"
placeholder="Cheermote full name (including bit value)" />
<span className="icon is-small is-left">
<div className="form-group">
<label className="sub-title">Or a custom minimum amount.</label>
<div className="form-group input-icon">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faGem} />
</span>
<input
value={customAmount}
onChange={handleCustomAmountUpdate}
className="form-control"
type="text"
placeholder="Cheermote full name (including bit value)"
/>
</div>
<p className="mt-3">
Remember that all Cheermotes in a single message add up, eg.{" "}
<em>&ldquo;Cheer100 + Cheer100 + Party100&rdquo;</em> is 300 bits. A
custom amount should give you flexibility to charge, say, 300 or 500
bits. Or 420. Whatever you want.
</p>
</div>
</div>
<article className="message">
<div className="message-body">
Remember that all Cheermotes in a single message add up,
eg. <em>&ldquo;Cheer100 + Cheer100 + Party100&rdquo;</em> is 300 bits.
A custom amount should give you flexibility to charge, say, 300 or 500 bits.
Or 420. Whatever you want.
</div>
</article>
</>
)
);
}
export { BitsSpendThresholdForm }
export { BitsSpendThresholdForm };
@@ -1,12 +1,14 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState } from "react";
interface ChannelPointsRewardNameExactMatchProps {
rewardName: string,
updateRewardName: (name: string) => void,
};
rewardName: string;
updateRewardName: (name: string) => void;
}
function ChannelPointsRewardNameExactMatchForm(props: ChannelPointsRewardNameExactMatchProps) {
const [rewardName, setRewardName] = useState<string>('');
function ChannelPointsRewardNameExactMatchForm(
props: ChannelPointsRewardNameExactMatchProps
) {
const [rewardName, setRewardName] = useState<string>("");
// NB: useState is not always setting from props correctly (after several re-renders)
// The following answers suggests using useEffect:
@@ -16,28 +18,29 @@ function ChannelPointsRewardNameExactMatchForm(props: ChannelPointsRewardNameExa
setRewardName(props.rewardName);
}, [props.rewardName]);
const updateRewardName = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
const updateRewardName = (ev: React.FormEvent<HTMLInputElement>): boolean => {
const value = (ev.target as HTMLInputElement).value;
setRewardName(value);
props.updateRewardName(value);
return true;
}
};
return (
<>
<div className="field">
<label className="label">Match the following reward name</label>
<div className="control is-medium">
<input
<div className="form-group">
<label className="sub-title">Match the following reward name</label>
<div>
<input
value={rewardName}
onChange={updateRewardName}
className="input is-medium"
type="text"
placeholder="Reward Name" />
className="form-control"
type="text"
placeholder="Reward Name"
/>
</div>
</div>
</>
)
);
}
export { ChannelPointsRewardNameExactMatchForm }
export { ChannelPointsRewardNameExactMatchForm };
@@ -1,35 +1,39 @@
import React, { useEffect, useState } from 'react';
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
import { TtsSingleVoiceForm } from './TtsSingleVoiceForm';
import { EventResponseType } from './EventResponseType';
import { EventResponse } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse';
import { buildEventResponse } from './BuildEventResponse';
import { TtsRandomVoiceForm } from './TtsRandomVoiceForm';
import React, { useEffect, useState } from "react";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { TtsSingleVoiceForm } from "./TtsSingleVoiceForm";
import { EventResponseType } from "./EventResponseType";
import { EventResponse } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse";
import { buildEventResponse } from "./BuildEventResponse";
import { TtsRandomVoiceForm } from "./TtsRandomVoiceForm";
// NB: This was created for "Edit" view, but we've forced it to work for "Create" view by hardcoding
// NB: This was created for "Edit" view, but we've forced it to work for "Create" view by hardcoding
// values that don't actually come from the server.
interface EventResponseComponentProps {
// Original response from server.
// (also immutable for sanity and because we may lose info on UI changes)
serverEventResponse: EventResponse,
serverEventResponse: EventResponse;
// Updates sent back up the tree
updateModifiedEventResponse: (response: EventResponse) => void,
updateModifiedEventResponse: (response: EventResponse) => void;
// FakeYou voices
allTtsModels: TtsModelListItem[],
allTtsModelsByToken: Map<string, TtsModelListItem>,
};
allTtsModels: TtsModelListItem[];
allTtsModelsByToken: Map<string, TtsModelListItem>;
}
function EventResponseComponent(props: EventResponseComponentProps) {
// ========== Core UI flow ==========
const [eventResponseType, setEventResponseType] = useState<EventResponseType>(EventResponseType.TtsSingleVoice);
const [eventResponseType, setEventResponseType] = useState<EventResponseType>(
EventResponseType.TtsSingleVoice
);
// ========== Cached Values for Editing ==========
const [selectedTtsModelTokens, setSelectedTtsModelTokens] = useState<string[]>([]);
const [selectedTtsModelTokens, setSelectedTtsModelTokens] = useState<
string[]
>([]);
// NB: useState is not always setting from props correctly (after several re-renders)
// The following answers suggests using useEffect:
@@ -37,44 +41,52 @@ function EventResponseComponent(props: EventResponseComponentProps) {
// https://stackoverflow.com/a/62982753
useEffect(() => {
let newEventResponseType = EventResponseType.TtsSingleVoice;
let modelTokens : string[] = [];
let modelTokens: string[] = [];
if (!!props.serverEventResponse.tts_single_voice) {
newEventResponseType = EventResponseType.TtsSingleVoice;
if (!!props.serverEventResponse.tts_single_voice.tts_model_token) {
modelTokens.push(props.serverEventResponse.tts_single_voice.tts_model_token)
modelTokens.push(
props.serverEventResponse.tts_single_voice.tts_model_token
);
}
} else if (!!props.serverEventResponse.tts_random_voice) {
newEventResponseType = EventResponseType.TtsRandomVoice;
if (!!props.serverEventResponse.tts_random_voice.tts_model_tokens &&
props.serverEventResponse.tts_random_voice.tts_model_tokens.length > 0)
{
modelTokens = [...props.serverEventResponse.tts_random_voice.tts_model_tokens];
if (
!!props.serverEventResponse.tts_random_voice.tts_model_tokens &&
props.serverEventResponse.tts_random_voice.tts_model_tokens.length > 0
) {
modelTokens = [
...props.serverEventResponse.tts_random_voice.tts_model_tokens,
];
}
}
setEventResponseType(newEventResponseType);
setSelectedTtsModelTokens(modelTokens);
}, [props.serverEventResponse]);
const handleChangedEventResponseType = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const handleChangedEventResponseType = (
ev: React.FormEvent<HTMLSelectElement>
): boolean => {
const value = (ev.target as HTMLSelectElement).value;
const newResponseType = value as EventResponseType;
const newEventResponse = buildEventResponse(selectedTtsModelTokens, newResponseType);
const newEventResponse = buildEventResponse(
selectedTtsModelTokens,
newResponseType
);
setEventResponseType(newResponseType);
props.updateModifiedEventResponse(newEventResponse);
return true;
}
};
const handleChangedTtsModelToken = (token: string) => {
// We may be only updating to a single token, but we want to be able to navigate back to
// We may be only updating to a single token, but we want to be able to navigate back to
// the "random voices" view, etc.
let updatedTokens = [...selectedTtsModelTokens];
@@ -84,72 +96,82 @@ function EventResponseComponent(props: EventResponseComponentProps) {
updatedTokens.push(token);
}
const newEventResponse = buildEventResponse(updatedTokens, eventResponseType);
const newEventResponse = buildEventResponse(
updatedTokens,
eventResponseType
);
setSelectedTtsModelTokens(updatedTokens);
props.updateModifiedEventResponse(newEventResponse);
}
};
const handleChangedTtsModelTokens = (tokens: string[]) => {
let updatedTokens = [...tokens];
const newEventResponse = buildEventResponse(updatedTokens, eventResponseType);
const newEventResponse = buildEventResponse(
updatedTokens,
eventResponseType
);
setSelectedTtsModelTokens(updatedTokens);
props.updateModifiedEventResponse(newEventResponse);
}
};
let responseForm = <></>
let responseForm = <></>;
switch (eventResponseType) {
case EventResponseType.TtsSingleVoice:
responseForm = <TtsSingleVoiceForm
selectedTtsModelTokens={selectedTtsModelTokens}
updateSelectedTtsModelToken={handleChangedTtsModelToken}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
responseForm = (
<TtsSingleVoiceForm
selectedTtsModelTokens={selectedTtsModelTokens}
updateSelectedTtsModelToken={handleChangedTtsModelToken}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
);
break;
case EventResponseType.TtsRandomVoice:
responseForm = <TtsRandomVoiceForm
selectedTtsModelTokens={selectedTtsModelTokens}
updateSelectedTtsModelTokens={handleChangedTtsModelTokens}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
responseForm = (
<TtsRandomVoiceForm
selectedTtsModelTokens={selectedTtsModelTokens}
updateSelectedTtsModelTokens={handleChangedTtsModelTokens}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
);
break;
}
return (
<>
<h2 className="title is-4">3) Pick how to respond</h2>
<div className="panel p-3 p-lg-4">
<h2 className="fw-bold pb-3">3) Pick how to respond</h2>
<div className="field">
<label className="label">Response Type</label>
<div className="control">
<div className="select is-medium is-fullwidth">
<select
<div className="form-group">
<label className="sub-title">Response Type</label>
<div className="control">
<select
value={eventResponseType}
onChange={handleChangedEventResponseType}>
<option value={EventResponseType.TtsSingleVoice}>Respond with a TTS voice</option>
<option value={EventResponseType.TtsRandomVoice}>Respond with a Random TTS voice from list</option>
onChange={handleChangedEventResponseType}
className="form-select"
>
<option value={EventResponseType.TtsSingleVoice}>
Respond with a TTS voice
</option>
<option value={EventResponseType.TtsRandomVoice}>
Respond with a Random TTS voice from list
</option>
</select>
</div>
</div>
</div>
<br />
<br />
<h2 className="title is-4">4) Configure the response</h2>
{responseForm}
<br />
<br />
<div className="panel p-3 p-lg-4">
<h2 className="fw-bold pb-3">4) Configure the response</h2>
{responseForm}
</div>
</>
)
);
}
export { EventResponseComponent }
export { EventResponseComponent };
@@ -1,37 +1,36 @@
import React, { useEffect } from 'react';
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
import { FakeYouExternalLink } from '@storyteller/components/src/elements/FakeYouExternalLink';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
import React, { useEffect } from "react";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { FakeYouExternalLink } from "@storyteller/components/src/elements/FakeYouExternalLink";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
interface TtsRandomVoiceFormProps {
selectedTtsModelTokens: string[],
updateSelectedTtsModelTokens: (tokens: string[]) => void,
selectedTtsModelTokens: string[];
updateSelectedTtsModelTokens: (tokens: string[]) => void;
// FakeYou voices
allTtsModels: TtsModelListItem[],
allTtsModelsByToken: Map<string, TtsModelListItem>,
};
allTtsModels: TtsModelListItem[];
allTtsModelsByToken: Map<string, TtsModelListItem>;
}
function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
// NB: useState is not always setting from props correctly (after several re-renders)
// The following answers suggests using useEffect:
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
// https://stackoverflow.com/a/62982753
useEffect(() => {
}, [props.selectedTtsModelTokens]);
useEffect(() => {}, [props.selectedTtsModelTokens]);
const handleAddVoice = (ev: React.FormEvent<HTMLButtonElement>) : boolean => {
const handleAddVoice = (ev: React.FormEvent<HTMLButtonElement>): boolean => {
ev.preventDefault();
let newModelTokens = [...props.selectedTtsModelTokens];
newModelTokens.push('');
newModelTokens.push("");
props.updateSelectedTtsModelTokens(newModelTokens);
return true;
}
};
const handleModelSelect = (ttsModelToken: string, voiceIndex: number) => {
let newModelTokens = [...props.selectedTtsModelTokens];
@@ -42,7 +41,7 @@ function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
newModelTokens[voiceIndex] = ttsModelToken;
props.updateSelectedTtsModelTokens(newModelTokens);
}
};
const handleModelDelete = (voiceIndex: number) => {
let newModelTokens = [...props.selectedTtsModelTokens];
@@ -55,11 +54,11 @@ function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
newModelTokens.splice(voiceIndex, 1);
props.updateSelectedTtsModelTokens(newModelTokens);
}
};
const deleteDisabled = props.selectedTtsModelTokens.length < 2;
let selectBoxes : JSX.Element[] = [];
let selectBoxes: JSX.Element[] = [];
// No tokens - show one box (unselected)
// One or more tokens - show boxes for each (with selections)
@@ -75,8 +74,8 @@ function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
deleteDisabled={deleteDisabled}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
)
/>
);
});
if (selectBoxes.length === 0) {
@@ -90,55 +89,54 @@ function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
deleteDisabled={false}
allTtsModels={props.allTtsModels}
allTtsModelsByToken={props.allTtsModelsByToken}
/>
)
/>
);
}
return (
<>
{selectBoxes}
<button
className="button is-large is-fullwidth is-info"
onClick={handleAddVoice}
>
<FontAwesomeIcon icon={faPlus} />&nbsp;Add Additional Voice
<button className="btn btn-primary mt-3" onClick={handleAddVoice}>
<FontAwesomeIcon icon={faPlus} className="me-2" />
Add Additional Voice
</button>
<br />
<article className="message">
<div className="message-body">
<FakeYouExternalLink>Check out the voices at FakeYou</FakeYouExternalLink> to search for voices, experiment with them,
and see how they sound. Sorry that this list is a nightmare to navigate. It will improve over time.
</div>
</article>
<p className="mt-4">
<FakeYouExternalLink>
Check out the voices at FakeYou
</FakeYouExternalLink>{" "}
to search for voices, experiment with them, and see how they sound.
Sorry that this list is a nightmare to navigate. It will improve over
time.
</p>
</>
)
);
}
interface VoiceDropdownProps {
// Actual token or empty string
selectedTtsModelToken: string,
selectedTtsModelToken: string;
// Position in the list, 0-indexed
voiceIndex: number,
voiceIndex: number;
// Callbacks
handleModelSelect: (token: string, index: number) => void,
handleModelDelete: (index: number) => void,
handleModelSelect: (token: string, index: number) => void;
handleModelDelete: (index: number) => void;
// Whether to disable the delete button
deleteDisabled: boolean,
deleteDisabled: boolean;
// FakeYou voices
allTtsModels: TtsModelListItem[],
allTtsModelsByToken: Map<string, TtsModelListItem>,
allTtsModels: TtsModelListItem[];
allTtsModelsByToken: Map<string, TtsModelListItem>;
}
function VoiceDropdown(props: VoiceDropdownProps) {
const handleSelectChange = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const handleSelectChange = (
ev: React.FormEvent<HTMLSelectElement>
): boolean => {
const maybeModelToken = (ev.target as HTMLSelectElement).value;
if (!maybeModelToken) {
@@ -153,47 +151,52 @@ function VoiceDropdown(props: VoiceDropdownProps) {
props.handleModelSelect(maybeModelToken, props.voiceIndex);
return true;
}
};
const handleRemoveClick = (ev: React.FormEvent<HTMLButtonElement>) : boolean => {
const handleRemoveClick = (
ev: React.FormEvent<HTMLButtonElement>
): boolean => {
props.handleModelDelete(props.voiceIndex);
return true;
}
};
return (
<div key={`dropdown-${props.voiceIndex}`}>
<label className="label">{props.voiceIndex+1}. TTS Voice Model (used as a Random Voice)</label>
<label className="sub-title">
{props.voiceIndex + 1}. TTS Voice Model (used as a Random Voice)
</label>
<div className="field is-grouped">
<div className="control is-expanded">
<div className="select is-medium is-fullwidth">
<select
value={props.selectedTtsModelToken}
onChange={handleSelectChange}
>
<option
key={`option-${props.voiceIndex}-*`}
value="">
Select a voice...</option>
{props.allTtsModels.map(ttsModel => {
return (
<option
key={`option-${props.voiceIndex}-${ttsModel.model_token}`}
value={ttsModel.model_token}
>{ttsModel.title}</option>
);
})}
</select>
</div>
<div className="form-group">
<div className="d-flex gap-3">
<select
value={props.selectedTtsModelToken}
onChange={handleSelectChange}
className="form-select"
>
<option key={`option-${props.voiceIndex}-*`} value="">
Select a voice...
</option>
{props.allTtsModels.map((ttsModel) => {
return (
<option
key={`option-${props.voiceIndex}-${ttsModel.model_token}`}
value={ttsModel.model_token}
>
{ttsModel.title}
</option>
);
})}
</select>
</div>
<div className="control">
<button
<div className="mt-3">
<button
onClick={handleRemoveClick}
className="button is-medium is-danger is-inverted"
className="btn btn-destructive"
disabled={props.deleteDisabled}
>
<FontAwesomeIcon icon={faTrash} />&nbsp;Remove
>
<FontAwesomeIcon icon={faTrash} className="me-2" />
Remove
</button>
</div>
</div>
@@ -203,4 +206,4 @@ function VoiceDropdown(props: VoiceDropdownProps) {
);
}
export { TtsRandomVoiceForm }
export { TtsRandomVoiceForm };
@@ -1,18 +1,18 @@
import React, { useEffect, useState } from 'react';
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
import { FakeYouExternalLink } from '@storyteller/components/src/elements/FakeYouExternalLink';
import React, { useEffect, useState } from "react";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
import { FakeYouExternalLink } from "@storyteller/components/src/elements/FakeYouExternalLink";
interface TtsSingleVoiceFormProps {
selectedTtsModelTokens: string[],
updateSelectedTtsModelToken: (token: string) => void,
selectedTtsModelTokens: string[];
updateSelectedTtsModelToken: (token: string) => void;
// FakeYou voices
allTtsModels: TtsModelListItem[],
allTtsModelsByToken: Map<string, TtsModelListItem>,
};
allTtsModels: TtsModelListItem[];
allTtsModelsByToken: Map<string, TtsModelListItem>;
}
function TtsSingleVoiceForm(props: TtsSingleVoiceFormProps) {
const [ttsModelToken, setTtsModelToken] = useState('');
const [ttsModelToken, setTtsModelToken] = useState("");
// NB: useState is not always setting from props correctly (after several re-renders)
// The following answers suggests using useEffect:
@@ -24,7 +24,9 @@ function TtsSingleVoiceForm(props: TtsSingleVoiceFormProps) {
}
}, [props.selectedTtsModelTokens]);
const handleModelSelect = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
const handleModelSelect = (
ev: React.FormEvent<HTMLSelectElement>
): boolean => {
const value = (ev.target as HTMLSelectElement).value;
if (!!value && !props.allTtsModelsByToken.has(value)) {
@@ -34,42 +36,45 @@ function TtsSingleVoiceForm(props: TtsSingleVoiceFormProps) {
setTtsModelToken(value);
props.updateSelectedTtsModelToken(value);
return true;
}
};
return (
<>
<div className="field">
<label className="label">TTS Voice Model</label>
<div className="control">
<div className="select is-medium is-fullwidth">
<select
value={ttsModelToken}
onChange={handleModelSelect}
>
<option value="">Select a voice...</option>
{props.allTtsModels.map(ttsModel => {
return (
<option
key={`option-${ttsModel.model_token}`}
value={ttsModel.model_token}
>{ttsModel.title}</option>
);
})}
</select>
</div>
<div className="d-flex flex-column gap-4">
<div className="form-group">
<label className="sub-title">TTS Voice Model</label>
<select
value={ttsModelToken}
onChange={handleModelSelect}
className="form-select"
>
<option value="">Select a voice...</option>
{props.allTtsModels.map((ttsModel) => {
return (
<option
key={`option-${ttsModel.model_token}`}
value={ttsModel.model_token}
>
{ttsModel.title}
</option>
);
})}
</select>
</div>
<p>
We apologize that this list is a nightmare to navigate right now. It
will improve over time. In the mean time,{" "}
<FakeYouExternalLink>
check out the voices at FakeYou
</FakeYouExternalLink>{" "}
to find the voices you want, experiment with them, and see which ones
work best for your stream. The names of the voices at FakeYou are the
same as in this dropdown, so you can search at FakeYou, then add them
here when you're done looking.
</p>
</div>
<article className="message">
<div className="message-body">
We apologize that this list is a nightmare to navigate right now. It will improve over time.
In the mean time, <FakeYouExternalLink>check out the voices at FakeYou</FakeYouExternalLink> to
find the voices you want, experiment with them, and see which ones work best for your stream. The
names of the voices at FakeYou are the same as in this dropdown, so you can search at FakeYou, then
add them here when you're done looking.</div>
</article>
</>
)
);
}
export { TtsSingleVoiceForm }
export { TtsSingleVoiceForm };
@@ -1,48 +1,89 @@
import React from 'react';
import { TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowDown, faArrowUp, faExternalLinkAlt, } from '@fortawesome/free-solid-svg-icons';
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
import React from "react";
import { TwitchEventRule } from "@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faArrowDown,
faArrowUp,
faExternalLinkAlt,
} from "@fortawesome/free-solid-svg-icons";
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
interface Props {
rule: TwitchEventRule,
rule: TwitchEventRule;
// Index of the rule in the parents' list
ruleIndex: number,
ruleIndex: number;
// Update callbacks
handleMoveUp: (index: number) => void,
handleMoveDown: (index: number) => void,
handleMoveUp: (index: number) => void;
handleMoveDown: (index: number) => void;
// FakeYou voices
allTtsModelsByToken: Map<string, TtsModelListItem>,
allTtsModelsByToken: Map<string, TtsModelListItem>;
}
function ReorderableTwitchEventRuleElement(props: Props) {
let subtitle = <></>;
let description = <></>;
if (props.rule.event_match_predicate.bits_cheermote_name_exact_match !== undefined) {
subtitle = (
<>Cheermote name matches "{props.rule.event_match_predicate.bits_cheermote_name_exact_match.cheermote_name}"</>
);
} else if (props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold !== undefined) {
if (
props.rule.event_match_predicate.bits_cheermote_name_exact_match !==
undefined
) {
subtitle = (
<>
Cheermote prefix is "{props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold.cheermote_prefix}"
and spend at least {props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold.minimum_bits_spent} bits
Cheermote name matches "
{
props.rule.event_match_predicate.bits_cheermote_name_exact_match
.cheermote_name
}
"
</>
);
} else if (props.rule.event_match_predicate.bits_spend_threshold !== undefined) {
} else if (
props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold !==
undefined
) {
subtitle = (
<>
Spend at least {props.rule.event_match_predicate.bits_spend_threshold.minimum_bits_spent} bits
Cheermote prefix is "
{
props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold
.cheermote_prefix
}
" and spend at least{" "}
{
props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold
.minimum_bits_spent
}{" "}
bits
</>
);
} else if (props.rule.event_match_predicate.channel_points_reward_name_exact_match !== undefined) {
} else if (
props.rule.event_match_predicate.bits_spend_threshold !== undefined
) {
subtitle = (
<>
Reward name matches "{props.rule.event_match_predicate.channel_points_reward_name_exact_match.reward_name}"
Spend at least{" "}
{
props.rule.event_match_predicate.bits_spend_threshold
.minimum_bits_spent
}{" "}
bits
</>
);
} else if (
props.rule.event_match_predicate.channel_points_reward_name_exact_match !==
undefined
) {
subtitle = (
<>
Reward name matches "
{
props.rule.event_match_predicate
.channel_points_reward_name_exact_match.reward_name
}
"
</>
);
}
@@ -61,7 +102,14 @@ function ReorderableTwitchEventRuleElement(props: Props) {
description = (
<>
TTS with voice: {modelName} <a href={link} target="_blank" rel="noreferrer"><FontAwesomeIcon icon={faExternalLinkAlt} /></a>
<p>
<span className="fw-semibold">TTS with voice:</span>
<br />
{modelName}{" "}
<a href={link} target="_blank" rel="noreferrer">
<FontAwesomeIcon icon={faExternalLinkAlt} />
</a>
</p>
</>
);
} else if (props.rule.event_response.tts_random_voice !== undefined) {
@@ -78,50 +126,55 @@ function ReorderableTwitchEventRuleElement(props: Props) {
return (
<li key={index}>
{modelName} <a href={link} target="_blank" rel="noreferrer"><FontAwesomeIcon icon={faExternalLinkAlt} /></a>
{modelName}{" "}
<a href={link} target="_blank" rel="noreferrer">
<FontAwesomeIcon icon={faExternalLinkAlt} />
</a>
</li>
);
});
description = (
<>
TTS with a random voice from the following {modelNameAndLinks.length} voice(s):
<ul>{modelNameAndLinks}</ul>
<p>
<span className="fw-semibold">
TTS with a random voice from the following{" "}
{modelNameAndLinks.length} voice(s):
</span>
<ul>{modelNameAndLinks}</ul>
</p>
</>
);
}
return (
<div key={props.rule.token}>
<div className="card">
<div className="card-content">
<div className="title is-5">
{subtitle}
</div>
<div>
{description}
</div>
<div className="panel p-3 p-lg-4">
<div className="mb-4">
<h4 className="fw-bold mb-4">{subtitle}</h4>
<div>{description}</div>
</div>
<div className="d-flex gap-3">
<button
onClick={() => props.handleMoveUp(props.ruleIndex)}
className="btn btn-secondary w-100"
>
<FontAwesomeIcon icon={faArrowUp} className="me-2" />
Up
</button>
<button
onClick={() => props.handleMoveDown(props.ruleIndex)}
className="btn btn-secondary w-100"
>
<FontAwesomeIcon icon={faArrowDown} className="me-2" />
Down
</button>
</div>
<footer className="card-footer">
<div className="card-footer-item">
<span className="icon">
<button onClick={() => props.handleMoveUp(props.ruleIndex)} className="button is-ghost">
<FontAwesomeIcon icon={faArrowUp} />&nbsp;Up
</button>
</span>
</div>
<div className="card-footer-item">
<span className="icon">
<button onClick={() => props.handleMoveDown(props.ruleIndex)} className="button is-ghost">
<FontAwesomeIcon icon={faArrowDown} />&nbsp;Down
</button>
</span>
</div>
</footer>
</div>
<br />
</div>
)
);
}
export { ReorderableTwitchEventRuleElement }
export { ReorderableTwitchEventRuleElement };
@@ -98,7 +98,7 @@ function TwitchEventRuleElement(props: Props) {
description = (
<>
<p>
TTS with voice:
<span className="fw-semibold">TTS with voice:</span>
<br />
{modelName}{" "}
<a href={link} target="_blank" rel="noreferrer">
@@ -132,8 +132,10 @@ function TwitchEventRuleElement(props: Props) {
description = (
<>
<p>
TTS with a random voice from the following {modelNameAndLinks.length}{" "}
voice(s):
<span className="fw-semibold">
TTS with a random voice from the following{" "}
{modelNameAndLinks.length} voice(s):
</span>
<ul>{modelNameAndLinks}</ul>
</p>
</>
@@ -143,7 +145,7 @@ function TwitchEventRuleElement(props: Props) {
const editUrl = `/tts_configs/edit/${props.rule.token}`;
const deleteUrl = `/tts_configs/delete/${props.rule.token}`;
let buttons = <></>;
let buttons = undefined;
if (!hideButtons) {
buttons = (
<>
@@ -163,7 +165,7 @@ function TwitchEventRuleElement(props: Props) {
return (
<div key={props.rule.token}>
<div className="panel p-4">
<div className="panel p-3 p-lg-4">
<div>
<h4 className="fw-bold mb-4">{subtitle}</h4>
<div className="mb-4">{description}</div>