ffmol-rdkit / frontend /src /components /ProjectForm.tsx
hercules168's picture
Upload 44 files
40ad0f5 verified
Raw
History Blame Contribute Delete
5.6 kB
import { Play, Search, Sparkles } from "lucide-react";
import { FormEvent, useState } from "react";
import { lookupDrug } from "../api";
type FormValues = {
name: string;
drug_name: string;
structure_input: string;
structure_format: "smiles" | "inchi";
target_name: string;
indication: string;
mechanism: string;
patent_text: string;
};
const DEFAULT_VALUES: FormValues = {
name: "EGFR follow-up",
drug_name: "Gefitinib",
structure_input: "CCOc1ccc2nc(S(N)(=O)=O)sc2c1",
structure_format: "smiles",
target_name: "EGFR",
indication: "oncology",
mechanism: "kinase inhibition",
patent_text: "Avoid sulfonamide substitutions and keep Tanimoto similarity below 0.95.",
};
export function ProjectForm({
onRun,
disabled,
}: {
onRun: (payload: Record<string, string>) => void;
disabled: boolean;
}) {
const [values, setValues] = useState<FormValues>(DEFAULT_VALUES);
const [referenceStructureSvg, setReferenceStructureSvg] = useState("");
const [lookupStatus, setLookupStatus] = useState<"idle" | "loading" | "loaded" | "failed">("idle");
const [lookupError, setLookupError] = useState<string | null>(null);
function updateValue(name: keyof FormValues, value: string) {
setValues((current) => ({ ...current, [name]: value }));
}
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
onRun({
...values,
});
}
async function handleLookup() {
if (!values.drug_name.trim()) return;
setLookupStatus("loading");
setLookupError(null);
try {
const result = await lookupDrug(values.drug_name);
setValues((current) => ({
...current,
name: current.name.trim() ? current.name : `${result.drug_name} follow-up`,
drug_name: result.drug_name,
structure_input: result.structure_input,
structure_format: result.structure_format,
target_name: result.target_name,
indication: result.indication,
mechanism: result.mechanism,
}));
setReferenceStructureSvg(result.structure_svg);
setLookupStatus("loaded");
} catch (err) {
setLookupStatus("failed");
setLookupError(err instanceof Error ? err.message : "Drug lookup failed");
}
}
return (
<form className="panel" onSubmit={submit}>
<div className="panel-heading">
<span className="eyebrow">
<Sparkles size={14} /> Molecular design input
</span>
<h2>Project Input</h2>
</div>
<label>
Project name
<input name="name" value={values.name} onChange={(event) => updateValue("name", event.target.value)} />
</label>
<label>
Drug name
<span className="lookup-row">
<input
name="drug_name"
value={values.drug_name}
onChange={(event) => updateValue("drug_name", event.target.value)}
/>
<button type="button" disabled={disabled || lookupStatus === "loading"} onClick={handleLookup}>
<Search size={16} /> Lookup drug
</button>
</span>
</label>
{lookupStatus === "loaded" && <p className="lookup-note">Drug context loaded into target fields.</p>}
{lookupError && <p className="error">{lookupError}</p>}
{referenceStructureSvg && (
<section className="reference-preview" aria-label={`Reference structure preview for ${values.drug_name}`}>
<div className="reference-preview-heading">
<span>Reference structure</span>
<strong>{values.drug_name}</strong>
</div>
<div
className="structure-depiction reference-structure"
dangerouslySetInnerHTML={{ __html: referenceStructureSvg }}
/>
</section>
)}
<label>
Reference structure
<textarea
name="structure_input"
value={values.structure_input}
onChange={(event) => updateValue("structure_input", event.target.value)}
/>
</label>
<label>
Format
<select
name="structure_format"
value={values.structure_format}
onChange={(event) => updateValue("structure_format", event.target.value)}
>
<option value="smiles">SMILES</option>
<option value="inchi">InChI</option>
</select>
</label>
<label>
Target
<input
name="target_name"
value={values.target_name}
onChange={(event) => updateValue("target_name", event.target.value)}
/>
</label>
<label>
Indication
<input
name="indication"
value={values.indication}
onChange={(event) => updateValue("indication", event.target.value)}
/>
</label>
<label>
Mechanism
<input
name="mechanism"
value={values.mechanism}
onChange={(event) => updateValue("mechanism", event.target.value)}
/>
</label>
<label>
Patent notes
<textarea
name="patent_text"
value={values.patent_text}
onChange={(event) => updateValue("patent_text", event.target.value)}
/>
</label>
<p className="field-note">
Patent notes are optional. Enter claim text or design constraints such as forbidden groups,
required scaffold changes, or similarity limits; leave blank if you only want structure-based generation.
</p>
<button disabled={disabled}>
<Play size={16} /> Run pipeline
</button>
</form>
);
}