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 */}
{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;