Skip to content

Commit 6548fe1

Browse files
marker-daomarker dao ®
andauthored
Chat: STT integration (#32505)
Co-authored-by: marker dao ® <youdontknow@marker-dao.eth>
1 parent f7cd5c0 commit 6548fe1

File tree

7 files changed

+600
-31
lines changed

7 files changed

+600
-31
lines changed

packages/devextreme/js/__internal/ui/chat/chat.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import type {
1616
MessageEnteredEvent,
1717
MessageUpdatedEvent,
1818
MessageUpdatingEvent,
19-
Properties,
19+
Properties as ChatProperties,
2020
TypingEndEvent,
2121
TypingStartEvent,
2222
} from '@js/ui/chat';
23+
import type { Properties as SpeechToTextProperties } from '@js/ui/speech_to_text';
2324
import { invokeConditionally } from '@ts/core/utils/conditional_invoke';
2425
import type { OptionChanged } from '@ts/core/widget/types';
2526
import Widget from '@ts/core/widget/widget';
@@ -40,6 +41,11 @@ import type {
4041
import MessageList from '@ts/ui/chat/messagelist';
4142
import type { DataChange } from '@ts/ui/collection/collection_widget.base';
4243

44+
type Properties = ChatProperties & {
45+
speechToTextEnabled?: boolean;
46+
speechToTextOptions?: SpeechToTextProperties;
47+
};
48+
4349
const CHAT_CLASS = 'dx-chat';
4450
const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input';
4551

@@ -101,6 +107,8 @@ class Chat extends Widget<Properties> {
101107
showDayHeaders: true,
102108
showMessageTimestamp: true,
103109
showUserName: true,
110+
speechToTextEnabled: false,
111+
speechToTextOptions: undefined,
104112
typingUsers: [],
105113
user: { id: new Guid().toString() },
106114
onMessageDeleted: undefined,
@@ -464,6 +472,8 @@ class Chat extends Widget<Properties> {
464472
hoverStateEnabled,
465473
// @ts-expect-error wait for .d.ts
466474
inputFieldText,
475+
speechToTextEnabled,
476+
speechToTextOptions,
467477
} = this.option();
468478

469479
const $messageBox = $('<div>');
@@ -476,6 +486,8 @@ class Chat extends Widget<Properties> {
476486
focusStateEnabled,
477487
hoverStateEnabled,
478488
text: inputFieldText,
489+
speechToTextEnabled,
490+
speechToTextOptions,
479491
onMessageEntered: (e) => {
480492
this._messageEnteredHandler(e);
481493
},
@@ -644,6 +656,8 @@ class Chat extends Widget<Properties> {
644656
case 'hoverStateEnabled':
645657
this._messageBox.option(name, value);
646658
break;
659+
case 'speechToTextEnabled':
660+
case 'speechToTextOptions':
647661
case 'fileUploaderOptions':
648662
this._messageBox.option(fullName, value);
649663
break;

packages/devextreme/js/__internal/ui/chat/message_box/chat_text_area.ts

Lines changed: 134 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,53 @@ import type {
1111
} from '@js/ui/button';
1212
import type Button from '@js/ui/button';
1313
import type { Attachment } from '@js/ui/chat';
14-
import type { UploadedEvent, UploadStartedEvent, ValueChangedEvent } from '@js/ui/file_uploader';
14+
import type {
15+
UploadedEvent,
16+
UploadStartedEvent,
17+
ValueChangedEvent,
18+
} from '@js/ui/file_uploader';
19+
import type dxSpeechToText from '@js/ui/speech_to_text';
20+
import type {
21+
EndEvent,
22+
InitializedEvent as InitializedSTTEvent,
23+
Properties as SpeechToTextProperties,
24+
ResultEvent,
25+
StartClickEvent,
26+
} from '@js/ui/speech_to_text';
1527
import { current, isMaterial } from '@js/ui/themes';
1628
import type { Item as ToolbarItem } from '@js/ui/toolbar';
1729
import Toolbar from '@js/ui/toolbar';
1830
import type { OptionChanged } from '@ts/core/widget/types';
1931
import type { SupportedKeys } from '@ts/core/widget/widget';
2032
import Widget from '@ts/core/widget/widget';
2133
import FileUploader from '@ts/ui/file_uploader/file_uploader';
22-
import type { CancelButtonClickEvent, FileValidationErrorEvent, Properties as FileUploaderProperties } from '@ts/ui/file_uploader/file_uploader.types';
34+
import type {
35+
CancelButtonClickEvent,
36+
FileValidationErrorEvent,
37+
Properties as FileUploaderProperties,
38+
} from '@ts/ui/file_uploader/file_uploader.types';
2339
import Informer from '@ts/ui/informer/informer';
2440
import type { TextAreaProperties } from '@ts/ui/m_text_area';
2541
import TextArea from '@ts/ui/m_text_area';
2642

43+
type EnterKeyEvent = NativeEventInfo<ChatTextArea, KeyboardEvent>;
44+
45+
export type SendEvent = ClickEvent | EnterKeyEvent;
46+
47+
type FileToSend = Attachment & {
48+
readyToSend: boolean;
49+
};
50+
51+
export type Properties = TextAreaProperties & {
52+
fileUploaderOptions?: FileUploaderProperties;
53+
54+
speechToTextEnabled?: boolean;
55+
56+
speechToTextOptions?: SpeechToTextProperties;
57+
58+
onSend?: (e: SendEvent) => void;
59+
};
60+
2761
const CHAT_TEXT_AREA_ATTACHMENTS = 'dx-chat-textarea-attachments';
2862
export const CHAT_TEXT_AREA_ATTACH_BUTTON = 'dx-chat-textarea-attach-button';
2963

@@ -38,23 +72,15 @@ const ERRORS = {
3872
fileLimit: messageLocalization.format('dxChat-fileLimitReachedWarning', MAX_ATTACHMENTS_COUNT),
3973
};
4074

41-
const isMobile = (): boolean => devices.current().deviceType !== 'desktop';
42-
43-
export const DEFAULT_ALLOWED_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.pdf', '.docx', '.xlsx', '.pptx', '.txt', '.rtf', '.csv', '.md'];
44-
45-
type EnterKeyEvent = NativeEventInfo<ChatTextArea, KeyboardEvent>;
46-
47-
export type SendEvent = ClickEvent | EnterKeyEvent;
48-
49-
type FileToSend = Attachment & {
50-
readyToSend: boolean;
75+
const STT_INITIAL_STATE = {
76+
stylingMode: 'text' as const,
77+
type: 'normal',
78+
stopIcon: 'micfilled',
5179
};
5280

53-
export type Properties = TextAreaProperties & {
54-
fileUploaderOptions?: FileUploaderProperties;
81+
const isMobile = (): boolean => devices.current().deviceType !== 'desktop';
5582

56-
onSend?: (e: SendEvent) => void;
57-
};
83+
export const DEFAULT_ALLOWED_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.pdf', '.docx', '.xlsx', '.pptx', '.txt', '.rtf', '.csv', '.md'];
5884

5985
class ChatTextArea extends TextArea<Properties> {
6086
_informerTimeoutId?: ReturnType<typeof setTimeout> | undefined;
@@ -71,10 +97,14 @@ class ChatTextArea extends TextArea<Properties> {
7197

7298
_filesToSend?: Map<File, FileToSend>;
7399

100+
_initialInputText?: string;
101+
74102
_attachButton?: Button;
75103

76104
_sendButton?: Button;
77105

106+
_speechToTextButton?: dxSpeechToText;
107+
78108
_sendAction?: (e: Partial<SendEvent>) => void;
79109

80110
getAttachments(): Attachment[] | undefined {
@@ -90,11 +120,13 @@ class ChatTextArea extends TextArea<Properties> {
90120
_getDefaultOptions(): Properties {
91121
return {
92122
...super._getDefaultOptions(),
93-
stylingMode: 'outlined',
94-
placeholder: messageLocalization.format('dxChat-textareaPlaceholder'),
95123
autoResizeEnabled: true,
96-
valueChangeEvent: 'input',
97124
maxHeight: '53.86em',
125+
placeholder: messageLocalization.format('dxChat-textareaPlaceholder'),
126+
speechToTextEnabled: false,
127+
speechToTextOptions: undefined,
128+
stylingMode: 'outlined',
129+
valueChangeEvent: 'input',
98130
};
99131
}
100132

@@ -136,6 +168,8 @@ class ChatTextArea extends TextArea<Properties> {
136168
}
137169

138170
_init(): void {
171+
this._initialInputText = '';
172+
139173
super._init();
140174

141175
this._createSendAction();
@@ -208,16 +242,20 @@ class ChatTextArea extends TextArea<Properties> {
208242
}
209243

210244
_getToolbarItems(): ToolbarItem[] {
211-
const { fileUploaderOptions } = this.option();
245+
const { fileUploaderOptions, speechToTextEnabled } = this.option();
212246

213-
const items = [
214-
this._getSendButtonConfig(),
215-
];
247+
const items: ToolbarItem[] = [];
216248

217249
if (fileUploaderOptions) {
218250
items.push(this._getAttachButtonConfig());
219251
}
220252

253+
if (speechToTextEnabled) {
254+
items.push(this._getSpeechToTextButtonConfig());
255+
}
256+
257+
items.push(this._getSendButtonConfig());
258+
221259
return items;
222260
}
223261

@@ -247,6 +285,54 @@ class ChatTextArea extends TextArea<Properties> {
247285
return configuration;
248286
}
249287

288+
_getSpeechToTextButtonOptions(): SpeechToTextProperties {
289+
const {
290+
activeStateEnabled,
291+
focusStateEnabled,
292+
hoverStateEnabled,
293+
speechToTextOptions,
294+
} = this.option();
295+
296+
const options = {
297+
activeStateEnabled,
298+
focusStateEnabled,
299+
hoverStateEnabled,
300+
...speechToTextOptions,
301+
...STT_INITIAL_STATE,
302+
onEnd: (e: EndEvent): void => {
303+
this._initialInputText = '';
304+
305+
speechToTextOptions?.onEnd?.(e);
306+
},
307+
onInitialized: (e: InitializedSTTEvent): void => {
308+
this._speechToTextButton = e.component;
309+
310+
speechToTextOptions?.onInitialized?.(e);
311+
},
312+
onResult: (e: ResultEvent): void => this._resultHandler(e),
313+
onStartClick: (e: StartClickEvent): void => {
314+
const { text } = this.option();
315+
316+
this._initialInputText = text;
317+
318+
speechToTextOptions?.onStartClick?.(e);
319+
},
320+
};
321+
322+
return options;
323+
}
324+
325+
_getSpeechToTextButtonConfig(): ToolbarItem {
326+
// @ts-expect-error dxSpeechToText should be added to ToolbarItemComponent
327+
const configuration = {
328+
widget: 'dxSpeechToText',
329+
location: 'after',
330+
options: this._getSpeechToTextButtonOptions(),
331+
} as ToolbarItem;
332+
333+
return configuration;
334+
}
335+
250336
_getSendButtonConfig(): ToolbarItem {
251337
const {
252338
activeStateEnabled,
@@ -280,6 +366,22 @@ class ChatTextArea extends TextArea<Properties> {
280366
return configuration;
281367
}
282368

369+
_resultHandler(e: ResultEvent): void {
370+
const { speechToTextOptions } = this.option();
371+
372+
// @ts-expect-error SpeechRecognition API is not supported in TS
373+
const speechRecognitionResult = Object.values(e.event.results)
374+
// @ts-expect-error SpeechRecognition API is not supported in TS
375+
.map((resultItem) => (resultItem[0].transcript as string).trim())
376+
.join(' ');
377+
378+
const result = `${this._initialInputText} ${speechRecognitionResult}`.trim();
379+
380+
this.option({ value: result });
381+
382+
speechToTextOptions?.onResult?.(e);
383+
}
384+
283385
_initFileUploader(): void {
284386
const { fileUploaderOptions } = this.option();
285387

@@ -449,6 +551,7 @@ class ChatTextArea extends TextArea<Properties> {
449551
case 'focusStateEnabled':
450552
case 'hoverStateEnabled':
451553
this._sendButton?.option(name, value);
554+
this._speechToTextButton?.option(name, value);
452555
break;
453556

454557
case 'text':
@@ -464,6 +567,14 @@ class ChatTextArea extends TextArea<Properties> {
464567
this._handleFileUploaderOptionsChange(args);
465568
break;
466569

570+
case 'speechToTextEnabled':
571+
this._toolbar?.option({ items: this._getToolbarItems() });
572+
break;
573+
574+
case 'speechToTextOptions':
575+
this._speechToTextButton?.option(this._getSpeechToTextButtonOptions());
576+
break;
577+
467578
default:
468579
super._optionChanged(args);
469580
}

packages/devextreme/js/__internal/ui/chat/message_box/message_box.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import $, { type dxElementWrapper } from '@js/core/renderer';
33
import type { InteractionEvent } from '@js/events';
44
import type { Attachment } from '@js/ui/chat';
55
import type { Properties as FileUploaderProperties } from '@js/ui/file_uploader';
6+
import type { Properties as SpeechToTextProperties } from '@js/ui/speech_to_text';
67
import type { InputEvent } from '@js/ui/text_area';
78
import type { DOMComponentProperties } from '@ts/core/widget/dom_component';
89
import DOMComponent from '@ts/core/widget/dom_component';
@@ -14,12 +15,6 @@ import type {
1415
import ChatTextArea from '@ts/ui/chat/message_box/chat_text_area';
1516
import EditingPreview from '@ts/ui/chat/message_box/editing_preview';
1617

17-
export const CHAT_MESSAGEBOX_CLASS = 'dx-chat-messagebox';
18-
export const CHAT_MESSAGEBOX_TEXTAREA_CONTAINER_CLASS = 'dx-chat-messagebox-textarea-container';
19-
20-
export const TYPING_END_DELAY = 2000;
21-
const ESCAPE_KEY = 'escape';
22-
2318
export type MessageEnteredEvent = NativeEventInfo<MessageBox, InteractionEvent>
2419
& {
2520
text?: string;
@@ -39,6 +34,10 @@ export interface Properties extends DOMComponentProperties<MessageBox> {
3934

4035
previewText?: string;
4136

37+
speechToTextEnabled?: boolean;
38+
39+
speechToTextOptions?: SpeechToTextProperties;
40+
4241
text?: string;
4342

4443
onMessageEntered?: (e: MessageEnteredEvent) => void;
@@ -52,6 +51,12 @@ export interface Properties extends DOMComponentProperties<MessageBox> {
5251
onMessageUpdating?: (e: { text: string }) => void;
5352
}
5453

54+
export const CHAT_MESSAGEBOX_CLASS = 'dx-chat-messagebox';
55+
export const CHAT_MESSAGEBOX_TEXTAREA_CONTAINER_CLASS = 'dx-chat-messagebox-textarea-container';
56+
57+
export const TYPING_END_DELAY = 2000;
58+
const ESCAPE_KEY = 'escape';
59+
5560
class MessageBox extends DOMComponent<MessageBox, Properties> {
5661
_textArea!: ChatTextArea;
5762

@@ -73,6 +78,8 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
7378
hoverStateEnabled: true,
7479
fileUploaderOptions: undefined,
7580
previewText: '',
81+
speechToTextEnabled: false,
82+
speechToTextOptions: undefined,
7683
text: '',
7784
onMessageEntered: undefined,
7885
onMessageEditCanceled: undefined,
@@ -163,6 +170,8 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
163170
focusStateEnabled,
164171
hoverStateEnabled,
165172
previewText,
173+
speechToTextEnabled,
174+
speechToTextOptions,
166175
text,
167176
} = this.option();
168177

@@ -173,6 +182,8 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
173182
hoverStateEnabled,
174183
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
175184
value: previewText || text,
185+
speechToTextEnabled,
186+
speechToTextOptions,
176187
onInput: (e: InputEvent): void => {
177188
this._triggerTypingStartAction(e);
178189
this._updateTypingEndTimeout();
@@ -287,6 +298,11 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
287298
this._createTypingEndAction();
288299
break;
289300

301+
case 'speechToTextEnabled':
302+
case 'speechToTextOptions':
303+
this._textArea.option(fullName, value);
304+
break;
305+
290306
case 'previewText':
291307
this._textArea.option('value', value);
292308
this._updateEditingPreview(value);

0 commit comments

Comments
 (0)