| """
|
| Modular section renderers for dynamic report generation.
|
| Each function renders one type of report section as HTML.
|
| """
|
|
|
| from typing import Dict, Any, Optional
|
| from schemas.analysis_intent import AnalysisIntent
|
| from schemas.decision_result import RegulatoryDecisionResult
|
|
|
|
|
| def render_executive_summary(
|
| intent: Optional[AnalysisIntent],
|
| result: Optional[RegulatoryDecisionResult],
|
| explanations: Dict[str, str],
|
| config: Dict[str, Any]
|
| ) -> str:
|
| """Render executive summary with LLM-generated narrative."""
|
| summary_text = explanations.get("executive_summary", "分析结果待生成")
|
| focus = config.get("focus", "general")
|
|
|
|
|
| highlights = ""
|
| if focus == "top_batch_recommendation" and result and result.batch_ranking:
|
| top_batch = result.batch_ranking[0]
|
| highlights = f"""
|
| <div class="alert alert-success">
|
| <strong>🏆 推荐批次:</strong> {top_batch.batch_name}<br>
|
| <strong>评分:</strong> {top_batch.score}<br>
|
| <strong>理由:</strong> {top_batch.reason}
|
| </div>
|
| """
|
|
|
| return f"""
|
| <div class="section">
|
| <div class="section-title">核心结论</div>
|
| <p>{summary_text}</p>
|
| {highlights}
|
| </div>
|
| """
|
|
|
|
|
| def render_ranking_table(
|
| intent: Optional[AnalysisIntent],
|
| result: Optional[RegulatoryDecisionResult],
|
| explanations: Dict[str, str],
|
| config: Dict[str, Any]
|
| ) -> str:
|
| """Render batch ranking table."""
|
| if not result or not result.batch_ranking:
|
| return ""
|
|
|
| show_scores = config.get("show_scores", True)
|
|
|
| rows = []
|
| for item in result.batch_ranking:
|
| rank_badge = f'<span class="badge badge-success">推荐</span>' if item.rank == 1 else ""
|
| score_cell = f"<td>{item.score}</td>" if show_scores else ""
|
|
|
| rows.append(f"""
|
| <tr style="{'background-color: #d4edda; font-weight: bold;' if item.rank == 1 else ''}">
|
| <td>{item.rank} {rank_badge}</td>
|
| <td>{item.batch_name}</td>
|
| {score_cell}
|
| <td>{item.reason}</td>
|
| </tr>
|
| """)
|
|
|
| score_header = "<th>评分</th>" if show_scores else ""
|
|
|
| return f"""
|
| <div class="section">
|
| <div class="section-title">批次排名</div>
|
| <table>
|
| <thead>
|
| <tr>
|
| <th style="width:15%">排名</th>
|
| <th style="width:25%">批次名称</th>
|
| {score_header}
|
| <th>评价理由</th>
|
| </tr>
|
| </thead>
|
| <tbody>
|
| {''.join(rows)}
|
| </tbody>
|
| </table>
|
| </div>
|
| """
|
|
|
|
|
| def render_prediction_table(
|
| intent: Optional[AnalysisIntent],
|
| result: Optional[RegulatoryDecisionResult],
|
| explanations: Dict[str, str],
|
| config: Dict[str, Any]
|
| ) -> str:
|
| """Render shelf-life predictions table."""
|
| if not result or not result.predictions:
|
| return ""
|
|
|
|
|
| valid_preds = {
|
| tp: pred for tp, pred in result.predictions.items()
|
| if pred.is_valid
|
| }
|
|
|
| if not valid_preds:
|
| return render_data_quality_warning(
|
| intent, result, explanations,
|
| {"message": "由于数据不足或外推限制,无法生成有效的货架期预测。"}
|
| )
|
|
|
| rows = []
|
| for timepoint, pred in valid_preds.items():
|
| status = "合规" if pred.is_compliant() else "超标"
|
| status_class = "badge-success" if pred.is_compliant() else "badge-warning"
|
|
|
| rows.append(f"""
|
| <tr>
|
| <td>{timepoint}</td>
|
| <td>{pred.point_estimate:.2f}%</td>
|
| <td>{pred.CI_lower:.2f}% - {pred.CI_upper:.2f}%</td>
|
| <td><span class="badge {status_class}">{status}</span></td>
|
| </tr>
|
| """)
|
|
|
| return f"""
|
| <div class="section">
|
| <div class="section-title">货架期预测</div>
|
| <table>
|
| <thead>
|
| <tr>
|
| <th>时间点</th>
|
| <th>点预测</th>
|
| <th>95% 置信区间</th>
|
| <th>状态</th>
|
| </tr>
|
| </thead>
|
| <tbody>
|
| {''.join(rows)}
|
| </tbody>
|
| </table>
|
| </div>
|
| """
|
|
|
|
|
| def render_kinetic_modeling(
|
| intent: Optional[AnalysisIntent],
|
| result: Optional[RegulatoryDecisionResult],
|
| explanations: Dict[str, str],
|
| config: Dict[str, Any]
|
| ) -> str:
|
| """Render kinetic modeling results."""
|
| if not result or not result.kinetic_fits:
|
| return ""
|
|
|
|
|
| rows = []
|
| for condition, fit in result.kinetic_fits.items():
|
|
|
| if fit.k is None and fit.R2 is None:
|
| continue
|
|
|
| k_str = f"{fit.k:.4f}" if fit.k is not None else "N/A"
|
| r2_str = f"{fit.R2:.4f}" if fit.R2 is not None else "N/A"
|
| se_str = f"{fit.SE_k:.3f}" if fit.SE_k is not None else "N/A"
|
|
|
|
|
| if fit.R2 is not None:
|
| if fit.R2 >= 0.95:
|
| confidence = "高"
|
| elif fit.R2 >= 0.8:
|
| confidence = "中"
|
| else:
|
| confidence = "低"
|
| else:
|
| confidence = "低"
|
|
|
| rows.append(f"""
|
| <tr>
|
| <td>{condition}</td>
|
| <td>{fit.model_type}</td>
|
| <td>{k_str}</td>
|
| <td>{r2_str}</td>
|
| <td>{se_str}</td>
|
| <td>{confidence}</td>
|
| </tr>
|
| """)
|
|
|
| if not rows:
|
| return ""
|
|
|
| return f"""
|
| <div class="section">
|
| <div class="section-title">动力学建模结果</div>
|
| <div class="alert alert-info">
|
| <strong>模型:</strong> 零级降解动力学 y(t) = y₀ + k·t
|
| </div>
|
| <table>
|
| <thead>
|
| <tr>
|
| <th>条件</th>
|
| <th>模型</th>
|
| <th>k (%/月)</th>
|
| <th>R²</th>
|
| <th>SE(k)</th>
|
| <th>置信度</th>
|
| </tr>
|
| </thead>
|
| <tbody>
|
| {''.join(rows)}
|
| </tbody>
|
| </table>
|
| </div>
|
| """
|
|
|
|
|
| def render_trend_visualization(
|
| intent: Optional[AnalysisIntent],
|
| result: Optional[RegulatoryDecisionResult],
|
| explanations: Dict[str, str],
|
| config: Dict[str, Any]
|
| ) -> str:
|
| """Render trend comparison chart (placeholder - reuse existing SVG logic)."""
|
|
|
|
|
| return f"""
|
| <div class="section">
|
| <div class="section-title">趋势对比可视化</div>
|
| <div class="alert alert-info">
|
| 图表生成中...(整合现有 SVG 绘图逻辑)
|
| </div>
|
| </div>
|
| """
|
|
|
|
|
| def render_data_quality_warning(
|
| intent: Optional[AnalysisIntent],
|
| result: Optional[RegulatoryDecisionResult],
|
| explanations: Dict[str, str],
|
| config: Dict[str, Any]
|
| ) -> str:
|
| """Render data quality warning."""
|
| message = config.get("message", "数据质量存在限制,请谨慎使用分析结果。")
|
|
|
| return f"""
|
| <div class="section">
|
| <div class="alert alert-warning">
|
| <strong>⚠️ 数据质量提示:</strong><br>
|
| {message}
|
| </div>
|
| </div>
|
| """
|
|
|
|
|
| def render_recommendations(
|
| intent: Optional[AnalysisIntent],
|
| result: Optional[RegulatoryDecisionResult],
|
| explanations: Dict[str, str],
|
| config: Dict[str, Any]
|
| ) -> str:
|
| """Render actionable recommendations."""
|
| recs_text = explanations.get("recommendations", "建议进行进一步的稳定性研究。")
|
|
|
| return f"""
|
| <div class="section">
|
| <div class="section-title">行动建议</div>
|
| <div class="alert alert-success">
|
| {recs_text}
|
| </div>
|
| </div>
|
| """
|
|
|
|
|
| def render_regulatory_compliance(
|
| intent: Optional[AnalysisIntent],
|
| result: Optional[RegulatoryDecisionResult],
|
| explanations: Dict[str, str],
|
| config: Dict[str, Any]
|
| ) -> str:
|
| """Render regulatory compliance statement."""
|
| return f"""
|
| <div class="section">
|
| <div class="section-title">法规合规性</div>
|
| <div class="alert alert-info">
|
| <strong>ICH Q1E:</strong> 本报告遵循 ICH Q1E 稳定性数据评价指导原则。
|
| </div>
|
| <p>本分析符合以下法规要求:</p>
|
| <ul>
|
| <li><strong>ICH Q1A(R2):</strong> 稳定性试验设计</li>
|
| <li><strong>ICH Q1E:</strong> 稳定性数据评价</li>
|
| <li><strong>WHO TRS 953:</strong> 统计学方法</li>
|
| </ul>
|
| </div>
|
| """
|
|
|
|
|
|
|
| SECTION_RENDERERS = {
|
| "executive_summary": render_executive_summary,
|
| "ranking_table": render_ranking_table,
|
| "prediction_table": render_prediction_table,
|
| "kinetic_modeling": render_kinetic_modeling,
|
| "trend_visualization": render_trend_visualization,
|
| "data_quality_warning": render_data_quality_warning,
|
| "recommendations": render_recommendations,
|
| "regulatory_compliance": render_regulatory_compliance,
|
| }
|
|
|