UNPKG

@progress/kendo-angular-conversational-ui

Version:

Kendo UI for Angular Conversational UI components

549 lines (548 loc) 24.9 kB
/**----------------------------------------------------------------------------------------- * 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"> @if (promptOutput) { <kendo-card width="100%"> <kendo-card-body> @if (outputTemplate) { <ng-container [ngTemplateOutlet]="outputTemplate" [ngTemplateOutletContext]="{ $implicit: promptOutput }" > </ng-container> } @if (!outputTemplate) { {{promptOutput.output}} } </kendo-card-body> @if (outputActions && outputActions.length > 0) { <kendo-card-actions> @for (action of outputActions; track action) { @if (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> } @if (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> @if (promptCommands && promptCommands.length > 0) { <button kendoButton #commandMenuButton [attr.title]="messageFor('commandsButtonTitle')" fillMode="flat" icon="menu" [svgIcon]="commandMenuIcon" (click)="onCommandButtonClick($event)" ></button> } @if (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"> @if (promptOutput) { <kendo-card width="100%"> <kendo-card-body> @if (outputTemplate) { <ng-container [ngTemplateOutlet]="outputTemplate" [ngTemplateOutletContext]="{ $implicit: promptOutput }" > </ng-container> } @if (!outputTemplate) { {{promptOutput.output}} } </kendo-card-body> @if (outputActions && outputActions.length > 0) { <kendo-card-actions> @for (action of outputActions; track action) { @if (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> } @if (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> @if (promptCommands && promptCommands.length > 0) { <button kendoButton #commandMenuButton [attr.title]="messageFor('commandsButtonTitle')" fillMode="flat" icon="menu" [svgIcon]="commandMenuIcon" (click)="onCommandButtonClick($event)" ></button> } @if (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'] }] } });