| import { useState, useEffect, useCallback, useRef } from 'react'; |
|
|
| export interface StreamingTextOptions { |
| text: string; |
| speed?: number; |
| onComplete?: () => void; |
| enabled?: boolean; |
| } |
|
|
| export interface StreamingTextResult { |
| displayedText: string; |
| isStreaming: boolean; |
| skip: () => void; |
| reset: () => void; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function useStreamingText(options: StreamingTextOptions): StreamingTextResult { |
| const { text, speed = 30, onComplete, enabled = true } = options; |
|
|
| const [displayedText, setDisplayedText] = useState(''); |
| const [isStreaming, setIsStreaming] = useState(false); |
|
|
| const frameRef = useRef<number | null>(null); |
| const startTimeRef = useRef<number | null>(null); |
| const lastIndexRef = useRef(0); |
|
|
| |
| |
| |
| const skip = useCallback(() => { |
| if (frameRef.current) { |
| cancelAnimationFrame(frameRef.current); |
| frameRef.current = null; |
| } |
| setDisplayedText(text); |
| setIsStreaming(false); |
| startTimeRef.current = null; |
| lastIndexRef.current = text.length; |
| onComplete?.(); |
| }, [text, onComplete]); |
|
|
| |
| |
| |
| const reset = useCallback(() => { |
| if (frameRef.current) { |
| cancelAnimationFrame(frameRef.current); |
| frameRef.current = null; |
| } |
| setDisplayedText(''); |
| setIsStreaming(false); |
| startTimeRef.current = null; |
| lastIndexRef.current = 0; |
| }, []); |
|
|
| useEffect(() => { |
| |
| |
| if (!enabled || !text) { |
| setDisplayedText((prev) => (prev !== text ? text : prev)); |
| setIsStreaming((prev) => (prev ? false : prev)); |
| return; |
| } |
|
|
| |
| if (text.length > 500) { |
| setDisplayedText(text); |
| setIsStreaming(false); |
| onComplete?.(); |
| return; |
| } |
|
|
| |
| setIsStreaming(true); |
| setDisplayedText(''); |
| |
| lastIndexRef.current = 0; |
|
|
| const animate = (timestamp: number) => { |
| if (!startTimeRef.current) { |
| startTimeRef.current = timestamp; |
| } |
|
|
| const elapsed = timestamp - startTimeRef.current; |
| const targetIndex = Math.min(Math.floor((elapsed / 1000) * speed), text.length); |
|
|
| if (targetIndex > lastIndexRef.current) { |
| lastIndexRef.current = targetIndex; |
| setDisplayedText(text.slice(0, targetIndex)); |
| } |
|
|
| if (targetIndex < text.length) { |
| frameRef.current = requestAnimationFrame(animate); |
| } else { |
| setIsStreaming(false); |
| startTimeRef.current = null; |
| onComplete?.(); |
| } |
| }; |
|
|
| frameRef.current = requestAnimationFrame(animate); |
|
|
| return () => { |
| if (frameRef.current) { |
| cancelAnimationFrame(frameRef.current); |
| } |
| }; |
| }, [text, speed, enabled, onComplete]); |
|
|
| return { |
| displayedText, |
| isStreaming, |
| skip, |
| reset, |
| }; |
| } |
|
|