open-pharma-plugins 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_framework.py +495 -0
- open_pharma_plugins-2.2.0.dist-info/METADATA +135 -0
- open_pharma_plugins-2.2.0.dist-info/RECORD +136 -0
- open_pharma_plugins-2.2.0.dist-info/WHEEL +5 -0
- open_pharma_plugins-2.2.0.dist-info/entry_points.txt +8 -0
- open_pharma_plugins-2.2.0.dist-info/licenses/LICENSE +202 -0
- open_pharma_plugins-2.2.0.dist-info/top_level.txt +8 -0
- open_pharma_plugins_campaign_studio/__init__.py +14 -0
- open_pharma_plugins_campaign_studio/__main__.py +11 -0
- open_pharma_plugins_campaign_studio/_campaign_store.py +162 -0
- open_pharma_plugins_campaign_studio/_claim_engine.py +262 -0
- open_pharma_plugins_campaign_studio/_renderer.py +119 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/legal.json +21 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/logo.svg +4 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/palette.json +11 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/product.png +1 -0
- open_pharma_plugins_campaign_studio/fixtures/brand_kit/typography.json +14 -0
- open_pharma_plugins_campaign_studio/fixtures/sample_approved_claims.json +119 -0
- open_pharma_plugins_campaign_studio/models/__init__.py +34 -0
- open_pharma_plugins_campaign_studio/models/_common.py +12 -0
- open_pharma_plugins_campaign_studio/models/brief.py +72 -0
- open_pharma_plugins_campaign_studio/models/claims.py +15 -0
- open_pharma_plugins_campaign_studio/models/copy.py +47 -0
- open_pharma_plugins_campaign_studio/models/journey.py +21 -0
- open_pharma_plugins_campaign_studio/models/message.py +25 -0
- open_pharma_plugins_campaign_studio/models/mlr.py +29 -0
- open_pharma_plugins_campaign_studio/models/validation.py +32 -0
- open_pharma_plugins_campaign_studio/policy/rules.json +79 -0
- open_pharma_plugins_campaign_studio/templates/banner.svg.j2 +26 -0
- open_pharma_plugins_campaign_studio/templates/email.html.j2 +54 -0
- open_pharma_plugins_campaign_studio/tools/__init__.py +0 -0
- open_pharma_plugins_campaign_studio/tools/create_campaign_brief.py +212 -0
- open_pharma_plugins_campaign_studio/tools/generate_audience_journey.py +129 -0
- open_pharma_plugins_campaign_studio/tools/generate_channel_copy.py +199 -0
- open_pharma_plugins_campaign_studio/tools/generate_message_architecture.py +121 -0
- open_pharma_plugins_campaign_studio/tools/package_mlr_submission.py +230 -0
- open_pharma_plugins_campaign_studio/tools/render_banner.py +99 -0
- open_pharma_plugins_campaign_studio/tools/render_email.py +101 -0
- open_pharma_plugins_campaign_studio/tools/render_poster.py +222 -0
- open_pharma_plugins_campaign_studio/tools/retrieve_approved_claims.py +71 -0
- open_pharma_plugins_campaign_studio/tools/retrieve_brand_components.py +76 -0
- open_pharma_plugins_campaign_studio/tools/validate_claims_and_fair_balance.py +285 -0
- open_pharma_plugins_competitive_intelligence/__init__.py +13 -0
- open_pharma_plugins_competitive_intelligence/__main__.py +11 -0
- open_pharma_plugins_competitive_intelligence/_artifacts.py +87 -0
- open_pharma_plugins_competitive_intelligence/_cache.py +144 -0
- open_pharma_plugins_competitive_intelligence/_clinical_trials.py +569 -0
- open_pharma_plugins_competitive_intelligence/_dailymed.py +260 -0
- open_pharma_plugins_competitive_intelligence/_fda.py +255 -0
- open_pharma_plugins_competitive_intelligence/_pubmed.py +342 -0
- open_pharma_plugins_competitive_intelligence/_regulatory.py +140 -0
- open_pharma_plugins_competitive_intelligence/_runs.py +278 -0
- open_pharma_plugins_competitive_intelligence/_transport.py +83 -0
- open_pharma_plugins_competitive_intelligence/_watchlist.py +113 -0
- open_pharma_plugins_competitive_intelligence/_web_search.py +331 -0
- open_pharma_plugins_competitive_intelligence/models.py +525 -0
- open_pharma_plugins_competitive_intelligence/tools/__init__.py +0 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_extract_events.py +247 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_landscape.py +195 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_refresh.py +101 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_report.py +402 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_news.py +48 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_publications.py +46 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_regulatory.py +64 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_scan_trials.py +85 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_status.py +106 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_timeline.py +453 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_track.py +121 -0
- open_pharma_plugins_competitive_intelligence/tools/ci_trial_detail.py +55 -0
- open_pharma_plugins_field_training/__init__.py +13 -0
- open_pharma_plugins_field_training/__main__.py +11 -0
- open_pharma_plugins_field_training/_content_store.py +131 -0
- open_pharma_plugins_field_training/_grounding.py +75 -0
- open_pharma_plugins_field_training/_html_renderers.py +546 -0
- open_pharma_plugins_field_training/fixtures/sample_product_message.pdf +156 -0
- open_pharma_plugins_field_training/fixtures/sample_training_deck.pptx +0 -0
- open_pharma_plugins_field_training/models.py +265 -0
- open_pharma_plugins_field_training/tools/__init__.py +0 -0
- open_pharma_plugins_field_training/tools/get_document_page.py +67 -0
- open_pharma_plugins_field_training/tools/ingest_document.py +147 -0
- open_pharma_plugins_field_training/tools/list_documents.py +51 -0
- open_pharma_plugins_field_training/tools/render_output.py +118 -0
- open_pharma_plugins_field_training/tools/search_content.py +57 -0
- open_pharma_plugins_hcp_intelligence/__init__.py +14 -0
- open_pharma_plugins_hcp_intelligence/__main__.py +11 -0
- open_pharma_plugins_hcp_intelligence/_crm_store.py +70 -0
- open_pharma_plugins_hcp_intelligence/batch.py +891 -0
- open_pharma_plugins_hcp_intelligence/batch_cli.py +221 -0
- open_pharma_plugins_hcp_intelligence/batch_csv.py +206 -0
- open_pharma_plugins_hcp_intelligence/fixtures/sample_accounts.csv +27 -0
- open_pharma_plugins_hcp_intelligence/models.py +356 -0
- open_pharma_plugins_hcp_intelligence/tools/__init__.py +0 -0
- open_pharma_plugins_hcp_intelligence/tools/get_account.py +48 -0
- open_pharma_plugins_hcp_intelligence/tools/list_accounts.py +66 -0
- open_pharma_plugins_hcp_intelligence/tools/search_clinical_trials.py +175 -0
- open_pharma_plugins_hcp_intelligence/tools/search_congresses.py +134 -0
- open_pharma_plugins_hcp_intelligence/tools/search_grants.py +181 -0
- open_pharma_plugins_hcp_intelligence/tools/search_guidelines.py +238 -0
- open_pharma_plugins_hcp_intelligence/tools/search_hco_web.py +73 -0
- open_pharma_plugins_hcp_intelligence/tools/search_hcp_web.py +199 -0
- open_pharma_plugins_hcp_intelligence/tools/search_orcid.py +214 -0
- open_pharma_plugins_hcp_intelligence/tools/search_publications.py +207 -0
- open_pharma_plugins_hcp_intelligence/tools/update_account.py +84 -0
- open_pharma_plugins_next_best_engagement/__init__.py +14 -0
- open_pharma_plugins_next_best_engagement/__main__.py +11 -0
- open_pharma_plugins_next_best_engagement/_optimizer.py +400 -0
- open_pharma_plugins_next_best_engagement/_renderer.py +149 -0
- open_pharma_plugins_next_best_engagement/_scoring.py +82 -0
- open_pharma_plugins_next_best_engagement/_universe.py +145 -0
- open_pharma_plugins_next_best_engagement/fixtures/sample_universe.csv +81 -0
- open_pharma_plugins_next_best_engagement/models.py +135 -0
- open_pharma_plugins_next_best_engagement/tools/__init__.py +0 -0
- open_pharma_plugins_next_best_engagement/tools/load_universe.py +47 -0
- open_pharma_plugins_next_best_engagement/tools/recommend_engagements.py +80 -0
- open_pharma_plugins_next_best_engagement/tools/render_plan.py +90 -0
- open_pharma_plugins_territory_alignment/__init__.py +14 -0
- open_pharma_plugins_territory_alignment/__main__.py +11 -0
- open_pharma_plugins_territory_alignment/data.py +300 -0
- open_pharma_plugins_territory_alignment/fixtures/constraints.csv +11 -0
- open_pharma_plugins_territory_alignment/fixtures/current_alignment.csv +81 -0
- open_pharma_plugins_territory_alignment/fixtures/hcps.csv +81 -0
- open_pharma_plugins_territory_alignment/fixtures/reps.csv +9 -0
- open_pharma_plugins_territory_alignment/geo.py +175 -0
- open_pharma_plugins_territory_alignment/models.py +201 -0
- open_pharma_plugins_territory_alignment/scoring.py +125 -0
- open_pharma_plugins_territory_alignment/solver.py +504 -0
- open_pharma_plugins_territory_alignment/tools/__init__.py +0 -0
- open_pharma_plugins_territory_alignment/tools/ta_align.py +138 -0
- open_pharma_plugins_territory_alignment/tools/ta_cluster.py +247 -0
- open_pharma_plugins_territory_alignment/tools/ta_compare.py +173 -0
- open_pharma_plugins_territory_alignment/tools/ta_evaluate.py +119 -0
- open_pharma_plugins_territory_alignment/tools/ta_status.py +34 -0
- open_pharma_plugins_territory_alignment/tools/ta_visualize.py +504 -0
- shared/__init__.py +11 -0
- shared/env.py +217 -0
- shared/filesystem.py +110 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"""Self-contained, source-ledger HTML renderers for Field Training outputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from html import escape
|
|
6
|
+
|
|
7
|
+
_CSS = """\
|
|
8
|
+
:root {
|
|
9
|
+
--ink:#102a43;--navy:#0b1f33;--teal:#0f766e;--teal-soft:#dff3f1;
|
|
10
|
+
--coral:#b83a3a;--coral-soft:#fde8e7;--paper:#f7f9fc;--white:#fff;
|
|
11
|
+
--slate:#526778;--line:#d8e1e8;--focus:#f4b942;
|
|
12
|
+
--shadow:0 18px 55px rgba(11,31,51,.12);
|
|
13
|
+
}
|
|
14
|
+
*{box-sizing:border-box}
|
|
15
|
+
html{scroll-behavior:smooth}
|
|
16
|
+
body{margin:0;color:var(--ink);background:linear-gradient(135deg,#e9f0f5 0,var(--paper) 42%,#edf6f5 100%);font-family:"Avenir Next",Avenir,"Segoe UI",sans-serif;font-size:16px;line-height:1.65}
|
|
17
|
+
button,input{font:inherit}button{cursor:pointer}a{color:inherit}
|
|
18
|
+
.skip-link{position:fixed;left:1rem;top:-5rem;z-index:100;padding:.7rem 1rem;color:var(--white);background:var(--navy);border-radius:.35rem}.skip-link:focus{top:1rem}
|
|
19
|
+
.dossier-shell{display:grid;grid-template-columns:260px minmax(0,1fr);width:min(1280px,calc(100% - 2rem));margin:1rem auto;overflow:clip;background:var(--white);border:1px solid rgba(16,42,67,.12);border-radius:18px;box-shadow:var(--shadow)}
|
|
20
|
+
.dossier-rail{position:relative;padding:2rem 1.35rem;color:#dce8f1;background:var(--navy)}
|
|
21
|
+
.rail-mark{display:grid;grid-template-columns:4px 1fr;gap:.75rem;align-items:stretch;margin-bottom:2.75rem}.rail-mark::before{content:"";background:var(--teal);border-radius:99px}
|
|
22
|
+
.rail-kicker,.eyebrow{font-family:"SFMono-Regular",Consolas,monospace;font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.rail-title{margin-top:.25rem;color:var(--white);font-size:1rem;font-weight:700}
|
|
23
|
+
.dossier-nav{position:sticky;top:1.5rem}.dossier-nav ul{margin:0;padding:0;list-style:none}.dossier-nav li+li{margin-top:.3rem}.dossier-nav a{display:block;padding:.6rem .7rem;color:#c7d7e4;text-decoration:none;border-left:2px solid transparent;border-radius:0 .35rem .35rem 0}.dossier-nav a:hover,.dossier-nav a:focus-visible{color:var(--white);background:rgba(255,255,255,.08);border-left-color:#53c5b9}
|
|
24
|
+
.dossier-workspace{min-width:0}.utility-bar{display:flex;justify-content:space-between;gap:1rem;align-items:center;padding:.8rem clamp(1.25rem,4vw,3.5rem);border-bottom:1px solid var(--line);background:rgba(255,255,255,.94)}
|
|
25
|
+
.draft-state{display:inline-flex;gap:.45rem;align-items:center;color:#7f1d1d;font-size:.78rem;font-weight:800;letter-spacing:.04em;text-transform:uppercase}.draft-state::before{content:"";width:.55rem;height:.55rem;background:var(--coral);border-radius:50%;box-shadow:0 0 0 4px var(--coral-soft)}
|
|
26
|
+
.utility-actions{display:flex;flex-wrap:wrap;gap:.5rem}.action-button,.filter-button{padding:.5rem .78rem;color:var(--ink);background:var(--white);border:1px solid var(--line);border-radius:.4rem;font-size:.82rem;font-weight:700}.action-button:hover,.filter-button:hover,.filter-button[aria-pressed="true"]{color:var(--white);background:var(--teal);border-color:var(--teal)}
|
|
27
|
+
.dossier-main{padding:clamp(1.5rem,4vw,3.5rem)}.dossier-hero{position:relative;margin-bottom:2.5rem;padding:clamp(1.5rem,4vw,3rem);overflow:hidden;color:var(--white);background:var(--navy);border-radius:14px}.dossier-hero::after{content:"SOURCE / MESSAGE / PRACTICE";position:absolute;right:-1.5rem;bottom:1rem;color:rgba(255,255,255,.07);font-family:"SFMono-Regular",Consolas,monospace;font-size:clamp(1.2rem,4vw,3rem);font-weight:800;letter-spacing:.08em;transform:rotate(-4deg)}
|
|
28
|
+
.eyebrow{margin:0 0 .75rem;color:#76d6cc}h1,h2,h3{color:var(--navy);line-height:1.2}h1{position:relative;z-index:1;max-width:780px;margin:0;color:var(--white);font-family:Georgia,"Times New Roman",serif;font-size:clamp(2rem,5vw,4.25rem);font-weight:500;letter-spacing:-.035em}h2{margin:0 0 1rem;font-family:Georgia,"Times New Roman",serif;font-size:clamp(1.55rem,3vw,2.3rem)}h3{margin:1.6rem 0 .8rem;font-size:1.05rem;letter-spacing:.01em}p{margin:0 0 .9rem}ul,ol{padding-left:1.3rem}
|
|
29
|
+
.hero-meta{display:flex;flex-wrap:wrap;gap:.7rem 1.3rem;margin-top:1.6rem;color:#dce8f1;font-size:.86rem}.section-block{margin-top:3rem;scroll-margin-top:1.5rem}.section-heading{display:grid;grid-template-columns:auto 1fr;gap:.8rem;align-items:center}.section-index{display:inline-grid;width:2.2rem;height:2.2rem;place-items:center;color:var(--white);background:var(--teal);border-radius:50%;font-family:"SFMono-Regular",Consolas,monospace;font-size:.76rem;font-weight:800}
|
|
30
|
+
.module-meta,.filter-bar{display:flex;flex-wrap:wrap;gap:.55rem;align-items:center}.module-meta{margin-bottom:1.2rem;color:var(--slate);font-size:.9rem}.filter-bar{margin:1rem 0;padding:.8rem;background:var(--paper);border:1px solid var(--line);border-radius:.6rem}.filter-search{min-width:min(100%,220px);flex:1;padding:.55rem .75rem;border:1px solid var(--line);border-radius:.4rem}
|
|
31
|
+
.card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:.9rem}.card{position:relative;padding:1.15rem;background:var(--white);border:1px solid var(--line);border-radius:.75rem;box-shadow:0 8px 24px rgba(16,42,67,.06)}.card[data-category="safety"]{border-top:3px solid var(--coral)}.card[data-category="efficacy"]{border-top:3px solid var(--teal)}.card-header{margin-bottom:.55rem;color:var(--navy);font-weight:800}
|
|
32
|
+
.badge{display:inline-flex;padding:.2rem .55rem;border-radius:99px;font-size:.72rem;font-weight:800;letter-spacing:.04em;text-transform:uppercase}.badge-green{color:#07534d;background:var(--teal-soft)}.badge-yellow{color:#6d4b00;background:#fff2c7}.badge-red{color:#8a2222;background:var(--coral-soft)}.badge-blue{color:#174a67;background:#e0f0f8}.badge-gray{color:#425466;background:#e9eef2}
|
|
33
|
+
.source-stack{margin-top:.8rem}.source-disclosure{margin-top:.45rem;overflow:hidden;background:#f3f8fa;border-left:3px solid var(--teal);border-radius:0 .45rem .45rem 0}.source-disclosure summary{padding:.55rem .7rem;color:#315064;cursor:pointer;font-size:.78rem;font-weight:800}.source-disclosure blockquote{margin:0;padding:0 .8rem .75rem;color:var(--slate);font-family:Georgia,serif;font-size:.86rem}
|
|
34
|
+
.facilitator-note{padding:.85rem;background:#fff8dd;border:1px dashed #b68a18;border-radius:.5rem}.rubric-weight{color:var(--teal);font-family:"SFMono-Regular",Consolas,monospace;font-weight:800}.meta{margin-top:3rem;padding-top:1rem;color:var(--slate);border-top:1px solid var(--line);font-size:.78rem}
|
|
35
|
+
[hidden],.is-filtered{display:none!important}:focus-visible{outline:3px solid var(--focus);outline-offset:3px}
|
|
36
|
+
@media (max-width:820px){.dossier-shell{display:block;width:100%;margin:0;border:0;border-radius:0}.dossier-rail{padding:1rem 1.25rem}.rail-mark{margin-bottom:.75rem}.dossier-nav{position:static;overflow-x:auto}.dossier-nav ul{display:flex;gap:.25rem}.dossier-nav a{white-space:nowrap;border-left:0;border-bottom:2px solid transparent}}
|
|
37
|
+
@media (max-width:560px){.utility-bar{align-items:stretch;flex-direction:column;padding:.75rem 1rem}.draft-state{align-self:flex-start;white-space:nowrap}.utility-actions{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}.action-button{padding:.48rem .25rem;font-size:.68rem}.dossier-main{padding:1rem}.dossier-hero{padding:1.25rem}.hero-meta{display:grid;gap:.25rem}}
|
|
38
|
+
@media print{body{background:#fff;font-size:10.5pt}.dossier-shell{display:block;width:100%;margin:0;border:0;box-shadow:none}.dossier-rail,.utility-bar,.filter-bar{display:none!important}.dossier-main{padding:0}.dossier-hero{color:#000;background:#fff;border:2px solid #000}.dossier-hero h1{color:#000}.source-disclosure{break-inside:avoid}.source-disclosure[open] blockquote,.source-disclosure:not([open])>blockquote{display:block}}
|
|
39
|
+
@media (prefers-reduced-motion: reduce){html{scroll-behavior:auto}*,*::before,*::after{transition-duration:.01ms!important;animation-duration:.01ms!important}}
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_JS = """\
|
|
44
|
+
(() => {
|
|
45
|
+
const all = (selector, root = document) => Array.from(root.querySelectorAll(selector));
|
|
46
|
+
const applyFilters = (scope) => {
|
|
47
|
+
const selected = scope.querySelector('[data-action="filter"][aria-pressed="true"]');
|
|
48
|
+
const category = selected?.dataset.filter || 'all';
|
|
49
|
+
const query = (scope.querySelector('[data-action="search"]')?.value || '').trim().toLowerCase();
|
|
50
|
+
all('[data-category]', scope).forEach((card) => {
|
|
51
|
+
const categoryMatch = category === 'all' || card.dataset.category === category;
|
|
52
|
+
const queryMatch = !query || card.textContent.toLowerCase().includes(query);
|
|
53
|
+
card.classList.toggle('is-filtered', !(categoryMatch && queryMatch));
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
document.addEventListener('click', (event) => {
|
|
57
|
+
const control = event.target.closest('[data-action]');
|
|
58
|
+
if (!control) return;
|
|
59
|
+
const action = control.dataset.action;
|
|
60
|
+
if (action === 'print') window.print();
|
|
61
|
+
if (action === 'filter') {
|
|
62
|
+
const scope = control.closest('[data-filter-scope]');
|
|
63
|
+
if (!scope) return;
|
|
64
|
+
all('[data-action="filter"]', scope).forEach((button) => {
|
|
65
|
+
button.setAttribute('aria-pressed', String(button === control));
|
|
66
|
+
});
|
|
67
|
+
applyFilters(scope);
|
|
68
|
+
}
|
|
69
|
+
if (action === 'sources') {
|
|
70
|
+
const open = control.getAttribute('aria-pressed') !== 'true';
|
|
71
|
+
all('.source-disclosure').forEach((item) => { item.open = open; });
|
|
72
|
+
control.setAttribute('aria-pressed', String(open));
|
|
73
|
+
control.textContent = open ? 'Collapse sources' : 'Expand sources';
|
|
74
|
+
}
|
|
75
|
+
if (action === 'facilitator-mode') {
|
|
76
|
+
const enabled = control.getAttribute('aria-pressed') !== 'true';
|
|
77
|
+
control.setAttribute('aria-pressed', String(enabled));
|
|
78
|
+
all('[data-facilitator]').forEach((item) => { item.hidden = !enabled; });
|
|
79
|
+
}
|
|
80
|
+
if (action === 'toggle-answer') {
|
|
81
|
+
const panel = document.getElementById(control.getAttribute('aria-controls'));
|
|
82
|
+
if (panel) {
|
|
83
|
+
panel.hidden = !panel.hidden;
|
|
84
|
+
control.setAttribute('aria-expanded', String(!panel.hidden));
|
|
85
|
+
control.textContent = panel.hidden ? 'Reveal answer' : 'Hide answer';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
document.addEventListener('input', (event) => {
|
|
90
|
+
if (event.target.matches('[data-action="search"]')) {
|
|
91
|
+
const scope = event.target.closest('[data-filter-scope]');
|
|
92
|
+
if (scope) applyFilters(scope);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
})();
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _esc(value: object) -> str:
|
|
100
|
+
return escape(str(value))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _badge_class(value: str) -> str:
|
|
104
|
+
return {
|
|
105
|
+
"efficacy": "badge-green",
|
|
106
|
+
"safety": "badge-red",
|
|
107
|
+
"dosing": "badge-blue",
|
|
108
|
+
"easy": "badge-green",
|
|
109
|
+
"medium": "badge-yellow",
|
|
110
|
+
"hard": "badge-red",
|
|
111
|
+
}.get(value.lower(), "badge-gray")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _source_html(source: dict) -> str:
|
|
115
|
+
return (
|
|
116
|
+
'<details class="source-disclosure">'
|
|
117
|
+
f"<summary>{_esc(source.get('document_name', ''))} / page or slide "
|
|
118
|
+
f"{_esc(source.get('page_number', '?'))}</summary>"
|
|
119
|
+
f"<blockquote>“{_esc(source.get('excerpt', ''))}”</blockquote>"
|
|
120
|
+
"</details>"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _sources_html(sources: list[dict]) -> str:
|
|
125
|
+
if not sources:
|
|
126
|
+
return ""
|
|
127
|
+
return f'<div class="source-stack">{"".join(_source_html(source) for source in sources)}</div>'
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _hero(title: str, eyebrow: str, facts: list[str]) -> str:
|
|
131
|
+
fact_html = "".join(f"<span>{_esc(fact)}</span>" for fact in facts)
|
|
132
|
+
return (
|
|
133
|
+
'<header class="dossier-hero" id="overview">'
|
|
134
|
+
f'<p class="eyebrow">{_esc(eyebrow)}</p><h1>{_esc(title)}</h1>'
|
|
135
|
+
f'<div class="hero-meta">{fact_html}</div></header>'
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _shell(
|
|
140
|
+
*,
|
|
141
|
+
title: str,
|
|
142
|
+
output_type: str,
|
|
143
|
+
body: str,
|
|
144
|
+
nav_items: list[tuple[str, str]],
|
|
145
|
+
extra_actions: str = "",
|
|
146
|
+
) -> str:
|
|
147
|
+
nav = "".join(f'<li><a href="#{_esc(anchor)}">{_esc(label)}</a></li>' for anchor, label in nav_items)
|
|
148
|
+
return (
|
|
149
|
+
'<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n'
|
|
150
|
+
'<meta name="viewport" content="width=device-width,initial-scale=1">\n'
|
|
151
|
+
'<meta name="color-scheme" content="light">\n'
|
|
152
|
+
'<meta http-equiv="Content-Security-Policy" '
|
|
153
|
+
"content=\"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:\">\n"
|
|
154
|
+
f"<title>{_esc(title)}</title>\n<style>{_CSS}</style>\n</head>\n"
|
|
155
|
+
f'<body data-output-type="{_esc(output_type.replace("_", "-"))}">\n'
|
|
156
|
+
'<a class="skip-link" href="#main-content">Skip to content</a>\n'
|
|
157
|
+
'<div class="dossier-shell"><aside class="dossier-rail">'
|
|
158
|
+
'<div class="rail-mark"><div><div class="rail-kicker">Open Pharma Plugins</div>'
|
|
159
|
+
'<div class="rail-title">Field evidence dossier</div></div></div>'
|
|
160
|
+
f'<nav class="dossier-nav" aria-label="Document sections"><ul>{nav}</ul></nav>'
|
|
161
|
+
'</aside><div class="dossier-workspace"><header class="utility-bar">'
|
|
162
|
+
'<span class="draft-state">Draft for MLR review</span><div class="utility-actions">'
|
|
163
|
+
'<button class="action-button" type="button" data-action="sources" aria-pressed="false">'
|
|
164
|
+
"Expand sources</button>"
|
|
165
|
+
f"{extra_actions}"
|
|
166
|
+
'<button class="action-button" type="button" data-action="print">Print / save PDF</button>'
|
|
167
|
+
"</div></header>"
|
|
168
|
+
f'<main class="dossier-main" id="main-content">{body}</main>'
|
|
169
|
+
f"</div></div><script>{_JS}</script></body></html>"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def render_learning_package(data: dict) -> str:
|
|
174
|
+
title = data.get("title", "Learning Package")
|
|
175
|
+
modules = data.get("modules", [])
|
|
176
|
+
source_documents = data.get("source_documents", [])
|
|
177
|
+
parts = [
|
|
178
|
+
_hero(
|
|
179
|
+
title,
|
|
180
|
+
"Approved-source learning package",
|
|
181
|
+
[
|
|
182
|
+
f"{len(modules)} module{'s' if len(modules) != 1 else ''}",
|
|
183
|
+
f"{len(source_documents)} approved source{'s' if len(source_documents) != 1 else ''}",
|
|
184
|
+
f"Generated {data.get('generated_at', '')}",
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
]
|
|
188
|
+
categories = sorted(
|
|
189
|
+
{
|
|
190
|
+
str(message.get("category", "other")).lower()
|
|
191
|
+
for module in modules
|
|
192
|
+
for message in module.get("key_messages", [])
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
for index, module in enumerate(modules, 1):
|
|
196
|
+
parts.append(f'<section class="section-block" data-filter-scope="module" id="module-{index}">')
|
|
197
|
+
parts.append(
|
|
198
|
+
'<div class="section-heading">'
|
|
199
|
+
f'<span class="section-index">{index:02d}</span><h2>{_esc(module.get("title", ""))}</h2></div>'
|
|
200
|
+
)
|
|
201
|
+
parts.append('<div class="module-meta">')
|
|
202
|
+
if module.get("product"):
|
|
203
|
+
parts.append(f"<span><strong>Product</strong> {_esc(module['product'])}</span>")
|
|
204
|
+
if module.get("therapeutic_area"):
|
|
205
|
+
parts.append(f"<span><strong>Therapeutic area</strong> {_esc(module['therapeutic_area'])}</span>")
|
|
206
|
+
parts.append("</div>")
|
|
207
|
+
|
|
208
|
+
objectives = module.get("objectives", [])
|
|
209
|
+
if objectives:
|
|
210
|
+
parts.append("<h3>Learning objectives</h3><ul>")
|
|
211
|
+
for objective in objectives:
|
|
212
|
+
parts.append(
|
|
213
|
+
f"<li>{_esc(objective.get('objective', ''))} "
|
|
214
|
+
f'<span class="badge badge-blue">{_esc(objective.get("bloom_level", ""))}</span></li>'
|
|
215
|
+
)
|
|
216
|
+
parts.append("</ul>")
|
|
217
|
+
|
|
218
|
+
messages = module.get("key_messages", [])
|
|
219
|
+
if messages:
|
|
220
|
+
parts.append('<h3>Key messages</h3><div class="filter-bar" aria-label="Filter key messages">')
|
|
221
|
+
parts.append(
|
|
222
|
+
'<input class="filter-search" type="search" data-action="search" '
|
|
223
|
+
'aria-label="Search key messages" placeholder="Search messages">'
|
|
224
|
+
)
|
|
225
|
+
parts.append(
|
|
226
|
+
'<button class="filter-button" type="button" data-action="filter" '
|
|
227
|
+
'data-filter="all" aria-pressed="true">All</button>'
|
|
228
|
+
)
|
|
229
|
+
for category in categories:
|
|
230
|
+
parts.append(
|
|
231
|
+
'<button class="filter-button" type="button" data-action="filter" '
|
|
232
|
+
f'data-filter="{_esc(category)}" aria-pressed="false">{_esc(category)}</button>'
|
|
233
|
+
)
|
|
234
|
+
parts.append('</div><div class="card-grid">')
|
|
235
|
+
for message in messages:
|
|
236
|
+
category = str(message.get("category", "other")).lower()
|
|
237
|
+
parts.append(f'<article class="card" data-category="{_esc(category)}">')
|
|
238
|
+
parts.append(f'<span class="badge {_badge_class(category)}">{_esc(category)}</span>')
|
|
239
|
+
parts.append(f"<p>{_esc(message.get('message', ''))}</p>")
|
|
240
|
+
parts.append(_sources_html(message.get("sources", [])))
|
|
241
|
+
parts.append("</article>")
|
|
242
|
+
parts.append("</div>")
|
|
243
|
+
|
|
244
|
+
talking_points = module.get("talking_points", [])
|
|
245
|
+
if talking_points:
|
|
246
|
+
parts.append('<h3>Talking points</h3><div class="card-grid">')
|
|
247
|
+
for point in talking_points:
|
|
248
|
+
parts.append('<article class="card">')
|
|
249
|
+
parts.append(f'<div class="card-header">{_esc(point.get("situation", ""))}</div>')
|
|
250
|
+
parts.append(f"<p><strong>Approved response</strong><br>{_esc(point.get('approved_response', ''))}</p>")
|
|
251
|
+
if point.get("supporting_data"):
|
|
252
|
+
parts.append(f"<p><strong>Supporting data</strong><br>{_esc(point['supporting_data'])}</p>")
|
|
253
|
+
parts.append(_sources_html(point.get("sources", [])))
|
|
254
|
+
parts.append("</article>")
|
|
255
|
+
parts.append("</div>")
|
|
256
|
+
|
|
257
|
+
objections = module.get("common_objections", [])
|
|
258
|
+
if objections:
|
|
259
|
+
parts.append('<h3>Objection handling</h3><div class="card-grid">')
|
|
260
|
+
for objection in objections:
|
|
261
|
+
parts.append('<article class="card">')
|
|
262
|
+
parts.append(f'<div class="card-header">{_esc(objection.get("objection", ""))}</div>')
|
|
263
|
+
parts.append(
|
|
264
|
+
f"<p><strong>Approved response</strong><br>{_esc(objection.get('approved_response', ''))}</p>"
|
|
265
|
+
)
|
|
266
|
+
parts.append(_sources_html(objection.get("sources", [])))
|
|
267
|
+
parts.append("</article>")
|
|
268
|
+
parts.append("</div>")
|
|
269
|
+
parts.append("</section>")
|
|
270
|
+
|
|
271
|
+
if source_documents:
|
|
272
|
+
parts.append('<section class="section-block" id="sources"><h2>Source ledger</h2><ul>')
|
|
273
|
+
parts.extend(f"<li>{_esc(document)}</li>" for document in source_documents)
|
|
274
|
+
parts.append("</ul></section>")
|
|
275
|
+
parts.append(f'<div class="meta">Generated: {_esc(data.get("generated_at", ""))}</div>')
|
|
276
|
+
|
|
277
|
+
nav_items = [("overview", "Overview")]
|
|
278
|
+
nav_items.extend(
|
|
279
|
+
(f"module-{index}", module.get("title", f"Module {index}")) for index, module in enumerate(modules, 1)
|
|
280
|
+
)
|
|
281
|
+
if source_documents:
|
|
282
|
+
nav_items.append(("sources", "Source ledger"))
|
|
283
|
+
return _shell(
|
|
284
|
+
title=title,
|
|
285
|
+
output_type="learning_package",
|
|
286
|
+
body="".join(parts),
|
|
287
|
+
nav_items=nav_items,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def render_roleplay_kit(data: dict) -> str:
|
|
292
|
+
title = data.get("title", "Role-Play Kit")
|
|
293
|
+
parts = [
|
|
294
|
+
_hero(
|
|
295
|
+
title,
|
|
296
|
+
"Facilitated practice dossier",
|
|
297
|
+
[data.get("topic", ""), data.get("hcp_persona", ""), f"Generated {data.get('generated_at', '')}"],
|
|
298
|
+
),
|
|
299
|
+
'<section class="section-block" id="scenario"><h2>Practice scenario</h2>',
|
|
300
|
+
f"<p>{_esc(data.get('scenario', ''))}</p><p><strong>HCP persona</strong><br>"
|
|
301
|
+
f"{_esc(data.get('hcp_persona', ''))}</p></section>",
|
|
302
|
+
]
|
|
303
|
+
objectives = data.get("objectives", [])
|
|
304
|
+
if objectives:
|
|
305
|
+
parts.append('<section class="section-block" id="objectives"><h2>Session objectives</h2><ul>')
|
|
306
|
+
for objective in objectives:
|
|
307
|
+
parts.append(f"<li>{_esc(objective.get('objective', ''))}</li>")
|
|
308
|
+
parts.append("</ul></section>")
|
|
309
|
+
messages = data.get("key_messages", [])
|
|
310
|
+
if messages:
|
|
311
|
+
parts.append('<section class="section-block" id="messages"><h2>Approved message bank</h2>')
|
|
312
|
+
parts.append('<div class="card-grid">')
|
|
313
|
+
for message in messages:
|
|
314
|
+
category = str(message.get("category", "other")).lower()
|
|
315
|
+
parts.append(f'<article class="card" data-category="{_esc(category)}">')
|
|
316
|
+
parts.append(f'<span class="badge {_badge_class(category)}">{_esc(category)}</span>')
|
|
317
|
+
parts.append(f"<p>{_esc(message.get('message', ''))}</p>")
|
|
318
|
+
parts.append(_sources_html(message.get("sources", [])))
|
|
319
|
+
parts.append("</article>")
|
|
320
|
+
parts.append("</div></section>")
|
|
321
|
+
objections = data.get("common_objections", [])
|
|
322
|
+
if objections:
|
|
323
|
+
parts.append('<section class="section-block" id="objections"><h2>Objection practice</h2>')
|
|
324
|
+
parts.append('<div class="card-grid">')
|
|
325
|
+
for objection in objections:
|
|
326
|
+
parts.append('<article class="card">')
|
|
327
|
+
parts.append(f'<div class="card-header">{_esc(objection.get("objection", ""))}</div>')
|
|
328
|
+
parts.append(
|
|
329
|
+
'<div class="facilitator-note" data-facilitator hidden><strong>Approved response</strong><br>'
|
|
330
|
+
f"{_esc(objection.get('approved_response', ''))}"
|
|
331
|
+
f"{_sources_html(objection.get('sources', []))}</div>"
|
|
332
|
+
)
|
|
333
|
+
parts.append("</article>")
|
|
334
|
+
parts.append("</div></section>")
|
|
335
|
+
prompts = data.get("facilitator_prompts", [])
|
|
336
|
+
if prompts:
|
|
337
|
+
parts.append('<section class="section-block" id="facilitation"><h2>Facilitator run of show</h2>')
|
|
338
|
+
for prompt in prompts:
|
|
339
|
+
parts.append('<article class="card">')
|
|
340
|
+
parts.append(f'<span class="badge badge-blue">{_esc(prompt.get("stage", ""))}</span>')
|
|
341
|
+
parts.append(f"<p>{_esc(prompt.get('prompt', ''))}</p>")
|
|
342
|
+
parts.append(
|
|
343
|
+
f'<div class="facilitator-note" data-facilitator hidden>{_esc(prompt.get("coaching_intent", ""))}</div>'
|
|
344
|
+
)
|
|
345
|
+
parts.append("</article>")
|
|
346
|
+
parts.append("</section>")
|
|
347
|
+
rubric = data.get("evaluation_rubric", [])
|
|
348
|
+
if rubric:
|
|
349
|
+
parts.append('<section class="section-block" id="rubric"><h2>Evaluation rubric</h2>')
|
|
350
|
+
for criterion in rubric:
|
|
351
|
+
parts.append('<article class="card">')
|
|
352
|
+
parts.append(
|
|
353
|
+
f'<div class="card-header">{_esc(criterion.get("criterion", ""))} '
|
|
354
|
+
f'<span class="rubric-weight">{_esc(criterion.get("weight_pct", 0))}%</span></div><ul>'
|
|
355
|
+
)
|
|
356
|
+
parts.extend(f"<li>{_esc(item)}</li>" for item in criterion.get("evidence_to_observe", []))
|
|
357
|
+
parts.append("</ul></article>")
|
|
358
|
+
parts.append("</section>")
|
|
359
|
+
parts.append(f'<div class="meta">Generated: {_esc(data.get("generated_at", ""))}</div>')
|
|
360
|
+
facilitator_action = (
|
|
361
|
+
'<button class="action-button" type="button" data-action="facilitator-mode" '
|
|
362
|
+
'aria-pressed="false">Facilitator mode</button>'
|
|
363
|
+
)
|
|
364
|
+
return _shell(
|
|
365
|
+
title=title,
|
|
366
|
+
output_type="roleplay_kit",
|
|
367
|
+
body="".join(parts),
|
|
368
|
+
nav_items=[
|
|
369
|
+
("overview", "Overview"),
|
|
370
|
+
("scenario", "Scenario"),
|
|
371
|
+
("objectives", "Objectives"),
|
|
372
|
+
("messages", "Message bank"),
|
|
373
|
+
("objections", "Objections"),
|
|
374
|
+
("facilitation", "Run of show"),
|
|
375
|
+
("rubric", "Rubric"),
|
|
376
|
+
],
|
|
377
|
+
extra_actions=facilitator_action,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def render_assessment(data: dict) -> str:
|
|
382
|
+
title = data.get("title", "Assessment")
|
|
383
|
+
questions = data.get("mcq_questions", [])
|
|
384
|
+
scenarios = data.get("scenario_questions", [])
|
|
385
|
+
parts = [
|
|
386
|
+
_hero(
|
|
387
|
+
title,
|
|
388
|
+
"Knowledge and application check",
|
|
389
|
+
[
|
|
390
|
+
f"{len(questions)} multiple-choice question{'s' if len(questions) != 1 else ''}",
|
|
391
|
+
f"{len(scenarios)} scenario{'s' if len(scenarios) != 1 else ''}",
|
|
392
|
+
f"Passing score {int(data.get('passing_score_pct', 0.8) * 100)}%",
|
|
393
|
+
],
|
|
394
|
+
)
|
|
395
|
+
]
|
|
396
|
+
if questions:
|
|
397
|
+
parts.append('<section class="section-block" id="questions"><h2>Knowledge check</h2>')
|
|
398
|
+
for index, question in enumerate(questions, 1):
|
|
399
|
+
question_id = str(question.get("question_id") or f"question-{index}")
|
|
400
|
+
safe_id = "".join(
|
|
401
|
+
character if character.isalnum() or character in "-_" else "-" for character in question_id
|
|
402
|
+
)
|
|
403
|
+
answer_id = f"answer-{safe_id}"
|
|
404
|
+
difficulty = str(question.get("difficulty", ""))
|
|
405
|
+
parts.append('<article class="card">')
|
|
406
|
+
parts.append(
|
|
407
|
+
f'<div class="card-header">Question {index} '
|
|
408
|
+
f'<span class="badge {_badge_class(difficulty)}">{_esc(difficulty)}</span></div>'
|
|
409
|
+
f'<p>{_esc(question.get("question", ""))}</p><ol type="A">'
|
|
410
|
+
)
|
|
411
|
+
for option in question.get("options", []):
|
|
412
|
+
parts.append(f"<li>{_esc(option.get('text', ''))}</li>")
|
|
413
|
+
parts.append("</ol>")
|
|
414
|
+
parts.append(
|
|
415
|
+
'<button class="action-button" type="button" data-action="toggle-answer" '
|
|
416
|
+
f'aria-controls="{_esc(answer_id)}" aria-expanded="false">Reveal answer</button>'
|
|
417
|
+
)
|
|
418
|
+
parts.append(f'<div id="{_esc(answer_id)}" class="answer-panel" hidden>')
|
|
419
|
+
parts.append(f"<p><strong>Correct answer: {_esc(question.get('correct_answer', ''))}</strong></p>")
|
|
420
|
+
parts.append(f"<p>{_esc(question.get('explanation', ''))}</p>")
|
|
421
|
+
if question.get("source"):
|
|
422
|
+
parts.append(_source_html(question["source"]))
|
|
423
|
+
parts.append("</div></article>")
|
|
424
|
+
parts.append("</section>")
|
|
425
|
+
if scenarios:
|
|
426
|
+
parts.append('<section class="section-block" id="scenarios"><h2>Scenario practice</h2>')
|
|
427
|
+
for index, scenario in enumerate(scenarios, 1):
|
|
428
|
+
answer_id = f"scenario-answer-{index}"
|
|
429
|
+
difficulty = str(scenario.get("difficulty", ""))
|
|
430
|
+
parts.append('<article class="card">')
|
|
431
|
+
parts.append(
|
|
432
|
+
f'<div class="card-header">Scenario {index} '
|
|
433
|
+
f'<span class="badge {_badge_class(difficulty)}">{_esc(difficulty)}</span></div>'
|
|
434
|
+
f"<p>{_esc(scenario.get('scenario', ''))}</p>"
|
|
435
|
+
f"<p><strong>Persona</strong><br>{_esc(scenario.get('hcp_persona', ''))}</p>"
|
|
436
|
+
)
|
|
437
|
+
parts.append(
|
|
438
|
+
'<button class="action-button" type="button" data-action="toggle-answer" '
|
|
439
|
+
f'aria-controls="{answer_id}" aria-expanded="false">Reveal answer</button>'
|
|
440
|
+
)
|
|
441
|
+
parts.append(f'<div id="{answer_id}" class="answer-panel" hidden><ul>')
|
|
442
|
+
parts.extend(f"<li>{_esc(point)}</li>" for point in scenario.get("ideal_response_points", []))
|
|
443
|
+
parts.append(f"</ul>{_sources_html(scenario.get('sources', []))}</div></article>")
|
|
444
|
+
parts.append("</section>")
|
|
445
|
+
source_documents = data.get("source_documents", [])
|
|
446
|
+
if source_documents:
|
|
447
|
+
parts.append('<section class="section-block" id="sources"><h2>Source ledger</h2><ul>')
|
|
448
|
+
parts.extend(f"<li>{_esc(document)}</li>" for document in source_documents)
|
|
449
|
+
parts.append("</ul></section>")
|
|
450
|
+
parts.append(f'<div class="meta">Generated: {_esc(data.get("generated_at", ""))}</div>')
|
|
451
|
+
nav_items = [("overview", "Overview")]
|
|
452
|
+
if questions:
|
|
453
|
+
nav_items.append(("questions", "Knowledge check"))
|
|
454
|
+
if scenarios:
|
|
455
|
+
nav_items.append(("scenarios", "Scenarios"))
|
|
456
|
+
if source_documents:
|
|
457
|
+
nav_items.append(("sources", "Source ledger"))
|
|
458
|
+
return _shell(
|
|
459
|
+
title=title,
|
|
460
|
+
output_type="assessment",
|
|
461
|
+
body="".join(parts),
|
|
462
|
+
nav_items=nav_items,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def render_roleplay_scorecard(data: dict) -> str:
|
|
467
|
+
topic = data.get("topic", "Role-Play")
|
|
468
|
+
score = float(data.get("score", 0.0))
|
|
469
|
+
score_pct = int(score * 100)
|
|
470
|
+
score_label = "Ready" if score >= 0.8 else "Developing" if score >= 0.6 else "Needs coaching"
|
|
471
|
+
parts = [
|
|
472
|
+
_hero(
|
|
473
|
+
f"Role-play scorecard: {topic}",
|
|
474
|
+
"Observed conversation review",
|
|
475
|
+
[f"Score {score_pct}%", score_label, data.get("hcp_persona", "")],
|
|
476
|
+
)
|
|
477
|
+
]
|
|
478
|
+
turns = data.get("turns", [])
|
|
479
|
+
if turns:
|
|
480
|
+
parts.append('<section class="section-block" id="transcript"><h2>Conversation transcript</h2>')
|
|
481
|
+
for turn in turns:
|
|
482
|
+
speaker = str(turn.get("speaker", "rep")).lower()
|
|
483
|
+
speaker_label = "HCP" if speaker == "hcp" else "Representative"
|
|
484
|
+
bubble_class = "bubble-hcp" if speaker == "hcp" else "bubble-rep"
|
|
485
|
+
parts.append(
|
|
486
|
+
'<div class="bubble-row">'
|
|
487
|
+
f'<div class="bubble {bubble_class}"><div class="bubble-label">{speaker_label}</div>'
|
|
488
|
+
f"{_esc(turn.get('message', ''))}</div></div>"
|
|
489
|
+
)
|
|
490
|
+
parts.append("</section>")
|
|
491
|
+
claims = data.get("claims_evaluated", [])
|
|
492
|
+
if claims:
|
|
493
|
+
parts.append('<section class="section-block" id="claims"><h2>Claims evaluation</h2>')
|
|
494
|
+
parts.append('<div class="card-grid">')
|
|
495
|
+
for claim in claims:
|
|
496
|
+
status = str(claim.get("status", ""))
|
|
497
|
+
parts.append('<article class="card">')
|
|
498
|
+
parts.append(f'<span class="badge {_badge_class(status)}">{_esc(status)}</span>')
|
|
499
|
+
parts.append(f"<p><strong>{_esc(claim.get('claim', ''))}</strong></p>")
|
|
500
|
+
parts.append(f"<p>{_esc(claim.get('feedback', ''))}</p>")
|
|
501
|
+
if claim.get("source"):
|
|
502
|
+
parts.append(_source_html(claim["source"]))
|
|
503
|
+
parts.append("</article>")
|
|
504
|
+
parts.append("</div></section>")
|
|
505
|
+
strengths = data.get("strengths", [])
|
|
506
|
+
improvements = data.get("areas_for_improvement", [])
|
|
507
|
+
if strengths or improvements:
|
|
508
|
+
parts.append('<section class="section-block" id="coaching"><h2>Coaching debrief</h2>')
|
|
509
|
+
parts.append('<div class="card-grid">')
|
|
510
|
+
if strengths:
|
|
511
|
+
parts.append('<article class="card"><div class="card-header">Strengths</div><ul>')
|
|
512
|
+
parts.extend(f"<li>{_esc(item)}</li>" for item in strengths)
|
|
513
|
+
parts.append("</ul></article>")
|
|
514
|
+
if improvements:
|
|
515
|
+
parts.append('<article class="card"><div class="card-header">Focus next</div><ul>')
|
|
516
|
+
parts.extend(f"<li>{_esc(item)}</li>" for item in improvements)
|
|
517
|
+
parts.append("</ul></article>")
|
|
518
|
+
parts.append("</div></section>")
|
|
519
|
+
source_documents = data.get("source_documents", [])
|
|
520
|
+
if source_documents:
|
|
521
|
+
parts.append('<section class="section-block" id="sources"><h2>Source ledger</h2><ul>')
|
|
522
|
+
parts.extend(f"<li>{_esc(document)}</li>" for document in source_documents)
|
|
523
|
+
parts.append("</ul></section>")
|
|
524
|
+
nav_items = [("overview", "Overview")]
|
|
525
|
+
if turns:
|
|
526
|
+
nav_items.append(("transcript", "Transcript"))
|
|
527
|
+
if claims:
|
|
528
|
+
nav_items.append(("claims", "Claims"))
|
|
529
|
+
if strengths or improvements:
|
|
530
|
+
nav_items.append(("coaching", "Coaching"))
|
|
531
|
+
if source_documents:
|
|
532
|
+
nav_items.append(("sources", "Source ledger"))
|
|
533
|
+
return _shell(
|
|
534
|
+
title=f"Role-play scorecard: {topic}",
|
|
535
|
+
output_type="roleplay_scorecard",
|
|
536
|
+
body="".join(parts),
|
|
537
|
+
nav_items=nav_items,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
RENDERERS = {
|
|
542
|
+
"learning_package": render_learning_package,
|
|
543
|
+
"assessment": render_assessment,
|
|
544
|
+
"roleplay_kit": render_roleplay_kit,
|
|
545
|
+
"roleplay_scorecard": render_roleplay_scorecard,
|
|
546
|
+
}
|