Skip to content

Commit 54b30fa

Browse files
committed
Modified the fade-in effect for revealing text when enhancing prompt
1 parent a806363 commit 54b30fa

File tree

5 files changed

+489
-43
lines changed

5 files changed

+489
-43
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+
5+
function PromptInput ({ className, children }) {
6+
7+
let textarea = null
8+
let actions = null
9+
let overlay = null
10+
11+
React.Children.forEach(children, child => {
12+
if (child.type && child.type.displayName === 'PromptInputTextarea') {
13+
textarea = child
14+
} else if (child.type && child.type.displayName === 'PromptInputActions') {
15+
actions = child
16+
} else if (child.type && child.type.name === 'TextRevealOverlay') {
17+
overlay = child
18+
}
19+
})
420

5-
function PromptInput ({ children, className }) {
621
return (
722
<div className={classnames('ds-prompt-input', className)}>
8-
{children}
23+
{textarea}
24+
{overlay}
25+
{actions}
26+
{React.Children.map(children, child => {
27+
if ((child.type && child.type.displayName !== 'PromptInputTextarea' &&
28+
child.type.displayName !== 'PromptInputActions') &&
29+
(child.type && child.type.name !== 'TextRevealOverlay')) {
30+
return child
31+
}
32+
return null
33+
})}
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: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import React, { useEffect, useState, useRef } from 'react'
2+
import PropTypes from 'prop-types'
3+
4+
function TextRevealOverlay({
5+
text,
6+
isRevealing,
7+
lastParagraphIndex,
8+
className,
9+
textareaRef,
10+
onAnimationComplete
11+
}) {
12+
const [paragraphs, setParagraphs] = useState([])
13+
const [isAnimatingLastParagraph, setIsAnimatingLastParagraph] = useState(false)
14+
const overlayRef = useRef(null)
15+
16+
// Split text into paragraphs whenever text changes
17+
useEffect(() => {
18+
if (text) {
19+
const split = text.split(/\n{2,}|\r\n{2,}/)
20+
.map(p => p.trim())
21+
.filter(p => p.length > 0)
22+
setParagraphs(split)
23+
} else {
24+
setParagraphs([])
25+
}
26+
}, [text])
27+
28+
useEffect(() => {
29+
if (isAnimatingLastParagraph && lastParagraphIndex === paragraphs.length - 1) {
30+
const timer = setTimeout(() => {
31+
setIsAnimatingLastParagraph(false)
32+
if (onAnimationComplete) {
33+
onAnimationComplete()
34+
}
35+
}, 500)
36+
37+
return () => clearTimeout(timer)
38+
}
39+
}, [isAnimatingLastParagraph, lastParagraphIndex, paragraphs.length, onAnimationComplete])
40+
41+
42+
useEffect(() => {
43+
if (lastParagraphIndex === paragraphs.length - 1 && paragraphs.length > 0) {
44+
setIsAnimatingLastParagraph(true)
45+
}
46+
}, [lastParagraphIndex, paragraphs.length])
47+
48+
49+
useEffect(() => {
50+
if (!textareaRef || !textareaRef.current || !overlayRef.current || !isRevealing) return
51+
52+
const textarea = textareaRef.current
53+
const overlay = overlayRef.current
54+
55+
// Function to update dimensions whenever they change
56+
const updateDimensions = () => {
57+
// Position relative to the prompt-input container
58+
const promptInput = textarea.closest('.ds-prompt-input')
59+
let paddingLeft = '16px'
60+
let paddingTop = '16px'
61+
62+
if (promptInput) {
63+
const promptStyles = window.getComputedStyle(promptInput)
64+
if (promptStyles.paddingLeft) {
65+
paddingLeft = promptStyles.paddingLeft
66+
}
67+
if (promptStyles.paddingTop) {
68+
paddingTop = promptStyles.paddingTop
69+
}
70+
}
71+
72+
// Get current styles
73+
const textareaStyles = window.getComputedStyle(textarea)
74+
75+
// Apply exact dimensions and styling
76+
overlay.style.width = textareaStyles.width
77+
overlay.style.fontSize = textareaStyles.fontSize
78+
overlay.style.lineHeight = textareaStyles.lineHeight
79+
overlay.style.fontFamily = textareaStyles.fontFamily
80+
overlay.style.fontWeight = textareaStyles.fontWeight
81+
overlay.style.color = textareaStyles.color
82+
83+
// Set exact position
84+
overlay.style.position = 'absolute'
85+
overlay.style.top = paddingTop
86+
overlay.style.left = paddingLeft
87+
overlay.style.width = 'calc(100% - ' + (parseInt(paddingLeft) * 2) + 'px)'
88+
89+
// Set max-height to match textarea max-height
90+
overlay.style.maxHeight = textareaStyles.maxHeight
91+
92+
// Match the actual height if it's calculated
93+
if (textarea.style.height) {
94+
overlay.style.height = textarea.style.height
95+
}
96+
}
97+
98+
// Initial update
99+
updateDimensions()
100+
101+
// Set up mutation observer to detect height changes
102+
let observer = null
103+
if (typeof MutationObserver !== 'undefined') {
104+
observer = new MutationObserver((mutations) => {
105+
mutations.forEach((mutation) => {
106+
if (mutation.attributeName === 'style') {
107+
updateDimensions()
108+
}
109+
})
110+
})
111+
112+
observer.observe(textarea, {
113+
attributes: true,
114+
attributeFilter: ['style']
115+
})
116+
}
117+
118+
const syncScroll = () => {
119+
if (overlayRef.current && isRevealing) {
120+
overlayRef.current.scrollTop = textarea.scrollTop
121+
}
122+
}
123+
124+
textarea.addEventListener('scroll', syncScroll)
125+
126+
window.addEventListener('resize', updateDimensions)
127+
128+
syncScroll()
129+
130+
return () => {
131+
textarea.removeEventListener('scroll', syncScroll)
132+
window.removeEventListener('resize', updateDimensions)
133+
if (observer) {
134+
observer.disconnect()
135+
}
136+
}
137+
}, [textareaRef, isRevealing, paragraphs])
138+
139+
if (!isRevealing || paragraphs.length === 0) {
140+
return null
141+
}
142+
143+
return (
144+
<div
145+
ref={overlayRef}
146+
className={`ds-prompt-input__text-reveal-overlay ${className || ''}`}
147+
aria-hidden="true"
148+
>
149+
{paragraphs.map((paragraph, index) => (
150+
<p
151+
key={index}
152+
className={`ds-prompt-input__text-reveal-paragraph ${
153+
index === lastParagraphIndex ? 'ds-prompt-input__text-reveal-paragraph--animating' : ''
154+
}`}
155+
>
156+
{paragraph}
157+
</p>
158+
))}
159+
</div>
160+
)
161+
}
162+
163+
TextRevealOverlay.propTypes = {
164+
text: PropTypes.string,
165+
isRevealing: PropTypes.bool,
166+
lastParagraphIndex: PropTypes.number,
167+
className: PropTypes.string,
168+
textareaRef: PropTypes.object,
169+
onAnimationComplete: PropTypes.func
170+
}
171+
172+
TextRevealOverlay.defaultProps = {
173+
text: '',
174+
isRevealing: false,
175+
lastParagraphIndex: -1,
176+
className: '',
177+
textareaRef: null,
178+
onAnimationComplete: null
179+
}
180+
181+
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

0 commit comments

Comments
 (0)