diff --git a/.gitignore b/.gitignore index 2c9a7e2..59c8729 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ yarn-error.log* /localdata/ /venv/ /embeddings_backup.npz +/AGENTS.md +/CLAUDE.md diff --git a/Onboarding Wireframes.pdf b/Onboarding Wireframes.pdf new file mode 100644 index 0000000..027a040 Binary files /dev/null and b/Onboarding Wireframes.pdf differ diff --git a/app/api/sample-genotype/route.ts b/app/api/sample-genotype/route.ts new file mode 100644 index 0000000..d0a0370 --- /dev/null +++ b/app/api/sample-genotype/route.ts @@ -0,0 +1,195 @@ +import { NextResponse } from 'next/server'; +import { inflateRawSync } from 'node:zlib'; + +const SAMPLE_DATA_URL = 'https://drive.google.com/uc?export=download&id=1WK3zZbqmu3_m6LvoQCylyIbWBkoO5pGI'; + +const textDecoder = new TextDecoder(); + +function isZipPayload(bytes: Uint8Array): boolean { + return bytes.length >= 4 + && bytes[0] === 0x50 + && bytes[1] === 0x4b + && bytes[2] === 0x03 + && bytes[3] === 0x04; +} + +function findEndOfCentralDirectory(bytes: Uint8Array): number { + const minOffset = Math.max(0, bytes.length - 0xffff - 22); + + for (let offset = bytes.length - 22; offset >= minOffset; offset -= 1) { + if ( + bytes[offset] === 0x50 + && bytes[offset + 1] === 0x4b + && bytes[offset + 2] === 0x05 + && bytes[offset + 3] === 0x06 + ) { + return offset; + } + } + + return -1; +} + +function extractFirstZipEntry(bytes: Uint8Array): { filename: string; data: Uint8Array } | null { + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const eocdOffset = findEndOfCentralDirectory(bytes); + + if (eocdOffset < 0) return null; + + const centralDirectoryOffset = view.getUint32(eocdOffset + 16, true); + if (centralDirectoryOffset + 46 > bytes.length) return null; + if (view.getUint32(centralDirectoryOffset, true) !== 0x02014b50) return null; + + const compressionMethod = view.getUint16(centralDirectoryOffset + 10, true); + const compressedSize = view.getUint32(centralDirectoryOffset + 20, true); + const fileNameLength = view.getUint16(centralDirectoryOffset + 28, true); + const extraLength = view.getUint16(centralDirectoryOffset + 30, true); + const commentLength = view.getUint16(centralDirectoryOffset + 32, true); + const localHeaderOffset = view.getUint32(centralDirectoryOffset + 42, true); + const filenameStart = centralDirectoryOffset + 46; + const filenameEnd = filenameStart + fileNameLength; + + if (filenameEnd > bytes.length) return null; + + const filename = textDecoder.decode(bytes.slice(filenameStart, filenameEnd)); + const nextEntryOffset = filenameEnd + extraLength + commentLength; + if (nextEntryOffset > bytes.length) return null; + + if (localHeaderOffset + 30 > bytes.length) return null; + if (view.getUint32(localHeaderOffset, true) !== 0x04034b50) return null; + + const localNameLength = view.getUint16(localHeaderOffset + 26, true); + const localExtraLength = view.getUint16(localHeaderOffset + 28, true); + const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength; + const dataEnd = dataStart + compressedSize; + + if (dataEnd > bytes.length) return null; + + const compressed = bytes.slice(dataStart, dataEnd); + + if (compressionMethod === 0) { + return { filename, data: compressed }; + } + + if (compressionMethod === 8) { + return { filename, data: inflateRawSync(compressed) }; + } + + return null; +} + +function collectCookieHeader(response: Response): string { + const header = response.headers.get('set-cookie'); + if (!header) return ''; + return header + .split(/,(?=[^;]+=[^;]+)/) + .map((cookie) => cookie.split(';')[0]?.trim()) + .filter(Boolean) + .join('; '); +} + +function extractConfirmedDownloadUrl(html: string): string | null { + const formMatch = html.match(/