Skip to main content

Ai Agent Panel

<sl-ai-agent-panel> | SlAiAgentPanel
Since 1.1.14 experimental

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.

myhub Scout Heffron
<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

What can we get started on today Linda?
myhub Scout Heffron
<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>