Skip to content

Commit 1a26fac

Browse files
committed
updated misc tools
1 parent 0d77c15 commit 1a26fac

File tree

2 files changed

+260
-14
lines changed

2 files changed

+260
-14
lines changed

app/misc_tools/page.tsx

Lines changed: 250 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,254 @@
1-
"use client"
1+
"use client";
22

3+
4+
import {
5+
Container,
6+
Flex,
7+
Anchor,
8+
ActionIcon,
9+
useMantineColorScheme,
10+
} from "@mantine/core";
11+
import { IconMoon, IconSun } from "@tabler/icons-react";
12+
import Link from "next/link";
13+
14+
15+
import React, { useState } from "react";
16+
import { Textarea, Group, Chip, Stack, Text, Loader, Paper, Button } from "@mantine/core";
317
import JSME from "../../components/tools/toolViz/JSMEComp";
418

5-
export default function JSMEPOPO() {
6-
function handleChange(smiles){
7-
console.log(smiles)
19+
type TargetSummary = {
20+
id: string; // original input (ChEMBL or UniProt)
21+
resolvedId: string; // ChEMBL target id used for activity query
22+
totalCount: number | null;
23+
error?: string;
24+
};
25+
26+
async function resolveToChemblTargetId(rawId: string): Promise<{ resolvedId?: string; error?: string }> {
27+
const id = rawId.trim();
28+
29+
// Heuristic: if it already looks like a ChEMBL target, use it directly.
30+
// (e.g. CHEMBL226, CHEMBL203, etc.) [web:9]
31+
if (/^CHEMBL\d+$/i.test(id)) {
32+
return { resolvedId: id.toUpperCase() };
33+
}
34+
35+
// Otherwise treat as UniProt accession and map via target endpoint. [web:9][web:11]
36+
const url = `https://www.ebi.ac.uk/chembl/api/data/target.json?target_components__accession=${encodeURIComponent(
37+
id
38+
)}`;
39+
40+
const res = await fetch(url);
41+
if (!res.ok) {
42+
return { error: `Target lookup HTTP ${res.status}` };
43+
}
44+
45+
const json = await res.json();
46+
const first = json?.targets?.[0];
47+
const chemblId = first?.target_chembl_id;
48+
49+
if (!chemblId) {
50+
return { error: "No ChEMBL target found for this UniProt ID" };
51+
}
52+
53+
return { resolvedId: chemblId };
54+
}
55+
56+
async function fetchTotalCountForTarget(rawId: string): Promise<TargetSummary> {
57+
const { resolvedId, error: resolveError } = await resolveToChemblTargetId(rawId);
58+
59+
if (!resolvedId) {
60+
return { id: rawId, resolvedId: rawId, totalCount: null, error: resolveError ?? "Unable to resolve ID" };
61+
}
62+
63+
const url = `https://www.ebi.ac.uk/chembl/api/data/activity.json?target_chembl_id=${encodeURIComponent(
64+
resolvedId
65+
)}`;
66+
67+
const res = await fetch(url);
68+
if (!res.ok) {
69+
return {
70+
id: rawId,
71+
resolvedId,
72+
totalCount: null,
73+
error: `Activity HTTP ${res.status}`,
74+
};
75+
}
76+
77+
const json = await res.json();
78+
// page_meta.total_count holds the total number of activity records. [file:1][web:9]
79+
const totalCount = json?.page_meta?.total_count ?? null;
80+
81+
return { id: rawId, resolvedId, totalCount };
82+
}
83+
84+
function ChemblActivityCounter() {
85+
const [input, setInput] = useState("");
86+
const [targets, setTargets] = useState<string[]>([]);
87+
const [results, setResults] = useState<TargetSummary[]>([]);
88+
const [loading, setLoading] = useState(false);
89+
90+
const parseTargets = (raw: string): string[] => {
91+
const parts = raw
92+
.split(/[\s,;]+/)
93+
.map((t) => t.trim())
94+
.filter(Boolean);
95+
return Array.from(new Set(parts));
96+
};
97+
98+
const handleInputChange = (value: string) => {
99+
setInput(value);
100+
const parsed = parseTargets(value);
101+
setTargets(parsed); // tags update automatically
102+
setResults([]);
103+
};
104+
105+
const handleRemoveTag = (id: string) => {
106+
const remaining = targets.filter((t) => t !== id);
107+
setTargets(remaining);
108+
setInput(remaining.join(", "));
109+
setResults((prev) => prev.filter((r) => r.id !== id));
110+
};
111+
112+
const handleRunQuery = async () => {
113+
if (!targets.length) return;
114+
setLoading(true);
115+
try {
116+
const summaries = await Promise.all(targets.map(fetchTotalCountForTarget));
117+
setResults(summaries);
118+
} finally {
119+
setLoading(false);
8120
}
9-
return (
10-
<div className="container">
11-
<div className="content-wrapper" style={{ marginTop: "60px" }}>
12-
<JSME height="500px" width="500px" onChange={handleChange} />
13-
</div>
14-
</div>
15-
)
16-
}
121+
};
122+
123+
return (
124+
<Paper withBorder shadow="sm" p="md" mt="md">
125+
<Stack gap="md">
126+
<Textarea
127+
label="Targets (ChEMBL or UniProt)"
128+
description="Paste ChEMBL target IDs (e.g. CHEMBL226) or UniProt accessions (e.g. P05067)."
129+
placeholder={`CHEMBL226
130+
P05067
131+
CHEMBL203`}
132+
minRows={4}
133+
value={input}
134+
onChange={(event) => handleInputChange(event.currentTarget.value)}
135+
/>
136+
137+
<Group justify="space-between">
138+
<Text size="sm" c="dimmed">
139+
IDs are automatically converted into tags below.
140+
</Text>
141+
<Button onClick={handleRunQuery} disabled={!targets.length || loading}>
142+
{loading ? <Loader size="xs" /> : "Run query"}
143+
</Button>
144+
</Group>
145+
146+
{targets.length > 0 && (
147+
<Stack gap="xs">
148+
<Text size="sm">Input IDs:</Text>
149+
<Group gap="xs">
150+
{targets.map((id) => (
151+
<Chip
152+
key={id}
153+
checked
154+
onChange={() => handleRemoveTag(id)}
155+
color="blue"
156+
radius="sm"
157+
>
158+
{id}
159+
</Chip>
160+
))}
161+
</Group>
162+
<Text size="xs" c="dimmed">
163+
Click a tag to remove it.
164+
</Text>
165+
</Stack>
166+
)}
167+
168+
{results.length > 0 && (
169+
<Stack gap="xs">
170+
<Text fw={500}>Activity counts per target</Text>
171+
{results.map((r) => (
172+
<Text key={r.id} size="sm">
173+
{r.id}
174+
{r.resolvedId && r.resolvedId !== r.id ? ` → ${r.resolvedId}` : ""}:{" "}
175+
{r.error
176+
? `Error: ${r.error}`
177+
: r.totalCount !== null
178+
? `${r.totalCount} Compounds With Activity`
179+
: "No total_count found"}
180+
</Text>
181+
))}
182+
</Stack>
183+
)}
184+
</Stack>
185+
</Paper>
186+
);
187+
}
188+
189+
export default function JSMEPOPO() {
190+
const { colorScheme, setColorScheme } = useMantineColorScheme();
191+
192+
function handleChange(smiles: string) {
193+
console.log(smiles);
194+
}
195+
196+
return (
197+
<>
198+
{/* Navbar */}
199+
<Container size="lg" py="md">
200+
<Flex align="center" justify="space-between">
201+
{/* Left: Title */}
202+
<Text size="lg" fw={600}>
203+
<Link href="/" style={{ textDecoration: "none", color: "inherit" }}>
204+
QSAR IN THE BROWSER
205+
</Link>
206+
</Text>
207+
208+
{/* Right: About link and theme button */}
209+
<Group gap="sm">
210+
<Anchor
211+
component={Link}
212+
href="/about"
213+
size="sm"
214+
underline="hover"
215+
c="dimmed"
216+
>
217+
About
218+
</Anchor>
219+
220+
<Anchor
221+
component={Link}
222+
href="/about"
223+
size="sm"
224+
underline="hover"
225+
c="dimmed"
226+
>
227+
GitHub
228+
</Anchor>
229+
230+
<ActionIcon
231+
onClick={() =>
232+
setColorScheme(colorScheme === "light" ? "dark" : "light")
233+
}
234+
variant="default"
235+
size="lg"
236+
radius="md"
237+
aria-label="Toggle color scheme"
238+
>
239+
{colorScheme === "dark" ? (
240+
<IconSun stroke={1.5} />
241+
) : (
242+
<IconMoon stroke={1.5} />
243+
)}
244+
</ActionIcon>
245+
</Group>
246+
</Flex>
247+
</Container>
248+
<Container size="lg" py="xl">
249+
<JSME height="500px" width="500px" onChange={handleChange} />
250+
<ChemblActivityCounter />
251+
</Container>
252+
</>
253+
);
254+
}

app/page.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,22 @@ export default function IndexPage() {
4848

4949
<Anchor
5050
component={Link}
51-
href="/about"
51+
href="https://github.com/syedzayyan/qsar-in-browser"
5252
size="sm"
5353
underline="hover"
5454
c="dimmed"
5555
>
5656
GitHub
5757
</Anchor>
58-
58+
<Anchor
59+
component={Link}
60+
href="/misc_tools"
61+
size="sm"
62+
underline="hover"
63+
c="dimmed"
64+
>
65+
Misc Tools
66+
</Anchor>
5967
<ActionIcon
6068
onClick={() =>
6169
setColorScheme(colorScheme === "light" ? "dark" : "light")

0 commit comments

Comments
 (0)