diff --git a/package-lock.json b/package-lock.json index f6f1b71..80882c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "react-router-dom": "^6.23.1", "react-scripts": "^5.0.1", "react-select": "^5.8.3", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "xlsx": "^0.18.5" }, "devDependencies": { "gh-pages": "^6.2.0" @@ -4474,6 +4475,15 @@ "node": ">=8.9" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -5551,6 +5561,19 @@ "node": ">=4" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5763,6 +5786,15 @@ "node": ">=4" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -6001,6 +6033,18 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8452,6 +8496,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -15380,6 +15433,18 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -17365,6 +17430,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17771,6 +17854,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", diff --git a/package.json b/package.json index 99f04d5..55c5772 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "react-router-dom": "^6.23.1", "react-scripts": "^5.0.1", "react-select": "^5.8.3", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "xlsx": "^0.18.5" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.css b/src/App.css index 95906ef..2fec97d 100644 --- a/src/App.css +++ b/src/App.css @@ -242,16 +242,97 @@ td { } .other-controls { + margin-bottom: 0.5rem; +} + +.action-buttons { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + gap: 0.5rem; margin-bottom: 1rem; + overflow: visible; +} + +.clear-filters-button, +.download-button { + background-color: #007bff; + color: #fff; + border: none; + padding: 0.3rem 0.75rem; + font-size: 0.85rem; + border-radius: 3px; + cursor: pointer; +} + +.clear-filters-button:hover, +.download-button:hover { + background-color: #0056b3; +} + +.download-dropdown { + position: relative; + display: inline-block; +} + +.download-panel { + position: fixed; + z-index: 1000; + background: #fff; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 0.8rem; + min-width: 280px; +} + +.download-panel-body { + display: flex; + gap: 1.5rem; } -.clear-filters-button { +.download-panel-heading { + font-size: 0.75rem; + font-weight: 600; + color: #666; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.5rem; +} + +.download-panel-scope label, +.download-panel-formats label { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.25rem 0; + cursor: pointer; + font-size: 0.9rem; + color: #333; + white-space: nowrap; +} + +.download-panel-action { + display: block; + width: 100%; + margin-top: 0.8rem; + padding: 0.45rem 0; background-color: #007bff; color: #fff; border: none; - padding: 0.5rem 1rem; + border-radius: 3px; cursor: pointer; - margin-left: 1rem; + font-size: 0.9rem; +} + +.download-panel-action:hover:not(:disabled) { + background-color: #0056b3; +} + +.download-panel-action:disabled { + background-color: #ccc; + cursor: not-allowed; } .section { diff --git a/src/Table/CSVTable.jsx b/src/Table/CSVTable.jsx index 7afd893..8b3c720 100644 --- a/src/Table/CSVTable.jsx +++ b/src/Table/CSVTable.jsx @@ -1,11 +1,12 @@ // src/Table/CSVTable.jsx -import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import Papa from 'papaparse'; import { calculateAverage, getGlobalAverage } from './Averaging'; import { useTable } from "./SortTable"; import { getModelInfo, getVariantGroup } from './modelLinks'; import { useSearchParams } from 'react-router-dom'; import Select from 'react-select'; +import { buildCurrentViewData, buildFullData, downloadCSV, downloadJSON, downloadExcel } from './downloadTable'; const CSVTable = ({dateStr}) => { @@ -24,7 +25,25 @@ const CSVTable = ({dateStr}) => { const [showVariants, setShowVariants] = useState(false); const [showHighUnseenBias, setShowHighUnseenBias] = useState(true); - const updateURL = (checkedCategories, newFilter, newSortField = null, newSortOrder = null, newShowProvider = null, newShowApiName = null, newShowReasoners = null, newShowOpenWeights = null, newShowVariants = null, newShowHighUnseenBias = null, newSearchQuery = null) => { + const [showDownloadMenu, setShowDownloadMenu] = useState(false); + const [downloadScope, setDownloadScope] = useState('current'); + const [downloadFormats, setDownloadFormats] = useState({ csv: false, json: false, xlsx: false }); + const [panelPos, setPanelPos] = useState({ top: 0, left: 0 }); + const downloadMenuRef = useRef(null); + const downloadBtnRef = useRef(null); + + useEffect(() => { + if (!showDownloadMenu) return; + const handleClickOutside = (e) => { + if (downloadMenuRef.current && !downloadMenuRef.current.contains(e.target)) { + setShowDownloadMenu(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [showDownloadMenu]); + + const updateURL =(checkedCategories, newFilter, newSortField = null, newSortOrder = null, newShowProvider = null, newShowApiName = null, newShowReasoners = null, newShowOpenWeights = null, newShowVariants = null, newShowHighUnseenBias = null, newSearchQuery = null) => { const params = new URLSearchParams(); let allAverages = true; @@ -426,6 +445,18 @@ const CSVTable = ({dateStr}) => { updateURL(defaultCategories, {}, 'ga', 'desc', true, false, true, false, false, false, ''); } + const handleDownload = () => { + const rows = downloadScope === 'current' + ? buildCurrentViewData(displayedData, checkedCategories, categories, { showProvider, showApiName, displayNameCounts }) + : buildFullData(data, categories); + const filename = `livebench_${downloadScope === 'current' ? 'current_view' : 'full_data'}`; + if (downloadFormats.csv) downloadCSV(rows, `${filename}.csv`); + if (downloadFormats.json) downloadJSON(rows, `${filename}.json`); + if (downloadFormats.xlsx) downloadExcel(rows, `${filename}.xlsx`); + setDownloadFormats({ csv: false, json: false, xlsx: false }); + setShowDownloadMenu(false); + }; + // Utility to compute class for sorting const getSortClass = (accessor) => { return sortField === accessor ? (sortOrder === "asc" ? "up" : "down") : "default"; @@ -559,7 +590,58 @@ const CSVTable = ({dateStr}) => { setShowHighUnseenBias(!showHighUnseenBias)} id="showHighUnseenBias" /> Show High Unseen Question Bias Models + +