Building an Advanced Chat Application with React and Flask Using Gemini API

Building an Advanced Chat Application with React and Flask Using Gemini API
2024-11-17Basic3 min

Chat applications are among the most useful and popular projects for developers beginning or mastering full-stack programming. In this blog post, I'll explain how I created a React-based chat app with advanced design that interfaces with a Flask server powered by the Gemini API to provide intelligent, generative responses.

Overview

The program uses React for a dynamic and responsive front-end, and Flask with Gemini API on the back-end to process and respond to user queries. Here's what we'll discuss:

  • Setting up the Flask backend using the Gemini API.
  • Developing a React frontend with advanced design and dynamic chat features.
  • Integrating the front and back ends.
  • Introducing dark mode, sidebar, and more advanced features.
  • What we are Going to Create ?

    Let's have a look at what we want to produce for this fantastic blog.

    Building an Advanced Chat Application with React and Flask Using Gemini API
    Building an Advanced Chat Application with React and Flask Using Gemini API
    Building an Advanced Chat Application with React and Flask Using Gemini API
    Building an Advanced Chat Application with React and Flask Using Gemini API

    Setting Up the Flask Back-End

    Before starting, ensure you have the following installed:

  • Python 3.x
  • Flask and Flask-CORS
  • Google Generative AI SDK
  • Install dependencies by running:

    pip install flask flask-cors google-generativeai
    

    Flask API Code

    The Flask back-end processes user messages and retrieves AI-generated responses using the Gemini API. Here’s the complete server code:

    Get You Google Gemini API key from the below link

    Google Gemini API
    from flask import Flask, request, jsonify
    import google.generativeai as genai
    from flask_cors import CORS
    
    app = Flask(__name__)
    CORS(app)
    
    # Set the Google API key
    key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"  # Your API key here
    genai.configure(api_key=key)
    model = genai.GenerativeModel("gemini-1.5-flash")
    
    @app.route('/api/chat', methods=['POST'])
    def chat():
        user_message = request.json.get('message')
        response = model.generate_content(user_message)
        return jsonify({'response': response.text})
    
    if __name__ == '__main__':
        app.run(debug=True)

    Creating the React Front-End

    Install the required packages:

    npm install react framer-motion lucide-react
    

    React Component Code

    Below is the main React component for our chat application:

    import React, { useState, useRef, useEffect } from 'react'
    import { motion, AnimatePresence } from 'framer-motion'
    import { ChevronLeft, ChevronRight, Plus, Trash, Edit, Send, Sparkles, Bot, User, Sun, Moon, Menu, Shield, ArrowDown, X } from 'lucide-react'
    
    const models = [
        { id: 'gpt-3.5', name: 'GPT-3.5' },
        { id: 'gpt-4', name: 'GPT-4' },
    ]
    
    const examplePrompts = [
        "Explain quantum computing in simple terms",
        "Write a poem about artificial intelligence",
        "How do I make a perfect cup of coffee?",
        "What are the best practices for sustainable living?",
    ]
    
    export default function Main() {
        const [sidebarOpen, setSidebarOpen] = useState(false)
        const [chats, setChats] = useState([])
        const [currentChat, setCurrentChat] = useState(null)
        const [input, setInput] = useState('')
        const [selectedModel, setSelectedModel] = useState(models[0])
        const [darkMode, setDarkMode] = useState(true)
        const [isLoading, setIsLoading] = useState(false)
        const [showScrollButton, setShowScrollButton] = useState(false)
        const messagesEndRef = useRef(null)
        const chatContainerRef = useRef(null)
    
        const scrollToBottom = () => {
            messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
        }
    
        useEffect(() => {
            scrollToBottom()
        }, [chats])
    
        useEffect(() => {
            const mediaQuery = window.matchMedia('(min-width: 768px)')
            const handleChange = (e) => {
                setSidebarOpen(e.matches)
            }
            mediaQuery.addListener(handleChange)
            setSidebarOpen(mediaQuery.matches)
            return () => mediaQuery.removeListener(handleChange)
        }, [])
    
        useEffect(() => {
            const handleScroll = () => {
                if (chatContainerRef.current) {
                    const { scrollTop, scrollHeight, clientHeight } = chatContainerRef.current
                    setShowScrollButton(scrollHeight - scrollTop > clientHeight + 100)
                }
            }
    
            const chatContainer = chatContainerRef.current
            if (chatContainer) {
                chatContainer.addEventListener('scroll', handleScroll)
            }
    
            return () => {
                if (chatContainer) {
                    chatContainer.removeEventListener('scroll', handleScroll)
                }
            }
        }, [])
    
        const handleNewChat = () => {
            const newChat = { id: Date.now(), name: 'New Chat', messages: [] }
            setChats([...chats, newChat])
            setCurrentChat(newChat.id)
        }
    
        const handleSwitchChat = (id) => {
            setCurrentChat(id)
            if (window.innerWidth < 768) {
                setSidebarOpen(false)
            }
        }
    
        const handleRenameChat = (id, newName) => {
            setChats(chats.map(chat => chat.id === id ? { ...chat, name: newName } : chat))
        }
    
        const handleDeleteChat = (id) => {
            setChats(chats.filter(chat => chat.id !== id))
            if (currentChat === id) {
                setCurrentChat(null)
            }
        }
    
        const handleSendMessage = async (e) => {
            e.preventDefault();
            if (!input.trim() || isLoading) return;
        
            const updatedChats = chats.map(chat => {
                if (chat.id === currentChat) {
                    return {
                        ...chat,
                        messages: [...chat.messages, { role: 'user', content: formatResponse(input) }]
                    };
                }
                return chat;
            });
        
            setChats(updatedChats);
            setInput('');
            setIsLoading(true);
        
            try {
                const response = await fetch('http://localhost:5000/api/chat', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ message: input }),
                });
        
                if (response.ok) {
                    const data = await response.json();
                    const aiResponse = { role: 'assistant', content: formatResponse(data.response) };
        
                    const updatedChatsWithAI = updatedChats.map(chat => {
                        if (chat.id === currentChat) {
                            return {
                                ...chat,
                                messages: [...chat.messages, aiResponse]
                            };
                        }
                        return chat;  // You might not need to format the other chats' messages here
                    });
        
                    setChats(updatedChatsWithAI);
                } else {
                    console.error('Error fetching AI response:', response.statusText);
                }
            } catch (error) {
                console.error('Error:', error);
            } finally {
                setIsLoading(false);
            }
        };
        
    
        function formatResponse(response) {
            // Wrap code blocks with <pre> and <code> tags for proper formatting
            return response
                .replace(/```([^`]+)```/g, function (match, p1) {
                    // Split the code block into lines
                    const lines = p1.trim().split('\n');
                    let languageLine = '';  // To store the language identifier line
    
                    // Check if the first line is a language identifier
                    if (lines.length > 1 && /^[a-zA-Z]+$/.test(lines[0])) {
                        // Extract the language identifier
                        languageLine = lines.shift(); // Removes and stores the first line
                    }
    
                    // Join the remaining lines and escape HTML entities
                    const code = lines.join('\n').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    
                    // Return formatted HTML with the language identifier and horizontal line
                    return `<div class="codeContainer"><div class="language-name mt-2">${languageLine}</div><pre style="overflow-x:scroll; background:black; border-radius:15px; padding:5px; margin-top:10px;" class="code-block"><code>${code}</code></pre></div>
                    
        `;
                })
                .replace(/`([^`]+)`/g, function (match, p1) {
                    // Format inline code with styling and escape HTML entities
                    return `<code class="inline-code">${p1.trim().replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code>`;
                })
                .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")  // Bold text without asterisks
                .replace(/\n/g, "<br>");  // Line breaks
        }
    
    
    
        const handleStopResponse = () => {
            setIsLoading(false)
            // In a real implementation, you would also need to cancel the API request here
        }
    
        const toggleDarkMode = () => {
            setDarkMode(!darkMode)
        }
    
       
    
        return (
            <div className={`flex h-screen ${darkMode ? 'bg-gradient-to-br from-gray-900 to-gray-800 text-gray-100 dark-mode' : 'bg-gradient-to-br from-gray-100 to-white text-gray-900 light-mode'}`}>
                {/* Sidebar */}
                <AnimatePresence>
                    {sidebarOpen && (
                        <motion.aside
                            initial={{ x: -300, opacity: 0 }}
                            animate={{ x: 0, opacity: 1 }}
                            exit={{ x: -300, opacity: 0 }}
                            transition={{ type: "spring", stiffness: 300, damping: 30 }}
                            className={`fixed inset-y-0 left-0 z-50 w-64 md:w-80 p-6 flex flex-col ${darkMode ? 'bg-gray-800 bg-opacity-90' : 'bg-white bg-opacity-90'} backdrop-blur-lg shadow-2xl`}
                        >
                            <div className="flex justify-between items-center mb-6">
                                <h2 className="text-xl font-bold">Chats</h2>
                                <button
                                    onClick={() => setSidebarOpen(false)}
                                    className={`${darkMode ? 'bg-gray-700 hover:bg-gray-600' : 'bg-gray-200 hover:bg-gray-300'} p-2 rounded-full transition-colors duration-300`}
                                >
                                    <ChevronLeft size={24} />
                                </button>
                            </div>
                            <button
                                onClick={handleNewChat}
                                className={`${darkMode ? 'bg-indigo-600 hover:bg-indigo-700' : 'bg-indigo-500 hover:bg-indigo-600'} text-white font-bold py-3 px-6 rounded-xl flex items-center justify-center mb-6 transition-all duration-300 transform hover:scale-105 shadow-lg`}
                            >
                                <Plus size={20} className="mr-2" /> New Chat
                            </button>
                            <div className="flex-grow overflow-y-auto space-y-2">
                                {chats.map(chat => (
                                    <motion.div
                                        key={chat.id}
                                        initial={{ opacity: 0, y: 20 }}
                                        animate={{ opacity: 1, y: 0 }}
                                        transition={{ duration: 0.3 }}
                                        className={`flex items-center justify-between p-3 rounded-xl cursor-pointer ${currentChat === chat.id
                                                ? darkMode ? 'bg-indigo-600 shadow-lg' : 'bg-indigo-100 shadow-lg'
                                                : darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-200'
                                            } transition-all duration-300`}
                                        onClick={() => handleSwitchChat(chat.id)}
                                    >
                                        <span className="truncate flex-grow font-medium">{chat.name}</span>
                                        <div className="flex items-center space-x-2">
                                            <button
                                                onClick={(e) => {
                                                    e.stopPropagation()
                                                    const newName = prompt('Enter new name:', chat.name)
                                                    if (newName) handleRenameChat(chat.id, newName)
                                                }}
                                                className={`${darkMode ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'} transition-colors duration-200`}
                                            >
                                                <Edit size={16} />
                                            </button>
                                            <button
                                                onClick={(e) => {
                                                    e.stopPropagation()
                                                    handleDeleteChat(chat.id)
                                                }}
                                                className={`${darkMode ? 'text-gray-400 hover:text-white' : 'text-gray-600 hover:text-gray-900'} transition-colors duration-200`}
                                            >
                                                <Trash size={16} />
                                            </button>
                                        </div>
                                    </motion.div>
                                ))}
                            </div>
                            <div className="mt-6">
                                <select
                                    value={selectedModel.id}
                                    onChange={(e) => setSelectedModel(models.find(m => m.id === e.target.value))}
                                    className={`w-full ${darkMode ? 'bg-gray-700 text-white' : 'bg-gray-200 text-gray-900'} py-3 px-4 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all duration-300`}
                                >
                                    {models.map(model => (
                                        <option key={model.id} value={model.id}>{model.name}</option>
                                    ))}
                                </select>
                            </div>
                        </motion.aside>
                    )}
                </AnimatePresence>
    
                {/* Main Content */}
                <div className={`flex-grow flex flex-col w-full transition-all duration-300 ${sidebarOpen ? 'md:ml-80' : ''}`}>
                    {/* Header */}
                    <header className={`flex justify-between items-center p-4 ${darkMode ? 'bg-gray-800' : 'bg-white'} shadow-md`}>
                        <motion.button
                            whileHover={{ scale: 1.1 }}
                            whileTap={{ scale: 0.9 }}
                            onClick={() => setSidebarOpen(!sidebarOpen)}
                            className={`${darkMode ? 'bg-gray-700 hover:bg-gray-600' : 'bg-gray-200 hover:bg-gray-300'} rounded-full p-3 transition-colors duration-300 shadow-lg`}
                        >
                            <Menu size={24} />
                        </motion.button>
                        <motion.button
                            whileHover={{ scale: 1.1 }}
                            whileTap={{ scale: 0.9 }}
                            onClick={toggleDarkMode}
                            className={`${darkMode ? 'bg-gray-700 hover:bg-gray-600' : 'bg-gray-200 hover:bg-gray-300'} rounded-full p-3 transition-colors duration-300 shadow-lg`}
                        >
                            {darkMode ? <Sun size={24} /> : <Moon size={24} />}
                        </motion.button>
                    </header>
    
                    {/* Chat Area or Welcome Screen */}
                    <div ref={chatContainerRef} className="flex-grow overflow-y-auto p-6 space-y-6">
                        {currentChat ? (
                            // Chat messages
                            chats.find(chat => chat.id === currentChat)?.messages.map((message, index) => (
                                <motion.div
                                    key={index}
                                    initial={{ opacity: 0, y: 20 }}
                                    animate={{ opacity: 1, y: 0 }}
                                    transition={{ duration: 0.5 }}
                                    className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
                                >
                                    <div
                                        className={`flex items-start space-x-4 max-w-3xl w-full ${message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''
                                            }`}
                                    >
                                        <div className={`p-3 rounded-2xl shadow-lg ${message.role === 'user'
                                                ? darkMode ? 'bg-indigo-600' : 'bg-indigo-500'
                                                : darkMode ? 'bg-gray-700' : 'bg-gray-300'
                                            } hidden lg:block`}>
                                            {message.role === 'user' ? (
                                                <User size={24} className="text-white" />
                                            ) : (
                                                <Bot size={24} className={darkMode ? 'text-indigo-400' : 'text-indigo-600'} />
                                            )}
                                        </div>
                                        <div
                                            className={`p-4 rounded-2xl w-[90%] ${message.role === 'user'
                                                    ? darkMode ? 'bg-indigo-500' : 'bg-indigo-100 text-gray-900'
                                                    : darkMode ? 'bg-gray-800' : 'bg-white text-gray-900'
                                                } shadow-lg`}
                                        >
                                         <div dangerouslySetInnerHTML={{ __html: message.content }} />
                                        </div>
                                    </div>
                                </motion.div>
                            ))
                        ) : (
                            // Welcome screen
                            <motion.div
                                initial={{ opacity: 0 }}
                                animate={{ opacity: 1 }}
                                transition={{ duration: 1 }}
                                className="flex flex-col items-center justify-center h-full text-center"
                            >
                                <h1 className="text-5xl font-bold mb-6">
                                    <span className="bg-clip-text text-transparent bg-gradient-to-r from-purple-400 via-pink-500 to-red-500">
                                        Welcome to AI Chat
                                    </span>
                                </h1>
                                <p className="text-2xl mb-8 max-w-2xl">
                                    Experience the power of AI-driven conversations
                                </p>
                                <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
                                    <div className="p-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl shadow-lg">
                                        <h2 className="text-3xl font-semibold mb-4">Intelligent</h2>
                                        <p>Powered by advanced language models</p>
                                    </div>
                                    <div className="p-6 bg-gradient-to-br from-green-500 to-teal-600 rounded-xl shadow-lg">
                                        <h2 className="text-3xl font-semibold mb-4">Versatile</h2>
                                        <p>Capable of handling a wide range of topics</p>
                                    </div>
                                    <div className="p-6 bg-gradient-to-br from-yellow-500 to-orange-600 rounded-xl shadow-lg">
                                        <h2 className="text-3xl font-semibold mb-4">Fast</h2>
                                        <p>Get instant responses to your queries</p>
                                    </div>
                                    <div className="p-6 bg-gradient-to-br from-red-500 to-pink-600 rounded-xl shadow-lg">
                                        <h2 className="text-3xl font-semibold mb-4">Secure</h2>
                                        <p>Your conversations are private and protected</p>
                                    </div>
                                </div>
                                <button
                                    onClick={handleNewChat}
                                    className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-4 px-8 rounded-xl text-xl transition-all duration-300 transform hover:scale-105 shadow-lg"
                                >
                                    Start a New Chat
                                </button>
                            </motion.div>
                        )}
                        <div ref={messagesEndRef} />
                    </div>
    
                    {/* Scroll to Bottom Button */}
                    <AnimatePresence>
                        {showScrollButton && (
                            <motion.button
                                initial={{ opacity: 0, y: 20 }}
                                animate={{ opacity: 1, y: 0 }}
                                exit={{ opacity: 0, y: 20 }}
                                transition={{ duration: 0.2 }}
                                onClick={scrollToBottom}
                                className={`fixed bottom-40 right-6 p-3 rounded-full shadow-lg ${darkMode ? 'bg-gray-700 hover:bg-gray-600' : 'bg-gray-200 hover:bg-gray-300'
                                    } transition-colors duration-300`}
                            >
                                <ArrowDown size={24} />
                            </motion.button>
                        )}
                    </AnimatePresence>
    
                    {/* Loading Indicator and Stop Button */}
                    <AnimatePresence>
                        {isLoading && (
                            <motion.div
                                initial={{ opacity: 0, y: 20 }}
                                animate={{ opacity: 1, y: 0 }}
                                exit={{ opacity: 0, y: 20 }}
                                transition={{ duration: 0.2 }}
                                className={`fixed bottom-40 right-6 p-4 rounded-xl shadow-lg ${darkMode ? 'bg-gray-700' : 'bg-white'
                                    } flex items-center space-x-4`}
                            >
                                <div className="animate-spin rounded-full h-6 w-6 border-t-2 border-b-2 border-indigo-500"></div>
                                <span>AI is thinking...</span>
                                <button
                                    onClick={handleStopResponse}
                                    className={`p-2 rounded-full ${darkMode ? 'bg-red-600 hover:bg-red-700' : 'bg-red-500 hover:bg-red-600'
                                        } transition-colors duration-300`}
                                >
                                    <X size={20} />
                                </button>
                            </motion.div>
                        )}
                    </AnimatePresence>
    
                    {/* Input Area */}
                    {currentChat && (
                        <div className={`p-6 ${darkMode ? 'bg-gray-800 bg-opacity-50' : 'bg-white bg-opacity-50'} backdrop-blur-lg border-t ${darkMode ? 'border-gray-700' : 'border-gray-200'}`}>
                            {chats.find(chat => chat.id === currentChat)?.messages.length === 0 && (
                                <motion.div
                                    initial={{ opacity: 0, y: 20 }}
                                    animate={{ opacity: 1, y: 0 }}
                                    transition={{ duration: 0.5 }}
                                    className="mb-6"
                                >
                                    <h3 className={`text-xl font-semibold mb-4 ${darkMode ? 'text-indigo-300' : 'text-indigo-600'}`}>Example prompts:</h3>
                                    <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                                        {examplePrompts.map((prompt, index) => (
                                            <motion.button
                                                key={index}
                                                whileHover={{ scale: 1.05 }}
                                                whileTap={{ scale: 0.95 }}
                                                onClick={() => setInput(prompt)}
                                                className={`text-left p-4 ${darkMode ? 'bg-gray-700 hover:bg-gray-600' : 'bg-gray-200 hover:bg-gray-300'} rounded-xl transition-all duration-300 shadow-md`}
                                            >
                                                {prompt}
                                            </motion.button>
                                        ))}
                                    </div>
                                </motion.div>
                            )}
                            <form onSubmit={handleSendMessage} className="flex items-center space-x-4 mb-4">
                                <input
                                    type="text"
                                    value={input}
                                    onChange={(e) => setInput(e.target.value)}
                                    placeholder="Type your message..."
                                    disabled={isLoading}
                                    className={`flex-grow p-4 ${darkMode ? 'bg-gray-700 text-white' : 'bg-gray-200 text-gray-900'} rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all duration-300 ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
                                />
                                <motion.button
                                    whileHover={{ scale: 1.05 }}
                                    whileTap={{ scale: 0.95 }}
                                    type="submit"
                                    disabled={isLoading}
                                    className={`${darkMode ? 'bg-indigo-600 hover:bg-indigo-700' : 'bg-indigo-500 hover:bg-indigo-600'} text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 shadow-lg flex items-center space-x-2 ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
                                >
                                    <Send size={20} />
                                    <span className="hidden md:inline">Send</span>
                                </motion.button>
                            </form>
                            <div className={`text-xs ${darkMode ? 'text-gray-400' : 'text-gray-600'} mt-4`}>
                                <p>
                                    <Shield size={14} className="inline mr-1" />
                                    By using this service, you agree to our <a href="#" className="underline">Terms of Service</a> and <a href="#" className="underline">Privacy Policy</a>.
                                </p>
                            </div>
                        </div>
                    )}
                </div>
            </div>
        )
    }

    Key Features in the Code:

  • Sidebar Toggle: Manage multiple chats.
  • Dark Mode: A toggle for switching themes.
  • Smooth Scroll: Automatically scroll to the latest message.
  • Integration

    Start Flask Backend

    python server.py

    Start React Front-End

    npm start

    This chat app demonstrates how to use the Gemini API to integrate React with a Flask-based backend to provide intelligent responses. Its advanced features, such as dark mode, multiple chat handling, and fluid animations, make it an excellent full-stack solution. Add features like user authentication and message storage to this app to help you learn more!