Spaces:
Running
Running
| // Minimal client-side CSV builder + browser download helper. | |
| // Detectability results come from the live backend (with an offline mock | |
| // fallback when it is unreachable); the CSV downloads are real files. | |
| export type CsvValue = string | number | boolean | null | undefined | |
| function escapeCell(value: unknown): string { | |
| if (value === null || value === undefined) return '' | |
| const s = String(value) | |
| if (/[",\n]/.test(s)) { | |
| return '"' + s.replace(/"/g, '""') + '"' | |
| } | |
| return s | |
| } | |
| /** Build a CSV string from an array of row objects, using `columns` order. */ | |
| export function toCSV<T>( | |
| rows: T[], | |
| columns?: { key: keyof T; header: string }[], | |
| ): string { | |
| if (rows.length === 0 && !columns) return '' | |
| const cols = | |
| columns ?? | |
| (Object.keys(rows[0] as object) as (keyof T)[]).map((k) => ({ | |
| key: k, | |
| header: String(k), | |
| })) | |
| const head = cols.map((c) => escapeCell(c.header)).join(',') | |
| const body = rows | |
| .map((row) => cols.map((c) => escapeCell(row[c.key])).join(',')) | |
| .join('\n') | |
| return head + '\n' + body | |
| } | |
| /** Trigger a browser download of `text` as `filename`. */ | |
| export function download( | |
| filename: string, | |
| text: string, | |
| mime = 'text/csv;charset=utf-8', | |
| ): void { | |
| const blob = new Blob([text], { type: mime }) | |
| const url = URL.createObjectURL(blob) | |
| const a = document.createElement('a') | |
| a.href = url | |
| a.download = filename | |
| document.body.appendChild(a) | |
| a.click() | |
| document.body.removeChild(a) | |
| URL.revokeObjectURL(url) | |
| } | |