from IPython.display import display, HTML
import json
class FunctionalGroupInteractive:
def __init__(self):
self.functional_groups = [
{'name': 'Alcohol (-OH)', 'hbd': True, 'hba': True},
{'name': 'Primary Amine (-NH₂)', 'hbd': True, 'hba': True},
{'name': 'Carbonyl (C=O)', 'hbd': False, 'hba': True},
{'name': 'Ether (-O-)', 'hbd': False, 'hba': True},
{'name': 'Carboxylic Acid (-COOH)', 'hbd': True, 'hba': True},
{'name': 'Amide (-CONH-)', 'hbd': True, 'hba': True},
{'name': 'Tertiary Amine (R₃N)', 'hbd': False, 'hba': True},
{'name': 'Ester (-COO-)', 'hbd': False, 'hba': True},
{'name': 'Phenol (Ar-OH)', 'hbd': True, 'hba': True},
{'name': 'Pyridine N', 'hbd': False, 'hba': True},
{'name': 'Nitrile (-C≡N)', 'hbd': False, 'hba': True},
{'name': 'Methyl (CH₃-)', 'hbd': False, 'hba': False},
]
def create_interface(self):
correct_answers_json = json.dumps({fg['name']: {'hbd': fg['hbd'], 'hba': fg['hba']} for fg in self.functional_groups})
html_code = f'''
<!DOCTYPE html>
<html>
<head>
<style>
.quiz-container {{
font-family: 'Segoe UI', Arial, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border: 1px solid #ddd;
}}
.header {{
text-align: center;
margin-bottom: 25px;
}}
.header h1 {{
color: #2c3e50;
margin: 0;
font-size: 24px;
}}
.header p {{
color: #7f8c8d;
margin: 5px 0;
font-size: 14px;
}}
.instructions {{
background: #e3f2fd;
border-left: 4px solid #2196F3;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}}
.instructions h4 {{
margin: 0 0 10px 0;
color: #1565c0;
}}
.instructions ol {{
margin: 5px 0;
padding-left: 20px;
color: #666;
font-size: 13px;
}}
.instructions li {{
margin: 5px 0;
}}
.instructions em {{
color: #e74c3c;
font-weight: 600;
}}
.groups-grid {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 10px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
border: 2px solid #e0e0e0;
margin-bottom: 20px;
}}
.functional-group {{
background: white;
border: 2px solid #ddd;
border-radius: 8px;
padding: 12px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
font-weight: 600;
position: relative;
user-select: none;
}}
.functional-group:hover {{
border-color: #2196F3;
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.2);
}}
.functional-group.selected {{
background-color: #fff9c4;
border-color: #ffc107;
box-shadow: 0 0 0 4px rgba(255, 193, 7, 0.3);
transform: scale(1.05);
z-index: 10;
}}
.drop-zones {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}}
.drop-zone {{
border: 3px dashed #ccc;
border-radius: 10px;
min-height: 250px;
padding: 15px;
background: white;
transition: all 0.3s;
cursor: pointer;
position: relative;
}}
.drop-zone:hover {{
border-color: #999;
background: #fafafa;
}}
.drop-zone.ready-to-receive {{
border-color: #4CAF50;
background: #e8f5e9;
border-style: solid;
animation: pulse 1s infinite;
}}
@keyframes pulse {{
0%, 100% {{ transform: scale(1); }}
50% {{ transform: scale(1.01); }}
}}
.drop-zone-header {{
text-align: center;
padding: 10px;
color: white;
border-radius: 6px;
margin-bottom: 15px;
}}
.hbd-header {{
background: #2196F3;
}}
.hba-header {{
background: #9C27B0;
}}
.dropped-item {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 12px;
margin: 5px 0;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s;
}}
.dropped-item:hover {{
opacity: 0.9;
transform: translateX(-2px);
}}
.button-row {{
display: flex;
gap: 10px;
margin-top: 15px;
}}
.button-row button {{
flex: 1;
}}
.btn {{
padding: 10px 20px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
}}
.btn-check {{
background: #2196F3;
color: white;
}}
.btn-check:hover {{
background: #1976D2;
}}
.btn-reset {{
background: #e74c3c;
color: white;
}}
.btn-reset:hover {{
background: #c0392b;
}}
.feedback {{
margin-top: 20px;
padding: 15px;
border-radius: 8px;
background: #f8f9fa;
display: none;
border: 1px solid #ddd;
}}
.feedback.show {{
display: block;
}}
.feedback h4 {{
margin-top: 0;
color: #2c3e50;
}}
.result-box {{
margin: 10px 0;
}}
.correct-mark {{
color: #27ae60;
font-weight: bold;
}}
.incorrect-mark {{
color: #e74c3c;
font-weight: bold;
}}
.info-box {{
margin-top: 20px;
font-size: 12px;
color: #666;
background: #f0f0f0;
padding: 12px;
border-radius: 6px;
}}
.info-box strong {{
color: #2c3e50;
}}
</style>
</head>
<body>
<div class="quiz-container">
<div class="header">
<h1>Functional Groups: H-Bond Donors & Acceptors</h1>
<p>Identify which functional groups can donate or accept hydrogen bonds</p>
</div>
<div class="instructions">
<h4>📋 Instructions:</h4>
<ol>
<li><strong>Click a functional group</strong> to select it (it will turn yellow)</li>
<li><strong>Click a box</strong> (H-Bond Donors or H-Bond Acceptors) to place it there</li>
<li><em>Important: Some groups belong in BOTH boxes!</em></li>
<li>Click on placed items to remove them</li>
<li>Press "Check Answer" when done</li>
</ol>
</div>
<div class="groups-grid" id="sourceGrid">
{self._generate_groups_html()}
</div>
<div class="drop-zones">
<div class="drop-zone" id="hbdZone" data-zone-type="hbd">
<div class="drop-zone-header hbd-header">
<strong>H-Bond DONORS</strong><br>
<small>(H on N/O/F)</small>
</div>
<div id="hbdItems"></div>
</div>
<div class="drop-zone" id="hbaZone" data-zone-type="hba">
<div class="drop-zone-header hba-header">
<strong>H-Bond ACCEPTORS</strong><br>
<small>(Lone pairs on N/O/F)</small>
</div>
<div id="hbaItems"></div>
</div>
</div>
<div class="button-row">
<button class="btn btn-check" id="checkBtn">✓ Check Answer</button>
<button class="btn btn-reset" id="resetBtn">🔄 Reset</button>
</div>
<div class="feedback" id="feedback"></div>
<div class="info-box">
<strong>ℹ️ About Hydrogen Bonding:</strong> Hydrogen bonds are crucial for drug-receptor interactions.
<strong>Donors</strong> have H attached to N, O, or F. <strong>Acceptors</strong> have lone pairs on N, O, or F.
Many functional groups can do both (e.g., -OH, -NH₂).
</div>
</div>
<script>
(function() {{
// --- STATE MANAGEMENT ---
const correctAnswers = {correct_answers_json};
let currentlySelectedName = null;
let currentlySelectedElement = null;
// --- SELECTION LOGIC ---
function selectGroup(name, element) {{
// Remove highlighting from previous selection
if (currentlySelectedElement) {{
currentlySelectedElement.classList.remove('selected');
}}
// If clicking the same one, deselect it
if (currentlySelectedName === name) {{
currentlySelectedName = null;
currentlySelectedElement = null;
updateZoneVisuals(false);
return;
}}
// Select new one
currentlySelectedName = name;
currentlySelectedElement = element;
element.classList.add('selected');
updateZoneVisuals(true);
}}
function updateZoneVisuals(active) {{
const zones = document.querySelectorAll('.drop-zone');
zones.forEach(z => {{
if (active) {{
z.classList.add('ready-to-receive');
}} else {{
z.classList.remove('ready-to-receive');
}}
}});
}}
// --- PLACEMENT LOGIC ---
function handleZoneClick(zoneType) {{
if (!currentlySelectedName) return;
addItemToZone(currentlySelectedName, zoneType);
// Deselect after placing
if (currentlySelectedElement) {{
currentlySelectedElement.classList.remove('selected');
}}
currentlySelectedName = null;
currentlySelectedElement = null;
updateZoneVisuals(false);
}}
function addItemToZone(name, zoneType) {{
const containerId = zoneType === 'hbd' ? 'hbdItems' : 'hbaItems';
const container = document.getElementById(containerId);
// Check for duplicates
const existingItems = Array.from(container.children);
if (existingItems.some(el => el.dataset.name === name)) {{
return;
}}
const div = document.createElement('div');
div.className = 'dropped-item';
div.dataset.name = name;
div.innerHTML = '<span>' + name + '</span> <span>✕</span>';
div.onclick = function(e) {{
e.stopPropagation();
div.remove();
}};
container.appendChild(div);
}}
// --- SETUP CLICK HANDLERS ---
function setupClickHandlers() {{
// Functional group clicks
const groups = document.querySelectorAll('.functional-group');
groups.forEach(group => {{
group.addEventListener('click', function(e) {{
e.stopPropagation();
const name = this.dataset.name;
selectGroup(name, this);
}});
}});
// Zone clicks
const zones = document.querySelectorAll('.drop-zone');
zones.forEach(zone => {{
zone.addEventListener('click', function(e) {{
// Only handle if clicking the zone itself, not a dropped item
if (e.target.classList.contains('drop-zone') ||
e.target.classList.contains('drop-zone-header') ||
e.target.closest('.drop-zone-header')) {{
const zoneType = this.dataset.zoneType;
handleZoneClick(zoneType);
}}
}});
}});
// Check button
document.getElementById('checkBtn').addEventListener('click', checkAnswer);
// Reset button
document.getElementById('resetBtn').addEventListener('click', resetQuiz);
}}
// --- CHECKING LOGIC ---
function checkAnswer() {{
const hbdItems = Array.from(document.getElementById('hbdItems').children).map(el => el.dataset.name);
const hbaItems = Array.from(document.getElementById('hbaItems').children).map(el => el.dataset.name);
let html = '<h4>Results</h4>';
function checkZone(userList, propName, label) {{
let correctList = Object.keys(correctAnswers).filter(k => correctAnswers[k][propName]);
let zoneHtml = '<div class="result-box"><strong>' + label + ':</strong><br>';
if (userList.length === 0) {{
zoneHtml += '<span style="color:#999; font-style:italic;">Empty - add some groups!</span><br>';
}}
userList.forEach(item => {{
if (correctAnswers[item] && correctAnswers[item][propName]) {{
zoneHtml += '<span class="correct-mark">✓ ' + item + '</span><br>';
}} else {{
zoneHtml += '<span class="incorrect-mark">✗ ' + item + ' (Should not be here)</span><br>';
}}
}});
// Check for missing
let missing = correctList.filter(x => !userList.includes(x));
if (missing.length > 0) {{
zoneHtml += '<span style="color:#e74c3c; font-size:0.9em;">❌ Missing: ' + missing.join(', ') + '</span>';
}} else if (userList.length > 0) {{
zoneHtml += '<span style="color:#27ae60; font-size:0.9em;">✅ Complete!</span>';
}}
zoneHtml += '</div><hr>';
return zoneHtml;
}}
html += checkZone(hbdItems, 'hbd', 'H-Bond Donors');
html += checkZone(hbaItems, 'hba', 'H-Bond Acceptors');
// Check if perfect score
let hbdCorrect = Object.keys(correctAnswers).filter(k => correctAnswers[k]['hbd']);
let hbaCorrect = Object.keys(correctAnswers).filter(k => correctAnswers[k]['hba']);
let hbdPerfect = hbdCorrect.every(x => hbdItems.includes(x)) && hbdItems.every(x => hbdCorrect.includes(x));
let hbaPerfect = hbaCorrect.every(x => hbaItems.includes(x)) && hbaItems.every(x => hbaCorrect.includes(x));
if (hbdPerfect && hbaPerfect) {{
html += '<div style="background:#d4edda; color:#155724; padding:12px; border-radius:6px; text-align:center; margin-top:10px; border:1px solid #c3e6cb;"><strong>🎉 Perfect!</strong> You got them all correct!</div>';
}}
const fb = document.getElementById('feedback');
fb.innerHTML = html;
fb.className = 'feedback show';
fb.scrollIntoView({{behavior: 'smooth', block: 'nearest'}});
}}
function resetQuiz() {{
document.getElementById('hbdItems').innerHTML = '';
document.getElementById('hbaItems').innerHTML = '';
document.getElementById('feedback').className = 'feedback';
currentlySelectedName = null;
if (currentlySelectedElement) {{
currentlySelectedElement.classList.remove('selected');
}}
currentlySelectedElement = null;
updateZoneVisuals(false);
}}
// --- INITIALIZE ---
setTimeout(function() {{
setupClickHandlers();
}}, 100);
}})();
</script>
</body>
</html>
'''
display(HTML(html_code))
def _generate_groups_html(self):
html = ''
for fg in self.functional_groups:
name = fg['name']
html += f'<div class="functional-group" data-name="{name}">{name}</div>'
return html
# Run the quiz
quiz = FunctionalGroupInteractive()
quiz.create_interface()