aiMessageList
Компонент aiMessageList показывает диалог в чате. Он отображает сообщения пользователя как обычный текст в пузырьке (bubble), а сообщения ассистента — как Markdown. Компонент поддерживает действия над отдельными сообщениями, индикатор размышления и баннер ошибки.
XML-элемент |
|
|---|---|
Java-класс |
|
Компонент 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. Фрагменты ответа публикуются из фоновой задачи и добавляются здесь. |
|
Для двух частей хода диалога действуют разные правила работы с потоками. Методы Исключение составляет лента статусов размышления, описанная ниже: она сама захватывает блокировку UI, поэтому её можно вызывать из любого потока. |
|
Индикатор размышления привязан к генерируемому сообщению. Если перевести список в состояние |
Метод 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);
});
}
|
Метод |
Действия над сообщениями
Действия объявляются для каждой роли отдельно и появляются на панели сообщения при наведении указателя или получении фокуса:
<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()));
}
|
Другой встроенный тип действия, |
Длинные сообщения
Длинное сообщение пользователя сворачивается до фиксированной высоты с эффектом затухания и переключателем Show more. Сообщения ассистента никогда не сворачиваются.
Порог задаётся высотой отображаемого сообщения, а не количеством символов, и сообщение, лишь немного превышающее порог, показывается полностью, а не сворачивается ради нескольких строк. Поэтому один и тот же текст может сворачиваться в узком контейнере и не сворачиваться в широком, так как от ширины зависит перенос строк.
Прокрутка
Пока ответ передаётся в потоковом режиме, список удерживает область просмотра внизу, чтобы новый текст оставался видимым.
Прокрутка вверх отключает это: список перестаёт следовать за новым текстом, и содержимое никогда не смещается из-под глаз читателя, сколько бы текста ни поступило за это время. Внизу списка появляется круглая кнопка для возврата; нажатие на неё прокручивает список вниз и снова включает следование. Ручная прокрутка вниз также снова включает его, а программная прокрутка никогда его не отключает.
Клавиши со стрелками перемещают фокус от сообщения к сообщению и прокручивают каждое из них в область видимости, поэтому диалог можно читать без мыши.
Доступность
Установите атрибут announceMessages, чтобы список сообщал о поступающих сообщениях программам чтения с экрана через live-регион ARIA:
<aichat:aiMessageList id="announceList"
width="100%"
announceMessages="true"/>
Локализация
Собственные надписи списка берутся из пакета сообщений дополнения по ключам aiMessageList.i18n.*: баннер ошибки, кнопка Retry, кнопка прокрутки вниз, действие копирования и переключатель Show more / Show less. Переопределите их в пакете сообщений приложения, чтобы изменить текст во всех списках.
Чтобы изменить текст в одном списке, добавьте вложенный элемент i18n. Он также может содержать вложенный элемент codeBlock, который изменяет текст блоков кода, отображаемых внутри сообщений ассистента этого списка:
<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, чтобы настроить внешний вид компонента.
| Имя | Описание |
|---|---|
|
Ширина, в пределах которой центрируется колонка диалога. |
|
Внутренний отступ вокруг диалога. |
|
Цвет полосы прокрутки. |
|
Размер значка на кнопке прокрутки вниз. |
|
Значок кнопки прокрутки вниз. |
|
Значок, отображаемый в баннере ошибки. |
|
Значок кнопки Retry. |
|
Значок, отмечающий завершённый статус размышления. |
|
Цвет текста сообщения. |
|
Размер шрифта текста сообщения. |
|
Высота строки текста сообщения. |
|
Внутренний отступ сообщения. |
|
Цвет галочки, подтверждающей копирование. |
|
Внутренний отступ кнопки действия над сообщением. |
|
Размер значка действия над сообщением. |
|
Радиус скругления углов кнопки действия над сообщением. |
|
Значок встроенного действия копирования. |
|
Значок, отображаемый при подтверждении копирования. |
|
Значок встроенного действия повторной генерации. |
|
Значок, отображаемый на вложении, которое не является изображением. |
|
Размер миниатюры вложения-изображения. |
|
Цвет ссылок в сообщении ассистента. |
|
Высота, до которой сворачивается длинное сообщение пользователя. См. Длинные сообщения. |
|
Значок переключателя Show more. |
|
Цвет индикатора размышления. |
|
Расстояние между точками индикатора размышления. |
|
Длительность одной пульсации индикатора размышления. |
|
Высота, до которой увеличивается лента статусов, прежде чем в ней появляется прокрутка. См. Лента статусов размышления. |
Свойства значков содержат изображение, применяемое как маска, поэтому переопределение такого свойства заменяет соответствующий значок.
Компоненты стилизованы для обеих тем Jmix, Aura и Lumo, и используют собственные токены темы для рамок фокуса, границ, радиусов скругления и отступов, поэтому у перечисленных выше свойств нет единого значения по умолчанию. Поддерживаются тёмный режим, режим высокой контрастности и режим уменьшенной анимации. Расположение элементов задаётся свойствами, не зависящими от направления, поэтому компоненты следуют направлению текста документа.
Вложения
Сообщения могут содержать файлы, которые отображаются под текстом сообщения как миниатюры (thumbnails) или «чипы». Вложения затрагивают несколько компонентов, поэтому они описаны вместе в разделе Вложения.
Атрибуты
aiMessageList имеет следующий уникальный атрибут:
| Имя | Описание | По умолчанию |
|---|---|---|
Определяет, сообщается ли о поступающих сообщениях программам чтения с экрана. См. Доступность. |
|
aiMessageList имеет следующие общие атрибуты:
Обработчики
aiMessageList имеет следующие уникальные обработчики:
| Имя | Описание |
|---|---|
Срабатывает, когда пользователь нажимает на вложение в сообщении. Содержит сообщение и файл. См. Скачивание из диалога. |
|
Срабатывает, когда пользователь нажимает 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 для блоков кода, отображаемых внутри сообщений списка. См. Локализация.