Completing the RAG UI
Congratulations on reaching the last part of the entire YouTube RAG series. Here we shall complete the project and tie everything together.
Setting up match_documents function
We had already setup the Vector Store when initialising the Supabase in our API logic. Now we need to create the actual PostgreSQL function in Supabase so that it can perform the vector similarity search.
- Go to Supabase Dashboard
Navigate to your project, then go to
SQL Editor. - Execute the SQL Query
Paste the following code and click
Run. This function find the most similar documents based on a vector similarity search.Supabase SQL EditorCREATE OR REPLACE FUNCTION match_documents ( query_embedding vector(768), match_count int DEFAULT 5, filter jsonb DEFAULT '{}' ) RETURNS TABLE ( id uuid, content text, metadata jsonb, similarity float ) LANGUAGE plpgsql AS $$ DECLARE doc_uuid_array uuid[]; BEGIN IF query_embedding IS NULL THEN RAISE EXCEPTION 'query_embedding cannot be NULL'; END IF; IF filter ? 'document_id' AND jsonb_array_length(filter->'document_id') > 0 THEN doc_uuid_array := ( SELECT array_agg(elem::uuid) FROM jsonb_array_elements_text(filter->'document_id') elem ); RETURN QUERY SELECT ed.id, ed.content, ed.metadata, 1 - (ed.embedding <=> query_embedding) AS similarity FROM embedded_documents ed WHERE ed.document_id = ANY(doc_uuid_array) ORDER BY ed.embedding <=> query_embedding LIMIT match_count; ELSE RETURN QUERY SELECT ed.id, ed.content, ed.metadata, 1 - (ed.embedding <=> query_embedding) AS similarity FROM embedded_documents ed ORDER BY ed.embedding <=> query_embedding LIMIT match_count; END IF; END; $$;This is how your screen would look like. Just click on Run to execute and create the function

This function is designed to find and return the most similar documents from an embedded_documents table based on a vector similarity search.
Function Breakdown
Inputs
query_embedding: 768-dim vector for similarity matching.match_count: Number of matching documents to return (defaults to 5).filter: Optional JSON filter for specific document IDs.
Mechanism
- Calculates similarity using the
<=>cosine distance operator. - Higher scores indicate more similar content.
- Returns UUID, content, metadata, and the calculated similarity score.
Final Frontend Implementation
Update your App.tsx to handle the full journey: Submitting a URL, managing the RAG state, and displaying a cinematic chat interface.
import { useState, useEffect } from "react";
import { v4 as uuidv4 } from "uuid";
import { createSupabaseClient } from "./api/api";
//Setting up the Message interface for defining what content will be assistant's and what will be user's.
interface Message {
role: "user" | "assistant";
content: string;
}
const App = () => {
const [url, setUrl] = useState("");
const [prompt, setPrompt] = useState("");
const [loading, setLoading] = useState(false);
const [streaming, setStreaming] = useState(false);
const [conversationId, setConversationId] = useState("");
const [documentIds, setDocumentIds] = useState<string[]>([]);
const [messages, setMessages] = useState<Message[]>([]);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
// Extract YouTube video ID and thumbnail URL
const extractVideoId = (url: string): string | null => {
const regExp =/(?:https?://(?:www.)?youtube.com(?:/(?:[^/
s]+/S+/?|(?:S*?v=|S*/S+/?v=))(w+))|(?:youtu.be/(w+)))/;
const match = url.match(regExp);
return match ? match[1] || match[2] : null;
};
//Display the thumbnail once the videoId is extracted
useEffect(() => {
if (url) {
const videoId = extractVideoId(url);
if (videoId) {
setThumbnailUrl(`https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`);
}
}
}, [url]);
//Submit and Save the video data in database with a POST request.
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const convId = uuidv4();
const docId = uuidv4();
// Create conversation and document entries
const supabase = createSupabaseClient();
await supabase.from("conversations").insert({ id: convId });
await supabase.from("documents").insert({ id: docId });
await supabase.from("conversation_documents").insert({
conversation_id: convId,
document_id: docId,
});
// Store document (simulate storing URL)
await fetch("http://localhost:8000/store-document", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, documentId: docId }),
});
setConversationId(convId);
setDocumentIds([docId]);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
//Query the RAG model with the user's questions
const handleSendPrompt = async (e: React.FormEvent) => {
e.preventDefault();
const userMessage: Message = { content: prompt, role: "user" };
const assistantMessage: Message = { content: "", role: "assistant" };
setMessages((prev) => [...prev, userMessage, assistantMessage]);
setStreaming(true);
setPrompt("");
try {
const response = await fetch("http://localhost:8000/query-document", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: prompt,
conversationId,
documentIds,
}),
});
if (!response.ok) {
throw new Error("Failed to fetch response");
}
const data = await response.json();
const assistantMessage: Message = { content: data, role: "assistant" };
setMessages((prev) => [...prev.slice(0, -1), assistantMessage]);
} catch (error) {
console.error("Error fetching response:", error);
const assistantMessage: Message = { content: "Sorry, something went wrong.", role: "assistant" };
setMessages((prev) => [...prev.slice(0, -1), assistantMessage]);
} finally {
setStreaming(false);
}
};
//UI for the conversation between user and assistant
if (conversationId) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 px-6 py-10 flex flex-col items-center text-white">
<h2 className="text-3xl font-bold mb-2">Chat with Any YouTube Video</h2>
<p className="text-sm text-gray-400 mb-6">Video URL: <span className="underline text-blue-400">{url}</span></p>
{thumbnailUrl && (
<div className="w-full max-w-3xl mb-4">
<iframe
width="700"
height="400"
src={`https://www.youtube.com/embed/${extractVideoId(url)}`}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="mt-4 rounded-lg shadow-lg object-contain"
></iframe>
</div>
)}
// Display area for displaying the user and assistant's conversations
<div className="w-full max-w-3xl space-y-4 mb-10 overflow-y-auto max-h-[60vh] scrollbar-thin scrollbar-thumb-blue-500 pr-2">
{messages.map((message, index) => (
<div key={index} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div
className={`rounded-2xl px-5 py-3 max-w-[70%] text-sm shadow-md ${
message.role === 'user' ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-100'
}`}>
{message.content || "..."}
</div>
</div>
))}
</div>
<form onSubmit={handleSendPrompt} className="w-full max-w-2xl flex gap-3 items-center">
<input
type="text"
placeholder="Ask a question about the video..."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="flex-1 rounded-full px-5 py-3 text-white focus:outline-none focus:ring-2 focus:ring-white border border-white"
/>
<button
type="submit"
disabled={streaming || !prompt.trim()}
className={`px-5 py-3 rounded-full font-medium ${
streaming || !prompt.trim() ? 'bg-gray-500 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 cursor-pointer'
}`}>
{streaming ? 'Processing...' : 'Send'}
</button>
</form>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex flex-col items-center justify-center text-white">
<div className="bg-gray-800 shadow-lg rounded-lg p-8 w-full max-w-md">
<h1 className="text-4xl font-extrabold text-center mb-6 text-indigo-400">
AI Chat with YouTube
</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
placeholder="Drop a YouTube URL here..."
value={url}
onChange={(e) => setUrl(e.target.value)}
className="w-full px-4 py-2 rounded-lg bg-gray-700 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<button
type="submit"
disabled={loading}
className={`w-full py-2 rounded-lg font-semibold text-white cursor-pointer ${loading
? "bg-indigo-300 cursor-not-allowed"
: "bg-indigo-500 hover:bg-indigo-600"
}`}>
{loading ? "Processing..." : "Submit"}
</button>
</form>
{loading && (
<div className="mt-4 flex justify-center">
<div className="loading-spinner border-t-4 border-indigo-500 rounded-full w-8 h-8 animate-spin"></div>
</div>
)}
</div>
</div>
);
};
};
export default App;You Did It! 🥳
You've successfully built a production-ready modern RAG application. From database architecture and vector search to a streaming React UI powered by LangChain.
Watch the final project wrap-up and demo here:
