import { useState, useRef, useEffect } from 'react' export default function App() { const [tab, setTab] = useState('local') // 'local' | 'drive' const [files, setFiles] = useState([]) const [driveUrl, setDriveUrl] = useState('') const [isDragging, setIsDragging] = useState(false) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) // Results const [jobId, setJobId] = useState(null) const [stats, setStats] = useState(null) const [driveApiReady, setDriveApiReady] = useState(null) const fileInputRef = useRef(null) const folderInputRef = useRef(null) const [params, setParams] = useState({ ppm: 15, sn: 1.5, min_pw: 5, max_pw: 20, pref_k: 0, pref_i: 0 }) useEffect(() => { if (tab === 'drive' && driveApiReady === null) { fetch('/api/gdrive-status') .then(r => r.json()) .then(d => setDriveApiReady(d.configured)) .catch(() => setDriveApiReady(false)) } }, [tab, driveApiReady]) // ── Handlers ───────────────────────────────────────────────────────────── const validateAndAdd = (selected) => { setError(null) setJobId(null) const valid = Array.from(selected).filter(f => { const n = f.name.toLowerCase() return n.endsWith('.mzml') || n.endsWith('.mzxml') }) const skipped = Array.from(selected).length - valid.length if (skipped > 0) setError(`${skipped} file(s) skipped — only .mzML / .mzXML allowed.`) if (valid.length > 0) setFiles(prev => { const existing = new Set(prev.map(f => f.name)) return [...prev, ...valid.filter(f => !existing.has(f.name))] }) } const clearAll = () => { setFiles([]); setJobId(null); setError(null) } const handleParamChange = (e) => { const { name, value } = e.target setParams(p => ({ ...p, [name]: value })) } // Drag & drop const onDragOver = e => { e.preventDefault(); setIsDragging(true) } const onDragLeave = e => { e.preventDefault(); setIsDragging(false) } const onDrop = e => { e.preventDefault(); setIsDragging(false); validateAndAdd(e.dataTransfer.files) } // ── Submission ────────────────────────────────────────────────────────── const startJob = async () => { if (tab === 'local' && files.length === 0) { setError('Please select files first.'); return } if (tab === 'drive' && !driveUrl.trim()) { setError('Please enter a Drive folder URL.'); return } setIsLoading(true); setError(null); setJobId(null) try { let response; if (tab === 'local') { const formData = new FormData() files.forEach(f => formData.append('files', f)) Object.keys(params).forEach(k => formData.append(k, params[k])) response = await fetch('/api/peak-picker/batch', { method: 'POST', body: formData }) } else { response = await fetch('/api/peak-picker/gdrive', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folderUrl: driveUrl, ...params }), }) } const data = await response.json(); if (!response.ok) throw new Error(data.error || 'Processing failed') setJobId(data.jobId) setStats({ totalFiles: data.total, successFiles: data.count, errors: data.errors || [] }) } catch (err) { setError(err.message) } finally { setIsLoading(false) } } // ── Render ────────────────────────────────────────────────────────────── const totalFiles = files.length; return (
LC-MS Batch Peak Picker · XCMS CentWave Engine
A Google Drive API key is required in HF Spaces Settings (Secrets). Please configure `GOOGLE_DRIVE_API_KEY`.
[1] Tautenhahn R, Böttcher C, Neumann S. (2008). Highly sensitive feature detection for high resolution LC/MS. BMC Bioinformatics, 9:504.
[2] Smith CA, et al. (2006). XCMS: processing mass spectrometry data for metabolite profiling using nonlinear peak alignment, matching, and identification. Anal. Chem., 78(3):779–787.