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
33 changes: 33 additions & 0 deletions demo/src/framework/demo-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,32 @@ export class DemoBody extends LitElement {
this.hasReceivedSetChatConfig = setChatConfigState.hasReceivedSetChatConfig;
}

/**
* Notify any listener in parent components that settings have changed.
*/
private _dispatchSettingsChangeEvent() {
this.dispatchEvent(
new CustomEvent("demo-settings-changed", {
detail: { settings: this.settings },
bubbles: true,
composed: true,
}),
);
}

/**
* Notify any listener in parent components that the chat instance reference changed.
*/
private _dispatchChatInstanceChangeEvent() {
this.dispatchEvent(
new CustomEvent("demo-chat-instance-changed", {
detail: { chatInstance: this.chatInstance },
bubbles: true,
composed: true,
}),
);
}

/**
* Set the chat instance and expose it globally for tests and debugging
*/
Expand All @@ -196,6 +222,7 @@ export class DemoBody extends LitElement {
delete window.chatInstance;
}

this._dispatchChatInstanceChangeEvent();
this.requestUpdate();
}

Expand Down Expand Up @@ -266,6 +293,10 @@ export class DemoBody extends LitElement {
// Set up accordion state persistence
this._setupAccordionStateManagement();

// Inform parent components of initial state so they can hydrate their UI
this._dispatchSettingsChangeEvent();
this._dispatchChatInstanceChangeEvent();

// Set data attribute for CSS styling
this.setAttribute(
"data-set-chat-config-mode",
Expand Down Expand Up @@ -388,6 +419,8 @@ export class DemoBody extends LitElement {

this.settings = newSettings;

this._dispatchSettingsChangeEvent();

// Update query parameters with new settings
this._updateQueryParamsForSettings(newSettings, shouldRefresh);

Expand Down
114 changes: 112 additions & 2 deletions demo/src/framework/demo-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,134 @@
*/

import { html, LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { customElement, property } from "lit/decorators.js";
import { iconLoader } from "@carbon/web-components/es/globals/internal/icon-loader.js";
import AiLaunch20 from "@carbon/icons/es/ai-launch/20.js";
import Switcher20 from "@carbon/icons/es/switcher/20.js";
import { ViewType, type ChatInstance } from "@carbon/ai-chat";

import type { Settings } from "./types";

/**
* `DemoHeader` is a custom Lit element representing a header component.
*/
@customElement("demo-header")
export class DemoHeader extends LitElement {
@property({ attribute: false })
accessor settings: Settings | undefined = undefined;

@property({ attribute: false })
accessor chatInstance: ChatInstance | null = null;

private _clickInProgress = false;

onClick = async () => {
if (this.chatInstance) {
const state = this.chatInstance?.getState();
if (state.viewState.mainWindow) {
await this.chatInstance.changeView(ViewType.LAUNCHER);
} else {
await this.chatInstance.changeView(ViewType.MAIN_WINDOW);
}
}
};

private _handleButtonClick = async () => {
if (this._clickInProgress) {
return;
}

this._clickInProgress = true;
this.requestUpdate();

try {
await this.onClick();
} finally {
this._clickInProgress = false;
this.requestUpdate();
}
};

private _handlePanelToggle = (event: Event) => {
const target = event.currentTarget as HTMLElement | null;
if (!target) {
return;
}

const panelId = target.getAttribute("panel-id");
if (!panelId) {
return;
}

const panel = this.renderRoot?.querySelector<HTMLElement>(`#${panelId}`);
if (!panel) {
return;
}

if (panel.hasAttribute("expanded")) {
panel.removeAttribute("expanded");
} else {
panel.setAttribute("expanded", "");
}
};

render() {
const chatInstanceReady = this.chatInstance ? true : false;
const isFullscreen = Boolean(
this.settings?.layout === "fullscreen" ||
this.settings?.layout === "fullscreen-no-gutter",
);

return html`
<cds-header
aria-label="Carbon AI Chat"
style="height: 40px; box-sizing: content-box;"
style="${isFullscreen ? `height: 40px; box-sizing: content-box;` : ""}"
>
<cds-header-name
href="https://chat.carbondesignsystem.com/tag/latest/docs/documents/Overview.html"
prefix="Carbon"
>AI chat</cds-header-name
>
<div class="cds--header__global">
${chatInstanceReady
? html`<cds-header-global-action
aria-label="Open AI Chat"
tooltip-text="Open AI Chat"
?disabled=${this._clickInProgress}
@click=${this._handleButtonClick}
>
${iconLoader(AiLaunch20, { slot: "icon" })}
</cds-header-global-action>`
: ""}
<cds-header-global-action
aria-label="Open Resources Panel"
tooltip-text="Open Resources Panel"
tooltip-alignment="right"
panel-id="switcher-panel"
@click=${this._handlePanelToggle}
>
${iconLoader(Switcher20, { slot: "icon" })}
</cds-header-global-action>
</div>
<cds-header-panel id="switcher-panel" aria-label="Resources Panel">
<cds-switcher aria-label="Resources">
<cds-switcher-item
aria-label="Documentation site"
href="https://chat.carbondesignsystem.com/tag/latest/docs/documents/Overview.html"
>Documentation site</cds-switcher-item
>
<cds-switcher-item
aria-label="React examples"
href="https://github.com/carbon-design-system/carbon-ai-chat/tree/main/examples/react"
>React examples</cds-switcher-item
>
<cds-switcher-item
aria-label="Web component examples"
href="https://github.com/carbon-design-system/carbon-ai-chat/tree/main/examples/web-components"
>Web component examples</cds-switcher-item
>
</cds-switcher>
</cds-header-panel>
</cds-header>
`;
}
Expand Down
40 changes: 0 additions & 40 deletions demo/src/framework/demo-side-bar-nav.ts

This file was deleted.

96 changes: 93 additions & 3 deletions demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ import "./framework/demo-layout-switcher";
import "./framework/demo-homescreen-switcher";
import "./framework/demo-theme-switcher";
import "./framework/demo-page-theme-switcher";
import "./framework/demo-side-bar-nav";
import "./framework/demo-writeable-elements-switcher";
import "./web-components/demo-app";

import { PublicConfig } from "@carbon/ai-chat";
import { html, LitElement, css } from "lit";
import { ChatInstance, PublicConfig } from "@carbon/ai-chat";
import { html, LitElement, PropertyValues, css } from "lit";
import { customElement, state } from "lit/decorators.js";

import { Settings } from "./framework/types";
Expand Down Expand Up @@ -67,13 +66,25 @@ export class Demo extends LitElement {
@state()
accessor hasReceivedSetChatConfig: boolean = false;

private headerSettings?: Settings;
private headerChatInstance: ChatInstance | null = null;
private headerSlot: HTMLSlotElement | null = null;

connectedCallback() {
super.connectedCallback();
// Listen for setChatConfig mode changes from demo-body
this.addEventListener(
"set-chat-config-mode-changed",
this._onSetChatConfigModeChanged as EventListener,
);
this.addEventListener(
"demo-settings-changed",
this._onDemoSettingsChanged as EventListener,
);
this.addEventListener(
"demo-chat-instance-changed",
this._onDemoChatInstanceChanged as EventListener,
);
}

disconnectedCallback() {
Expand All @@ -82,6 +93,19 @@ export class Demo extends LitElement {
"set-chat-config-mode-changed",
this._onSetChatConfigModeChanged as EventListener,
);
this.removeEventListener(
"demo-settings-changed",
this._onDemoSettingsChanged as EventListener,
);
this.removeEventListener(
"demo-chat-instance-changed",
this._onDemoChatInstanceChanged as EventListener,
);
this.headerSlot?.removeEventListener(
"slotchange",
this._onHeaderSlotChange,
);
this.headerSlot = null;
}

private _onSetChatConfigModeChanged = (event: Event) => {
Expand All @@ -90,6 +114,72 @@ export class Demo extends LitElement {
this.hasReceivedSetChatConfig = customEvent.detail.hasReceivedSetChatConfig;
};

private _onDemoSettingsChanged = (event: Event) => {
const customEvent = event as CustomEvent<{ settings: Settings }>;
this.headerSettings = customEvent.detail.settings;
this._applyHeaderStateToComponent();
};

private _onDemoChatInstanceChanged = (event: Event) => {
const customEvent = event as CustomEvent<{
chatInstance: ChatInstance | null;
}>;
this.headerChatInstance = customEvent.detail.chatInstance ?? null;
this._applyHeaderStateToComponent();
};

private _onHeaderSlotChange = () => {
this._applyHeaderStateToComponent();
};

protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
this.headerSlot = this.shadowRoot?.querySelector(
'slot[name="demo-header"]',
) as HTMLSlotElement | null;
this.headerSlot?.addEventListener("slotchange", this._onHeaderSlotChange);
this._applyHeaderStateToComponent();
}

private _getDemoHeaderElement(): HTMLElement | null {
const slot =
this.headerSlot ??
(this.shadowRoot?.querySelector(
'slot[name="demo-header"]',
) as HTMLSlotElement | null);
if (!slot) {
return null;
}

const assignedElements = slot.assignedElements({ flatten: true });
for (const element of assignedElements) {
if (!(element instanceof HTMLElement)) {
continue;
}

if (element.tagName.toLowerCase() === "demo-header") {
return element;
}

const nestedHeader = element.querySelector("demo-header");
if (nestedHeader) {
return nestedHeader as HTMLElement;
}
}

return null;
}

private _applyHeaderStateToComponent() {
const headerElement = this._getDemoHeaderElement();
if (!headerElement) {
return;
}

(headerElement as any).settings = this.headerSettings;
(headerElement as any).chatInstance = this.headerChatInstance;
}

private _leaveSetChatConfigMode = () => {
// Remove setChatConfig query parameters and reload
const urlParams = new URLSearchParams(window.location.search);
Expand Down
Loading