@progress/kendo-angular-conversational-ui
Version:
Kendo UI for Angular Conversational UI components
549 lines (548 loc) • 24.9 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, NgZone, Output, Renderer2, ViewChild, ViewContainerRef, Optional, SkipSelf, inject, TemplateRef } from '@angular/core';
import { NgClass, NgTemplateOutlet } from '@angular/common';
import { Subscription } from 'rxjs';
import { menuIcon, paperPlaneIcon, stopSmIcon } from '@progress/kendo-svg-icons';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { validatePackage } from '@progress/kendo-licensing';
import { KENDO_BUTTONS } from '@progress/kendo-angular-buttons';
import { packageMetadata } from '../package-metadata';
import { TextAreaComponent, KENDO_TEXTAREA } from '@progress/kendo-angular-inputs';
import { ContextMenuComponent, KENDO_CONTEXTMENU } from '@progress/kendo-angular-menu';
import { LocalizedMessagesDirective } from './localization/localized-messages.directive';
import { isDocumentAvailable, isPresent } from '@progress/kendo-angular-common';
import { calculateMeasurement, defaultOutputActions } from './utils';
import { KENDO_CARD } from '@progress/kendo-angular-layout';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
import * as i2 from "@progress/kendo-angular-buttons";
import * as i3 from "@progress/kendo-angular-inputs";
import * as i4 from "@progress/kendo-angular-menu";
import * as i5 from "@progress/kendo-angular-layout";
const TEXTAREA_MAX_ROWS = 5;
const TEXTAREA_INITIAL_ROWS = 1;
/**
* @hidden
*/
export class InlineAIPromptContentComponent {
ngZone;
renderer;
element;
localization;
className = true;
get dirAttr() {
return this.direction;
}
get maxHeightStyle() {
return this.calculateMeasurement(this.maxHeight);
}
get widthStyle() {
return this.calculateMeasurement(this.width);
}
popupElement;
promptValue = "";
placeholder;
promptOutput;
enableSpeechToText = true;
streaming = false;
width = 550;
maxHeight;
appendTo;
defaultOutputActions = defaultOutputActions;
set outputActions(actions) {
this._outputActions = this.mergeWithDefaultActions(actions);
}
get outputActions() {
return this._outputActions;
}
set promptCommands(commands) {
this._promptCommands = commands || [];
this.commandMenuItems = this.transformCommands(commands || []);
}
get promptCommands() {
return this._promptCommands;
}
outputTemplate;
promptRequest = new EventEmitter();
commandExecute = new EventEmitter();
outputActionClick = new EventEmitter();
promptRequestCancel = new EventEmitter();
close = new EventEmitter();
promptValueChange = new EventEmitter();
onEscapeKey(event) {
if (event.key === 'Escape') {
if (this.streaming) {
event.stopPropagation();
this.promptRequestCancel.emit();
}
else {
this.close.emit();
}
}
}
textArea;
contextMenu;
calculateMeasurement = calculateMeasurement;
commandMenuIcon = menuIcon;
sendIcon = paperPlaneIcon;
stopGenerationIcon = stopSmIcon;
isListening = false;
commandMenuItems = [];
messages = {};
maxRows = TEXTAREA_MAX_ROWS;
initialRows = TEXTAREA_INITIAL_ROWS;
_outputActions = this.defaultOutputActions;
_promptCommands = [];
direction;
localizationSubs = new Subscription();
subs = new Subscription();
constructor(ngZone, renderer, element, localization) {
this.ngZone = ngZone;
this.renderer = renderer;
this.element = element;
this.localization = localization;
validatePackage(packageMetadata);
if (!this.localization) {
this.localization = inject(LocalizationService);
}
this.direction = this.localization?.rtl ? 'rtl' : 'ltr';
this.localizationSubs.add(this.localization.changes.subscribe(({ rtl }) => {
this.direction = rtl ? 'rtl' : 'ltr';
}));
}
ngAfterViewInit() {
this.ngZone.runOutsideAngular(() => {
if (!isDocumentAvailable()) {
return;
}
// add a delay to avoid catching the same click event that triggered the component opening
setTimeout(() => {
this.subs.add(this.renderer.listen('document', 'click', (e) => {
this.outsideClickClose(e);
}));
});
});
}
ngOnDestroy() {
this.subs.unsubscribe();
this.localizationSubs.unsubscribe();
this.contextMenu?.hide();
}
focus() {
if (this.textArea) {
this.textArea.focus();
}
}
onActionClick(event) {
const eventArgs = {
action: event,
output: this.promptOutput,
};
this.outputActionClick.emit(eventArgs);
this.handleDefaultActions(event);
}
handleDefaultActions(event) {
switch (event.name) {
case 'copy':
navigator.clipboard.writeText(this.promptOutput.output);
break;
case 'retry':
this.promptRequest.emit({
prompt: this.promptOutput?.prompt,
isRetry: true
});
break;
case 'discard':
this.close.emit();
break;
}
}
handleSpeechResult(event) {
if (event.alternatives && event.alternatives.length > 0) {
if (!isPresent(this.promptValue)) {
this.promptValue = '';
}
this.promptValue += event.alternatives[0].transcript + ' ';
}
}
onClick(action) {
this.commandExecute.next(action);
}
handlePromptValueChange(value) {
this.promptValue = value;
this.promptValueChange.emit(value);
}
handleTextAreaKeydown(event) {
if (event.key === 'Enter' && !event.shiftKey && !this.streaming) {
event.preventDefault();
this.handlePromptRequest();
}
}
onCommandButtonClick(event) {
event.preventDefault();
event.stopPropagation();
if (this.contextMenu) {
this.contextMenu.show(this.popupElement);
}
}
onCommandClick(event) {
// avoid triggering the document click listener to keep the popup open
if (event.originalEvent) {
event.originalEvent.stopPropagation();
event.originalEvent.preventDefault();
}
const eventArgs = {
...event.item.originalCommand
};
this.commandExecute.emit(eventArgs);
}
messageFor(text) {
if (this.messages?.[text]) {
return this.messages[text];
}
return this.localization?.get(text);
}
handlePromptRequest() {
if (this.streaming) {
this.promptRequestCancel.emit();
return;
}
if (!this.promptValue) {
return;
}
const eventArgs = {
prompt: this.promptValue
};
this.promptRequest.emit(eventArgs);
}
mergeWithDefaultActions(userActions) {
if (!userActions || userActions.length === 0) {
return [];
}
return userActions.map(userAction => {
const defaultAction = defaultOutputActions.find(action => action.name === userAction?.name);
if (defaultAction) {
return { ...defaultAction, ...userAction };
}
return userAction;
});
}
transformCommands = (commands) => commands.map(command => ({
text: command.text,
icon: command.icon,
svgIcon: command.svgIcon,
disabled: command.disabled,
originalCommand: command,
items: command.children ? this.transformCommands(command.children) : undefined
}));
outsideClickClose(e) {
if (!this.element.nativeElement.contains(e.target)) {
this.ngZone.run(() => {
this.close.emit();
});
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InlineAIPromptContentComponent, deps: [{ token: i0.NgZone }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i1.LocalizationService, optional: true, skipSelf: true }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: InlineAIPromptContentComponent, isStandalone: true, selector: "kendo-inlineaiprompt-content", inputs: { popupElement: "popupElement", promptValue: "promptValue", placeholder: "placeholder", promptOutput: "promptOutput", enableSpeechToText: "enableSpeechToText", streaming: "streaming", width: "width", maxHeight: "maxHeight", appendTo: "appendTo", outputActions: "outputActions", promptCommands: "promptCommands", outputTemplate: "outputTemplate" }, outputs: { promptRequest: "promptRequest", commandExecute: "commandExecute", outputActionClick: "outputActionClick", promptRequestCancel: "promptRequestCancel", close: "close", promptValueChange: "promptValueChange" }, host: { listeners: { "keydown": "onEscapeKey($event)" }, properties: { "class.k-prompt": "this.className", "attr.dir": "this.dirAttr", "style.max-height": "this.maxHeightStyle", "style.width": "this.widthStyle" } }, providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.inlineaiprompt',
},
], viewQueries: [{ propertyName: "textArea", first: true, predicate: TextAreaComponent, descendants: true }, { propertyName: "contextMenu", first: true, predicate: ["kendoContextMenu"], descendants: true }], exportAs: ["kendoInlineAIPromptContent"], ngImport: i0, template: `
<ng-container kendoInlineAIPromptLocalizedMessages
i18n-commandsButtonTitle="kendo.inlineaiprompt.commandsButtonTitle|Sets the Commands button title."
commandsButtonTitle="Command Menu"
i18n-generateButtonTitle="kendo.inlineaiprompt.generateButtonTitle|Sets the Generate button title."
generateButtonTitle="Generate"
i18n-speechToTextButtonTitle="kendo.inlineaiprompt.speechToTextButtonTitle|Sets the Speech to Text button title."
speechToTextButtonTitle="Speech to Text"
>
</ng-container>
<div class="k-prompt-content">
<div class="k-prompt-view">
(promptOutput) {
<kendo-card width="100%">
<kendo-card-body>
(outputTemplate) {
<ng-container
[ngTemplateOutlet]="outputTemplate"
[ngTemplateOutletContext]="{ $implicit: promptOutput }"
>
</ng-container>
}
(!outputTemplate) {
{{promptOutput.output}}
}
</kendo-card-body>
(outputActions && outputActions.length > 0) {
<kendo-card-actions>
(action of outputActions; track action) {
(action.type === 'button') {
<button kendoButton
[attr.title]="action?.title"
[fillMode]="action?.fillMode"
[themeColor]="action?.themeColor"
[rounded]="action?.rounded"
[icon]="action?.icon"
[svgIcon]="action?.svgIcon"
(click)="onActionClick(action)"
>{{action?.text}}</button>
}
(action.type === 'spacer') {
<div class="k-spacer"></div>
}
}
</kendo-card-actions>
}
</kendo-card>
}
<kendo-textarea
[value]="promptValue ? promptValue : null"
(valueChange)="handlePromptValueChange($event)"
[rows]="initialRows"
resizable="auto"
flow="horizontal"
[placeholder]="placeholder"
[showPrefixSeparator]="true"
[selectOnFocus]="true"
[maxResizableRows]="maxRows"
(keydown)="handleTextAreaKeydown($event)"
>
<kendo-textarea-prefix>
(promptCommands && promptCommands.length > 0) {
<button
kendoButton
#commandMenuButton
[attr.title]="messageFor('commandsButtonTitle')"
fillMode="flat"
icon="menu"
[svgIcon]="commandMenuIcon"
(click)="onCommandButtonClick($event)"
></button>
}
(enableSpeechToText) {
<button
kendoSpeechToTextButton
[attr.title]="messageFor('speechToTextButtonTitle')"
fillMode="flat"
(result)="handleSpeechResult($event)"
(start)="isListening = true"
(end)="isListening = false"
></button>
}
</kendo-textarea-prefix>
<kendo-textarea-suffix>
<button
kendoButton
[attr.title]="messageFor('generateButtonTitle')"
fillMode="flat"
class="k-prompt-send"
[ngClass]="{ 'k-generating': streaming, 'k-active': streaming }"
(click)="handlePromptRequest()"
[disabled]="!streaming && (!promptValue?.trim() || isListening)"
[svgIcon]="streaming ? stopGenerationIcon : sendIcon"
[icon]="streaming ? 'stop-sm' : 'paper-plane'"
></button>
</kendo-textarea-suffix>
</kendo-textarea>
</div>
</div>
<kendo-contextmenu
#kendoContextMenu
[alignToAnchor]="true"
[items]="commandMenuItems"
[appendTo]="appendTo"
class="k-hidden"
(select)="onCommandClick($event)">
</kendo-contextmenu>
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: LocalizedMessagesDirective, selector: "[kendoInlineAIPromptLocalizedMessages]" }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "component", type: i2.SpeechToTextButtonComponent, selector: "button[kendoSpeechToTextButton]", inputs: ["disabled", "size", "rounded", "fillMode", "themeColor", "integrationMode", "lang", "continuous", "interimResults", "maxAlternatives"], outputs: ["start", "end", "result", "error", "click"], exportAs: ["kendoSpeechToTextButton"] }, { kind: "component", type: i3.TextAreaComponent, selector: "kendo-textarea", inputs: ["focusableId", "flow", "inputAttributes", "adornmentsOrientation", "rows", "cols", "maxlength", "maxResizableRows", "tabindex", "tabIndex", "resizable", "size", "rounded", "fillMode", "showPrefixSeparator", "showSuffixSeparator"], outputs: ["focus", "blur", "valueChange"], exportAs: ["kendoTextArea"] }, { kind: "component", type: i3.TextAreaPrefixComponent, selector: "kendo-textarea-prefix", inputs: ["flow", "orientation"], exportAs: ["kendoTextAreaPrefix"] }, { kind: "component", type: i3.TextAreaSuffixComponent, selector: "kendo-textarea-suffix", inputs: ["flow", "orientation"], exportAs: ["kendoTextAreaSuffix"] }, { kind: "component", type: i4.ContextMenuComponent, selector: "kendo-contextmenu", inputs: ["showOn", "target", "filter", "alignToAnchor", "vertical", "popupAnimate", "popupAlign", "anchorAlign", "collision", "appendTo", "ariaLabel"], outputs: ["popupOpen", "popupClose", "select", "open", "close"], exportAs: ["kendoContextMenu"] }, { kind: "component", type: i5.CardComponent, selector: "kendo-card", inputs: ["orientation", "width"] }, { kind: "component", type: i5.CardActionsComponent, selector: "kendo-card-actions", inputs: ["orientation", "layout", "actions"], outputs: ["action"] }, { kind: "component", type: i5.CardBodyComponent, selector: "kendo-card-body" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InlineAIPromptContentComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoInlineAIPromptContent',
selector: 'kendo-inlineaiprompt-content',
providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.inlineaiprompt',
},
],
template: `
<ng-container kendoInlineAIPromptLocalizedMessages
i18n-commandsButtonTitle="kendo.inlineaiprompt.commandsButtonTitle|Sets the Commands button title."
commandsButtonTitle="Command Menu"
i18n-generateButtonTitle="kendo.inlineaiprompt.generateButtonTitle|Sets the Generate button title."
generateButtonTitle="Generate"
i18n-speechToTextButtonTitle="kendo.inlineaiprompt.speechToTextButtonTitle|Sets the Speech to Text button title."
speechToTextButtonTitle="Speech to Text"
>
</ng-container>
<div class="k-prompt-content">
<div class="k-prompt-view">
(promptOutput) {
<kendo-card width="100%">
<kendo-card-body>
(outputTemplate) {
<ng-container
[ngTemplateOutlet]="outputTemplate"
[ngTemplateOutletContext]="{ $implicit: promptOutput }"
>
</ng-container>
}
(!outputTemplate) {
{{promptOutput.output}}
}
</kendo-card-body>
(outputActions && outputActions.length > 0) {
<kendo-card-actions>
(action of outputActions; track action) {
(action.type === 'button') {
<button kendoButton
[attr.title]="action?.title"
[fillMode]="action?.fillMode"
[themeColor]="action?.themeColor"
[rounded]="action?.rounded"
[icon]="action?.icon"
[svgIcon]="action?.svgIcon"
(click)="onActionClick(action)"
>{{action?.text}}</button>
}
(action.type === 'spacer') {
<div class="k-spacer"></div>
}
}
</kendo-card-actions>
}
</kendo-card>
}
<kendo-textarea
[value]="promptValue ? promptValue : null"
(valueChange)="handlePromptValueChange($event)"
[rows]="initialRows"
resizable="auto"
flow="horizontal"
[placeholder]="placeholder"
[showPrefixSeparator]="true"
[selectOnFocus]="true"
[maxResizableRows]="maxRows"
(keydown)="handleTextAreaKeydown($event)"
>
<kendo-textarea-prefix>
(promptCommands && promptCommands.length > 0) {
<button
kendoButton
#commandMenuButton
[attr.title]="messageFor('commandsButtonTitle')"
fillMode="flat"
icon="menu"
[svgIcon]="commandMenuIcon"
(click)="onCommandButtonClick($event)"
></button>
}
(enableSpeechToText) {
<button
kendoSpeechToTextButton
[attr.title]="messageFor('speechToTextButtonTitle')"
fillMode="flat"
(result)="handleSpeechResult($event)"
(start)="isListening = true"
(end)="isListening = false"
></button>
}
</kendo-textarea-prefix>
<kendo-textarea-suffix>
<button
kendoButton
[attr.title]="messageFor('generateButtonTitle')"
fillMode="flat"
class="k-prompt-send"
[ngClass]="{ 'k-generating': streaming, 'k-active': streaming }"
(click)="handlePromptRequest()"
[disabled]="!streaming && (!promptValue?.trim() || isListening)"
[svgIcon]="streaming ? stopGenerationIcon : sendIcon"
[icon]="streaming ? 'stop-sm' : 'paper-plane'"
></button>
</kendo-textarea-suffix>
</kendo-textarea>
</div>
</div>
<kendo-contextmenu
#kendoContextMenu
[alignToAnchor]="true"
[items]="commandMenuItems"
[appendTo]="appendTo"
class="k-hidden"
(select)="onCommandClick($event)">
</kendo-contextmenu>
`,
standalone: true,
imports: [NgClass, NgTemplateOutlet, LocalizedMessagesDirective, KENDO_BUTTONS, KENDO_TEXTAREA, KENDO_CONTEXTMENU, KENDO_CARD],
}]
}], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i1.LocalizationService, decorators: [{
type: Optional
}, {
type: SkipSelf
}] }], propDecorators: { className: [{
type: HostBinding,
args: ['class.k-prompt']
}], dirAttr: [{
type: HostBinding,
args: ['attr.dir']
}], maxHeightStyle: [{
type: HostBinding,
args: ['style.max-height']
}], widthStyle: [{
type: HostBinding,
args: ['style.width']
}], popupElement: [{
type: Input
}], promptValue: [{
type: Input
}], placeholder: [{
type: Input
}], promptOutput: [{
type: Input
}], enableSpeechToText: [{
type: Input
}], streaming: [{
type: Input
}], width: [{
type: Input
}], maxHeight: [{
type: Input
}], appendTo: [{
type: Input
}], outputActions: [{
type: Input
}], promptCommands: [{
type: Input
}], outputTemplate: [{
type: Input
}], promptRequest: [{
type: Output
}], commandExecute: [{
type: Output
}], outputActionClick: [{
type: Output
}], promptRequestCancel: [{
type: Output
}], close: [{
type: Output
}], promptValueChange: [{
type: Output
}], onEscapeKey: [{
type: HostListener,
args: ['keydown', ['$event']]
}], textArea: [{
type: ViewChild,
args: [TextAreaComponent]
}], contextMenu: [{
type: ViewChild,
args: ['kendoContextMenu']
}] } });