diff --git a/docs/chat-ui-component.md b/docs/chat-ui-component.md index 0693233e..7c61fdad 100644 --- a/docs/chat-ui-component.md +++ b/docs/chat-ui-component.md @@ -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: diff --git a/nx2/blocks/chat/chat-controller.js b/nx2/blocks/chat/chat-controller.js index 95990f11..7105235e 100644 --- a/nx2/blocks/chat/chat-controller.js +++ b/nx2/blocks/chat/chat-controller.js @@ -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()}`); } @@ -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(); diff --git a/nx2/blocks/chat/chat.js b/nx2/blocks/chat/chat.js index 38218115..c44bfa26 100644 --- a/nx2/blocks/chat/chat.js +++ b/nx2/blocks/chat/chat.js @@ -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'; @@ -22,6 +23,7 @@ class NxChat extends LitElement { connected: { type: Boolean }, toolCards: { type: Object }, _prompts: { state: true }, + _attachedItems: { state: true }, }; set context(value) { @@ -29,6 +31,14 @@ class NxChat extends LitElement { 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); @@ -95,6 +105,7 @@ class NxChat extends LitElement { }); this._controller.connect().then(() => this._controller.loadInitialMessages()); + document.addEventListener('nx-add-to-chat', this._onAddToChat); } disconnectedCallback() { @@ -102,6 +113,7 @@ class NxChat extends LitElement { this._unsubscribeHash?.(); this._controller?.destroy(); document.removeEventListener('keydown', this._onApprovalKeydown); + document.removeEventListener('nx-add-to-chat', this._onAddToChat); } _pendingApproval() { @@ -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) { @@ -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 ?? []) @@ -226,6 +244,11 @@ class NxChat extends LitElement {