За последний год мы наблюдаем бум ИИ‑помощников, и это не обошло стороной интерфейсы в Yandex Cloud: то в техподдержке завёлся чат‑бот с моделью, то в консоли — агент для рабочих операций. Команды подключали модели, продумывали диалоговую логику, рисовали дизайн и собирали чаты — и делали всё это поодиночке.
Разные команды собирали интерфейсы на общем фреймворке Gravity UI, но постепенно там появилось столько вариаций, что стало сложно поддерживать единый пользовательский опыт. Да и коллеги всё чаще сталкивались с тем, что тратят время на одни и те же решения.
Чтобы перестать каждый раз изобретать велосипед, мы собрали накопленные практики в единый подход и сделали инструмент для чат‑ботов с ИИ — @gravity‑ui/aikit. Он позволяет создать полноценный интерфейс ассистента за несколько дней и при этом легко адаптировать его под разные сценарии.
Меня зовут Илья Ломтев, я старший разработчик в команде Foundation Services Yandex Cloud, и в статье я расскажу, почему мы решили собрать AIKit, как он устроен, немного о планах на будущее — и о том, что можно попробовать у себя.
За последний год в Yandex Cloud выросло число сервисов с ИИ‑ассистентами, например:
Code Assistant Chat в SourceCraft — ассистент помогает разработчикам писать код, а в режиме ИИ‑агента создаёт и настраивает репозитории, запускает CI/CD‑процессы, отвечает на вопросы по документации и автоматизирует задачи. Также умеет управлять issues, пул‑реквестами, работать с кодом: объяснять, создавать и редактировать файлы.
ИИ‑ассистент в облачной консоли — ассистент, разработанный для управления ресурсами в Yandex Cloud. Основная задача — помочь быстро и безопасно настраивать, изменять и управлять облачной инфраструктурой, скрывая сложность взаимодействия с API и инструментами.
В экосистеме возник десяток чатов, каждый со своей логикой, своим форматом сообщений и набором корнер‑кейсов.
Мы обнаружили, что команды приходят к примерно одинаковому набору задач. Что нужно большинству:
аккуратно отображать сообщения пользователя и ассистента,
правильно организовать стриминг ответов,
показывать индикатор «ассистент печатает»,
обрабатывать ошибки вроде оборвавшегося соединения или ретраев.
Задачи по сути одинаковые, а вот путей решения много, и UX отличается. Например, расположение и способ отображения истории чатов: это может быть как отдельный экран, открывающийся как меню, так и список чатов в popup.
Проявилась проблема: опыт в разных чатах существенно отличался. Где‑то ассистент стримил ответ, а где‑то показывал сразу готовый текст. В одном интерфейсе сообщения группировались, а в другом — шли сплошной лентой. Это ломало общий UX — получалось, что пользователь переходит между продуктами одной экосистемы, а ощущения от ассистента совершенно разные.
Ещё стало заметно, что раскатывать новые фичи в модели становилось всё сложнее. Чтобы донести до пользователей, например, инструментальность, мультимодальность или структурированные ответы тулов, нужно было согласовать контракт, доработать бэкенды, а затем обновить UI в каждой команде отдельно. В таких условиях любые изменения занимали много времени и плохо масштабировались.
Мы захотели остановить этот рост вариативности и вернуть предсказуемость. Для этого требовалось унифицировать модель данных и паттерны работы, дать готовые компоненты и хуки, чтобы командам не приходилось начинать с нуля, и оставить пространство для кастомизации — ведь сценарии у всех разные.
Так мы пришли к идее отдельной библиотеки @gravity‑ui/aikit — это расширение Gravity UI, которое следует тем же принципам, но ориентировано на современные ИИ‑сценарии: диалоги, ассистентов, мультимодальность.
Проектируя AIKkit, мы ориентировались на опыт AI SDK и несколько фундаментальных принципов.
Atomic Design в основе: вся библиотека строится от атомов к страницам. Такая структура даёт чёткую иерархию, позволяет переиспользовать компоненты и при необходимости менять поведение на любом уровне.
Полностью SDK‑agnostic: AIKit не зависит от конкретного ИИ‑провайдера. Можно использовать OpenAI, Alice AI LLM или свой бэкенд — UI принимает данные через props, а состояние и запросы остаются на стороне продукта.
Два уровня использования для сложных сценариев: есть готовый компонент, который работает «из коробки», и есть хук с логикой, который позволяет полностью контролировать UI. Например, можно воспользоваться PromptInput или собрать своё поле ввода на базе usePromptInput. Это даёт гибкость без необходимости переписывать фундамент.
Расширяемая система типов. Чтобы обеспечить единообразие и типобезопасность, мы собрали расширяемую модель данных. Сообщения представлены единой типизированной структурой: есть сообщения пользователя, сообщения ассистента и несколько базовых типов контента — текст (text), размышления модели (thinking), инструменты (tool). При этом можно добавлять свои типы через MessageRendererRegistry.
Всё это типизировано в TypeScript, что помогает быстрее собирать сложные сценарии и избежать ошибок на этапе разработки.
// 1. Определяем тип данных type ChartMessageContent = TMessageContent<'chart' chartData: number[]; chartType: 'bar' | 'line'; , { }>; // 2. Создаём компонент отображения const ChartRenderer = ({ part }: MessageContentComponentProps<ChartMessageContent>) => { return <div>Визуализация графика: {part.data.chartType}</div>; }; // 3. Регистрируем рендерер const customRegistry = registerMessageRenderer( createMessageRendererRegistry(), 'chart' , { component: ChartRenderer } ); // 4. Используем в AssistantMessage <AssistantMessage message={message} messageRendererRegistry={customRegistry} />
Наконец мы предусмотрели темизацию через CSS-переменные, добавили i18n (RU/EN), обеспечили доступность (ARIA, клавиатурная навигация), и настроили визуальные регрессионные тесты через Playwright Component Testing в Docker — и библиотека была готова к продакшн-использованию.
В основе AIKit — единая модель диалога. Чтобы её создать, для начала потребовалось разобраться с иерархией сообщений.
Сообщения сами по себе довольно многогранные сущности. Есть первое сообщение от LLM — это один стрим. Но в рамках него может быть много разных вложенных сообщений: по сути это рассуждения, предложения, вызовы тулов для решения одного вопроса. Все эти разные подсообщения — по факту одно сообщение от бэкенда. Но также каждое из них вполне может быть отдельным сообщением в простом использовании LLM.
Поэтому мы оставили возможность использовать чат обоими способами: сообщения могут быть вложены друг в друга, а могут быть плоскими — здесь всё зависит от потребности.
Управление состоянием при этом остаётся у сервиса. AIKit не хранит данные сам — он принимает их извне. Команды могут использовать React State, Redux, Zustand, Reatom — всё, что удобно. Мы лишь даём хуки, которые инкапсулируют типовую UI-логику, например:
умную прокрутку с помощью useSmartScroll;
работу с датами, к примеру, форматирование дат с учётом локали useDateFormatter;
обработку тул-сообщений useToolMessage;
и всё остальное, что нужно для построения диалога.
Вдобавок к этому AIKit остаётся расширяемым. Можно подключать любые модели, создавать собственные типы контента и строить UI полностью под свои задачи — пользуясь логикой из хуков или используя готовые компоненты как базу. Архитектура позволяет экспериментировать, не нарушая общих принципов.
Для создания своего первого чата воспользуемся подготовленным компонентом ChatContainer:
import React, { useState } from 'react'; import { ChatContainer } from 'aikit'; import type { ChatType, MessageType } from 'aikit'; function App() { const [messages, setMessages] = useState<MessageType[]>([]); const [chats, setChats] = useState<ChatType[]>([]); const [activeChat, setActiveChat] = useState<ChatType | null>(null); const handleSendMessage = async (content: string) => { // Your message sending logic const response = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ message: content }) }); const data = await response.json(); // Update state setMessages(prev => [...prev, data]); }; return ( <ChatContainer messages={[]} onSendMessage={() => {}} welcomeConfig={{ description: 'Start a conversation by typing a message or selecting a suggestion.', image: <Icon data={() => {}} size={48}/>, suggestionTitle: 'Try asking:', suggestions: [ { id: '1', title: 'Explain quantum computing in simple terms' }, { id: '2', title: 'Write a poem about nature' }, { id: '3', title: 'Help me debug my JavaScript code' }, { id: '4', title: 'Summarize recent AI developments' } ], title: 'Welcome to AI Chat' }} /> ); }
«Из коробки» всё выглядит вот так:
Добавим немного праздника:
Поправим начальное состояние.
Для более тонкой настройки соберём чат из отдельных компонентов: Header, MessageList, PromptBox.
import { Header, MessageList, PromptBox } from 'aikit'; function CustomChat() { return ( <div className="custom-chat"> <Header title="AI Assistant" onNewChat={() => {}} /> <MessageList messages={messages} showTimestamp /> <PromptBox onSend={handleSend} placeholder="Спросите что угодно..." /> </div> ); }
Применим разные встроенные типы сообщений, импортированные через MessageType.
— thinking — покажет процесс размышления ИИ (так пользователь может изучить логику, по которой ассистент готовит ответ).
— tool — подойдёт для отображения интерактивных блоков ответа, в нашем случае, это блок с кодом, в котором корректно работает подсветка синтаксиса, поддержаны операции редактирования и копирования в буфер обмена.
Также можно добавлять собственные типы, например, сообщения с изображениями:
type ImageMessage = BaseMessage<ImageMessageData> & { type: 'image' }; const ImageMessageView = ({ message }: { message: ImageMessage }) => ( <div> <img src={message.data.imageUrl} /> {message.data.caption && <p>{message.data.caption}</p>} </div> ); const customTypes: MessageTypeRegistry = { image: { component: ImageMessageView, validator: (msg) => msg.type === 'image' } }; <ChatContainer messages={messages} messageTypeRegistry={customTypes} />
Добавим стилизацию через CSS…
…и получим чат с Дедом Морозом:)
Для полной кастомизации отдельных элементов можно использовать хуки — будем рады увидеть ваши варианты стилизации в комментариях под статьёй!
Результат использования AIKit в Yandex Cloud стал заметен быстро. Во всех сервисах ассистенты стали вести себя одинаково: одинаково стримить ответы, одинаково показывать ошибки, одинаково группировать сообщения. UX стал единообразным, теперь с ним проще взаимодействовать во всей экосистеме, поведение более ожидаемое и предсказуемое.
UX-язык стал единым — чаты ассистентов в разных продуктах теперь ощущаются как часть одной экосистемы. Пользователи видят предсказуемое поведение: одинаковый стриминг, обработку ошибок, паттерны взаимодействия.
Скорость разработки UI чата гораздо выше.
Централизованное развитие — новые фичи вроде типа контента thinking или улучшенной работы с тулами добавляются один раз и автоматически доступны всем.
Библиотека стала основой для формирования стандартов ИИ-интерфейсов в экосистеме.
Теперь о планах. Мы выделили несколько направлений:
Улучшение производительности через виртуализацию для работы с очень большими историями чатов.
Расширение базовых сценариев под новые возможности ИИ‑агентов, которые активно развиваются.
Добавление утилит, чтобы упростить маппинг данных популярных ИИ‑моделей в нашу модель данных чата.
Дополнительно будем развивать документацию и примеры. И, конечно, развитие сообщества — хотим, чтобы библиотека была полезна не только внутри компании, но и внешним разработчикам.
Переходите в раздел библиотеки на нашем сайте. Если вы делаете собственный ИИ‑ассистент, хотите быстрый и предсказуемый чат‑интерфейс и уже используете Gravity UI (или готовы попробовать), загляните в README и примеры. И ещё будем благодарны за обратную связь — заводите issue, присылайте PR, рассказывайте, что ещё нужно для ваших сценариев!
Если Вам нравится наш проект, будем рады ⭐️ в AIKit и UIKit!
Источник



Финансы
Поделиться
Поделиться этой статьей
Скопировать ссылкуX (Twitter)LinkedInFacebookEmail
Самые влиятельные: Хавьер Перес-Тассо
Перес