DEV Community

PrachiBhende
PrachiBhende

Posted on

Visualizing AI Prompt Responses in Chart Format Using React and Node.js

Artificial Intelligence (AI) has transformed how we handle data, turning raw information into clear, actionable insights. While AI excels at processing large datasets and generating responses, presenting this information in an easy-to-understand format is key. Data visualization helps bridge this gap by converting AI outputs into charts and graphs, making complex data easier to grasp. In this blog, we'll discuss the importance of visualizing AI responses, the best chart types for different data, and how to implement these visualizations in a web application.

Why Visualize AI Responses?

AI models, particularly those based on machine learning and natural language processing, generate insights that can be dense and hard to interpret in raw text form. Visualizations help bridge the gap between complex AI output and user comprehension by:

  • Simplifying Data Interpretation: Charts and graphs distill complex datasets into simple visual elements, making patterns and trends easier to identify.

  • Enhancing User Engagement: Interactive charts can make data exploration more engaging, allowing users to interact with and explore the data more deeply.

  • Facilitating Data-Driven Decisions: Visual representations of data help stakeholders quickly grasp critical insights, supporting faster and more informed decision-making.

Solution Overview

To achieve the desired functionality, the application is built using the following technologies:

Server Side: Node.js with Express.js for handling requests, OpenAI API for generating embeddings, Supabase as a vector store for embeddings.
Client Side: React.js for the UI, react-chartjs-2 for rendering charts.

Server-Side Implementation

Let's start with the server-side setup. The server handles two main tasks:

Processing and Training the Model:

  • Accepting and reading files in various formats.
  • Using OpenAI's API to generate embeddings for the text data.
  • Storing the embeddings in Supabase for future retrieval and querying.

Responding to User Queries:

  • Accepting user queries in natural language.
  • Fetching relevant data from the vector store based on embeddings.
  • Returning the results as a JSON response.

Step-by-Step Guide:

  • Setup Node.js and Express Server
npm install express multer supabase-js openai
Enter fullscreen mode Exit fullscreen mode
  • Save the confidential info in the .env file
SUPERBASE_API_KEY= YOUR_SUPERBASE_API_KEY
SUPERBASE_URL=YOUR_SUPERBASE_URL
OPEN_AI_API_KEY=YOUR_OPEN_AI_API_KEY
Enter fullscreen mode Exit fullscreen mode
  • Setup a basic express server
import express from 'express';
import multer from 'multer';
import { join, dirname } from 'path';
import { unlinkSync } from 'fs';
import { train } from './train.js';
import {  processPrompt } from './prompt.js';
import cors from 'cors';  // Use default import
import bodyParser from 'body-parser';
import { fileURLToPath } from 'url';
import fs from 'fs';

import dotenv from 'dotenv';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const app = express();
const port = 3001;
app.use(cors());

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function (req, file, cb) {
    cb(null, file.originalname);
  },
});

const upload = multer({ storage: storage });
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir);
}

app.use(bodyParser.json());

app.post('/train', upload.single('file'), async (req, res) => {
  const filePath = join(__dirname, req.file.path);
  try {
    await train(filePath);
    unlinkSync(filePath); // Clean up the uploaded file
    res.status(200).send('Training data uploaded and processed');
  } catch (error) {
    res.status(500).send(`Error training model: ${error.message}`);
  }
});

app.post('/processPrompt', async (req, res) => {
  try {
    const question = req.body.question;
    if (!question) {
      return res.status(400).send('Question is required');
    }

    const response = await processPrompt(question);

    res.status(200).send({ response });
  } catch (error) {
    res.status(500).send(`Error processing prompt: ${error.message}`);
  }
});

app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
});

Enter fullscreen mode Exit fullscreen mode
  • Handling File Uploads and Processing:

  • Use multer for handling file uploads.

  • Read and parse the content of .txt, .xlsx, .xls, and .pdf files.

  • Generate embeddings using the OpenAI API.

Example code snippet for processing files:

import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import fs from 'fs/promises';
import { createClient } from '@supabase/supabase-js';
import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase';
import { OpenAIEmbeddings } from '@langchain/openai';
import xlsx from 'xlsx'; 

import dotenv from 'dotenv';
dotenv.config();

async function train(filepath) {
    try {
        const superbase_api_key = process.env.SUPERBASE_API_KEY;
        const superbase_url = process.env.SUPERBASE_URL;
        const openAIApiKey = process.env.OPEN_AI_API_KEY;

        const splitter = new RecursiveCharacterTextSplitter({
            chunkSize: 500,
            separators: ['\n\n', '\n', ' ', ''],
            chunkOverlap: 50
        });
        let text = '';
        if (filepath.endsWith('.xlsx') || filepath.endsWith('.xls')) {
            const workbook = xlsx.readFile(filepath);
            const sheetNames = workbook.SheetNames;
            const firstSheet = workbook.Sheets[sheetNames[0]];
            const jsonData = xlsx.utils.sheet_to_json(firstSheet, { header: 1 });

            text = jsonData.map(row => row.join(' ')).join('\n');
        } else {
            text = await fs.readFile(filepath, 'utf-8');
        }

        const output = await splitter.createDocuments([text]);
        console.log(output);

        const client = createClient(superbase_url, superbase_api_key);
        console.log(client);

        await retryAsyncOperation(async () => {
            await SupabaseVectorStore.fromDocuments(
                output,
                new OpenAIEmbeddings({ openAIApiKey }),
                {
                    client,
                    tableName: 'documents',
                }
            );
        });

    } catch (err) {
        console.log(err);
    }
}

async function retryAsyncOperation(operation, retries = 5, delay = 1000) {
    for (let i = 0; i < retries; i++) {
      try {
        await operation();
        return;
      } catch (err) {
        console.error(`Error on attempt ${i + 1}: ${err.message}`);
        if (err.response) {
          console.error('Supabase response:', err.response.data);
        }
        if (i < retries - 1) {
          console.log(`Retrying... (${i + 1}/${retries})`);
          await new Promise(resolve => setTimeout(resolve, delay));
        } else {
          throw err;
        }
      }
    }
  }
export { train};
Enter fullscreen mode Exit fullscreen mode
  • Handling User Queries:

  • Accept user queries from the client side.

  • Retrieve relevant embeddings from Supabase.

  • Use the retrieved data to generate responses.

Example code snippet for handling queries:

import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { retriever } from './utils/retriever.js';
import { combineDocuments } from './utils/combineDocuments.js';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RunnableSequence, RunnablePassthrough } from '@langchain/core/runnables';
import dotenv from 'dotenv';
dotenv.config();

async function processPrompt(question) {
    const openAIApiKey = process.env.OPEN_AI_API_KEY;

    const standaloneQuestionTemplate = 'Given a question, convert it to a standalone question. question: {question} standalone question:';

    const answerTemplate = `You are a chart generator bot that generates accurate charts based on the provided context. Use only the provided context to generate the final answer. Do not invent or assume any information not explicitly stated in the context. While generating the final answer, please follow these guidelines:
        1. Your response must always be in valid JSON format, which can be parsed as valid JSON. Do not include backticks, newlines, or extra spaces in the JSON structure. Use the following structure for your response object and do not change the names of the keys: chartType, title, labels, values. chartType is a string, title is a string, labels is an array of strings, and values is an array of numbers. In case if any value is unavailable then set it to 0.
        2. If chartType isn't specified in the question, use "bar" as default.
        3. DO NOT include "chart" or any other keyword in the chartType string.
        4. If you do not find any valid information associated with the user's question in the provided Context, or if the Context does not contain sufficient data to answer the question accurately, return No data found
        5. Ensure that the response only includes data relevant to the specific query and is fully supported by the provided context, if requested then please to calculations as well.
        6. Do not make assumptions or generate data that is not explicitly provided in the Context.

        Context: {context}
        Question: {question}
        Answer:`;

    try {
        const llm = new ChatOpenAI({ openAIApiKey, verbose: true });
        const standaloneQuestionPrompt = PromptTemplate.fromTemplate(standaloneQuestionTemplate);
        const answerPrompt = PromptTemplate.fromTemplate(answerTemplate);

        const standaloneQuestionChain = RunnableSequence.from([standaloneQuestionPrompt, llm, new StringOutputParser()]);

        const retrieverChain = RunnableSequence.from([
            standaloneQuestionChain, 
            retriever, 
            combineDocuments 
        ]);

        const answerChain = RunnableSequence.from([answerPrompt, llm, new StringOutputParser()]);

        const chain = RunnableSequence.from([
            {
                context: retrieverChain, 
                question: ({ question }) => question 
            },
            answerChain 
        ]);

        const response = await chain.invoke({
            question: question,
            verbose: true
        });

        let finalResponse = {
            statusCode: 200,
            body: response
        };

        return finalResponse;
    } catch (err) {
        console.error("There is an error in the code:", err);
        return {
            statusCode: 500,
            body: 'Internal server error'
        };
    }
}

export { processPrompt };
Enter fullscreen mode Exit fullscreen mode
  • Actual retriever function
import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase'
import { OpenAIEmbeddings } from '@langchain/openai'
import { createClient } from '@supabase/supabase-js'
import dotenv from 'dotenv';
dotenv.config();
const openAIApiKey = process.env.OPEN_AI_API_KEY

const embeddings = new OpenAIEmbeddings({ openAIApiKey })
const sbApiKey = process.env.SUPERBASE_API_KEY
const sbUrl = process.env.SUPERBASE_URL
const client = createClient(sbUrl, sbApiKey)

const vectorStore = new SupabaseVectorStore(embeddings, {
    client,
    tableName: 'documents',
    queryName: 'match_documents'

})

const retriever = vectorStore.asRetriever()

export { retriever }
Enter fullscreen mode Exit fullscreen mode

Client-Side Implementation

The client side is built using React.js. It sends user queries to the server, receives the response, and dynamically visualizes the data using react-chartjs-2.

Step-by-Step Guide:

  • Setup React Application:
npm install axios chart.js react-chartjs-2
Enter fullscreen mode Exit fullscreen mode
  • Fetching Data from the Server: Use axios to make API requests to the server for uploading files and querying data.
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { Chart as ChartJS, registerables } from 'chart.js';
import ChartComponent from './ChartComponent';
import './PromptInput.css';

ChartJS.register(...registerables);

const PromptInput = () => {
  const [input, setInput] = useState('');
  const [chatHistory, setChatHistory] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [isButtonDisabled, setIsButtonDisabled] = useState(true);
  const chatContainerRef = useRef(null);

  useEffect(() => {
    setIsButtonDisabled(input.trim() === '');
  }, [input]);

  useEffect(() => {
    if (chatContainerRef.current) {
      chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
    }
  }, [chatHistory]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    if (!input.trim()) {
      setError('Please enter a question.');
      setLoading(false);
      return;
    }

    setChatHistory((prevHistory) => [
      ...prevHistory,
      { sender: 'user', message: input },
    ]);

    try {
      const res = await axios.post('http://localhost:3001/processPrompt', { question: input });
      let result = res?.data?.response;
      console.log("result", result);
      console.log("type of result", typeof result.body);

      if (result && result.statusCode === 200 && result?.body) {
        let body = result.body !== 'No data found' && typeof result?.body === 'string' ? JSON.parse(result.body) : result.body;
        console.log('body:', body);

        const hasValidData = body.values && body.values.length > 0 && body.values.some(value => value !== 0);

        if (hasValidData) {
          setChatHistory((prevHistory) => [
            ...prevHistory,
            { sender: 'bot', message: `Chart generated for: ${body.title}`, chartData: body },
          ]);
        } else {
          setChatHistory((prevHistory) => [
            ...prevHistory,
            { sender: 'bot', message: 'No data found.' },
          ]);
        }
      } else {
        setError('An error occurred while fetching the response. Please try again.');
      }
    } catch (error) {
      console.error('Error:', error);
      setError('An error occurred while fetching the response. Please try again.');
    } finally {
      setLoading(false);
      setInput('');
    }
  };

  return (
    <div className="App">
      <div className="chat-container" ref={chatContainerRef}>
        {chatHistory.map((chat, index) => (
          <div
            key={index}
            className={chat.sender === 'user' ? 'user-message' : 'bot-message'}
          >
            <p>{chat.message}</p>
            {chat.sender === 'bot' && chat.chartData && (
              <ChartComponent chartData={chat.chartData} />
            )}
          </div>
        ))}
      </div>

      {error && <p className="error-message">{error}</p>}

      <form className="input-container" onSubmit={handleSubmit}>
        <input
          type="textarea"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Ask a question..."
          className="prompt-text"
        />
        <button type="submit" disabled={isButtonDisabled || loading}>
          {loading ? 'Loading...' : 'Submit'}
        </button>
      </form>
    </div>
  );
};

export default PromptInput;

Enter fullscreen mode Exit fullscreen mode
  • Visualizing Data with react-chartjs-2:

  • Use different chart types (Bar, Line, Pie, etc.) to visualize data based on user queries.

  • Customize chart options for better data representation.
    Example chart setup:

import React, { useEffect, useRef } from 'react';
import { Chart } from 'chart.js';

const ChartComponent = ({ chartData }) => {
  const chartRef = useRef(null);
  const chartInstanceRef = useRef(null);

  useEffect(() => {
    if (!chartData || !chartRef.current) return;

    if (chartInstanceRef.current) {
      chartInstanceRef.current.destroy();
    }

    const generateColors = (numColors) => {
      const colors = [];
      for (let i = 0; i < numColors; i++) {
        const r = Math.floor(Math.random() * 255);
        const g = Math.floor(Math.random() * 255);
        const b = Math.floor(Math.random() * 255);
        colors.push(`rgba(${r}, ${g}, ${b}, 0.6)`);
      }
      return colors;
    };

    const backgroundColors = generateColors(chartData.values.length);
    const borderColors = backgroundColors.map(color => color.replace('0.6', '1')); 

    chartInstanceRef.current = new Chart(chartRef.current, {
      type: chartData.chartType,
      data: {
        labels: chartData.labels,
        datasets: [
          {
            label: chartData.title,
            data: chartData.values,
            backgroundColor: backgroundColors,
            borderColor: borderColors,
            borderWidth: 1,
          },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false, 
        scales: {
          y: {
            beginAtZero: true
          }
        }
      },
    });
  }, [chartData]);

  return (
    <div style={{ width: '100%', height: '300px' }}> 
      <canvas ref={chartRef} style={{ width: '100%', height: '100%' }} />
    </div>
  );
};

export default ChartComponent;
Enter fullscreen mode Exit fullscreen mode

Conclusion

Visualizing AI responses in chart format provides a powerful way to convey complex insights more intuitively and effectively. By leveraging popular libraries like react-chartjs-2 in a React application, developers can create dynamic and interactive data visualizations that enhance user experience and support data-driven decisions.
As AI continues to evolve, integrating visualization into AI-driven applications will become increasingly important. Start experimenting with different chart types and libraries today to make the most of your AI insights!

Top comments (0)