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 { PromptInput } from '@dnb/eufemia/extensions/ai'
import '@dnb/eufemia/extensions/ai/style'
render(<PromptInput />)

Description#

Ai.PromptInput is where the user writes a message. On desktop, Enter sends the message and Shift+Enter adds a new line. On touch-primary devices (hover: none and pointer: coarse), Enter adds a new line; use the send button to submit. Set isBusy while a reply is pending or streaming. With onStop, the send button becomes a stop button. Pressing empty space on the card focuses the textarea with the caret at the end. Custom controls and context content keep their own interactions. After sending, focus returns to the textarea. Stopping also returns keyboard focus when the empty compact input removes the stop button.

Use variant="compact" for a single line on a welcome screen. It shows the Aino icon by default and hides the send button until the user enters text. While busy, the stop button stays available even when the input is empty. Use icon to change the leading icon or icon={null} to hide it.

The default variant is flat and gains a shadow when the textarea is focused. The card uses Textarea hover and focus border colors and thickness; its toolbar buttons retain their own focus styles. The compact variant has a shadow by default; set dropShadow={false} for an inline surface or dropShadow to elevate the default variant. Keyboard focus remains visible in both variants.

With disabled, the card uses Textarea's disabled background and border tokens. Set textareaProps.status for error feedback; the card keeps its error border while the textarea is focused.

Pass context chips as children to show attachments or references above the input. The context area opens and closes with HeightAnimation, including its spacing. Your application owns their data and removal actions. Use controls with type="button" so removing context does not submit the message.

Composing custom controls#

Use startActions to place controls on the left of the toolbar, or before the textarea in the compact variant. Use endActions to place controls before the built-in Send or Stop button. Both accept React content, including multiple buttons, a menu or a custom component. Their DOM order follows the visual order for keyboard navigation.

Compose actions with Button, Dropdown or your own controls. PromptInput does not provide attachment or microphone buttons because their behavior and labels belong to the application. For icon-only buttons, provide a translated aria-label and a tooltip. Use type="button" on custom buttons to avoid submitting the message.

disabled applies to the textarea and built-in Send or Stop button. Pass the same disabled state to custom controls when they should also be unavailable. PromptInput does not change the properties of custom content. Keep controls concise in the compact variant; use the default variant or move additional actions into a menu when space is limited.

Demos#

Prompt input#

On desktop, Enter sends the message and Shift+Enter adds a new line. On touch-primary devices, Enter adds a new line and the send button submits.

Du har brukt 0 av 111 tegn.

<PromptInput characterCounter={111} />

Compact prompt input#

<PromptInput variant="compact" placeholder="Ask Aino" />

Inline compact prompt input#

Use dropShadow={false} for the flat inline surface.

<PromptInput
  variant="compact"
  dropShadow={false}
  placeholder="Ask Aino"
/>

Custom actions#

Compose attachment and microphone buttons with startActions and endActions. Here, attachment adds an example document and microphone inserts example dictated text. Connect these controls to your application's file picker and speech recognition.

const CustomActions = () => {
  const [value, setValue] = useState('')
  const [hasAttachment, setHasAttachment] = useState(false)
  return (
    <PromptInput
      variant={variant}
      icon={null}
      value={value}
      onChange={({ value }) => setValue(value)}
      onSubmit={() => setValue('')}
      startActions={
        <Button
          type="button"
          variant="tertiary"
          bounding
          icon={add}
          iconSize="default"
          aria-label="Add attachment"
          tooltip="Add attachment"
          disabled={hasAttachment}
          onClick={() => setHasAttachment(true)}
        />
      }
      endActions={
        <Button
          type="button"
          variant="tertiary"
          bounding
          icon={microphone}
          iconSize="default"
          aria-label="Use microphone"
          tooltip="Use microphone"
          onClick={() => setValue('Show my latest transactions')}
        />
      }
    >
      {hasAttachment && (
        <Button
          type="button"
          variant="tertiary"
          bounding
          icon={close}
          iconSize="default"
          text="Budget.pdf"
          aria-label="Remove Budget.pdf"
          onClick={() => setHasAttachment(false)}
        />
      )}
    </PromptInput>
  )
}
render(<CustomActions />)
const CustomActions = () => {
  const [value, setValue] = useState('')
  const [hasAttachment, setHasAttachment] = useState(false)
  return (
    <PromptInput
      variant={variant}
      icon={null}
      value={value}
      onChange={({ value }) => setValue(value)}
      onSubmit={() => setValue('')}
      startActions={
        <Button
          type="button"
          variant="tertiary"
          bounding
          icon={add}
          iconSize="default"
          aria-label="Add attachment"
          tooltip="Add attachment"
          disabled={hasAttachment}
          onClick={() => setHasAttachment(true)}
        />
      }
      endActions={
        <Button
          type="button"
          variant="tertiary"
          bounding
          icon={microphone}
          iconSize="default"
          aria-label="Use microphone"
          tooltip="Use microphone"
          onClick={() => setValue('Show my latest transactions')}
        />
      }
    >
      {hasAttachment && (
        <Button
          type="button"
          variant="tertiary"
          bounding
          icon={close}
          iconSize="default"
          text="Budget.pdf"
          aria-label="Remove Budget.pdf"
          onClick={() => setHasAttachment(false)}
        />
      )}
    </PromptInput>
  )
}
render(<CustomActions />)

Prompt input with context#

Pass your context as children. The application owns the reference and its removal. The custom action adds it again after removal.

dnb.no/...Insurance
const ContextChip = styled.div`
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.5rem;
  background: var(--token-color-background-neutral-subtle);
  border: 0.0625rem solid var(--token-color-stroke-neutral-subtle);
  border-radius: 0.5rem;
`
const InputWithContext = () => {
  const [hasReference, setHasReference] = useState(true)
  const [value, setValue] = useState(
    'Add the companies from the attached document to my watchlist.'
  )
  return (
    <PromptInput
      dropShadow
      value={value}
      onChange={({ value }) => setValue(value)}
      onSubmit={() => setValue('')}
      startActions={
        <Button
          type="button"
          variant="tertiary"
          size="small"
          icon={add}
          iconSize="default"
          aria-label="Add reference"
          tooltip="Add reference"
          disabled={hasReference}
          onClick={() => setHasReference(true)}
        />
      }
    >
      {hasReference && (
        <ContextChip>
          <Avatar size="small" src={referenceAvatar} aria-hidden />
          <span>dnb.no/...Insurance</span>
          <Button
            type="button"
            variant="tertiary"
            size="small"
            icon={close}
            iconSize="default"
            aria-label="Remove reference"
            onClick={() => setHasReference(false)}
          />
        </ContextChip>
      )}
    </PromptInput>
  )
}
render(<InputWithContext />)

Stop while streaming#

Set isBusy and pass onStop to show a stop button while the reply streams in.

const Chat = () => {
  const [isBusy, setIsBusy] = useState(false)
  useEffect(() => {
    if (!isBusy) {
      return // stop here
    }

    const timeout = setTimeout(() => setIsBusy(false), 4000)
    return () => clearTimeout(timeout)
  }, [isBusy])
  return (
    <PromptInput
      variant={variant}
      isBusy={isBusy}
      onSubmit={() => setIsBusy(true)}
      onStop={() => setIsBusy(false)}
    />
  )
}
render(<Chat />)
const Chat = () => {
  const [isBusy, setIsBusy] = useState(false)
  useEffect(() => {
    if (!isBusy) {
      return // stop here
    }

    const timeout = setTimeout(() => setIsBusy(false), 4000)
    return () => clearTimeout(timeout)
  }, [isBusy])
  return (
    <PromptInput
      variant={variant}
      isBusy={isBusy}
      onSubmit={() => setIsBusy(true)}
      onStop={() => setIsBusy(false)}
    />
  )
}
render(<Chat />)

Prompt input with an error#

The message could not be sent. Try again.
<PromptInput
  textareaProps={{
    status: 'The message could not be sent. Try again.',
  }}
/>

Disabled prompt input#

<PromptInput
  disabled
  value="Message unavailable"
  startActions={
    <Button
      type="button"
      variant="tertiary"
      size="small"
      text="Add reference"
      disabled
    />
  }
/>
Suggest an edit