import React, { useState, useEffect, useRef } from 'react'; // Plotly.js CDN import - Plotly is globally available after this script loads // Tailwind CSS is assumed to be available // lucide-react for icons const App = () => { const [csvData, setCsvData] = useState(null); const [insights, setInsights] = useState({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isPlotlyLoaded, setIsPlotlyLoaded] = useState(false); const chartRefs = useRef({}); const dashboardRef = useRef(null); // Ref for the entire dashboard content to export // Dynamically load Plotly.js useEffect(() => { const script = document.createElement('script'); script.src = 'https://cdn.plot.ly/plotly-2.32.0.min.js'; script.onload = () => { setIsPlotlyLoaded(true); console.log("Plotly.js loaded successfully."); }; script.onerror = () => { setError("Failed to load Plotly.js. Please check your internet connection."); }; document.head.appendChild(script); // Dynamically load html2pdf.js const html2pdfScript = document.createElement('script'); html2pdfScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js'; html2pdfScript.onload = () => { console.log("html2pdf.js loaded successfully."); }; html2pdfScript.onerror = () => { setError("Failed to load html2pdf.js. PDF export might not work."); }; document.head.appendChild(html2pdfScript); // Cleanup scripts on component unmount return () => { document.head.removeChild(script); document.head.removeChild(html2pdfScript); }; }, []); // Function to normalize column names const normalizeColumnName = (name) => { return name.toLowerCase().replace(/[^a-z0-9]/g, ''); }; // Handle CSV file upload and data cleaning const handleFileUpload = (event) => { const file = event.target.files[0]; console.log("File selected:", file); if (!file) { setError("No file selected."); console.log("No file selected."); return; } if (file.type !== 'text/csv') { setError("Please upload a CSV file."); console.log("Incorrect file type:", file.type); return; } setLoading(true); setError(null); setCsvData(null); setInsights({}); const reader = new FileReader(); reader.onload = (e) => { console.log("FileReader onload triggered."); try { const text = e.target.result.trim(); // Trim overall text const lines = text.split('\n').filter(line => line.trim() !== ''); console.log("Lines parsed:", lines.length); if (lines.length === 0) { throw new Error("CSV file is empty or contains no valid data."); } const headerLine = lines[0]; const rawHeaders = headerLine.split(',').map(h => h.trim()); const normalizedHeaders = rawHeaders.map(normalizeColumnName); console.log("Headers:", rawHeaders, "Normalized:", normalizedHeaders); // Define expected normalized headers const expectedHeaders = ['date', 'platform', 'sentiment', 'location', 'engagements', 'mediatype']; const missingHeaders = expectedHeaders.filter(header => !normalizedHeaders.includes(header)); if (missingHeaders.length > 0) { throw new Error(`Missing required CSV columns: ${missingHeaders.join(', ')}. Please ensure your CSV has 'Date', 'Platform', 'Sentiment', 'Location', 'Engagements', 'Media Type' columns.`); } const parsedData = lines.slice(1).map(line => { const values = line.split(',').map(v => v.trim()); const row = {}; normalizedHeaders.forEach((header, index) => { row[header] = values[index]; }); return row; }); console.log("Raw parsed data rows:", parsedData.length); const cleaned = cleanData(parsedData); setCsvData(cleaned); console.log("Cleaned data rows:", cleaned.length); } catch (err) { setError(`Error processing file: ${err.message}`); console.error("File processing error in onload:", err); } finally { setLoading(false); } }; reader.onerror = (e) => { setError(`Failed to read file: ${reader.error ? reader.error.message : 'Unknown error'}`); console.error("FileReader error:", reader.error); setLoading(false); }; reader.readAsText(file); }; // Clean and normalize data const cleanData = (data) => { return data.map(row => { const cleanedRow = {}; for (const key in row) { cleanedRow[normalizeColumnName(key)] = row[key]; } // Convert 'date' to datetime // Assuming 'date' is in a format parseable by Date constructor, e.g.,YYYY-MM-DD try { cleanedRow.date = new Date(cleanedRow.date); if (isNaN(cleanedRow.date.getTime())) { // Check for invalid date throw new Error(`Invalid date format for: ${row.date}`); } } catch (e) { console.warn(`Could not parse date '${row.date}', setting to null. Error: ${e.message}`); cleanedRow.date = null; // Set to null if invalid } // Fill missing 'engagements' with 0 cleanedRow.engagements = cleanedRow.engagements ? parseInt(cleanedRow.engagements, 10) : 0; if (isNaN(cleanedRow.engagements)) { console.warn(`Invalid engagement value for: ${row.engagements}, setting to 0.`); cleanedRow.engagements = 0; } return cleanedRow; }).filter(row => row.date !== null); // Filter out rows with invalid dates }; // Generate a single chart using Plotly const renderChart = (chartId, data, layout, config = {}) => { if (isPlotlyLoaded && csvData && chartRefs.current[chartId]) { Plotly.react(chartRefs.current[chartId], data, layout, config); } }; // Effect to render all charts and generate insights once data is loaded and Plotly is ready useEffect(() => { if (csvData && isPlotlyLoaded) { generateAllCharts(); generateAllInsights(); } }, [csvData, isPlotlyLoaded]); // Dependencies: re-run when csvData or Plotly loading state changes const generateAllCharts = () => { // Chart 1: Sentiment Breakdown (Pie Chart) const sentimentCounts = csvData.reduce((acc, row) => { acc[row.sentiment] = (acc[row.sentiment] || 0) + 1; return acc; }, {}); const sentimentData = [{ labels: Object.keys(sentimentCounts), values: Object.values(sentimentCounts), type: 'pie', hole: .4, marker: { colors: ['#34D399', '#FCD34D', '#EF4444', '#60A5FA', '#A78BFA'], // Custom colors for sentiment } }]; const sentimentLayout = { title: { text: 'Sentiment Breakdown', font: { family: 'Inter', size: 24, color: '#333' } }, height: 400, width: '100%', margin: { t: 50, b: 50, l: 50, r: 50 }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', legend: { font: { family: 'Inter', size: 12 }, orientation: 'h', // Horizontal legend x: 0.5, y: -0.1, // Position legend below plot xanchor: 'center' } }; renderChart('sentiment-chart', sentimentData, sentimentLayout); // Chart 2: Engagement Trend over time (Line Chart) const engagementTrend = csvData.sort((a, b) => a.date - b.date).reduce((acc, row) => { const dateStr = row.date.toISOString().split('T')[0]; //YYYY-MM-DD if (!acc[dateStr]) { acc[dateStr] = 0; } acc[dateStr] += row.engagements; return acc; }, {}); const dates = Object.keys(engagementTrend); const engagements = Object.values(engagementTrend); const engagementTrendData = [{ x: dates, y: engagements, mode: 'lines+markers', name: 'Engagements', line: { color: '#60A5FA', width: 2 }, marker: { size: 6, color: '#60A5FA' } }]; const engagementTrendLayout = { title: { text: 'Engagement Trend Over Time', font: { family: 'Inter', size: 24, color: '#333' } }, xaxis: { title: 'Date', type: 'date', tickformat: '%Y-%m-%d', rangeslider: { visible: true }, // Add range slider gridcolor: '#e2e8f0' }, yaxis: { title: 'Total Engagements', gridcolor: '#e2e8f0' }, height: 400, width: '100%', margin: { t: 50, b: 50, l: 50, r: 50 }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', }; renderChart('engagement-trend-chart', engagementTrendData, engagementTrendLayout); // Chart 3: Platform Engagements (Bar Chart) const platformEngagements = csvData.reduce((acc, row) => { acc[row.platform] = (acc[row.platform] || 0) + row.engagements; return acc; }, {}); const platformData = [{ x: Object.keys(platformEngagements), y: Object.values(platformEngagements), type: 'bar', marker: { color: Object.keys(platformEngagements).map((_, i) => `hsl(${i * 60}, 70%, 60%)`), // Dynamic colors line: { color: '#fff', width: 1.5 } } }]; const platformLayout = { title: { text: 'Platform Engagements', font: { family: 'Inter', size: 24, color: '#333' } }, xaxis: { title: 'Platform', automargin: true }, yaxis: { title: 'Total Engagements' }, height: 400, width: '100%', margin: { t: 50, b: 50, l: 50, r: 50 }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', }; renderChart('platform-engagements-chart', platformData, platformLayout); // Chart 4: Media Type Mix (Pie Chart) const mediaTypeCounts = csvData.reduce((acc, row) => { acc[row.mediatype] = (acc[row.mediatype] || 0) + 1; return acc; }, {}); const mediaTypeData = [{ labels: Object.keys(mediaTypeCounts), values: Object.values(mediaTypeCounts), type: 'pie', hole: .4, marker: { colors: ['#A78BFA', '#FACC15', '#FB923C', '#DC2626', '#10B981'], // Custom colors } }]; const mediaTypeLayout = { title: { text: 'Media Type Mix', font: { family: 'Inter', size: 24, color: '#333' } }, height: 400, width: '100%', margin: { t: 50, b: 50, l: 50, r: 50 }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', legend: { font: { family: 'Inter', size: 12 }, orientation: 'h', // Horizontal legend x: 0.5, y: -0.1, // Position legend below plot xanchor: 'center' } }; renderChart('media-type-chart', mediaTypeData, mediaTypeLayout); // Chart 5: Top 5 Locations (Bar Chart) const locationEngagements = csvData.reduce((acc, row) => { acc[row.location] = (acc[row.location] || 0) + row.engagements; return acc; }, {}); const sortedLocations = Object.entries(locationEngagements).sort(([, a], [, b]) => b - a).slice(0, 5); const topLocations = sortedLocations.map(([location, _]) => location); const topLocationEngagements = sortedLocations.map(([_, engagements]) => engagements); const topLocationData = [{ x: topLocations, y: topLocationEngagements, type: 'bar', marker: { color: topLocations.map((_, i) => `hsl(${i * 70}, 80%, 50%)`), // Dynamic colors line: { color: '#fff', width: 1.5 } } }]; const topLocationLayout = { title: { text: 'Top 5 Locations by Engagements', font: { family: 'Inter', size: 24, color: '#333' } }, xaxis: { title: 'Location', automargin: true }, yaxis: { title: 'Total Engagements' }, height: 400, width: '100%', margin: { t: 50, b: 50, l: 50, r: 50 }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', }; renderChart('top-locations-chart', topLocationData, topLocationLayout); }; // Generate insights using Gemini API const generateAllInsights = async () => { setLoading(true); const newInsights = {}; // Helper function to call Gemini API const getGeminiInsight = async (promptText) => { const chatHistory = [{ role: "user", parts: [{ text: promptText }] }]; const payload = { contents: chatHistory }; const apiKey = ""; // This will be provided by the environment const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; try { const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); if (result.candidates && result.candidates.length > 0 && result.candidates[0].content && result.candidates[0].content.parts && result.candidates[0].content.parts.length > 0) { return result.candidates[0].content.parts[0].text; } else { console.error("Gemini API response structure unexpected:", result); return "Could not generate insights."; } } catch (apiError) { console.error("Error calling Gemini API:", apiError); return `Error generating insights: ${apiError.message}`; } }; // Prepare data summaries for insights const sentimentSummary = csvData.reduce((acc, row) => { acc[row.sentiment] = (acc[row.sentiment] || 0) + 1; return acc; }, {}); const engagementTimeSeries = csvData.sort((a, b) => a.date - b.date).map(row => ({ date: row.date.toISOString().split('T')[0], engagements: row.engagements })); const platformSummary = csvData.reduce((acc, row) => { acc[row.platform] = (acc[row.platform] || 0) + row.engagements; return acc; }, {}); const mediaTypeSummary = csvData.reduce((acc, row) => { acc[row.mediatype] = (acc[row.mediatype] || 0) + 1; return acc; }, {}); const locationSummary = csvData.reduce((acc, row) => { acc[row.location] = (acc[row.location] || 0) + row.engagements; return acc; }, {}); const sortedLocationSummary = Object.entries(locationSummary).sort(([, a], [, b]) => b - a); // Insight 1: Sentiment Breakdown newInsights.sentiment = await getGeminiInsight( `Given the following sentiment distribution from a media dataset: ${JSON.stringify(sentimentSummary)}. Provide 3 key insights about the overall sentiment. Focus on the most prevalent sentiments and any notable imbalances. Present insights as plain text, without any markdown formatting like bolding or bullet points.` ); // Insight 2: Engagement Trend over time newInsights.engagementTrend = await getGeminiInsight( `Analyze the following engagement data over time (Date, Engagements): ${JSON.stringify(engagementTimeSeries.slice(0, 50))}... (showing first 50 entries for brevity). Describe the trend of engagements over the period. Are there any peaks, troughs, or consistent patterns? Give 3 key insights. Present insights as plain text, without any markdown formatting like bolding or bullet points.` ); // Insight 3: Platform Engagements newInsights.platform = await getGeminiInsight( `Based on the total engagements per platform: ${JSON.stringify(platformSummary)}. What are the top platforms driving engagements? Are there any platforms significantly underperforming? Provide 3 key insights. Present insights as plain text, without any markdown formatting like bolding or bullet points.` ); // Insight 4: Media Type Mix newInsights.mediaType = await getGeminiInsight( `Given the distribution of media types: ${JSON.stringify(mediaTypeSummary)}. What are the most common media types used? Is there a significant preference for certain types? Give 3 key insights. Present insights as plain text, without any markdown formatting like bolding or bullet points.` ); // Insight 5: Top 5 Locations newInsights.topLocations = await getGeminiInsight( `Here are the top 5 locations by engagements: ${JSON.stringify(sortedLocationSummary.slice(0, 5))}. What does this data tell us about geographical engagement? Are there specific regions that are highly active? Provide 3 key insights. Present insights as plain text, without any markdown formatting like bolding or bullet points.` ); setInsights(newInsights); setLoading(false); }; // Function to export the dashboard as PDF const handleExportPdf = () => { if (!window.html2pdf) { setError("PDF export library not loaded. Please try again or check your internet connection."); return; } if (!dashboardRef.current) { setError("Could not find dashboard content to export."); return; } setLoading(true); setError(null); // Add a small delay to ensure all charts are fully rendered setTimeout(() => { // Options for PDF generation const options = { margin: 10, filename: 'Media_Intelligence_Dashboard.pdf', image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2.5, // Adjusted scale for better resolution and fit logging: true, dpi: 192, letterRendering: true, useCORS: true // Important for images/fonts loaded from CDNs if any }, jsPDF: { unit: 'pt', // Use points for precise A4 dimensions format: [595, 842], // A4 size in points (width, height) orientation: 'portrait' }, pagebreak: { mode: 'avoid-all', // Strictly avoid breaking elements before: '.page-break-before' // Custom class for page breaks } }; // Get the entire dashboard content to export const element = dashboardRef.current; window.html2pdf().from(element).set(options).save() .then(() => { setLoading(false); console.log("PDF exported successfully!"); }) .catch((err) => { setLoading(false); setError(`Failed to export PDF: ${err.message}`); console.error("PDF export error:", err); }); }, 500); // 500ms delay }; return (

Interactive Media Intelligence Dashboard

{/* Removed Export PDF Button */}

1. Upload Your CSV File

Please upload a CSV file with the following columns: Date, Platform, Sentiment, Location, Engagements, Media Type.

{loading && (
Processing data and generating insights...
)} {error && (
{error}
)}
{csvData && ( <>

2. Data Cleaning Summary

{/* This ul is correctly placed outside any p tag */}
  • 'Date' column converted to datetime objects. Invalid dates were filtered out.
  • Missing 'Engagements' values filled with 0.
  • Column names normalized (e.g., 'Media Type' became 'mediatype').

Successfully processed {csvData.length} rows of data.

3. Interactive Charts

{/* Chart 1: Sentiment Breakdown */}

Sentiment Breakdown

chartRefs.current['sentiment-chart'] = el} className="w-full h-96">

Top 3 Insights:

{insights.sentiment || "Generating insights..."}

{/* Chart 2: Engagement Trend over time */}

Engagement Trend Over Time

chartRefs.current['engagement-trend-chart'] = el} className="w-full h-96">

Top 3 Insights:

{insights.engagementTrend || "Generating insights..."}

{/* Chart 3: Platform Engagements */}

Platform Engagements

chartRefs.current['platform-engagements-chart'] = el} className="w-full h-96">

Top 3 Insights:

{insights.platform || "Generating insights..."}

{/* Chart 4: Media Type Mix */}

Media Type Mix

chartRefs.current['media-type-chart'] = el} className="w-full h-96">

Top 3 Insights:

{insights.mediaType || "Generating insights..."}

{/* Chart 5: Top 5 Locations */}

Top 5 Locations by Engagements

chartRefs.current['top-locations-chart'] = el} className="w-full h-96">

Top 3 Insights:

{insights.topLocations || "Generating insights..."}

)}
); }; export default App;