Preformu / app.deprecated.py
unknown
Fix: Upload font using LFS
0c157e9
"""
Drug Stability Prompt Orchestrator - Professional Bilingual UI
A beautiful, professional UI for drug-excipient compatibility analysis.
Features:
- Bilingual support (Chinese/English)
- Modern glassmorphism design
- Gradient backgrounds
- Smooth animations
- Professional pharmaceutical industry aesthetics
"""
import os
import sys
import tempfile
from pathlib import Path
from datetime import datetime
from typing import Optional, Tuple, List, Dict
import gradio as gr
# Add project root to path for imports
PROJECT_ROOT = Path(__file__).parent
sys.path.insert(0, str(PROJECT_ROOT))
from schemas.canonical_schema import AnalysisRequest
from layers.input_normalizer import InputNormalizer
from layers.prompt_orchestrator import PromptOrchestrator
from layers.model_invoker import ModelInvoker
from layers.output_normalizer import OutputNormalizer
from layers.llm_providers import get_available_providers, LLMProvider
from report.generator import ReportGenerator
from utils.molecule_renderer import MoleculeRenderer
from utils.stability_report_formatter import StabilityReportFormatter
# =============================================================================
# Bilingual Text System
# =============================================================================
TEXTS = {
"app_title": {
"cn": "🧪 药物-辅料相容性分析系统",
"en": "🧪 API-Excipient Compatibility Analyzer"
},
"app_subtitle": {
"cn": "专业制剂研发智能预测平台",
"en": "Intelligent Formulation R&D Prediction Platform"
},
"tab_analysis": {
"cn": "📊 相容性分析",
"en": "📊 Compatibility Analysis"
},
"tab_settings": {
"cn": "⚙️ 系统设置",
"en": "⚙️ Settings"
},
"tab_about": {
"cn": "ℹ️ 关于",
"en": "ℹ️ About"
},
"input_section": {
"cn": "📋 输入信息",
"en": "📋 Input Information"
},
"api_smiles_label": {
"cn": "API SMILES 结构式",
"en": "API SMILES Notation"
},
"api_smiles_placeholder": {
"cn": "请输入药物分子的SMILES结构式...\n例如: CC(=O)Nc1ccc(O)cc1 (对乙酰氨基酚)",
"en": "Enter SMILES notation of the drug molecule...\nExample: CC(=O)Nc1ccc(O)cc1 (Acetaminophen)"
},
"structure_preview": {
"cn": "分子结构预览",
"en": "Molecular Structure Preview"
},
"properties": {
"cn": "理化性质",
"en": "Physicochemical Properties"
},
"excipient_label": {
"cn": "辅料信息",
"en": "Excipient Information"
},
"excipient_placeholder": {
"cn": "请输入辅料名称...\n例如: 乳糖, 微晶纤维素",
"en": "Enter excipient name...\nExample: Lactose, Microcrystalline Cellulose"
},
"stability_label": {
"cn": "稳定性数据 (可选)",
"en": "Stability Data (Optional)"
},
"stability_placeholder": {
"cn": "如有稳定性试验数据,请在此描述...",
"en": "Enter stability study data if available..."
},
"file_upload": {
"cn": "上传文件 (可选)",
"en": "Upload Files (Optional)"
},
"btn_clear": {
"cn": "🗑️ 清空",
"en": "🗑️ Clear"
},
"btn_analyze": {
"cn": "🔬 开始分析",
"en": "🔬 Start Analysis"
},
"btn_example": {
"cn": "📌 加载示例",
"en": "📌 Load Example"
},
"output_section": {
"cn": "📊 分析报告",
"en": "📊 Analysis Report"
},
"status_label": {
"cn": "状态",
"en": "Status"
},
"status_waiting": {
"cn": "等待输入...",
"en": "Waiting for input..."
},
"status_complete": {
"cn": "分析完成!",
"en": "Analysis complete!"
},
"download_html": {
"cn": "下载 HTML 报告",
"en": "Download HTML Report"
},
"download_pdf": {
"cn": "下载 PDF 报告",
"en": "Download PDF Report"
},
"settings_title": {
"cn": "🔑 LLM 提供商设置",
"en": "🔑 LLM Provider Settings"
},
"settings_desc": {
"cn": "选择您的LLM提供商并输入API密钥。如不配置,系统将以占位符模式运行。",
"en": "Select your LLM provider and enter API key. System runs in placeholder mode if not configured."
},
"provider_label": {
"cn": "选择 LLM 提供商",
"en": "Select LLM Provider"
},
"api_key_label": {
"cn": "API 密钥",
"en": "API Key"
},
"api_key_placeholder": {
"cn": "请输入您的API密钥...",
"en": "Enter your API key..."
},
"btn_apply": {
"cn": "✓ 应用设置",
"en": "✓ Apply Settings"
},
"status_not_configured": {
"cn": "⚠️ 未配置LLM提供商(占位符模式)",
"en": "⚠️ LLM provider not configured (placeholder mode)"
},
"status_configured": {
"cn": "✓ 已配置",
"en": "✓ Configured"
},
"about_title": {
"cn": "关于本系统",
"en": "About This System"
},
"report_placeholder": {
"cn": "请在左侧输入药物和辅料信息,然后点击\"开始分析\"按钮",
"en": "Enter drug and excipient information on the left, then click \"Start Analysis\""
},
"error_missing_input": {
"cn": "请填写API和辅料信息",
"en": "Please fill in API and excipient information"
},
"smiles_invalid": {
"cn": "无效的SMILES结构式",
"en": "Invalid SMILES notation"
},
"smiles_waiting": {
"cn": "输入SMILES后自动渲染",
"en": "Structure renders after SMILES input"
},
"rdkit_not_installed": {
"cn": "RDKit未安装,无法渲染分子结构",
"en": "RDKit not installed, cannot render structure"
},
"language": {
"cn": "English",
"en": "中文"
},
"example_scenario": {
"cn": "**示例场景:** 对乙酰氨基酚 + 乳糖相容性分析",
"en": "**Example:** Acetaminophen + Lactose compatibility analysis"
},
}
# LLM Provider choices (bilingual)
LLM_PROVIDERS = [
("OpenAI (ChatGPT)", "openai"),
("Google Gemini", "gemini"),
("Anthropic Claude", "claude"),
("xAI Grok", "grok"),
("Moonshot Kimi", "kimi"),
("Deepseek", "deepseek"),
("智谱清言 (GLM)", "zhipu"),
]
# =============================================================================
# Molecular Renderer
# =============================================================================
mol_renderer = MoleculeRenderer()
# =============================================================================
# Core Analysis Pipeline
# =============================================================================
class CompatibilityAnalyzer:
"""Main analyzer orchestrating the full analysis pipeline.
Now uses the dual-phase ProfessionalAnalyzer for:
- Phase 1: Deep chemical reasoning
- Phase 2: Professional report writing
"""
def __init__(self):
try:
from layers.professional_analyzer import ProfessionalAnalyzer
self.professional_analyzer = ProfessionalAnalyzer()
self.model_invoker = self.professional_analyzer.model_invoker
self._use_professional = True
except ImportError:
# Fallback to old analyzer if professional_analyzer not available
self.model_invoker = ModelInvoker()
self.professional_analyzer = None
self._use_professional = False
print("Warning: ProfessionalAnalyzer not available, using legacy analyzer")
def set_llm_provider(self, provider: str, api_key: str):
if provider and api_key:
self.model_invoker.set_provider(provider, api_key)
def analyze(
self,
api_text: str,
excipient_text: str,
stability_text: str = "",
uploaded_files: List[str] = None,
progress_callback=None,
structure_image: str = None,
lang: str = "cn",
) -> Tuple[str, Optional[str]]:
"""Run the full analysis pipeline using dual-phase approach."""
try:
progress_msgs = {
"cn": {
"parsing": "正在解析分子结构...",
"groups": "正在识别反应活性基团...",
"rendering": "正在生成分子结构图...",
"phase1": "Phase 1: 深度机理推理中...",
"phase2": "Phase 2: 专业报告撰写中...",
"complete": "分析完成!",
},
"en": {
"parsing": "Parsing molecular structure...",
"groups": "Identifying reactive groups...",
"rendering": "Generating structure image...",
"phase1": "Phase 1: Deep reasoning analysis...",
"phase2": "Phase 2: Professional report writing...",
"complete": "Analysis complete!",
}
}
# Extract input
smiles = api_text.strip()
excipient_name = excipient_text.strip()
goal = stability_text.strip()
# Check if professional analyzer is available
if not self._use_professional or self.professional_analyzer is None:
return '<div style="color:red;padding:20px;">ProfessionalAnalyzer not available. Please ensure professional_analyzer.py is deployed.</div>', None
# =================================================================
# BRANCH 1: Stability Data Analysis (File-Driven)
# Trigger if: No SMILES provided, but files and goal are present
# =================================================================
if not smiles and (uploaded_files and goal):
def stability_progress(progress: float, msg: str):
if progress_callback:
progress_callback(progress, msg)
# 🔬 Use ADVANCED scientific modeling mode
result = self.professional_analyzer.analyze_stability_advanced(
goal=goal,
file_paths=uploaded_files,
api_info=f"API Info Provided: {bool(smiles)}", # Minimal context if any
excipient_info=excipient_name,
progress_callback=stability_progress
)
if not result["success"]:
return f'<div style="color:red;padding:20px;">稳定性分析失败: {result.get("error")}</div>', None
# Format using PROFESSIONAL scientific report template
html_report = StabilityReportFormatter.format_professional_report(
analysis_result=result,
goal=goal
)
return html_report, None
# =================================================================
# BRANCH 2: Compatibility Analysis (Structure-Driven)
# Trigger if: SMILES is provided
# =================================================================
elif smiles:
# API Name determination
api_name = smiles[:30] + "..." if len(smiles) > 30 else smiles
# Run the professional dual-phase analysis
def wrapped_progress(progress: float, msg: str):
if progress_callback:
if progress <= 0.2:
display_msg = progress_msgs[lang]["parsing"]
elif progress <= 0.3:
display_msg = progress_msgs[lang]["phase1"]
elif progress <= 0.7:
display_msg = progress_msgs[lang]["phase1"]
elif progress < 1.0:
display_msg = progress_msgs[lang]["phase2"]
else:
display_msg = progress_msgs[lang]["complete"]
progress_callback(progress, display_msg)
result = self.professional_analyzer.analyze(
smiles=smiles,
excipient_name=excipient_name,
api_name=api_name,
progress_callback=wrapped_progress,
)
if not result["success"]:
error_msg = result.get("error", "Unknown error")
return f'<div style="color:red;padding:20px;">相容性分析出错: {error_msg}</div>', None
# Generate HTML report
html_report = self.professional_analyzer.format_html_report(
analysis_result=result,
api_name=api_name,
excipient_name=excipient_name,
)
else:
return '<div style="color:red;padding:20px;">请输入 API SMILES 结构(用于相容性分析)或 上传数据文件并填写分析目标(用于稳定性分析)。</div>', None
# Add the structure image if we have one
if result.get("structure_image") and "[结构图待生成]" in html_report:
html_report = html_report.replace(
"[结构图待生成]",
f'<img src="{result["structure_image"]}" alt="Structure" style="max-width:100%;max-height:180px;">'
)
# Try to generate PDF
pdf_path = None
try:
from report.generator import ReportGenerator
from schemas.canonical_schema import AnalysisResult
# Create minimal AnalysisResult for PDF generation
# Note: PDF generation may fail if WeasyPrint is not available
with tempfile.NamedTemporaryFile(suffix='.html', delete=False, mode='w', encoding='utf-8') as tmp_html:
tmp_html.write(html_report)
tmp_html_path = tmp_html.name
# Try WeasyPrint directly
try:
from weasyprint import HTML
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False, prefix=f"report_{result['report_id']}_") as tmp_pdf:
HTML(string=html_report).write_pdf(tmp_pdf.name)
pdf_path = tmp_pdf.name
except ImportError:
print("WeasyPrint not available, PDF generation skipped")
except Exception as pdf_err:
print(f"PDF generation failed: {pdf_err}")
except Exception as e:
print(f"PDF preparation failed: {e}")
if progress_callback:
progress_callback(1.0, progress_msgs[lang]["complete"])
return html_report, pdf_path
except Exception as e:
import traceback
traceback.print_exc()
error_msg = "分析出错" if lang == "cn" else "Analysis error"
return f'<div style="color:red;padding:20px;">{error_msg}: {str(e)}</div>', None
# =============================================================================
# Helper Functions
# =============================================================================
def render_molecule(smiles: str, lang: str) -> Tuple[str, str, str]:
"""Render molecular structure."""
if not smiles or not smiles.strip():
msg = TEXTS["smiles_waiting"][lang]
return f"<div class='mol-placeholder'>{msg}</div>", "", ""
smiles = smiles.strip()
if not mol_renderer.is_available:
msg = TEXTS["rdkit_not_installed"][lang]
return f"<div class='mol-error'>{msg}</div>", "", ""
svg = mol_renderer.render_2d_svg(smiles, 320, 240)
if not svg:
msg = TEXTS["smiles_invalid"][lang]
return f"<div class='mol-error'>{msg}</div>", "", ""
props = mol_renderer.calculate_properties(smiles)
props_html = ""
if props:
labels = {
"cn": ["分子量", "LogP", "H-供体", "H-受体", "TPSA", "可旋转键"],
"en": ["MW", "LogP", "HBD", "HBA", "TPSA", "RotBonds"]
}
props_html = f"""
<div class="props-grid">
<div class="prop-item"><span class="prop-label">{labels[lang][0]}</span><span class="prop-value">{props['molecular_weight']}</span></div>
<div class="prop-item"><span class="prop-label">{labels[lang][1]}</span><span class="prop-value">{props['logp']}</span></div>
<div class="prop-item"><span class="prop-label">{labels[lang][2]}</span><span class="prop-value">{props['hbd']}</span></div>
<div class="prop-item"><span class="prop-label">{labels[lang][3]}</span><span class="prop-value">{props['hba']}</span></div>
<div class="prop-item"><span class="prop-label">{labels[lang][4]}</span><span class="prop-value">{props['tpsa']}</span></div>
<div class="prop-item"><span class="prop-label">{labels[lang][5]}</span><span class="prop-value">{props['rotatable_bonds']}</span></div>
</div>
"""
return f"<div class='mol-display'>{svg}</div>", props_html, mol_renderer.get_data_uri(smiles, 320, 240) or ""
def get_text(key: str, lang: str) -> str:
"""Get localized text."""
return TEXTS.get(key, {}).get(lang, key)
# =============================================================================
# Professional CSS
# =============================================================================
CUSTOM_CSS = """
/* ========================================
Global Styles & Variables
======================================== */
:root {
--primary: #1a5f7a;
--primary-light: #2d8bb8;
--primary-dark: #134a5f;
--accent: #57c5b6;
--accent-light: #7dd3c8;
--bg-dark: #0f1419;
--bg-card: rgba(255, 255, 255, 0.95);
--text-dark: #1a1a2e;
--text-light: #6b7280;
--border-light: rgba(0, 0, 0, 0.08);
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.12);
--shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.16);
--gradient-primary: linear-gradient(135deg, #1a5f7a 0%, #2d8bb8 50%, #57c5b6 100%);
--gradient-accent: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 20px;
}
.gradio-container {
font-family: 'Inter', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, sans-serif !important;
background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%) !important;
min-height: 100vh;
}
/* ========================================
Header Styles
======================================== */
.app-header {
background: var(--gradient-primary);
padding: 40px 30px;
border-radius: var(--radius-lg);
margin-bottom: 24px;
text-align: center;
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
}
.app-header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
animation: shimmer 15s infinite linear;
}
@keyframes shimmer {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.app-header h1 {
margin: 0;
font-size: 32px;
font-weight: 700;
color: white;
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
position: relative;
z-index: 1;
}
.app-header p {
margin: 12px 0 0;
font-size: 16px;
color: rgba(255,255,255,0.9);
font-weight: 400;
position: relative;
z-index: 1;
}
.lang-toggle {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
}
.lang-toggle button {
background: rgba(255,255,255,0.2) !important;
border: 1px solid rgba(255,255,255,0.3) !important;
color: white !important;
padding: 8px 16px !important;
border-radius: 20px !important;
font-weight: 500 !important;
backdrop-filter: blur(10px);
transition: all 0.3s ease !important;
}
.lang-toggle button:hover {
background: rgba(255,255,255,0.3) !important;
transform: translateY(-2px);
}
/* ========================================
Card Styles
======================================== */
.card {
background: var(--bg-card);
border-radius: var(--radius-md);
padding: 24px;
box-shadow: var(--shadow-md);
border: 1px solid var(--border-light);
backdrop-filter: blur(10px);
}
.card-header {
font-size: 18px;
font-weight: 600;
color: var(--primary);
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--accent);
display: flex;
align-items: center;
gap: 10px;
}
/* ========================================
Molecule Display
======================================== */
.mol-display {
background: linear-gradient(135deg, #f8fafc 0%, #e8f4f8 100%);
border-radius: var(--radius-md);
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 260px;
border: 1px solid var(--border-light);
}
.mol-placeholder, .mol-error {
color: var(--text-light);
font-size: 14px;
text-align: center;
padding: 40px;
}
.mol-error {
color: #dc3545;
}
.props-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 16px;
}
.prop-item {
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4f8 100%);
padding: 12px;
border-radius: var(--radius-sm);
text-align: center;
border: 1px solid rgba(45, 139, 184, 0.1);
}
.prop-label {
display: block;
font-size: 11px;
color: var(--text-light);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.prop-value {
display: block;
font-size: 16px;
font-weight: 600;
color: var(--primary);
}
/* ========================================
Button Styles
======================================== */
.btn-primary {
background: var(--gradient-primary) !important;
color: white !important;
border: none !important;
padding: 14px 28px !important;
border-radius: var(--radius-sm) !important;
font-weight: 600 !important;
font-size: 15px !important;
box-shadow: var(--shadow-sm) !important;
transition: all 0.3s ease !important;
}
.btn-primary:hover {
transform: translateY(-3px) !important;
box-shadow: var(--shadow-md) !important;
}
.btn-secondary {
background: #f1f5f9 !important;
color: var(--text-dark) !important;
border: 1px solid var(--border-light) !important;
padding: 14px 28px !important;
border-radius: var(--radius-sm) !important;
font-weight: 500 !important;
transition: all 0.3s ease !important;
}
.btn-secondary:hover {
background: #e2e8f0 !important;
}
.btn-example {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%) !important;
color: #92400e !important;
border: 1px solid #fcd34d !important;
padding: 10px 20px !important;
border-radius: var(--radius-sm) !important;
font-weight: 500 !important;
}
/* ========================================
Input Styles
======================================== */
.input-field textarea, .input-field input {
border-radius: var(--radius-sm) !important;
border: 1px solid #e2e8f0 !important;
padding: 14px !important;
font-size: 14px !important;
transition: all 0.3s ease !important;
}
.input-field textarea:focus, .input-field input:focus {
border-color: var(--primary-light) !important;
box-shadow: 0 0 0 3px rgba(45, 139, 184, 0.1) !important;
}
/* ========================================
Report Display
======================================== */
.report-container {
background: white;
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
min-height: 500px;
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.report-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
color: var(--text-light);
text-align: center;
padding: 40px;
}
.report-placeholder svg {
width: 80px;
height: 80px;
margin-bottom: 20px;
opacity: 0.5;
}
/* ========================================
Settings Panel
======================================== */
.settings-panel {
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
border: 1px solid #fcd34d;
border-radius: var(--radius-md);
padding: 24px;
}
.settings-panel .label {
color: #92400e;
font-weight: 600;
}
/* ========================================
Status Badge
======================================== */
.status-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-configured {
background: #d1fae5;
color: #065f46;
}
.status-pending {
background: #fef3c7;
color: #92400e;
}
/* ========================================
Animations
======================================== */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.5s ease forwards;
}
/* ========================================
Responsive Design
======================================== */
@media (max-width: 768px) {
.app-header h1 {
font-size: 24px;
}
.props-grid {
grid-template-columns: repeat(2, 1fr);
}
}
"""
# =============================================================================
# Gradio Interface
# =============================================================================
def create_interface() -> gr.Blocks:
"""Create the professional bilingual Gradio interface."""
analyzer = CompatibilityAnalyzer()
with gr.Blocks(
title="Drug-Excipient Compatibility Analyzer",
css=CUSTOM_CSS,
theme=gr.themes.Soft(
primary_hue="blue",
secondary_hue="cyan",
font=["Inter", "Microsoft YaHei", "sans-serif"],
)
) as demo:
# State variables
lang_state = gr.State("cn")
structure_state = gr.State("")
mode_state = gr.State("compatibility") # 'compatibility' or 'stability'
# =========== Header ===========
with gr.Row():
with gr.Column():
header_html = gr.HTML("""
<div class="app-header">
<h1 id="app-title">🧪 药物-辅料相容性分析系统</h1>
<p id="app-subtitle">专业制剂研发智能预测平台</p>
</div>
""")
# Language toggle
with gr.Row():
with gr.Column(scale=4):
pass
with gr.Column(scale=1):
lang_btn = gr.Button("English", size="sm", elem_classes=["lang-toggle"])
# =========== Main Tabs ===========
with gr.Tabs() as main_tabs:
# ---------- Analysis Tab ----------
with gr.TabItem("📊 智能分析 (Analysis)", id="tab_analysis") as analysis_tab:
with gr.Row(equal_height=False):
# Left: Input
with gr.Column(scale=2):
input_header = gr.Markdown("### 📋 输入信息")
# Use Tabs for Mode Selection
with gr.Tabs() as input_tabs:
# --- Mode 1: Compatibility ---
with gr.TabItem("相容性预测 (Compatibility)", id="input_tab_compat"):
gr.Markdown("##### 🧪 基于API结构预测与辅料的反应")
smiles_input = gr.Textbox(
label="API SMILES 结构式",
placeholder="请输入药物分子的SMILES结构式...\n例如: CC(=O)Nc1ccc(O)cc1",
lines=2,
elem_classes=["input-field"],
)
structure_html = gr.HTML(
value="<div class='mol-placeholder'>输入SMILES后自动渲染</div>",
)
props_html = gr.HTML(value="")
excipient_input = gr.Textbox(
label="辅料名称",
placeholder="请输入辅料名称...\n例如: 乳糖",
lines=1,
elem_classes=["input-field"],
)
with gr.Row():
example_compat_btn = gr.Button("📌 加载相容性示例", elem_classes=["btn-example"])
# --- Mode 2: Stability ---
with gr.TabItem("稳定性分析 (Stability)", id="input_tab_stability"):
gr.Markdown("##### 📈 基于实验数据评估趋势与效期")
file_upload = gr.File(
label="上传稳定性数据 (Excel/CSV/PDF/Word)",
file_count="multiple",
file_types=[".xlsx", ".xls", ".csv", ".txt", ".pdf", ".docx", ".doc", ".pptx", ".ppt"],
)
stability_goal_input = gr.Textbox(
label="分析目标与判断标准",
placeholder="例如:基于长期试验数据(0,3,6月),评估杂质A增长趋势,判断是否满足24个月货架期要求...",
lines=4,
elem_classes=["input-field"],
)
with gr.Row():
example_stab_btn = gr.Button("📌 加载稳定性示例", elem_classes=["btn-example"])
# Actions
with gr.Row(elem_classes=["action-bar"]):
clear_btn = gr.Button("🗑️ 清空重置", elem_classes=["btn-secondary"])
analyze_btn = gr.Button("🔬 开始专业分析", elem_classes=["btn-primary"])
# Right: Output
with gr.Column(scale=3):
output_header = gr.Markdown("### 📊 分析报告")
status_text = gr.Textbox(
label="当前状态",
value="系统就绪,等待下达指令...",
interactive=False,
)
report_output = gr.HTML(
value="""
<div class="report-placeholder">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p>请在左侧选择 [相容性] 或 [稳定性] 模式进行分析</p>
</div>
""",
elem_classes=["report-container"],
)
with gr.Row():
download_html = gr.File(label="下载 HTML", visible=False)
download_pdf = gr.File(label="下载 PDF", visible=False)
# ---------- Settings Tab ----------
with gr.TabItem("⚙️ 系统设置", id="tab_settings") as settings_tab:
settings_title = gr.Markdown("### 🔑 LLM 提供商设置")
settings_desc = gr.Markdown("选择您的LLM提供商并输入API密钥。如不配置,系统将以占位符模式运行。")
with gr.Group(elem_classes=["settings-panel"]):
provider_dropdown = gr.Dropdown(
label="选择 LLM 提供商",
choices=LLM_PROVIDERS,
value=None,
)
api_key_input = gr.Textbox(
label="API 密钥",
placeholder="请输入您的API密钥...",
type="password",
)
provider_status = gr.Textbox(
label="状态",
value="⚠️ 未配置LLM提供商(占位符模式)",
interactive=False,
)
apply_btn = gr.Button("✓ 应用设置", elem_classes=["btn-primary"])
gr.Markdown("""
### 📝 API密钥获取
| 提供商 | 网址 |
|--------|------|
| OpenAI | platform.openai.com |
| Gemini | aistudio.google.com |
| Claude | console.anthropic.com |
| Grok | console.x.ai |
| Kimi | platform.moonshot.cn |
| Deepseek | platform.deepseek.com |
| 智谱 | open.bigmodel.cn |
""")
# ---------- About Tab ----------
with gr.TabItem("ℹ️ 关于", id="tab_about"):
gr.Markdown("""
## 关于本系统 | About This System
本系统是一个专业的药物-辅料相容性分析平台,旨在帮助制剂研发人员快速评估药物与辅料之间的潜在相互作用风险。
This is a professional API-Excipient compatibility analysis platform designed to help formulation R&D scientists quickly assess potential interaction risks between drugs and excipients.
---
### 主要功能 | Key Features
- **相容性预测** | Compatibility Prediction: 基于SMILES结构自动识别活性基团和反应风险
- **稳定性分析** | Stability Analysis: 基于实验数据(Excel/CSV)进行趋势分析与效期评估
- **专业报告** | Professional Report: 生成双语专业研发报告 (QbD/ICH准则)
- **多模型支持** | Multi-LLM Support: 支持7种主流大语言模型
---
### 技术栈 | Tech Stack
- **Frontend**: Gradio with custom CSS
- **Backend**: Python with modular layer architecture
- **Chemistry**: RDKit for molecular rendering
- **AI**: Multi-provider LLM integration
---
### 免责声明 | Disclaimer
本系统生成的分析报告仅供研发参考。相容性结论需通过实验验证(如加速稳定性试验)进行最终确认。
Analysis reports are for R&D reference only. Compatibility conclusions should be confirmed through experimental verification (e.g., accelerated stability testing).
""")
# =========== Event Handlers ===========
def toggle_language(current_lang):
new_lang = "en" if current_lang == "cn" else "cn"
# Update header
header = f"""
<div class="app-header">
<h1>{get_text('app_title', new_lang)}</h1>
<p>{get_text('app_subtitle', new_lang)}</p>
</div>
"""
# Return all updated components
return (
new_lang,
header,
get_text('language', new_lang),
f"### {get_text('input_section', new_lang)}",
f"### {get_text('output_section', new_lang)}",
get_text('status_waiting', new_lang),
f"### {get_text('settings_title', new_lang)}",
get_text('settings_desc', new_lang),
get_text('status_not_configured', new_lang),
get_text('example_scenario', new_lang),
)
def update_structure(smiles, lang):
svg, props, uri = render_molecule(smiles, lang)
return svg, props, uri
def apply_settings(provider, api_key, lang):
if provider and api_key:
analyzer.set_llm_provider(provider, api_key)
name = dict(LLM_PROVIDERS).get(provider, provider)
return f"✓ {get_text('status_configured', lang)} {name}"
return get_text('status_not_configured', lang)
def run_analysis(smiles, excipient, stability_goal, files, struct_img, lang, progress=gr.Progress()):
# Validation Logic: Either SMILES+Excipient OR Files+Goal must be present
has_compat_input = bool(smiles and excipient)
has_stability_input = bool(files and stability_goal)
if not has_compat_input and not has_stability_input:
return (
f"<div class='report-placeholder' style='color:red;'>⚠️ 请至少完善一种分析模式的输入信息:<br>1. 相容性预测:需填写 SMILES 和 辅料名称<br>2. 稳定性分析:需上传数据文件 并 填写分析目标</div>",
"输入不完整",
gr.update(visible=False),
gr.update(visible=False),
)
file_paths = [f.name for f in files] if files else []
html, pdf = analyzer.analyze(
api_text=smiles if smiles else "",
excipient_text=excipient if excipient else "",
stability_text=stability_goal if stability_goal else "",
uploaded_files=file_paths,
progress_callback=lambda v, m: progress(v, desc=m),
structure_image=struct_img,
lang=lang,
)
html_file = None
try:
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
f.write(html)
html_file = f.name
except:
pass
return (
html,
get_text('status_complete', lang),
gr.update(value=html_file, visible=html_file is not None),
gr.update(value=pdf, visible=pdf is not None),
)
def load_example_compat():
return "CC(=O)Nc1ccc(O)cc1", "乳糖", None, ""
def load_example_stability():
# Create a dummy CSV for example
import os
dummy_path = "example_stability_data.csv"
with open(dummy_path, "w", encoding="utf-8") as f:
f.write("Time(Month),Assay(%),Impurity A(%),pH\n0,99.8,0.05,6.5\n3,99.2,0.12,6.4\n6,98.5,0.25,6.2")
goal = "评估杂质A增长趋势,判断是否满足24个月效期标准(限度<0.5%)"
return "", "", [dummy_path], goal
def clear_all(lang):
placeholder = f"<div class='mol-placeholder'>{get_text('smiles_waiting', lang)}</div>"
return "", "", "", None, placeholder, "", ""
# Wire events
lang_btn.click(
fn=toggle_language,
inputs=[lang_state],
outputs=[
lang_state, header_html, lang_btn,
input_header, output_header, status_text,
settings_title, settings_desc, provider_status,
gr.Markdown() # dummy
],
)
smiles_input.change(
fn=update_structure,
inputs=[smiles_input, lang_state],
outputs=[structure_html, props_html, structure_state],
)
apply_btn.click(
fn=apply_settings,
inputs=[provider_dropdown, api_key_input, lang_state],
outputs=[provider_status],
)
analyze_btn.click(
fn=run_analysis,
inputs=[smiles_input, excipient_input, stability_goal_input, file_upload, structure_state, lang_state],
outputs=[report_output, status_text, download_html, download_pdf],
)
example_compat_btn.click(
fn=load_example_compat,
outputs=[smiles_input, excipient_input, file_upload, stability_goal_input],
)
example_stab_btn.click(
fn=load_example_stability,
outputs=[smiles_input, excipient_input, file_upload, stability_goal_input],
)
clear_btn.click(
fn=clear_all,
inputs=[lang_state],
outputs=[smiles_input, excipient_input, stability_goal_input, file_upload, structure_html, props_html, structure_state],
)
return demo
# =============================================================================
# Main
# =============================================================================
def main():
demo = create_interface()
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
show_error=True,
)
if __name__ == "__main__":
main()