Skip to content

Experimental: The AI extension and its component APIs may change in the future. Please give us feedback on your experience with them.

Import#

import {
Conversation,
Message,
PromptInput,
Response,
} from '@dnb/eufemia/extensions/ai'
import '@dnb/eufemia/extensions/ai/style'

Import the components and hooks you use by name. The Ai. prefix in the documentation identifies the extension; the examples use named imports directly.

Isolated styles#

For applications using style isolation, replace the regular AI style import with the isolated entry point:

import '@dnb/eufemia/style/isolated'
import '@dnb/eufemia/extensions/ai/style/isolated'
import { IsolatedStyleScope } from '@dnb/eufemia/shared'
import { Response } from '@dnb/eufemia/extensions/ai'
render(
<IsolatedStyleScope>
<Response>{'Hello from the assistant.'}</Response>
</IsolatedStyleScope>
)

The AI entry point adds only the extension styles. Keep the isolated basis and theme imports for the Eufemia components used inside it.

Description#

Ai provides building blocks for AI chat experiences, built with Eufemia components. Use them to build a chat with an assistant, from the welcome screen with suggestions to the conversation and the prompt input.

Usage#

Your application owns the messages, transport, tool execution and error handling. Eufemia renders the content and handles presentation, keyboard interaction and conversation scrolling.

The message type below belongs to this example. Use your application's data and map it to the component props.

import { useId } from 'react'
import {
Conversation,
Message,
MessageIndicator,
PromptInput,
Response,
} from '@dnb/eufemia/extensions/ai'
import { GlobalStatus } from '@dnb/eufemia/components'
import '@dnb/eufemia/extensions/ai/style'
type ChatMessage = {
id: string
from: 'user' | 'assistant'
text: string
isStreaming?: boolean
}
type ChatProps = {
messages: Array<ChatMessage>
isBusy: boolean
isWaiting: boolean
hasError: boolean
onSend: (text: string) => void
onStop: () => void
}
export function Chat({
messages,
isBusy,
isWaiting,
hasError,
onSend,
onStop,
}: ChatProps) {
const prefix = useId()
return (
<>
<Conversation
label="Chat with the assistant"
style={{ height: '32rem' }}
>
{messages.map((message) => (
<Message
key={message.id}
id={prefix + message.id}
from={message.from}
>
{message.from === 'user' ? (
message.text
) : (
<Response
parseIncompleteMarkdown={message.isStreaming ?? false}
>
{message.text}
</Response>
)}
</Message>
))}
{isWaiting && <MessageIndicator />}
</Conversation>
{hasError && (
<GlobalStatus
show
autoScroll={false}
omitSetFocus
id={prefix + '-error'}
text="The reply could not be completed. Try again."
/>
)}
<PromptInput
isBusy={isBusy}
onSubmit={({ value }) => onSend(value)}
onStop={onStop}
/>
</>
)
}

Compose tool results and sources inside each message using Ai.Task and Ai.Sources. See Integrations for complete framework examples and ownership of each state.

Use Ai.AgentAction for compact agent actions with progress, a result, and optional Undo. The application controls the action state and reversal window.

Use Ai.Confirmation to ask the user to confirm or decline before proceeding with a proposed action.

Guides#

For streamed-reply announcements, follow the application-owned announcement recipe.

  • Translations: select a locale and customize component labels.

  • Integrations: composition and recipes for AI SDK and TanStack AI.

  • Message scroller: how the conversation scrolls while replies stream in.

Components#

Ai.Actions#

Shows buttons for a message, like copy or feedback.

Ai.ActivityText#

Describes what the assistant is doing, such as thinking or searching.

Ai.AgentAction#

Shows an agent action progressing, its result, and an optional Undo.

Ai.Confirmation#

Asks the user to confirm or decline a proposed action.

Ai.Conversation#

Holds the messages of a chat and keeps the latest message in view while the reply streams in.

Ai.DateMarker#

Shows the date above the messages of that day.

Ai.Info#

Shows supporting information about a conversation.

Ai.Message#

Shows a message from the user or the assistant, as a bubble or as plain content.

Ai.MessageIndicator#

Indicates that an assistant message is being prepared.

Ai.PromptInput#

Where the user writes a message, with a send button that becomes a stop button while the reply streams in.

Ai.Response#

Renders streamed markdown from an AI model with Eufemia elements.

Ai.Sources#

Shows a collapsible list of the sources used in a reply.

Ai.Suggestions#

Shows prompts the user can choose, as chips or cards.

Ai.Task#

Shows work in progress and its outcome, with optional details or results.

Ai.Welcome#

Greets the user before the conversation starts.

Relevant links#

Examples#

Full chat experience#

A complete chat with an assistant. Choose a suggestion or write a message, e.g. "Block my card" or "Compare my spending", to see the tools, answer and sources stream in. Press stop to end the reply, or regenerate the last answer.

The demo uses scripted replies and local chat state. Connect your own messages, status, send, stop and regenerate actions to the components in your app.

New chat opens an empty conversation and clears any draft or pending reply. The demo content and component labels are in English. See Translations for changing component labels; your application supplies translated chat content.

Aino

DNB Banking Assistant

Hello, Peter

Where do you want to start today?

You have used 0 of 200 characters.

const primarySuggestions = [
  {
    icon: card,
    prompt: 'Block my Visa card',
  },
  {
    icon: bank,
    prompt: 'Compare my spending with last month',
  },
  {
    icon: transfer,
    prompt: 'Show my latest transactions',
  },
]
const suggestionChips = [
  {
    icon: loan,
    label: 'loan offers',
    prompt: 'What loan offers do you have?',
  },
  {
    label: 'what can you do?',
    prompt: 'What can you help me with?',
  },
]
const ainoAvatar = <Avatar size="small" icon={aino} hasLabel />
const BankingAssistant = () => {
  const [chatStarted, setChatStarted] = useState(false)
  const [draft, setDraft] = useState('')
  const promptRef = useRef<HTMLTextAreaElement>(null)
  const [feedback, setFeedback] = useState({})
  const { messages, status, sendMessage, stop, regenerate, setMessages } =
    useChatSimulation()
  const send = (text) =>
    sendMessage({
      text,
    })
  const questions = messages.filter(({ role }) => role === 'user')
  return (
    <Provider locale="en-GB">
      <ChatLayout>
        <section className="chat" style={chatStyle}>
          <Flex.Horizontal align="center" justify="space-between">
            <Flex.Horizontal align="center" gap="small">
              {ainoAvatar}
              <div>
                <strong>Aino</strong>
                <P size="small">DNB Banking Assistant</P>
              </div>
            </Flex.Horizontal>

            <Button
              variant="tertiary"
              icon={add}
              iconPosition="left"
              onClick={() => {
                setMessages([])
                setFeedback({})
                setDraft('')
                setChatStarted(true)
                promptRef.current?.focus()
              }}
            >
              New chat
            </Button>
          </Flex.Horizontal>

          {!chatStarted && messages.length === 0 ? (
            <Welcome
              greeting="Hello, Peter"
              avatar={<Avatar size="small" icon={aino} hasLabel />}
              title="Where do you want to start today?"
              style={{
                margin: 'auto 0',
              }}
            >
              <Suggestions>
                {primarySuggestions.map(({ icon, prompt }) => (
                  <Suggestion
                    key={prompt}
                    icon={icon}
                    suggestion={prompt}
                    onClick={({ suggestion }) => {
                      send(suggestion)
                      promptRef.current?.focus()
                    }}
                  />
                ))}
              </Suggestions>
              <Suggestions>
                {suggestionChips.map(({ icon, label, prompt }) => (
                  <Suggestion
                    key={label}
                    icon={icon}
                    suggestion={prompt}
                    onClick={({ suggestion }) => {
                      send(suggestion)
                      promptRef.current?.focus()
                    }}
                  >
                    {label}
                  </Suggestion>
                ))}
              </Suggestions>
            </Welcome>
          ) : (
            <Conversation
              id="banking-chat"
              aria-live="off"
              scrollBehavior="end"
              style={{
                flex: '1 1 auto',
              }}
            >
              <DateMarker date={new Date()} />
              <Info>
                <P>
                  Aino is a chatbot for customer service. Do not share
                  personal information with the chatbot. Read more about{' '}
                  <Anchor href="https://www.dnb.no/personvern">
                    your privacy
                  </Anchor>
                  .
                </P>
                <P top>
                  Some answers are generated with artificial intelligence
                  and are marked with a tag. AI-generated answers can
                  contain errors.
                </P>
              </Info>

              {messages.map((message, index) => {
                const isAssistant = message.role === 'assistant'
                const isLast = index === messages.length - 1
                const isDone = !(isLast && status === 'streaming')
                const transactions = getTransactions(message)
                const actions = isAssistant && isDone && (
                  <Actions>
                    <CopyOnClick.Button
                      size="small"
                      iconSize="default"
                      bounding
                      copyContent={getText(message)}
                      title="Copy response"
                      tooltip="Copy response"
                    />
                    <Action
                      icon={thumbs_up}
                      label="Good answer"
                      selected={feedback[message.id] === true}
                      onClick={() =>
                        setFeedback((all) => ({
                          ...all,
                          [message.id]:
                            all[message.id] === true ? undefined : true,
                        }))
                      }
                    />
                    <Action
                      icon={thumbs_down}
                      label="Bad answer"
                      selected={feedback[message.id] === false}
                      onClick={() =>
                        setFeedback((all) => ({
                          ...all,
                          [message.id]:
                            all[message.id] === false ? undefined : false,
                        }))
                      }
                    />
                    {isLast && (
                      <Action
                        icon={refresh}
                        label="Regenerate"
                        onClick={() => {
                          regenerate()
                          promptRef.current?.focus()
                        }}
                      />
                    )}
                  </Actions>
                )
                return (
                  <Fragment key={message.id}>
                    <Message
                      id={message.id}
                      from={message.role}
                      aria-label={isAssistant ? 'Aino' : 'You'}
                      grouped={
                        isAssistant &&
                        messages[index - 1]?.role === 'assistant'
                      }
                      avatar={isAssistant ? ainoAvatar : undefined}
                      aiGenerated={isAssistant}
                      actions={transactions ? undefined : actions}
                    >
                      <DemoMessageContent message={message} />
                      {message.stopped && (
                        <P size="small">Reply stopped.</P>
                      )}
                    </Message>

                    {transactions && (
                      <Message variant="plain" actions={actions}>
                        <List.Container>
                          {transactions.map(
                            ({ name, account, date, amount }) => (
                              <List.Item.Action
                                key={name}
                                icon={amount < 0 ? transfer : pay_from}
                                onClick={() =>
                                  send(
                                    `Show the payment confirmation for ${name}`
                                  )
                                }
                              >
                                <List.Cell.Title>
                                  <List.Cell.Title.Overline>
                                    {account} · {date}
                                  </List.Cell.Title.Overline>
                                  {name}
                                </List.Cell.Title>
                                <List.Cell.End>
                                  <NumberFormat.Currency value={amount} />
                                </List.Cell.End>
                              </List.Item.Action>
                            )
                          )}
                        </List.Container>
                      </Message>
                    )}
                  </Fragment>
                )
              })}

              {status === 'submitted' && (
                <MessageIndicator name="Aino" avatar={ainoAvatar} />
              )}
            </Conversation>
          )}
          <ReplyAnnouncement
            isBusy={status !== 'ready'}
            messageId={
              messages.at(-1)?.role === 'assistant'
                ? messages.at(-1).id
                : undefined
            }
            outcome={
              messages.at(-1)?.stopped ? 'Reply stopped.' : undefined
            }
          />

          <PromptInput
            value={draft}
            textareaProps={{
              ref: promptRef,
            }}
            onChange={({ value }) => setDraft(value)}
            isBusy={status === 'submitted' || status === 'streaming'}
            characterCounter={200}
            onSubmit={({ value }) => {
              send(value)
              setDraft('')
            }}
            onStop={stop}
          />
        </section>

        {questions.length > 1 && (
          <QuestionOverview questions={questions} />
        )}
      </ChatLayout>
    </Provider>
  )
}

// Jump between the questions, and see which one you read
const QuestionOverview = ({ questions }) => {
  const { scrollToMessage } = useConversation('banking-chat')
  const { currentTurnId } = useConversationVisibility('banking-chat')
  return (
    <nav aria-label="Questions in this chat">
      <P bottom="x-small">
        <strong>Questions</strong>
      </P>
      <List.Container>
        {questions.map((question) => (
          <List.Item.Action
            key={question.id}
            selected={question.id === currentTurnId}
            aria-current={
              question.id === currentTurnId ? 'step' : undefined
            }
            onClick={() => scrollToMessage(question.id)}
          >
            <List.Cell.Title>{getText(question)}</List.Cell.Title>
          </List.Item.Action>
        ))}
      </List.Container>
    </nav>
  )
}
render(<BankingAssistant />)

Chat window and customer-service handover#

Compose a chat window with Dialog for its keyboard focus, close controls and responsive modal behavior. The input uses the large radius token inside the window. Suggestions sit above the input; the application owns sending them. A customer-service reply uses the assistant side with a headset avatar and no AI-generated label.

<Dialog
  restoreFocusTo={() => document.getElementById('ai-chat-window-trigger')}
  trigger={(props) => (
    <Button {...props} id="ai-chat-window-trigger">
      Open chat window
    </Button>
  )}
  minWidth="23rem"
  maxWidth="23rem"
  title="Chat with DNB"
>
  <Flex.Stack
    gap="small"
    style={{
      height: 'min(38rem, 65dvh)',
      minHeight: '18rem',
    }}
  >
    <Conversation
      scrollBehavior="end"
      aria-live="off"
      style={{
        flex: '1 1 auto',
      }}
    >
      <Message from="user">Can I speak to a person?</Message>
      <Message
        aria-label="Aino"
        avatar={<Avatar size="small" icon={aino} hasLabel />}
      >
        I will connect you to customer service.
      </Message>
      <Message
        aria-label="Maria, customer service"
        avatar={<Avatar size="small" icon={customer_service} hasLabel />}
      >
        Hello, I am Maria from DNB. How can I help?
      </Message>
    </Conversation>
    <Suggestions aria-label="Suggested replies">
      <Suggestion suggestion="Help with a payment" />
      <Suggestion suggestion="Help with my card" />
    </Suggestions>
    <PromptInput
      style={
        {
          '--ai-prompt-input-radius': 'var(--token-radius-lg)',
        } as React.CSSProperties
      }
    />
  </Flex.Stack>
</Dialog>

Streamed reply announcements#

The application keeps a separate AriaLive region mounted and updates it once when the reply ends, including when Stop preserves a partial answer. The conversation's automatic live announcements are disabled to avoid duplicates. See Screen reader announcements for the application helper and failure handling.

const Chat = () => {
  const { messages, status, sendMessage, stop } = useChatSimulation()
  const isBusy = status !== 'ready'
  const reply = messages.at(-1)
  return (
    <>
      <Conversation
        aria-live="off"
        style={{
          height: '18rem',
        }}
      >
        {messages.map((message) => (
          <Message key={message.id} id={message.id} from={message.role}>
            <DemoMessageContent message={message} />
            {message.stopped && <P size="small">Reply stopped.</P>}
          </Message>
        ))}
      </Conversation>
      <ReplyAnnouncement
        isBusy={isBusy}
        messageId={reply?.role === 'assistant' ? reply.id : undefined}
        outcome={reply?.stopped ? 'Reply stopped.' : undefined}
      />
      <PromptInput
        isBusy={isBusy}
        onSubmit={({ value }) => {
          sendMessage({
            text: value,
          })
        }}
        onStop={() => {
          stop()
        }}
      />
    </>
  )
}
render(<Chat />)

Chat with compact input#

Hello, Peter

Where do you want to start today?

<Welcome
  greeting="Hello, Peter"
  avatar={<Avatar size="small" icon={aino} hasLabel />}
  title="Where do you want to start today?"
>
  <Suggestions>
    <Suggestion icon={bank} suggestion="Explain my spending this month" />
    <Suggestion icon={bank} suggestion="Show my savings goals" />
    <Suggestion icon={bank} suggestion="When is my next bill due?" />
  </Suggestions>
  <PromptInput variant="compact" placeholder="Start typing here" />
</Welcome>

Chat with content in the conversation#

User messages are placed on the right. Content like a list can be shown without a bubble by using variant="plain".

You - 15:23
Hi, I need the payment confirmation for a transfer I made from my salary account 12.02
Aino - 15:24
Hi, it seems you made 2 transfers 12.02. I have provided both for you below. Just click the correct one and you will be able to get to the payment confirmation.
  • (xxxx.xx.xxxxx)
    Intro Aksel
  • (xxxx.xx.xxxxx)
    Kim Olsen

Du har brukt 0 av 111 tegn.

<div style={chatStyle}>
  <Conversation
    style={{
      flex: '1 1 auto',
    }}
  >
    <Message
      from="user"
      name="You"
      timestamp="15:23"
      avatar={<Avatar hasLabel>P</Avatar>}
    >
      Hi, I need the payment confirmation for a transfer I made from my
      salary account 12.02
    </Message>

    <Message
      name="Aino"
      timestamp="15:24"
      avatar={
        <Avatar variant="secondary" hasLabel>
          A
        </Avatar>
      }
    >
      Hi, it seems you made 2 transfers 12.02. I have provided both for you
      below. Just click the correct one and you will be able to get to the
      payment confirmation.
    </Message>

    <Message variant="plain">
      <List.Container>
        <List.Item.Action icon={transfer}>
          <List.Cell.Title>
            <List.Cell.Title.Overline>
              (xxxx.xx.xxxxx)
            </List.Cell.Title.Overline>
            Intro Aksel
          </List.Cell.Title>
          <List.Cell.End>
            <NumberFormat.Currency value={-888888} />
          </List.Cell.End>
        </List.Item.Action>
        <List.Item.Action icon={pay_from}>
          <List.Cell.Title>
            <List.Cell.Title.Overline>
              (xxxx.xx.xxxxx)
            </List.Cell.Title.Overline>
            Kim Olsen
          </List.Cell.Title>
          <List.Cell.End>
            <NumberFormat.Currency value={-888888} />
          </List.Cell.End>
        </List.Item.Action>
      </List.Container>
    </Message>
  </Conversation>

  <PromptInput characterCounter={111} />
</div>
Suggest an edit