From 77d0bcb3238537f1cfaccead86ac929b19631d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Thu, 9 Apr 2026 17:00:09 +0200 Subject: [PATCH 1/3] Chat: Add suggestion option type --- packages/devextreme/js/ui/chat.d.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/devextreme/js/ui/chat.d.ts b/packages/devextreme/js/ui/chat.d.ts index 101af800253b..4a31636dc88b 100644 --- a/packages/devextreme/js/ui/chat.d.ts +++ b/packages/devextreme/js/ui/chat.d.ts @@ -13,6 +13,7 @@ import { import { Properties as FileUploaderProperties } from './file_uploader'; import { Properties as SpeechToTextProperties } from './speech_to_text'; +import { Properties as ButtonGroupProperties } from './button_group'; import { Properties as TextAreaProperties } from './text_area'; import { ValueChangedInfo } from './editor/editor'; import Widget, { WidgetOptions } from './widget/ui.widget'; @@ -532,6 +533,12 @@ export interface dxChatOptions extends WidgetOptions { * @public */ speechToTextOptions?: Omit; + /** + * @docid + * @type dxButtonGroupOptions + * @public + */ + suggestions?: Omit; /** * @docid * @default [] From 5c85dc28e1197255dc3227d8ad849adf3b63107f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Thu, 9 Apr 2026 22:12:12 +0200 Subject: [PATCH 2/3] feat(make-angular-metadata.ts): Use removeMembers for suggestions --- packages/devextreme-metadata/make-angular-metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme-metadata/make-angular-metadata.ts b/packages/devextreme-metadata/make-angular-metadata.ts index 8754150e81dc..42774f532e9f 100644 --- a/packages/devextreme-metadata/make-angular-metadata.ts +++ b/packages/devextreme-metadata/make-angular-metadata.ts @@ -51,7 +51,7 @@ Ng.makeMetadata({ removeMembers(/\/card_view:/), removeMembers(/\/chat:TextMessage.attachments/), removeMembers( - /\/chat:dxChatOptions\.(fileUploaderOptions|inputFieldText|sendButtonOptions|speechToTextOptions|onAttachmentDownloadClick)/, + /\/chat:dxChatOptions\.(fileUploaderOptions|inputFieldText|sendButtonOptions|speechToTextOptions|suggestions|onAttachmentDownloadClick)/, ), removeMembers(/\/form:dxFormOptions\.(aiIntegration|onSmartPasting|onSmartPasted|smartPaste)/), removeMembers(/\/form:dxFormSimpleItem\.aiOptions/), From 2700ebec740e30ae9f5e5bfe719759e8d0b104f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Fri, 10 Apr 2026 08:59:14 +0200 Subject: [PATCH 3/3] regenerate(chat) --- .../devextreme-angular/src/ui/chat/index.ts | 28 ++ .../src/ui/chat/nested/chat-item-dxi.ts | 159 ++++++++++ .../src/ui/chat/nested/index.ts | 3 + .../src/ui/chat/nested/item-dxi.ts | 85 ++++- .../ui/chat/nested/suggestions-item-dxi.ts | 155 +++++++++ .../src/ui/chat/nested/suggestions.ts | 300 ++++++++++++++++++ packages/devextreme-react/src/chat.ts | 160 +++++++++- packages/devextreme-vue/src/chat.ts | 176 +++++++++- packages/devextreme/ts/dx.all.d.ts | 4 + 9 files changed, 1056 insertions(+), 14 deletions(-) create mode 100644 packages/devextreme-angular/src/ui/chat/nested/chat-item-dxi.ts create mode 100644 packages/devextreme-angular/src/ui/chat/nested/suggestions-item-dxi.ts create mode 100644 packages/devextreme-angular/src/ui/chat/nested/suggestions.ts diff --git a/packages/devextreme-angular/src/ui/chat/index.ts b/packages/devextreme-angular/src/ui/chat/index.ts index 01c951c04130..62047c212042 100644 --- a/packages/devextreme-angular/src/ui/chat/index.ts +++ b/packages/devextreme-angular/src/ui/chat/index.ts @@ -28,6 +28,7 @@ import type { Store } from 'devextreme/data/store'; import type { Format } from 'devextreme/common/core/localization'; import type { dxFileUploaderOptions } from 'devextreme/ui/file_uploader'; import type { dxSpeechToTextOptions } from 'devextreme/ui/speech_to_text'; +import type { dxButtonGroupOptions } from 'devextreme/ui/button_group'; import DxChat from 'devextreme/ui/chat'; @@ -55,6 +56,7 @@ import { DxoUserModule } from 'devextreme-angular/ui/nested'; import { DxiChatAlertModule } from 'devextreme-angular/ui/chat/nested'; import { DxiChatAttachmentModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatAuthorModule } from 'devextreme-angular/ui/chat/nested'; +import { DxiChatChatItemModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatCustomSpeechRecognizerModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatDayHeaderFormatModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatEditingModule } from 'devextreme-angular/ui/chat/nested'; @@ -64,6 +66,8 @@ import { DxoChatMessageTimestampFormatModule } from 'devextreme-angular/ui/chat/ import { DxoChatSendButtonOptionsModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatSpeechRecognitionConfigModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatSpeechToTextOptionsModule } from 'devextreme-angular/ui/chat/nested'; +import { DxoChatSuggestionsModule } from 'devextreme-angular/ui/chat/nested'; +import { DxiChatSuggestionsItemModule } from 'devextreme-angular/ui/chat/nested'; import { DxiChatTypingUserModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatUserModule } from 'devextreme-angular/ui/chat/nested'; import { @@ -454,6 +458,16 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges } + + @Input() + get suggestions(): dxButtonGroupOptions { + return this._getOption('suggestions'); + } + set suggestions(value: dxButtonGroupOptions) { + this._setOption('suggestions', value); + } + + /** * [descr:dxChatOptions.typingUsers] @@ -806,6 +820,13 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges */ @Output() speechToTextOptionsChange: EventEmitter; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() suggestionsChange: EventEmitter; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -888,6 +909,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges { emit: 'showUserNameChange' }, { emit: 'speechToTextEnabledChange' }, { emit: 'speechToTextOptionsChange' }, + { emit: 'suggestionsChange' }, { emit: 'typingUsersChange' }, { emit: 'userChange' }, { emit: 'visibleChange' }, @@ -956,6 +978,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxiChatAlertModule, DxiChatAttachmentModule, DxoChatAuthorModule, + DxiChatChatItemModule, DxoChatCustomSpeechRecognizerModule, DxoChatDayHeaderFormatModule, DxoChatEditingModule, @@ -965,6 +988,8 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxoChatSendButtonOptionsModule, DxoChatSpeechRecognitionConfigModule, DxoChatSpeechToTextOptionsModule, + DxoChatSuggestionsModule, + DxiChatSuggestionsItemModule, DxiChatTypingUserModule, DxoChatUserModule, DxIntegrationModule, @@ -983,6 +1008,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxiChatAlertModule, DxiChatAttachmentModule, DxoChatAuthorModule, + DxiChatChatItemModule, DxoChatCustomSpeechRecognizerModule, DxoChatDayHeaderFormatModule, DxoChatEditingModule, @@ -992,6 +1018,8 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxoChatSendButtonOptionsModule, DxoChatSpeechRecognitionConfigModule, DxoChatSpeechToTextOptionsModule, + DxoChatSuggestionsModule, + DxiChatSuggestionsItemModule, DxiChatTypingUserModule, DxoChatUserModule, DxTemplateModule diff --git a/packages/devextreme-angular/src/ui/chat/nested/chat-item-dxi.ts b/packages/devextreme-angular/src/ui/chat/nested/chat-item-dxi.ts new file mode 100644 index 000000000000..5a0f83dd4831 --- /dev/null +++ b/packages/devextreme-angular/src/ui/chat/nested/chat-item-dxi.ts @@ -0,0 +1,159 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + NgModule, + Host, + SkipSelf, + Input, + ContentChildren, + QueryList +} from '@angular/core'; + + + + +import type { Attachment, User } from 'devextreme/ui/chat'; + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { CollectionNestedOption } from 'devextreme-angular/core'; + +import { PROPERTY_TOKEN_items } from 'devextreme-angular/core/tokens'; +import { + PROPERTY_TOKEN_attachments, +} from 'devextreme-angular/core/tokens'; + +@Component({ + selector: 'dxi-chat-chat-item', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [ + NestedOptionHost, + { + provide: PROPERTY_TOKEN_items, + useExisting: DxiChatChatItemComponent, + } + ] +}) +export class DxiChatChatItemComponent extends CollectionNestedOption { + @ContentChildren(PROPERTY_TOKEN_attachments) + set _attachmentsContentChildren(value: QueryList) { + this.setChildren('attachments', value); + } + + @Input() + get alt(): string { + return this._getOption('alt'); + } + set alt(value: string) { + this._setOption('alt', value); + } + + @Input() + get attachments(): Array { + return this._getOption('attachments'); + } + set attachments(value: Array) { + this._setOption('attachments', value); + } + + @Input() + get author(): User { + return this._getOption('author'); + } + set author(value: User) { + this._setOption('author', value); + } + + @Input() + get id(): number | string { + return this._getOption('id'); + } + set id(value: number | string) { + this._setOption('id', value); + } + + @Input() + get isDeleted(): boolean { + return this._getOption('isDeleted'); + } + set isDeleted(value: boolean) { + this._setOption('isDeleted', value); + } + + @Input() + get isEdited(): boolean { + return this._getOption('isEdited'); + } + set isEdited(value: boolean) { + this._setOption('isEdited', value); + } + + @Input() + get src(): string { + return this._getOption('src'); + } + set src(value: string) { + this._setOption('src', value); + } + + @Input() + get text(): string { + return this._getOption('text'); + } + set text(value: string) { + this._setOption('text', value); + } + + @Input() + get timestamp(): Date | number | string { + return this._getOption('timestamp'); + } + set timestamp(value: Date | number | string) { + this._setOption('timestamp', value); + } + + @Input() + get type(): string | undefined { + return this._getOption('type'); + } + set type(value: string | undefined) { + this._setOption('type', value); + } + + + protected get _optionPath() { + return 'items'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + + ngOnDestroy() { + this._deleteRemovedOptions(this._fullOptionPath()); + } + +} + +@NgModule({ + imports: [ + DxiChatChatItemComponent + ], + exports: [ + DxiChatChatItemComponent + ], +}) +export class DxiChatChatItemModule { } diff --git a/packages/devextreme-angular/src/ui/chat/nested/index.ts b/packages/devextreme-angular/src/ui/chat/nested/index.ts index 3dc191bfbd5c..f0db937f8d34 100644 --- a/packages/devextreme-angular/src/ui/chat/nested/index.ts +++ b/packages/devextreme-angular/src/ui/chat/nested/index.ts @@ -1,6 +1,7 @@ export * from './alert-dxi'; export * from './attachment-dxi'; export * from './author'; +export * from './chat-item-dxi'; export * from './custom-speech-recognizer'; export * from './day-header-format'; export * from './editing'; @@ -10,6 +11,8 @@ export * from './message-timestamp-format'; export * from './send-button-options'; export * from './speech-recognition-config'; export * from './speech-to-text-options'; +export * from './suggestions-item-dxi'; +export * from './suggestions'; export * from './typing-user-dxi'; export * from './user'; diff --git a/packages/devextreme-angular/src/ui/chat/nested/item-dxi.ts b/packages/devextreme-angular/src/ui/chat/nested/item-dxi.ts index 979e3dff6d7d..9d248d4aa3a4 100644 --- a/packages/devextreme-angular/src/ui/chat/nested/item-dxi.ts +++ b/packages/devextreme-angular/src/ui/chat/nested/item-dxi.ts @@ -5,20 +5,29 @@ import { Component, NgModule, Host, + ElementRef, + Renderer2, + Inject, + AfterViewInit, SkipSelf, Input, ContentChildren, QueryList } from '@angular/core'; - +import { DOCUMENT } from '@angular/common'; import type { Attachment, User } from 'devextreme/ui/chat'; +import type { ButtonType } from 'devextreme/common'; import { DxIntegrationModule, NestedOptionHost, + extractTemplate, + DxTemplateDirective, + IDxTemplateHost, + DxTemplateHost, } from 'devextreme-angular/core'; import { CollectionNestedOption } from 'devextreme-angular/core'; @@ -30,18 +39,20 @@ import { @Component({ selector: 'dxi-chat-item', standalone: true, - template: '', - styles: [''], + template: '', + styles: [':host { display: block; }'], imports: [ DxIntegrationModule ], providers: [ NestedOptionHost, + DxTemplateHost, { provide: PROPERTY_TOKEN_items, useExisting: DxiChatItemComponent, } ] }) -export class DxiChatItemComponent extends CollectionNestedOption { +export class DxiChatItemComponent extends CollectionNestedOption implements AfterViewInit, + IDxTemplateHost { @ContentChildren(PROPERTY_TOKEN_attachments) set _attachmentsContentChildren(value: QueryList) { this.setChildren('attachments', value); @@ -120,13 +131,61 @@ export class DxiChatItemComponent extends CollectionNestedOption { } @Input() - get type(): string | undefined { + get type(): string | undefined | ButtonType { return this._getOption('type'); } - set type(value: string | undefined) { + set type(value: string | undefined | ButtonType) { this._setOption('type', value); } + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + @Input() + get elementAttr(): Record { + return this._getOption('elementAttr'); + } + set elementAttr(value: Record) { + this._setOption('elementAttr', value); + } + + @Input() + get hint(): string { + return this._getOption('hint'); + } + set hint(value: string) { + this._setOption('hint', value); + } + + @Input() + get icon(): string { + return this._getOption('icon'); + } + set icon(value: string) { + this._setOption('icon', value); + } + + @Input() + get template(): any { + return this._getOption('template'); + } + set template(value: any) { + this._setOption('template', value); + } + + @Input() + get visible(): boolean { + return this._getOption('visible'); + } + set visible(value: boolean) { + this._setOption('visible', value); + } + protected get _optionPath() { return 'items'; @@ -134,10 +193,22 @@ export class DxiChatItemComponent extends CollectionNestedOption { constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, - @Host() optionHost: NestedOptionHost) { + @Host() optionHost: NestedOptionHost, + private renderer: Renderer2, + @Inject(DOCUMENT) private document: any, + @Host() templateHost: DxTemplateHost, + private element: ElementRef) { super(); parentOptionHost.setNestedOption(this); optionHost.setHost(this, this._fullOptionPath.bind(this)); + templateHost.setHost(this); + } + + setTemplate(template: DxTemplateDirective) { + this.template = template; + } + ngAfterViewInit() { + extractTemplate(this, this.element, this.renderer, this.document); } diff --git a/packages/devextreme-angular/src/ui/chat/nested/suggestions-item-dxi.ts b/packages/devextreme-angular/src/ui/chat/nested/suggestions-item-dxi.ts new file mode 100644 index 000000000000..2ba1eb8e4bea --- /dev/null +++ b/packages/devextreme-angular/src/ui/chat/nested/suggestions-item-dxi.ts @@ -0,0 +1,155 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + NgModule, + Host, + ElementRef, + Renderer2, + Inject, + AfterViewInit, + SkipSelf, + Input +} from '@angular/core'; + +import { DOCUMENT } from '@angular/common'; + + +import type { ButtonType } from 'devextreme/common'; + +import { + DxIntegrationModule, + NestedOptionHost, + extractTemplate, + DxTemplateDirective, + IDxTemplateHost, + DxTemplateHost, +} from 'devextreme-angular/core'; +import { CollectionNestedOption } from 'devextreme-angular/core'; + +import { PROPERTY_TOKEN_items } from 'devextreme-angular/core/tokens'; + +@Component({ + selector: 'dxi-chat-suggestions-item', + standalone: true, + template: '', + styles: [':host { display: block; }'], + imports: [ DxIntegrationModule ], + providers: [ + NestedOptionHost, + DxTemplateHost, + { + provide: PROPERTY_TOKEN_items, + useExisting: DxiChatSuggestionsItemComponent, + } + ] +}) +export class DxiChatSuggestionsItemComponent extends CollectionNestedOption implements AfterViewInit, + IDxTemplateHost { + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + @Input() + get elementAttr(): Record { + return this._getOption('elementAttr'); + } + set elementAttr(value: Record) { + this._setOption('elementAttr', value); + } + + @Input() + get hint(): string { + return this._getOption('hint'); + } + set hint(value: string) { + this._setOption('hint', value); + } + + @Input() + get icon(): string { + return this._getOption('icon'); + } + set icon(value: string) { + this._setOption('icon', value); + } + + @Input() + get template(): any { + return this._getOption('template'); + } + set template(value: any) { + this._setOption('template', value); + } + + @Input() + get text(): string { + return this._getOption('text'); + } + set text(value: string) { + this._setOption('text', value); + } + + @Input() + get type(): ButtonType | string { + return this._getOption('type'); + } + set type(value: ButtonType | string) { + this._setOption('type', value); + } + + @Input() + get visible(): boolean { + return this._getOption('visible'); + } + set visible(value: boolean) { + this._setOption('visible', value); + } + + + protected get _optionPath() { + return 'items'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost, + private renderer: Renderer2, + @Inject(DOCUMENT) private document: any, + @Host() templateHost: DxTemplateHost, + private element: ElementRef) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + templateHost.setHost(this); + } + + setTemplate(template: DxTemplateDirective) { + this.template = template; + } + ngAfterViewInit() { + extractTemplate(this, this.element, this.renderer, this.document); + } + + + + ngOnDestroy() { + this._deleteRemovedOptions(this._fullOptionPath()); + } + +} + +@NgModule({ + imports: [ + DxiChatSuggestionsItemComponent + ], + exports: [ + DxiChatSuggestionsItemComponent + ], +}) +export class DxiChatSuggestionsItemModule { } diff --git a/packages/devextreme-angular/src/ui/chat/nested/suggestions.ts b/packages/devextreme-angular/src/ui/chat/nested/suggestions.ts new file mode 100644 index 000000000000..2733af8ffed1 --- /dev/null +++ b/packages/devextreme-angular/src/ui/chat/nested/suggestions.ts @@ -0,0 +1,300 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input, + Output, + EventEmitter, + ContentChildren, + QueryList +} from '@angular/core'; + + + + +import type { dxButtonGroupItem, ContentReadyEvent, DisposingEvent, InitializedEvent, ItemClickEvent, OptionChangedEvent, SelectionChangedEvent } from 'devextreme/ui/button_group'; +import type { SingleMultipleOrNone, ButtonStyle } from 'devextreme/common'; + +import { + DxIntegrationModule, + NestedOptionHost, + CollectionNestedOption, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + +import { + PROPERTY_TOKEN_items, +} from 'devextreme-angular/core/tokens'; + +@Component({ + selector: 'dxo-chat-suggestions', + standalone: true, + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [NestedOptionHost] +}) +export class DxoChatSuggestionsComponent extends NestedOption implements OnDestroy, OnInit { + @ContentChildren(PROPERTY_TOKEN_items) + set _itemsContentChildren(value: QueryList) { + this.setChildren('items', value); + } + + @Input() + get accessKey(): string | undefined { + return this._getOption('accessKey'); + } + set accessKey(value: string | undefined) { + this._setOption('accessKey', value); + } + + @Input() + get activeStateEnabled(): boolean { + return this._getOption('activeStateEnabled'); + } + set activeStateEnabled(value: boolean) { + this._setOption('activeStateEnabled', value); + } + + @Input() + get buttonTemplate(): any { + return this._getOption('buttonTemplate'); + } + set buttonTemplate(value: any) { + this._setOption('buttonTemplate', value); + } + + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + @Input() + get elementAttr(): Record { + return this._getOption('elementAttr'); + } + set elementAttr(value: Record) { + this._setOption('elementAttr', value); + } + + @Input() + get focusStateEnabled(): boolean { + return this._getOption('focusStateEnabled'); + } + set focusStateEnabled(value: boolean) { + this._setOption('focusStateEnabled', value); + } + + @Input() + get height(): number | string | undefined { + return this._getOption('height'); + } + set height(value: number | string | undefined) { + this._setOption('height', value); + } + + @Input() + get hint(): string | undefined { + return this._getOption('hint'); + } + set hint(value: string | undefined) { + this._setOption('hint', value); + } + + @Input() + get hoverStateEnabled(): boolean { + return this._getOption('hoverStateEnabled'); + } + set hoverStateEnabled(value: boolean) { + this._setOption('hoverStateEnabled', value); + } + + @Input() + get items(): Array { + return this._getOption('items'); + } + set items(value: Array) { + this._setOption('items', value); + } + + @Input() + get keyExpr(): Function | string { + return this._getOption('keyExpr'); + } + set keyExpr(value: Function | string) { + this._setOption('keyExpr', value); + } + + @Input() + get onContentReady(): ((e: ContentReadyEvent) => void) { + return this._getOption('onContentReady'); + } + set onContentReady(value: ((e: ContentReadyEvent) => void)) { + this._setOption('onContentReady', value); + } + + @Input() + get onDisposing(): ((e: DisposingEvent) => void) { + return this._getOption('onDisposing'); + } + set onDisposing(value: ((e: DisposingEvent) => void)) { + this._setOption('onDisposing', value); + } + + @Input() + get onInitialized(): ((e: InitializedEvent) => void) { + return this._getOption('onInitialized'); + } + set onInitialized(value: ((e: InitializedEvent) => void)) { + this._setOption('onInitialized', value); + } + + @Input() + get onItemClick(): ((e: ItemClickEvent) => void) { + return this._getOption('onItemClick'); + } + set onItemClick(value: ((e: ItemClickEvent) => void)) { + this._setOption('onItemClick', value); + } + + @Input() + get onOptionChanged(): ((e: OptionChangedEvent) => void) { + return this._getOption('onOptionChanged'); + } + set onOptionChanged(value: ((e: OptionChangedEvent) => void)) { + this._setOption('onOptionChanged', value); + } + + @Input() + get onSelectionChanged(): ((e: SelectionChangedEvent) => void) { + return this._getOption('onSelectionChanged'); + } + set onSelectionChanged(value: ((e: SelectionChangedEvent) => void)) { + this._setOption('onSelectionChanged', value); + } + + @Input() + get rtlEnabled(): boolean { + return this._getOption('rtlEnabled'); + } + set rtlEnabled(value: boolean) { + this._setOption('rtlEnabled', value); + } + + @Input() + get selectedItemKeys(): Array { + return this._getOption('selectedItemKeys'); + } + set selectedItemKeys(value: Array) { + this._setOption('selectedItemKeys', value); + } + + @Input() + get selectedItems(): Array { + return this._getOption('selectedItems'); + } + set selectedItems(value: Array) { + this._setOption('selectedItems', value); + } + + @Input() + get selectionMode(): SingleMultipleOrNone { + return this._getOption('selectionMode'); + } + set selectionMode(value: SingleMultipleOrNone) { + this._setOption('selectionMode', value); + } + + @Input() + get stylingMode(): ButtonStyle { + return this._getOption('stylingMode'); + } + set stylingMode(value: ButtonStyle) { + this._setOption('stylingMode', value); + } + + @Input() + get tabIndex(): number { + return this._getOption('tabIndex'); + } + set tabIndex(value: number) { + this._setOption('tabIndex', value); + } + + @Input() + get visible(): boolean { + return this._getOption('visible'); + } + set visible(value: boolean) { + this._setOption('visible', value); + } + + @Input() + get width(): number | string | undefined { + return this._getOption('width'); + } + set width(value: number | string | undefined) { + this._setOption('width', value); + } + + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() selectedItemKeysChange: EventEmitter>; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() selectedItemsChange: EventEmitter>; + protected get _optionPath() { + return 'suggestions'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + this._createEventEmitters([ + { emit: 'selectedItemKeysChange' }, + { emit: 'selectedItemsChange' } + ]); + + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + imports: [ + DxoChatSuggestionsComponent + ], + exports: [ + DxoChatSuggestionsComponent + ], +}) +export class DxoChatSuggestionsModule { } diff --git a/packages/devextreme-react/src/chat.ts b/packages/devextreme-react/src/chat.ts index e3c1d230ed2f..34e74a4807fb 100644 --- a/packages/devextreme-react/src/chat.ts +++ b/packages/devextreme-react/src/chat.ts @@ -11,7 +11,9 @@ import NestedOption from "./core/nested-option"; import type { Message, AttachmentDownloadClickEvent, DisposingEvent, InitializedEvent, MessageDeletedEvent, MessageDeletingEvent, MessageEditCanceledEvent, MessageEditingStartEvent, MessageEnteredEvent, MessageUpdatedEvent, MessageUpdatingEvent, TypingEndEvent, TypingStartEvent, Attachment as ChatAttachment, User as ChatUser, SendButtonAction, SendButtonClickEvent } from "devextreme/ui/chat"; import type { DisposingEvent as FileUploaderDisposingEvent, InitializedEvent as FileUploaderInitializedEvent, BeforeSendEvent, ContentReadyEvent, DropZoneEnterEvent, DropZoneLeaveEvent, FilesUploadedEvent, OptionChangedEvent, ProgressEvent, UploadAbortedEvent, UploadedEvent, UploadErrorEvent, UploadStartedEvent, ValueChangedEvent, UploadHttpMethod, FileUploadMode } from "devextreme/ui/file_uploader"; import type { DisposingEvent as SpeechToTextDisposingEvent, InitializedEvent as SpeechToTextInitializedEvent, ContentReadyEvent as SpeechToTextContentReadyEvent, OptionChangedEvent as SpeechToTextOptionChangedEvent, CustomSpeechRecognizer as SpeechToTextCustomSpeechRecognizer, EndEvent, ErrorEvent, ResultEvent, StartClickEvent, StopClickEvent, SpeechRecognitionConfig as SpeechToTextSpeechRecognitionConfig } from "devextreme/ui/speech_to_text"; -import type { Format, ValidationStatus, ButtonStyle, ButtonType } from "devextreme/common"; +import type { DisposingEvent as ButtonGroupDisposingEvent, InitializedEvent as ButtonGroupInitializedEvent, ContentReadyEvent as ButtonGroupContentReadyEvent, OptionChangedEvent as ButtonGroupOptionChangedEvent, dxButtonGroupItem, ItemClickEvent, SelectionChangedEvent } from "devextreme/ui/button_group"; +import type { Format, ValidationStatus, ButtonType, template, ButtonStyle, SingleMultipleOrNone } from "devextreme/common"; +import type { CollectionWidgetItem } from "devextreme/ui/collection/ui.collection_widget.base"; import type UploadInfo from "devextreme/file_management/upload_info"; @@ -60,7 +62,7 @@ const Chat = memo( } ), []); - const subscribableOptions = useMemo(() => (["items"]), []); + const subscribableOptions = useMemo(() => (["items","suggestions.selectedItemKeys","suggestions.selectedItems"]), []); const independentEvents = useMemo(() => (["onAttachmentDownloadClick","onDisposing","onInitialized","onMessageDeleted","onMessageDeleting","onMessageEditCanceled","onMessageEditingStart","onMessageEntered","onMessageUpdated","onMessageUpdating","onTypingEnd","onTypingStart"]), []); const defaults = useMemo(() => ({ @@ -69,6 +71,7 @@ const Chat = memo( const expectedChildren = useMemo(() => ({ alert: { optionName: "alerts", isCollectionItem: true }, + chatItem: { optionName: "items", isCollectionItem: true }, dayHeaderFormat: { optionName: "dayHeaderFormat", isCollectionItem: false }, editing: { optionName: "editing", isCollectionItem: false }, fileUploaderOptions: { optionName: "fileUploaderOptions", isCollectionItem: false }, @@ -76,6 +79,7 @@ const Chat = memo( messageTimestampFormat: { optionName: "messageTimestampFormat", isCollectionItem: false }, sendButtonOptions: { optionName: "sendButtonOptions", isCollectionItem: false }, speechToTextOptions: { optionName: "speechToTextOptions", isCollectionItem: false }, + suggestions: { optionName: "suggestions", isCollectionItem: false }, typingUser: { optionName: "typingUsers", isCollectionItem: true }, user: { optionName: "user", isCollectionItem: false } }), []); @@ -131,7 +135,7 @@ const Alert = Object.assign(_compon }); // owners: -// Item +// ChatItem type IAttachmentProps = React.PropsWithChildren<{ name?: string; size?: number; @@ -151,7 +155,7 @@ const Attachment = Object.assign(_comp componentType: "option", }); +// owners: +// Chat +type IChatItemProps = React.PropsWithChildren<{ + alt?: string; + attachments?: Array; + author?: ChatUser; + id?: number | string; + isDeleted?: boolean; + isEdited?: boolean; + src?: string; + text?: string; + timestamp?: Date | number | string; + type?: string | undefined; +}> +const _componentChatItem = (props: IChatItemProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "items", + IsCollectionItem: true, + ExpectedChildren: { + attachment: { optionName: "attachments", isCollectionItem: true }, + author: { optionName: "author", isCollectionItem: false } + }, + }, + }); +}; + +const ChatItem = Object.assign(_componentChatItem, { + componentType: "option", +}); + // owners: // SpeechToTextOptions type ICustomSpeechRecognizerProps = React.PropsWithChildren<{ @@ -320,6 +356,7 @@ const FileUploaderOptions = Object.assign; @@ -330,7 +367,15 @@ type IItemProps = React.PropsWithChildren<{ src?: string; text?: string; timestamp?: Date | number | string; - type?: string | undefined; + type?: string | undefined | ButtonType; + disabled?: boolean; + elementAttr?: Record; + hint?: string; + icon?: string; + template?: ((itemData: CollectionWidgetItem, itemIndex: number, itemElement: any) => string | any) | template; + visible?: boolean; + render?: (...params: any) => React.ReactNode; + component?: React.ComponentType; }> const _componentItem = (props: IItemProps) => { return React.createElement(NestedOption, { @@ -342,6 +387,11 @@ const _componentItem = (props: IItemProps) => { attachment: { optionName: "attachments", isCollectionItem: true }, author: { optionName: "author", isCollectionItem: false } }, + TemplateProps: [{ + tmplOption: "template", + render: "render", + component: "component" + }], }, }); }; @@ -465,6 +515,100 @@ const SpeechToTextOptions = Object.assign string | any) | template; + disabled?: boolean; + elementAttr?: Record; + focusStateEnabled?: boolean; + height?: number | string | undefined; + hint?: string | undefined; + hoverStateEnabled?: boolean; + items?: Array; + keyExpr?: (() => void) | string; + onContentReady?: ((e: ButtonGroupContentReadyEvent) => void); + onDisposing?: ((e: ButtonGroupDisposingEvent) => void); + onInitialized?: ((e: ButtonGroupInitializedEvent) => void); + onItemClick?: ((e: ItemClickEvent) => void); + onOptionChanged?: ((e: ButtonGroupOptionChangedEvent) => void); + onSelectionChanged?: ((e: SelectionChangedEvent) => void); + rtlEnabled?: boolean; + selectedItemKeys?: Array; + selectedItems?: Array; + selectionMode?: SingleMultipleOrNone; + stylingMode?: ButtonStyle; + tabIndex?: number; + visible?: boolean; + width?: number | string | undefined; + defaultSelectedItemKeys?: Array; + onSelectedItemKeysChange?: (value: Array) => void; + defaultSelectedItems?: Array; + onSelectedItemsChange?: (value: Array) => void; + buttonRender?: (...params: any) => React.ReactNode; + buttonComponent?: React.ComponentType; +}> +const _componentSuggestions = (props: ISuggestionsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "suggestions", + DefaultsProps: { + defaultSelectedItemKeys: "selectedItemKeys", + defaultSelectedItems: "selectedItems" + }, + ExpectedChildren: { + item: { optionName: "items", isCollectionItem: true }, + suggestionsItem: { optionName: "items", isCollectionItem: true } + }, + TemplateProps: [{ + tmplOption: "buttonTemplate", + render: "buttonRender", + component: "buttonComponent" + }], + }, + }); +}; + +const Suggestions = Object.assign(_componentSuggestions, { + componentType: "option", +}); + +// owners: +// Suggestions +type ISuggestionsItemProps = React.PropsWithChildren<{ + disabled?: boolean; + elementAttr?: Record; + hint?: string; + icon?: string; + template?: ((itemData: CollectionWidgetItem, itemIndex: number, itemElement: any) => string | any) | template; + text?: string; + type?: ButtonType | string; + visible?: boolean; + render?: (...params: any) => React.ReactNode; + component?: React.ComponentType; +}> +const _componentSuggestionsItem = (props: ISuggestionsItemProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "items", + IsCollectionItem: true, + TemplateProps: [{ + tmplOption: "template", + render: "render", + component: "component" + }], + }, + }); +}; + +const SuggestionsItem = Object.assign(_componentSuggestionsItem, { + componentType: "option", +}); + // owners: // Chat type ITypingUserProps = React.PropsWithChildren<{ @@ -519,6 +663,8 @@ export { IAttachmentProps, Author, IAuthorProps, + ChatItem, + IChatItemProps, CustomSpeechRecognizer, ICustomSpeechRecognizerProps, DayHeaderFormat, @@ -537,6 +683,10 @@ export { ISpeechRecognitionConfigProps, SpeechToTextOptions, ISpeechToTextOptionsProps, + Suggestions, + ISuggestionsProps, + SuggestionsItem, + ISuggestionsItemProps, TypingUser, ITypingUserProps, User, diff --git a/packages/devextreme-vue/src/chat.ts b/packages/devextreme-vue/src/chat.ts index 3475c1be04bf..5ce6628a93c6 100644 --- a/packages/devextreme-vue/src/chat.ts +++ b/packages/devextreme-vue/src/chat.ts @@ -40,8 +40,9 @@ import { import { Format as CommonFormat, ValidationStatus, - ButtonStyle, ButtonType, + ButtonStyle, + SingleMultipleOrNone, } from "devextreme/common"; import { dxFileUploaderOptions, @@ -76,6 +77,16 @@ import { StopClickEvent, SpeechRecognitionConfig, } from "devextreme/ui/speech_to_text"; +import { + dxButtonGroupOptions, + dxButtonGroupItem, + ContentReadyEvent as ButtonGroupContentReadyEvent, + DisposingEvent as ButtonGroupDisposingEvent, + InitializedEvent as ButtonGroupInitializedEvent, + ItemClickEvent, + OptionChangedEvent as ButtonGroupOptionChangedEvent, + SelectionChangedEvent, +} from "devextreme/ui/button_group"; import { prepareConfigurationComponentConfig } from "./core/index"; type AccessibleOptions = Pick>, + suggestions: Object as PropType>, typingUsers: Array as PropType>, user: Object as PropType>, visible: Boolean, @@ -222,6 +235,7 @@ const componentConfig = { "update:showUserName": null, "update:speechToTextEnabled": null, "update:speechToTextOptions": null, + "update:suggestions": null, "update:typingUsers": null, "update:user": null, "update:visible": null, @@ -237,6 +251,7 @@ const componentConfig = { (this as any).$_hasAsyncTemplate = true; (this as any).$_expectedChildren = { alert: { isCollectionItem: true, optionName: "alerts" }, + chatItem: { isCollectionItem: true, optionName: "items" }, dayHeaderFormat: { isCollectionItem: false, optionName: "dayHeaderFormat" }, editing: { isCollectionItem: false, optionName: "editing" }, fileUploaderOptions: { isCollectionItem: false, optionName: "fileUploaderOptions" }, @@ -244,6 +259,7 @@ const componentConfig = { messageTimestampFormat: { isCollectionItem: false, optionName: "messageTimestampFormat" }, sendButtonOptions: { isCollectionItem: false, optionName: "sendButtonOptions" }, speechToTextOptions: { isCollectionItem: false, optionName: "speechToTextOptions" }, + suggestions: { isCollectionItem: false, optionName: "suggestions" }, typingUser: { isCollectionItem: true, optionName: "typingUsers" }, user: { isCollectionItem: false, optionName: "user" } }; @@ -318,6 +334,46 @@ const DxAuthor = defineComponent(DxAuthorConfig); (DxAuthor as any).$_optionName = "author"; +const DxChatItemConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:alt": null, + "update:attachments": null, + "update:author": null, + "update:id": null, + "update:isDeleted": null, + "update:isEdited": null, + "update:src": null, + "update:text": null, + "update:timestamp": null, + "update:type": null, + }, + props: { + alt: String, + attachments: Array as PropType>, + author: Object as PropType>, + id: [Number, String], + isDeleted: Boolean, + isEdited: Boolean, + src: String, + text: String, + timestamp: [Date, Number, String], + type: String + } +}; + +prepareConfigurationComponentConfig(DxChatItemConfig); + +const DxChatItem = defineComponent(DxChatItemConfig); + +(DxChatItem as any).$_optionName = "items"; +(DxChatItem as any).$_isCollectionItem = true; +(DxChatItem as any).$_expectedChildren = { + attachment: { isCollectionItem: true, optionName: "attachments" }, + author: { isCollectionItem: false, optionName: "author" } +}; + const DxCustomSpeechRecognizerConfig = { emits: { "update:isActive": null, @@ -533,25 +589,37 @@ const DxItemConfig = { "update:alt": null, "update:attachments": null, "update:author": null, + "update:disabled": null, + "update:elementAttr": null, + "update:hint": null, + "update:icon": null, "update:id": null, "update:isDeleted": null, "update:isEdited": null, "update:src": null, + "update:template": null, "update:text": null, "update:timestamp": null, "update:type": null, + "update:visible": null, }, props: { alt: String, attachments: Array as PropType>, author: Object as PropType>, + disabled: Boolean, + elementAttr: Object as PropType>, + hint: String, + icon: String, id: [Number, String], isDeleted: Boolean, isEdited: Boolean, src: String, + template: {}, text: String, timestamp: [Date, Number, String], - type: String + type: String as PropType, + visible: Boolean } }; @@ -716,6 +784,107 @@ const DxSpeechToTextOptions = defineComponent(DxSpeechToTextOptionsConfig); speechRecognitionConfig: { isCollectionItem: false, optionName: "speechRecognitionConfig" } }; +const DxSuggestionsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:accessKey": null, + "update:activeStateEnabled": null, + "update:buttonTemplate": null, + "update:disabled": null, + "update:elementAttr": null, + "update:focusStateEnabled": null, + "update:height": null, + "update:hint": null, + "update:hoverStateEnabled": null, + "update:items": null, + "update:keyExpr": null, + "update:onContentReady": null, + "update:onDisposing": null, + "update:onInitialized": null, + "update:onItemClick": null, + "update:onOptionChanged": null, + "update:onSelectionChanged": null, + "update:rtlEnabled": null, + "update:selectedItemKeys": null, + "update:selectedItems": null, + "update:selectionMode": null, + "update:stylingMode": null, + "update:tabIndex": null, + "update:visible": null, + "update:width": null, + }, + props: { + accessKey: String, + activeStateEnabled: Boolean, + buttonTemplate: {}, + disabled: Boolean, + elementAttr: Object as PropType>, + focusStateEnabled: Boolean, + height: [Number, String], + hint: String, + hoverStateEnabled: Boolean, + items: Array as PropType>, + keyExpr: [Function, String] as PropType<((() => void)) | string>, + onContentReady: Function as PropType<((e: ButtonGroupContentReadyEvent) => void)>, + onDisposing: Function as PropType<((e: ButtonGroupDisposingEvent) => void)>, + onInitialized: Function as PropType<((e: ButtonGroupInitializedEvent) => void)>, + onItemClick: Function as PropType<((e: ItemClickEvent) => void)>, + onOptionChanged: Function as PropType<((e: ButtonGroupOptionChangedEvent) => void)>, + onSelectionChanged: Function as PropType<((e: SelectionChangedEvent) => void)>, + rtlEnabled: Boolean, + selectedItemKeys: Array as PropType>, + selectedItems: Array as PropType>, + selectionMode: String as PropType, + stylingMode: String as PropType, + tabIndex: Number, + visible: Boolean, + width: [Number, String] + } +}; + +prepareConfigurationComponentConfig(DxSuggestionsConfig); + +const DxSuggestions = defineComponent(DxSuggestionsConfig); + +(DxSuggestions as any).$_optionName = "suggestions"; +(DxSuggestions as any).$_expectedChildren = { + item: { isCollectionItem: true, optionName: "items" }, + suggestionsItem: { isCollectionItem: true, optionName: "items" } +}; + +const DxSuggestionsItemConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:disabled": null, + "update:elementAttr": null, + "update:hint": null, + "update:icon": null, + "update:template": null, + "update:text": null, + "update:type": null, + "update:visible": null, + }, + props: { + disabled: Boolean, + elementAttr: Object as PropType>, + hint: String, + icon: String, + template: {}, + text: String, + type: String as PropType, + visible: Boolean + } +}; + +prepareConfigurationComponentConfig(DxSuggestionsItemConfig); + +const DxSuggestionsItem = defineComponent(DxSuggestionsItemConfig); + +(DxSuggestionsItem as any).$_optionName = "items"; +(DxSuggestionsItem as any).$_isCollectionItem = true; + const DxTypingUserConfig = { emits: { "update:isActive": null, @@ -769,6 +938,7 @@ export { DxAlert, DxAttachment, DxAuthor, + DxChatItem, DxCustomSpeechRecognizer, DxDayHeaderFormat, DxEditing, @@ -778,6 +948,8 @@ export { DxSendButtonOptions, DxSpeechRecognitionConfig, DxSpeechToTextOptions, + DxSuggestions, + DxSuggestionsItem, DxTypingUser, DxUser }; diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 51da85b9e775..44b09be25f1b 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -11484,6 +11484,10 @@ declare module DevExpress.ui { DevExpress.ui.dxSpeechToText.Properties, 'stylingMode' | 'type' >; + /** + * [descr:dxChatOptions.suggestions] + */ + suggestions?: Omit; /** * [descr:dxChatOptions.typingUsers] */