Skip to content

feat(ui5-ai-notice-indicator): create AINoticeIndicator webcomponent #11941

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
114 changes: 114 additions & 0 deletions packages/main/cypress/specs/AINoticeIndicator.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { html } from "lit";
import AINoticeIndicator from "../../src/AINoticeIndicator.js";
import ResponsivePopover from "../../src/ResponsivePopover.js";
import Button from "../../src/Button.js";

describe("AINoticeIndicator", () => {
describe("Rendering", () => {
it("should render attribution link and verification text only in Default mode", () => {
cy.mount(
<AINoticeIndicator id="ai-default" mode="Default" />
);
cy.get("#ai-default").should("exist");
cy.get("#ai-default").shadow().find(".ui5-ai-notice-indicator-root").should("exist");
cy.get("#ai-default").shadow().find("ui5-link").should("exist");
cy.get("#ai-default").shadow().find("ui5-label").should("exist");
cy.get("#ai-default").shadow().find("ui5-icon").should("not.exist");
});

it("should render attribution link only in Shortened mode", () => {
cy.mount(
<AINoticeIndicator id="ai-shortened" mode="Shortened" />
);
cy.get("#ai-shortened").should("exist");
cy.get("#ai-shortened").shadow().find(".ui5-ai-notice-indicator-root").should("exist");
cy.get("#ai-shortened").shadow().find("ui5-link").should("exist");
cy.get("#ai-shortened").shadow().find("ui5-icon").should("not.exist");
cy.get("#ai-shortened").shadow().find("ui5-label").should("not.exist");
});

it("should render icon, attribution link, and verification text in Emphasized mode", () => {
cy.mount(
<AINoticeIndicator id="ai-emphasized" mode="Emphasized" />
);
cy.get("#ai-emphasized").should("exist");
cy.get("#ai-emphasized").shadow().find(".ui5-ai-notice-indicator-root").should("exist");
cy.get("#ai-emphasized").shadow().find("ui5-link").should("exist");
cy.get("#ai-emphasized").shadow().find("ui5-icon").should("exist");
cy.get("#ai-emphasized").shadow().find("ui5-label").should("exist");
});

it("it should render icon only in IconOnly mode", () => {
cy.mount(
<AINoticeIndicator id="ai-icononly" mode="IconOnly" />
);
cy.get("#ai-icononly").should("exist");
cy.get("#ai-icononly").shadow().find(".ui5-ai-notice-indicator-root").should("exist");
cy.get("#ai-icononly").shadow().find("ui5-icon").should("exist");
cy.get("#ai-icononly").shadow().find("ui5-link").should("not.exist");
cy.get("#ai-icononly").shadow().find("ui5-label").should("not.exist");
});
});

describe("Properties", () => {
it("displays custom attributionText and verificationText when provided", () => {
const attribution = "Generated by AI";
const verification = "Please verify before use";
cy.mount(
<AINoticeIndicator
id="ai-custom-text"
mode="Default"
attributionText={attribution}
verificationText={verification}
/>
);
cy.get("#ai-custom-text").shadow().find("ui5-link").should("contain.text", attribution);
cy.get("#ai-custom-text").shadow().find("ui5-label").should("contain.text", verification);
});

it("falls back to default i18n texts when properties are not set", () => {
cy.mount(
<AINoticeIndicator id="ai-default-text" mode="Default" />
);
cy.get("#ai-default-text").shadow().find("ui5-link").should("not.be.empty");
cy.get("#ai-default-text").shadow().find("ui5-label").should("not.be.empty");
});
});

describe("Popover Slot", () => {
it("renders slotted popover content", () => {
cy.mount(
<AINoticeIndicator id="ai-popover" mode="Default">
<ResponsivePopover
slot="aiNoticePopover"
id="test-popover"
headerText="Popover Header"
>
<Button id="popover-btn">Inside Popover</Button>
</ResponsivePopover>
</AINoticeIndicator>
);
cy.get("#ai-popover").shadow().find("slot[name='aiNoticePopover']").should("exist");
cy.get("#test-popover").should("exist");
});

it("clicking the attribution link opens the popover", () => {
cy.mount(
<AINoticeIndicator id="ai-popover-click" mode="Default">
<ResponsivePopover
slot="aiNoticePopover"
id="test-popover-click"
headerText="Popover Header"
>
<Button id="popover-btn-click">Inside Popover</Button>
</ResponsivePopover>
</AINoticeIndicator>
);
cy.get("#ai-popover-click").shadow().find("ui5-link").click();
cy.get("#test-popover-click").should("have.prop", "open", true);
});
});


});

92 changes: 92 additions & 0 deletions packages/main/src/AINoticeIndicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";

import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
// Template
import AINoticeIndicatorTemplate from "./AINoticeIndicatorTemplate.js";
// Styles
import AINoticeIndicatorCss from "./generated/themes/AINoticeIndicator.css.js";

import type Link from "./Link.js";
import type AINoticeIndicatorMode from "./types/AINoticeIndicatorMode.js";
import type { LinkClickEventDetail } from "./Link.js";
import type ResponsivePopover from "./ResponsivePopover.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";

import { AI_NOTICE_ATTRIBUTION_TEXT, AI_NOTICE_VERIFICATION_TEXT } from "./generated/i18n/i18n-defaults.js";

@customElement({
tag: "ui5-ai-notice-indicator",
languageAware: true,
renderer: jsxRenderer,
styles: AINoticeIndicatorCss,
template: AINoticeIndicatorTemplate,
})
class AINoticeIndicator extends UI5Element {
/**
* AI attribution notice text (e.g., "Created with AI").
*/
@property()
attributionText?: string;

/**
* AI verification prompt text (e.g., "Verify results before use").
*/
@property()
verificationText?: string;

/**
* The mode of the AI Notice Indicator.
* It can be set to "Default", "Shortened", "Emphasized", or "IconOnly".
*/
@property({ type: String })
mode: `${AINoticeIndicatorMode}` = "Default";

/**
* Slot for popover content.
*/
@slot({ type: HTMLElement, invalidateOnChildChange: true })
aiNoticePopover!: Array<HTMLElement>;

@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;

constructor() {
super();
}

get attributionTextValue() {
if (this.attributionText) {
return this.attributionText;
}

const defaultLabel = AINoticeIndicator.i18nBundle.getText(AI_NOTICE_ATTRIBUTION_TEXT);

return defaultLabel;
}

get verificationTextValue() {
if (this.verificationText) {
return this.verificationText;
}

const defaultLabel = AINoticeIndicator.i18nBundle.getText(AI_NOTICE_VERIFICATION_TEXT);

return defaultLabel;
}

_openRespPopover(e: CustomEvent<LinkClickEventDetail>) {
const resPopover = this.getSlottedNodes<ResponsivePopover>("aiNoticePopover")[0];
if (resPopover) {
resPopover.opener = e.target as Link;
resPopover.openPopup();
}
}
}

AINoticeIndicator.define();

export default AINoticeIndicator;
40 changes: 40 additions & 0 deletions packages/main/src/AINoticeIndicatorTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Button from "./Button.js";
import Link from "./Link.js";
import Label from "./Label.js";
import Icon from "./Icon.js";

import type AINoticeIndicator from "./AINoticeIndicator.js";

export default function AINoticeIndicatorTemplate(this: AINoticeIndicator) {
const { mode, attributionTextValue, verificationTextValue } = this;

const iconButton = (
<Button design="Transparent">
<Icon name="ai" />
</Button>
);

const attributionLink = (
<Link
href="#"
onClick={this._openRespPopover}
>
{attributionTextValue}
</Link>
);

const verificationTextLabel = (
<Label>
{verificationTextValue}
</Label>
);

return (
<div class="ui5-ai-notice-indicator-root">
{mode !== "Default" && mode !== "Shortened" && iconButton}
{mode !== "IconOnly" && attributionLink}
{mode !== "IconOnly" && mode !== "Shortened" && verificationTextLabel}
<slot name="aiNoticePopover"></slot>
</div>
);
}
1 change: 1 addition & 0 deletions packages/main/src/bundle.esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { getAllRegisteredTags } from "@ui5/webcomponents-base/dist/CustomElement
// or for custom icon collection
// setDefaultIconCollection("sap_fiori_3", "my-custom-icons");

import AINoticeIndicator from "./AINoticeIndicator.js";
import Avatar from "./Avatar.js";
import AvatarGroup from "./AvatarGroup.js";
import Bar from "./Bar.js";
Expand Down
18 changes: 18 additions & 0 deletions packages/main/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
#This is the resource bundle for the UI5 Web Components
#__ldi.translation.uuid=96bea51a-d5e3-46f0-b1d1-514d97be02ec

#XTIT: Title of the AI Notice
AI_NOTICE_TITLE=Created with AI

#XTIT: Draft title of the AI Notice
AI_NOTICE_DRAFT_TITLE=Draft created with AI

#XMSG: Attribution text of the AI Notice
AI_NOTICE_ATTRIBUTION_TEXT=Created with AI.

#XMSG: Verification text of the AI Notice
AI_NOTICE_VERIFICATION_TEXT=Verify results before use.

#XMSG: Content of the AI Notice popover
AI_NOTICE_POPOVER_CONTENT=This content was partially or fully generated by artificial intelligence (AI) technologies.\n\n The AI-generated content may contain inaccuracies due to using multiple information sources. Verify results before use.

#XBUT: AI Notice popover close button
AI_NOTICE_POPOVER_CLOSE_BUTTON=Close

#XBUT: Card Content aria-label text
ARIA_LABEL_CARD_CONTENT=Card Content

Expand Down
7 changes: 7 additions & 0 deletions packages/main/src/themes/AINoticeIndicator.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
:host {
display: flex;
align-items: center;
font-family: var(--sapFontFamily);
font-size: var(--sapFontSize);
color: var(--sapTextColor);
}
27 changes: 27 additions & 0 deletions packages/main/src/types/AINoticeIndicatorMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Different types of AINoticeIndicatorMode.
* @public
*/
enum AINoticeIndicatorMode {
/**
* The default mode, displaying the full content.
*/
Default = "Default",

/**
* A shortened mode, displaying a condensed version of the content.
*/
Shortened = "Shortened",

/**
* An emphasized mode, highlighting important content.
*/
Emphasized = "Emphasized",

/**
* An icon-only mode, displaying only an icon without text.
*/
IconOnly = "IconOnly"
}

export default AINoticeIndicatorMode;
Loading
Loading