Skip to content

feat(core/chat): add new chat components#2569

Draft
danielleroux wants to merge 32 commits into
mainfrom
feat/chat-components
Draft

feat(core/chat): add new chat components#2569
danielleroux wants to merge 32 commits into
mainfrom
feat/chat-components

Conversation

@danielleroux
Copy link
Copy Markdown
Collaborator

@danielleroux danielleroux commented May 27, 2026

💡 What is the current behavior?

JIRA: IX-4307

🆕 What is the new behavior?

🏁 Checklist

A pull request can only be merged if all of these conditions are met (where applicable):

  • 🦮 Accessibility (a11y) features were implemented
  • 🗺️ Internationalization (i18n) - no hard coded strings
  • 📲 Responsiveness - components handle viewport changes and content overflow gracefully
  • 📕 Add or update a Storybook story
  • 📄 Documentation was reviewed/updated siemens/ix-docs
  • 🧪 Unit tests were added/updated and pass (pnpm test)
  • 📸 Visual regression tests were added/updated and pass (Guide)
  • 🧐 Static code analysis passes (pnpm lint)
  • 🏗️ Successful compilation (pnpm build, changes pushed)

👨‍💻 Help & support

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 27, 2026

🦋 Changeset detected

Latest commit: b7e605b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@siemens/ix-docs Minor
@siemens/ix Minor
@siemens/ix-aggrid Major
@siemens/ix-angular Minor
@siemens/ix-echarts Major
@siemens/ix-react Minor
@siemens/ix-vue Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify
Copy link
Copy Markdown

netlify Bot commented May 27, 2026

Deploy Preview for ix-storybook ready!

Name Link
🔨 Latest commit b7e605b
🔍 Latest deploy log https://app.netlify.com/projects/ix-storybook/deploys/6a194ae73acf0b0008cfd9c7
😎 Deploy Preview https://deploy-preview-2569--ix-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the new ix-chat component suite for AI prompt entry and conversation layouts, including ix-chat-input, ix-chat-prompt-attachment, ix-chat-user-message, and ix-chat-ai-message. It also includes a fix for ix-chip to ensure interactive close buttons render correctly when the chip is inactive. The review identified several critical issues in the new chat components, including missing lifecycle handling for observers on component re-attachment, incorrect state handling for the submit button during processing, premature form submission during IME composition, a missing key prop in dynamic lists, potential runtime TypeErrors with null/undefined file names, and a regression in the ix-chip pointer-events styling.

I am having trouble creating individual review comments. Click here to see my feedback.

packages/core/src/components/chat-input/chat-input.tsx (229-244)

high

The component does not implement connectedCallback to re-initialize the MutationObserver and ResizeObserver when the component is disconnected and re-attached to the DOM. This causes the observers to remain disconnected and breaks the attachment overflow and follow-up updates on re-attachment. Implementing connectedCallback with a guard flag ensures proper re-initialization.

  private hasDisconnected = false;

  connectedCallback() {
    if (this.hasDisconnected) {
      this.initAttachmentMutationObserver();
      if (this.attachmentsRef.current) {
        this.initAttachmentResizeObserver(this.attachmentsRef.current);
      }
      this.hasDisconnected = false;
    }
  }

  componentDidLoad() {
    this.updateHasFollowUp();
    this.initAttachmentMutationObserver();
    this.scheduleAttachmentOverflowUpdate();
    this.updateTextareaHeight();
  }

  componentDidRender() {
    this.scheduleAttachmentOverflowUpdate();
    this.updateTextareaHeight();
  }

  disconnectedCallback() {
    this.attachmentResizeObserver?.disconnect();
    this.attachmentMutationObserver?.disconnect();
    this.hasDisconnected = true;
  }

packages/core/src/components/chat-input/chat-input.tsx (292-315)

high

When state === 'processing', the submit button acts as a "Stop" button. However, canSubmit() does not check this.state, which means if the input is empty, the "Stop" button is disabled and cannot be clicked. Furthermore, if the input is not empty, clicking it or pressing Enter will submit the prompt again instead of stopping the processing. We should ensure the button is enabled during processing and handles the stop action correctly.

  private canSubmit() {
    return (
      !this.disabled &&
      !this.readonly &&
      (this.state === 'processing' ||
        ((this.value || '').trim().length > 0 &&
          !this.isCharacterLimitReached()))
    );
  }

  private emitIxChangeIfNeeded() {
    if (this.initialValue !== this.value) {
      this.ixChange.emit(this.value);
      this.initialValue = this.value;
    }
  }

  private submitPrompt() {
    if (this.state === 'processing') {
      this.promptSubmit.emit('');
      return;
    }

    if (!this.canSubmit()) {
      return;
    }

    this.emitIxChangeIfNeeded();
    this.promptSubmit.emit(this.value);
  }

packages/core/src/components/chat-input/chat-input.tsx (420-433)

medium

Pressing Enter while composing text in an IME (Input Method Editor) will trigger the keydown event and submit the prompt prematurely. We should check event.isComposing to prevent this.

  private handleKeyDown(event: KeyboardEvent) {
    if (
      event.key !== 'Enter' ||
      event.isComposing ||
      event.shiftKey ||
      event.altKey ||
      event.ctrlKey ||
      event.metaKey ||
      this.insertLineBreakOnEnter
    ) {
      return;
    }

    event.preventDefault();
    this.submitPrompt();
  }

packages/core/src/components/chat-input/chat-input.tsx (784-811)

medium

The dynamic list rendered by this.attachmentOverflowEntries.map is missing a key prop on the outer element. This can cause rendering and reconciliation issues when attachments are dynamically added or removed.

  private renderAttachmentOverflowItems() {
    return this.attachmentOverflowEntries.map((entry) => (
      <div
        key={entry.index}
        class="attachment-overflow-menu-item"
        data-attachment-overflow-generated
        data-attachment-overflow-label={entry.label}
      >
        <ix-dropdown-item
          class="attachment-overflow-generated-item"
          icon={entry.icon}
          ariaLabelButton={entry.label}
          onClick={() => this.handleAttachmentOverflowItemClick(entry.index)}
        >
          {entry.label}
        </ix-dropdown-item>
        {entry.canRemove && (
          <ix-icon-button
            aria-label={entry.removeAriaLabel}
            class="attachment-overflow-remove-button"
            icon={iconCloseSmall}
            size="24"
            variant="subtle-tertiary"
            onClick={(event: MouseEvent) =>
              this.handleAttachmentOverflowRemoveClick(event, entry.index)
            }
          ></ix-icon-button>
        )}
      </div>
    ));
  }

packages/core/src/components/chat-prompt-attachment/chat-prompt-attachment.tsx (75-87)

medium

If this.fileName is null or undefined, calling this.fileName.trim() will throw a runtime TypeError. We should guard against null/undefined values to ensure robust defensive programming.

  private splitFileName() {
    const fileName = (this.fileName || '').trim();
    const extensionStart = fileName.lastIndexOf('.');

    if (extensionStart <= 0 || extensionStart === fileName.length - 1) {
      return { name: fileName, extension: '' };
    }

    return {
      name: fileName.slice(0, extensionStart),
      extension: fileName.slice(extensionStart),
    };
  }

packages/core/src/components/chip/chip.scss (74-76)

medium

Removing pointer-events: none from :host(.inactive) and only applying it to .chip-main allows click events on the chip body to bubble up to the host. This means any click listeners on ix-chip will still be triggered even when the chip is inactive. A cleaner CSS-only solution is to keep pointer-events: none on :host(.inactive) and explicitly set pointer-events: auto on .chip-close.

:host(.inactive) {
  pointer-events: none;

  .chip-close {
    pointer-events: auto;
  }
}

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant