Skip to content

Commit 283cce7

Browse files
authored
docs(TextArea (AI)): add stream example (#7923)
1 parent b80af8d commit 283cce7

File tree

6 files changed

+355
-6
lines changed

6 files changed

+355
-6
lines changed

.storybook/utils.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type * as CEM from '@ui5/webcomponents-tools/lib/cem/types-internal';
2-
import { useMemo, useRef, useState, useTransition } from 'react';
2+
import { useEffect, useMemo, useRef, useState, useTransition } from 'react';
33
// @ts-expect-error: storybook can handle this
44
import cemAi from './custom-element-manifests/ai.json';
55
// @ts-expect-error: storybook can handle this
@@ -91,13 +91,17 @@ type StartStreamOptions = {
9191
onComplete?: (fullText: string) => void;
9292
onProcessingComplete?: () => void;
9393
};
94-
export function useFakeStream(typingDelay = 10, startingDelay = 1500) {
95-
const [value, setValue] = useState('');
94+
export function useFakeStream(initialValue = '', typingDelay = 10, startingDelay = 1500) {
95+
const [value, setValue] = useState(initialValue);
9696
const [transitionIsPending, startTransition] = useTransition(); // active character updates
9797
const [isProcessing, setIsProcessing] = useState(false); // starting delay
9898
const [isTyping, setIsTyping] = useState(false); // actively typing characters
9999
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
100100
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
101+
const isProcessingRef = useRef(isProcessing);
102+
const isTypingRef = useRef(isTyping);
103+
isProcessingRef.current = isProcessing;
104+
isTypingRef.current = isTyping;
101105

102106
const startStream = ({ text, onComplete, onProcessingComplete }: StartStreamOptions) => {
103107
// Stop previous stream and timeout
@@ -161,3 +165,24 @@ export function useFakeStream(typingDelay = 10, startingDelay = 1500) {
161165

162166
return { value, transitionIsPending, isProcessing, isTyping, setValue, startStream, stopStream };
163167
}
168+
169+
export function useStopStreamByESC(loading: boolean, stopStream: () => void, onStop?: () => void) {
170+
const loadingRef = useRef(loading);
171+
loadingRef.current = loading;
172+
173+
useEffect(() => {
174+
const handleKeyDown = (e: KeyboardEvent) => {
175+
if (e.key === 'Escape' && loadingRef.current) {
176+
stopStream();
177+
if (onStop) {
178+
onStop();
179+
}
180+
}
181+
};
182+
183+
window.addEventListener('keydown', handleKeyDown);
184+
return () => {
185+
window.removeEventListener('keydown', handleKeyDown);
186+
};
187+
}, [stopStream, onStop]);
188+
}

config/version-info.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,5 @@
5858
"2.13.1": "2.13.0",
5959
"2.14.0": "2.14.0",
6060
"2.15.0": "2.15.0",
61-
"2.16.0": "2.16.1"
61+
"2.16.1": "2.16.0"
6262
}

packages/ai/src/components/Input/Input.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ Input component implementing simple stream handling.
206206
const [placeholder, setPlaceholder] = useState(initialPlaceholder);
207207
const hasHistory = versionHistory.length > 0;
208208
const currentActionRef = useRef<string>('');
209-
const { value, isTyping, isProcessing, setValue, startStream, stopStream } = useFakeStream(50);
209+
const { value, isTyping, isProcessing, setValue, startStream, stopStream } = useFakeStream();
210210

211211
const handleVersionChange: InputPropTypes['onVersionChange'] = (e) => {
212212
setCurrentHistoryIndex((prev) => (e.detail.backwards ? prev - 1 : prev + 1));

packages/ai/src/components/Input/Input.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export const WithFakeStream: Story = {
130130
const [placeholder, setPlaceholder] = useState(initialPlaceholder);
131131
const hasHistory = versionHistory.length > 0;
132132
const currentActionRef = useRef<string>('');
133-
const { value, isTyping, isProcessing, setValue, startStream, stopStream } = useFakeStream(50);
133+
const { value, isTyping, isProcessing, setValue, startStream, stopStream } = useFakeStream();
134134

135135
const handleVersionChange: InputPropTypes['onVersionChange'] = (e) => {
136136
setCurrentHistoryIndex((prev) => (e.detail.backwards ? prev - 1 : prev + 1));

packages/ai/src/components/TextArea/TextArea.mdx

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)