Spaces:
Paused
Paused
Deploy Seqcolyte Studio
Browse files- studio/deploy/hf/demo-seed/10x-pbmc-3-adapter-dimer-qc-demo-d13261/project.json +3 -2
- studio/deploy/hf/demo-seed/10x-pbmc-3-adapter-dimer-qc-demo-d13261/spec/spec.md +71 -0
- studio/deploy/hf/demo-seed/10x-pbmc-3-healthy-library-demo-9ec7eb/project.json +3 -2
- studio/deploy/hf/demo-seed/10x-pbmc-3-healthy-library-demo-9ec7eb/spec/spec.md +71 -0
- studio/deploy/hf/demo-seed/sc-nanopore-3-tso-concatemer-demo/project.json +3 -2
- studio/deploy/hf/demo-seed/sc-nanopore-3-tso-concatemer-demo/spec/spec.md +57 -0
- studio/src/app/api/projects/[id]/generate-spec/route.ts +55 -0
- studio/src/components/chat/chat.tsx +62 -5
- studio/src/components/files/files-panel.tsx +10 -0
- studio/src/components/workspace.tsx +74 -7
- studio/src/lib/chat.ts +3 -2
- studio/src/lib/extractRunner.ts +94 -0
- studio/src/lib/types.ts +2 -0
- studio/tsconfig.tsbuildinfo +0 -0
studio/deploy/hf/demo-seed/10x-pbmc-3-adapter-dimer-qc-demo-d13261/project.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"schemaVersion": "studio.project.v1",
|
| 3 |
"id": "10x-pbmc-3-adapter-dimer-qc-demo-d13261",
|
| 4 |
-
"name": "10x Chromium 3
|
| 5 |
"assay": "10x Chromium Single Cell 3' Gene Expression",
|
| 6 |
"specId": "10x_3p_v3",
|
| 7 |
"createdAt": "2026-07-09T00:09:57.059Z",
|
|
@@ -24,5 +24,6 @@
|
|
| 24 |
"phase": "complete",
|
| 25 |
"demo": true,
|
| 26 |
"specConfirmed": true,
|
| 27 |
-
"demoBlurb": "A 10x short-read library with adapter dimers
|
|
|
|
| 28 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"schemaVersion": "studio.project.v1",
|
| 3 |
"id": "10x-pbmc-3-adapter-dimer-qc-demo-d13261",
|
| 4 |
+
"name": "10x Chromium 3′ — adapter dimers",
|
| 5 |
"assay": "10x Chromium Single Cell 3' Gene Expression",
|
| 6 |
"specId": "10x_3p_v3",
|
| 7 |
"createdAt": "2026-07-09T00:09:57.059Z",
|
|
|
|
| 24 |
"phase": "complete",
|
| 25 |
"demo": true,
|
| 26 |
"specConfirmed": true,
|
| 27 |
+
"demoBlurb": "A 10x short-read library with adapter dimers — QC flags the TSO-at-R2 and poly-G signatures and explains the fix.",
|
| 28 |
+
"specMarkdownPath": "spec/spec.md"
|
| 29 |
}
|
studio/deploy/hf/demo-seed/10x-pbmc-3-adapter-dimer-qc-demo-d13261/spec/spec.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 10x 3' scRNA-seq (v3)
|
| 2 |
+
|
| 3 |
+
**Assay:** 10x Chromium Single Cell 3' Gene Expression
|
| 4 |
+
**Platform:** illumina · **Chemistry:** v3/v3.1
|
| 5 |
+
|
| 6 |
+
Droplet-based single-cell 3' gene-expression profiling on Illumina. Cells are partitioned into gel-bead emulsions (GEMs); mRNA is barcoded during reverse transcription with a per-cell 16 nt barcode + 12 nt UMI, and the amplified cDNA is fragmented and built into a P5/P7-indexed short-read library. Profiles the transcriptomes of thousands of single cells per run.
|
| 7 |
+
|
| 8 |
+
## Read structure
|
| 9 |
+
|
| 10 |
+
- **R1** (bottom strand, 28 cycles): `cell_barcode:16` + `umi:12`
|
| 11 |
+
- **I1** (bottom strand, 8 cycles): `sample_index_i7:8`
|
| 12 |
+
- **R2** (top strand, 90 cycles): `cdna_insert`
|
| 13 |
+
|
| 14 |
+
**R1 segments**
|
| 15 |
+
|
| 16 |
+
| segment | type | length |
|
| 17 |
+
| --- | --- | --- |
|
| 18 |
+
| cell_barcode | barcode | 16 |
|
| 19 |
+
| umi | umi | 12 |
|
| 20 |
+
|
| 21 |
+
**I1 segments**
|
| 22 |
+
|
| 23 |
+
| segment | type | length |
|
| 24 |
+
| --- | --- | --- |
|
| 25 |
+
| sample_index_i7 | index | 8 |
|
| 26 |
+
|
| 27 |
+
**R2 segments**
|
| 28 |
+
|
| 29 |
+
| segment | type | length |
|
| 30 |
+
| --- | --- | --- |
|
| 31 |
+
| cdna_insert | insert | |
|
| 32 |
+
|
| 33 |
+
## Oligos & adapters
|
| 34 |
+
|
| 35 |
+
| id | role | sequence |
|
| 36 |
+
| --- | --- | --- |
|
| 37 |
+
| oligo_beads_oligo_dt | oligo | `CTACACGACGCTCTTCCGATCT[CELL_BARCODE:16][UMI:12]TTTTTTTTTTTTT…` |
|
| 38 |
+
| oligo_template_switching_oligo_tso | oligo | `AAGCAGTGGTATCAACGCAGAGTACATGGG` |
|
| 39 |
+
| oligo_cdna_forward_primer | primer | `CTACACGACGCTCTTCCGATCT` |
|
| 40 |
+
| oligo_cdna_reverse_primer | primer | `AAGCAGTGGTATCAACGCAGAG` |
|
| 41 |
+
| oligo_illumina_truseq_read_1_primer | primer | `ACACTCTTTCCCTACACGACGCTCTTCCGATCT` |
|
| 42 |
+
| oligo_illumina_truseq_read_2_primer | primer | `GTGACTGGAGTTCAGACGTGTGCTCTTCCGATCT` |
|
| 43 |
+
| oligo_truseq_adapter | adapter | |
|
| 44 |
+
| oligo_library_pcr_primer_1 | primer | `AATGATACGGCGACCACCGAGATCTACACTCTTTCCCTACACGACGCTC` |
|
| 45 |
+
| oligo_library_pcr_primer_2 | primer | `CAAGCAGAAGACGGCATACGAGAT[SAMPLE_INDEX:8]GTGACTGGAGTTCAGACGTG…` |
|
| 46 |
+
| oligo_sample_index_sequencing_primer | primer | `GATCGGAAGAGCACACGTCTGAACTCCAGTCAC` |
|
| 47 |
+
| oligo_illumina_p5_adapter | adapter | `AATGATACGGCGACCACCGAGATCTACAC` |
|
| 48 |
+
| oligo_illumina_p7_adapter | adapter | `CAAGCAGAAGACGGCATACGAGAT` |
|
| 49 |
+
| oligo_r2_readthrough_adapter | read_through_adapter | `AGATCGGAAGAGCACACGTCTGAACTCCAGTCAC` |
|
| 50 |
+
| oligo_r1_readinto_adapter | read_through_adapter | `AGATCGGAAGAGCGTCGTGTAGGGAAAGAGTGT` |
|
| 51 |
+
| oligo_p5_rc | adapter_rc | `GTGTAGATCTCGGTGGTCGCCGTATCATT` |
|
| 52 |
+
|
| 53 |
+
## Library preparation
|
| 54 |
+
|
| 55 |
+
1. **mRNA capture using Beads-oligo-dT in the droplets, and reverse transcription using MMLV** — Poly(dT) beads capture mRNA in droplets; MMLV reverse-transcribes it into barcoded cDNA.
|
| 56 |
+
2. **The terminal tranferase acitivity of MMLV adds extra Cs** — MMLV's terminal-transferase activity adds a few untemplated C's to the cDNA 3' end.
|
| 57 |
+
3. **Adding TSO for second strand synthesis** — The TSO base-pairs with the C overhang to prime second-strand synthesis.
|
| 58 |
+
4. **Adding cDNA Forward and Reverse primers to amplify full length cDNA** — cDNA Forward/Reverse primers PCR-amplify the full-length cDNA.
|
| 59 |
+
5. **Use Fragmentase to fragment cDNA and perform A-tailing** — Fragmentase shears the cDNA and A-tails the fragment ends.
|
| 60 |
+
6. **Add double stranded Illumina Truseq adapter (with a T overhang) for ligation** — A T-overhang TruSeq adapter is ligated onto the A-tailed fragments.
|
| 61 |
+
7. **Add Library PCR Primers 1 & 2 to amplify library** — Library PCR primers 1 & 2 add P5/P7 and the i7 sample index.
|
| 62 |
+
8. **Final library structure** — The final P5/P7-indexed Illumina paired-end library.
|
| 63 |
+
|
| 64 |
+
## Final library (5'→3')
|
| 65 |
+
|
| 66 |
+
```
|
| 67 |
+
Illumina P5 | TruSeq Read 1 | cell barcode | UMI | poly(dT)VN | cDNA | TruSeq Read 2 | sample index (i7) | Illumina P7
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
*Generated by Seqcolyte from the extracted library spec. Review it in the workspace, then click **Confirm spec** to run QC on your reads.*
|
studio/deploy/hf/demo-seed/10x-pbmc-3-healthy-library-demo-9ec7eb/project.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"schemaVersion": "studio.project.v1",
|
| 3 |
"id": "10x-pbmc-3-healthy-library-demo-9ec7eb",
|
| 4 |
-
"name": "10x Chromium 3
|
| 5 |
"assay": "10x Chromium Single Cell 3' Gene Expression",
|
| 6 |
"specId": "10x_3p_v3",
|
| 7 |
"createdAt": "2026-07-10T03:16:53.342Z",
|
|
@@ -24,5 +24,6 @@
|
|
| 24 |
"20260710T031653Z-82a2fa"
|
| 25 |
],
|
| 26 |
"demo": true,
|
| 27 |
-
"demoBlurb": "A clean 10x PBMC v3 short-read library
|
|
|
|
| 28 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"schemaVersion": "studio.project.v1",
|
| 3 |
"id": "10x-pbmc-3-healthy-library-demo-9ec7eb",
|
| 4 |
+
"name": "10x Chromium 3′ — clean library",
|
| 5 |
"assay": "10x Chromium Single Cell 3' Gene Expression",
|
| 6 |
"specId": "10x_3p_v3",
|
| 7 |
"createdAt": "2026-07-10T03:16:53.342Z",
|
|
|
|
| 24 |
"20260710T031653Z-82a2fa"
|
| 25 |
],
|
| 26 |
"demo": true,
|
| 27 |
+
"demoBlurb": "A clean 10x PBMC v3 short-read library — every QC check lands in range. The baseline for comparison.",
|
| 28 |
+
"specMarkdownPath": "spec/spec.md"
|
| 29 |
}
|
studio/deploy/hf/demo-seed/10x-pbmc-3-healthy-library-demo-9ec7eb/spec/spec.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 10x 3' scRNA-seq (v3)
|
| 2 |
+
|
| 3 |
+
**Assay:** 10x Chromium Single Cell 3' Gene Expression
|
| 4 |
+
**Platform:** illumina · **Chemistry:** v3/v3.1
|
| 5 |
+
|
| 6 |
+
Droplet-based single-cell 3' gene-expression profiling on Illumina. Cells are partitioned into gel-bead emulsions (GEMs); mRNA is barcoded during reverse transcription with a per-cell 16 nt barcode + 12 nt UMI, and the amplified cDNA is fragmented and built into a P5/P7-indexed short-read library. Profiles the transcriptomes of thousands of single cells per run.
|
| 7 |
+
|
| 8 |
+
## Read structure
|
| 9 |
+
|
| 10 |
+
- **R1** (bottom strand, 28 cycles): `cell_barcode:16` + `umi:12`
|
| 11 |
+
- **I1** (bottom strand, 8 cycles): `sample_index_i7:8`
|
| 12 |
+
- **R2** (top strand, 90 cycles): `cdna_insert`
|
| 13 |
+
|
| 14 |
+
**R1 segments**
|
| 15 |
+
|
| 16 |
+
| segment | type | length |
|
| 17 |
+
| --- | --- | --- |
|
| 18 |
+
| cell_barcode | barcode | 16 |
|
| 19 |
+
| umi | umi | 12 |
|
| 20 |
+
|
| 21 |
+
**I1 segments**
|
| 22 |
+
|
| 23 |
+
| segment | type | length |
|
| 24 |
+
| --- | --- | --- |
|
| 25 |
+
| sample_index_i7 | index | 8 |
|
| 26 |
+
|
| 27 |
+
**R2 segments**
|
| 28 |
+
|
| 29 |
+
| segment | type | length |
|
| 30 |
+
| --- | --- | --- |
|
| 31 |
+
| cdna_insert | insert | |
|
| 32 |
+
|
| 33 |
+
## Oligos & adapters
|
| 34 |
+
|
| 35 |
+
| id | role | sequence |
|
| 36 |
+
| --- | --- | --- |
|
| 37 |
+
| oligo_beads_oligo_dt | oligo | `CTACACGACGCTCTTCCGATCT[CELL_BARCODE:16][UMI:12]TTTTTTTTTTTTT…` |
|
| 38 |
+
| oligo_template_switching_oligo_tso | oligo | `AAGCAGTGGTATCAACGCAGAGTACATGGG` |
|
| 39 |
+
| oligo_cdna_forward_primer | primer | `CTACACGACGCTCTTCCGATCT` |
|
| 40 |
+
| oligo_cdna_reverse_primer | primer | `AAGCAGTGGTATCAACGCAGAG` |
|
| 41 |
+
| oligo_illumina_truseq_read_1_primer | primer | `ACACTCTTTCCCTACACGACGCTCTTCCGATCT` |
|
| 42 |
+
| oligo_illumina_truseq_read_2_primer | primer | `GTGACTGGAGTTCAGACGTGTGCTCTTCCGATCT` |
|
| 43 |
+
| oligo_truseq_adapter | adapter | |
|
| 44 |
+
| oligo_library_pcr_primer_1 | primer | `AATGATACGGCGACCACCGAGATCTACACTCTTTCCCTACACGACGCTC` |
|
| 45 |
+
| oligo_library_pcr_primer_2 | primer | `CAAGCAGAAGACGGCATACGAGAT[SAMPLE_INDEX:8]GTGACTGGAGTTCAGACGTG…` |
|
| 46 |
+
| oligo_sample_index_sequencing_primer | primer | `GATCGGAAGAGCACACGTCTGAACTCCAGTCAC` |
|
| 47 |
+
| oligo_illumina_p5_adapter | adapter | `AATGATACGGCGACCACCGAGATCTACAC` |
|
| 48 |
+
| oligo_illumina_p7_adapter | adapter | `CAAGCAGAAGACGGCATACGAGAT` |
|
| 49 |
+
| oligo_r2_readthrough_adapter | read_through_adapter | `AGATCGGAAGAGCACACGTCTGAACTCCAGTCAC` |
|
| 50 |
+
| oligo_r1_readinto_adapter | read_through_adapter | `AGATCGGAAGAGCGTCGTGTAGGGAAAGAGTGT` |
|
| 51 |
+
| oligo_p5_rc | adapter_rc | `GTGTAGATCTCGGTGGTCGCCGTATCATT` |
|
| 52 |
+
|
| 53 |
+
## Library preparation
|
| 54 |
+
|
| 55 |
+
1. **mRNA capture using Beads-oligo-dT in the droplets, and reverse transcription using MMLV** — Poly(dT) beads capture mRNA in droplets; MMLV reverse-transcribes it into barcoded cDNA.
|
| 56 |
+
2. **The terminal tranferase acitivity of MMLV adds extra Cs** — MMLV's terminal-transferase activity adds a few untemplated C's to the cDNA 3' end.
|
| 57 |
+
3. **Adding TSO for second strand synthesis** — The TSO base-pairs with the C overhang to prime second-strand synthesis.
|
| 58 |
+
4. **Adding cDNA Forward and Reverse primers to amplify full length cDNA** — cDNA Forward/Reverse primers PCR-amplify the full-length cDNA.
|
| 59 |
+
5. **Use Fragmentase to fragment cDNA and perform A-tailing** — Fragmentase shears the cDNA and A-tails the fragment ends.
|
| 60 |
+
6. **Add double stranded Illumina Truseq adapter (with a T overhang) for ligation** — A T-overhang TruSeq adapter is ligated onto the A-tailed fragments.
|
| 61 |
+
7. **Add Library PCR Primers 1 & 2 to amplify library** — Library PCR primers 1 & 2 add P5/P7 and the i7 sample index.
|
| 62 |
+
8. **Final library structure** — The final P5/P7-indexed Illumina paired-end library.
|
| 63 |
+
|
| 64 |
+
## Final library (5'→3')
|
| 65 |
+
|
| 66 |
+
```
|
| 67 |
+
Illumina P5 | TruSeq Read 1 | cell barcode | UMI | poly(dT)VN | cDNA | TruSeq Read 2 | sample index (i7) | Illumina P7
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
*Generated by Seqcolyte from the extracted library spec. Review it in the workspace, then click **Confirm spec** to run QC on your reads.*
|
studio/deploy/hf/demo-seed/sc-nanopore-3-tso-concatemer-demo/project.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"schemaVersion": "studio.project.v1",
|
| 3 |
"id": "sc-nanopore-3-tso-concatemer-demo",
|
| 4 |
-
"name": "sc-Nanopore 3
|
| 5 |
"assay": "10x Chromium Single Cell 3' Gene Expression (Nanopore long-read)",
|
| 6 |
"specId": "nanopore_10x_3p_direct_ligation",
|
| 7 |
"createdAt": "2026-07-10T16:13:33.075625Z",
|
|
@@ -25,5 +25,6 @@
|
|
| 25 |
"runIds": [
|
| 26 |
"20260710T161333Z-28dc0a"
|
| 27 |
],
|
| 28 |
-
"demoBlurb": "A long-read sc-Nanopore library where 25.7% of reads are TSO concatemers
|
|
|
|
| 29 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"schemaVersion": "studio.project.v1",
|
| 3 |
"id": "sc-nanopore-3-tso-concatemer-demo",
|
| 4 |
+
"name": "sc-Nanopore 3′ — TSO concatemers",
|
| 5 |
"assay": "10x Chromium Single Cell 3' Gene Expression (Nanopore long-read)",
|
| 6 |
"specId": "nanopore_10x_3p_direct_ligation",
|
| 7 |
"createdAt": "2026-07-10T16:13:33.075625Z",
|
|
|
|
| 25 |
"runIds": [
|
| 26 |
"20260710T161333Z-28dc0a"
|
| 27 |
],
|
| 28 |
+
"demoBlurb": "A long-read sc-Nanopore library where 25.7% of reads are TSO concatemers — QC finds them and recommends read-splitting / biotin enrichment.",
|
| 29 |
+
"specMarkdownPath": "spec/spec.md"
|
| 30 |
}
|
studio/deploy/hf/demo-seed/sc-nanopore-3-tso-concatemer-demo/spec/spec.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# sc-Nanopore 10x 3' cDNA (direct ligation)
|
| 2 |
+
|
| 3 |
+
**Assay:** 10x Chromium Single Cell 3' Gene Expression amplified cDNA — baseline Oxford Nanopore direct-ligation simulation
|
| 4 |
+
**Platform:** nanopore · **Chemistry:** v3.1/v4
|
| 5 |
+
|
| 6 |
+
Full-length single-cell 3' cDNA sequenced on Oxford Nanopore. Amplified 10x cDNA (16 nt cell barcode + 12 nt UMI) is taken through ONT end-prep and ligation-adapter attachment and read end-to-end as one long read per molecule. This is a baseline direct-ligation simulation model; it omits the biotin-enrichment / streptavidin pull-down steps of the optimized ONT workflow.
|
| 7 |
+
|
| 8 |
+
## Read structure
|
| 9 |
+
|
| 10 |
+
- **L1** (normalized adapter1-first strand strand): `r1_handle:22` + `cell_barcode:16` + `umi:12` + `polyt` + `vn_anchor:2` + `cdna_insert` + `tso_rc:30`
|
| 11 |
+
|
| 12 |
+
**L1 segments**
|
| 13 |
+
|
| 14 |
+
| segment | type | length |
|
| 15 |
+
| --- | --- | --- |
|
| 16 |
+
| r1_handle | constant | 22 |
|
| 17 |
+
| cell_barcode | barcode | 16 |
|
| 18 |
+
| umi | umi | 12 |
|
| 19 |
+
| polyt | homopolymer | |
|
| 20 |
+
| vn_anchor | anchor | 2 |
|
| 21 |
+
| cdna_insert | insert | |
|
| 22 |
+
| tso_rc | constant | 30 |
|
| 23 |
+
|
| 24 |
+
## Oligos & adapters
|
| 25 |
+
|
| 26 |
+
| id | role | sequence |
|
| 27 |
+
| --- | --- | --- |
|
| 28 |
+
| oligo_beads_oligo_dt | Gel-bead-tethered RT primer: capture of poly(A) mRNA and addition of the 10x Barcode, UMI and partial TruSeq Read 1 handle | `CTACACGACGCTCTTCCGATCT[CELL_BARCODE:16][UMI:12]TTTTTTTTTTTTT…` |
|
| 29 |
+
| oligo_template_switching_oligo_tso | Template switching at the cDNA 3' end; adds a constant handle used as the cDNA Reverse primer binding site | `AAGCAGTGGTATCAACGCAGAGTACATGGG` |
|
| 30 |
+
| oligo_cdna_forward_primer | Bulk cDNA amplification (PCR), anneals to the partial TruSeq Read 1 handle | `CTACACGACGCTCTTCCGATCT` |
|
| 31 |
+
| oligo_cdna_reverse_primer | Bulk cDNA amplification (PCR), anneals to the TSO-derived handle | `AAGCAGTGGTATCAACGCAGAG` |
|
| 32 |
+
|
| 33 |
+
## Library preparation
|
| 34 |
+
|
| 35 |
+
1. **mRNA capture on the gel-bead poly(dT) primer (GEM formation)** — mRNA is captured on the gel-bead poly(dT) primer as GEMs form.
|
| 36 |
+
2. **Reverse transcription (GEM-RT, 53 C 45 min)** — GEM-RT makes barcoded first-strand cDNA.
|
| 37 |
+
3. **Template Switch Oligo annealing** — The TSO anneals to the cDNA 3' C-overhang.
|
| 38 |
+
4. **Template switching & transcript extension** — Template switching extends the transcript with the TSO handle into full-length cDNA.
|
| 39 |
+
5. **cDNA amplification (bulk PCR with cDNA Primers, 11-13 cycles)** — Bulk PCR amplifies the full-length cDNA to build mass.
|
| 40 |
+
6. **End repair & dA-tailing (ONT end-prep, NEBNext Ultra II)** — End repair + 3' dA-tailing prepares the amplicon for ligation (no fragmentation).
|
| 41 |
+
7. **ONT ligation-adapter attachment (SQK-LSK114)** — ONT sequencing adapters (SQK-LSK114) are ligated directly to the full-length cDNA.
|
| 42 |
+
8. **Nanopore sequencing — single-strand read, either orientation** — Each molecule is read end-to-end on the flow cell, in either orientation.
|
| 43 |
+
|
| 44 |
+
## Final library (5'→3')
|
| 45 |
+
|
| 46 |
+
```
|
| 47 |
+
CTACACGACGCTCTTCCGATCT = R1 handle (partial TruSeq Read 1; 10x bead oligo)
|
| 48 |
+
[CELL_BARCODE:16] = 16 bp 10x cell barcode
|
| 49 |
+
[UMI:12] = 12 bp UMI
|
| 50 |
+
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTT = poly(dT) 30 nt
|
| 51 |
+
VN = poly(dT)VN anchor (V=A/C/G, N=any)
|
| 52 |
+
[CDNA] = full-length cDNA insert
|
| 53 |
+
CCCATGTACTCTGCGTTGATACCACTGCTT = reverse complement of the TSO (DERIVED = reverse_complement(TSO)); 3' end
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
*Generated by Seqcolyte from the extracted library spec. Review it in the workspace, then click **Confirm spec** to run QC on your reads.*
|
studio/src/app/api/projects/[id]/generate-spec/route.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { promises as fs } from "node:fs";
|
| 2 |
+
import { NextResponse } from "next/server";
|
| 3 |
+
import { getProject, updateProject } from "@/lib/store";
|
| 4 |
+
import { inProject } from "@/lib/paths";
|
| 5 |
+
import { readConversation } from "@/lib/chat";
|
| 6 |
+
import { startExtract } from "@/lib/extractRunner";
|
| 7 |
+
|
| 8 |
+
export const runtime = "nodejs";
|
| 9 |
+
export const dynamic = "force-dynamic";
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Build a spec from the user's free-text library description (typed in chat)
|
| 13 |
+
* instead of an uploaded protocol file. Saves the description as the project's
|
| 14 |
+
* lab notes and runs the same extractor `startExtract` uses, so the result flows
|
| 15 |
+
* into the normal review → Confirm-spec → run-QC path. The chat assistant is
|
| 16 |
+
* read-only and cannot do this itself — this is the workspace "Generate spec"
|
| 17 |
+
* button's endpoint.
|
| 18 |
+
*/
|
| 19 |
+
export async function POST(_req: Request, ctx: { params: Promise<{ id: string }> }) {
|
| 20 |
+
const { id } = await ctx.params;
|
| 21 |
+
let project;
|
| 22 |
+
try {
|
| 23 |
+
project = await getProject(id);
|
| 24 |
+
} catch {
|
| 25 |
+
return NextResponse.json({ error: "project not found" }, { status: 404 });
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// The description is everything the user typed in chat (minus upload notices).
|
| 29 |
+
const conv = await readConversation(id);
|
| 30 |
+
const description = conv
|
| 31 |
+
.filter((e) => e.role === "user" && !e.text.startsWith("📎"))
|
| 32 |
+
.map((e) => e.text.trim())
|
| 33 |
+
.filter(Boolean)
|
| 34 |
+
.join("\n\n")
|
| 35 |
+
.trim();
|
| 36 |
+
|
| 37 |
+
if (description.length < 20) {
|
| 38 |
+
return NextResponse.json(
|
| 39 |
+
{
|
| 40 |
+
error:
|
| 41 |
+
"Describe your library structure in the chat first — read layout (R1/R2), UMI/barcode positions and lengths, and any adapters — then click Generate spec.",
|
| 42 |
+
},
|
| 43 |
+
{ status: 400 },
|
| 44 |
+
);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Persist as the project's lab notes (shown under Files) and extract from it.
|
| 48 |
+
const rel = project.inputs.notesPath || "inputs/notes.md";
|
| 49 |
+
await fs.mkdir(inProject(id, "inputs"), { recursive: true });
|
| 50 |
+
await fs.writeFile(inProject(id, rel), `${description}\n`, "utf8");
|
| 51 |
+
await updateProject(id, { inputs: { ...project.inputs, notesPath: rel } });
|
| 52 |
+
|
| 53 |
+
await startExtract(id, rel);
|
| 54 |
+
return NextResponse.json({ ok: true });
|
| 55 |
+
}
|
studio/src/components/chat/chat.tsx
CHANGED
|
@@ -167,6 +167,8 @@ export function Chat({
|
|
| 167 |
const [input, setInput] = useState("");
|
| 168 |
const [uploading, setUploading] = useState(false);
|
| 169 |
const [upload, setUpload] = useState<{ name: string; loaded: number; total: number } | null>(null);
|
|
|
|
|
|
|
| 170 |
const [extract, setExtract] = useState<{ status: string; log: string; doc: string } | null>(null);
|
| 171 |
const scrollRef = useRef<HTMLDivElement>(null);
|
| 172 |
const fileRef = useRef<HTMLInputElement>(null);
|
|
@@ -242,10 +244,7 @@ export function Chat({
|
|
| 242 |
};
|
| 243 |
}
|
| 244 |
|
| 245 |
-
async function
|
| 246 |
-
const file = e.target.files?.[0];
|
| 247 |
-
e.target.value = "";
|
| 248 |
-
if (!file) return;
|
| 249 |
setUploading(true);
|
| 250 |
setUpload({ name: file.name, loaded: 0, total: file.size });
|
| 251 |
try {
|
|
@@ -268,8 +267,65 @@ export function Chat({
|
|
| 268 |
}
|
| 269 |
}
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
return (
|
| 272 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
<header className="border-border/60 flex shrink-0 items-center gap-2 border-b px-3 py-2.5">
|
| 274 |
<button
|
| 275 |
onClick={onToggleRail}
|
|
@@ -349,6 +405,7 @@ export function Chat({
|
|
| 349 |
<input
|
| 350 |
ref={fileRef}
|
| 351 |
type="file"
|
|
|
|
| 352 |
accept={UPLOAD_ACCEPT}
|
| 353 |
className="hidden"
|
| 354 |
onChange={onPickFile}
|
|
|
|
| 167 |
const [input, setInput] = useState("");
|
| 168 |
const [uploading, setUploading] = useState(false);
|
| 169 |
const [upload, setUpload] = useState<{ name: string; loaded: number; total: number } | null>(null);
|
| 170 |
+
const [dragActive, setDragActive] = useState(false);
|
| 171 |
+
const dragDepth = useRef(0);
|
| 172 |
const [extract, setExtract] = useState<{ status: string; log: string; doc: string } | null>(null);
|
| 173 |
const scrollRef = useRef<HTMLDivElement>(null);
|
| 174 |
const fileRef = useRef<HTMLInputElement>(null);
|
|
|
|
| 244 |
};
|
| 245 |
}
|
| 246 |
|
| 247 |
+
async function uploadFile(file: File) {
|
|
|
|
|
|
|
|
|
|
| 248 |
setUploading(true);
|
| 249 |
setUpload({ name: file.name, loaded: 0, total: file.size });
|
| 250 |
try {
|
|
|
|
| 267 |
}
|
| 268 |
}
|
| 269 |
|
| 270 |
+
/** Upload one or more files sequentially (keeps R1/R2 slotting deterministic). */
|
| 271 |
+
async function uploadFiles(files: File[]) {
|
| 272 |
+
for (const f of files) await uploadFile(f);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
async function onPickFile(e: React.ChangeEvent<HTMLInputElement>) {
|
| 276 |
+
const files = Array.from(e.target.files ?? []);
|
| 277 |
+
e.target.value = "";
|
| 278 |
+
if (files.length) await uploadFiles(files);
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// --- drag-and-drop upload onto the whole chat surface ---
|
| 282 |
+
function hasFiles(e: React.DragEvent): boolean {
|
| 283 |
+
return Array.from(e.dataTransfer?.types ?? []).includes("Files");
|
| 284 |
+
}
|
| 285 |
+
function onDragEnter(e: React.DragEvent) {
|
| 286 |
+
if (!hasFiles(e)) return;
|
| 287 |
+
e.preventDefault();
|
| 288 |
+
dragDepth.current += 1;
|
| 289 |
+
setDragActive(true);
|
| 290 |
+
}
|
| 291 |
+
function onDragOver(e: React.DragEvent) {
|
| 292 |
+
if (hasFiles(e)) e.preventDefault(); // required for the drop event to fire
|
| 293 |
+
}
|
| 294 |
+
function onDragLeave(e: React.DragEvent) {
|
| 295 |
+
if (!hasFiles(e)) return;
|
| 296 |
+
dragDepth.current -= 1;
|
| 297 |
+
if (dragDepth.current <= 0) {
|
| 298 |
+
dragDepth.current = 0;
|
| 299 |
+
setDragActive(false);
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
async function onDrop(e: React.DragEvent) {
|
| 303 |
+
if (!hasFiles(e)) return;
|
| 304 |
+
e.preventDefault();
|
| 305 |
+
dragDepth.current = 0;
|
| 306 |
+
setDragActive(false);
|
| 307 |
+
if (uploading) return;
|
| 308 |
+
const files = Array.from(e.dataTransfer.files ?? []);
|
| 309 |
+
if (files.length) await uploadFiles(files);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
return (
|
| 313 |
+
<div
|
| 314 |
+
className="relative flex h-full min-h-0 flex-col overflow-hidden"
|
| 315 |
+
onDragEnter={onDragEnter}
|
| 316 |
+
onDragOver={onDragOver}
|
| 317 |
+
onDragLeave={onDragLeave}
|
| 318 |
+
onDrop={onDrop}
|
| 319 |
+
>
|
| 320 |
+
{dragActive && (
|
| 321 |
+
<div className="border-primary/50 bg-background/80 pointer-events-none absolute inset-2 z-20 flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed backdrop-blur-sm">
|
| 322 |
+
<Paperclip className="text-primary size-6" />
|
| 323 |
+
<div className="text-sm font-medium">Drop to upload</div>
|
| 324 |
+
<div className="text-muted-foreground text-xs">
|
| 325 |
+
protocol (PDF / txt / md), FASTQ reads, or design tables
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
+
)}
|
| 329 |
<header className="border-border/60 flex shrink-0 items-center gap-2 border-b px-3 py-2.5">
|
| 330 |
<button
|
| 331 |
onClick={onToggleRail}
|
|
|
|
| 405 |
<input
|
| 406 |
ref={fileRef}
|
| 407 |
type="file"
|
| 408 |
+
multiple
|
| 409 |
accept={UPLOAD_ACCEPT}
|
| 410 |
className="hidden"
|
| 411 |
onChange={onPickFile}
|
studio/src/components/files/files-panel.tsx
CHANGED
|
@@ -112,6 +112,16 @@ export function FilesPanel({
|
|
| 112 |
downloadHref: `${fileHref(project.activeSpecPath)}?download`,
|
| 113 |
});
|
| 114 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
for (const run of runs) {
|
| 116 |
if (run.steps.qc?.status === "succeeded" || run.overall != null) {
|
| 117 |
rows.push({
|
|
|
|
| 112 |
downloadHref: `${fileHref(project.activeSpecPath)}?download`,
|
| 113 |
});
|
| 114 |
}
|
| 115 |
+
if (project.specMarkdownPath) {
|
| 116 |
+
const rel = project.specMarkdownPath;
|
| 117 |
+
rows.push({
|
| 118 |
+
icon: FileText,
|
| 119 |
+
label: "Spec (markdown)",
|
| 120 |
+
sub: "readable library summary",
|
| 121 |
+
open: { kind: "markdown", path: rel, title: "Spec" },
|
| 122 |
+
downloadHref: `${fileHref(rel)}?download`,
|
| 123 |
+
});
|
| 124 |
+
}
|
| 125 |
for (const run of runs) {
|
| 126 |
if (run.steps.qc?.status === "succeeded" || run.overall != null) {
|
| 127 |
rows.push({
|
studio/src/components/workspace.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
import { useCallback, useEffect, useRef, useState } from "react";
|
| 4 |
import { useRouter } from "next/navigation";
|
| 5 |
-
import { Check, Loader2 } from "lucide-react";
|
| 6 |
import { toast } from "sonner";
|
| 7 |
import type { ImperativePanelHandle } from "react-resizable-panels";
|
| 8 |
import type { ProjectManifest, RunRecord, StepStatus } from "@/lib/types";
|
|
@@ -35,6 +35,7 @@ export function Workspace({
|
|
| 35 |
const [openFile, setOpenFile] = useState<OpenFile | null>(null);
|
| 36 |
const [traceRunId, setTraceRunId] = useState<string | null>(initialRuns[0]?.id ?? null);
|
| 37 |
const [confirming, setConfirming] = useState(false);
|
|
|
|
| 38 |
|
| 39 |
const railRef = useRef<ImperativePanelHandle>(null);
|
| 40 |
const viewerRef = useRef<ImperativePanelHandle>(null);
|
|
@@ -98,6 +99,52 @@ export function Workspace({
|
|
| 98 |
}
|
| 99 |
}, [project.id, router]);
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
const onRunDone = useCallback(
|
| 102 |
async (status: StepStatus, live: boolean) => {
|
| 103 |
await refreshRuns();
|
|
@@ -150,12 +197,31 @@ export function Workspace({
|
|
| 150 |
</button>
|
| 151 |
) : null;
|
| 152 |
|
|
|
|
|
|
|
| 153 |
return (
|
| 154 |
-
<
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
<ResizablePanel
|
| 160 |
id="rail"
|
| 161 |
order={1}
|
|
@@ -214,6 +280,7 @@ export function Workspace({
|
|
| 214 |
onRunDone={onRunDone}
|
| 215 |
/>
|
| 216 |
</ResizablePanel>
|
| 217 |
-
|
|
|
|
| 218 |
);
|
| 219 |
}
|
|
|
|
| 2 |
|
| 3 |
import { useCallback, useEffect, useRef, useState } from "react";
|
| 4 |
import { useRouter } from "next/navigation";
|
| 5 |
+
import { Check, Loader2, Sparkles } from "lucide-react";
|
| 6 |
import { toast } from "sonner";
|
| 7 |
import type { ImperativePanelHandle } from "react-resizable-panels";
|
| 8 |
import type { ProjectManifest, RunRecord, StepStatus } from "@/lib/types";
|
|
|
|
| 35 |
const [openFile, setOpenFile] = useState<OpenFile | null>(null);
|
| 36 |
const [traceRunId, setTraceRunId] = useState<string | null>(initialRuns[0]?.id ?? null);
|
| 37 |
const [confirming, setConfirming] = useState(false);
|
| 38 |
+
const [generating, setGenerating] = useState(false);
|
| 39 |
|
| 40 |
const railRef = useRef<ImperativePanelHandle>(null);
|
| 41 |
const viewerRef = useRef<ImperativePanelHandle>(null);
|
|
|
|
| 99 |
}
|
| 100 |
}, [project.id, router]);
|
| 101 |
|
| 102 |
+
// Build a spec from the user's chat description (no uploaded protocol needed),
|
| 103 |
+
// then poll until extraction resolves and reveal the spec for review.
|
| 104 |
+
const onGenerateSpec = useCallback(async () => {
|
| 105 |
+
setGenerating(true);
|
| 106 |
+
try {
|
| 107 |
+
const res = await fetch(`/api/projects/${project.id}/generate-spec`, { method: "POST" });
|
| 108 |
+
const data = (await res.json().catch(() => ({}))) as { ok?: boolean; error?: string };
|
| 109 |
+
if (!res.ok) {
|
| 110 |
+
toast.error(data.error ?? "Could not generate a spec");
|
| 111 |
+
setGenerating(false);
|
| 112 |
+
return;
|
| 113 |
+
}
|
| 114 |
+
const deadline = Date.now() + 3 * 60 * 1000;
|
| 115 |
+
const tick = async () => {
|
| 116 |
+
try {
|
| 117 |
+
const p = (await (
|
| 118 |
+
await fetch(`/api/projects/${project.id}`, { cache: "no-store" })
|
| 119 |
+
).json()) as ProjectManifest;
|
| 120 |
+
if (p.phase === "awaiting_spec_review") {
|
| 121 |
+
setGenerating(false);
|
| 122 |
+
router.refresh();
|
| 123 |
+
openSpec();
|
| 124 |
+
return;
|
| 125 |
+
}
|
| 126 |
+
if (p.phase === "awaiting_inputs") {
|
| 127 |
+
setGenerating(false);
|
| 128 |
+
toast.error("Spec generation failed — see the chat for details");
|
| 129 |
+
router.refresh();
|
| 130 |
+
return;
|
| 131 |
+
}
|
| 132 |
+
} catch {
|
| 133 |
+
/* transient — keep polling */
|
| 134 |
+
}
|
| 135 |
+
if (Date.now() < deadline) setTimeout(tick, 1500);
|
| 136 |
+
else {
|
| 137 |
+
setGenerating(false);
|
| 138 |
+
toast.error("Spec generation timed out");
|
| 139 |
+
}
|
| 140 |
+
};
|
| 141 |
+
setTimeout(tick, 1500);
|
| 142 |
+
} catch {
|
| 143 |
+
setGenerating(false);
|
| 144 |
+
toast.error("Could not generate a spec");
|
| 145 |
+
}
|
| 146 |
+
}, [project.id, router, openSpec]);
|
| 147 |
+
|
| 148 |
const onRunDone = useCallback(
|
| 149 |
async (status: StepStatus, live: boolean) => {
|
| 150 |
await refreshRuns();
|
|
|
|
| 197 |
</button>
|
| 198 |
) : null;
|
| 199 |
|
| 200 |
+
const needsSpec = (project.phase ?? "awaiting_inputs") === "awaiting_inputs" && !project.activeSpecPath;
|
| 201 |
+
|
| 202 |
return (
|
| 203 |
+
<div className={cn("flex min-h-0 flex-col", className)}>
|
| 204 |
+
{needsSpec && (
|
| 205 |
+
<div className="border-border/60 bg-muted/30 flex shrink-0 items-center gap-3 border-b px-4 py-2">
|
| 206 |
+
<Sparkles className="text-muted-foreground size-3.5 shrink-0" />
|
| 207 |
+
<span className="text-muted-foreground text-xs">
|
| 208 |
+
Described your library in the chat? Turn it into a reviewable spec, then confirm to run QC.
|
| 209 |
+
</span>
|
| 210 |
+
<button
|
| 211 |
+
onClick={onGenerateSpec}
|
| 212 |
+
disabled={generating}
|
| 213 |
+
className="border-primary/40 bg-primary/10 text-primary hover:bg-primary/20 ml-auto inline-flex shrink-0 items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-medium disabled:opacity-50"
|
| 214 |
+
>
|
| 215 |
+
{generating ? <Loader2 className="size-3 animate-spin" /> : <Sparkles className="size-3" />}
|
| 216 |
+
{generating ? "Generating spec…" : "Generate spec"}
|
| 217 |
+
</button>
|
| 218 |
+
</div>
|
| 219 |
+
)}
|
| 220 |
+
<ResizablePanelGroup
|
| 221 |
+
direction="horizontal"
|
| 222 |
+
autoSaveId="project-workspace"
|
| 223 |
+
className="min-h-0 flex-1"
|
| 224 |
+
>
|
| 225 |
<ResizablePanel
|
| 226 |
id="rail"
|
| 227 |
order={1}
|
|
|
|
| 280 |
onRunDone={onRunDone}
|
| 281 |
/>
|
| 282 |
</ResizablePanel>
|
| 283 |
+
</ResizablePanelGroup>
|
| 284 |
+
</div>
|
| 285 |
);
|
| 286 |
}
|
studio/src/lib/chat.ts
CHANGED
|
@@ -188,6 +188,7 @@ export function buildGatingPreamble(project: ProjectManifest): string {
|
|
| 188 |
hasOligoInfo ? (hasSpec ? "extracted into the spec" : "present in inputs") : "MISSING"
|
| 189 |
}`,
|
| 190 |
`- Reads: ${hasReads ? reads : "not chosen (a labeled demo dataset is available)"}`,
|
|
|
|
| 191 |
];
|
| 192 |
|
| 193 |
switch (phase) {
|
|
@@ -196,8 +197,8 @@ export function buildGatingPreamble(project: ProjectManifest): string {
|
|
| 196 |
missing.length
|
| 197 |
? `GATE: Inputs are incomplete. Ask the user specifically for the MISSING item(s): ${missing.join(
|
| 198 |
"; ",
|
| 199 |
-
)}. They can attach files with the 📎 button
|
| 200 |
-
:
|
| 201 |
);
|
| 202 |
break;
|
| 203 |
case "extracting":
|
|
|
|
| 188 |
hasOligoInfo ? (hasSpec ? "extracted into the spec" : "present in inputs") : "MISSING"
|
| 189 |
}`,
|
| 190 |
`- Reads: ${hasReads ? reads : "not chosen (a labeled demo dataset is available)"}`,
|
| 191 |
+
"CAPABILITY: You are a READ-ONLY assistant. You CANNOT run QC, extract, or confirm/commit a spec yourself — each of those is a button the user clicks in the workspace. NEVER claim you have run QC, 'confirmed'/'locked' a spec, or produced results, and never invent numbers. When the user asks you to run or confirm, name the exact button to click.",
|
| 192 |
];
|
| 193 |
|
| 194 |
switch (phase) {
|
|
|
|
| 197 |
missing.length
|
| 198 |
? `GATE: Inputs are incomplete. Ask the user specifically for the MISSING item(s): ${missing.join(
|
| 199 |
"; ",
|
| 200 |
+
)}. They can attach files with the 📎 button, OR describe the library structure in chat and then click the "Generate spec" button at the top of the workspace to turn that description into a reviewable spec — describing it in chat alone does NOT create a spec. Do NOT claim to have built or run anything.`
|
| 201 |
+
: 'GATE: Inputs look complete. Point the user to the "Generate spec" button (or the 📎 protocol upload) to build the spec — you cannot build it yourself.',
|
| 202 |
);
|
| 203 |
break;
|
| 204 |
case "extracting":
|
studio/src/lib/extractRunner.ts
CHANGED
|
@@ -186,6 +186,92 @@ function summarizeSpec(spec: SpecDoc): string {
|
|
| 186 |
return lines.join("\n");
|
| 187 |
}
|
| 188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
export async function startExtract(projectId: string, docRel: string): Promise<void> {
|
| 190 |
if (live.has(projectId)) return;
|
| 191 |
live.add(projectId);
|
|
@@ -286,6 +372,14 @@ async function driveExtract(
|
|
| 286 |
phase: "awaiting_spec_review",
|
| 287 |
specConfirmed: false,
|
| 288 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
if (spec.assay) patch.assay = spec.assay;
|
| 290 |
// Adopt the identified id only when it's a known structure; a custom library
|
| 291 |
// keeps the neutral "new" tag rather than inventing a fake catalog id.
|
|
|
|
| 186 |
return lines.join("\n");
|
| 187 |
}
|
| 188 |
|
| 189 |
+
/** Human-readable markdown of the extracted spec — saved beside spec.json and shown under Files. */
|
| 190 |
+
function renderSpecMarkdown(spec: SpecDoc): string {
|
| 191 |
+
const L: string[] = [];
|
| 192 |
+
const push = (s = "") => L.push(s);
|
| 193 |
+
const cell = (s: unknown) => String(s ?? "").replace(/\|/g, "\\|").replace(/\s*\n\s*/g, " ").trim();
|
| 194 |
+
|
| 195 |
+
const title = spec.title || spec.assay || "Sequencing library spec";
|
| 196 |
+
push(`# ${title}`);
|
| 197 |
+
push();
|
| 198 |
+
if (spec.assay && spec.assay !== title) push(`**Assay:** ${spec.assay}`);
|
| 199 |
+
const meta: string[] = [];
|
| 200 |
+
if (spec.platform) meta.push(`**Platform:** ${spec.platform}`);
|
| 201 |
+
if (spec.chemistry_version) meta.push(`**Chemistry:** ${spec.chemistry_version}`);
|
| 202 |
+
if (spec.modality) meta.push(`**Modality:** ${spec.modality}`);
|
| 203 |
+
if (spec.method_type) meta.push(`**Method:** ${spec.method_type}`);
|
| 204 |
+
if (meta.length) push(meta.join(" · "));
|
| 205 |
+
push();
|
| 206 |
+
if (spec.description) {
|
| 207 |
+
push(spec.description);
|
| 208 |
+
push();
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
const reads = spec.read_structure?.reads ?? [];
|
| 212 |
+
if (reads.length) {
|
| 213 |
+
push("## Read structure");
|
| 214 |
+
push();
|
| 215 |
+
for (const r of reads) {
|
| 216 |
+
const segs = (r.segments ?? [])
|
| 217 |
+
.map((s) => `\`${s.name}${s.length ? `:${s.length}` : ""}\``)
|
| 218 |
+
.join(" + ");
|
| 219 |
+
const tail = [r.template ? `${r.template} strand` : "", r.cycles ? `${r.cycles} cycles` : ""]
|
| 220 |
+
.filter(Boolean)
|
| 221 |
+
.join(", ");
|
| 222 |
+
push(`- **${r.read}**${tail ? ` (${tail})` : ""}${segs ? `: ${segs}` : ""}`);
|
| 223 |
+
}
|
| 224 |
+
push();
|
| 225 |
+
for (const r of reads) {
|
| 226 |
+
if (!r.segments?.length) continue;
|
| 227 |
+
push(`**${r.read} segments**`);
|
| 228 |
+
push();
|
| 229 |
+
push("| segment | type | length |");
|
| 230 |
+
push("| --- | --- | --- |");
|
| 231 |
+
for (const s of r.segments) push(`| ${cell(s.name)} | ${cell(s.type)} | ${cell(s.length)} |`);
|
| 232 |
+
push();
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
if (spec.oligos?.length) {
|
| 237 |
+
push("## Oligos & adapters");
|
| 238 |
+
push();
|
| 239 |
+
push("| id | role | sequence |");
|
| 240 |
+
push("| --- | --- | --- |");
|
| 241 |
+
for (const o of spec.oligos) {
|
| 242 |
+
const seq = o.sequence
|
| 243 |
+
? `\`${o.sequence.length > 60 ? o.sequence.slice(0, 60) + "…" : o.sequence}\``
|
| 244 |
+
: "";
|
| 245 |
+
push(`| ${cell(o.oligo_id)} | ${cell(o.role ?? o.kind)} | ${seq} |`);
|
| 246 |
+
}
|
| 247 |
+
push();
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
if (spec.library_generation?.length) {
|
| 251 |
+
push("## Library preparation");
|
| 252 |
+
push();
|
| 253 |
+
for (const st of spec.library_generation) {
|
| 254 |
+
push(`${st.step}. **${st.title}**${st.summary ? ` — ${st.summary}` : ""}`);
|
| 255 |
+
}
|
| 256 |
+
push();
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
if (spec.final_library?.annotation_lines?.length) {
|
| 260 |
+
push("## Final library (5'→3')");
|
| 261 |
+
push();
|
| 262 |
+
push("```");
|
| 263 |
+
for (const line of spec.final_library.annotation_lines) push(line);
|
| 264 |
+
push("```");
|
| 265 |
+
push();
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
push("---");
|
| 269 |
+
push(
|
| 270 |
+
"*Generated by Seqcolyte from the extracted library spec. Review it in the workspace, then click **Confirm spec** to run QC on your reads.*",
|
| 271 |
+
);
|
| 272 |
+
return L.join("\n");
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
export async function startExtract(projectId: string, docRel: string): Promise<void> {
|
| 276 |
if (live.has(projectId)) return;
|
| 277 |
live.add(projectId);
|
|
|
|
| 372 |
phase: "awaiting_spec_review",
|
| 373 |
specConfirmed: false,
|
| 374 |
};
|
| 375 |
+
// Save a readable markdown of the spec beside the JSON, surfaced under Files.
|
| 376 |
+
try {
|
| 377 |
+
const mdRel = "spec/spec.md";
|
| 378 |
+
await fs.writeFile(inProject(projectId, mdRel), renderSpecMarkdown(spec), "utf8");
|
| 379 |
+
patch.specMarkdownPath = mdRel;
|
| 380 |
+
} catch {
|
| 381 |
+
/* markdown is a convenience artifact — never fail extraction over it */
|
| 382 |
+
}
|
| 383 |
if (spec.assay) patch.assay = spec.assay;
|
| 384 |
// Adopt the identified id only when it's a known structure; a custom library
|
| 385 |
// keeps the neutral "new" tag rather than inventing a fake catalog id.
|
studio/src/lib/types.ts
CHANGED
|
@@ -88,6 +88,8 @@ export interface ProjectManifest {
|
|
| 88 |
updatedAt: string;
|
| 89 |
/** relative-to-project path of the active spec (extract output or reference copy) */
|
| 90 |
activeSpecPath: string | null;
|
|
|
|
|
|
|
| 91 |
/** Current lifecycle phase (defaults to awaiting_inputs when absent). */
|
| 92 |
phase?: ProjectPhase;
|
| 93 |
/** The user reviewed and confirmed the extracted spec. */
|
|
|
|
| 88 |
updatedAt: string;
|
| 89 |
/** relative-to-project path of the active spec (extract output or reference copy) */
|
| 90 |
activeSpecPath: string | null;
|
| 91 |
+
/** relative-to-project path of the human-readable spec markdown (written beside the spec JSON) */
|
| 92 |
+
specMarkdownPath?: string | null;
|
| 93 |
/** Current lifecycle phase (defaults to awaiting_inputs when absent). */
|
| 94 |
phase?: ProjectPhase;
|
| 95 |
/** The user reviewed and confirmed the extracted spec. */
|
studio/tsconfig.tsbuildinfo
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|