aiMessageList

Компонент aiMessageList показывает диалог в чате. Он отображает сообщения пользователя как обычный текст в пузырьке (bubble), а сообщения ассистента — как Markdown. Компонент поддерживает действия над отдельными сообщениями, индикатор размышления и баннер ошибки.

XML-элемент

aiMessageList

Java-класс

AiMessageList

Компонент aiChat встраивает его и управляет им автоматически. Используйте его отдельно, чтобы показать сохранённый диалог или собрать собственный чат из базовых компонентов. Приложение может полностью управлять списком само, как описано в разделе Управление генерацией.

Диалог в списке сообщений

Основы

Объявите пространство имён aichat в XML-дескрипторе экрана:

<view xmlns="http://jmix.io/schema/flowui/view"
      xmlns:aichat="http://jmix.io/schema/aichat/ui"
      title="msg://MessageListView.title">
Studio добавляет пространство имён автоматически, когда вы добавляете компонент с помощью действия Add Component на верхней панели действий. См. Палитра компонентов.

Списку не нужно ничего, кроме его элемента:

<aichat:aiMessageList id="transcriptList" width="100%"/>

Сообщения

Сообщения — это элементы, создаваемые фабричными методами AiMessageListItem, а не компоненты, добавляемые в список:

transcriptList.setItems(
        AiMessageListItem.user("How do I load orders for a customer?"),
        AiMessageListItem.assistant("""
                ## Loading orders

                Use `DataManager` with a JPQL query:

                ```java
                List<Order> orders = dataManager.load(Order.class)
                        .query("select o from Order o where o.customer = :customer")
                        .parameter("customer", customer)
                        .list();
                ```

                A few things to keep in mind:

                - add a fetch plan when you need referenced entities
                - use `maxResults` to page the result
                """),
        AiMessageListItem.user(LONG_QUESTION)
);

Сообщения ассистента отображаются как Markdown: заголовки, списки, таблицы, ссылки и встроенный код. Блок кода становится настоящим компонентом aiCodeBlock с меткой языка, подсветкой и собственными действиями копирования и переноса строк. Сообщения пользователя отображаются как обычный текст в пузырьке, поэтому Markdown в сообщении пользователя показывается так, как он был введён.

Метод addItem добавляет одно сообщение в конец списка, который уже отображается на экране:

@Subscribe("addItemButton")
public void onAddItemButtonClick(final ClickEvent<JmixButton> event) {
    transcriptList.addItem(AiMessageListItem.user("And how do I page the result?"));
}
Экземпляр элемента принадлежит одному списку и может присутствовать в нём только один раз. Повторное добавление того же экземпляра или добавление его во второй список отклоняется.

Управление генерацией

Приложение может выполнить весь ход диалога непосредственно со списком, без aiChat. Последовательность такая: добавить сообщение пользователя, добавить пустое сообщение ассистента для потоковой передачи ответа, переключить список в состояние GENERATING, добавлять ответ по мере его поступления, а затем вернуть список в состояние IDLE.

@Subscribe("generateButton")
public void onGenerateButtonClick(final ClickEvent<JmixButton> event) {
    drivenList.addItem(AiMessageListItem.user("What is a fetch plan?"));

    AiMessageListItem answer = AiMessageListItem.assistant("");
    drivenList.addItem(answer);
    drivenList.setState(GenerationState.GENERATING);

    backgroundWorker.handle(new AnswerTask(answer)).execute();
}

private class AnswerTask extends BackgroundTask<String, Void> {

    private final AiMessageListItem answer;

    private AnswerTask(AiMessageListItem answer) {
        super(30, TimeUnit.SECONDS, MessageListView.this);
        this.answer = answer;
    }

    @Override
    public Void run(TaskLifeCycle<String> taskLifeCycle) throws Exception {
        ThinkingStatusPublisher statuses = drivenList.getThinkingStatusPublisher();

        ThinkingStatusItem searching = statuses.start("Searching the documentation");
        Thread.sleep(1200);
        searching.complete("4 pages");

        ThinkingStatusItem writing = statuses.start("Writing the answer");

        for (String chunk : ANSWER_CHUNKS) {
            Thread.sleep(400);
            taskLifeCycle.publish(chunk);
        }

        writing.complete();
        return null;
    }

    @Override
    public void progress(List<String> chunks) { (1)
        chunks.forEach(answer::appendText);
    }

    @Override
    public void done(Void result) {
        drivenList.setState(GenerationState.IDLE);
        drivenList.getThinkingStatusPublisher().clear();
    }
}
1 progress выполняется в потоке UI, что и требуется для appendText. Фрагменты ответа публикуются из фоновой задачи и добавляются здесь.

Для двух частей хода диалога действуют разные правила работы с потоками.

Методы appendText, addItem и setState должны вызываться в потоке UI. Прямой вызов appendText из фонового потока завершается ошибкой о блокировке сессии. В примере выше фрагменты ответа публикуются из фоновой задачи и добавляются в progress, который Jmix выполняет в потоке UI.

Исключение составляет лента статусов размышления, описанная ниже: она сама захватывает блокировку UI, поэтому её можно вызывать из любого потока.

Индикатор размышления привязан к генерируемому сообщению. Если перевести список в состояние GENERATING, когда последним сообщением является сообщение пользователя, ничего не отобразится: ни индикатора, ни ошибки. Сначала добавьте пустое сообщение ассистента, как показано выше, и индикатор появится на нём.

Метод getActiveItem() возвращает это последнее сообщение ассистента, пока список находится в процессе генерации, и null в остальных случаях.

Индикатор размышления

Пока список находится в процессе генерации, а ответ ещё пуст, он показывает анимированный индикатор из трёх пульсирующих точек с надписью рядом:

Встроенный индикатор размышления

Индикатор можно заменить любым компонентом с помощью элемента thinkingIndicator. Заменяется только анимация, надпись рядом остаётся:

<aichat:thinkingIndicator>
    <image id="customIndicator"
           resource="public/images/logo.png"
           alternateText="msg://customIndicator.alternateText"
           width="1.75em"
           height="1.75em"/>
</aichat:thinkingIndicator>
Заменённый индикатор размышления

Чем дольше идёт генерация, тем больше меняется надпись, чтобы медленный ответ не выглядел как зависший. Надпись задаётся элементом thinkingStages. Каждый этап задаёт задержку, после которой его текст заменяет предыдущий:

<aichat:thinkingStages>
    <aichat:thinkingStage delayMillis="600"
                          text="msg://thinkingStage.thinking"/>
    <aichat:thinkingStage delayMillis="4000"
                          text="msg://thinkingStage.stillThinking"/>
</aichat:thinkingStages>

Лента статусов размышления

Лента статусов сообщает, что делает ассистент, по одной строке на каждый шаг, и отображается над индикатором размышления. В каждой строке есть индикатор загрузки, который превращается в галочку, когда шаг завершается:

Лента статусов размышления

Получите из списка объект для публикации статусов, запускайте статус в начале шага и завершайте его в конце шага, при необходимости с кратким результатом:

ThinkingStatusPublisher statuses = drivenList.getThinkingStatusPublisher();

ThinkingStatusItem searching = statuses.start("Searching the documentation");
Thread.sleep(1200);
searching.complete("4 pages");

ThinkingStatusItem writing = statuses.start("Writing the answer");

Этот объект сам захватывает блокировку UI, поэтому эти вызовы безопасно выполнять из фонового потока, как в примере выше, где они выполняются внутри метода run задачи. Статусы временные, поэтому очищайте их по завершении хода диалога.

Ошибка и повтор

Перевод списка в состояние ERROR показывает баннер с кнопкой Retry. Баннер является частью самого компонента, и приложению не нужно задавать для него разметку:

Баннер ошибки
@Subscribe("failButton")
public void onFailButtonClick(final ClickEvent<JmixButton> event) {
    drivenList.addItem(AiMessageListItem.user("Why did that fail?"));
    drivenList.setState(GenerationState.ERROR);
}

private void registerRetryListener() {
    drivenList.addRetryListener(event -> {
        AiMessageListItem userMessage = event.getUserMessage();
        notifications.show(messageBundle.formatMessage("retry.notification",
                userMessage != null ? userMessage.getText() : "-"));
        drivenList.setState(GenerationState.IDLE);
    });
}

Метод getUserMessage() возвращает последнее сообщение пользователя в списке на момент нажатия Retry, а не то сообщение, генерация ответа на которое завершилась ошибкой. Обычно это одно и то же сообщение. Они различаются, если приложение добавило ещё одно сообщение пользователя после ошибки.

Действия над сообщениями

Действия объявляются для каждой роли отдельно и появляются на панели сообщения при наведении указателя или получении фокуса:

Действия над сообщениями
<aichat:aiMessageList id="actionsList" width="100%">
    <aichat:userActions>
        <aichat:action id="userCopy" type="aichat_messageCopy"/>
    </aichat:userActions>
    <aichat:assistantActions>
        <aichat:action id="assistantCopy" type="aichat_messageCopy"/>
        <aichat:action id="reportAction"
                       icon="FLAG"
                       description="msg://reportAction.description"/>
    </aichat:assistantActions>
</aichat:aiMessageList>

Действие aichat_messageCopy встроено в компонент и копирует исходный текст сообщения в буфер обмена. Любое другое действие — это обычное действие Jmix: задайте ему идентификатор, значок и описание и обработайте его в контроллере. Событие содержит сообщение, для которого было вызвано действие:

@Subscribe("actionsList.reportAction")
public void onReportAction(final AiMessageActionPerformedEvent event) {
    AiMessageListItem item = event.getItem();
    notifications.show(messageBundle.formatMessage("reportAction.notification",
            item.getText()));
}

Другой встроенный тип действия, aichat_messageRegenerate, работает только внутри aiChat, который сам назначает себя его целью. Если объявить его в отдельном списке, оно отображается как обычно, но при нажатии выбрасывает исключение.

Длинные сообщения

Длинное сообщение пользователя сворачивается до фиксированной высоты с эффектом затухания и переключателем Show more. Сообщения ассистента никогда не сворачиваются.

Свёрнутое сообщение пользователя

Порог задаётся высотой отображаемого сообщения, а не количеством символов, и сообщение, лишь немного превышающее порог, показывается полностью, а не сворачивается ради нескольких строк. Поэтому один и тот же текст может сворачиваться в узком контейнере и не сворачиваться в широком, так как от ширины зависит перенос строк.

Прокрутка

Пока ответ передаётся в потоковом режиме, список удерживает область просмотра внизу, чтобы новый текст оставался видимым.

Кнопка прокрутки вниз

Прокрутка вверх отключает это: список перестаёт следовать за новым текстом, и содержимое никогда не смещается из-под глаз читателя, сколько бы текста ни поступило за это время. Внизу списка появляется круглая кнопка для возврата; нажатие на неё прокручивает список вниз и снова включает следование. Ручная прокрутка вниз также снова включает его, а программная прокрутка никогда его не отключает.

Клавиши со стрелками перемещают фокус от сообщения к сообщению и прокручивают каждое из них в область видимости, поэтому диалог можно читать без мыши.

Доступность

Установите атрибут announceMessages, чтобы список сообщал о поступающих сообщениях программам чтения с экрана через live-регион ARIA:

<aichat:aiMessageList id="announceList"
                      width="100%"
                      announceMessages="true"/>

Локализация

Собственные надписи списка берутся из пакета сообщений дополнения по ключам aiMessageList.i18n.*: баннер ошибки, кнопка Retry, кнопка прокрутки вниз, действие копирования и переключатель Show more / Show less. Переопределите их в пакете сообщений приложения, чтобы изменить текст во всех списках.

Чтобы изменить текст в одном списке, добавьте вложенный элемент i18n. Он также может содержать вложенный элемент codeBlock, который изменяет текст блоков кода, отображаемых внутри сообщений ассистента этого списка:

XML
<aichat:aiMessageList id="i18nList" width="100%">
    <aichat:i18n showMore="msg://i18nList.showMore"
                 showLess="msg://i18nList.showLess">
        <aichat:codeBlock copy="msg://i18nList.codeCopy"/>
    </aichat:i18n>
</aichat:aiMessageList>
Пакет сообщений
com.company.demo.view.messagelist/i18nList.showMore=Read the whole question
com.company.demo.view.messagelist/i18nList.showLess=Collapse the question
com.company.demo.view.messagelist/i18nList.codeCopy=Copy this snippet

О том, как сочетаются эти два уровня, см. Приоритет.

Стилизация

Список сообщений предоставляет свои внутренние части как shadow parts, к которым можно обращаться с помощью селектора ::part().

Следующие свойства стиля можно использовать в таблицах стилей CSS, чтобы настроить внешний вид компонента.

Имя Описание

--jmix-ai-message-list-max-width

Ширина, в пределах которой центрируется колонка диалога.

--jmix-ai-message-list-padding

Внутренний отступ вокруг диалога.

--jmix-ai-message-list-scrollbar-color

Цвет полосы прокрутки.

--jmix-ai-message-list-scroll-button-icon-size

Размер значка на кнопке прокрутки вниз.

--jmix-ai-message-list-icon-scroll-down

Значок кнопки прокрутки вниз.

--jmix-ai-message-list-icon-error

Значок, отображаемый в баннере ошибки.

--jmix-ai-message-list-icon-retry

Значок кнопки Retry.

--jmix-ai-message-list-icon-check

Значок, отмечающий завершённый статус размышления.

--jmix-ai-message-text-color

Цвет текста сообщения.

--jmix-ai-message-font-size

Размер шрифта текста сообщения.

--jmix-ai-message-line-height

Высота строки текста сообщения.

--jmix-ai-message-padding

Внутренний отступ сообщения.

--jmix-ai-message-success-color

Цвет галочки, подтверждающей копирование.

--jmix-ai-message-action-padding

Внутренний отступ кнопки действия над сообщением.

--jmix-ai-message-action-icon-size

Размер значка действия над сообщением.

--jmix-ai-message-action-border-radius

Радиус скругления углов кнопки действия над сообщением.

--jmix-ai-message-icon-copy

Значок встроенного действия копирования.

--jmix-ai-message-icon-check

Значок, отображаемый при подтверждении копирования.

--jmix-ai-message-icon-regenerate

Значок встроенного действия повторной генерации.

--jmix-ai-message-icon-file

Значок, отображаемый на вложении, которое не является изображением.

--jmix-ai-message-attachment-image-size

Размер миниатюры вложения-изображения.

--jmix-ai-markdown-link-color

Цвет ссылок в сообщении ассистента.

--jmix-ai-user-message-collapsed-max-height

Высота, до которой сворачивается длинное сообщение пользователя. См. Длинные сообщения.

--jmix-ai-user-message-icon-chevron

Значок переключателя Show more.

--jmix-ai-thinking-indicator-color

Цвет индикатора размышления.

--jmix-ai-thinking-indicator-gap

Расстояние между точками индикатора размышления.

--jmix-ai-thinking-indicator-animation-duration

Длительность одной пульсации индикатора размышления.

--jmix-ai-thinking-status-max-height

Высота, до которой увеличивается лента статусов, прежде чем в ней появляется прокрутка. См. Лента статусов размышления.

Свойства значков содержат изображение, применяемое как маска, поэтому переопределение такого свойства заменяет соответствующий значок.

Компоненты стилизованы для обеих тем Jmix, Aura и Lumo, и используют собственные токены темы для рамок фокуса, границ, радиусов скругления и отступов, поэтому у перечисленных выше свойств нет единого значения по умолчанию. Поддерживаются тёмный режим, режим высокой контрастности и режим уменьшенной анимации. Расположение элементов задаётся свойствами, не зависящими от направления, поэтому компоненты следуют направлению текста документа.

Вложения

Сообщения могут содержать файлы, которые отображаются под текстом сообщения как миниатюры (thumbnails) или «чипы». Вложения затрагивают несколько компонентов, поэтому они описаны вместе в разделе Вложения.

Атрибуты

aiMessageList имеет следующий уникальный атрибут:

Имя Описание По умолчанию

announceMessages

Определяет, сообщается ли о поступающих сообщениях программам чтения с экрана. См. Доступность.

false

aiMessageList имеет следующие общие атрибуты:

Обработчики

aiMessageList имеет следующие уникальные обработчики:

Имя Описание

AttachmentClickEvent

Срабатывает, когда пользователь нажимает на вложение в сообщении. Содержит сообщение и файл. См. Скачивание из диалога.

RetryEvent

Срабатывает, когда пользователь нажимает Retry на баннере ошибки. См. Ошибка и повтор.

aiMessageList имеет следующие общие обработчики:

Элементы

Компонент aiMessageList может содержать следующие вложенные элементы: thinkingIndicator, thinkingStages, userActions, assistantActions и i18n.

thinkingIndicator

Элемент thinkingIndicator содержит один компонент, который заменяет встроенный индикатор. См. Индикатор размышления.

thinkingStages

Элемент thinkingStages содержит элементы thinkingStage, каждый из которых имеет обязательные атрибуты delayMillis и text. Они задают меняющуюся надпись рядом с индикатором. См. Индикатор размышления.

userActions, assistantActions

Эти элементы содержат элементы action, доступные для сообщений пользователя и сообщений ассистента соответственно. Элемент action требует атрибут id и принимает атрибуты type, description, classNames и атрибуты значка. См. Действия над сообщениями.

i18n

Элемент i18n изменяет тексты этого списка. Он поддерживает атрибуты error, retry, scrollToBottom, copy, copied, showMore и showLess, каждый из которых принимает ключ сообщения, а также вложенный элемент codeBlock для блоков кода, отображаемых внутри сообщений списка. См. Локализация.