| import gradio as gr |
| from fastapi import FastAPI |
| from starlette.staticfiles import StaticFiles |
| import uvicorn |
| import logging |
| from pydantic import BaseModel |
|
|
| logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') |
| logger = logging.getLogger(__name__) |
|
|
| class SmilesData(BaseModel): |
| smiles: str |
|
|
| app = FastAPI() |
|
|
| |
| app.mount("/ketcher", StaticFiles(directory="ketcher"), name="ketcher") |
|
|
| |
| @app.post("/update_smiles") |
| async def update_smiles(data: SmilesData): |
| logger.info(f"Received SMILES from front-end: {data.smiles}") |
| print(f"[PRINT] Received SMILES from front-end: {data.smiles}") |
| return {"status": "ok", "received_smiles": data.smiles} |
|
|
| |
| |
| |
| KETCHER_HTML = r''' |
| <iframe id="ifKetcher" src="/ketcher/index.html" width="100%" height="600px" style="border: 1px solid #ccc;"></iframe> |
| |
| <script> |
| console.log("[Front-end] Ketcher-Gradio integration script loaded."); |
| |
| let ketcher = null; |
| let lastSmiles = ''; |
| |
| function findSmilesInput() { |
| const inputContainer = document.getElementById('smiles_input'); |
| if (!inputContainer) { |
| console.warn("[Front-end] smiles_input element not found."); |
| return null; |
| } |
| const input = inputContainer.querySelector('input[type="text"]'); |
| return input; |
| } |
| |
| function updateGradioInput(smiles) { |
| const input = findSmilesInput(); |
| if (input && input.value !== smiles) { |
| input.value = smiles; |
| // 手动触发input事件让Gradio更新状态 |
| input.dispatchEvent(new Event('input', { bubbles: true })); |
| console.log("[Front-end] Updated Gradio input with SMILES:", smiles); |
| } |
| } |
| |
| async function handleKetcherChange() { |
| console.log("[Front-end] handleKetcherChange called, retrieving SMILES..."); |
| try { |
| const smiles = await ketcher.getSmiles({ arom: false }); |
| console.log("[Front-end] SMILES retrieved from Ketcher:", smiles); |
| if (smiles !== lastSmiles) { |
| lastSmiles = smiles; |
| updateGradioInput(smiles); |
| |
| // 将SMILES发送给后端 |
| console.log("[Front-end] Sending SMILES to backend..."); |
| fetch('/update_smiles', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({smiles: smiles}) |
| }) |
| .then(res => res.json()) |
| .then(data => { |
| console.log("[Front-end] Backend response:", data); |
| }) |
| .catch(err => console.error("[Front-end] Error sending SMILES to backend:", err)); |
| } |
| } catch (err) { |
| console.error("[Front-end] Error getting SMILES from Ketcher:", err); |
| } |
| } |
| |
| function initKetcher() { |
| console.log("[Front-end] initKetcher started."); |
| const iframe = document.getElementById('ifKetcher'); |
| if (!iframe) { |
| console.error("[Front-end] iframe not found."); |
| setTimeout(initKetcher, 500); |
| return; |
| } |
| |
| const ketcherWindow = iframe.contentWindow; |
| if (!ketcherWindow || !ketcherWindow.ketcher) { |
| console.log("[Front-end] ketcher not yet available in iframe, retrying..."); |
| setTimeout(initKetcher, 500); |
| return; |
| } |
| |
| ketcher = ketcherWindow.ketcher; |
| console.log("[Front-end] Ketcher instance acquired:", ketcher); |
| |
| // 设置初始分子 |
| ketcher.setMolecule('C').then(() => { |
| console.log("[Front-end] Initial molecule set to 'C'."); |
| }); |
| |
| const editor = ketcher.editor; |
| console.log("[Front-end] Editor object:", editor); |
| |
| // 尝试绑定变化事件,根据Ketcher版本,可尝试多种方式 |
| let eventBound = false; |
| if (editor && typeof editor.subscribe === 'function') { |
| console.log("[Front-end] Using editor.subscribe('change', ...)"); |
| editor.subscribe('change', handleKetcherChange); |
| eventBound = true; |
| } |
| // 如果其他版本需要 editor.events.change.subscribe(handleKetcherChange); |
| // 或 editor.structChanged.add(handleKetcherChange); 可在此添加尝试。 |
| |
| if (!eventBound) { |
| console.error("[Front-end] No suitable event binding found. Check Ketcher version and event API."); |
| } |
| } |
| |
| document.getElementById('ifKetcher').addEventListener('load', () => { |
| console.log("[Front-end] iframe loaded. Initializing Ketcher in 1s..."); |
| setTimeout(initKetcher, 1000); |
| }); |
| </script> |
| ''' |
|
|
| em_js = ''' |
| setTimeout(() => { |
| console.log('inited') |
| k = document.getElementById('ifKetcher'); |
| btn = document.getElementById('btn'); |
| btn.addEventListener('click', () => { |
| k.contentWindow.ketcher.getSmiles().then((v) => { |
| document.querySelector('#output_smi textarea').value = v; |
| }) |
| }); |
| }, 2e3) |
| ''' |
|
|
| def create_interface(): |
| with gr.Blocks(js=em_js) as demo: |
| gr.Markdown("# Ketcher 与 Gradio 整合示例") |
| with gr.Row(): |
| with gr.Column(scale=2): |
| |
| gr.HTML(KETCHER_HTML) |
|
|
| with gr.Column(scale=1): |
| gr.Markdown("### SMILES 输入框 (自动更新)") |
| smiles_input = gr.Textbox( |
| label="SMILES", |
| value="C", |
| placeholder="SMILES structure", |
| elem_id="smiles_input" |
| ) |
|
|
| get_smiles_btn = gr.Button("手动获取当前SMILES", elem_id='btn') |
| smiles_output = gr.Textbox(label="当前 SMILES(按钮点击获取)", elem_id='output_smi') |
|
|
| def get_smiles_from_input(): |
| logger.info("Button clicked: Retrieved SMILES value from input.") |
| print("Button clicked: Current SMILES from input:", smiles_input.value) |
| return smiles_input.value |
|
|
| get_smiles_btn.click(fn=None, inputs=None, outputs=smiles_output, js="async () => { k = document.getElementById('ifKetcher'); a = await k.contentWindow.ketcher.getSmiles(); return a; }") |
|
|
| return demo |
|
|
| demo = create_interface() |
| app = gr.mount_gradio_app(app, demo, path="/") |
|
|
| if __name__ == "__main__": |
| |
| uvicorn.run(app, host="127.0.0.1", port=7860) |
|
|