Skip to content

[JIMT] dance ai modal refactoring part 2#55192

Merged
thomasoniii merged 123 commits into
stagingfrom
jimt/dance-modal-refactoring-part-2
Jan 3, 2024
Merged

[JIMT] dance ai modal refactoring part 2#55192
thomasoniii merged 123 commits into
stagingfrom
jimt/dance-modal-refactoring-part-2

Conversation

@thomasoniii

@thomasoniii thomasoniii commented Nov 28, 2023

Copy link
Copy Markdown
Contributor

This is about a day's worth of work, so seems like a good place to stop and get it in.

Re-arranges a bunch of stuff, in a lot of interlocking ways that makes it difficult to tease apart into multiple PRs. I'd rather just send in a single big one now which has most/all of the teasing apart done in place and then we're in better shape for smaller ones going forward. Otherwise this'll be a rapid fire dozen or so.

Anyway, this includes, at a minimum:

  • broke apart DanceAiClient into separate utils/- files/functions
  • moved DanceAiModal/getPreviewCode into separate file
  • moved EmojiIcon into separate component
  • created DanceAiEmojiIcon wrapper component
  • created DanceAiModalHeader wrapper component
  • created DanceAiModalFlipCard wrapper component
  • created new DanceAiModalEmojiInputGrid component
  • DanceAiModal/getItem is a memoized function using an itemsById cached lookup
  • changed useInputs variable to myInputs
  • simplified return logic in calculateMinMax
  • retooled handleItemClick to remove currentInputSlot dependency - big render perf boost
  • added selectedEmojis helper data (inputs.map(getItem))
  • moved getItem to a separate utils/file
  • removed superfluous keys`
  • added lots of memoization
  • added lots of callbacks

Notes in the code. Let's meet about this one on a call.

Links

Testing story

Deployment strategy

Follow-up work

Privacy

Security

Caching

PR Checklist:

  • Tests provide adequate coverage
  • Privacy and Security impacts have been assessed
  • Code is well-commented
  • New features are translatable or updates will not break translations
  • Relevant documentation has been added or updated
  • User impact is well-understood and desirable
  • Pull Request is labeled appropriately
  • Follow-up work items (including potential tech debt) are tracked and linked

thomasoniii and others added 10 commits November 27, 2023 12:30
* broke apart DanceAiClient into separate utils/* files/functions
* moved DanceAiModal/getPreviewCode into separate file
* moved EmojiIcon into separate component
* created DanceAiEmojiIcon wrapper component
* created DanceAiModalHeader wrapper component
* created DanceAiModalFlipCard wrapper component
* DanceAiModal/getItem is a memoized function using an itemsById cached lookup
* changed `useInputs` variable to `myInputs`
* simplified return logic in calculateMinMax
* retooled handleItemClick to remove currentInputSlot dependency - big render perf boost
* allItems list is now emojiInputGrid; contains all output needed (NOTE - may go into separate component)
* added selectedEmojis helper data (inputs.map(getItem))
Comment on lines +65 to +70
<div
id={id}
style={{width: size, height: size}}
className={moduleStyles.previewVisualization}
ref={containerRef}
/>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just an extra <div> wrapper, which we don't need AFAIK.

Comment thread apps/src/dance/ai/DanceAiEmojiIcon.tsx Outdated
);
};

export default React.memo(DanceAiEmojiIcon);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this component is a wrapper around the more generic EmojiIcon.

Each DanceAiEmojiIcon gets a custom click handler which is passed the item's id and its availability.

This way we can memoize the component and render properly. Otherwise we'd be creating a new click handler every render, and need to re-draw.

mid-long term, this could be replaced by a new click handler which doesn't pass along the isItemAvailable flag (it can be looked up from the selectedEmojis and then gets the ID from a data-tag. But we can circle back to that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super cool. And thanks for the tip about using the Profiler and checking the 'Highlight updates when components render' box!

Comment thread apps/src/dance/ai/DanceAiModal.tsx Outdated
Comment thread apps/src/dance/ai/DanceAiModal.tsx Outdated
generateAiEffectBlocksFromResult,
generatePreviewCode,
getEmojiImageUrl,
getItem,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should likewise be renamed. getEmoji? getEmojiItem?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that getItem is vague. getEmoji seems clear and concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was out of date - it was already moved over to getDanceAiModalItem

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I'm curious why we're using 'item' vs 'emoji' or 'input'.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had a bunch of terms in the code initially, and they haven't been totally cleaned up and standardized. But, what I've been roughly working towards:

  • inputs are emoji ids
  • DanceAiModelItem are the values loaded out of the ai-inputs.json file (previously just item)
  • emoji is overloaded in that it could be thought of as the human readable string, the actual image, or a DanceAiModelItem.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This is helpful and makes sense.
I see we are using selectedEmojis. Would it make sense to call these selectedDanceAiModalItems?


const [mode, setMode] = useState(Mode.INITIAL);
const [currentInputSlot, setCurrentInputSlot] = useState(0);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is now just looked up from the inputs.length down below.

() => inputs.map(getItem).filter(item => item !== undefined),
[inputs]
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by memoizing selectedEmojis we can save a few render steps + not need to look up the item every time.

Comment thread apps/src/dance/ai/DanceAiModal.tsx Outdated
Comment on lines +179 to +171
(useInputs: string[], step: number) => {
(myInputs: string[], step: number) => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useInputs as a variable looks like a hook, which it isn't.

changed to myInputs to still be local, but not conflicting.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we talk about my in most variables.. how about currentInputs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want that variable to emphasize that it's local to those functions and not state in the modal. currentInputs still feels ambiguous (especially with the other discussion about previousInputs vs currentInputs). A my prefix emphasizes it nicely.

What about givenInputs?

Comment on lines +193 to +195
).reduce((accumulator: number, step: number) => {
const scores = getScores(myInputs, step);
return Math.min(...scores, accumulator);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a logic simplification. It's returning the minimum of the previous value + all of the scores.

We could even remove the scores variable, but I felt it was more clear to keep.

const previousInputString = currentAiModalField?.getValue();
if (previousInputString) {
const previousInput: AiFieldValue = JSON.parse(previousInputString);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentValues is pretty vague, so I changed it to the more descriptive seeming previousInput.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still the current value unless it's overwritten.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 Brendan's comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but it's also not actually a current value in the modal. It's the value that the modal reads from the blockly block when it's opened up, and that is then used to populate the inputs field of the modal, and then that becomes the actual current inputs value.

It's used to create inputs which are the current ones, but they haven't become current yet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about savedValueString?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about initialValueString/ initialInput? Emphasizes more that it's the value that's going into the modal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM!

}, [currentAiModalField, mode, calculateMinMax]);

const getLabels = () => {
const labels = useMemo(() => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getLabels function was only called once. This is better written as a useMemo so we can calculate the value once and leave it alone w/o changing via re-render or a single function call later.

Also a good candidate to re-write into a separate hook in the future.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bencodeorg I remember when you initially added this, you had run into some weird side effects from using useMemo, right? It sounds like those dependency issues may have gotten already solved along the way, but just tagging you in case there was something else we're missing here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yes, was it that issue where the preview wouldn't show up blank intermittently if you re-opened the modal after generating an effect? That might have even been before we switched to live preview though...

Comment on lines -279 to -301
const getItem = (id: string) =>
inputLibraryJson.items.find(item => item.id === id);

@thomasoniii thomasoniii Nov 28, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was moved to utils/getDanceAiModalItem and optimized.

Notes are in that function.

Comment thread apps/src/dance/ai/DanceAiModal.tsx Outdated
Comment on lines +260 to +277
(id: string, available: boolean) => {
setInputs(inputs => {
// if we've clicked on an emoji that's not available, remove it from our inputs
if (!available) {
playSound('ai-deselect-emoji');
return inputs.filter(input => input !== id);
// if we have more input slots, we can add the emoji
} else if (inputs.length < SLOT_COUNT) {
playSound('ai-select-emoji');
setInputAddCount(inputAddCount => inputAddCount + 1);
return [...inputs, id];
// otherwise, it's not a remove and we don't have space - do nothing.
} else {
return inputs;
}
});
},
[playSound]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needed a logic change to prevent a re-render.

handleItemClick was being recreated every render, which was causing all components that depended upon it to re-render (every emoji in this case). It meant that if you clicked on an emoji, then all of the emoji would redraw.

First step was to make it a callback, but it still would've had a dependency on the currentInputSlot, which still changed on every click and would trigger all the re-renders again.

Instead, it relies upon logic inside the setInputs mutator. This way, it can read the inputs state from within the mutator and behave as appropriate. currentInputSlot is not used as state any more (it's calculated from the inputs.length) so we don't need to worry about it. It only removes the item if necessary or adds it, and plays the sound.

Finally, if we're not adding or removing (meaning all slots are filled and we clicked on a new emoji), then just return the existing list of inputs for no change and no render.

Comment thread apps/src/dance/ai/DanceAiModal.tsx Outdated
Comment on lines +301 to +291
const handleGenerateClick = () => {
const handleGenerateClick = useCallback(() => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All functions should be in useCallback, especially event handlers, so as to prevent needless re-renders.

// Remove item from inputs.
setInputs(inputs.filter(input => input !== id));
setCurrentInputSlot(currentInputSlot - 1);
const startAi = useCallback(() => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github got really confused here. But this function is the same as before, just in a callback.

Comment thread apps/src/dance/ai/DanceAiModal.tsx Outdated
setGeneratedProgress(0);
setMode(Mode.SELECT_INPUTS);
};
}, [inputs]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra handlers here would trigger re-renders every time the inputs change, but fortunately they're only attached to buttons that appear after all inputs have come into place. So we can get away with leaving them as is.

This could actually be dealt with via the same trick of hopping into the setInputs mutator as such:

setInputs(inputs => 
  analyticsReporter.sendEvent(EVENTS.DANCE_PARTY_AI_BACKGROUND_RESTARTED, {
    emojis: inputs,
  });
  return inputs
)

And then no dependency is required and the callback persists forever. But it's unclear why it's doing that, and since the button it's attached to wouldn't exist until inputs are set, I left it as is. If I think of a clever way to pull out the analytics reporting from the handler to make this truly survive forever, I will.

tempWorkspace.dispose();
return previewCode;
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved to utils/getPreviewCode and is a candidate for further refactoring since it's always called with the result of getGeneratedEffect and maybe those should be combined.

].map((part: string, index: number) => {
return <span key={index}>{part}</span>;
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was pulled out into the separate DanceAiModalHeader component

Comment thread apps/src/dance/ai/DanceAiModal.tsx Outdated
Comment on lines +681 to +579
<button key={0} type="button" value={Toggle.EFFECT}>
<button type="button" value={Toggle.EFFECT}>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keys are only meaningful in a loop. They're not needed inside of static content like this, so I dropped them.

currentGeneratedEffect={currentGeneratedEffect}
getPreviewCode={getPreviewCode}
getGeneratedEffect={getGeneratedEffect}
/>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component is a candidate for further refactoring, since it's pulling in a lot of state from the Modal and I think several of these things can be crushed together into better structures. But that's a future item.

<div
className={classNames(
moduleStyles.dot,
explanationKeyDotColor[index]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general rule, keying off of indexes is frowned upon and it's better to key with a unique value, such as the item's id in this case.

Further, since we now have the selectedEmojis list, just iterates on that instead.

@thomasoniii thomasoniii changed the base branch from staging-next to staging December 13, 2023 19:37
@thomasoniii thomasoniii requested a review from a team as a code owner December 13, 2023 19:40
Comment thread apps/src/dance/ai/DanceAiModalFlipCard.tsx Outdated
Comment thread apps/src/dance/ai/utils/calculateOutputSummedWeights.ts Outdated
Comment thread apps/src/dance/ai/DanceAiModalFlipCard.tsx Outdated
};

export type AIEmojiItem = EmojiItem & {
modelDescriptiveName: string;

@fisher-alice fisher-alice Jan 2, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep the first 'i' in 'AIEmojiItem' lowercase for consistency, e.g., 'AiEmojiItem'?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 Just a typo oversight. Fixed!

@sanchitmalhotra126 sanchitmalhotra126 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just had one follow-up on only exporting a subset of the AI utils functions (instead of all of them). Thanks for all the back and forth on this!

@thomasoniii thomasoniii merged commit 1011d3d into staging Jan 3, 2024
@thomasoniii thomasoniii deleted the jimt/dance-modal-refactoring-part-2 branch January 3, 2024 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.