Projektowanie przejrzystego interfejsu RAG przy użyciu React, Tailwind i Motion
Stwórz interfejs RAG, który umożliwia wyświetlanie etapów wyszukiwania, interaktywne cytaty, panel źródeł oraz działania po udzieleniu odpowiedzi, wykorzystując React state, Tailwind CSS i Framer Motion.
Źródła: How I’d Build a Modern RAG Interface with React, Tailwind CSS & Framer Motion
Proces generacji wzbogaconej o wyszukiwanie może być technicznie doskonały, ale nadal sprawiać wrażenie słabego produktu, gdy interfejs ukrywa to, co robi system. Jedna tylko pola do czatowania z ikonką spinera nie informuje użytkownika o tym, jakie dokumenty zostały przeszukane, skąd pochodzi odpowiedź ani czy jest ona nadal tworzona. Ten przewodnik pokazuje, jak zbudować interfejs React wokół rzeczywistych etapów żądania typu RAG, aby wyszukiwanie, źródła, transmisja danych w czasie rzeczywistym oraz cytaty stały się widocznymi i sprawdzalnymi elementami użytkowniczego doświadczenia.
Projektowanie wokół etapów wyszukiwania
Kluczową zmianą jest traktowanie produktu jako sekwencji, którą użytkownik może śledzić, a nie jako „czatu”: pytanie, wyszukiwanie, źródła, generowanie, cytaty, eksploracja. To właśnie ta sekwencja określa strukturę aplikacji React.
Dla typowego wewnętrznego asystenta wiedzy ekran może być podzielony na trzy obszary: bibliotekę dokumentów po lewej, rozmowę pośrodku oraz źródła dotyczące aktualnej odpowiedzi po prawej.
---------------------------------------------------------
| Documents | Conversation | Sources |
| | | |
| SOPs | User: How does onboarding work?| SOP.pdf |
| Policies | | |
| FAQs | Searching knowledge... | FAQ.pdf |
| | | |
| | AI response... | |
---------------------------------------------------------
Po nadal jest to interfejs czatu, ale rozmowa stanowi tylko jedną część produktu. Wokół niej aplikacja musi również udostępniać:
- wiedzę dostępną do wyszukiwania
- bieżący stan pozyskiwania informacji
- źródła użyte do udzielenia odpowiedzi
- metadane dokumentów
- cytaty
- dalsze działania
- wcześniejsze rozmowy
Umieszczanie tego wszystkiego w jednym komponencie czatu szybko staje się trudne do zarządzania.
Drzewo komponentów odpowiadające produktowi
Jedna praktyczna struktura wygląda następująco:
RAGApplication
|
|-- Sidebar
| |-- NewConversation
| |-- ConversationHistory
| `-- DocumentLibrary
|
|-- ChatWorkspace
| |-- MessageList
| |-- RetrievalStatus
| |-- StreamingResponse
| |-- CitationList
| `-- PromptInput
|
`-- SourcePanel
|-- SourceCard
|-- DocumentPreview
|-- RetrievedChunk
`-- SourceMetadata
Rozbicie pokazuje, jak różnie zachowuje się każdy region. Komunikaty gromadzą się jak w rozmowie, wskaźnik pobierania jest krótkotrwały, odpowiedzi napływają strumieniowo, panel źródeł zależy od wybranej cytaty, a biblioteka funkcjonuje jak panel sterowania. Oddzielne komponenty umożliwiają niezależne zarządzanie stanem każdego z nich oraz zapobiegają ponownemu renderowaniu całego ekranu przy każdym tokenie.
Krok 1: Zwróć wystarczająco dużo danych z API
Animacje i układ pojawiają się później. Najpierw backend musi dostarczyć frontendowi coś, z czym może pracować. Odpowiedź zawierająca tylko wygenerowany tekst pozostawia interfejs bez niczego do pokazania poza tym tekstem:
{
"answer": "..."
}
Bogatszy kontrakt obejmuje status oraz listę źródeł, na których opiera się odpowiedź – każde z nich ma identyfikator, tytuł, odniesienie do sekcji oraz krótki fragment tekstu:
{
"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..."
}
]
}
Dzięki takiemu układowi React może wyświetlić odpowiedź, pokazać elementy odnoszące się do źródeł, otworzyć poszczególne źródła i pokazać odpowiedni fragment tekstu, dzięki czemu dowody stają się elementem pierwszorzędnym w interakcji, a nie czymś dodanym później. Należy nalegać na stabilny id: służy on jako klucz React dla kart źródeł oraz do otwierania dokumentu.
Krok 2: Pobieranie modelu jako wyraźny stan interfejsu
Zapytanie RAG przechodzi przez kilka odrębnych faz, więc warto je wyraźnie nazwać:
idle
↓
retrieving
↓
sources_found
↓
generating
↓
complete
Najprostszym punktem wyjścia w React jest kilka elementów stanu odpowiadających bieżącej fazie, źródłom i odpowiedzi:
const [status, setStatus] = useState("idle");
const [sources, setSources] = useState([]);
const [answer, setAnswer] = useState("");
Gdy użytkownik wysyła pytanie, mechanizm obsługi przechodzi przez te fazy w miarę postępów zapytania:
async function handleAsk(question) {
setStatus("retrieving");
const result = await askKnowledgeBase(question); setSources(result.sources);
setStatus("generating"); setAnswer(result.answer);
setStatus("complete");
}
To jest celowo uproszczone. Ponieważ jedna wywołanie zwraca zarówno źródła, jak i odpowiedź, generating jest zastępowane przez complete w tym samym kroku, dzięki czemu React grupuje aktualizacje, a użytkownik nigdy nie widzi stanu pośredniego (sources_found w ogóle nie jest ustawiany). Implementacja typu streaming zamiast tego wysyłałaby zdarzenie o zakończeniu pobierania, następnie tokeny odpowiedzi, a potem informację o zakończeniu. Obsługa tego procesu wymaga również bloków try/catch oraz statusu error, aby nieudana prośba nie trwała w nieskończoność. Mimo to wyraźny status pozwala interfejsowi pokazać dokładnie, co się dzieje, a w miarę wzrostu liczby faz useReducer z zdefiniowanymi przejściami łatwiej zachowuje spójność niż kilka wywołań useState.
Krok 3: Zastąpienie elementu spinującego wskaźnikiem pobierania
Pusty komunikat „Loading...” nie mówi prawie nic. Z kolei aplikacja typu RAG może opisywać przebieg swoich działań. Najpierw użytkownik widzi, że wyszukiwana jest baza wiedzy:
Searching your knowledge base...
następnie ile dowodów zostało znalezionych:
Found 4 relevant sources
i w końcu że przygotowywana jest odpowiedź:
Generating response...
To niewielkie zmiany pomagają wyjaśnić przebieg procesu i kształtują oczekiwania co do czasu oczekiwania. Minimalny komponent mapuje status na odpowiedni komunikat:
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;
}
Nie ma w tym nic skomplikowanego, a właśnie o to chodzi: komponent zwraca null, gdy nie ma nic do poinformowania, a mimo to aplikacja sprawia wrażenie bardziej przemyślanej. Umieszczenie komunikatu w obszarze z atrybutem aria-live="polite" umożliwia użytkownikom z czytnikami ekranu słyszenie tych samych aktualizacji.
Krok 4: Budowanie hierarchii wizualnej za pomocą Tailwind
Ekrany RAG szybko się zapełniają wiadomościami od użytkowników i modeli, nazwami dokumentów oraz ich fragmentami, cytatami, ostrzeżeniami, instrukcjami i informacjami o stanie wyszukiwania. Klasы pomocnicze Tailwind umożliwiają szybkie ustanowienie jasnej hierarchii bez konieczności tworzenia rozbudowanej, dostosowanej szaty stylowej. Na przykład karta źródła może składać się z obszaru z obramowaniem i lekkim cieniem oraz tłustym tytułem:
<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>
za którym następuje stonowany etykieta sekcji oraz czytelny fragment tekstu z dużą odległością między wierszami:
<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>
Styl jest celowo prosty. W interfejsach AI kluczowa jest przejrzystość, a nie dekoracje, ponieważ użytkownik chce znać trzy rzeczy: odpowiedź, jej źródło oraz możliwy następny krok. Wizualna waga elementów powinna odpowiadać tej kolejności.
Krok 5: Uczynienie cytatów interaktywnymi
Wiele interfejsów RAG kończy się numerem w nawiasach, np. [1]. Citat jest o wiele bardziej przydatny, gdy umożliwia użytkownikowi sprawdzenie dowodów stojących za danym twierdzeniem. Prostym sposobem na to jest przedstawienie każdego cytatu jako przycisku, który zapisuje wybrany źródło:
<button
onClick={() => setSelectedSource(source)}
className="rounded-full border px-3 py-1 text-sm"
>
Refund Policy · 3.2
</button>
Kliknięcie w ten przycisk otwiera panel z źródłem, który pokazuje tytuł dokumentu:
Refund Policy
razem z sekcją, cytowanym fragmentem tekstu oraz sposobem otwarcia pełnego dokumentu:
Section 3.2"Refund requests must be submitted within..."[Open document]
Cytaty teraz faktycznie pełnią swoją funkcję, a podstawa odpowiedzi staje się widoczna: użytkownicy mogą sprawdzić twierdzenie jednym kliknięciem, zamiast polegać wyłącznie na zaufaniu. Prawdziwy element button umożliwia również dostęp do cytatów za pomocą klawiatury.
Krok 6: Wykorzystanie animacji do wyjaśnienia zmian
Framer Motion zasługuje na miejsce w tego typu interfejsach tylko wtedy, gdy animacja komunikuje stan. Nic nie powinno się poruszać samo z siebie. Dobrym zastosowaniem jest sprawienie, by karty źródłowe delikatnie przygaszały i przesuwały się w górę po zakończeniu pobierania danych. Zacznij od imporowania komponentu motion:
import { motion } from "framer-motion";
Następnie otocz każdą kartę elementem motion.div, który animuje ją od stanu przezroczystego i przesuniętego do pełnie widocznego w ciągu ćwierci sekundy:
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
>
<SourceCard source={source} />
</motion.div>
Panel źródłowy może przesuwać się z boku po wybraniu cytatu i cofać się, gdy zostanie zamknięty:
<motion.aside
initial={{ x: 40, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 40, opacity: 0 }}
>
<SourcePanel source={selectedSource} />
</motion.aside>
Jedna uwaga: animacja exit uruchamia się tylko wtedy, gdy komponent jest renderowany wewnątrz AnimatePresence, co zapobiega usunięciu elementu dopóki nie zakończy się jego animacja wyjścia. Bez tego React natychmiast usuwa panel, a animacja wyjścia w ogóle nie jest wykonywana. Należy również zauważyć, że w chwili pisania Framer Motion jest dostarczany jako pakiet motion z punktem wejścia motion/react; sprawdź aktualną dokumentację, aby znaleźć ścieżkę importu odpowiadającą zainstalowanej wersji.
Znaczenie ma komunikat przekazywany przez ruch: coś się zmieniło, nadeszły nowe informacje, a ten źródło należy do tej odpowiedzi. Szanuj preferencję reduced-motion, aby nigdy nie stała się uciążliwa.
Krok 7: Traktuj transmisję strumieniową inaczej niż ładowanie
Strumieniowanie stwarza własny problem związany z doświadczeniem użytkownika. Gdy tekst przychodzi token po tokenie, interfejs musi pokazać wyraźnie, że odpowiedź jest nadal pisana. Mała, pulsująca kropka po tekście dobrze to realizuje. Komponent renderuje dotychczasowy treść:
function StreamingAnswer({ content, isStreaming }) {
return (
<div className="prose max-w-none">
{content}
a kropkę dodaje tylko wtedy, gdy strumieniowanie jest aktywne:
{isStreaming && (
<span className="ml-1 inline-block h-4 w-1 animate-pulse bg-slate-500" />
)}
</div>
);
}
Rozsądne jest również ograniczenie możliwości użytkownika w przypadku nieukończonej odpowiedzi. Działania związane z czytaniem mogą pozostać dostępne, natomiast wszystko, co ma zapisać odpowiedź w innym miejscu, musi czekać na jej ukończenie:
Copy answer ✓
View sources ✓
Create task disabled while streaming
Export disabled while streaming
Celem jest uczciwa komunikacja stanu, a nie tylko jego upiększanie: eksportowanie nieukończonej odpowiedzi dałby błędny wynik.
Krok 8: Uczynienie panelu źródłowego częścią produktu
Prawidłowy panel źródłowy to jedna z najważniejszych funkcji, jakie może oferować interfejs RAG. Należy wziąć pod uwagę cały proces:
User asks question
↓
RAG retrieves 4 chunks
↓
AI generates answer
↓
Citation cards appear
↓
User clicks citation
↓
Relevant document section opens
Jako minimum panel może pokazywać:
- tytuł dokumentu
- sekcję
- wybrany fragment tekstu
- typ dokumentu
- informacje o istotności
- link lub przycisk do otwarcia dokumentu
Z upływem czasu może rozszerzyć się o:
- wyodrębnienie pasującego tekstu
- sąsiednie fragmenty w celu uzupełnienia kontekstu
- widok prezentacyjny dokumentu
- filtry metadanych
Każde dodatkowe element sprawia, że system staje się mniej „czarną skrzynką”. Dobry panel pokazuje również słabe wyniki wyszukiwania, co jest jednym z powodów, dla których warto inwestować w jakość źródeł już na wczesnym etapie projektu RAG.
Krok 9: Udostępnienie działań po odpowiedzi
Tutaj chatbot typu RAG zaczyna przekształcać się w aplikację. Załóżmy, że ktoś pyta, jak zespół integruje nowego klienta. System pobiera procedurę integracji i odpowiada listą kontrolną:
1. Create workspace
2. Request account access
3. Confirm scope
4. Assign team
5. Schedule kickoff
Zamiast na tym poprzestać, interfejs może zaproponować link z powrotem do dokumentu źródłowego:
[View SOP]
oraz funkcje, które wykorzystują uzyskany wynik:
[Copy checklist][Create task list]
Najciekawsza jest funkcja zarządzania listą zadań. Wartość systemu AI często leży nie tyle w samym odpowiedzi, ile w tym, co umożliwia dalej, i właśnie tutaj aplikacje typu RAG zaczynają ewoluować w kierunku agentów. Nasz przewodnik po tworzeniu interfejsów użytkownika dla agentów AI wieloetapowych omawia ten kierunek rozwoju.
Kompletna interakcja, od początku do końca
Łącząc wszystko razem, przez całą strukturę przepływa jedno pytanie. Zaczyna się od zapytania użytkownika, czy klient korporacyjny może anulować zamówienie po 30 dniach:
USER
"Can an enterprise client cancel after 30 days?"
Następnie React tworzy stan zapytania, API RAG pobiera dokumenty, linia statusu informuje o wynikach wyszukiwania, pokazuje się liczba źródeł, model LLM generuje odpowiedź opartą na faktach, która jest wyświetlana na ekranie, pojawiają się elementy z cytatami, a kliknięcie w jeden z nich przesuwa panel ze źródłem wraz z odpowiednią częścią tekstu na widoczne miejsce:
↓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
Pomyśl o tym jako o dwóch współpracujących warstwach: warstwie inteligencji w tle, która pobiera i generuje dane, oraz warstwie interfejsu użytkownika z przodu, która sprawia, że cała procedura jest zrozumiała. Są one budowane oddzielnie, ale muszą być projektowane razem, dlatego tak ważny jest kontrakt kroku 1.
Praktyczna struktura technologiczna
Lekką wersję tej aplikacji można stworzyć za pomocą:
- Frontend: React, Tailwind CSS i Framer Motion.
- Backend: FastAPI w Pythonie.
- Pobieranie danych: embeddingi oraz baza danych wektorowych, zarządzane za pomocą LangChain lub małego, dostosowanego pipeline’a.
- Model: dowolna odpowiednia API hostowana, np. OpenAI, Gemini lub Claude.
- Zapis danych: PostgreSQL lub Supabase, w połączeniu z Qdrant lub podobnym magazynem wektorowym.
Nie ma potrzeby wdrażania wszystkiego od razu. Do projektu portfolio wystarczy minimalny punkt wyjścia:
React
+
Tailwind
+
Framer Motion
+
FastAPI
+
one LLM
+
one vector store
Jakość pobierania danych, ponowne sortowanie, autoryzacja, przetwarzanie dokumentów oraz inne funkcje można stopniowo ulepszać, gdy już zadziała podstawowy mechanizm.
Wzorzec, którego należy unikać
Wersja, od której warto trzymać się z daleka, wygląda tak:
Upload PDF
↓
Chat box
↓
Spinner
↓
Answer
To dowodzi, że pipeline działa, ale pokazuje niewiele myślenia o produkcie: nic nie jest widoczne, weryfikowalne ani dostępne do działania. Silniejszy cel łączy kilka warstw:
Knowledge
+
Retrieval
+
Transparent states
+
Citations
+
Source exploration
+
Useful actions
To połączenie demonstruje zarówno inżynierię AI, jak i solidną implementację frontendu.
Dlaczego warstwa interfejsu zasługuje na uwagę
Model to tylko połowa produktu AI. Resztą jest to, czego zawsze wymagało dobre oprogramowanie: zrozumiałe interakcje, jasna informacja zwrotna, przewidywalne stany, przydatne procesy pracy oraz solidna architektura informacji. React, Tailwind i Motion nie wnoszą żadnej inteligencji, a mimo to decydują o tym, czy ktoś może z nich skorzystać, przy czym praca nad frontendem jest regularnie niedoceniana.
Główne wnioski
- Najpierw zaprojektuj odpowiedź API; źródła z stabilnymi ID, sekcjami i fragmentami to to, co umożliwia cytowanie oraz panele źródeł.
AnimatePresence i szanuj ustawienia dotyczące ograniczenia animacji.