LLM-front / clm-frontend.py
Songyou's picture
Upload 8 files
99415de verified
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()
# 将 ketcher 目录挂载为静态资源,确保 ketcher/index.html 存在
app.mount("/ketcher", StaticFiles(directory="ketcher"), name="ketcher")
# 接收前端发送的SMILES数据的接口
@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}
# 前端嵌入的HTML和脚本
# 这个HTML中会有一个iframe加载/ketcher/index.html
# 并在前端监听Ketcher事件,将SMILES更新到Gradio输入框并发送给后端
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):
# 嵌入前面定义的HTML
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__":
# 启动命令:python app.py
uvicorn.run(app, host="127.0.0.1", port=7860)