From 826f6b3f95ac9885204e9065a5dce4dd8fc8661d Mon Sep 17 00:00:00 2001 From: Aayam Bansal Date: Wed, 29 Jul 2026 05:44:48 +0800 Subject: [PATCH 01/40] feat: detect scientific file formats --- frontend/workspace/src/science/files.test.ts | 82 ++++++++++++++++ frontend/workspace/src/science/files.ts | 99 ++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 frontend/workspace/src/science/files.test.ts create mode 100644 frontend/workspace/src/science/files.ts diff --git a/frontend/workspace/src/science/files.test.ts b/frontend/workspace/src/science/files.test.ts new file mode 100644 index 00000000..ad55b5fc --- /dev/null +++ b/frontend/workspace/src/science/files.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, test } from "bun:test" +import { detectScientificFile } from "./files" + +describe("detectScientificFile", () => { + test("routes small-molecule structures into the 3D chemistry renderer", () => { + expect(detectScientificFile("xyz", "2\nwater\nO 0 0 0\nH 0 0 1")).toEqual({ + kind: "chem-3d", + data: { + data: "2\nwater\nO 0 0 0\nH 0 0 1", + format: "xyz", + }, + format: "xyz", + }) + }) + + test("routes macromolecular structures case-insensitively", () => { + expect(detectScientificFile("PDB", "ATOM 1")).toEqual({ + kind: "protein-structure", + data: { + data: "ATOM 1", + format: "pdb", + }, + format: "pdb", + }) + }) + + test("does not infer a scientific renderer from file contents alone", () => { + expect(detectScientificFile("txt", "ATOM 1")).toBeUndefined() + }) + + test("does not render empty scientific files", () => { + expect(detectScientificFile("sdf", " \n")).toBeUndefined() + }) + + test("uses the first SMILES record for two-dimensional chemistry", () => { + expect(detectScientificFile("smi", "# compounds\n\nCCO ethanol\nCCC propane")).toEqual({ + kind: "chem-2d", + data: { + smiles: "CCO", + records: 2, + }, + format: "smiles", + }) + }) + + test("combines wrapped FASTA lines into a single sequence", () => { + expect(detectScientificFile("fasta", ">alpha human sample\nAC GT\nTG\n")).toEqual({ + kind: "sequence", + data: { + id: "alpha human sample", + sequence: "ACGTTG", + records: 1, + }, + format: "fasta", + }) + }) + + test("routes equal-length FASTA records into the alignment viewer", () => { + expect(detectScientificFile("FA", ">alpha\nACGT\n>beta\nAC-T")).toEqual({ + kind: "msa", + data: { + sequences: [ + { id: "alpha", seq: "ACGT" }, + { id: "beta", seq: "AC-T" }, + ], + }, + format: "fasta", + }) + }) + + test("does not pretend unequal FASTA records form an alignment", () => { + expect(detectScientificFile("faa", ">alpha\nMKT\n>beta\nMKTA")).toEqual({ + kind: "sequence", + data: { + id: "alpha", + sequence: "MKT", + records: 2, + }, + format: "fasta", + }) + }) +}) diff --git a/frontend/workspace/src/science/files.ts b/frontend/workspace/src/science/files.ts new file mode 100644 index 00000000..4860636b --- /dev/null +++ b/frontend/workspace/src/science/files.ts @@ -0,0 +1,99 @@ +import type { ArtifactKind } from "./renderers/registry" + +export interface ScientificFile { + kind: ArtifactKind + data: unknown + format: string +} + +const protein: Record = { + pdb: "pdb", + ent: "pdb", + cif: "mmcif", + mmcif: "mmcif", + pdbqt: "pdbqt", + gro: "gro", +} + +const molecule: Record = { + xyz: "xyz", + sdf: "sdf", + mol: "mol", + mol2: "mol2", +} + +const fasta = new Set(["fa", "fasta", "faa", "fna", "ffn", "frn"]) + +export function detectScientificFile(extension: string, content: string): ScientificFile | undefined { + if (!content.trim()) return + + const ext = extension.toLowerCase() + const proteinFormat = protein[ext] + if (proteinFormat) { + return { + kind: "protein-structure", + data: { data: content, format: proteinFormat }, + format: proteinFormat, + } + } + + const moleculeFormat = molecule[ext] + if (moleculeFormat) { + return { + kind: "chem-3d", + data: { data: content, format: moleculeFormat }, + format: moleculeFormat, + } + } + + if (ext === "smi" || ext === "smiles") { + const records = content + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")) + const smiles = records[0]?.split(/\s+/)[0] + if (!smiles) return + return { + kind: "chem-2d", + data: { smiles, records: records.length }, + format: "smiles", + } + } + + if (!fasta.has(ext)) return + const records = content.split(/\r?\n/).reduce<{ id: string; seq: string }[]>((all, line) => { + const value = line.trim() + if (!value) return all + if (value.startsWith(">")) { + all.push({ id: value.slice(1).trim() || `sequence_${all.length + 1}`, seq: "" }) + return all + } + const record = all[all.length - 1] + if (record) { + record.seq += value.replace(/\s+/g, "") + return all + } + all.push({ id: "sequence_1", seq: value.replace(/\s+/g, "") }) + return all + }, []) + const sequences = records.filter((record) => record.seq) + const first = sequences[0] + if (!first) return + const aligned = sequences.length > 1 && sequences.every((record) => record.seq.length === first.seq.length) + if (aligned) { + return { + kind: "msa", + data: { sequences }, + format: "fasta", + } + } + return { + kind: "sequence", + data: { + id: first.id, + sequence: first.seq, + records: sequences.length, + }, + format: "fasta", + } +} From ee0e014c6af4557b98a6b439363d905bb7e9b817 Mon Sep 17 00:00:00 2001 From: Aayam Bansal Date: Wed, 29 Jul 2026 05:47:10 +0800 Subject: [PATCH 02/40] feat: render scientific files in document tabs --- frontend/workspace/src/atlas/FilePreview.tsx | 30 ++++++++++++++++++-- frontend/workspace/src/science/files.test.ts | 15 ++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/frontend/workspace/src/atlas/FilePreview.tsx b/frontend/workspace/src/atlas/FilePreview.tsx index ab372e7c..6e4934d3 100644 --- a/frontend/workspace/src/atlas/FilePreview.tsx +++ b/frontend/workspace/src/atlas/FilePreview.tsx @@ -17,6 +17,8 @@ import { useSync } from "@/context/sync" import { usePlatform } from "@/context/platform" import { FONT_MONO, FONT_SANS, FONT_CODE } from "@/styles/tokens" import { PdfViewer } from "@/science/renderers/documents/PdfViewer" +import { ScienceArtifact } from "@/science/ScienceArtifact" +import { detectScientificFile } from "@/science/files" import { toast } from "@/atlas/Toast" import { IconFile, IconX, IconCopy, IconDownload, IconBookOpen, IconBraces, IconRefresh } from "@/atlas/shared/Icon" @@ -26,6 +28,7 @@ import { IconFile, IconX, IconCopy, IconDownload, IconBookOpen, IconBraces, Icon * A file's extension picks the renderer: * .md / .markdown → formatted markdown (@synsci/ui Markdown) * .pdf → PdfViewer (pdfjs page rasterizer) + * molecular/FASTA → scientific artifact renderer, with editable source * .tex / .latex → highlighted LaTeX source (a .tex is a source FILE, not a * math expression — the KaTeX LatexView is reserved for * kind:"latex" math ARTIFACTS with a single math string) @@ -98,7 +101,7 @@ const LANG: Record = { log: "text", } -type Kind = "markdown" | "pdf" | "image" | "code" | "binary" +type Kind = "markdown" | "pdf" | "image" | "science" | "code" | "binary" type FileData = { content?: string; encoding?: string; mimeType?: string } @@ -150,6 +153,7 @@ export function FileView(props: { const dataUrl = () => `data:${mime() || "application/octet-stream"};base64,${b64()}` const text = () => (!data() || isBinary() ? "" : (data()!.content ?? "")) const dirty = () => draft() !== savedText() + const scientific = createMemo(() => (isBinary() ? undefined : detectScientificFile(e(), draft()))) const kind = createMemo(() => { const x = e() @@ -160,6 +164,7 @@ export function FileView(props: { } if (x === "md" || x === "markdown" || x === "mdx") return "markdown" if (x === "pdf") return "pdf" + if (scientific()) return "science" // .tex / .latex / .sty / .cls are source files → highlighted "code" view // (LANG maps them to the shiki `latex` grammar). They are NEVER routed to // KaTeX, which blanks on a full \documentclass document. @@ -169,6 +174,7 @@ export function FileView(props: { const badge = () => { const k = kind() if (k === "code") return LANG[e()] ?? e() ?? "text" + if (k === "science") return scientific()?.format ?? e() return k } @@ -213,7 +219,7 @@ export function FileView(props: { } catch {} } - const toggleable = () => kind() === "markdown" || kind() === "code" + const toggleable = () => kind() === "markdown" || kind() === "science" || kind() === "code" return (
+ {/* scientific file */} + + + {(artifact) => ( +
+ +
+ )} +
+
+ {/* binary */}
{/* code / text — editable source, or highlighted read view */} - +