Skip to content

Commit 91c0f81

Browse files
authored
Updated the fade-in effect on reveal of enhanced text and fix other issues on feedback (#83)
* Update dropshadow of popover * Update hover background color * Adjust the size of the Website icon * Modified the fade-in effect for revealing text when enhancing prompt * refactor: Created helper component and move type checking logic * refactor: Add comments to the useEffect and used classnames library * refactor: Added comments to the useEffects * refactor: Removed identifyChildComponent helper and used lodash/get to check displayName * Updated Metronome version
1 parent babf8e9 commit 91c0f81

File tree

8 files changed

+512
-51
lines changed

8 files changed

+512
-51
lines changed
Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,36 @@
11
import React from 'react'
2-
import classnames from 'classnames'
32
import PropTypes from 'prop-types'
3+
import classnames from 'classnames'
4+
import get from 'lodash/get'
5+
6+
function PromptInput ({ className, children }) {
7+
let textarea = null
8+
let actions = null
9+
let overlay = null
10+
const otherChildren = []
11+
12+
React.Children.forEach(children, child => {
13+
const isTextarea = get(child, 'type.displayName') === 'PromptInputTextarea'
14+
const isActions = get(child, 'type.displayName') === 'PromptInputActions'
15+
const isOverlay = get(child, 'type.name') === 'TextRevealOverlay'
16+
17+
if (isTextarea) {
18+
textarea = child
19+
} else if (isActions) {
20+
actions = child
21+
} else if (isOverlay) {
22+
overlay = child
23+
} else if (child) {
24+
otherChildren.push(child)
25+
}
26+
})
427

5-
function PromptInput ({ children, className }) {
628
return (
729
<div className={classnames('ds-prompt-input', className)}>
8-
{children}
30+
{textarea}
31+
{overlay}
32+
{actions}
33+
{otherChildren}
934
</div>
1035
)
1136
}
@@ -15,10 +40,5 @@ PromptInput.propTypes = {
1540
children: PropTypes.node
1641
}
1742

18-
PromptInput.defaultProps = {
19-
className: '',
20-
children: null
21-
}
22-
2343
export default PromptInput
2444

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import React, { useEffect, useState, useRef } from 'react'
2+
import PropTypes from 'prop-types'
3+
import classnames from 'classnames'
4+
5+
function TextRevealOverlay({
6+
text,
7+
isRevealing,
8+
lastParagraphIndex,
9+
className,
10+
textareaRef,
11+
onAnimationComplete
12+
}) {
13+
const [paragraphs, setParagraphs] = useState([])
14+
const [isAnimatingLastParagraph, setIsAnimatingLastParagraph] = useState(false)
15+
const overlayRef = useRef(null)
16+
17+
// Split text into paragraphs whenever text changes
18+
useEffect(() => {
19+
if (text) {
20+
const split = text.split(/\n{2,}|\r\n{2,}/)
21+
.map(p => p.trim())
22+
.filter(p => p.length > 0)
23+
setParagraphs(split)
24+
} else {
25+
setParagraphs([])
26+
}
27+
}, [text])
28+
29+
// Handle animation completion when the last paragraph finishes animating
30+
useEffect(() => {
31+
if (isAnimatingLastParagraph && lastParagraphIndex === paragraphs.length - 1) {
32+
const timer = setTimeout(() => {
33+
setIsAnimatingLastParagraph(false)
34+
if (onAnimationComplete) {
35+
onAnimationComplete()
36+
}
37+
}, 500)
38+
39+
return () => clearTimeout(timer)
40+
}
41+
}, [isAnimatingLastParagraph, lastParagraphIndex, paragraphs.length, onAnimationComplete])
42+
43+
// Detect when the last paragraph appears and mark it for animation
44+
useEffect(() => {
45+
if (lastParagraphIndex === paragraphs.length - 1 && paragraphs.length > 0) {
46+
setIsAnimatingLastParagraph(true)
47+
}
48+
}, [lastParagraphIndex, paragraphs.length])
49+
50+
// Sync overlay dimensions, positioning and scrolling with the textarea
51+
useEffect(() => {
52+
if (!textareaRef || !textareaRef.current || !overlayRef.current || !isRevealing) return
53+
54+
const textarea = textareaRef.current
55+
const overlay = overlayRef.current
56+
57+
// Function to update dimensions whenever they change
58+
const updateDimensions = () => {
59+
// Position relative to the prompt-input container
60+
const promptInput = textarea.closest('.ds-prompt-input')
61+
let paddingLeft = '16px'
62+
let paddingTop = '16px'
63+
64+
if (promptInput) {
65+
const promptStyles = window.getComputedStyle(promptInput)
66+
if (promptStyles.paddingLeft) {
67+
paddingLeft = promptStyles.paddingLeft
68+
}
69+
if (promptStyles.paddingTop) {
70+
paddingTop = promptStyles.paddingTop
71+
}
72+
}
73+
74+
// Get current styles
75+
const textareaStyles = window.getComputedStyle(textarea)
76+
77+
// Apply exact dimensions and styling
78+
overlay.style.width = textareaStyles.width
79+
overlay.style.fontSize = textareaStyles.fontSize
80+
overlay.style.lineHeight = textareaStyles.lineHeight
81+
overlay.style.fontFamily = textareaStyles.fontFamily
82+
overlay.style.fontWeight = textareaStyles.fontWeight
83+
overlay.style.color = textareaStyles.color
84+
85+
// Set exact position
86+
overlay.style.position = 'absolute'
87+
overlay.style.top = paddingTop
88+
overlay.style.left = paddingLeft
89+
overlay.style.width = `calc(100% - ${parseInt(paddingLeft) * 2}px)`
90+
91+
92+
// Set max-height to match textarea max-height
93+
overlay.style.maxHeight = textareaStyles.maxHeight
94+
95+
// Match the actual height if it's calculated
96+
if (textarea.style.height) {
97+
overlay.style.height = textarea.style.height
98+
}
99+
}
100+
101+
// Initial update
102+
updateDimensions()
103+
104+
// Set up mutation observer to detect height changes
105+
let observer = null
106+
if (typeof MutationObserver !== 'undefined') {
107+
observer = new MutationObserver((mutations) => {
108+
mutations.forEach((mutation) => {
109+
if (mutation.attributeName === 'style') {
110+
updateDimensions()
111+
}
112+
})
113+
})
114+
115+
observer.observe(textarea, {
116+
attributes: true,
117+
attributeFilter: ['style']
118+
})
119+
}
120+
121+
const syncScroll = () => {
122+
if (overlayRef.current && isRevealing) {
123+
overlayRef.current.scrollTop = textarea.scrollTop
124+
}
125+
}
126+
127+
textarea.addEventListener('scroll', syncScroll)
128+
129+
window.addEventListener('resize', updateDimensions)
130+
131+
syncScroll()
132+
133+
return () => {
134+
textarea.removeEventListener('scroll', syncScroll)
135+
window.removeEventListener('resize', updateDimensions)
136+
if (observer) {
137+
observer.disconnect()
138+
}
139+
}
140+
}, [textareaRef, isRevealing, paragraphs])
141+
142+
if (!isRevealing || paragraphs.length === 0) {
143+
return null
144+
}
145+
146+
return (
147+
<div
148+
ref={overlayRef}
149+
className={classnames(
150+
'ds-prompt-input__text-reveal-overlay',
151+
className
152+
)}
153+
aria-hidden="true"
154+
>
155+
{paragraphs.map((paragraph, index) => (
156+
<p
157+
key={index}
158+
className={classnames(
159+
'ds-prompt-input__text-reveal-paragraph',
160+
{
161+
'ds-prompt-input__text-reveal-paragraph--animating': index === lastParagraphIndex
162+
}
163+
)}
164+
>
165+
{paragraph}
166+
</p>
167+
))}
168+
</div>
169+
)
170+
}
171+
172+
TextRevealOverlay.propTypes = {
173+
text: PropTypes.string,
174+
isRevealing: PropTypes.bool,
175+
lastParagraphIndex: PropTypes.number,
176+
className: PropTypes.string,
177+
textareaRef: PropTypes.object,
178+
onAnimationComplete: PropTypes.func
179+
}
180+
181+
TextRevealOverlay.defaultProps = {
182+
text: '',
183+
isRevealing: false,
184+
lastParagraphIndex: -1,
185+
className: '',
186+
textareaRef: null,
187+
onAnimationComplete: null
188+
}
189+
190+
export default TextRevealOverlay
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
2+
import { useState, useEffect, useRef, useCallback } from 'react'
3+
4+
function useProgressiveTextReveal({
5+
onComplete = () => {},
6+
paragraphDelay = 400,
7+
onCompleteTimeout = 600,
8+
onParagraphAdded = () => {}
9+
} = {}) {
10+
const [isRevealing, setIsRevealing] = useState(false)
11+
const [currentText, setCurrentText] = useState('')
12+
const [newParagraphAdded, setNewParagraphAdded] = useState(false)
13+
const [lastParagraphIndex, setLastParagraphIndex] = useState(-1)
14+
const timeoutsRef = useRef([])
15+
const paragraphsRef = useRef([])
16+
const currentIndexRef = useRef(0)
17+
18+
const clearAllTimeouts = useCallback(() => {
19+
timeoutsRef.current.forEach(timeoutId => clearTimeout(timeoutId))
20+
timeoutsRef.current = []
21+
}, [])
22+
23+
// Reset the reveal state
24+
const resetTextReveal = useCallback(() => {
25+
clearAllTimeouts()
26+
setCurrentText('')
27+
setIsRevealing(false)
28+
setNewParagraphAdded(false)
29+
setLastParagraphIndex(-1)
30+
paragraphsRef.current = []
31+
currentIndexRef.current = 0
32+
}, [clearAllTimeouts])
33+
34+
// This function adds a single paragraph with animation trigger
35+
const addNextParagraph = useCallback(() => {
36+
if (currentIndexRef.current >= paragraphsRef.current.length) {
37+
38+
// Allow time for the final animation to complete before signaling done
39+
const finalTimeout = setTimeout(() => {
40+
onComplete(currentText)
41+
}, onCompleteTimeout)
42+
43+
timeoutsRef.current.push(finalTimeout)
44+
return
45+
}
46+
47+
// First paragraph or append with double newline
48+
const paragraphToAdd = paragraphsRef.current[currentIndexRef.current]
49+
50+
if (currentIndexRef.current === 0) {
51+
setCurrentText(paragraphToAdd)
52+
} else {
53+
setCurrentText(prev => `${prev}\n\n${paragraphToAdd}`)
54+
}
55+
56+
// Track the index of the paragraph we just added
57+
setLastParagraphIndex(currentIndexRef.current)
58+
59+
// Signal that a new paragraph was added (for animation)
60+
setNewParagraphAdded(true)
61+
62+
// Call the paragraph added callback
63+
if (onParagraphAdded) {
64+
onParagraphAdded(currentIndexRef.current)
65+
}
66+
67+
// Reset animation flag after animation completes
68+
const resetTimeout = setTimeout(() => {
69+
setNewParagraphAdded(false)
70+
}, 500)
71+
72+
timeoutsRef.current.push(resetTimeout)
73+
74+
75+
currentIndexRef.current++
76+
77+
// Schedule next paragraph if there are more
78+
if (currentIndexRef.current < paragraphsRef.current.length) {
79+
const nextTimeout = setTimeout(addNextParagraph, paragraphDelay)
80+
timeoutsRef.current.push(nextTimeout)
81+
}
82+
}, [currentText, onComplete, onParagraphAdded, paragraphDelay])
83+
84+
// Start revealing text paragraph by paragraph
85+
const revealText = useCallback((text) => {
86+
if (!text) return
87+
88+
resetTextReveal()
89+
setIsRevealing(true)
90+
91+
// Split text into paragraphs (handles different line break formats)
92+
const paragraphs = text.split(/\n{2,}|\r\n{2,}/)
93+
.map(p => p.trim())
94+
.filter(p => p.length > 0)
95+
96+
if (paragraphs.length === 0) {
97+
setIsRevealing(false)
98+
return
99+
}
100+
101+
paragraphsRef.current = paragraphs
102+
currentIndexRef.current = 0
103+
104+
105+
addNextParagraph()
106+
107+
}, [resetTextReveal, addNextParagraph])
108+
109+
110+
useEffect(() => {
111+
return () => clearAllTimeouts()
112+
}, [clearAllTimeouts])
113+
114+
return {
115+
revealText,
116+
resetTextReveal,
117+
currentText,
118+
isRevealing,
119+
newParagraphAdded,
120+
lastParagraphIndex
121+
}
122+
}
123+
124+
export default useProgressiveTextReveal

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@b12/metronome",
3-
"version": "1.1.25",
3+
"version": "1.1.26",
44
"description": "",
55
"main": "index.es6.js",
66
"scripts": {

0 commit comments

Comments
 (0)