161161 font-weight : 600 ;
162162 }
163163
164- .modal-header .close-btn {
165- background : none;
166- border : none;
167- font-size : 1.5rem ;
168- cursor : pointer;
169- padding : 0 0.5rem ;
170- line-height : 1 ;
171- color : var (--text );
172- }
164+ .modal-header .close-btn {
165+ background : none;
166+ border : none;
167+ font-size : 1.5rem ;
168+ cursor : pointer;
169+ padding : 0 0.5rem ;
170+ line-height : 1 ;
171+ color : var (--text );
172+ }
173+
174+ .modal-header .action-btn {
175+ font-size : 0.85rem ;
176+ }
173177
174178 .modal-header .copy-btn {
175179 background : var (--accent );
280284 border-color : # 22c55e ;
281285 }
282286
287+ # historyList > div {
288+ position : relative;
289+ }
290+
283291 # historyList > div : hover {
284292 background : # f8fafc ;
285293 border-color : var (--accent );
286294 transform : translateY (-1px );
287295 box-shadow : 0 2px 4px rgba (0 , 0 , 0 , 0.1 );
288296 }
289297
298+ .history-item-delete {
299+ position : absolute;
300+ top : 0.5rem ;
301+ right : 0.5rem ;
302+ background : none;
303+ border : none;
304+ cursor : pointer;
305+ padding : 0.25rem ;
306+ border-radius : 4px ;
307+ opacity : 0 ;
308+ transition : opacity 0.15s ease;
309+ color : var (--muted );
310+ }
311+
312+ # historyList > div : hover .history-item-delete {
313+ opacity : 1 ;
314+ }
315+
316+ .history-item-delete : hover {
317+ background : # fee2e2 ;
318+ color : # dc2626 ;
319+ }
320+
321+ # historyList > div > div {
322+ padding-right : 1.5rem ;
323+ }
324+
290325 .action-buttons {
291326 display : flex;
292327 gap : 0.75rem ;
355390 border-color : # 16a34a ;
356391 }
357392
393+ .action-btn .export {
394+ background : linear-gradient (135deg , # fef9c3 0% , # fef08a 100% );
395+ border-color : # eab308 ;
396+ color : # ca8a04 ;
397+ }
398+
399+ .action-btn .export : hover {
400+ background : linear-gradient (135deg , # fef08a 0% , # fde047 100% );
401+ border-color : # ca8a04 ;
402+ }
403+
358404 .drop {
359405 margin-top : 0.75rem ;
360406 border : 1px dashed var (--border );
@@ -1224,6 +1270,12 @@ <h1>Molecule Clipboard</h1>
12241270 } else {
12251271 historyList . innerHTML = compoundHistory . map ( ( compound , index ) => `
12261272 <div style="border: 1px solid var(--border); border-radius: 8px; padding: 1rem; cursor: pointer; transition: all 0.15s ease;" onclick="loadFromHistory(${ index } )">
1273+ <button class="history-item-delete" onclick="event.stopPropagation(); deleteFromHistory(${ index } )" title="Delete from history">
1274+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
1275+ <line x1="18" y1="6" x2="6" y2="18"/>
1276+ <line x1="6" y1="6" x2="18" y2="18"/>
1277+ </svg>
1278+ </button>
12271279 <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
12281280 <div>
12291281 <strong style="display: block; margin-bottom: 0.25rem;">${ compound . name } </strong>
@@ -1244,6 +1296,156 @@ <h1>Molecule Clipboard</h1>
12441296 document . getElementById ( 'historyModal' ) . style . display = 'none' ;
12451297 }
12461298
1299+ async function exportHistoryToCSV ( ) {
1300+ if ( compoundHistory . length === 0 ) {
1301+ alert ( 'No history to export.' ) ;
1302+ return ;
1303+ }
1304+
1305+ const headers = [
1306+ 'Name' ,
1307+ 'SMILES' ,
1308+ 'InChI' ,
1309+ 'InChIKey' ,
1310+ 'MW' ,
1311+ 'cLogP' ,
1312+ 'TPSA' ,
1313+ 'HBD' ,
1314+ 'HBA' ,
1315+ 'RotB' ,
1316+ 'Exact MW' ,
1317+ 'Heavy Atoms' ,
1318+ 'Heteroatoms' ,
1319+ 'Total Rings' ,
1320+ 'Aromatic Rings' ,
1321+ 'Aliphatic Rings' ,
1322+ 'Fsp3' ,
1323+ 'Labute ASA' ,
1324+ 'Lipinski' ,
1325+ 'Veber' ,
1326+ 'Egan' ,
1327+ 'Ghose' ,
1328+ 'MCE-18' ,
1329+ 'REOS' ,
1330+ 'High LogP' ,
1331+ 'High TPSA' ,
1332+ 'Too Flexible' ,
1333+ 'Low Fsp3' ,
1334+ 'Too Many Rings' ,
1335+ 'Complex Stereo' ,
1336+ 'Timestamp'
1337+ ] ;
1338+
1339+ let csvContent = headers . join ( ',' ) + '\n' ;
1340+
1341+ for ( const compound of compoundHistory ) {
1342+ try {
1343+ const m = rdkitModule . get_mol ( compound . smiles ) ;
1344+ if ( ! m || ! m . is_valid ( ) ) {
1345+ csvContent += compound . name + ',ERROR,,,,,,,,,,,,,,,,,,,,,,,,,,,' + compound . timestamp + '\n' ;
1346+ continue ;
1347+ }
1348+
1349+ const inchi = m . get_inchi ( ) ;
1350+ const inchikey = rdkitModule . get_inchikey_for_inchi ( inchi ) ;
1351+ const d = JSON . parse ( m . get_descriptors ( ) ) ;
1352+
1353+ const lipinski =
1354+ d . amw <= 500 &&
1355+ d . CrippenClogP <= 5 &&
1356+ d . NumHBD <= 5 &&
1357+ d . NumHBA <= 10 ;
1358+
1359+ const veber =
1360+ d . tpsa <= 140 &&
1361+ d . NumRotatableBonds <= 10 ;
1362+
1363+ const egan = d . tpsa <= 132 ;
1364+
1365+ const ghose =
1366+ d . amw >= 160 && d . amw <= 480 &&
1367+ d . CrippenClogP >= - 0.4 && d . CrippenClogP <= 5.6 &&
1368+ d . NumRings >= 1 && d . NumRings <= 5 &&
1369+ d . NumAtoms >= 20 && d . NumAtoms <= 70 &&
1370+ d . NumHeteroatoms >= 2 && d . NumHeteroatoms <= 15 ;
1371+
1372+ const mce18 =
1373+ d . amw <= 450 &&
1374+ d . CrippenClogP <= 5.3 &&
1375+ d . tpsa <= 131 &&
1376+ d . NumHBD <= 3 &&
1377+ d . NumHBA <= 6 &&
1378+ d . NumRotatableBonds <= 9 ;
1379+
1380+ const reos =
1381+ d . CrippenClogP > 0 && d . CrippenClogP < 5 &&
1382+ d . amw < 500 &&
1383+ d . NumHBD < 4 &&
1384+ d . NumHBA < 9 ;
1385+
1386+ const alertHighLogP = d . CrippenClogP > 5 ;
1387+ const alertHighTPSA = d . tpsa > 140 ;
1388+ const alertTooFlexible = d . NumRotatableBonds > 10 ;
1389+ const alertLowFsp3 = d . FractionCSP3 < 0.25 ;
1390+ const alertTooManyRings = d . NumRings > 5 ;
1391+ const alertStereo = d . NumAtomStereoCenters > 3 ;
1392+
1393+ const row = [
1394+ compound . name ,
1395+ compound . smiles ,
1396+ inchi ,
1397+ inchikey ,
1398+ d . amw . toFixed ( 2 ) ,
1399+ d . CrippenClogP . toFixed ( 2 ) ,
1400+ d . tpsa . toFixed ( 2 ) ,
1401+ d . NumHBD ,
1402+ d . NumHBA ,
1403+ d . NumRotatableBonds ,
1404+ d . exactmw . toFixed ( 2 ) ,
1405+ d . NumHeavyAtoms ,
1406+ d . NumHeteroatoms ,
1407+ d . NumRings ,
1408+ d . NumAromaticRings ,
1409+ d . NumAliphaticRings ,
1410+ d . FractionCSP3 . toFixed ( 3 ) ,
1411+ d . labuteASA . toFixed ( 2 ) ,
1412+ lipinski ? 'Pass' : 'Fail' ,
1413+ veber ? 'Pass' : 'Fail' ,
1414+ egan ? 'Pass' : 'Fail' ,
1415+ ghose ? 'Pass' : 'Fail' ,
1416+ mce18 ? 'Pass' : 'Fail' ,
1417+ reos ? 'Pass' : 'Fail' ,
1418+ alertHighLogP ? 'Alert' : 'OK' ,
1419+ alertHighTPSA ? 'Alert' : 'OK' ,
1420+ alertTooFlexible ? 'Alert' : 'OK' ,
1421+ alertLowFsp3 ? 'Alert' : 'OK' ,
1422+ alertTooManyRings ? 'Alert' : 'OK' ,
1423+ alertStereo ? 'Alert' : 'OK' ,
1424+ compound . timestamp
1425+ ] . map ( field => {
1426+ const s = String ( field ) ;
1427+ if ( s . includes ( ',' ) || s . includes ( '"' ) || s . includes ( '\n' ) ) {
1428+ return '"' + s . replace ( / " / g, '""' ) + '"' ;
1429+ }
1430+ return s ;
1431+ } ) . join ( ',' ) ;
1432+
1433+ csvContent += row + '\n' ;
1434+ m . delete ( ) ;
1435+ } catch ( e ) {
1436+ csvContent += compound . name + ',ERROR,,,,,,,,,,,,,,,,,,,,,,,,,,,' + compound . timestamp + '\n' ;
1437+ }
1438+ }
1439+
1440+ const blob = new Blob ( [ csvContent ] , { type : 'text/csv;charset=utf-8;' } ) ;
1441+ const url = URL . createObjectURL ( blob ) ;
1442+ const a = document . createElement ( 'a' ) ;
1443+ a . href = url ;
1444+ a . download = 'molecule_history_' + new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) + '.csv' ;
1445+ a . click ( ) ;
1446+ URL . revokeObjectURL ( url ) ;
1447+ }
1448+
12471449 function loadFromHistory ( index ) {
12481450 const compound = compoundHistory [ index ] ;
12491451 if ( compound ) {
@@ -1254,6 +1456,14 @@ <h1>Molecule Clipboard</h1>
12541456 }
12551457 }
12561458
1459+ function deleteFromHistory ( index ) {
1460+ if ( index >= 0 && index < compoundHistory . length ) {
1461+ compoundHistory . splice ( index , 1 ) ;
1462+ saveHistory ( ) ;
1463+ showHistory ( ) ;
1464+ }
1465+ }
1466+
12571467 function clearHistory ( ) {
12581468 if ( confirm ( 'Are you sure you want to clear all history?' ) ) {
12591469 compoundHistory = [ ] ;
@@ -1302,7 +1512,15 @@ <h2>JSME Editor</h2>
13021512 < div class ="modal-content ">
13031513 < div class ="modal-header ">
13041514 < h2 > History</ h2 >
1305- < div >
1515+ < div style ="display: flex; gap: 0.5rem; align-items: center; ">
1516+ < button onclick ="exportHistoryToCSV() " class ="action-btn export ">
1517+ < svg viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 ">
1518+ < path d ="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4 "/>
1519+ < polyline points ="7,10 12,15 17,10 "/>
1520+ < line x1 ="12 " y1 ="15 " x2 ="12 " y2 ="3 "/>
1521+ </ svg >
1522+ Export CSV
1523+ </ button >
13061524 < button onclick ="clearHistory() " class ="action-btn clear ">
13071525 < svg viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 ">
13081526 < polyline points ="3,6 5,6 21,6 "/>
0 commit comments