Ai Agent Panel
<sl-ai-agent-panel> | SlAiAgentPanel
AI Agent Panel is a component that provides a chat interface for interacting with an AI agent. It displays a conversation history between the user and the agent, and includes an AI Prompt component for submitting new prompts to the agent. The panel supports streaming responses from the agent, showing messages block-by-block as they are received.
<sl-ai-agent-panel class="agent-panel" id="ai-panel" typing-speed="10" welcome-text-header="Unlocking possibility" welcome-text="What can we get started on today Linda?" initials="SL"> <sl-ai-prompt id="ai-prompt" slot="ai-prompt"> <sl-select id="ai-select" slot="agent-models" value="myhub"> <sl-option value="myhub">myhub</sl-option> <sl-option value="scout">Scout</sl-option> <sl-option value="heffron">Heffron</sl-option> </sl-select> </sl-ai-prompt> <div class="secondary-text" slot="secondary-text"> <a slot="secondary-text" href="#">What's the weather like?</a> <a slot="secondary-text" href="#">How to create a new portfolio?</a> <a slot="secondary-text" href="#">What is my fortune today?</a> <a slot="secondary-text" href="#">What is the latest financial news?</a> </div> </sl-ai-agent-panel> <style> .agent-panel { max-height: 783px; width: 100%; height: 100%; } .agent-panel .secondary-text { padding: var(--hds-space-10x) 0; display: flex; align-items: center; gap: var(--hds-space-6x); } .agent-panel::part(base) { max-height: 783px; width: 100%; height: 100%; } sl-ai-agent-panel.agent-panel::part(chat-container) { width: 100%; padding: 0 calc(calc(100% - 990px) / 2); max-height: 100%; } .agent-panel sl-ai-prompt { max-width: 990px; width: 100%; --textarea-max-height: 200px; } .agent-panel sl-select { width: 100px; } </style> <script type="module"> const getImagePath = (imgName) => { const shoelaceScript = document.querySelectorAll('script[src*="shoelace"]')[0]; const baseUrl = shoelaceScript?.src.split('shoelace')[0].replace(/\/$/, '') || './dist'; return `${baseUrl}/images/${imgName}`; } const updateAgentChatContent = (content, time, shouldReplace = false, status = null) => { setTimeout(()=> { aiAgentPanel.messages[aiAgentPanel.messages.length - 1].content = shouldReplace ? content : [...aiAgentPanel.messages[aiAgentPanel.messages.length - 1].content, ...content]; if (status) { aiAgentPanel.messages[aiAgentPanel.messages.length - 1].status = status; } aiAgentPanel.requestUpdate('messages'); }, time); }; const updateMessage = (id, updates) => { aiAgentPanel.messages = aiAgentPanel.messages.map(msg => (msg.id === id ? { ...msg, ...updates } : msg)); } const aiAgentPanel = document.querySelector('.agent-panel'); const aiPrompt = aiAgentPanel.querySelector('#ai-prompt'); const aiSelect = aiPrompt.querySelector('#ai-select'); await Promise.all([ customElements.whenDefined('sl-ai-prompt'), customElements.whenDefined('sl-ai-agent-panel'), customElements.whenDefined('sl-select') ]).then(() => { aiAgentPanel.agentName = aiSelect.value; }); aiSelect.addEventListener('sl-change', (evt) => { aiAgentPanel.agentName = aiSelect.value; }); aiAgentPanel.addEventListener('sl-typing-end', (evt) => { aiPrompt.state = "default"; const id = evt.detail.messageId; updateMessage(id, { status: evt.detail.result === 'success' ? 'sent' : 'error' }) }); let index = 0; aiPrompt.addEventListener('sl-prompt-submit', (evt) => { aiPrompt.state = "disabled"; aiAgentPanel.messages = [ ...aiAgentPanel.messages, { id: Date.now().toLocaleString(), content:[{type:'text', value: evt.detail.value}], role: 'user', status: 'sent', timestamp: Date.now() }, { id: Date.now().toLocaleString(), content: [{type:'text', value: 'Thinking'}], role: 'agent', status: 'pending', timestamp: Date.now() } ]; if (index === 0) { updateAgentChatContent([{type: 'text', value: "Analysing"}], 8000, true); updateAgentChatContent([{type: 'text', value: "Generating response"}], 15000, true); updateAgentChatContent([{type: 'text', value: "Still working — thanks for your patience"}], 20000, true); updateAgentChatContent([{type: 'text', value: "Here’s lorem ipsum dolor sit amet consectetur. Proin at fermentum in consectetur donec amet varius. Porttitor sed sed morbi sit orci vel. Pharetra elit turpis id aliquet enim sit sit. Enim lorem ut dictum fermentum suscipit auctor eget congue lobortis."}, {type: 'image', value: getImagePath("test.jpg")}], 21000, true, 'streaming'); } if (index > 0) { updateAgentChatContent([{type: 'text', value: "Lorem ipsum dolor sit amet consectetur. Elit amet platea semper consequat pharetra. Molestie phasellus elit facilisis a urna lacus eget. Nascetur suspendisse tortor eu et. Nulla dui morbi erat eros venenatis. Et interdum venenatis purus sagittis posuere lobortis. Justo morbi pellentesque aliquam quam aliquet cursus leo. Commodo ac viverra amet suspendisse ultrices. Aliquet vulputate justo donec pellentesque. Sapien faucibus diam sem purus at lorem adipiscing. Aliquam lacus phasellus pellentesque fermentum. Ipsum in semper volutpat viverra in."}, {type:'text', value: "Aliquam nulla consectetur donec sed neque pretium tellus auctor at. Leo euismod ultrices maecenas suspendisse at purus. Integer amet aliquam ut orci. Nibh nunc lectus risus feugiat ac viverra mi. Aliquam nisl praesent vitae magna in sed posuere morbi. Aliquet dignissim nisl dictum sit euismod consectetur tempor vel eu. Fringilla blandit in amet condimentum aenean. Ac varius dui porttitor ultricies cursus nec mus cras. Vitae enim malesuada massa sit faucibus est dui dictum ac."}, {type:'text', value: "Fermentum lorem velit est et. Venenatis orci volutpat turpis diam neque dapibus vestibulum et. Pretium id amet at tincidunt sociis eu amet aliquam. Felis leo sed euismod convallis egestas in dolor bibendum integer. Eget amet ut fusce semper. Ipsum nisl fringilla duis duis viverra lectus tempus nibh fames. Dui quis vitae nunc purus. Euismod enim pellentesque scelerisque neque nec amet sagittis leo. In sapien enim leo molestie consectetur maecenas in. Lacus ultricies in quam sit sit scelerisque. Consectetur ut et elementum in senectus vel pulvinar diam venenatis."}, {type: 'text', value: "Proin risus dictum est proin. Praesent ultrices cras ut in. Varius eget eget nisi viverra diam viverra. Cursus rhoncus venenatis euismod cum fermentum tristique ullamcorper eu tincidunt. Justo id scelerisque orci eros congue. Mattis quis sagittis imperdiet tellus. Vestibulum elit semper tortor pulvinar. Massa nunc mattis vivamus dictumst tincidunt quam."}, {type: 'text', value: "Lorem ipsum dolor sit amet consectetur. Elit amet platea semper consequat pharetra. Molestie phasellus elit facilisis a urna lacus eget. Nascetur suspendisse tortor eu et. Nulla dui morbi erat eros venenatis. Et interdum venenatis purus sagittis posuere lobortis. Justo morbi pellentesque aliquam quam aliquet cursus leo. Commodo ac viverra amet suspendisse ultrices. Aliquet vulputate justo donec pellentesque. Sapien faucibus diam sem purus at lorem adipiscing. Aliquam lacus phasellus pellentesque fermentum. Ipsum in semper volutpat viverra in."}, {type:'text', value: "Aliquam nulla consectetur donec sed neque pretium tellus auctor at. Leo euismod ultrices maecenas suspendisse at purus. Integer amet aliquam ut orci. Nibh nunc lectus risus feugiat ac viverra mi. Aliquam nisl praesent vitae magna in sed posuere morbi. Aliquet dignissim nisl dictum sit euismod consectetur tempor vel eu. Fringilla blandit in amet condimentum aenean. Ac varius dui porttitor ultricies cursus nec mus cras. Vitae enim malesuada massa sit faucibus est dui dictum ac."}, {type:'text', value: "Fermentum lorem velit est et. Venenatis orci volutpat turpis diam neque dapibus vestibulum et. Pretium id amet at tincidunt sociis eu amet aliquam. Felis leo sed euismod convallis egestas in dolor bibendum integer. Eget amet ut fusce semper. Ipsum nisl fringilla duis duis viverra lectus tempus nibh fames. Dui quis vitae nunc purus. Euismod enim pellentesque scelerisque neque nec amet sagittis leo. In sapien enim leo molestie consectetur maecenas in. Lacus ultricies in quam sit sit scelerisque. Consectetur ut et elementum in senectus vel pulvinar diam venenatis."}, {type: 'text', value: "Proin risus dictum est proin. Praesent ultrices cras ut in. Varius eget eget nisi viverra diam viverra. Cursus rhoncus venenatis euismod cum fermentum tristique ullamcorper eu tincidunt. Justo id scelerisque orci eros congue. Mattis quis sagittis imperdiet tellus. Vestibulum elit semper tortor pulvinar. Massa nunc mattis vivamus dictumst tincidunt quam."}], 3000, true, 'streaming'); } index++; }); </script>
Examples
Myhub
<sl-ai-agent-panel class="my-hub" id="ai-panel" typing-speed="10" initials="SL"> <div class="start-text" slot="welcome-text"> <sl-icon name="myhubbie" library="hub24"></sl-icon> <span class="welcome">What can we get started on today Linda?</span> </div> <sl-ai-prompt id="ai-prompt" slot="ai-prompt"> <sl-select id="ai-select" slot="agent-models" value="myhub"> <sl-option value="myhub">myhub</sl-option> <sl-option value="scout">Scout</sl-option> <sl-option value="heffron">Heffron</sl-option> </sl-select> </sl-ai-prompt> </sl-ai-agent-panel> <style> .my-hub { width: 100%; height: 415px; } .my-hub .start-text { align-content: center; justify-content: flex-start; display: flex; gap: var(--hds-space-4x); width: 100%; max-width: 990px; min-width: max-content; } .my-hub .start-text .welcome { font-size: var(--hds-font-size-body-3xl) } .my-hub .start-text sl-icon { font-size: var(--hds-space-8x); } .my-hub::part(base) { max-height: 783px; width: 100%; height: 100%; } .my-hub::part(welcome-container) { gap: var(--hds-space-6x); margin-top: 40px; width: 100%; } sl-ai-agent-panel.my-hub::part(chat-container) { width: 100%; padding: 0 calc(calc(100% - 990px) / 2); max-height: 100%; } .my-hub sl-ai-prompt { max-width: 990px; width: 100%; --textarea-max-height: 200px; } .my-hub sl-select { width: 100px; } </style> <script type="module"> const getImagePath = (imgName) => { const shoelaceScript = document.querySelectorAll('script[src*="shoelace"]')[0]; const baseUrl = shoelaceScript?.src.split('shoelace')[0].replace(/\/$/, '') || './dist'; return `${baseUrl}/images/${imgName}`; } const updateAgentChatContent = (content, time, shouldReplace = false, status = null) => { setTimeout(()=> { aiAgentPanel.messages[aiAgentPanel.messages.length - 1].content = shouldReplace ? content : [...aiAgentPanel.messages[aiAgentPanel.messages.length - 1].content, ...content]; if (status) { aiAgentPanel.messages[aiAgentPanel.messages.length - 1].status = status; } aiAgentPanel.requestUpdate('messages'); }, time); }; const updateMessage = (id, updates) => { aiAgentPanel.messages = aiAgentPanel.messages.map(msg => (msg.id === id ? { ...msg, ...updates } : msg)); } const aiAgentPanel = document.querySelector('.my-hub'); const aiPrompt = aiAgentPanel.querySelector('#ai-prompt'); const aiSelect = aiPrompt.querySelector('#ai-select'); await Promise.all([ customElements.whenDefined('sl-ai-prompt'), customElements.whenDefined('sl-ai-agent-panel'), customElements.whenDefined('sl-select') ]).then(() => { aiAgentPanel.agentName = aiSelect.value; }); aiSelect.addEventListener('sl-change', (evt) => { aiAgentPanel.agentName = aiSelect.value; }); aiAgentPanel.addEventListener('sl-typing-end', (evt) => { aiPrompt.state = "default"; const id = evt.detail.messageId; updateMessage(id, { status: evt.detail.result === 'success' ? 'sent' : 'error' }) }); let index = 0; aiPrompt.addEventListener('sl-prompt-submit', (evt) => { aiPrompt.state = "disabled"; aiAgentPanel.messages = [ ...aiAgentPanel.messages, { id: Date.now().toLocaleString(), content:[{type:'text', value: evt.detail.value}], role: 'user', status: 'sent', timestamp: Date.now() }, { id: Date.now().toLocaleString(), content: [{type:'text', value: 'Thinking'}], role: 'agent', status: 'pending', timestamp: Date.now() } ]; if (index === 0) { updateAgentChatContent([{type: 'text', value: "Here’s lorem ipsum dolor sit amet consectetur. Proin at fermentum in consectetur donec amet varius. Porttitor sed sed morbi sit orci vel. Pharetra elit turpis id aliquet enim sit sit. Enim lorem ut dictum fermentum suscipit auctor eget congue lobortis."}, {type: 'image', value: getImagePath("test.jpg")}], 3000, true, 'streaming'); } if (index > 0) { updateAgentChatContent([{type: 'text', value: "Lorem ipsum dolor sit amet consectetur. Elit amet platea semper consequat pharetra. Molestie phasellus elit facilisis a urna lacus eget. Nascetur suspendisse tortor eu et. Nulla dui morbi erat eros venenatis. Et interdum venenatis purus sagittis posuere lobortis. Justo morbi pellentesque aliquam quam aliquet cursus leo. Commodo ac viverra amet suspendisse ultrices. Aliquet vulputate justo donec pellentesque. Sapien faucibus diam sem purus at lorem adipiscing. Aliquam lacus phasellus pellentesque fermentum. Ipsum in semper volutpat viverra in."}, {type:'text', value: "Aliquam nulla consectetur donec sed neque pretium tellus auctor at. Leo euismod ultrices maecenas suspendisse at purus. Integer amet aliquam ut orci. Nibh nunc lectus risus feugiat ac viverra mi. Aliquam nisl praesent vitae magna in sed posuere morbi. Aliquet dignissim nisl dictum sit euismod consectetur tempor vel eu. Fringilla blandit in amet condimentum aenean. Ac varius dui porttitor ultricies cursus nec mus cras. Vitae enim malesuada massa sit faucibus est dui dictum ac."}, {type:'text', value: "Fermentum lorem velit est et. Venenatis orci volutpat turpis diam neque dapibus vestibulum et. Pretium id amet at tincidunt sociis eu amet aliquam. Felis leo sed euismod convallis egestas in dolor bibendum integer. Eget amet ut fusce semper. Ipsum nisl fringilla duis duis viverra lectus tempus nibh fames. Dui quis vitae nunc purus. Euismod enim pellentesque scelerisque neque nec amet sagittis leo. In sapien enim leo molestie consectetur maecenas in. Lacus ultricies in quam sit sit scelerisque. Consectetur ut et elementum in senectus vel pulvinar diam venenatis."}, {type: 'text', value: "Proin risus dictum est proin. Praesent ultrices cras ut in. Varius eget eget nisi viverra diam viverra. Cursus rhoncus venenatis euismod cum fermentum tristique ullamcorper eu tincidunt. Justo id scelerisque orci eros congue. Mattis quis sagittis imperdiet tellus. Vestibulum elit semper tortor pulvinar. Massa nunc mattis vivamus dictumst tincidunt quam."}, {type: 'text', value: "Lorem ipsum dolor sit amet consectetur. Elit amet platea semper consequat pharetra. Molestie phasellus elit facilisis a urna lacus eget. Nascetur suspendisse tortor eu et. Nulla dui morbi erat eros venenatis. Et interdum venenatis purus sagittis posuere lobortis. Justo morbi pellentesque aliquam quam aliquet cursus leo. Commodo ac viverra amet suspendisse ultrices. Aliquet vulputate justo donec pellentesque. Sapien faucibus diam sem purus at lorem adipiscing. Aliquam lacus phasellus pellentesque fermentum. Ipsum in semper volutpat viverra in."}, {type:'text', value: "Aliquam nulla consectetur donec sed neque pretium tellus auctor at. Leo euismod ultrices maecenas suspendisse at purus. Integer amet aliquam ut orci. Nibh nunc lectus risus feugiat ac viverra mi. Aliquam nisl praesent vitae magna in sed posuere morbi. Aliquet dignissim nisl dictum sit euismod consectetur tempor vel eu. Fringilla blandit in amet condimentum aenean. Ac varius dui porttitor ultricies cursus nec mus cras. Vitae enim malesuada massa sit faucibus est dui dictum ac."}, {type:'text', value: "Fermentum lorem velit est et. Venenatis orci volutpat turpis diam neque dapibus vestibulum et. Pretium id amet at tincidunt sociis eu amet aliquam. Felis leo sed euismod convallis egestas in dolor bibendum integer. Eget amet ut fusce semper. Ipsum nisl fringilla duis duis viverra lectus tempus nibh fames. Dui quis vitae nunc purus. Euismod enim pellentesque scelerisque neque nec amet sagittis leo. In sapien enim leo molestie consectetur maecenas in. Lacus ultricies in quam sit sit scelerisque. Consectetur ut et elementum in senectus vel pulvinar diam venenatis."}, {type: 'text', value: "Proin risus dictum est proin. Praesent ultrices cras ut in. Varius eget eget nisi viverra diam viverra. Cursus rhoncus venenatis euismod cum fermentum tristique ullamcorper eu tincidunt. Justo id scelerisque orci eros congue. Mattis quis sagittis imperdiet tellus. Vestibulum elit semper tortor pulvinar. Massa nunc mattis vivamus dictumst tincidunt quam."}], 3000, true, 'streaming'); } index++; }); </script>
[component-metadata:sl-ai-agent-panel]
Slots
| Name | Description |
|---|---|
welcome-text
|
A slot for the welcome text shown in the initial “start” state of the panel. |
secondary-text
|
A slot for additional text shown in the initial “start” state of the panel. |
ai-prompt
|
A slot for the AI Prompt component used to submit prompts to the agent. This should be a
<sl-ai-prompt> element.
|
Learn more about using slots.
Properties
| Name | Description | Reflects | Type | Default |
|---|---|---|---|---|
useVirtualizer
use-virtualizer
|
Whether to use the virtualizer for rendering chat messages. This is still experimental and may have issues. |
boolean
|
false
|
|
state
|
The current state of the panel. - “start”: The initial state showing welcome text and prompt. - “chat”: The state showing the chat history and prompt. |
'start' | 'chat'
|
'start'
|
|
agentName
agent-name
|
The name of the AI agent, displayed in the chat bubbles for agent messages. Default is “Agent name”. |
string
|
'Agent name'
|
|
messages
|
The list of chat messages displayed in the panel. This should be controlled by the consumer of the component; the component does not manage this state internally. Each message should have a unique id, a role (“user” or “agent”), an array of content blocks (text or image), a status (“sent”, “pending”, “error”, or “streaming”), and a timestamp. Example message: { id: ‘msg1’, role: ‘agent’, content: [ { type: ‘text’, value: ‘Hello, how can I assist you today?’ }, { type: ‘image’, value: ‘https://example.com/image.png’ } ], status: ‘sent’, timestamp: 1634567890123 } |
ChatMessage[]
|
[]
|
|
typingSpeed
typing-speed
|
The speed (in ms) at which the agent’s text responses are “typed” out when streaming. The smaller the value, the faster the typing effect. Default is 30ms per character. |
number
|
30
|
|
welcomeTextHeader
welcome-text-header
|
The header text for the welcome message. |
string
|
''
|
|
welcomeText
welcome-text
|
The main welcome text. |
string
|
''
|
|
image
|
The image source to use for the avatar. |
string
|
''
|
|
label
|
A label to use to describe the avatar to assistive devices. |
string
|
''
|
|
initials
|
Initials to use as a fallback when no image is available (1–2 characters max recommended). |
string
|
''
|
|
loading
|
Indicates how the browser should load the image. |
'eager' | 'lazy'
|
'eager'
|
|
shape
|
The shape of the avatar. |
'circle' | 'square' | 'rounded'
|
'circle'
|
|
chatContainer
|
The container that holds the chat messages. |
HTMLElement
|
- | |
prompt
|
The prompt component used to submit prompts to the agent. This should be a
<sl-ai-prompt> element.
|
HTMLElement
|
- | |
updateComplete |
A read-only promise that resolves when the component has finished updating. |
Learn more about attributes and properties.
Events
| Name | React Event | Description | Event Detail |
|---|---|---|---|
sl-typing-end |
|
Emitted when the agent finishes “typing” a streamed response. The event detail includes the messageId of the message that finished typing. | - |
Learn more about events.
Custom Properties
| Name | Description | Default |
|---|---|---|
--chat-bubble-max-width |
The maximum width of chat bubbles in the panel. Default is 100%. | |
--virtual-item-width |
The width of the chat box when using virtualizer. Default is 990px. |
Learn more about customizing CSS custom properties.
Parts
| Name | Description |
|---|---|
base |
The component’s base wrapper. |
welcome-container |
The container for the welcome text and prompt in the “start” state. |
secondary-text |
The container for the secondary text in the “start” state. |
chat-container |
The container that holds the chat messages. |
user-avatar |
The container that holds the user’s avatar. |
Learn more about customizing CSS parts.
Dependencies
This component automatically imports the following dependencies.
<sl-ai-prompt><sl-avatar><sl-icon><sl-icon-button><sl-textarea>