dsh-molbio-panel 0.1.7 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/index.mjs +1 -1
- package/lib/client.js +376 -109
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -12,12 +12,12 @@ DSH Web GUI 的**浏览器内 Molbio 面板**:右侧栏的一个 tab,列出
|
|
|
12
12
|
面板必须走 **bundle 渠道**(`dsh.client` 双面包):preset 渠道挂不了客户端 UI
|
|
13
13
|
(详见 `dsh-molbio-tools` 仓库的 `docs/client-pipeline-exploration.md`)。
|
|
14
14
|
|
|
15
|
-
而 `dsh-molbio-tools` 的宿主半边会注册 **
|
|
15
|
+
而 `dsh-molbio-tools` 的宿主半边会注册 **64 个工具**——把它挂进一个共享的 Web profile,
|
|
16
16
|
等于给**该 profile 的每个会话**都塞进这些工具。所以面板单独成包:
|
|
17
17
|
|
|
18
18
|
| 包 | 宿主半边 | 用途 |
|
|
19
19
|
| --- | --- | --- |
|
|
20
|
-
| `dsh-molbio-tools` |
|
|
20
|
+
| `dsh-molbio-tools` | 64 个 molbio 工具 | 分子生物学专用 profile / preset |
|
|
21
21
|
| `dsh-molbio-panel` | **不注册任何工具**(空实现) | 只想在共享 profile 里加一个面板 |
|
|
22
22
|
|
|
23
23
|
宿主半边刻意保持空:客户端扫描按 **Loader 条目**取包清单,条目只要能加载,浏览器半就能
|
package/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* dsh-molbio-panel — host half.
|
|
3
3
|
*
|
|
4
4
|
* This package exists so the browser panel can be installed WITHOUT dragging
|
|
5
|
-
* the
|
|
5
|
+
* the molbio toolset into every session of a profile. It is a `dsh.client`
|
|
6
6
|
* dual-face package: the host half below, and the browser half built from the
|
|
7
7
|
* `dsh-molbio-tools` sources into `lib/client.js`.
|
|
8
8
|
*
|
package/lib/client.js
CHANGED
|
@@ -4964,12 +4964,35 @@ window.__ModuleLoader__.load({
|
|
|
4964
4964
|
return entries.map((entry) => `>${entry.id}${entry.description !== '' ? ' ' + entry.description : ''}\n${entry.sequence}`).join('\n') + '\n';
|
|
4965
4965
|
}
|
|
4966
4966
|
|
|
4967
|
+
/** Render entries as four-line FASTQ text (quality is preserved verbatim). */
|
|
4968
|
+
function toFastq(entries) {
|
|
4969
|
+
return entries.map((entry) => {
|
|
4970
|
+
if (typeof entry.quality !== 'string' || entry.quality.length !== entry.sequence.length) {
|
|
4971
|
+
throw new MolbioInputError(`entry ${entry.id}: sequence and quality lengths differ`);
|
|
4972
|
+
}
|
|
4973
|
+
return `@${entry.id}${entry.description !== undefined && entry.description !== '' ? ' ' + entry.description : ''}\n${entry.sequence}\n+\n${entry.quality}`;
|
|
4974
|
+
}).join('\n') + '\n';
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4967
4977
|
/** Summary statistics over parsed entries. */
|
|
4968
4978
|
function entryStats(entries, qualityMode = false) {
|
|
4969
|
-
|
|
4979
|
+
// Count in a loop instead of spreading into Math.min/Math.max: a large FASTQ
|
|
4980
|
+
// can hold hundreds of thousands of reads (and millions of quality
|
|
4981
|
+
// characters), and spreading such an array overflows the engine's argument
|
|
4982
|
+
// limit with a RangeError. The numbers are identical; only the failure mode
|
|
4983
|
+
// changes.
|
|
4984
|
+
let totalBases = 0;
|
|
4985
|
+
let minLength = entries.length === 0 ? 0 : Infinity;
|
|
4986
|
+
let maxLength = 0;
|
|
4987
|
+
let longest = 0;
|
|
4970
4988
|
let gc = 0;
|
|
4971
4989
|
let at = 0;
|
|
4972
4990
|
for (const entry of entries) {
|
|
4991
|
+
const length = entry.sequence.length;
|
|
4992
|
+
totalBases += length;
|
|
4993
|
+
if (length < minLength) minLength = length;
|
|
4994
|
+
if (length > maxLength) maxLength = length;
|
|
4995
|
+
if (length > longest) longest = length;
|
|
4973
4996
|
for (const base of entry.sequence) {
|
|
4974
4997
|
if (base === 'G' || base === 'C') gc++;
|
|
4975
4998
|
else if (base === 'A' || base === 'T') at++;
|
|
@@ -4977,22 +5000,34 @@ window.__ModuleLoader__.load({
|
|
|
4977
5000
|
}
|
|
4978
5001
|
const stats = {
|
|
4979
5002
|
entries: entries.length,
|
|
4980
|
-
total_bases:
|
|
4981
|
-
min_length:
|
|
4982
|
-
max_length:
|
|
4983
|
-
mean_length:
|
|
5003
|
+
total_bases: totalBases,
|
|
5004
|
+
min_length: minLength,
|
|
5005
|
+
max_length: maxLength,
|
|
5006
|
+
mean_length: entries.length === 0 ? 0 : Math.round((totalBases / entries.length) * 100) / 100,
|
|
4984
5007
|
gc_percent: gc + at === 0 ? 0 : Math.round((gc / (gc + at)) * 1000) / 10,
|
|
4985
5008
|
};
|
|
4986
5009
|
if (qualityMode) {
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
5010
|
+
let qualitySum = 0;
|
|
5011
|
+
let qualityCount = 0;
|
|
5012
|
+
let lowCount = 0;
|
|
5013
|
+
let qualityMin = Infinity;
|
|
5014
|
+
let qualityMax = -Infinity;
|
|
5015
|
+
for (const entry of entries) {
|
|
5016
|
+
for (const ch of entry.quality) {
|
|
5017
|
+
const q = ch.charCodeAt(0) - 33;
|
|
5018
|
+
qualitySum += q;
|
|
5019
|
+
qualityCount++;
|
|
5020
|
+
if (q < 20) lowCount++;
|
|
5021
|
+
if (q < qualityMin) qualityMin = q;
|
|
5022
|
+
if (q > qualityMax) qualityMax = q;
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
if (qualityCount > 0) {
|
|
5026
|
+
stats.quality_mean = Math.round((qualitySum / qualityCount) * 100) / 100;
|
|
5027
|
+
stats.quality_min = qualityMin;
|
|
5028
|
+
stats.quality_max = qualityMax;
|
|
5029
|
+
stats.low_quality_fraction = Math.round((lowCount / qualityCount) * 1000) / 1000;
|
|
5030
|
+
const maxLen = Math.min(150, longest);
|
|
4996
5031
|
const perPosition = [];
|
|
4997
5032
|
for (let pos = 0; pos < maxLen; pos++) {
|
|
4998
5033
|
const values = entries.filter((e) => e.sequence.length > pos).map((e) => e.quality.charCodeAt(pos) - 33);
|
|
@@ -5008,6 +5043,7 @@ window.__ModuleLoader__.load({
|
|
|
5008
5043
|
exports.parseFasta = parseFasta;
|
|
5009
5044
|
exports.parseFastq = parseFastq;
|
|
5010
5045
|
exports.toFasta = toFasta;
|
|
5046
|
+
exports.toFastq = toFastq;
|
|
5011
5047
|
exports.entryStats = entryStats;
|
|
5012
5048
|
return exports;
|
|
5013
5049
|
};
|
|
@@ -5051,17 +5087,17 @@ window.__ModuleLoader__.load({
|
|
|
5051
5087
|
* from `anchor` and not from the RC-scan `start`. It is the REVERSE primer,
|
|
5052
5088
|
* and the amplicon ends at `anchor + length - 1`.
|
|
5053
5089
|
*
|
|
5054
|
-
*
|
|
5055
|
-
*
|
|
5056
|
-
*
|
|
5057
|
-
*
|
|
5058
|
-
*
|
|
5059
|
-
*
|
|
5060
|
-
*
|
|
5061
|
-
*
|
|
5062
|
-
*
|
|
5063
|
-
*
|
|
5064
|
-
*
|
|
5090
|
+
* `designIntronSpanningPrimers` (tool: `molbio_design_intron_primers`)
|
|
5091
|
+
* follows the SAME convention since 0.23.0. The junction is carried by the
|
|
5092
|
+
* REVERSE primer (the assay sits 5' of the junction), so the reverse candidate
|
|
5093
|
+
* is scanned on the reverse complement of the spliced transcript: every 3'-end
|
|
5094
|
+
* constraint is then evaluated on the molecule that is ordered, and the hit is
|
|
5095
|
+
* mapped back to sense coordinates through `anchor` (its 3' end, the LEFT edge
|
|
5096
|
+
* of its site). `forward` is upstream of `reverse`, the spliced amplicon runs
|
|
5097
|
+
* from the forward 5' end to the reverse 5' end, and the reported reverse
|
|
5098
|
+
* sequence is what goes on the order form. Before 0.23.0 the tool reported both
|
|
5099
|
+
* primers as sense substrings, so `forward` sat downstream of `reverse` and the
|
|
5100
|
+
* reported reverse was the reverse complement of the molecule to order.
|
|
5065
5101
|
*/
|
|
5066
5102
|
|
|
5067
5103
|
const { DNA_BASES, MolbioInputError, baseCounts, complement, dimerThermo, endGcCount5, endStability5, findRuns, hairpinThermo, normalizeSequence, primerTm, reverseComplement, selfAnyScore, selfEndScore } = __molbio_require("lib.mjs");
|
|
@@ -5737,37 +5773,60 @@ window.__ModuleLoader__.load({
|
|
|
5737
5773
|
// ── cross-intron primer design ──────────────────────────────────────────────
|
|
5738
5774
|
|
|
5739
5775
|
/**
|
|
5740
|
-
* Mismatch report for cross-intron candidates.
|
|
5741
|
-
*
|
|
5742
|
-
*
|
|
5743
|
-
*
|
|
5744
|
-
*
|
|
5745
|
-
* both
|
|
5776
|
+
* Mismatch report for cross-intron candidates. The forward primer is the
|
|
5777
|
+
* sense-strand substring at its binding site; the reverse primer is the REVERSE
|
|
5778
|
+
* COMPLEMENT of the sense-strand substring at its site (the molecule that is
|
|
5779
|
+
* ordered). `anchorStart` is the 0-based sense-strand position of the primer's
|
|
5780
|
+
* 3' end — for the reverse primer that is the LEFT edge of its site — and
|
|
5781
|
+
* `isReverse` flips both the perfect-match base and how a primer offset maps
|
|
5782
|
+
* onto the transcript.
|
|
5746
5783
|
*/
|
|
5747
|
-
function intronMismatchReport(candidate, spliced, splicedToGenomic) {
|
|
5784
|
+
function intronMismatchReport(candidate, spliced, splicedToGenomic, anchorStart, isReverse) {
|
|
5748
5785
|
const len = candidate.length;
|
|
5749
|
-
return candidate.mismatches.map(({ p, base }) =>
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5786
|
+
return candidate.mismatches.map(({ p, base }) => {
|
|
5787
|
+
const offset = isReverse ? len - 1 - p : p;
|
|
5788
|
+
const index = anchorStart + offset;
|
|
5789
|
+
return {
|
|
5790
|
+
position: p + 1,
|
|
5791
|
+
template_base: isReverse ? complement(spliced[index]) : spliced[index],
|
|
5792
|
+
primer_base: base,
|
|
5793
|
+
spliced_position: index + 1,
|
|
5794
|
+
genomic_position: splicedToGenomic[index] + 1,
|
|
5795
|
+
distance_from_3prime: len - 1 - p,
|
|
5796
|
+
};
|
|
5797
|
+
});
|
|
5757
5798
|
}
|
|
5758
5799
|
|
|
5759
5800
|
/**
|
|
5760
|
-
*
|
|
5761
|
-
*
|
|
5762
|
-
*
|
|
5763
|
-
*
|
|
5801
|
+
* The same weights as `mismatchPenalty`, over the snake_case report fields.
|
|
5802
|
+
* The old intron path handed snake_case reports to the camelCase helper, so the
|
|
5803
|
+
* 3'-zone surcharge never applied there; this keeps the two conventions apart on
|
|
5804
|
+
* purpose instead of loosening the shared helper.
|
|
5805
|
+
*/
|
|
5806
|
+
function intronMismatchPenalty(report, zone) {
|
|
5807
|
+
return report.reduce((sum, mismatch) => sum + 8 + (mismatch.distance_from_3prime <= zone ? 4 : 0), 0);
|
|
5808
|
+
}
|
|
5809
|
+
|
|
5810
|
+
/**
|
|
5811
|
+
* Design qPCR primer pairs across an exon-exon junction so genomic DNA cannot
|
|
5812
|
+
* be amplified: the REVERSE primer straddles the junction (>= minJunctionBases
|
|
5813
|
+
* on each side), the FORWARD primer sits upstream inside a single exon, and the
|
|
5814
|
+
* spliced amplicon crosses the junction inside the reverse primer's site.
|
|
5815
|
+
*
|
|
5816
|
+
* Coordinates are reported both on the spliced transcript and on the genomic
|
|
5817
|
+
* sequence. Orientation follows `designPrimerPairs` (see the file header):
|
|
5818
|
+
* `forward.sequence` is the sense-strand substring at its site and
|
|
5819
|
+
* `reverse.sequence` is the reverse complement of the sense-strand substring at
|
|
5820
|
+
* its site. The reverse candidate is scanned on the reverse complement of the
|
|
5821
|
+
* spliced transcript, so every 3'-end constraint (GC clamp, end stability, end
|
|
5822
|
+
* GC, self-end, hairpin) applies to the molecule that is ordered.
|
|
5764
5823
|
*/
|
|
5765
5824
|
function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
5766
5825
|
if (!Array.isArray(exons) || exons.length < 2) throw new MolbioInputError('exons must be an array of at least two {start, end} spans');
|
|
5767
5826
|
const sorted = [...exons].sort((a, b) => a.start - b.start);
|
|
5768
5827
|
for (const exon of sorted) {
|
|
5769
5828
|
if (!Number.isInteger(exon.start) || !Number.isInteger(exon.end) || exon.start < 1 || exon.end > genomic.length || exon.start > exon.end) {
|
|
5770
|
-
throw new MolbioInputError(
|
|
5829
|
+
throw new MolbioInputError('exon ' + JSON.stringify(exon) + ' is outside the genomic sequence (length ' + genomic.length + ')');
|
|
5771
5830
|
}
|
|
5772
5831
|
}
|
|
5773
5832
|
for (let i = 1; i < sorted.length; i++) {
|
|
@@ -5790,33 +5849,39 @@ window.__ModuleLoader__.load({
|
|
|
5790
5849
|
}
|
|
5791
5850
|
});
|
|
5792
5851
|
if (spliced.length < 24) throw new MolbioInputError('the spliced transcript is too short for primer design');
|
|
5852
|
+
const splicedLength = spliced.length;
|
|
5853
|
+
const rcSpliced = reverseComplement(spliced);
|
|
5793
5854
|
|
|
5794
5855
|
// v13 target position preference (coordinates on the spliced transcript).
|
|
5795
5856
|
const targetPosition = opts.targetPosition;
|
|
5796
|
-
if (targetPosition !== undefined && (!Number.isInteger(targetPosition) || targetPosition < 1 || targetPosition >
|
|
5797
|
-
throw new MolbioInputError(
|
|
5857
|
+
if (targetPosition !== undefined && (!Number.isInteger(targetPosition) || targetPosition < 1 || targetPosition > splicedLength)) {
|
|
5858
|
+
throw new MolbioInputError('target_position ' + targetPosition + ' is outside the spliced transcript (length ' + splicedLength + ')');
|
|
5798
5859
|
}
|
|
5799
5860
|
const targetWeight = opts.targetPenalty ?? DESIGN_DEFAULTS.targetPenalty;
|
|
5800
5861
|
|
|
5801
|
-
// Forward candidates
|
|
5802
|
-
const fwd =
|
|
5803
|
-
|
|
5862
|
+
// Forward candidates: entirely inside one exon on the sense strand.
|
|
5863
|
+
const fwd = scanCandidates(spliced, 0, splicedLength - opts.lenMin, opts)
|
|
5864
|
+
.filter((candidate) => splicedToExon[candidate.start] === splicedToExon[candidate.start + candidate.length - 1])
|
|
5865
|
+
.map((candidate) => ({ ...candidate, exon: splicedToExon[candidate.start] }))
|
|
5866
|
+
.sort((a, b) => a.start - b.start);
|
|
5867
|
+
|
|
5868
|
+
// Reverse candidates: scanned on the reverse complement (the ordered
|
|
5869
|
+
// molecule), then mapped back to the sense-strand 3' end `anchor`.
|
|
5870
|
+
const rev = [];
|
|
5871
|
+
for (const candidate of scanCandidates(rcSpliced, 0, splicedLength - opts.lenMin, opts)) {
|
|
5872
|
+
const anchor = splicedLength - (candidate.start + candidate.length);
|
|
5873
|
+
const topEnd = anchor + candidate.length - 1;
|
|
5804
5874
|
for (const junction of junctionPositions) {
|
|
5805
|
-
const left = junction.splicedPos -
|
|
5806
|
-
const right =
|
|
5875
|
+
const left = junction.splicedPos - anchor;
|
|
5876
|
+
const right = topEnd + 1 - junction.splicedPos;
|
|
5807
5877
|
if (left >= minSide && right >= minSide) {
|
|
5808
|
-
|
|
5878
|
+
rev.push({ ...candidate, anchor, junction, junction_left: left, junction_right: right });
|
|
5809
5879
|
break;
|
|
5810
5880
|
}
|
|
5811
5881
|
}
|
|
5812
5882
|
}
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
const rev = scanCandidates(spliced, 0, spliced.length - opts.lenMin, opts)
|
|
5816
|
-
.filter((candidate) => splicedToExon[candidate.start] === splicedToExon[candidate.start + candidate.length - 1])
|
|
5817
|
-
.map((candidate) => ({ ...candidate, exon: splicedToExon[candidate.start] }))
|
|
5818
|
-
.sort((a, b) => a.start - b.start);
|
|
5819
|
-
const revStarts = rev.map((candidate) => candidate.start);
|
|
5883
|
+
rev.sort((a, b) => a.anchor - b.anchor);
|
|
5884
|
+
const revAnchors = rev.map((candidate) => candidate.anchor);
|
|
5820
5885
|
|
|
5821
5886
|
const tmCenter = (opts.tmMin + opts.tmMax) / 2;
|
|
5822
5887
|
const misprimingIndex = opts.checkMispriming
|
|
@@ -5825,48 +5890,52 @@ window.__ModuleLoader__.load({
|
|
|
5825
5890
|
const maxSites = opts.misprimingMaxSites ?? DESIGN_DEFAULTS.misprimingMaxSites;
|
|
5826
5891
|
const pairs = [];
|
|
5827
5892
|
for (const f of fwd) {
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5893
|
+
// The reverse site must start after the forward 3' end, and the amplicon
|
|
5894
|
+
// (forward 5' -> reverse 5') must fit the window. The bounds are widened by
|
|
5895
|
+
// the primer-length range and the exact length is checked below, because a
|
|
5896
|
+
// long reverse primer can start earlier and still close inside the window.
|
|
5897
|
+
const aMin = Math.max(f.start + f.length, f.start + opts.ampliconMin - opts.lenMax);
|
|
5898
|
+
const aMax = f.start + opts.ampliconMax - opts.lenMin;
|
|
5899
|
+
if (aMax < aMin) continue;
|
|
5831
5900
|
let lo = 0;
|
|
5832
|
-
let hi =
|
|
5901
|
+
let hi = revAnchors.length;
|
|
5833
5902
|
while (lo < hi) {
|
|
5834
5903
|
const mid = (lo + hi) >> 1;
|
|
5835
|
-
if (
|
|
5904
|
+
if (revAnchors[mid] < aMin) lo = mid + 1;
|
|
5836
5905
|
else hi = mid;
|
|
5837
5906
|
}
|
|
5838
5907
|
for (let i = lo; i < rev.length; i++) {
|
|
5839
5908
|
const r = rev[i];
|
|
5840
|
-
if (r.
|
|
5841
|
-
|
|
5842
|
-
const
|
|
5843
|
-
const
|
|
5909
|
+
if (r.anchor > aMax) break;
|
|
5910
|
+
const ampliconStart = f.start;
|
|
5911
|
+
const ampliconEnd = r.anchor + r.length - 1;
|
|
5912
|
+
const ampliconLength = ampliconEnd - ampliconStart + 1;
|
|
5913
|
+
if (ampliconLength < opts.ampliconMin || ampliconLength > opts.ampliconMax) continue;
|
|
5914
|
+
const gStart = splicedToGenomic[ampliconStart];
|
|
5915
|
+
const gEnd = splicedToGenomic[ampliconEnd];
|
|
5844
5916
|
if (gEnd - gStart + 1 < minGenomicSpan) continue;
|
|
5845
5917
|
if (Math.abs(f.tm - r.tm) > opts.maxTmDelta) continue;
|
|
5846
5918
|
const dimer = dimerThermo(f.sequence, r.sequence, opts.ctMolar ?? DEFAULT_CT_MOLAR);
|
|
5847
5919
|
if (dimer.any_tm > opts.maxDimerTm || dimer.end_tm > opts.maxDimerEndTm) continue;
|
|
5920
|
+
const misK = opts.mispriming3PrimeBases ?? DESIGN_DEFAULTS.mispriming3PrimeBases;
|
|
5848
5921
|
let fMispriming = { count: 0, sites: [] };
|
|
5849
5922
|
let rMispriming = { count: 0, sites: [] };
|
|
5850
5923
|
if (misprimingIndex !== undefined) {
|
|
5851
|
-
|
|
5852
|
-
fMispriming = misprimingForCandidate(f, f.sequence.slice(-misK), misprimingIndex, opts, f.start);
|
|
5924
|
+
// Exclude each primer's own 3'-tail window (not its 5' end).
|
|
5925
|
+
fMispriming = misprimingForCandidate(f, f.sequence.slice(-misK), misprimingIndex, opts, f.start + f.length - misK);
|
|
5853
5926
|
if (fMispriming.count > maxSites) continue;
|
|
5854
|
-
|
|
5855
|
-
// the real oligo is its reverse complement, so its 3' tail is the RC
|
|
5856
|
-
// of the FIRST k bases and the intended site is r.start
|
|
5857
|
-
const rTail = reverseComplement(r.sequence.slice(0, misK));
|
|
5858
|
-
rMispriming = misprimingForCandidate(r, rTail, misprimingIndex, opts, r.start);
|
|
5927
|
+
rMispriming = misprimingForCandidate(r, r.sequence.slice(-misK), misprimingIndex, opts, r.anchor);
|
|
5859
5928
|
if (rMispriming.count > maxSites) continue;
|
|
5860
5929
|
}
|
|
5861
|
-
const fMismatches = intronMismatchReport(f, spliced, splicedToGenomic);
|
|
5862
|
-
const rMismatches = intronMismatchReport(r, spliced, splicedToGenomic);
|
|
5930
|
+
const fMismatches = intronMismatchReport(f, spliced, splicedToGenomic, f.start, false);
|
|
5931
|
+
const rMismatches = intronMismatchReport(r, spliced, splicedToGenomic, r.anchor, true);
|
|
5863
5932
|
const zone = opts.mismatch3PrimeZone ?? DESIGN_DEFAULTS.mismatch3PrimeZone;
|
|
5864
5933
|
let fTargetDistance;
|
|
5865
5934
|
let rTargetDistance;
|
|
5866
5935
|
let targetDistance;
|
|
5867
5936
|
if (targetPosition !== undefined) {
|
|
5868
|
-
fTargetDistance = Math.abs(f.start + f.length - targetPosition); //
|
|
5869
|
-
rTargetDistance = Math.abs(r.
|
|
5937
|
+
fTargetDistance = Math.abs(f.start + f.length - targetPosition); // forward 3' end (1-based)
|
|
5938
|
+
rTargetDistance = Math.abs(r.anchor + 1 - targetPosition); // reverse 3' end (1-based)
|
|
5870
5939
|
targetDistance = Math.min(fTargetDistance, rTargetDistance);
|
|
5871
5940
|
}
|
|
5872
5941
|
const penalty = 0.6 * Math.abs(f.tm - r.tm) + Math.abs(f.tm - tmCenter) + Math.abs(r.tm - tmCenter)
|
|
@@ -5874,8 +5943,8 @@ window.__ModuleLoader__.load({
|
|
|
5874
5943
|
+ 1.0 * Math.max(0, f.self_end - 1) + 1.0 * Math.max(0, r.self_end - 1)
|
|
5875
5944
|
+ 0.2 * Math.max(0, f.hairpin_tm - 40) + 0.2 * Math.max(0, r.hairpin_tm - 40)
|
|
5876
5945
|
+ 0.2 * Math.max(0, dimer.any_tm - 40) + 0.2 * Math.max(0, dimer.end_tm - 40)
|
|
5877
|
-
+
|
|
5878
|
-
+
|
|
5946
|
+
+ intronMismatchPenalty(fMismatches, zone)
|
|
5947
|
+
+ intronMismatchPenalty(rMismatches, zone)
|
|
5879
5948
|
+ 8 * fMispriming.count + 8 * rMispriming.count
|
|
5880
5949
|
+ (targetDistance !== undefined ? targetWeight * targetDistance : 0);
|
|
5881
5950
|
pairs.push({
|
|
@@ -5893,9 +5962,7 @@ window.__ModuleLoader__.load({
|
|
|
5893
5962
|
spliced_end: f.start + f.length,
|
|
5894
5963
|
genomic_start: splicedToGenomic[f.start] + 1,
|
|
5895
5964
|
genomic_end: splicedToGenomic[f.start + f.length - 1] + 1,
|
|
5896
|
-
|
|
5897
|
-
junction_left: f.junction_left,
|
|
5898
|
-
junction_right: f.junction_right,
|
|
5965
|
+
exon: f.exon + 1,
|
|
5899
5966
|
mismatch_count: fMismatches.length,
|
|
5900
5967
|
mismatches: fMismatches,
|
|
5901
5968
|
mispriming_count: fMispriming.count,
|
|
@@ -5917,11 +5984,13 @@ window.__ModuleLoader__.load({
|
|
|
5917
5984
|
hairpin_tm: r.hairpin_tm,
|
|
5918
5985
|
end_stability_kcal: r.end_stability_kcal,
|
|
5919
5986
|
end_gc_count: r.end_gc_count,
|
|
5920
|
-
spliced_start: r.
|
|
5921
|
-
spliced_end: r.
|
|
5922
|
-
genomic_start: splicedToGenomic[r.
|
|
5923
|
-
genomic_end: splicedToGenomic[r.
|
|
5924
|
-
|
|
5987
|
+
spliced_start: r.anchor + 1,
|
|
5988
|
+
spliced_end: r.anchor + r.length,
|
|
5989
|
+
genomic_start: splicedToGenomic[r.anchor] + 1,
|
|
5990
|
+
genomic_end: splicedToGenomic[r.anchor + r.length - 1] + 1,
|
|
5991
|
+
exons: [String(r.junction.up + 1), String(r.junction.down + 1)],
|
|
5992
|
+
junction_left: r.junction_left,
|
|
5993
|
+
junction_right: r.junction_right,
|
|
5925
5994
|
mismatch_count: rMismatches.length,
|
|
5926
5995
|
mismatches: rMismatches,
|
|
5927
5996
|
mispriming_count: rMispriming.count,
|
|
@@ -5933,7 +6002,7 @@ window.__ModuleLoader__.load({
|
|
|
5933
6002
|
})),
|
|
5934
6003
|
...(rTargetDistance !== undefined ? { target_distance: rTargetDistance } : {}),
|
|
5935
6004
|
},
|
|
5936
|
-
spliced_amplicon: { start:
|
|
6005
|
+
spliced_amplicon: { start: ampliconStart + 1, end: ampliconEnd + 1, length: ampliconLength },
|
|
5937
6006
|
genomic_amplicon_length: gEnd - gStart + 1,
|
|
5938
6007
|
...(targetDistance !== undefined ? { target_distance: targetDistance } : {}),
|
|
5939
6008
|
penalty: Math.round(penalty * 100) / 100,
|
|
@@ -5944,7 +6013,7 @@ window.__ModuleLoader__.load({
|
|
|
5944
6013
|
const seen = new Set();
|
|
5945
6014
|
const unique = [];
|
|
5946
6015
|
for (const pair of pairs) {
|
|
5947
|
-
const key =
|
|
6016
|
+
const key = pair.spliced_amplicon.start + ':' + pair.spliced_amplicon.end;
|
|
5948
6017
|
if (seen.has(key)) continue;
|
|
5949
6018
|
seen.add(key);
|
|
5950
6019
|
unique.push(pair);
|
|
@@ -7093,12 +7162,27 @@ window.__ModuleLoader__.load({
|
|
|
7093
7162
|
};
|
|
7094
7163
|
const WATER = 18.015;
|
|
7095
7164
|
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7165
|
+
/**
|
|
7166
|
+
* Selectable pK sets for the Henderson-Hasselbalch charge model. `bjellqvist`
|
|
7167
|
+
* is the set `proteinProperties` has always used for its pI; `emboss` is the
|
|
7168
|
+
* Epk.dat table EMBOSS ships with `iep`. They are NOT interchangeable — the
|
|
7169
|
+
* reported set name travels with every charge/pI value.
|
|
7170
|
+
*/
|
|
7171
|
+
const PK_SETS = {
|
|
7172
|
+
bjellqvist: {
|
|
7173
|
+
label: 'Bjellqvist 1993',
|
|
7174
|
+
Nterm: 8.6, Cterm: 3.6, C: 9.0, D: 4.05, E: 4.45, H: 6.04,
|
|
7175
|
+
K: 10.8, R: 12.0, Y: 10.3,
|
|
7176
|
+
},
|
|
7177
|
+
emboss: {
|
|
7178
|
+
label: 'EMBOSS Epk.dat',
|
|
7179
|
+
Nterm: 8.6, Cterm: 3.6, C: 8.5, D: 3.9, E: 4.1, H: 6.5,
|
|
7180
|
+
K: 10.8, R: 12.5, Y: 10.1,
|
|
7181
|
+
},
|
|
7100
7182
|
};
|
|
7101
7183
|
|
|
7184
|
+
const PK = PK_SETS.bjellqvist;
|
|
7185
|
+
|
|
7102
7186
|
const HYDROPATHY = {
|
|
7103
7187
|
A: 1.8, R: -4.5, N: -3.5, D: -3.5, C: 2.5, Q: -3.5, E: -3.5,
|
|
7104
7188
|
G: -0.4, H: -3.2, I: 4.5, L: 3.8, K: -3.9, M: 1.9, F: 2.8,
|
|
@@ -7111,30 +7195,37 @@ window.__ModuleLoader__.load({
|
|
|
7111
7195
|
return counts;
|
|
7112
7196
|
}
|
|
7113
7197
|
|
|
7114
|
-
|
|
7198
|
+
/**
|
|
7199
|
+
* Net charge at a given pH by Henderson-Hasselbalch summation. The terminal
|
|
7200
|
+
* groups count once each; K/R/H are positive, D/E/C/Y negative. `pkaSet` is a
|
|
7201
|
+
* key of PK_SETS (or a set object), so callers can name the table they used.
|
|
7202
|
+
*/
|
|
7203
|
+
function netCharge(seq, counts, pH, pkaSet = 'bjellqvist') {
|
|
7204
|
+
const pk = typeof pkaSet === 'string' ? PK_SETS[pkaSet] : pkaSet;
|
|
7205
|
+
if (pk === undefined) throw new MolbioInputError('unknown pK set ' + JSON.stringify(pkaSet) + '; expected one of ' + Object.keys(PK_SETS).join(', '));
|
|
7115
7206
|
let charge = 0;
|
|
7116
7207
|
const pos = (pKa) => 1 / (1 + Math.pow(10, pH - pKa));
|
|
7117
7208
|
const neg = (pKa) => 1 / (1 + Math.pow(10, pKa - pH));
|
|
7118
|
-
charge += pos(
|
|
7119
|
-
for (const [aa, pKa] of [['K',
|
|
7209
|
+
charge += pos(pk.Nterm);
|
|
7210
|
+
for (const [aa, pKa] of [['K', pk.K], ['R', pk.R], ['H', pk.H]]) {
|
|
7120
7211
|
if (counts[aa] !== undefined) charge += counts[aa] * pos(pKa);
|
|
7121
7212
|
}
|
|
7122
|
-
charge -= neg(
|
|
7123
|
-
for (const [aa, pKa] of [['D',
|
|
7213
|
+
charge -= neg(pk.Cterm);
|
|
7214
|
+
for (const [aa, pKa] of [['D', pk.D], ['E', pk.E], ['C', pk.C], ['Y', pk.Y]]) {
|
|
7124
7215
|
if (counts[aa] !== undefined) charge -= counts[aa] * neg(pKa);
|
|
7125
7216
|
}
|
|
7126
7217
|
return charge;
|
|
7127
7218
|
}
|
|
7128
7219
|
|
|
7129
7220
|
/** Isoelectric point by bisection over the net-charge function. */
|
|
7130
|
-
function isoelectricPoint(seq, counts) {
|
|
7221
|
+
function isoelectricPoint(seq, counts, pkaSet = 'bjellqvist') {
|
|
7131
7222
|
let lo = 0;
|
|
7132
7223
|
let hi = 14;
|
|
7133
|
-
let chargeLo =
|
|
7134
|
-
let chargeHi =
|
|
7224
|
+
let chargeLo = netCharge(seq, counts, lo, pkaSet);
|
|
7225
|
+
let chargeHi = netCharge(seq, counts, hi, pkaSet);
|
|
7135
7226
|
for (let i = 0; i < 60; i++) {
|
|
7136
7227
|
const mid = (lo + hi) / 2;
|
|
7137
|
-
const chargeMid =
|
|
7228
|
+
const chargeMid = netCharge(seq, counts, mid, pkaSet);
|
|
7138
7229
|
if (Math.abs(chargeMid) < 1e-4) return Math.round(mid * 100) / 100;
|
|
7139
7230
|
if (Math.sign(chargeMid) === Math.sign(chargeLo)) {
|
|
7140
7231
|
lo = mid;
|
|
@@ -7439,6 +7530,9 @@ window.__ModuleLoader__.load({
|
|
|
7439
7530
|
}
|
|
7440
7531
|
|
|
7441
7532
|
exports.normalizeProtein = normalizeProtein;
|
|
7533
|
+
exports.PK_SETS = PK_SETS;
|
|
7534
|
+
exports.countAminoAcids = countAminoAcids;
|
|
7535
|
+
exports.netCharge = netCharge;
|
|
7442
7536
|
exports.isoelectricPoint = isoelectricPoint;
|
|
7443
7537
|
exports.proteinProperties = proteinProperties;
|
|
7444
7538
|
exports.peptideDigest = peptideDigest;
|
|
@@ -7728,14 +7822,22 @@ window.__ModuleLoader__.load({
|
|
|
7728
7822
|
}
|
|
7729
7823
|
if (current !== null) peaks.push(current);
|
|
7730
7824
|
const total = points.reduce((sum, point) => sum + point.hydropathy, 0);
|
|
7825
|
+
// Loop rather than Math.min(...points): the protein length is not capped and
|
|
7826
|
+
// a long input would overflow the argument limit with a RangeError.
|
|
7827
|
+
let minimumHydropathy = Infinity;
|
|
7828
|
+
let maximumHydropathy = -Infinity;
|
|
7829
|
+
for (const point of points) {
|
|
7830
|
+
if (point.hydropathy < minimumHydropathy) minimumHydropathy = point.hydropathy;
|
|
7831
|
+
if (point.hydropathy > maximumHydropathy) maximumHydropathy = point.hydropathy;
|
|
7832
|
+
}
|
|
7731
7833
|
return {
|
|
7732
7834
|
length: sequence.length,
|
|
7733
7835
|
window,
|
|
7734
7836
|
threshold,
|
|
7735
7837
|
gravy: sequence.length === 0 ? 0 : round2([...sequence].reduce((sum, aa) => sum + KYTE_DOOLITTLE[aa], 0) / sequence.length),
|
|
7736
7838
|
mean_profile: round3(total / points.length),
|
|
7737
|
-
minimum_hydropathy: round3(
|
|
7738
|
-
maximum_hydropathy: round3(
|
|
7839
|
+
minimum_hydropathy: round3(minimumHydropathy),
|
|
7840
|
+
maximum_hydropathy: round3(maximumHydropathy),
|
|
7739
7841
|
peaks: peaks.map((peak) => ({ ...peak, maximum: round3(peak.maximum), length: peak.end - peak.start + 1 })),
|
|
7740
7842
|
points,
|
|
7741
7843
|
};
|
|
@@ -8394,6 +8496,91 @@ window.__ModuleLoader__.load({
|
|
|
8394
8496
|
return parts.join(' · ');
|
|
8395
8497
|
}
|
|
8396
8498
|
|
|
8499
|
+
// ── figure cards (the same `tool.call.toolview` seat) ───────────────────────
|
|
8500
|
+
|
|
8501
|
+
/** The meta kind the sequence-logo and gRNA tools attach to their results. */
|
|
8502
|
+
const FIGURE_CARD_KIND = 'molbio-figure';
|
|
8503
|
+
|
|
8504
|
+
const CARD_SUMMARY_MAX = 300;
|
|
8505
|
+
const CARD_PATH_MAX = 500;
|
|
8506
|
+
const CARD_GUIDES_MAX = 5;
|
|
8507
|
+
|
|
8508
|
+
/** A bounded string, or '' when the value is not a string. */
|
|
8509
|
+
function cardText(value, max) {
|
|
8510
|
+
return typeof value === 'string' ? value.slice(0, max) : '';
|
|
8511
|
+
}
|
|
8512
|
+
|
|
8513
|
+
/** One guide row, validated field by field; malformed rows are dropped. */
|
|
8514
|
+
function cardGuide(row) {
|
|
8515
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row)) return undefined;
|
|
8516
|
+
const sequence = cardText(row.sequence, 60);
|
|
8517
|
+
if (sequence === '') return undefined;
|
|
8518
|
+
return {
|
|
8519
|
+
rank: Number.isInteger(row.rank) ? row.rank : 0,
|
|
8520
|
+
sequence,
|
|
8521
|
+
pam: cardText(row.pam, 12),
|
|
8522
|
+
strand: row.strand === 'reverse' ? 'reverse' : 'forward',
|
|
8523
|
+
start: Number.isInteger(row.start) ? row.start : 0,
|
|
8524
|
+
end: Number.isInteger(row.end) ? row.end : 0,
|
|
8525
|
+
score: typeof row.score === 'number' && Number.isFinite(row.score) ? row.score : 0,
|
|
8526
|
+
offTargets: Number.isInteger(row.off_target_count) ? row.off_target_count : 0,
|
|
8527
|
+
};
|
|
8528
|
+
}
|
|
8529
|
+
|
|
8530
|
+
/**
|
|
8531
|
+
* Read a tool-result block's meta into what the figure card draws.
|
|
8532
|
+
*
|
|
8533
|
+
* Like `mapCardView`, the value is a projection this package produced itself,
|
|
8534
|
+
* but the card renders inside the conversation, so every field is checked and
|
|
8535
|
+
* bounded rather than trusted: a block from an older version, another tool, a
|
|
8536
|
+
* running call or a failed run must degrade to a notice, never throw.
|
|
8537
|
+
*
|
|
8538
|
+
* @param {unknown} meta the block's meta, as the client received it.
|
|
8539
|
+
* @returns {{kind: 'notice', message: string, ...} | {kind: 'figure', figure:
|
|
8540
|
+
* 'logo'|'grna', summary: string, svg: string, svgPath: string,
|
|
8541
|
+
* csvPath: string, mapPath: string, guides: Array}}
|
|
8542
|
+
*/
|
|
8543
|
+
function figureCardView(meta) {
|
|
8544
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
8545
|
+
return { kind: 'notice', message: 'No figure data was attached to this call.' };
|
|
8546
|
+
}
|
|
8547
|
+
if (meta.kind !== FIGURE_CARD_KIND) {
|
|
8548
|
+
return { kind: 'notice', message: 'This call carries no figure.' };
|
|
8549
|
+
}
|
|
8550
|
+
const figure = meta.figure === 'grna' ? 'grna' : meta.figure === 'logo' ? 'logo' : '';
|
|
8551
|
+
if (figure === '') {
|
|
8552
|
+
return { kind: 'notice', message: 'This call carries an unknown figure type.' };
|
|
8553
|
+
}
|
|
8554
|
+
const base = {
|
|
8555
|
+
figure,
|
|
8556
|
+
summary: cardText(meta.summary, CARD_SUMMARY_MAX),
|
|
8557
|
+
svgPath: cardText(meta.svg_path, CARD_PATH_MAX),
|
|
8558
|
+
csvPath: cardText(meta.csv_path, CARD_PATH_MAX),
|
|
8559
|
+
mapPath: cardText(meta.map_path, CARD_PATH_MAX),
|
|
8560
|
+
guides: Array.isArray(meta.guides)
|
|
8561
|
+
// Bound the work before validating, then keep at most five VALID rows:
|
|
8562
|
+
// malformed entries must not consume the budget and hide good ones.
|
|
8563
|
+
? meta.guides.slice(0, 50).map(cardGuide).filter((guide) => guide !== undefined).slice(0, CARD_GUIDES_MAX)
|
|
8564
|
+
: [],
|
|
8565
|
+
svg: '',
|
|
8566
|
+
};
|
|
8567
|
+
if (!isSvgMarkup(meta.svg)) {
|
|
8568
|
+
const message = meta.svg_missing === true
|
|
8569
|
+
? 'No picture was requested for this call; the numbers are in the tool result.'
|
|
8570
|
+
: meta.svg_omitted === true
|
|
8571
|
+
? `The figure is too large to draw inline (${Math.round((Number.isInteger(meta.svg_bytes) ? meta.svg_bytes : 0) / 1024)} KB); the written file is named below.`
|
|
8572
|
+
: 'The figure markup did not travel with this call.';
|
|
8573
|
+
return { kind: 'notice', message, ...base };
|
|
8574
|
+
}
|
|
8575
|
+
return { kind: 'figure', ...base, svg: meta.svg };
|
|
8576
|
+
}
|
|
8577
|
+
|
|
8578
|
+
/** The header line for a figure card, with a stable fallback. */
|
|
8579
|
+
function figureCardSummary(view) {
|
|
8580
|
+
if (view.summary !== '') return view.summary;
|
|
8581
|
+
return view.figure === 'logo' ? 'Sequence logo' : 'gRNA design';
|
|
8582
|
+
}
|
|
8583
|
+
|
|
8397
8584
|
// ── literature library (papers.json) ────────────────────────────────────────
|
|
8398
8585
|
|
|
8399
8586
|
/** The library file the molbio_paper_* tools write, relative to the workspace. */
|
|
@@ -8571,6 +8758,9 @@ window.__ModuleLoader__.load({
|
|
|
8571
8758
|
exports.MAP_CARD_KIND = MAP_CARD_KIND;
|
|
8572
8759
|
exports.mapCardView = mapCardView;
|
|
8573
8760
|
exports.mapCardSummary = mapCardSummary;
|
|
8761
|
+
exports.FIGURE_CARD_KIND = FIGURE_CARD_KIND;
|
|
8762
|
+
exports.figureCardView = figureCardView;
|
|
8763
|
+
exports.figureCardSummary = figureCardSummary;
|
|
8574
8764
|
exports.LIBRARY_FILE = LIBRARY_FILE;
|
|
8575
8765
|
exports.readWorkspaceText = readWorkspaceText;
|
|
8576
8766
|
exports.parseLibrary = parseLibrary;
|
|
@@ -8619,7 +8809,7 @@ window.__ModuleLoader__.load({
|
|
|
8619
8809
|
* inside the factory would also have to be torn down by hand on unload.
|
|
8620
8810
|
*/
|
|
8621
8811
|
const { createElement: h, useEffect, useMemo, useRef, useState } = __ext0;
|
|
8622
|
-
const { LIBRARY_FILE, NCBI_KEY_REF, childPath, classifyEntry, credentialInfo, isMolbioDetailSubject, remoteFailure, decodeWorkspaceBytes, filterPapers, isOpenable, libraryTags, logoSvg, mapCardSummary, mapCardView, paperFields, paperLink, paperSummary, parseAlignmentFile, parseLibrary, parsePlasmidFile, plasmidSvg, readWorkspaceText, sortEntries } = __molbio_require("build/panel-core.mjs");
|
|
8812
|
+
const { LIBRARY_FILE, NCBI_KEY_REF, childPath, classifyEntry, credentialInfo, figureCardSummary, figureCardView, isMolbioDetailSubject, remoteFailure, decodeWorkspaceBytes, filterPapers, isOpenable, libraryTags, logoSvg, mapCardSummary, mapCardView, paperFields, paperLink, paperSummary, parseAlignmentFile, parseLibrary, parsePlasmidFile, plasmidSvg, readWorkspaceText, sortEntries } = __molbio_require("build/panel-core.mjs");
|
|
8623
8813
|
/** This implementation's identity, and the key its body/title register under. */
|
|
8624
8814
|
const PANEL_ID = 'dsh-molbio-tools';
|
|
8625
8815
|
/** Type discriminator the tab is opened by. */
|
|
@@ -8858,8 +9048,10 @@ window.__ModuleLoader__.load({
|
|
|
8858
9048
|
return 'Papers';
|
|
8859
9049
|
}
|
|
8860
9050
|
|
|
8861
|
-
/** The tool names whose transcript card this package draws. */
|
|
9051
|
+
/** The tool names whose transcript card this package draws (plasmid maps). */
|
|
8862
9052
|
const MAP_TOOL_KEYS = ['molbio_plasmid_map', 'molbio_plasmid_map_file'];
|
|
9053
|
+
/** Figure tools: a drawn logo / gRNA map plus (for gRNA) the ranked list. */
|
|
9054
|
+
const FIGURE_TOOL_KEYS = ['molbio_sequence_logo', 'molbio_grna_design'];
|
|
8863
9055
|
|
|
8864
9056
|
/**
|
|
8865
9057
|
* The transcript card for the map tools (`tool.call.toolview`, keyed by tool
|
|
@@ -8928,6 +9120,72 @@ window.__ModuleLoader__.load({
|
|
|
8928
9120
|
return h('div', { style: styles.card }, ...content);
|
|
8929
9121
|
}
|
|
8930
9122
|
|
|
9123
|
+
/**
|
|
9124
|
+
* The transcript card for the sequence-logo and gRNA tools. The logo card draws
|
|
9125
|
+
* the logo; the gRNA card draws the map when `map_path` was requested AND lists
|
|
9126
|
+
* the top-ranked guides (a gRNA call without a map still gets the table). Like
|
|
9127
|
+
* the map card it is a pure function of the block, and everything it reads is
|
|
9128
|
+
* validated and bounded by `figureCardView`.
|
|
9129
|
+
*/
|
|
9130
|
+
function MolbioFigureCard({ block, inspect }) {
|
|
9131
|
+
const settled = block !== null && typeof block === 'object' && block.kind === 'tool-result';
|
|
9132
|
+
const view = figureCardView(settled ? block.meta : undefined);
|
|
9133
|
+
const host = useRef(null);
|
|
9134
|
+
const svg = view.kind === 'figure' ? view.svg : '';
|
|
9135
|
+
const [drawError, setDrawError] = useState(null);
|
|
9136
|
+
|
|
9137
|
+
useEffect(() => {
|
|
9138
|
+
const node = host.current;
|
|
9139
|
+
if (node === null) return;
|
|
9140
|
+
node.replaceChildren();
|
|
9141
|
+
if (svg === '') return;
|
|
9142
|
+
const parsed = new DOMParser().parseFromString(svg, 'image/svg+xml');
|
|
9143
|
+
if (parsed.querySelector('parsererror') !== null) {
|
|
9144
|
+
setDrawError('the figure markup could not be parsed as SVG');
|
|
9145
|
+
return;
|
|
9146
|
+
}
|
|
9147
|
+
setDrawError(null);
|
|
9148
|
+
const element = parsed.documentElement;
|
|
9149
|
+
element.setAttribute('style', 'width:100%;height:auto;max-width:640px');
|
|
9150
|
+
node.append(document.importNode(element, true));
|
|
9151
|
+
}, [svg]);
|
|
9152
|
+
|
|
9153
|
+
const content = [h('div', { key: 'head', style: styles.cardHead }, settled ? figureCardSummary(view) : 'Drawing the figure…')];
|
|
9154
|
+
if (view.kind === 'figure') {
|
|
9155
|
+
if (drawError !== null) content.push(h('div', { key: 'drawerr', style: styles.cardNote }, drawError));
|
|
9156
|
+
content.push(h('div', { key: 'svg', ref: host, style: styles.cardSvg }));
|
|
9157
|
+
} else {
|
|
9158
|
+
content.push(h('div', { key: 'note', style: styles.cardNote }, view.message));
|
|
9159
|
+
}
|
|
9160
|
+
if (view.guides.length > 0) {
|
|
9161
|
+
const head = ['#', 'Guide', 'PAM', 'Position', 'Score', 'Off-targets'];
|
|
9162
|
+
const rows = view.guides.map((guide) => h('tr', { key: guide.rank },
|
|
9163
|
+
h('td', { style: styles.td }, String(guide.rank)),
|
|
9164
|
+
h('td', { style: styles.td }, guide.sequence),
|
|
9165
|
+
h('td', { style: styles.td }, guide.pam === '' ? '—' : guide.pam),
|
|
9166
|
+
h('td', { style: styles.td }, `${guide.strand === 'reverse' ? '−' : '+'}${guide.start}-${guide.end}`),
|
|
9167
|
+
h('td', { style: styles.td }, String(guide.score)),
|
|
9168
|
+
h('td', { style: styles.td }, String(guide.offTargets)),
|
|
9169
|
+
));
|
|
9170
|
+
content.push(h('table', { key: 'guides', style: styles.table },
|
|
9171
|
+
h('thead', null, h('tr', null, ...head.map((label) => h('th', { key: label, style: styles.th }, label)))),
|
|
9172
|
+
h('tbody', null, ...rows)));
|
|
9173
|
+
}
|
|
9174
|
+
if (view.svgPath !== '') content.push(h('div', { key: 'path', style: styles.cardPath }, view.svgPath));
|
|
9175
|
+
if (view.csvPath !== '') content.push(h('div', { key: 'csv', style: styles.cardPath }, view.csvPath));
|
|
9176
|
+
if (settled && block.isError === true) {
|
|
9177
|
+
const text = (Array.isArray(block.content) ? block.content : [])
|
|
9178
|
+
.map((item) => (item !== null && typeof item === 'object' && item.type === 'text' ? String(item.text ?? '') : ''))
|
|
9179
|
+
.join('\n')
|
|
9180
|
+
.trim();
|
|
9181
|
+
content.push(h('div', { key: 'err', style: styles.error }, text === '' ? 'The call failed.' : text));
|
|
9182
|
+
if (typeof inspect === 'function') {
|
|
9183
|
+
content.push(h('button', { key: 'inspect', style: styles.cardButton, onClick: () => inspect() }, 'Inspect'));
|
|
9184
|
+
}
|
|
9185
|
+
}
|
|
9186
|
+
return h('div', { style: styles.card }, ...content);
|
|
9187
|
+
}
|
|
9188
|
+
|
|
8931
9189
|
/**
|
|
8932
9190
|
* The literature panel: the workspace `papers.json` the molbio_paper_* tools
|
|
8933
9191
|
* maintain, rendered as a searchable reading list with a detail pane.
|
|
@@ -9276,6 +9534,15 @@ window.__ModuleLoader__.load({
|
|
|
9276
9534
|
key: toolName,
|
|
9277
9535
|
}, MolbioMapCard)), `molbio panel: ${toolName} card`);
|
|
9278
9536
|
}
|
|
9537
|
+
// Figure cards: the sequence-logo and gRNA tools declare a `molbio-figure`
|
|
9538
|
+
// projection; the same component draws a logo or a gRNA map and, for gRNA,
|
|
9539
|
+
// lists the top-ranked guides even when no map was requested.
|
|
9540
|
+
for (const toolName of FIGURE_TOOL_KEYS) {
|
|
9541
|
+
ctx.effect(() => ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
9542
|
+
name: 'tool.call.toolview',
|
|
9543
|
+
key: toolName,
|
|
9544
|
+
}, MolbioFigureCard)), `molbio panel: ${toolName} card`);
|
|
9545
|
+
}
|
|
9279
9546
|
// Plugins -> the installed plugin's detail page: the one place a user can
|
|
9280
9547
|
// store the NCBI key without editing a YAML file or exporting a variable.
|
|
9281
9548
|
//
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-molbio-panel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Browser panel for the DeepSeek Harness Web GUI: a Molbio tab in the right sidebar that draws plasmid maps and sequence logos from workspace sequence files, using the same computation modules as the dsh-molbio-tools plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"0.2.1-rc.2": "compatible",
|
|
28
28
|
"0.2.1-rc.3": "compatible",
|
|
29
29
|
"0.2.1-rc.4": "unknown",
|
|
30
|
-
"0.2.1-rc.5": "compatible"
|
|
30
|
+
"0.2.1-rc.5": "compatible",
|
|
31
|
+
"0.2.2-rc.1": "compatible"
|
|
31
32
|
}
|
|
32
33
|
},
|
|
33
34
|
"bundle": {
|