@@ -16,4 +16,221 @@ import * as ComponentStories from './TextArea.stories.tsx';
1616
1717<ControlsWithNote of = { ComponentStories .Default } />
1818
19+ ## TextArea with Fake Stream
20+
21+ TextArea component implementing simple stream handling.
22+
23+ <Canvas of = { ComponentStories .WithFakeStream } />
24+
25+ <details >
26+
27+ <summary >Show Static Code</summary >
28+
29+ ``` tsx
30+ import { TextArea , TextAreaPropTypes } from ' @ui5/webcomponents-ai-react' ;
31+ import { Menu , MenuItem , MenuPropTypes } from ' @ui5/webcomponents-react' ;
32+ import { useEffect , useRef , useState , useTransition } from ' react' ;
33+
34+ type StartStreamOptions = {
35+ text: string ;
36+ onComplete? : (fullText : string ) => void ;
37+ onProcessingComplete? : () => void ;
38+ };
39+ export function useFakeStream(initialValue = ' ' , typingDelay = 10 , startingDelay = 1500 ) {
40+ const [value, setValue] = useState (initialValue );
41+ const [transitionIsPending, startTransition] = useTransition (); // active character updates
42+ const [isProcessing, setIsProcessing] = useState (false ); // starting delay
43+ const [isTyping, setIsTyping] = useState (false ); // actively typing characters
44+ const intervalRef = useRef <ReturnType <typeof setInterval > | null >(null );
45+ const timeoutRef = useRef <ReturnType <typeof setTimeout > | null >(null );
46+ const isProcessingRef = useRef (isProcessing );
47+ const isTypingRef = useRef (isTyping );
48+ isProcessingRef .current = isProcessing ;
49+ isTypingRef .current = isTyping ;
50+
51+ const startStream = ({ text , onComplete , onProcessingComplete }: StartStreamOptions ) => {
52+ // Stop previous stream and timeout
53+ if (intervalRef .current ) {
54+ clearInterval (intervalRef .current );
55+ intervalRef .current = null ;
56+ }
57+ if (timeoutRef .current ) {
58+ clearTimeout (timeoutRef .current );
59+ timeoutRef .current = null ;
60+ }
61+
62+ setValue (' ' );
63+ setIsProcessing (true );
64+
65+ timeoutRef .current = setTimeout (() => {
66+ setIsProcessing (false );
67+
68+ if (onProcessingComplete ) {
69+ onProcessingComplete ();
70+ }
71+
72+ setIsTyping (true );
73+ let index = 0 ;
74+
75+ intervalRef .current = setInterval (() => {
76+ if (index < text .length ) {
77+ const nextChar = text [index ];
78+ index ++ ;
79+
80+ startTransition (() => {
81+ setValue ((prev ) => prev + nextChar );
82+ });
83+ } else {
84+ if (intervalRef .current ) {
85+ clearInterval (intervalRef .current );
86+ intervalRef .current = null ;
87+ }
88+ setIsTyping (false );
89+
90+ if (onComplete ) {
91+ onComplete (text );
92+ }
93+ }
94+ }, typingDelay );
95+ }, startingDelay );
96+ };
97+
98+ const stopStream = () => {
99+ if (intervalRef .current ) {
100+ clearInterval (intervalRef .current );
101+ intervalRef .current = null ;
102+ }
103+ if (timeoutRef .current ) {
104+ clearTimeout (timeoutRef .current );
105+ timeoutRef .current = null ;
106+ }
107+ setIsProcessing (false );
108+ setIsTyping (false );
109+ };
110+
111+ return { value , transitionIsPending , isProcessing , isTyping , setValue , startStream , stopStream };
112+ }
113+
114+ export function useStopStreamByESC(loading : boolean , stopStream : () => void , onStop ? : () => void ) {
115+ const loadingRef = useRef (loading );
116+ loadingRef .current = loading ;
117+
118+ useEffect (() => {
119+ const handleKeyDown = (e : KeyboardEvent ) => {
120+ if (e .key === ' Escape' && loadingRef .current ) {
121+ stopStream ();
122+ if (onStop ) {
123+ onStop ();
124+ }
125+ }
126+ };
127+
128+ window .addEventListener (' keydown' , handleKeyDown );
129+ return () => {
130+ window .removeEventListener (' keydown' , handleKeyDown );
131+ };
132+ }, [stopStream , onStop ]);
133+ }
134+
135+ const SAMPLE_TEXT =
136+ ' Innovation managers operate with both creativity and business acumen, driving initiatives that cultivate an innovation-friendly culture, streamline the execution of new ideas, and ultimately unlock value for the organization and its customers.' ;
137+
138+ type VersionHistoryItem = {
139+ action: string ;
140+ endAction: string ;
141+ timestamp: string ;
142+ value: string ;
143+ promptDescription: string ;
144+ };
145+
146+ function AITextArea(props ) {
147+ const { value, isTyping, isProcessing, setValue, startStream, stopStream } = useFakeStream ();
148+ const [versionHistory, setVersionHistory] = useState <VersionHistoryItem []>([]);
149+ const [currentHistoryIndex, setCurrentHistoryIndex] = useState (- 1 );
150+ const [promptDescription, setPromptDescription] = useState (' ' );
151+ const currentActionRef = useRef <string >(' ' );
152+ const isLoading = isProcessing || isTyping ;
153+
154+ const handleItemClick: MenuPropTypes [' onItemClick' ] = (e ) => {
155+ const { action } = e .detail .item .dataset ;
156+ if (isProcessing || ! action ) {
157+ return ;
158+ }
159+ currentActionRef .current = action ;
160+ setPromptDescription (' Generating text...' );
161+ startStream ({
162+ text: SAMPLE_TEXT ,
163+ onComplete : (fullText ) => {
164+ setVersionHistory ((prev ) => [
165+ ... prev ,
166+ {
167+ action ,
168+ endAction: ' completed' ,
169+ timestamp: new Date ().toISOString (),
170+ value: fullText ,
171+ promptDescription: ' Generated text' ,
172+ },
173+ ]);
174+ setCurrentHistoryIndex ((prev ) => prev + 1 );
175+ setValue (' ' );
176+ setPromptDescription (' ' );
177+ },
178+ });
179+ };
180+
181+ const handleStopGeneration: TextAreaPropTypes [' onStopGeneration' ] = () => {
182+ stopStream ();
183+ handleStop ();
184+ };
185+
186+ const handleStop = () => {
187+ setVersionHistory ((prev ) => [
188+ ... prev ,
189+ {
190+ action: currentActionRef .current ,
191+ endAction: ' stopped' ,
192+ timestamp: new Date ().toISOString (),
193+ value: value ,
194+ promptDescription: ' Generated text (stopped)' ,
195+ },
196+ ]);
197+ setCurrentHistoryIndex ((prev ) => prev + 1 );
198+ setValue (' ' );
199+ setPromptDescription (' ' );
200+ };
201+
202+ const handleVersionChange: TextAreaPropTypes [' onVersionChange' ] = (e ) => {
203+ setCurrentHistoryIndex ((prev ) => (e .detail .backwards ? prev - 1 : prev + 1 ));
204+ setValue (' ' );
205+ };
206+
207+ const handleInput: TextAreaPropTypes [' onInput' ] = (e ) => {
208+ setValue (e .target .value );
209+ };
210+
211+ useStopStreamByESC (isLoading , stopStream , handleStop );
212+
213+ return (
214+ <TextArea
215+ { ... props }
216+ value = { value || versionHistory [currentHistoryIndex ]?.value || ' ' }
217+ currentVersion = { currentHistoryIndex + 1 }
218+ totalVersions = { versionHistory .length }
219+ loading = { isLoading }
220+ promptDescription = { promptDescription || versionHistory [currentHistoryIndex ]?.promptDescription || ' ' }
221+ onStopGeneration = { handleStopGeneration }
222+ onVersionChange = { handleVersionChange }
223+ onInput = { handleInput }
224+ menu = {
225+ <Menu onItemClick = { handleItemClick } >
226+ <MenuItem text = " Generate text" data-action = " generate" />
227+ </Menu >
228+ }
229+ />
230+ );
231+ }
232+ ```
233+
234+ </details >
235+
19236<Footer />
0 commit comments