dbtips-curate 0.2.0__py3-none-any.whl
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.
- dbtips_curate/__init__.py +1 -0
- dbtips_curate/cli/__init__.py +0 -0
- dbtips_curate/cli/_common.py +67 -0
- dbtips_curate/cli/_output.py +51 -0
- dbtips_curate/cli/_widget.py +207 -0
- dbtips_curate/cli/dev.py +358 -0
- dbtips_curate/cli/entity.py +723 -0
- dbtips_curate/cli/history.py +213 -0
- dbtips_curate/cli/main.py +70 -0
- dbtips_curate/cli/op.py +399 -0
- dbtips_curate/core/__init__.py +0 -0
- dbtips_curate/core/config.py +181 -0
- dbtips_curate/core/fs.py +154 -0
- dbtips_curate/core/git.py +80 -0
- dbtips_curate/core/git_history.py +183 -0
- dbtips_curate/core/merge.py +314 -0
- dbtips_curate/core/ops.py +78 -0
- dbtips_curate/core/schema.py +161 -0
- dbtips_curate/core/worklist.py +166 -0
- dbtips_curate/mcp_server.py +114 -0
- dbtips_curate/skill/_deferred/targetability-design-note.md +170 -0
- dbtips_curate/skill/dbtips-curate/SKILL.md +400 -0
- dbtips_curate/skill/dbtips-curate-antibodies/SKILL.md +219 -0
- dbtips_curate/skill/dbtips-curate-assayability/SKILL.md +175 -0
- dbtips_curate/skill/dbtips-curate-gene-essentiality/SKILL.md +107 -0
- dbtips_curate/skill/dbtips-curate-ontology/SKILL.md +129 -0
- dbtips_curate/skill/dbtips-curate-orthologs/SKILL.md +123 -0
- dbtips_curate/skill/dbtips-curate-supplementary-materials/SKILL.md +147 -0
- dbtips_curate/skill/dbtips-curate-target-literature/SKILL.md +286 -0
- dbtips_curate/skill/dbtips-curate-target-pathway/SKILL.md +166 -0
- dbtips_curate/skill/dbtips-curate-target-pipeline/SKILL.md +291 -0
- dbtips_curate-0.2.0.dist-info/METADATA +278 -0
- dbtips_curate-0.2.0.dist-info/RECORD +35 -0
- dbtips_curate-0.2.0.dist-info/WHEEL +4 -0
- dbtips_curate-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Helpers shared by command handlers — config + entity resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from dbtips_curate.cli._output import emit_error
|
|
11
|
+
from dbtips_curate.core.config import Config, load_config
|
|
12
|
+
from dbtips_curate.core.fs import EntityPaths
|
|
13
|
+
from dbtips_curate.core.schema import Section
|
|
14
|
+
|
|
15
|
+
# Sentinel: when a section-aware command's --type option receives this,
|
|
16
|
+
# resolve() derives entity_type from the section's entity_scope. Explicit
|
|
17
|
+
# --type values on the CLI still win — this only fires when the user doesn't
|
|
18
|
+
# pass --type. Used by every command that also takes --section (delete, add,
|
|
19
|
+
# patch, entity status/track/snapshot/records/diff/ingest/compare/finalize).
|
|
20
|
+
AUTO_ENTITY_TYPE = "__auto__"
|
|
21
|
+
|
|
22
|
+
# Back-compat default for commands that DON'T take --section (list, undo).
|
|
23
|
+
# They can't consult the section registry to auto-derive the type.
|
|
24
|
+
DEFAULT_ENTITY_TYPE = "target_disease"
|
|
25
|
+
DEFAULT_SECTION = "/evidence/target-literature/"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_config(ctx: typer.Context) -> Config:
|
|
29
|
+
config = ctx.obj.get("config") if ctx.obj else None
|
|
30
|
+
if config is None:
|
|
31
|
+
try:
|
|
32
|
+
config = load_config()
|
|
33
|
+
except FileNotFoundError as e:
|
|
34
|
+
emit_error("NO_CONFIG", str(e), json_mode=ctx.obj.get("json", False),
|
|
35
|
+
hint="Run from inside a directory under your platform repo "
|
|
36
|
+
"with a curation/.dbtips-curate.toml")
|
|
37
|
+
raise typer.Exit(code=2)
|
|
38
|
+
ctx.ensure_object(dict)
|
|
39
|
+
ctx.obj["config"] = config
|
|
40
|
+
return config
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve(ctx: typer.Context, entity_type: str, entity_id: str, endpoint: str) -> tuple[EntityPaths, Section]:
|
|
44
|
+
config = get_config(ctx)
|
|
45
|
+
try:
|
|
46
|
+
section = config.registry.get(endpoint)
|
|
47
|
+
except KeyError:
|
|
48
|
+
emit_error("UNKNOWN_SECTION", f"No schema registered for section {endpoint!r}",
|
|
49
|
+
json_mode=ctx.obj.get("json", False),
|
|
50
|
+
hint="Add an entry under [sections.\"%s\"] in .dbtips-curate.toml" % endpoint)
|
|
51
|
+
raise typer.Exit(code=2)
|
|
52
|
+
# Derive --type from the section's entity_scope when the user didn't pass
|
|
53
|
+
# --type explicitly. Explicit --type still wins (needed for tests and for
|
|
54
|
+
# sections that are legitimately curated in both scopes).
|
|
55
|
+
if entity_type == AUTO_ENTITY_TYPE:
|
|
56
|
+
entity_type = section.entity_scope
|
|
57
|
+
paths = EntityPaths(config, entity_type, entity_id)
|
|
58
|
+
return paths, section
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def require_served_file(ctx: typer.Context, paths: EntityPaths) -> None:
|
|
62
|
+
if not paths.served_path.exists():
|
|
63
|
+
emit_error("NO_SERVED_FILE",
|
|
64
|
+
f"Served file not found: {paths.served_path}",
|
|
65
|
+
json_mode=ctx.obj.get("json", False),
|
|
66
|
+
hint="Has the pipeline generated this entity yet?")
|
|
67
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Shared output helpers — keeps human/JSON formatting out of command handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _stdout() -> Console:
|
|
14
|
+
return Console(file=sys.stdout, force_terminal=False, no_color=_no_color())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _stderr() -> Console:
|
|
18
|
+
return Console(file=sys.stderr, force_terminal=False, no_color=_no_color())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _no_color() -> bool:
|
|
22
|
+
return bool(os.environ.get("NO_COLOR")) or not sys.stdout.isatty()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def emit_ok(data: Any, *, json_mode: bool, human: str | None = None) -> None:
|
|
26
|
+
"""Result -> stdout. JSON or human."""
|
|
27
|
+
if json_mode:
|
|
28
|
+
print(json.dumps({"ok": True, "data": data}, default=str))
|
|
29
|
+
else:
|
|
30
|
+
if human is not None:
|
|
31
|
+
print(human)
|
|
32
|
+
else:
|
|
33
|
+
print(json.dumps(data, indent=2, default=str))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def emit_error(code: str, message: str, *, json_mode: bool, hint: str | None = None) -> None:
|
|
37
|
+
"""Errors -> stderr; do not pollute stdout."""
|
|
38
|
+
if json_mode:
|
|
39
|
+
payload = {"ok": False, "error": {"code": code, "message": message}}
|
|
40
|
+
if hint:
|
|
41
|
+
payload["error"]["hint"] = hint
|
|
42
|
+
print(json.dumps(payload), file=sys.stderr)
|
|
43
|
+
else:
|
|
44
|
+
print(f"Error: {message}", file=sys.stderr)
|
|
45
|
+
if hint:
|
|
46
|
+
print(f" {hint}", file=sys.stderr)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def emit_progress(message: str) -> None:
|
|
50
|
+
"""Progress -> stderr in both modes."""
|
|
51
|
+
print(message, file=sys.stderr)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Self-contained HTML artifact generator for the diff preview.
|
|
2
|
+
|
|
3
|
+
The CLI reads the cache directly, so it can inline *full* abstracts for every
|
|
4
|
+
record with no token budget — unlike a chat agent that has to transcribe the
|
|
5
|
+
widget code through its own context. `render_diff_widget` returns a widget
|
|
6
|
+
fragment (no <html>/<head>/<body>) suitable for inline artifact rendering in
|
|
7
|
+
the Claude Code desktop app: CSS variables for theming, a shared <dialog>
|
|
8
|
+
abstract modal, pagination, checkbox selection, and `sendPrompt` action
|
|
9
|
+
buttons that the dbtips-curate skill's batch-execution rule picks up.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _rows_from_queue(entries: list[dict], identity_key: str) -> list[dict]:
|
|
19
|
+
"""Flatten a diff queue into rows the widget understands."""
|
|
20
|
+
rows = []
|
|
21
|
+
for e in entries:
|
|
22
|
+
preview = e.get("_preview", {}) or {}
|
|
23
|
+
identity = {k: v for k, v in e.items() if k != "_preview"}
|
|
24
|
+
pmid = identity.get(identity_key)
|
|
25
|
+
if pmid is None and identity:
|
|
26
|
+
pmid = next(iter(identity.values()))
|
|
27
|
+
# Display title falls back across sections: literature Title, reagent
|
|
28
|
+
# product_name, pathway figtitle, assay Description. The modal body uses
|
|
29
|
+
# the richest text available (Abstract / description).
|
|
30
|
+
title = (preview.get("Title") or preview.get("product_name")
|
|
31
|
+
or preview.get("figtitle") or preview.get("Description") or "")
|
|
32
|
+
provider = preview.get("Provider")
|
|
33
|
+
if provider and not preview.get("Title"):
|
|
34
|
+
title = f"{title} — {provider}"
|
|
35
|
+
rows.append({
|
|
36
|
+
"pmid": str(pmid) if pmid is not None else "",
|
|
37
|
+
"year": preview.get("Year"),
|
|
38
|
+
"cite": preview.get("citedby"),
|
|
39
|
+
"title": title,
|
|
40
|
+
"abstract": preview.get("Abstract") or preview.get("description") or "",
|
|
41
|
+
})
|
|
42
|
+
return rows
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def render_diff_widget(payload: dict[str, Any], *, identity_key: str = "PMID") -> str:
|
|
46
|
+
"""Build the inline diff artifact for `entity diff --emit-widget`."""
|
|
47
|
+
entity_id = payload["entity"]
|
|
48
|
+
queue = payload.get("queue", {})
|
|
49
|
+
totals = payload.get("totals", {})
|
|
50
|
+
|
|
51
|
+
data = {
|
|
52
|
+
"entity": entity_id,
|
|
53
|
+
"base_raw": payload.get("base_raw"),
|
|
54
|
+
"section": payload.get("section"),
|
|
55
|
+
"totals": totals,
|
|
56
|
+
"new": _rows_from_queue(queue.get("new", []), identity_key),
|
|
57
|
+
"removed": _rows_from_queue(queue.get("removed_by_pipeline", []), identity_key),
|
|
58
|
+
"resurrected": _rows_from_queue(queue.get("resurrected", []), identity_key),
|
|
59
|
+
"conflicts": queue.get("conflicts", []),
|
|
60
|
+
}
|
|
61
|
+
data_json = json.dumps(data, ensure_ascii=False, default=str)
|
|
62
|
+
# Close any stray </script> inside abstracts so the embed can't break out.
|
|
63
|
+
data_json = data_json.replace("</", "<\\/")
|
|
64
|
+
return _TEMPLATE.replace("/*__DATA__*/", data_json)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_TEMPLATE = r"""<div style="padding:0.5rem 0;">
|
|
68
|
+
<h2 class="sr-only">Diff preview artifact: new, removed-by-pipeline, resurrected, and conflict records with full-abstract modal, pagination, and batch actions.</h2>
|
|
69
|
+
<div id="summary" style="font-size:13px; color:var(--color-text-secondary); margin-bottom:14px;"></div>
|
|
70
|
+
|
|
71
|
+
<div style="display:flex; gap:6px; margin-bottom:12px; flex-wrap:wrap;">
|
|
72
|
+
<button class="tab" data-tab="new">New from pipeline</button>
|
|
73
|
+
<button class="tab" data-tab="removed">Removed by pipeline</button>
|
|
74
|
+
<button class="tab" data-tab="resurrected">Resurrected</button>
|
|
75
|
+
<button class="tab" data-tab="conflicts">Conflicts</button>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div id="toolbar" style="display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:10px;">
|
|
79
|
+
<span id="secnote" style="font-size:12px; color:var(--color-text-tertiary); flex:1;"></span>
|
|
80
|
+
<button id="checkall" style="font-size:13px;">Select all on page</button>
|
|
81
|
+
<button id="clearall" style="font-size:13px;">Clear</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<table style="width:100%; border-collapse:collapse; font-size:13px; table-layout:fixed;">
|
|
85
|
+
<colgroup><col style="width:30px"><col style="width:26px"><col style="width:78px"><col style="width:52px"><col style="width:42px"><col><col style="width:54px"></colgroup>
|
|
86
|
+
<thead><tr id="thead" style="text-align:left; color:var(--color-text-secondary); border-bottom:0.5px solid var(--color-border-secondary);"></tr></thead>
|
|
87
|
+
<tbody id="rows"></tbody>
|
|
88
|
+
</table>
|
|
89
|
+
|
|
90
|
+
<div style="display:flex; gap:8px; align-items:center; justify-content:center; margin-top:14px;">
|
|
91
|
+
<button id="prev" style="font-size:13px;"><i class="ti ti-chevron-left" aria-hidden="true"></i> Prev</button>
|
|
92
|
+
<span id="pageinfo" style="font-size:13px; color:var(--color-text-secondary); min-width:130px; text-align:center;"></span>
|
|
93
|
+
<button id="next" style="font-size:13px;">Next <i class="ti ti-chevron-right" aria-hidden="true"></i></button>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div style="border-top:0.5px solid var(--color-border-tertiary); margin-top:16px; padding-top:14px; display:flex; align-items:center; gap:12px;">
|
|
97
|
+
<span id="selsum" style="font-size:13px; color:var(--color-text-secondary); flex:1;"></span>
|
|
98
|
+
<button id="apply" style="font-size:14px; padding:7px 18px; color:var(--color-text-danger); border-color:var(--color-border-danger);"><i class="ti ti-trash" aria-hidden="true"></i> Apply <span id="applyverb">deletions</span> ↗</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<dialog id="amodal" style="max-width:620px; padding:18px; border:none; border-radius:var(--border-radius-lg); background:var(--color-background-primary); color:var(--color-text-primary);">
|
|
102
|
+
<header style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
|
103
|
+
<strong id="m-pmid" style="font-family:var(--font-mono); font-size:13px;"></strong>
|
|
104
|
+
<span style="flex:1"></span>
|
|
105
|
+
<button onclick="document.getElementById('amodal').close()" style="font-size:13px;">Close</button>
|
|
106
|
+
</header>
|
|
107
|
+
<p id="m-title" style="font-size:14px; font-weight:500; margin:0 0 10px; line-height:1.4;"></p>
|
|
108
|
+
<p id="m-abs" style="white-space:pre-wrap; line-height:1.55; font-size:13px; color:var(--color-text-secondary); max-height:55vh; overflow:auto; margin:0;"></p>
|
|
109
|
+
</dialog>
|
|
110
|
+
</div>
|
|
111
|
+
<style>
|
|
112
|
+
.tab{font-size:13px;}
|
|
113
|
+
.tab[aria-selected="true"]{background:var(--color-background-secondary); border-color:var(--color-border-primary);}
|
|
114
|
+
</style>
|
|
115
|
+
<script>
|
|
116
|
+
const DB=/*__DATA__*/;
|
|
117
|
+
const PS=20;
|
|
118
|
+
const VERB={new:"deletions", removed:"drops"};
|
|
119
|
+
const ACTION={
|
|
120
|
+
new:(ids)=>`Delete these ${ids.length} new-from-pipeline records for ${DB.entity} (run op delete on each, no further confirmation needed): ${ids.join(", ")}`,
|
|
121
|
+
removed:(ids)=>`Delete these ${ids.length} removed-by-pipeline records for ${DB.entity} (run op delete on each, no further confirmation needed): ${ids.join(", ")}`
|
|
122
|
+
};
|
|
123
|
+
let tab="new", page=0;
|
|
124
|
+
const sel={new:new Set(), removed:new Set()};
|
|
125
|
+
function esc(s){return String(s==null?"":s).replace(/&/g,"&").replace(/</g,"<");}
|
|
126
|
+
function fmt(v){return v==null?"":v;}
|
|
127
|
+
function curRows(){
|
|
128
|
+
if(tab==="new")return DB.new;
|
|
129
|
+
if(tab==="removed")return DB.removed;
|
|
130
|
+
if(tab==="resurrected")return DB.resurrected;
|
|
131
|
+
return DB.conflicts;
|
|
132
|
+
}
|
|
133
|
+
function summary(){
|
|
134
|
+
const t=DB.totals||{};
|
|
135
|
+
document.getElementById("summary").innerHTML=
|
|
136
|
+
`<strong>${esc(DB.entity)}</strong> · base=${esc(DB.base_raw||"—")} vs current served · `+
|
|
137
|
+
`merged ${fmt(t.merged)} · new ${fmt(t.new_from_pipeline)} · removed ${fmt(t.removed_by_pipeline)} · `+
|
|
138
|
+
`resurrected ${fmt(t.resurrected_tombstones)} · conflicts ${fmt(t.conflicts)}`;
|
|
139
|
+
const selectable=(tab==="new"||tab==="removed");
|
|
140
|
+
document.getElementById("apply").style.display=selectable?"":"none";
|
|
141
|
+
document.getElementById("checkall").style.display=selectable?"":"none";
|
|
142
|
+
document.getElementById("clearall").style.display=selectable?"":"none";
|
|
143
|
+
document.getElementById("applyverb").textContent=VERB[tab]||"";
|
|
144
|
+
const n=selectable?sel[tab].size:0;
|
|
145
|
+
document.getElementById("selsum").textContent=n?`${n} selected`:"No records selected";
|
|
146
|
+
document.getElementById("apply").disabled=n===0;
|
|
147
|
+
const notes={new:"Records the refresh newly emitted — triage and delete noise.",
|
|
148
|
+
removed:"In the previous file but dropped by the refresh — retained on ingest unless you drop them.",
|
|
149
|
+
resurrected:"You deleted these before; the pipeline re-emitted them. They stay deleted on ingest.",
|
|
150
|
+
conflicts:"Field-level disagreements between your patch and the pipeline."};
|
|
151
|
+
document.getElementById("secnote").textContent=notes[tab]||"";
|
|
152
|
+
}
|
|
153
|
+
function openModal(r){
|
|
154
|
+
document.getElementById("m-pmid").textContent="PMID "+r.pmid+(r.year?" · "+r.year:"")+(r.cite!=null?" · cited "+r.cite:"");
|
|
155
|
+
document.getElementById("m-title").textContent=r.title||"";
|
|
156
|
+
document.getElementById("m-abs").textContent=r.abstract||"(no abstract)";
|
|
157
|
+
document.getElementById("amodal").showModal();
|
|
158
|
+
}
|
|
159
|
+
function render(){
|
|
160
|
+
document.querySelectorAll(".tab").forEach(b=>b.setAttribute("aria-selected", b.dataset.tab===tab?"true":"false"));
|
|
161
|
+
const rows=curRows();
|
|
162
|
+
const TP=Math.max(1, Math.ceil(rows.length/PS));
|
|
163
|
+
if(page>=TP)page=0;
|
|
164
|
+
const thead=document.getElementById("thead");
|
|
165
|
+
const tb=document.getElementById("rows"); tb.innerHTML="";
|
|
166
|
+
if(tab==="conflicts"){
|
|
167
|
+
thead.innerHTML=`<th style="padding:8px 4px;">key</th><th>field</th><th>ours</th><th>theirs</th><th>base</th>`;
|
|
168
|
+
rows.slice(page*PS,page*PS+PS).forEach(c=>{
|
|
169
|
+
const tr=document.createElement("tr"); tr.style.borderTop="0.5px solid var(--color-border-tertiary)";
|
|
170
|
+
tr.innerHTML=`<td style="padding:8px 4px; font-family:var(--font-mono); font-size:12px;">${esc(JSON.stringify(c.key))}</td>`+
|
|
171
|
+
`<td>${esc(c.field)}</td><td>${esc(c.ours)}</td><td>${esc(c.theirs)}</td><td>${esc(c.base)}</td>`;
|
|
172
|
+
tb.appendChild(tr);
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
const selectable=(tab==="new"||tab==="removed");
|
|
176
|
+
thead.innerHTML=`<th style="padding:8px 4px;">${selectable?'<i class="ti ti-trash" aria-hidden="true"></i>':''}</th><th>#</th><th>PMID</th><th>Year</th><th>Cite</th><th>Title</th><th>Abs</th>`;
|
|
177
|
+
const start=page*PS, end=Math.min(start+PS, rows.length);
|
|
178
|
+
for(let i=start;i<end;i++){
|
|
179
|
+
const r=rows[i];
|
|
180
|
+
const tr=document.createElement("tr"); tr.style.borderTop="0.5px solid var(--color-border-tertiary)";
|
|
181
|
+
const box=selectable?`<input type="checkbox" data-pmid="${esc(r.pmid)}" ${sel[tab].has(r.pmid)?"checked":""} style="width:16px;height:16px;cursor:pointer;">`:"";
|
|
182
|
+
tr.innerHTML=`<td style="padding:9px 4px; vertical-align:top;">${box}</td>`+
|
|
183
|
+
`<td style="vertical-align:top; color:var(--color-text-tertiary);">${i+1}</td>`+
|
|
184
|
+
`<td style="vertical-align:top; font-family:var(--font-mono); font-size:12px;">${esc(r.pmid)}</td>`+
|
|
185
|
+
`<td style="vertical-align:top;">${esc(fmt(r.year))}</td>`+
|
|
186
|
+
`<td style="vertical-align:top;">${esc(fmt(r.cite))}</td>`+
|
|
187
|
+
`<td style="vertical-align:top; white-space:normal; word-break:break-word; line-height:1.45;">${esc(r.title)}</td>`+
|
|
188
|
+
`<td style="vertical-align:top;"><button data-i="${i}" class="absbtn" style="font-size:11px; padding:3px 8px;"><i class="ti ti-file-text" aria-hidden="true"></i></button></td>`;
|
|
189
|
+
tb.appendChild(tr);
|
|
190
|
+
}
|
|
191
|
+
tb.querySelectorAll("input[type=checkbox]").forEach(cb=>cb.onchange=()=>{cb.checked?sel[tab].add(cb.dataset.pmid):sel[tab].delete(cb.dataset.pmid); summary();});
|
|
192
|
+
tb.querySelectorAll("button.absbtn").forEach(b=>b.onclick=()=>openModal(rows[+b.dataset.i]));
|
|
193
|
+
}
|
|
194
|
+
document.getElementById("pageinfo").textContent=`Page ${page+1}/${TP} · ${rows.length} rows`;
|
|
195
|
+
document.getElementById("prev").disabled=page===0;
|
|
196
|
+
document.getElementById("next").disabled=page>=TP-1;
|
|
197
|
+
summary();
|
|
198
|
+
}
|
|
199
|
+
document.querySelectorAll(".tab").forEach(b=>b.onclick=()=>{tab=b.dataset.tab; page=0; render();});
|
|
200
|
+
document.getElementById("prev").onclick=()=>{if(page>0){page--;render();}};
|
|
201
|
+
document.getElementById("next").onclick=()=>{page++;render();};
|
|
202
|
+
document.getElementById("checkall").onclick=()=>{const rows=curRows(); rows.slice(page*PS,page*PS+PS).forEach(r=>sel[tab].add(r.pmid)); render();};
|
|
203
|
+
document.getElementById("clearall").onclick=()=>{sel[tab].clear(); render();};
|
|
204
|
+
document.getElementById("apply").onclick=()=>{const ids=[...sel[tab]]; if(!ids.length)return; sendPrompt(ACTION[tab](ids));};
|
|
205
|
+
document.getElementById("amodal").addEventListener("click",e=>{if(e.target.id==="amodal")e.target.close();});
|
|
206
|
+
render();
|
|
207
|
+
</script>"""
|