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
21 changes: 21 additions & 0 deletions docs/chat-ui-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ The component manages its own controller internally. No external wiring needed.
{ role: 'tool', ... } // filtered from display automatically
```

**Request body:** The controller POSTs `{ messages, pageContext, context, imsToken, room }` to the agent. `context` is the array of attached items added via `addAttachment()` for that message — it is cleared after each send and not included in persisted message history.

## Methods

| Method | Description |
|---|---|
| `chat.addAttachment({ id, label, ...rest })` | Adds a pill above the textarea. `id` is required — duplicate ids are silently ignored. `label` is the display text. Any additional fields are forwarded to the agent as context alongside the next message. |
| `chat.clear()` | Clears conversation history and resets IndexedDB for the current room. |

**Current scope:** `addAttachment` supports simple content references — e.g. a block or element from the document editor. Binary file attachments (images, uploads) are not yet supported and will extend this same API when introduced.

**Pills display:** All attached pills are currently shown with vertical scroll capped at two rows. Collapsing overflow into a "+N more" control is pending UX mocks.

## Events in

Components that want to add pills without holding a direct reference to the chat element can dispatch on `document`:

| Event | Detail | Description |
|---|---|---|
| `nx-add-to-chat` | Same shape as `addAttachment()` | Adds a pill. Handled identically to calling `addAttachment()` directly. |

## Agent stream contract

The controller consumes a server-sent event stream from `da-agent`. Each line is a JSON object with a `type` field. The UI depends on the following event types:
Expand Down
6 changes: 5 additions & 1 deletion nx2/blocks/chat/chat-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,15 @@ export default class ChatController {
body: JSON.stringify({
messages: this._messages.filter((msg) => !msg.virtual),
pageContext,
context: this._pendingContext ?? [],
imsToken: accessToken?.token ?? null,
room,
}),
signal: this._abortController.signal,
});

this._pendingContext = [];

if (!resp.ok) {
throw new Error(`Agent responded with ${resp.status}: ${await resp.text()}`);
}
Expand All @@ -237,9 +240,10 @@ export default class ChatController {
});
}

async sendMessage(message) {
async sendMessage(message, context = []) {
if (this._thinking || !this._connected) return;

this._pendingContext = context;
this._messages = [...(this._messages ?? []), { role: ROLE.USER, content: message }];
this._thinking = true;
this._update();
Expand Down
27 changes: 25 additions & 2 deletions nx2/blocks/chat/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ChatController from './chat-controller.js';
import { renderMessage, renderApprovalCard } from './renderers.js';
import './welcome/welcome.js';
import './prompts/prompts.js';
import './pills/pills.js';
import '../shared/menu/menu.js';
import { loadStyle, hashChange } from '../../utils/utils.js';
import { loadChatIcons } from './utils.js';
Expand All @@ -22,13 +23,22 @@ class NxChat extends LitElement {
connected: { type: Boolean },
toolCards: { type: Object },
_prompts: { state: true },
_attachedItems: { state: true },
};

set context(value) {
this._explicitContext = true;
this._applyContext(value);
}

_onAddToChat = ({ detail }) => this.addAttachment(detail);

addAttachment(item) {
const current = this._attachedItems ?? [];
if (current.some((i) => i.id === item.id)) return;
this._attachedItems = [...current, item];
}

_applyContext(value) {
this._context = value;
this._controller?.setContext(value);
Expand Down Expand Up @@ -95,13 +105,15 @@ class NxChat extends LitElement {
});

this._controller.connect().then(() => this._controller.loadInitialMessages());
document.addEventListener('nx-add-to-chat', this._onAddToChat);
}

disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeHash?.();
this._controller?.destroy();
document.removeEventListener('keydown', this._onApprovalKeydown);
document.removeEventListener('nx-add-to-chat', this._onAddToChat);
}

_pendingApproval() {
Expand Down Expand Up @@ -183,9 +195,11 @@ class NxChat extends LitElement {
}
const input = this.shadowRoot.querySelector('.chat-input');
const message = input.value.trim();
if (!message) return;
this._controller.sendMessage(message);
if (!message && !this._attachedItems?.length) return;
const context = this._attachedItems ?? [];
this._controller.sendMessage(message, context);
input.value = '';
this._attachedItems = [];
}

_sendPrompt(prompt) {
Expand All @@ -198,6 +212,10 @@ class NxChat extends LitElement {
if (id === MENU_OPTIONS.PROMPT) this._openPrompts();
}

_handlePillRemove({ detail: { id } }) {
this._attachedItems = (this._attachedItems ?? []).filter((item) => item.id !== id);
}

render() {
const { view } = this._context ?? {};
const prompts = (this._prompts ?? [])
Expand Down Expand Up @@ -226,6 +244,11 @@ class NxChat extends LitElement {
<div class="chat-form-wrap">
${renderApprovalCard(this._pendingApproval(), this._controller.approveToolCall)}
<form class="chat-form" autocomplete="off" @submit=${this._submit}>
${this._attachedItems?.length ? html`
<nx-chat-pills
.items=${this._attachedItems}
@nx-pill-remove=${this._handlePillRemove}
></nx-chat-pills>` : nothing}
<textarea
name="chat-input"
class="chat-input"
Expand Down
62 changes: 62 additions & 0 deletions nx2/blocks/chat/pills/pills.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
:host {
button {
background: none;
border: none;
cursor: pointer;
padding: 0;
}

ul {
margin: 0;
list-style: none;
}

.pills-container {
--pill-row-height: calc(
var(--s2-component-s-medium-line-height) + 2 * var(--s2-spacing-75)
);

display: flex;
flex-wrap: wrap;
gap: var(--s2-spacing-100);
max-height: calc(2 * var(--pill-row-height) + var(--s2-spacing-100));
overflow-y: auto;
padding: var(--s2-spacing-75) 0;
}

.pill {
display: inline-flex;
align-items: center;
gap: var(--s2-spacing-75);
padding: var(--s2-spacing-75) var(--s2-spacing-100);
background-color: var(--s2-blue-200);
border-radius: var(--s2-corner-radius-400);
max-width: 200px;
min-width: 0;
box-sizing: border-box;
justify-content: center;
}

.pill-icon::before {
content: "";
display: block;
width: 12px;
height: 12px;
background-color: var(--s2-gray-800);
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
mask-image: url("/nx2/img/icons/S2_Icon_Close_20_N.svg");
}

.pill-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
font-size: var(--s2-body-size-xs);
color: var(--s2-blue-1000);
line-height: var(--s2-component-s-medium-line-height);
}
}
49 changes: 49 additions & 0 deletions nx2/blocks/chat/pills/pills.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { LitElement, html, nothing } from 'da-lit';
import { loadStyle } from '../../../utils/utils.js';

const styles = await loadStyle(import.meta.url);

class NxChatPills extends LitElement {
static properties = {
items: { type: Array },
};

constructor() {
super();
this.items = [];
}

connectedCallback() {
super.connectedCallback();
this.shadowRoot.adoptedStyleSheets = [styles];
}

_remove(id) {
this.dispatchEvent(new CustomEvent('nx-pill-remove', { detail: { id } }));
}

_renderPill({ id, label }) {
return html`
<li class="pill">
<button
class="pill-icon"
type="button"
aria-label="Remove ${label}"
@click=${() => this._remove(id)}
></button>
<span class="pill-label" title=${label}>${label}</span>
Comment thread
sharanyavinod marked this conversation as resolved.
</li>
`;
}

render() {
if (!this.items?.length) return nothing;
return html`
<ul class="pills-container" aria-label="Attached items" aria-live="polite">
${this.items.map((item) => this._renderPill(item))}
</ul>
`;
}
}

customElements.define('nx-chat-pills', NxChatPills);
Loading