diff --git a/packages/storyteller-stream-dashboard/src/App.scss b/packages/storyteller-stream-dashboard/src/App.scss index 99b620bd5a..af0f45dedc 100644 --- a/packages/storyteller-stream-dashboard/src/App.scss +++ b/packages/storyteller-stream-dashboard/src/App.scss @@ -94,6 +94,10 @@ p { color: rgba(255, 255, 255, 0.85); } +ul { + margin-bottom: 0; +} + .fw-semibold { font-weight: 500; } diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsCreateRulePage.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsCreateRulePage.tsx index 0adc6bee8c..7c6ac5d4d6 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsCreateRulePage.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsCreateRulePage.tsx @@ -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, + sessionWrapper: SessionWrapper; + allTtsModels: TtsModelListItem[]; + allTtsModelsByToken: Map; } 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({}); const [serverEventResponse] = useState({}); - const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] = useState({}); - const [modifiedEventResponse, setModifiedEventResponse] = useState({}); + const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] = + useState({}); + const [modifiedEventResponse, setModifiedEventResponse] = + useState({}); 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) : Promise => { + const handleFormSubmit = async ( + ev: React.FormEvent + ): Promise => { 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

Must Log In

; } // 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.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 ( <> -
-

{pageTitle}

+
+

+ {pageTitle} +

-
-
- -
- - + + -
-
- - - -

This is the rule:

- -
- -
+ /> - - - -
+

This is the rule:

- -  Cancel / Go Back - + + + + + + Cancel / Go Back + +
- ) + ); } -export { TtsConfigsCreateRulePage } \ No newline at end of file +export { TtsConfigsCreateRulePage }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsEditRulePage.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsEditRulePage.tsx index d880de1ff0..117d2208a5 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsEditRulePage.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsEditRulePage.tsx @@ -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, + sessionWrapper: SessionWrapper; + allTtsModels: TtsModelListItem[]; + allTtsModelsByToken: Map; } 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(undefined); + const [twitchEventRule, setTwitchEventRule] = useState< + TwitchEventRule | undefined + >(undefined); // Sent to sub-components to initialize UI - const [serverEventMatchPredicate, setServerEventMatchPredicate] = useState({}); - const [serverEventResponse, setServerEventResponse] = useState({}); + const [serverEventMatchPredicate, setServerEventMatchPredicate] = + useState({}); + const [serverEventResponse, setServerEventResponse] = useState( + {} + ); // ========== In-Progress Model Edits ========== - const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] = useState({}); - const [modifiedEventResponse, setModifiedEventResponse] = useState({}); + const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] = + useState({}); + const [modifiedEventResponse, setModifiedEventResponse] = + useState({}); 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) : Promise => { + const handleFormSubmit = async ( + ev: React.FormEvent + ): Promise => { 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

Must Log In

; @@ -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 ( <> -
-

Edit Rule

+
+

+ Edit Rule +

-
-
- -
- - + + -
-
- - - -

This is the rule:

- -
- + +

This is the rule:

+ +
+ -
+
- - - -
- - -  Cancel / Go Back - + + + + + Cancel / Go Back + +
- ) + ); } -export { TtsConfigsEditRulePage } \ No newline at end of file +export { TtsConfigsEditRulePage }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsIndexPage.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsIndexPage.tsx index 88faf9b468..ccac8cc2af 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsIndexPage.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsIndexPage.tsx @@ -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"; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsReorderPage.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsReorderPage.tsx index 3417747217..0cd0b49263 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsReorderPage.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/TtsConfigsReorderPage.tsx @@ -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, + sessionWrapper: SessionWrapper; + allTtsModelsByToken: Map; } 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([]); + const [twitchEventRules, setTwitchEventRules] = useState( + [] + ); - 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) : Promise => { + }; + const handleSaveOrderings = async ( + ev: React.FormEvent + ): Promise => { 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

Must Log In

; @@ -114,44 +138,51 @@ function TtsConfigsReorderPage(props: Props) { return ( <> -
-

Reorder Rules

-

This affects match priority. Rules at the top come first.

+
+

+ Reorder Rules +

+

+ {" "} + This affects match priority. Rules at the top come first.{" "} +

-
-
-
- You can create rules for matching different categories of events: cheers (bits), - channel point rewards, etc. - - When an event occurs in your stream, we compare it against the rules in its - category. The first matched rule (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. -
-
+
+

+ You can create rules for matching{" "} + different categories of events: cheers (bits), channel point + rewards, etc. When an event occurs in your stream, we compare it + against the rules in its category.{" "} + The first matched rule (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. +

{twitchEventRules.map((rule, index) => { - return + ); })} -
- + + Save Orderings + +
- ) + ); } -export { TtsConfigsReorderPage } \ No newline at end of file +export { TtsConfigsReorderPage }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/EventMatchPredicateBuilderComponent.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/EventMatchPredicateBuilderComponent.tsx index ff14e57b9c..e52c8588a1 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/EventMatchPredicateBuilderComponent.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/EventMatchPredicateBuilderComponent.tsx @@ -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.BitsCheermoteNameExactMatch); - const [channelPointsRuleType, setChannelPointsRuleType] = useState(ChannelPointsRuleType.ChannelPointsRewardNameExactMatch); + const [bitsRuleType, setBitsRuleType] = useState( + BitsRuleType.BitsCheermoteNameExactMatch + ); + const [channelPointsRuleType, setChannelPointsRuleType] = + useState( + 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) : boolean => { + const handleChangedBitsRuleType = ( + ev: React.FormEvent + ): 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) : boolean => { + const handleChangedChannelPointsRuleType = ( + ev: React.FormEvent + ): 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 = ( <> - ); switch (bitsRuleType) { case BitsRuleType.BitsCheermoteNameExactMatch: - matchingRulesForm = ; + matchingRulesForm = ( + + ); break; case BitsRuleType.BitsCheermotePrefixSpendThreshold: - matchingRulesForm = + ); break; case BitsRuleType.BitsSpendThreshold: - matchingRulesForm = ; + matchingRulesForm = ( + + ); break; } - } else if (props.twitchEventCategory === TwitchEventCategory.ChannelPoints) { ruleTypeSelect = ( <> - ); - matchingRulesForm = ; + matchingRulesForm = ( + + ); } return ( <> -

1) Pick what to match on

+
+
+

1) Pick what to match on

+
{ruleTypeSelect}
+
-
-
-
- {ruleTypeSelect} -
+
+

2) Configure the matching

+
{matchingRulesForm}
- -
-
- -

2) Configure the matching

- - {matchingRulesForm} - ) + ); } -export { EventMatchPredicateBuilderComponent } +export { EventMatchPredicateBuilderComponent }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsCheermoteNameExactMatchForm.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsCheermoteNameExactMatchForm.tsx index 0ff460078f..50e643007d 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsCheermoteNameExactMatchForm.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsCheermoteNameExactMatchForm.tsx @@ -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(''); + const [cheerPrefix, setCheerPrefix] = useState(""); const [bitsValue, setBitsValue] = useState(1); // The freeform text input - const [manualCheerValue, setManualCheerValue] = useState(''); + const [manualCheerValue, setManualCheerValue] = 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 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) : boolean => { + const updateCheerPrefix = ( + ev: React.FormEvent + ): boolean => { const value = (ev.target as HTMLSelectElement).value; setCheerPrefix(value); recalcuateFieldValue(value, bitsValue); return true; - } + }; - const updateBitsValue = (ev: React.FormEvent) : boolean => { + const updateBitsValue = (ev: React.FormEvent): 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) : boolean => { + const updateTextCheerValue = ( + ev: React.FormEvent + ): 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 ( <> -
-
- -
- -
+
+
+ +
-
- -
-
- -
-
+
+ +
- -

- - + +

+ -

+ +
- ) + ); } -export { BitsCheermoteNameExactMatchForm } +export { BitsCheermoteNameExactMatchForm }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsCheermotePrefixSpendThresholdForm.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsCheermotePrefixSpendThresholdForm.tsx index 362eca4550..5ad8bbc8b6 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsCheermotePrefixSpendThresholdForm.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsCheermotePrefixSpendThresholdForm.tsx @@ -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(''); - const [customCheerPrefix, setCustomCheerPrefix] = useState(''); +function BitsCheermotePrefixSpendThresholdForm( + props: BitsCheermotePrefixSpendThresholdProps +) { + const [cheerPrefix, setCheerPrefix] = useState(""); + const [customCheerPrefix, setCustomCheerPrefix] = useState(""); const [bitsValue, setBitsValue] = useState(1); const [customBitsValue, setCustomBitsValue] = useState(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) : boolean => { + const updateCheerPrefix = ( + ev: React.FormEvent + ): boolean => { const value = (ev.target as HTMLSelectElement).value; setCheerPrefix(value); setCustomCheerPrefix(value); props.updateCheerPrefix(value); return true; - } + }; - const updateTextCheerValue = (ev: React.FormEvent) : boolean => { + const updateTextCheerValue = ( + ev: React.FormEvent + ): boolean => { const value = (ev.target as HTMLInputElement).value; setCheerPrefix(value); setCustomCheerPrefix(value); props.updateCheerPrefix(value); return true; - } + }; - const handleBitSelect = (ev: React.FormEvent) : boolean => { + const handleBitSelect = (ev: React.FormEvent): 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) : boolean => { + }; + const handleCustomBitsValueUpdate = ( + ev: React.FormEvent + ): boolean => { const value = (ev.target as HTMLInputElement).value; const numericValue = parseInt(value); setCustomBitsValue(numericValue); props.updateMinimumBitsSpent(numericValue); return true; - } + }; return ( <> -
-
- This rule type requires both the cheermote - to match and the bits spent to be equal or greater than the amount specified. -
-
+
+ This rule type requires both the cheermote to match and + the bits spent to be equal or greater than the amount specified. +
-

Cheermote matching

- -
-
- -
- -
+
+

Cheermote matching

+
+ +
-
- -

- + +

+ + + + - - - -

-
-
- -
-
- 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). -
-
- -

Bits spent

- -
-
- -
-
- -
+ className="form-control" + type="text" + placeholder="Cheermote full name (including bit value)" + />
-
- -

- - +

+ 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). +

+ +

Bits spent

+ +
+ + +
+
+ +
+ -

+ +
+

+ Remember that all Cheermotes in a single message add up, eg.{" "} + “Cheer100 + Cheer100 + Party100” is 300 bits. A + custom amount should give you flexibility to charge, say, 300 or 500 + bits. Or 420. Whatever you want. +

-
-
- Remember that all Cheermotes in a single message add up, - eg. “Cheer100 + Cheer100 + Party100” is 300 bits. - A custom amount should give you flexibility to charge, say, 300 or 500 bits. - Or 420. Whatever you want. -
-
- ) + ); } -export { BitsCheermotePrefixSpendThresholdForm } +export { BitsCheermotePrefixSpendThresholdForm }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsSpendThresholdForm.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsSpendThresholdForm.tsx index 0474a4754b..4681b6a7c8 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsSpendThresholdForm.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/BitsSpendThresholdForm.tsx @@ -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) : boolean => { + const handleBitSelect = (ev: React.FormEvent): 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) : boolean => { + const handleCustomAmountUpdate = ( + ev: React.FormEvent + ): boolean => { const value = (ev.target as HTMLInputElement).value; const numericValue = parseInt(value); @@ -51,60 +57,54 @@ function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) { } return true; - } + }; return ( <> -
-
- -
-
- -
-
+
+
+ +
-
- -

- - +

+ +
+ + +
+

+ Remember that all Cheermotes in a single message add up, eg.{" "} + “Cheer100 + Cheer100 + Party100” is 300 bits. A + custom amount should give you flexibility to charge, say, 300 or 500 + bits. Or 420. Whatever you want.

-
-
- Remember that all Cheermotes in a single message add up, - eg. “Cheer100 + Cheer100 + Party100” is 300 bits. - A custom amount should give you flexibility to charge, say, 300 or 500 bits. - Or 420. Whatever you want. -
-
- ) + ); } -export { BitsSpendThresholdForm } +export { BitsSpendThresholdForm }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/ChannelPointsRewardNameExactMatchForm.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/ChannelPointsRewardNameExactMatchForm.tsx index d489c6024e..d603b95cbd 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/ChannelPointsRewardNameExactMatchForm.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_match_predicate_builder/subforms/ChannelPointsRewardNameExactMatchForm.tsx @@ -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(''); +function ChannelPointsRewardNameExactMatchForm( + props: ChannelPointsRewardNameExactMatchProps +) { + const [rewardName, setRewardName] = useState(""); // 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) : boolean => { + const updateRewardName = (ev: React.FormEvent): boolean => { const value = (ev.target as HTMLInputElement).value; setRewardName(value); props.updateRewardName(value); return true; - } + }; return ( <> -
- -
- + +
+ + className="form-control" + type="text" + placeholder="Reward Name" + />
- ) + ); } -export { ChannelPointsRewardNameExactMatchForm } +export { ChannelPointsRewardNameExactMatchForm }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/EventResponseComponent.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/EventResponseComponent.tsx index 6bdd77afb5..461680c164 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/EventResponseComponent.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/EventResponseComponent.tsx @@ -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, -}; + allTtsModels: TtsModelListItem[]; + allTtsModelsByToken: Map; +} function EventResponseComponent(props: EventResponseComponentProps) { // ========== Core UI flow ========== - const [eventResponseType, setEventResponseType] = useState(EventResponseType.TtsSingleVoice); + const [eventResponseType, setEventResponseType] = useState( + EventResponseType.TtsSingleVoice + ); // ========== Cached Values for Editing ========== - const [selectedTtsModelTokens, setSelectedTtsModelTokens] = useState([]); + 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) : boolean => { + const handleChangedEventResponseType = ( + ev: React.FormEvent + ): 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 = + ); break; case EventResponseType.TtsRandomVoice: - responseForm = + ); break; } return ( <> -

3) Pick how to respond

+
+

3) Pick how to respond

-
- -
-
- - - + onChange={handleChangedEventResponseType} + className="form-select" + > + +
-
-
- -

4) Configure the response

- - {responseForm} - -
-
- +
+

4) Configure the response

+ {responseForm} +
- ) + ); } -export { EventResponseComponent } +export { EventResponseComponent }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/TtsRandomVoiceForm.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/TtsRandomVoiceForm.tsx index 41b72bbc79..25224663db 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/TtsRandomVoiceForm.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/TtsRandomVoiceForm.tsx @@ -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, -}; + allTtsModels: TtsModelListItem[]; + allTtsModelsByToken: Map; +} 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) : boolean => { + const handleAddVoice = (ev: React.FormEvent): 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} - -
- -
-
- Check out the voices at FakeYou 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. -
-
+

+ + Check out the voices at FakeYou + {" "} + 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. +

- ) + ); } 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, + allTtsModels: TtsModelListItem[]; + allTtsModelsByToken: Map; } function VoiceDropdown(props: VoiceDropdownProps) { - - const handleSelectChange = (ev: React.FormEvent) : boolean => { + const handleSelectChange = ( + ev: React.FormEvent + ): 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) : boolean => { + const handleRemoveClick = ( + ev: React.FormEvent + ): boolean => { props.handleModelDelete(props.voiceIndex); return true; - } + }; return (
- + -
-
- -
- -
+
+
+
-
-
@@ -203,4 +206,4 @@ function VoiceDropdown(props: VoiceDropdownProps) { ); } -export { TtsRandomVoiceForm } +export { TtsRandomVoiceForm }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/TtsSingleVoiceForm.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/TtsSingleVoiceForm.tsx index e3a3fbdd4d..749ca9fe53 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/TtsSingleVoiceForm.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/event_response_builder/TtsSingleVoiceForm.tsx @@ -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, -}; + allTtsModels: TtsModelListItem[]; + allTtsModelsByToken: Map; +} 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) : boolean => { + const handleModelSelect = ( + ev: React.FormEvent + ): 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 ( <> -
- -
-
- -
+
+
+ +
+

+ We apologize that this list is a nightmare to navigate right now. It + will improve over time. In the mean time,{" "} + + check out the voices at FakeYou + {" "} + 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. +

- -
-
- We apologize that this list is a nightmare to navigate right now. It will improve over time. - In the mean time, check out the voices at FakeYou 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.
-
- ) + ); } -export { TtsSingleVoiceForm } +export { TtsSingleVoiceForm }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/rule_cards/ReorderableTwitchEventRuleElement.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/rule_cards/ReorderableTwitchEventRuleElement.tsx index 7c9cf14fa8..b8a942cb6a 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/rule_cards/ReorderableTwitchEventRuleElement.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/rule_cards/ReorderableTwitchEventRuleElement.tsx @@ -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, + allTtsModelsByToken: Map; } 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} +

+ TTS with voice: +
+ {modelName}{" "} + + + +

); } else if (props.rule.event_response.tts_random_voice !== undefined) { @@ -78,50 +126,55 @@ function ReorderableTwitchEventRuleElement(props: Props) { return (
  • - {modelName} + {modelName}{" "} + + +
  • ); }); description = ( <> - TTS with a random voice from the following {modelNameAndLinks.length} voice(s): -
      {modelNameAndLinks}
    +

    + + TTS with a random voice from the following{" "} + {modelNameAndLinks.length} voice(s): + +

      {modelNameAndLinks}
    +

    ); } return (
    -
    -
    -
    - {subtitle} -
    -
    - {description} -
    +
    +
    +

    {subtitle}

    +
    {description}
    +
    +
    + + +
    -
    -
    - - - -
    -
    - - - -
    -

    - ) + ); } -export { ReorderableTwitchEventRuleElement } \ No newline at end of file +export { ReorderableTwitchEventRuleElement }; diff --git a/packages/storyteller-stream-dashboard/src/pages/tts_configs/rule_cards/TwitchEventRuleElement.tsx b/packages/storyteller-stream-dashboard/src/pages/tts_configs/rule_cards/TwitchEventRuleElement.tsx index 46f9f1e01e..281f553a55 100644 --- a/packages/storyteller-stream-dashboard/src/pages/tts_configs/rule_cards/TwitchEventRuleElement.tsx +++ b/packages/storyteller-stream-dashboard/src/pages/tts_configs/rule_cards/TwitchEventRuleElement.tsx @@ -98,7 +98,7 @@ function TwitchEventRuleElement(props: Props) { description = ( <>

    - TTS with voice: + TTS with voice:
    {modelName}{" "} @@ -132,8 +132,10 @@ function TwitchEventRuleElement(props: Props) { description = ( <>

    - TTS with a random voice from the following {modelNameAndLinks.length}{" "} - voice(s): + + TTS with a random voice from the following{" "} + {modelNameAndLinks.length} voice(s): +

    @@ -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 (
    -
    +

    {subtitle}

    {description}