From 241690c842e9127ec993960f461d225c66ec923a Mon Sep 17 00:00:00 2001 From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:16:16 -0600 Subject: [PATCH 1/4] add welcome message --- .../chat/message/welcome/ng-package.json | 6 + .../chat/message/welcome/public-api.ts | 2 + .../spright-chat-message-welcome.directive.ts | 32 ++++ .../spright-chat-message-welcome.module.ts | 16 ++ ...ght-chat-message-welcome.directive.spec.ts | 142 ++++++++++++++++++ .../SprightChatMessageWelcome.razor | 7 + .../SprightChatMessageWelcome.razor.cs | 30 ++++ .../SprightChatMessageWelcomeTests.cs | 52 +++++++ .../src/chat/message/welcome/index.ts | 4 + .../spright-components/src/all-components.ts | 1 + .../src/chat/message/welcome/index.ts | 44 ++++++ .../src/chat/message/welcome/styles.ts | 70 +++++++++ .../src/chat/message/welcome/template.ts | 27 ++++ .../tests/chat-message-welcome.spec.ts | 120 +++++++++++++++ .../chat/conversation/chat-conversation.mdx | 11 ++ .../conversation/chat-conversation.stories.ts | 15 ++ .../message/chat-message-matrix.stories.ts | 13 ++ .../chat/message/chat-message.stories.ts | 72 +++++++++ 18 files changed, 664 insertions(+) create mode 100644 packages/angular-workspace/spright-angular/chat/message/welcome/ng-package.json create mode 100644 packages/angular-workspace/spright-angular/chat/message/welcome/public-api.ts create mode 100644 packages/angular-workspace/spright-angular/chat/message/welcome/spright-chat-message-welcome.directive.ts create mode 100644 packages/angular-workspace/spright-angular/chat/message/welcome/spright-chat-message-welcome.module.ts create mode 100644 packages/angular-workspace/spright-angular/chat/message/welcome/tests/spright-chat-message-welcome.directive.spec.ts create mode 100644 packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor create mode 100644 packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs create mode 100644 packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs create mode 100644 packages/react-workspace/spright-react/src/chat/message/welcome/index.ts create mode 100644 packages/spright-components/src/chat/message/welcome/index.ts create mode 100644 packages/spright-components/src/chat/message/welcome/styles.ts create mode 100644 packages/spright-components/src/chat/message/welcome/template.ts create mode 100644 packages/spright-components/src/chat/message/welcome/tests/chat-message-welcome.spec.ts diff --git a/packages/angular-workspace/spright-angular/chat/message/welcome/ng-package.json b/packages/angular-workspace/spright-angular/chat/message/welcome/ng-package.json new file mode 100644 index 0000000000..e5440110fb --- /dev/null +++ b/packages/angular-workspace/spright-angular/chat/message/welcome/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} \ No newline at end of file diff --git a/packages/angular-workspace/spright-angular/chat/message/welcome/public-api.ts b/packages/angular-workspace/spright-angular/chat/message/welcome/public-api.ts new file mode 100644 index 0000000000..6419b230cf --- /dev/null +++ b/packages/angular-workspace/spright-angular/chat/message/welcome/public-api.ts @@ -0,0 +1,2 @@ +export * from './spright-chat-message-welcome.directive'; +export * from './spright-chat-message-welcome.module'; \ No newline at end of file diff --git a/packages/angular-workspace/spright-angular/chat/message/welcome/spright-chat-message-welcome.directive.ts b/packages/angular-workspace/spright-angular/chat/message/welcome/spright-chat-message-welcome.directive.ts new file mode 100644 index 0000000000..056ea09f00 --- /dev/null +++ b/packages/angular-workspace/spright-angular/chat/message/welcome/spright-chat-message-welcome.directive.ts @@ -0,0 +1,32 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import { type ChatMessageWelcome, chatMessageWelcomeTag } from '@ni/spright-components/dist/esm/chat/message/welcome'; + +export type { ChatMessageWelcome }; +export { chatMessageWelcomeTag }; + +/** + * Directive to provide Angular integration for the chat welcome message. + */ +@Directive({ + selector: 'spright-chat-message-welcome', + standalone: false +}) +export class SprightChatMessageWelcomeDirective { + public get title(): string | undefined { + return this.elementRef.nativeElement.welcomeTitle; + } + + @Input() public set title(value: string | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'welcomeTitle', value); + } + + public get subtitle(): string | undefined { + return this.elementRef.nativeElement.subtitle; + } + + @Input() public set subtitle(value: string | undefined) { + this.renderer.setProperty(this.elementRef.nativeElement, 'subtitle', value); + } + + public constructor(private readonly renderer: Renderer2, private readonly elementRef: ElementRef) {} +} \ No newline at end of file diff --git a/packages/angular-workspace/spright-angular/chat/message/welcome/spright-chat-message-welcome.module.ts b/packages/angular-workspace/spright-angular/chat/message/welcome/spright-chat-message-welcome.module.ts new file mode 100644 index 0000000000..fcec362d42 --- /dev/null +++ b/packages/angular-workspace/spright-angular/chat/message/welcome/spright-chat-message-welcome.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SprightChatMessageWelcomeDirective } from './spright-chat-message-welcome.directive'; + +import '@ni/spright-components/dist/esm/chat/message/welcome'; + +@NgModule({ + declarations: [ + SprightChatMessageWelcomeDirective + ], + imports: [CommonModule], + exports: [ + SprightChatMessageWelcomeDirective + ] +}) +export class SprightChatMessageWelcomeModule { } \ No newline at end of file diff --git a/packages/angular-workspace/spright-angular/chat/message/welcome/tests/spright-chat-message-welcome.directive.spec.ts b/packages/angular-workspace/spright-angular/chat/message/welcome/tests/spright-chat-message-welcome.directive.spec.ts new file mode 100644 index 0000000000..fd5a7e63ce --- /dev/null +++ b/packages/angular-workspace/spright-angular/chat/message/welcome/tests/spright-chat-message-welcome.directive.spec.ts @@ -0,0 +1,142 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SprightChatMessageWelcomeDirective, type ChatMessageWelcome } from '../spright-chat-message-welcome.directive'; +import { SprightChatMessageWelcomeModule } from '../spright-chat-message-welcome.module'; + +describe('Spright chat message welcome', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SprightChatMessageWelcomeModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('spright-chat-message-welcome')).not.toBeUndefined(); + }); + }); + + describe('with no values in template', () => { + @Component({ + template: ` + Content + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('message', { read: SprightChatMessageWelcomeDirective }) public directive: SprightChatMessageWelcomeDirective; + @ViewChild('message', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: SprightChatMessageWelcomeDirective; + let nativeElement: ChatMessageWelcome; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [SprightChatMessageWelcomeModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('has expected defaults for title', () => { + expect(directive.title).toBeUndefined(); + expect(nativeElement.welcomeTitle).toBeUndefined(); + }); + + it('has expected defaults for subtitle', () => { + expect(directive.subtitle).toBeUndefined(); + expect(nativeElement.subtitle).toBeUndefined(); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + Content + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('message', { read: SprightChatMessageWelcomeDirective }) public directive: SprightChatMessageWelcomeDirective; + @ViewChild('message', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: SprightChatMessageWelcomeDirective; + let nativeElement: ChatMessageWelcome; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [SprightChatMessageWelcomeModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for title', () => { + expect(directive.title).toBe('Welcome title'); + expect(nativeElement.welcomeTitle).toBe('Welcome title'); + }); + + it('will use template string values for subtitle', () => { + expect(directive.subtitle).toBe('Welcome subtitle'); + expect(nativeElement.subtitle).toBe('Welcome subtitle'); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + Content + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('message', { read: SprightChatMessageWelcomeDirective }) public directive: SprightChatMessageWelcomeDirective; + @ViewChild('message', { read: ElementRef }) public elementRef: ElementRef; + public title = 'initial title'; + public subtitle = 'initial subtitle'; + } + + let fixture: ComponentFixture; + let directive: SprightChatMessageWelcomeDirective; + let nativeElement: ChatMessageWelcome; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [SprightChatMessageWelcomeModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for title and subtitle', () => { + expect(directive.title).toBe('initial title'); + expect(nativeElement.welcomeTitle).toBe('initial title'); + expect(directive.subtitle).toBe('initial subtitle'); + expect(nativeElement.subtitle).toBe('initial subtitle'); + + fixture.componentInstance.title = 'updated title'; + fixture.componentInstance.subtitle = 'updated subtitle'; + fixture.detectChanges(); + + expect(directive.title).toBe('updated title'); + expect(nativeElement.welcomeTitle).toBe('updated title'); + expect(directive.subtitle).toBe('updated subtitle'); + expect(nativeElement.subtitle).toBe('updated subtitle'); + }); + }); +}); \ No newline at end of file diff --git a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor new file mode 100644 index 0000000000..64e41172f5 --- /dev/null +++ b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor @@ -0,0 +1,7 @@ +@namespace SprightBlazor + + @ChildContent + \ No newline at end of file diff --git a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs new file mode 100644 index 0000000000..67ac10e294 --- /dev/null +++ b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Components; + +namespace SprightBlazor; + +public partial class SprightChatMessageWelcome : ComponentBase +{ + /// + /// Gets or sets the primary welcome title text. + /// + [Parameter] + public string? Title { get; set; } + + /// + /// Gets or sets the secondary subtitle text. + /// + [Parameter] + public string? Subtitle { get; set; } + + /// + /// The child content of the element. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Any additional attributes that did not match known properties. + /// + [Parameter(CaptureUnmatchedValues = true)] + public IDictionary? AdditionalAttributes { get; set; } +} \ No newline at end of file diff --git a/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs b/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs new file mode 100644 index 0000000000..f4620b960c --- /dev/null +++ b/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq.Expressions; +using Bunit; +using Xunit; + +namespace SprightBlazor.Tests.Unit.Components; + +/// +/// Test for . +/// +public class SprightChatMessageWelcomeTests +{ + [Fact] + public void SprightChatMessageWelcome_Render_HasChatMessageMarkup() + { + var context = new TestContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + var expectedMarkup = "spright-chat-message-welcome"; + + var component = context.RenderComponent(); + + Assert.Contains(expectedMarkup, component.Markup); + } + + [Fact] + public void SprightChatMessageWelcome_SupportsAdditionalAttributes() + { + var context = new TestContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + var exception = Record.Exception(() => context.RenderComponent(ComponentParameter.CreateParameter("class", "foo"))); + Assert.Null(exception); + } + + [Theory] + [InlineData("Welcome to Nigel", "title=\"Welcome to Nigel\"")] + [InlineData("Log in to continue", "subtitle=\"Log in to continue\"")] + public void SprightChatMessageWelcome_AttributeIsSet(string value, string expectedAttribute) + { + var message = expectedAttribute.StartsWith("title", StringComparison.Ordinal) + ? RenderWithPropertySet(x => x.Title, value) + : RenderWithPropertySet(x => x.Subtitle, value); + + Assert.Contains(expectedAttribute, message.Markup); + } + + private IRenderedComponent RenderWithPropertySet(Expression> propertyGetter, TProperty propertyValue) + { + var context = new TestContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + return context.RenderComponent(p => p.Add(propertyGetter, propertyValue)); + } +} \ No newline at end of file diff --git a/packages/react-workspace/spright-react/src/chat/message/welcome/index.ts b/packages/react-workspace/spright-react/src/chat/message/welcome/index.ts new file mode 100644 index 0000000000..e2c2e05d5f --- /dev/null +++ b/packages/react-workspace/spright-react/src/chat/message/welcome/index.ts @@ -0,0 +1,4 @@ +import { ChatMessageWelcome } from '@ni/spright-components/dist/esm/chat/message/welcome'; +import { wrap } from '../../../utilities/react-wrapper'; + +export const SprightChatMessageWelcome = wrap(ChatMessageWelcome); \ No newline at end of file diff --git a/packages/spright-components/src/all-components.ts b/packages/spright-components/src/all-components.ts index 6baa8b8ec2..30a0639845 100644 --- a/packages/spright-components/src/all-components.ts +++ b/packages/spright-components/src/all-components.ts @@ -12,5 +12,6 @@ import './chat/message'; import './chat/message/inbound'; import './chat/message/outbound'; import './chat/message/system'; +import './chat/message/welcome'; import './icons/all-icons'; import './rectangle'; diff --git a/packages/spright-components/src/chat/message/welcome/index.ts b/packages/spright-components/src/chat/message/welcome/index.ts new file mode 100644 index 0000000000..aa007412d4 --- /dev/null +++ b/packages/spright-components/src/chat/message/welcome/index.ts @@ -0,0 +1,44 @@ +import { attr } from '@ni/fast-element'; +import { + applyMixins, + DesignSystem, + FoundationElement, + StartEnd, + type FoundationElementDefinition, + type StartEndOptions +} from '@ni/fast-foundation'; +import { styles } from './styles'; +import { template } from './template'; + +declare global { + interface HTMLElementTagNameMap { + 'spright-chat-message-welcome': ChatMessageWelcome; + } +} + +/** + * SprightChatMessageWelcome configuration options + * @public + */ +export type ChatMessageWelcomeOptions = FoundationElementDefinition & StartEndOptions; + +/** + * A Spright component for displaying a welcome chat message + */ +export class ChatMessageWelcome extends FoundationElement { + @attr({ attribute: 'title' }) + public welcomeTitle?: string; + + @attr + public subtitle?: string; +} +applyMixins(ChatMessageWelcome, StartEnd); + +const sprightChatMessageWelcome = ChatMessageWelcome.compose({ + baseName: 'chat-message-welcome', + template, + styles +}); + +DesignSystem.getOrCreate().withPrefix('spright').register(sprightChatMessageWelcome()); +export const chatMessageWelcomeTag = 'spright-chat-message-welcome'; diff --git a/packages/spright-components/src/chat/message/welcome/styles.ts b/packages/spright-components/src/chat/message/welcome/styles.ts new file mode 100644 index 0000000000..225ce0e463 --- /dev/null +++ b/packages/spright-components/src/chat/message/welcome/styles.ts @@ -0,0 +1,70 @@ +import { css } from '@ni/fast-element'; + +import { + bodyFont, + bodyFontColor, + mediumPadding, + standardPadding, + subtitleFont, + subtitleFontColor, + titlePlus1Font, + titlePlus1FontColor, +} from '@ni/nimble-components/dist/esm/theme-provider/design-tokens'; +import { display } from '../../../utilities/style/display'; + +export const styles = css` + ${display('flex')} + + :host { + min-width: ${standardPadding}; + min-height: ${standardPadding}; + + flex-direction: row; + justify-content: center; + flex-shrink: 0; + font: ${bodyFont}; + color: ${bodyFontColor}; + } + + .container { + display: flex; + flex-direction: column; + align-items: center; + max-width: calc(90%); + gap: ${mediumPadding}; + } + + .brand-icon { + display: flex; + align-items: center; + justify-content: center; + } + + .title { + font: ${titlePlus1Font}; + color: ${titlePlus1FontColor}; + text-align: center; + } + + .subtitle { + font: ${subtitleFont}; + color: ${subtitleFontColor}; + text-align: center; + } + + .message-content { + display: flex; + flex-direction: column; + align-items: center; + width: fit-content; + height: fit-content; + overflow-x: auto; + gap: ${mediumPadding}; + } + + .end { + display: flex; + column-gap: ${standardPadding}; + margin-top: ${mediumPadding}; + } +`; diff --git a/packages/spright-components/src/chat/message/welcome/template.ts b/packages/spright-components/src/chat/message/welcome/template.ts new file mode 100644 index 0000000000..4805a2577b --- /dev/null +++ b/packages/spright-components/src/chat/message/welcome/template.ts @@ -0,0 +1,27 @@ +import { html, ViewTemplate, when } from '@ni/fast-element'; +import { + endSlotTemplate, + type FoundationElementTemplate +} from '@ni/fast-foundation'; +import type { ChatMessageWelcome, ChatMessageWelcomeOptions } from '.'; + +export const template: FoundationElementTemplate< +ViewTemplate, +ChatMessageWelcomeOptions +> = (context, definition) => html` +
+
+ +
+ ${when(x => x.welcomeTitle, html` +
${x => x.welcomeTitle}
+ `)} + ${when(x => x.subtitle, html` +
${x => x.subtitle}
+ `)} +
+ +
+ ${endSlotTemplate(context, definition)} +
+`; diff --git a/packages/spright-components/src/chat/message/welcome/tests/chat-message-welcome.spec.ts b/packages/spright-components/src/chat/message/welcome/tests/chat-message-welcome.spec.ts new file mode 100644 index 0000000000..2503a0aa0c --- /dev/null +++ b/packages/spright-components/src/chat/message/welcome/tests/chat-message-welcome.spec.ts @@ -0,0 +1,120 @@ +import { html } from '@ni/fast-element'; +import { + ChatMessageWelcome, + chatMessageWelcomeTag +} from '..'; +import { fixture, type Fixture } from '../../../../utilities/tests/fixture'; + +async function setup( + welcomeTitle?: string, + subtitle?: string +): Promise> { + const titleAttr = welcomeTitle + ? `title="${welcomeTitle}"` + : ''; + const subtitleAttr = subtitle ? `subtitle="${subtitle}"` : ''; + return await fixture( + // prettier-ignore + html`<${chatMessageWelcomeTag} ${titleAttr} ${subtitleAttr}>Welcome content` + ); +} + +describe('ChatMessageWelcome', () => { + let element: ChatMessageWelcome; + let connect: () => Promise; + let disconnect: () => Promise; + + afterEach(async () => { + await disconnect(); + }); + + it('can construct an element instance', () => { + expect( + document.createElement(chatMessageWelcomeTag) + ).toBeInstanceOf(ChatMessageWelcome); + }); + + it('should have a default slot element in the shadow DOM', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + const slot = element.shadowRoot?.querySelector( + 'slot:not([name])' + ); + expect(slot).not.toBeNull(); + }); + + it('should display the welcome title when set', async () => { + ({ element, connect, disconnect } = await setup( + 'Welcome to Nigel AI' + )); + await connect(); + const titleDiv = element.shadowRoot?.querySelector('.title'); + expect(titleDiv?.textContent?.trim()).toBe('Welcome to Nigel AI'); + }); + + it('should display the subtitle when set', async () => { + ({ element, connect, disconnect } = await setup( + undefined, + 'Chat below to get started' + )); + await connect(); + const subtitleDiv = element.shadowRoot?.querySelector('.subtitle'); + expect(subtitleDiv?.textContent?.trim()).toBe( + 'Chat below to get started' + ); + }); + + it('should not render title element when title is not set', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + const titleDiv = element.shadowRoot?.querySelector('.title'); + expect(titleDiv).toBeNull(); + }); + + it('should not render subtitle element when subtitle is not set', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + const subtitleDiv = element.shadowRoot?.querySelector('.subtitle'); + expect(subtitleDiv).toBeNull(); + }); + + it('should have a brand-icon slot', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + const brandIconSlot = element.shadowRoot?.querySelector( + 'slot[name="brand-icon"]' + ); + expect(brandIconSlot).not.toBeNull(); + }); + + it('should have an end slot', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + const endSlot = element.shadowRoot?.querySelector( + 'slot[name="end"]' + ); + expect(endSlot).not.toBeNull(); + }); + + it('should render both title and subtitle when both are set', async () => { + ({ element, connect, disconnect } = await setup( + 'Welcome to Nigel AI', + 'Chat below to get started' + )); + await connect(); + const titleDiv = element.shadowRoot?.querySelector('.title'); + const subtitleDiv = element.shadowRoot?.querySelector('.subtitle'); + expect(titleDiv?.textContent?.trim()).toBe('Welcome to Nigel AI'); + expect(subtitleDiv?.textContent?.trim()).toBe( + 'Chat below to get started' + ); + }); + + it('should display slotted content in the default slot', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + expect( + element.innerText.includes('Welcome content') + ).toBeTrue(); + }); +}); diff --git a/packages/storybook/src/spright/chat/conversation/chat-conversation.mdx b/packages/storybook/src/spright/chat/conversation/chat-conversation.mdx index 1367be6e77..f0b76af597 100644 --- a/packages/storybook/src/spright/chat/conversation/chat-conversation.mdx +++ b/packages/storybook/src/spright/chat/conversation/chat-conversation.mdx @@ -4,6 +4,7 @@ import { chatInputTag } from '@ni/spright-components/dist/esm/chat/input'; import { chatMessageInboundTag } from '@ni/spright-components/dist/esm/chat/message/inbound'; import { chatMessageOutboundTag } from '@ni/spright-components/dist/esm/chat/message/outbound'; import { chatMessageSystemTag } from '@ni/spright-components/dist/esm/chat/message/system'; +import { chatMessageWelcomeTag } from '@ni/spright-components/dist/esm/chat/message/welcome'; import * as conversationStories from './chat-conversation.stories'; import * as messageStories from '../message/chat-message.stories'; import * as inputStories from '../input/chat-input.stories'; @@ -69,6 +70,16 @@ Use the element to display content in a syst +#### Welcome message + +Use the element to display welcome content in the chat window. + +##### Welcome message content + + + + + ### Chat input Use the element to allow the user to create and send diff --git a/packages/storybook/src/spright/chat/conversation/chat-conversation.stories.ts b/packages/storybook/src/spright/chat/conversation/chat-conversation.stories.ts index 529caeb919..57ccdabc0b 100644 --- a/packages/storybook/src/spright/chat/conversation/chat-conversation.stories.ts +++ b/packages/storybook/src/spright/chat/conversation/chat-conversation.stories.ts @@ -20,6 +20,7 @@ import type { ChatInputSendEventDetail } from '@ni/spright-components/dist/esm/c import { chatMessageInboundTag } from '@ni/spright-components/dist/esm/chat/message/inbound'; import { chatMessageOutboundTag } from '@ni/spright-components/dist/esm/chat/message/outbound'; import { chatMessageSystemTag } from '@ni/spright-components/dist/esm/chat/message/system'; +import { chatMessageWelcomeTag } from '@ni/spright-components/dist/esm/chat/message/welcome'; import { richTextViewerTag } from '@ni/nimble-components/dist/esm/rich-text/viewer'; import { spinnerTag } from '@ni/nimble-components/dist/esm/spinner'; import { iconCopyTextTag } from '@ni/nimble-components/dist/esm/icons/copy-text'; @@ -41,6 +42,7 @@ import { isChromatic } from '../../../utilities/isChromatic'; interface ChatConversationArgs { appearance: keyof typeof ChatConversationAppearance; content: string; + welcome: boolean; toolbar: boolean; start: boolean; input: boolean; @@ -81,6 +83,13 @@ export const chatConversation: StoryObj = { This is the message text of this banner. It tells you something interesting. `)} + ${when(x => x.welcome, html` + <${chatMessageWelcomeTag} title="Welcome to Nigel\u2122 AI" subtitle="Chat below to get started"> + <${iconMessagesSparkleTag} slot="brand-icon"> + <${buttonTag} appearance="block">Help me analyze test data + <${buttonTag} appearance="block">Summarize recent test results + + `)} <${chatMessageSystemTag}> To start, press any key. @@ -160,6 +169,11 @@ export const chatConversation: StoryObj = { 'A slot to optionally include content (such as banners) which will be displayed below the toolbar and above the messages.', table: { category: apiCategory.slots } }, + welcome: { + description: + `Toggle a \`${chatMessageWelcomeTag}\` in the default slot to display a welcome message with a brand icon, title, subtitle, and suggested prompt buttons.`, + table: { category: apiCategory.slots } + }, input: { description: `A slot to optionally include a \`${chatInputTag}\` which will be displayed below the messages.`, table: { category: apiCategory.slots } @@ -170,6 +184,7 @@ export const chatConversation: StoryObj = { }, args: { appearance: 'default', + welcome: true, input: true, toolbar: true, start: true, diff --git a/packages/storybook/src/spright/chat/message/chat-message-matrix.stories.ts b/packages/storybook/src/spright/chat/message/chat-message-matrix.stories.ts index 06fc7a851f..300f3256d9 100644 --- a/packages/storybook/src/spright/chat/message/chat-message-matrix.stories.ts +++ b/packages/storybook/src/spright/chat/message/chat-message-matrix.stories.ts @@ -4,6 +4,7 @@ import { chatMessageTag } from '@ni/spright-components/dist/esm/chat/message'; import { chatMessageInboundTag } from '@ni/spright-components/dist/esm/chat/message/inbound'; import { chatMessageOutboundTag } from '@ni/spright-components/dist/esm/chat/message/outbound'; import { chatMessageSystemTag } from '@ni/spright-components/dist/esm/chat/message/system'; +import { chatMessageWelcomeTag } from '@ni/spright-components/dist/esm/chat/message/welcome'; import { sharedMatrixParameters, createMatrixThemeStory @@ -68,3 +69,15 @@ export const messageSystemTextCustomized: StoryFn = createMatrixThemeStory( html`<${chatMessageSystemTag}>System Message` ) ); + +export const messageWelcomeHidden: StoryFn = createStory( + hiddenWrapper( + html`<${chatMessageWelcomeTag} hidden title="Welcome" subtitle="Get started">Hidden Chat Welcome Message` + ) +); + +export const messageWelcomeTextCustomized: StoryFn = createMatrixThemeStory( + textCustomizationWrapper( + html`<${chatMessageWelcomeTag} title="Welcome" subtitle="Get started">Welcome Message` + ) +); diff --git a/packages/storybook/src/spright/chat/message/chat-message.stories.ts b/packages/storybook/src/spright/chat/message/chat-message.stories.ts index 8a14638643..b32dfc4700 100644 --- a/packages/storybook/src/spright/chat/message/chat-message.stories.ts +++ b/packages/storybook/src/spright/chat/message/chat-message.stories.ts @@ -5,9 +5,12 @@ import { buttonTag } from '@ni/nimble-components/dist/esm/button'; import { chatMessageInboundTag } from '@ni/spright-components/dist/esm/chat/message/inbound'; import { chatMessageOutboundTag } from '@ni/spright-components/dist/esm/chat/message/outbound'; import { chatMessageSystemTag } from '@ni/spright-components/dist/esm/chat/message/system'; +import { chatMessageWelcomeTag } from '@ni/spright-components/dist/esm/chat/message/welcome'; import { richTextViewerTag } from '@ni/nimble-components/dist/esm/rich-text/viewer'; +import { anchorButtonTag } from '@ni/nimble-components/dist/esm/anchor-button'; import { spinnerTag } from '@ni/nimble-components/dist/esm/spinner'; import { SpinnerAppearance } from '@ni/nimble-components/dist/esm/spinner/types'; +import { iconMessagesSparkleTag } from '@ni/nimble-components/dist/esm/icons/messages-sparkle'; import { iconThumbUpTag } from '@ni/nimble-components/dist/esm/icons/thumb-up'; import { iconThumbDownTag } from '@ni/nimble-components/dist/esm/icons/thumb-down'; import { isChromatic } from '../../../utilities/isChromatic'; @@ -156,3 +159,72 @@ export const chatMessageImage: StoryObj = { endButtons: false } }; + +interface ChatMessageWelcomeArgs { + welcomeTitle: string; + subtitle: string; + defaultSlot: boolean; + brandIcon: boolean; + endButtons: boolean; +} + +export const chatMessageWelcome: StoryObj = { + render: createUserSelectedThemeStory(html` + <${chatMessageWelcomeTag} + title="${x => x.welcomeTitle}" + subtitle="${x => x.subtitle}" + > + ${when(x => x.brandIcon, html` + <${iconMessagesSparkleTag} slot="brand-icon"> + `)} + ${when(x => x.defaultSlot, html` + <${anchorButtonTag} appearance="block" appearance-variant="primary" href="javascript:void(0)"> + Login + + <${buttonTag} appearance="block"> + Help me get started + + <${buttonTag} appearance="block"> + What can you do? + + `)} + ${when(x => x.endButtons, html` + <${buttonTag} slot="end" appearance="block">Suggested prompt 1 + <${buttonTag} slot="end" appearance="block">Suggested prompt 2 + `)} + + `), + argTypes: { + welcomeTitle: { + name: 'title', + description: 'The primary welcome title text displayed in the message.', + table: { category: apiCategory.attributes } + }, + subtitle: { + description: 'The secondary subtitle text displayed below the title.', + table: { category: apiCategory.attributes } + }, + defaultSlot: { + name: 'default', + description: 'Content to display below the icon, title, and subtitle. For example, a login button or suggested outbound messages.', + table: { category: apiCategory.slots } + }, + brandIcon: { + name: 'brand-icon', + description: 'A slot to customize the brand image displayed at the top of the welcome message.', + table: { category: apiCategory.slots } + }, + endButtons: { + name: 'end', + description: 'Place 0 or more text buttons below the main welcome content.', + table: { category: apiCategory.slots } + } + }, + args: { + welcomeTitle: 'Welcome to Nigel\u2122 AI', + subtitle: 'Chat below to get started', + defaultSlot: true, + brandIcon: true, + endButtons: false + } +}; From 6555af12f84a55996aa63fcf946d90c86a45da10 Mon Sep 17 00:00:00 2001 From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:18:57 -0600 Subject: [PATCH 2/4] Change files --- ...right-angular-3dd80b72-fa3a-4f62-9749-f852ef6a2b91.json | 7 +++++++ ...pright-blazor-d588028d-8950-4858-88e6-3fc776f415fa.json | 7 +++++++ ...ht-components-8210bc64-3fb5-426c-9a91-82be850e2405.json | 7 +++++++ ...spright-react-0718d981-c3df-4a58-8307-7bd5e895e960.json | 7 +++++++ 4 files changed, 28 insertions(+) create mode 100644 change/@ni-spright-angular-3dd80b72-fa3a-4f62-9749-f852ef6a2b91.json create mode 100644 change/@ni-spright-blazor-d588028d-8950-4858-88e6-3fc776f415fa.json create mode 100644 change/@ni-spright-components-8210bc64-3fb5-426c-9a91-82be850e2405.json create mode 100644 change/@ni-spright-react-0718d981-c3df-4a58-8307-7bd5e895e960.json diff --git a/change/@ni-spright-angular-3dd80b72-fa3a-4f62-9749-f852ef6a2b91.json b/change/@ni-spright-angular-3dd80b72-fa3a-4f62-9749-f852ef6a2b91.json new file mode 100644 index 0000000000..ca8d8cbc24 --- /dev/null +++ b/change/@ni-spright-angular-3dd80b72-fa3a-4f62-9749-f852ef6a2b91.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add welcome message for spright chat conversation", + "packageName": "@ni/spright-angular", + "email": "5265744+hellovolcano@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@ni-spright-blazor-d588028d-8950-4858-88e6-3fc776f415fa.json b/change/@ni-spright-blazor-d588028d-8950-4858-88e6-3fc776f415fa.json new file mode 100644 index 0000000000..bd086604d2 --- /dev/null +++ b/change/@ni-spright-blazor-d588028d-8950-4858-88e6-3fc776f415fa.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add welcome message for spright chat conversation", + "packageName": "@ni/spright-blazor", + "email": "5265744+hellovolcano@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@ni-spright-components-8210bc64-3fb5-426c-9a91-82be850e2405.json b/change/@ni-spright-components-8210bc64-3fb5-426c-9a91-82be850e2405.json new file mode 100644 index 0000000000..febd62d7b1 --- /dev/null +++ b/change/@ni-spright-components-8210bc64-3fb5-426c-9a91-82be850e2405.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add welcome message for spright chat conversation", + "packageName": "@ni/spright-components", + "email": "5265744+hellovolcano@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@ni-spright-react-0718d981-c3df-4a58-8307-7bd5e895e960.json b/change/@ni-spright-react-0718d981-c3df-4a58-8307-7bd5e895e960.json new file mode 100644 index 0000000000..05bd821e19 --- /dev/null +++ b/change/@ni-spright-react-0718d981-c3df-4a58-8307-7bd5e895e960.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add welcome message for spright chat conversation", + "packageName": "@ni/spright-react", + "email": "5265744+hellovolcano@users.noreply.github.com", + "dependentChangeType": "patch" +} From ba3c0cdb301850d95208f5713598fbb405b1dc55 Mon Sep 17 00:00:00 2001 From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:29:34 -0600 Subject: [PATCH 3/4] update blazor wrapper --- .../SprightChatMessageWelcome.razor | 4 +--- .../SprightChatMessageWelcome.razor.cs | 12 ------------ .../SprightChatMessageWelcomeTests.cs | 19 ------------------- 3 files changed, 1 insertion(+), 34 deletions(-) diff --git a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor index 64e41172f5..c8e5c5081a 100644 --- a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor +++ b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor @@ -1,7 +1,5 @@ @namespace SprightBlazor + @attributes="AdditionalAttributes"> @ChildContent \ No newline at end of file diff --git a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs index 67ac10e294..e629874e8b 100644 --- a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs +++ b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs @@ -4,18 +4,6 @@ namespace SprightBlazor; public partial class SprightChatMessageWelcome : ComponentBase { - /// - /// Gets or sets the primary welcome title text. - /// - [Parameter] - public string? Title { get; set; } - - /// - /// Gets or sets the secondary subtitle text. - /// - [Parameter] - public string? Subtitle { get; set; } - /// /// The child content of the element. /// diff --git a/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs b/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs index f4620b960c..76c293f87e 100644 --- a/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs +++ b/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq.Expressions; using Bunit; using Xunit; @@ -31,22 +30,4 @@ public void SprightChatMessageWelcome_SupportsAdditionalAttributes() Assert.Null(exception); } - [Theory] - [InlineData("Welcome to Nigel", "title=\"Welcome to Nigel\"")] - [InlineData("Log in to continue", "subtitle=\"Log in to continue\"")] - public void SprightChatMessageWelcome_AttributeIsSet(string value, string expectedAttribute) - { - var message = expectedAttribute.StartsWith("title", StringComparison.Ordinal) - ? RenderWithPropertySet(x => x.Title, value) - : RenderWithPropertySet(x => x.Subtitle, value); - - Assert.Contains(expectedAttribute, message.Markup); - } - - private IRenderedComponent RenderWithPropertySet(Expression> propertyGetter, TProperty propertyValue) - { - var context = new TestContext(); - context.JSInterop.Mode = JSRuntimeMode.Loose; - return context.RenderComponent(p => p.Add(propertyGetter, propertyValue)); - } } \ No newline at end of file From 423d22d8689c30a34e35226c001a8acb4abd047e Mon Sep 17 00:00:00 2001 From: Valerie Gleason <5265744+hellovolcano@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:35:06 -0600 Subject: [PATCH 4/4] add title and subtitle attributes --- .../SprightChatMessageWelcome.razor | 4 +++- .../SprightChatMessageWelcome.razor.cs | 12 ++++++++++++ .../SprightChatMessageWelcomeTests.cs | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor index c8e5c5081a..64e41172f5 100644 --- a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor +++ b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor @@ -1,5 +1,7 @@ @namespace SprightBlazor + @attributes="AdditionalAttributes" + title="@Title" + subtitle="@Subtitle"> @ChildContent \ No newline at end of file diff --git a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs index e629874e8b..67ac10e294 100644 --- a/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs +++ b/packages/blazor-workspace/SprightBlazor/Components/SprightChatMessageWelcome.razor.cs @@ -4,6 +4,18 @@ namespace SprightBlazor; public partial class SprightChatMessageWelcome : ComponentBase { + /// + /// Gets or sets the primary welcome title text. + /// + [Parameter] + public string? Title { get; set; } + + /// + /// Gets or sets the secondary subtitle text. + /// + [Parameter] + public string? Subtitle { get; set; } + /// /// The child content of the element. /// diff --git a/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs b/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs index 76c293f87e..f4620b960c 100644 --- a/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs +++ b/packages/blazor-workspace/Tests/SprightBlazor.Tests/Unit/Components/SprightChatMessageWelcomeTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq.Expressions; using Bunit; using Xunit; @@ -30,4 +31,22 @@ public void SprightChatMessageWelcome_SupportsAdditionalAttributes() Assert.Null(exception); } + [Theory] + [InlineData("Welcome to Nigel", "title=\"Welcome to Nigel\"")] + [InlineData("Log in to continue", "subtitle=\"Log in to continue\"")] + public void SprightChatMessageWelcome_AttributeIsSet(string value, string expectedAttribute) + { + var message = expectedAttribute.StartsWith("title", StringComparison.Ordinal) + ? RenderWithPropertySet(x => x.Title, value) + : RenderWithPropertySet(x => x.Subtitle, value); + + Assert.Contains(expectedAttribute, message.Markup); + } + + private IRenderedComponent RenderWithPropertySet(Expression> propertyGetter, TProperty propertyValue) + { + var context = new TestContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + return context.RenderComponent(p => p.Add(propertyGetter, propertyValue)); + } } \ No newline at end of file