05FINAL STEPS

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.

  1. Go to Supabase Dashboard

    Navigate to your project, then go to SQL Editor.

  2. 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 Editor
    CREATE 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

    Match Documents

    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.

App.tsx
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: