Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |