[JIMT] dance ai modal refactoring part 2#55192
Conversation
… made headerValue a memo
* 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))
| <div | ||
| id={id} | ||
| style={{width: size, height: size}} | ||
| className={moduleStyles.previewVisualization} | ||
| ref={containerRef} | ||
| /> |
There was a problem hiding this comment.
This is just an extra <div> wrapper, which we don't need AFAIK.
| ); | ||
| }; | ||
|
|
||
| export default React.memo(DanceAiEmojiIcon); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
This is super cool. And thanks for the tip about using the Profiler and checking the 'Highlight updates when components render' box!
| generateAiEffectBlocksFromResult, | ||
| generatePreviewCode, | ||
| getEmojiImageUrl, | ||
| getItem, |
There was a problem hiding this comment.
This should likewise be renamed. getEmoji? getEmojiItem?
There was a problem hiding this comment.
I agree that getItem is vague. getEmoji seems clear and concise.
There was a problem hiding this comment.
This comment was out of date - it was already moved over to getDanceAiModalItem
There was a problem hiding this comment.
In general, I'm curious why we're using 'item' vs 'emoji' or 'input'.
There was a problem hiding this comment.
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
DanceAiModelItemare the values loaded out of theai-inputs.jsonfile (previously justitem)emojiis overloaded in that it could be thought of as the human readable string, the actual image, or aDanceAiModelItem.
There was a problem hiding this comment.
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); | ||
|
|
There was a problem hiding this comment.
this is now just looked up from the inputs.length down below.
| () => inputs.map(getItem).filter(item => item !== undefined), | ||
| [inputs] | ||
| ); | ||
|
|
There was a problem hiding this comment.
by memoizing selectedEmojis we can save a few render steps + not need to look up the item every time.
| (useInputs: string[], step: number) => { | ||
| (myInputs: string[], step: number) => { |
There was a problem hiding this comment.
useInputs as a variable looks like a hook, which it isn't.
changed to myInputs to still be local, but not conflicting.
There was a problem hiding this comment.
I don't think we talk about my in most variables.. how about currentInputs?
There was a problem hiding this comment.
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?
| ).reduce((accumulator: number, step: number) => { | ||
| const scores = getScores(myInputs, step); | ||
| return Math.min(...scores, accumulator); |
There was a problem hiding this comment.
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); | ||
|
|
There was a problem hiding this comment.
currentValues is pretty vague, so I changed it to the more descriptive seeming previousInput.
There was a problem hiding this comment.
It's still the current value unless it's overwritten.
There was a problem hiding this comment.
+1 Brendan's comment.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
What about savedValueString?
There was a problem hiding this comment.
how about initialValueString/ initialInput? Emphasizes more that it's the value that's going into the modal.
| }, [currentAiModalField, mode, calculateMinMax]); | ||
|
|
||
| const getLabels = () => { | ||
| const labels = useMemo(() => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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...
| const getItem = (id: string) => | ||
| inputLibraryJson.items.find(item => item.id === id); |
There was a problem hiding this comment.
This function was moved to utils/getDanceAiModalItem and optimized.
Notes are in that function.
| (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] |
There was a problem hiding this comment.
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.
| const handleGenerateClick = () => { | ||
| const handleGenerateClick = useCallback(() => { |
There was a problem hiding this comment.
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(() => { |
There was a problem hiding this comment.
github got really confused here. But this function is the same as before, just in a callback.
| setGeneratedProgress(0); | ||
| setMode(Mode.SELECT_INPUTS); | ||
| }; | ||
| }, [inputs]); |
There was a problem hiding this comment.
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; | ||
| }; | ||
|
|
There was a problem hiding this comment.
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>; | ||
| }); | ||
|
|
There was a problem hiding this comment.
This was pulled out into the separate DanceAiModalHeader component
| <button key={0} type="button" value={Toggle.EFFECT}> | ||
| <button type="button" value={Toggle.EFFECT}> |
There was a problem hiding this comment.
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} | ||
| /> |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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.
… and generic input grid
| }; | ||
|
|
||
| export type AIEmojiItem = EmojiItem & { | ||
| modelDescriptiveName: string; |
There was a problem hiding this comment.
Should we keep the first 'i' in 'AIEmojiItem' lowercase for consistency, e.g., 'AiEmojiItem'?
There was a problem hiding this comment.
💯 Just a typo oversight. Fixed!
sanchitmalhotra126
left a comment
There was a problem hiding this comment.
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!
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:
useInputsvariable tomyInputskeys`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: