Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions api-goldens/element-ng/chat-messages/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,32 @@

```ts

import { AfterViewInit } from '@angular/core';
import * as _angular_core from '@angular/core';
import { ElementRef } from '@angular/core';
import { FileUploadError } from '@siemens/element-ng/file-uploader';
import * as i1 from '@siemens/element-ng/resize-observer';
import { MenuItem } from '@siemens/element-ng/menu';
import * as _siemens_element_translate_ng_translate_types from '@siemens/element-translate-ng/translate-types';
import { SiModalService } from '@siemens/element-ng/modal';
import { TemplateRef } from '@angular/core';
import { TranslatableString } from '@siemens/element-translate-ng/translate-types';
import { TranslatableString as TranslatableString_2 } from '@siemens/element-translate-ng/translate';
import { UploadFile } from '@siemens/element-ng/file-uploader';

// @public
export interface Attachment {
name: string;
previewTemplate?: TemplateRef<any> | (() => TemplateRef<any>);
}

// @public (undocumented)
export interface ChatInputAttachment extends Attachment {
file: File;
size: number;
type: string;
}

// @public
export interface MessageAction {
action: (actionParam: any, source: this) => void;
Expand Down Expand Up @@ -66,6 +77,93 @@ export class SiAttachmentListComponent {
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SiAttachmentListComponent, never>;
}

// @public (undocumented)
export class SiChatInputComponent implements AfterViewInit {
readonly accept: _angular_core.InputSignal<string | undefined>;
readonly actionParam: _angular_core.InputSignal<any>;
readonly actions: _angular_core.InputSignal<MessageAction[]>;
// (undocumented)
protected adjustTextareaHeight(event: Event): void;
readonly allowAttachments: _angular_core.InputSignal<boolean>;
readonly attachFileLabel: _angular_core.InputSignal<TranslatableString_2>;
// (undocumented)
protected get attachmentList(): Attachment[];
readonly attachments: _angular_core.ModelSignal<ChatInputAttachment[]>;
readonly autoFocus: _angular_core.InputSignalWithTransform<boolean, unknown>;
// (undocumented)
protected readonly buttonDisabled: _angular_core.Signal<boolean>;
// (undocumented)
protected readonly buttonIcon: _angular_core.Signal<string>;
// (undocumented)
protected readonly buttonLabel: _angular_core.Signal<TranslatableString_2>;
// (undocumented)
protected readonly canSend: _angular_core.Signal<boolean>;
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
readonly disclaimer: _angular_core.InputSignal<TranslatableString_2 | undefined>;
readonly fileError: _angular_core.OutputEmitterRef<FileUploadError>;
focus(): void;
// (undocumented)
protected readonly hasActions: _angular_core.Signal<boolean>;
// (undocumented)
protected readonly hasAttachments: _angular_core.Signal<boolean>;
// (undocumented)
protected readonly hasContent: _angular_core.Signal<boolean>;
// (undocumented)
protected readonly hasSecondaryActions: _angular_core.Signal<boolean>;
// (undocumented)
protected readonly id: string;
readonly interrupt: _angular_core.OutputEmitterRef<void>;
readonly interruptButtonLabel: _angular_core.InputSignal<TranslatableString_2>;
readonly interruptible: _angular_core.InputSignalWithTransform<boolean, unknown>;
readonly label: _angular_core.InputSignal<string>;
readonly maxFileSize: _angular_core.InputSignal<number>;
readonly maxLength: _angular_core.InputSignal<number | undefined>;
// (undocumented)
ngAfterViewInit(): void;
// (undocumented)
protected onButtonClick(): void;
// (undocumented)
protected onContainerClick(event: Event): void;
// (undocumented)
protected onFileError(error: FileUploadError): void;
// (undocumented)
protected onFilesAdded(uploadFiles: UploadFile[]): void;
// (undocumented)
protected onInputChange(value: string): void;
// (undocumented)
protected onKeyDown(event: KeyboardEvent): void;
// (undocumented)
protected onSend(): void;
readonly placeholder: _angular_core.InputSignal<TranslatableString_2>;
// (undocumented)
protected removeAttachment(attachment: Attachment): void;
readonly removeAttachmentLabel: _angular_core.InputSignal<TranslatableString_2>;
readonly secondaryActions: _angular_core.InputSignal<MenuItem[]>;
readonly secondaryActionsLabel: _angular_core.InputSignal<TranslatableString_2>;
readonly send: _angular_core.OutputEmitterRef<{
content: string;
attachments: ChatInputAttachment[];
}>;
readonly sendButtonIcon: _angular_core.InputSignal<string>;
readonly sendButtonLabel: _angular_core.InputSignal<TranslatableString_2>;
readonly sending: _angular_core.InputSignalWithTransform<boolean, unknown>;
// (undocumented)
protected readonly showInterruptButton: _angular_core.Signal<boolean>;
readonly value: _angular_core.ModelSignal<string>;
// (undocumented)
static ɵcmp: _angular_core.ɵɵComponentDeclaration<SiChatInputComponent, "si-chat-input", never, { "value": { "alias": "value"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "sending": { "alias": "sending"; "required": false; "isSignal": true; }; "interruptible": { "alias": "interruptible"; "required": false; "isSignal": true; }; "maxLength": { "alias": "maxLength"; "required": false; "isSignal": true; }; "disclaimer": { "alias": "disclaimer"; "required": false; "isSignal": true; }; "actions": { "alias": "actions"; "required": false; "isSignal": true; }; "secondaryActions": { "alias": "secondaryActions"; "required": false; "isSignal": true; }; "allowAttachments": { "alias": "allowAttachments"; "required": false; "isSignal": true; }; "accept": { "alias": "accept"; "required": false; "isSignal": true; }; "maxFileSize": { "alias": "maxFileSize"; "required": false; "isSignal": true; }; "attachments": { "alias": "attachments"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "actionParam": { "alias": "actionParam"; "required": false; "isSignal": true; }; "sendButtonLabel": { "alias": "sendButtonLabel"; "required": false; "isSignal": true; }; "sendButtonIcon": { "alias": "sendButtonIcon"; "required": false; "isSignal": true; }; "interruptButtonLabel": { "alias": "interruptButtonLabel"; "required": false; "isSignal": true; }; "autoFocus": { "alias": "autoFocus"; "required": false; "isSignal": true; }; "attachFileLabel": { "alias": "attachFileLabel"; "required": false; "isSignal": true; }; "removeAttachmentLabel": { "alias": "removeAttachmentLabel"; "required": false; "isSignal": true; }; "secondaryActionsLabel": { "alias": "secondaryActionsLabel"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "attachments": "attachmentsChange"; "send": "send"; "interrupt": "interrupt"; "fileError": "fileError"; }, never, ["*", "[siChatInputDisclaimer]"], true, never>;
// (undocumented)
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SiChatInputComponent, never>;
}

// @public
export class SiChatInputDisclaimerDirective {
// (undocumented)
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SiChatInputDisclaimerDirective, "[siChatInputDisclaimer]", never, {}, {}, never, never, true, never>;
// (undocumented)
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SiChatInputDisclaimerDirective, never>;
}

// @public
export class SiChatMessageActionDirective {
// (undocumented)
Expand Down
12 changes: 12 additions & 0 deletions api-goldens/element-ng/translate/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ export interface SiTranslatableKeys {
// (undocumented)
'SI_CHANGE_PASSWORD.PASSWORD_POLICY'?: string;
// (undocumented)
'SI_CHAT_INPUT.ATTACH_FILE'?: string;
// (undocumented)
'SI_CHAT_INPUT.INTERRUPT'?: string;
// (undocumented)
'SI_CHAT_INPUT.LABEL'?: string;
// (undocumented)
'SI_CHAT_INPUT.PLACEHOLDER'?: string;
// (undocumented)
'SI_CHAT_INPUT.SECONDARY_ACTIONS'?: string;
// (undocumented)
'SI_CHAT_INPUT.SEND'?: string;
// (undocumented)
'SI_COLUMN_SELECTION_DIALOG.CANCEL'?: string;
// (undocumented)
'SI_COLUMN_SELECTION_DIALOG.HIDDEN'?: string;
Expand Down
6 changes: 5 additions & 1 deletion docs/components/chat-messages/chat-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ If multiple attachments are added, they wrap and stack within the input field to

## Code ---

Angular component is coming soon.
<si-docs-component example="si-chat-messages/si-chat-input"></si-docs-component>

<si-docs-api component="SiChatInputComponent"></si-docs-api>

<si-docs-types></si-docs-types>
1 change: 1 addition & 0 deletions playwright/e2e/element-examples/static.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,4 @@ test('si-chat-messages/si-ai-message', ({ si }) => si.static());
test('si-chat-messages/si-user-message', ({ si }) => si.static());
test('si-chat-messages/si-chat-message', ({ si }) => si.static());
test('si-chat-messages/si-attachment-list', ({ si }) => si.static());
test('si-chat-messages/si-chat-input', ({ si }) => si.static());
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
- group:
- text: project-spec.pdf
- button "Remove attachment project-spec.pdf"
- group:
- text: mockup.png
- button "Remove attachment mockup.png"
- textbox "Chat message input":
- /placeholder: Enter a command, question or topic...
- button "Attach files"
- button "Take photo"
- button "Text formatting"
- button "More actions"
- button "Insert emoji"
- button "Send Message"
- text: The content is AI generated. Always verify the information for accuracy.
2 changes: 2 additions & 0 deletions projects/element-ng/chat-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/
export * from './si-ai-message.component';
export * from './si-attachment-list.component';
export * from './si-chat-input.component';
export * from './si-chat-input-disclaimer.directive';
export * from './si-chat-message-action.directive';
export * from './si-chat-message.component';
export * from './si-user-message.component';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) Siemens 2016 - 2025
* SPDX-License-Identifier: MIT
*/
import { Directive } from '@angular/core';

/**
* Directive to mark content as chat input disclaimer.
* Apply this directive to content that should be slotted into the disclaimer area.
*
* @example
* ```html
* <si-chat-input>
* <div siChatInputDisclaimer>
* Custom disclaimer content
* </div>
* </si-chat-input>
* ```
*/
@Directive({
selector: '[siChatInputDisclaimer]'
})
export class SiChatInputDisclaimerDirective {}
108 changes: 108 additions & 0 deletions projects/element-ng/chat-messages/si-chat-input.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<div class="input-wrapper border rounded-3 bg-body" (click)="onContainerClick($event)">
@if (hasAttachments()) {
<div class="p-4 pb-0">
<si-attachment-list
[attachments]="attachmentList"
[removeLabel]="removeAttachmentLabel()"
[removable]="true"
(remove)="removeAttachment($event)"
/>
</div>
}

<div class="p-4 pe-2 pb-0 si-body-2">
<label class="form-label d-none" [for]="id">{{ label() | translate }}</label>
<textarea
#textInput
class="chat-textarea w-100 border-0 p-2"
rows="1"
[id]="id"
[placeholder]="placeholder() | translate"
[disabled]="disabled()"
[maxlength]="maxLength() || null"
[(ngModel)]="value"
(keydown)="onKeyDown($event)"
(input)="adjustTextareaHeight($event)"
></textarea>
</div>

<div class="d-flex align-items-center justify-content-between px-4 ps-5 pb-2">
<div class="d-flex align-items-center gap-4">
@if (allowAttachments()) {
<input
#fileInput
type="file"
class="d-none"
siFileUpload
[accept]="accept()"
[maxFileSize]="maxFileSize()"
[multiple]="true"
(validFiles)="onFilesAdded($event)"
(fileError)="onFileError($event)"
/>

<button
type="button"
class="btn btn-ghost btn-circle btn-sm"
[attr.aria-label]="attachFileLabel() | translate"
[disabled]="disabled()"
(click)="fileInput.click()"
>
<si-icon icon="element-attachment" />
</button>
}

@if (hasActions() || hasSecondaryActions()) {
<div class="d-flex gap-4 ai-message-actions" siChatMessageAction>
@for (action of actions(); track $index) {
<button
type="button"
class="btn btn-ghost btn-circle btn-sm"
[disabled]="action.disabled"
[attr.aria-label]="action.label | translate"
(click)="action.action(actionParam(), action)"
>
<si-icon [icon]="action.icon" />
</button>
}

@if (secondaryActions().length > 0) {
<button
type="button"
class="btn btn-ghost btn-circle btn-sm"
[cdkMenuTriggerFor]="secondaryActionsMenu"
[attr.aria-label]="secondaryActionsLabel() | translate"
[attr.title]="secondaryActionsLabel() | translate"
>
<si-icon icon="element-optionsVertical" />
</button>

<ng-template #secondaryActionsMenu>
<si-menu-factory [items]="secondaryActions()" [actionParam]="actionParam()" />
</ng-template>
}
</div>
}
<div #projected class="d-flex flex-wrap align-items-start gap-4">
<ng-content />
</div>
</div>

<button
type="button"
class="btn btn-ghost btn-circle btn-sm"
[disabled]="buttonDisabled()"
[attr.aria-label]="buttonLabel() | translate"
(click)="onButtonClick()"
>
<si-icon class="text-primary" [icon]="buttonIcon()" />
</button>
</div>
</div>

<div class="disclaimer-wrapper text-center mt-4 px-3">
@if (disclaimer()) {
<span class="si-caption text-secondary d-block">{{ disclaimer() | translate }}</span>
}
<ng-content select="[siChatInputDisclaimer]" />
</div>
45 changes: 45 additions & 0 deletions projects/element-ng/chat-messages/si-chat-input.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@use 'sass:map';
@use '@siemens/element-theme/src/styles/variables';

:host {
max-inline-size: 720px;
}

.input-wrapper {
border-color: variables.$element-ui-4;
background-color: variables.$element-base-input-experimental;
}

.chat-textarea {
// Prevent layout shift on first input
min-block-size: calc(1.5em * variables.$si-line-height-body);
font-family: inherit;
outline: none;
resize: none;
// stylelint-disable-next-line declaration-no-important
background-color: transparent !important;

&::placeholder {
color: variables.$element-text-secondary;
}

&:disabled {
// stylelint-disable-next-line declaration-no-important
background-color: transparent !important;
color: variables.$element-text-disabled;
cursor: not-allowed;

&::placeholder {
color: variables.$element-text-disabled;
}
}
}

.input-wrapper:has(.chat-textarea:focus-visible) {
@include variables.make-outline-focus();
border-color: variables.$element-ui-1;
}

.disclaimer-wrapper:empty {
display: none;
}
Loading
Loading