Розробка прозорого інтерфейсу RAG за допомогою React, Tailwind та Motion
Створіть фронтенд RAG, який відображатиме етапи пошуку інформації, інтерактивні цитати, панель джерел та дії після надання відповіді, використовуючи React state, Tailwind CSS та Framer Motion.
Джерела: 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і.
- Отримання даних: embeddingи та векторна база даних, організовані за допомогою 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, та дотримуйтесь налаштувань щодо скорочення кількості анімацій.