Главная / Статьи

Разработка прозрачного интерфейса RAG с использованием React, Tailwind и Motion

Создайте фронтенд RAG, который позволяет отображать этапы поиска информации, интерактивные цитаты, панель источников и действия после ответа с использованием состояния React, Tailwind CSS и Framer Motion.

Издатель: REACTAPP.TOP11 мин чтения2544 слов

Источники: How I’d Build a Modern RAG Interface with React, Tailwind CSS & Framer Motion

Инструментарий для генерации с усилением на основе поиска может быть технически отличным, но при этом казаться слабым продуктом, если интерфейс скрывает то, что делает система. Одно поле чата с индикатором загрузки ничего не говорит пользователю о том, какие документы были исследованы, откуда взялся ответ и продолжается ли его формирование. В этом руководстве показано, как структурировать фронтенд на React с учётом реальных этапов запроса RAG, чтобы процессы поиска, использования источников, потоковой обработки данных и формирования цитат стали видимыми и доступными для анализа элементами интерфейса.

Дизайн с учётом этапов поиска

Ключевой сдвиг заключается в том, чтобы рассматривать продукт как последовательность действий, которую может проследить пользователь, а не как «чат»: вопрос, поиск, источники, генерация, цитаты, дальнейшее исследование. Именно эта последовательность определяет структуру React-приложения.

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

---------------------------------------------------------
| Documents |          Conversation          | Sources |
|           |                                |         |
| SOPs      | User: How does onboarding work?| SOP.pdf |
| Policies  |                                |         |
| FAQs      | Searching knowledge...         | FAQ.pdf |
|           |                                |         |
|           | AI response...                 |         |
---------------------------------------------------------

Это по-прежнему интерфейс чата, но разговор является лишь одной из составляющих продукта. Помимо него приложение также должно предоставлять:

  • знания, доступные для поиска
  • текущее состояние поиска
  • источники, использованные для ответа
  • метаданные документов
  • цитаты
  • дальнейшие действия
  • прошлые разговоры

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

Древо компонентов, соответствующее продукту

Одна из работоспособных структур выглядит следующим образом:

RAGApplication
|
|-- Sidebar
|   |-- NewConversation
|   |-- ConversationHistory
|   `-- DocumentLibrary
|
|-- ChatWorkspace
|   |-- MessageList
|   |-- RetrievalStatus
|   |-- StreamingResponse
|   |-- CitationList
|   `-- PromptInput
|
`-- SourcePanel
    |-- SourceCard
    |-- DocumentPreview
    |-- RetrievedChunk
    `-- SourceMetadata

Разделение отражает различия в поведении каждого региона. Сообщения накапливаются, как в диалоге; индикатор получения информации существует недолго; ответы поступают потоком; панель исходного кода зависит от выбранной цитаты, а библиотека функционирует как панель управления. Отдельные компоненты позволяют независимо управлять состоянием каждого из них и предотвращают перерисовку всего экрана при поступлении каждого нового элемента.

Шаг 1: Возвращение достаточного количества данных из API

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

{
  "answer": "..."
}

Более подробный контракт включает статус и список источников, легших в основу ответа; каждый из них имеет идентификатор, заголовок, ссылку на раздел и краткий фрагмент текста:

{
  "answer": "Enterprise customers can request...",
  "status": "complete",
  "sources": [
    {
      "id": "doc-1",
      "title": "Refund Policy",
      "section": "3.2",
      "snippet": "Refund requests must be submitted..."
    },
    {
      "id": "doc-2",
      "title": "Enterprise Terms",
      "section": "5",
      "snippet": "Enterprise subscriptions..."
    }
  ]
}

Благодаря такой структуре React может отображать ответ, показывать источники, открывать отдельные документы и просматривать соответствующие фрагменты, превращая доказательства в ключевой элемент интерфейса, а не в нечто второстепенное. Обязательно используйте стабильный id: он будет служить ключом для карточек источников в React и средством для открытия документа.

Шаг 2: Получение данных из модели как явное состояние интерфейса

Запрос RAG проходит через несколько отдельных этапов, и полезно явно назвать их:

idle
↓
retrieving
↓
sources_found
↓
generating
↓
complete

Самой простой отправной точкой в React являются несколько элементов состояния для текущего этапа, источников и ответа:

const [status, setStatus] = useState("idle");
const [sources, setSources] = useState([]);
const [answer, setAnswer] = useState("");

Когда пользователь отправляет вопрос, обработчик проходит по этим этапам по мере выполнения запроса:

async function handleAsk(question) {
  setStatus("retrieving");
  const result = await askKnowledgeBase(question);  setSources(result.sources);
  setStatus("generating");  setAnswer(result.answer);
  setStatus("complete");
}

Это сделано намеренно упрощённым образом. Поскольку один вызов возвращает как источники, так и ответ, функция generating заменяется на complete в тот же момент, поэтому React группирует обновления, и пользователь никогда не видит промежуточное состояние (а переменная sources_found вообще не устанавливается). В реализации с потоковой передачей данных сначала отправляется событие о завершении поиска, затем токены ответа, а уже потом сигнал о завершении. Обработчик также требует блоков try/catch и статуса error, чтобы сбойный запрос не работал бесконечно. Тем не менее наличие явного статуса позволяет интерфейсу точно указывать, что происходит, и по мере увеличения количества этапов использование useReducer с определёнными переходами обеспечивает большую стабильность, чем несколько вызовов useState.

Шаг 3: Заменить индикатор загрузки на индикатор поиска

Простое сообщение «Загрузка...» почти ничего не говорит. Приложение типа RAG может описывать процесс обработки данных. Сначала пользователь видит, что идет поиск в базе знаний:

Searching your knowledge base...

затем показывается количество найденных доказательств:

Found 4 relevant sources

и, наконец, сообщается о том, что формируется ответ:

Generating response...

Такая небольшая изменение объясняет процесс обработки данных и формирует ожидания у пользователя относительно времени ожидания. Простой компонент соответствует статусу процесса определенному сообщению:

function RetrievalStatus({ status, sourceCount }) {
  if (status === "retrieving") {
    return <p>Searching your knowledge base...</p>;
  }
  if (status === "generating") {
    return <p>Found {sourceCount} sources. Generating response...</p>;
  }  return null;
}

Ничего сложного, и в этом-то и суть: компонент возвращает null, когда нет информации для отображения, но приложение тем не менее кажется более организованным. Размещение сообщения в области с атрибутом aria-live="polite" позволяет пользователям с экранными читалками слышать те же обновления.

Шаг 4: Создание визуальной иерархии с помощью Tailwind

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

<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
  <p className="text-sm font-semibold text-slate-900">
    Refund Policy
  </p>

за которым следует мягко выделенный заголовок раздела и читаемый фрагмент с достаточно большим интервалом между строками:

  <p className="mt-1 text-xs text-slate-500">
    Section 3.2
  </p>  <p className="mt-3 text-sm leading-6 text-slate-700">
    Refund requests must be submitted within...
  </p>
</div>

Стилизация здесь преднамеренно простая. В интерфейсах ИИ важна ясность, а не декоративные элементы, поскольку пользователь хочет знать три вещи: ответ, его источник и возможный следующий шаг. Визуальный вес элементов должен соответствовать этому порядку.

Шаг 5: Сделать цитаты интерактивными

Многие интерфейсы RAG ограничиваются номером в квадратных скобках, например [1]. Цитата гораздо полезнее, когда она позволяет пользователю просмотреть доказательства, лежащие в основе утверждения. Простым способом реализовать это является отображение каждой цитаты в виде кнопки, записывающей выбранный источник:

<button
  onClick={() => setSelectedSource(source)}
  className="rounded-full border px-3 py-1 text-sm"
>
  Refund Policy · 3.2
</button>

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

Refund Policy

а также раздел, цитируемый фрагмент и возможность открыть полный документ:

Section 3.2"Refund requests must be submitted within..."[Open document]

Теперь цитаты действительно выполняют свою функцию, и основания ответа становятся видимыми: пользователи могут проверить утверждение одним кликом, вместо того чтобы полагаться на слова автора. Настоящий элемент button также обеспечивает доступ к цитатам с помощью клавиатуры.

Шаг 6: Использование анимаций для объяснения изменений

Framer Motion занимает своё место в таком типе интерфейса только тогда, когда анимация передаёт информацию о состоянии. Ничто не должно анимироваться просто ради собственного удовольствия. Хорошим примером использования является постепенное исчезновение и слегка вертикальное перемещение карточек-источников после завершения загрузки. Начните с импорта компонента motion:

import { motion } from "framer-motion";

Затем оберните каждую карточку в элемент motion.div, который будет анимироваться от прозрачного состояния с смещением до полностью видимого за четверть секунды:

<motion.div
  initial={{ opacity: 0, y: 8 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.25 }}
>
  <SourceCard source={source} />
</motion.div>

Панель источников может выезжать сбоку при выборе цитаты и уходить обратно при её закрытии:

<motion.aside
  initial={{ x: 40, opacity: 0 }}
  animate={{ x: 0, opacity: 1 }}
  exit={{ x: 40, opacity: 0 }}
>
  <SourcePanel source={selectedSource} />
</motion.aside>

Одно ограничение: анимация exit выполняется только тогда, когда компонент отрисовывается внутри AnimatePresence, что позволяет элементу оставаться в состоянии монтирования до завершения его анимации выхода. Без этого React сразу удаляет панель, и анимация выхода так и не запускается. Также обратите внимание, что на момент написания Framer Motion распространяется как пакет motion с точкой входа motion/react; проверьте актуальную документацию на предмет пути импорта, соответствующего установленной версии.

Ценность заключается в сообщении, которое несет движение: что-то изменилось, поступила новая информация, и этот источник принадлежит данному ответу. Соблюдайте настройку reduced-motion, чтобы она никогда не становилась помехой.

Шаг 7: Различать стриминг и загрузку

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

function StreamingAnswer({ content, isStreaming }) {
  return (
    <div className="prose max-w-none">
      {content}

и добавляет точку только во время активного стриминга:

      {isStreaming && (
        <span className="ml-1 inline-block h-4 w-1 animate-pulse bg-slate-500" />
      )}
    </div>
  );
}

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

Copy answer       ✓
View sources      ✓
Create task       disabled while streaming
Export            disabled while streaming

Цель — честная передача информации о состоянии, а не его улучшение: экспорт незавершённого ответа приведёт к ошибочному результату.

Шаг 8: Сделать панель исходного кода частью продукта

Надлежащая панель исходного кода — одна из сильнейших функций, которые может предложить интерфейс RAG. Учтите весь процесс работы:

User asks question
        ↓
RAG retrieves 4 chunks
        ↓
AI generates answer
        ↓
Citation cards appear
        ↓
User clicks citation
        ↓
Relevant document section opens

Как минимум, панель может отображать:

  • название документа
  • раздел
  • выделенный фрагмент текста
  • тип документа
  • информацию о релевантности
  • ссылку или кнопку для открытия документа

Со временем она может расшириться и включать:

  • выделение соответствующего текста
  • соседние фрагменты для контекста
  • предварительный просмотр документа
  • фильтры метаданных

Каждое дополнение делает систему менее закрытой. Хорошая панель также позволяет увидеть недостатки процесса поиска, что является одной из причин для вложений в качество исходных данных с самого начала проекта RAG на ранних этапах.

Шаг 9: Предоставление действий после ответа

Именно здесь чат-бот на основе RAG начинает превращаться в приложение. Предположим, кто-то спрашивает, как команда вводит нового клиента. Система извлекает соответствующий процедурный документ и отвечает с чек-листом:

1. Create workspace
2. Request account access
3. Confirm scope
4. Assign team
5. Schedule kickoff

Вместо того чтобы останавливаться на этом, интерфейс может предложить ссылку обратно на исходный документ:

[View SOP]

а также действия, которые используют полученные результаты:

[Copy checklist][Create task list]

Наиболее интересным является действие с списком задач. Ценность ИИ-системы часто заключается не столько в самом ответе, сколько в том, что она позволяет сделать дальше, и именно здесь приложения на основе RAG начинают переходить к формату агентов. Наш гид по созданию UI для многоэтапных ИИ-агентов рассматривает именно этот направление.

Полная взаимодействие от начала до конца

Если объединить всё вместе, через весь технологический стек проходит один вопрос. Всё начинается с того, что пользователь спрашивает, может ли корпоративный клиент отменить заказ через 30 дней:

USER
"Can an enterprise client cancel after 30 days?"

Далее React создаёт состояние запроса, API RAG получает документы, строка статуса отображает результаты поиска, показывается количество источников, LLM генерирует основанный на данных ответ, который выводится на экран, появляются ярлыки с цитатами, а нажатие на один из них выдвигает панель с источником вместе с соответствующим отрывком:

        ↓REACT
Creates query state        ↓RAG API
Retrieves relevant documents        ↓UI STATUS
"Searching your knowledge base..."        ↓SOURCES
3 relevant sources found        ↓LLM
Generates grounded response        ↓STREAMING UI
Response appears progressively        ↓CITATIONS
Refund Policy · 3.2
Enterprise Terms · 5        ↓USER
Clicks citation        ↓MOTION
Source panel slides into view        ↓SOURCE
Relevant passage displayed

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

Прагматичный технологический стек

Легкую версию этого приложения можно создать с использованием:

  • Фронтенд: React, Tailwind CSS и Framer Motion.
  • Бэкенд: FastAPI на Python.
  • Поиск: эмбеддинги и векторная база данных, организованные с помощью LangChain или небольшой пользовательской системы.
  • Модель: любой подходящий хостингованный API, такой как OpenAI, Gemini или Claude.
  • Хранилище: PostgreSQL или Supabase вместе с Qdrant или аналогичным хранилищем векторов.

Не обязательно использовать всё сразу. Для проекта в портфолио достаточно минимальной отправной точки:

React
+
Tailwind
+
Framer Motion
+
FastAPI
+
one LLM
+
one vector store

Качество поиска, переранжирование, аутентификация, обработка документов и другие функции можно улучшать постепенно, как только начнет работать основной цикл.

Паттерн, которого следует избегать

Версия, от которой стоит отказаться, выглядит так:

Upload PDF
↓
Chat box
↓
Spinner
↓
Answer

Это демонстрирует, что конвейер работает, но показывает мало продуктового мышления: ничего не является видимым, проверяемым или пригодным к использованию. Более сильная цель объединяет несколько слоев:

Knowledge
+
Retrieval
+
Transparent states
+
Citations
+
Source exploration
+
Useful actions

Такое сочетание демонстрирует как инженерию ИИ, так и качественную реализацию фронтенда.

Почему слой интерфейса заслуживает внимания

Модель — это лишь половина ИИ-продукта. Остальное — это то, что всегда требовалось хорошему программному обеспечению: понятные интеракции, четкая обратная связь, предсказуемые состояния, полезные рабочие процессы и надежная архитектура информации. React, Tailwind и Motion не добавляют никакой интеллектуальной составляющей, однако они определяют, сможет ли кто-либо извлечь из этого пользу, причем работа над фронтендом часто недооценивается.

Основные выводы

  • Сначала необходимо спроектировать ответ API; источники с стабильными ID, разделами и фрагментами позволяют создавать цитаты и панели с источниками.
  • Моделируйте запрос как набор этапов с указанием имен, планируйте работу с потоковой передачей данных и обработкой ошибок, вместо того чтобы просто переключать единый флаг загрузки.
  • Рассматривайте цитаты как кнопки, открывающие доказательства, и уделяйте панели источников должное внимание.
  • Используйте анимации только для обозначения изменений, оборачивайте удаляющиеся элементы в AnimatePresence и учитывайте настройки сокращения анимаций.
  • Ограничивайте доступ к действиям, зависящим от готового ответа, и предлагайте следующие шаги после его получения, ведь именно тогда чат-бот превращается в приложение.