openclaw-smartmeter 0.5.0 → 0.5.2
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.
- package/canvas-template/app.js +254 -23
- package/canvas-template/index.html +42 -6
- package/canvas-template/styles.css +228 -1
- package/package.json +1 -1
package/canvas-template/app.js
CHANGED
|
@@ -123,6 +123,7 @@ function renderAll() {
|
|
|
123
123
|
safe(updateModelDetails);
|
|
124
124
|
safe(updateLastUpdated);
|
|
125
125
|
safe(checkCostDataNotice);
|
|
126
|
+
safe(() => populateStepPanel(activeStep));
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/* ─── KPIs ─── */
|
|
@@ -925,14 +926,21 @@ function showBalanceDisplay(usageData) {
|
|
|
925
926
|
balanceSection.style.display = 'block';
|
|
926
927
|
|
|
927
928
|
if (usageData) {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
929
|
+
// Handle both API server shape (credits.total/used/remaining, account.limit)
|
|
930
|
+
// and direct OpenRouter shape (usage, limit, rate_limit)
|
|
931
|
+
const credits = usageData.credits || {};
|
|
932
|
+
const account = usageData.account || {};
|
|
933
|
+
const usage = credits.used ?? usageData.usage ?? account.usageBalance ?? 0;
|
|
934
|
+
const limit = credits.total ?? usageData.limit ?? account.limit ?? 0;
|
|
935
|
+
const remaining = credits.remaining ?? (limit - usage);
|
|
936
|
+
const rate = usageData.rate_limit?.requests
|
|
937
|
+
|| usageData.rateLimit?.requests
|
|
938
|
+
|| usageData.rate?.requests
|
|
939
|
+
|| '--';
|
|
940
|
+
|
|
941
|
+
setText('gsBalanceCredits', `$${Number(limit).toFixed(2)}`);
|
|
942
|
+
setText('gsBalanceUsage', `$${Number(usage).toFixed(2)}`);
|
|
943
|
+
setText('gsBalanceRemaining', `$${Number(remaining).toFixed(2)}`);
|
|
936
944
|
setText('gsBalanceRate', typeof rate === 'number' ? `${rate}/s` : rate);
|
|
937
945
|
}
|
|
938
946
|
|
|
@@ -984,6 +992,228 @@ async function initGetStartedCard() {
|
|
|
984
992
|
}
|
|
985
993
|
}
|
|
986
994
|
|
|
995
|
+
/* ─── Step Panels (Analyze / Evaluate / Guide) ─── */
|
|
996
|
+
let activeStep = 1;
|
|
997
|
+
|
|
998
|
+
function activateStep(n) {
|
|
999
|
+
activeStep = n;
|
|
1000
|
+
// Toggle active class on step elements
|
|
1001
|
+
for (let i = 1; i <= 3; i++) {
|
|
1002
|
+
const step = document.getElementById('gsStep' + i);
|
|
1003
|
+
if (step) step.classList.toggle('gs-step-active', i === n);
|
|
1004
|
+
}
|
|
1005
|
+
// Show/hide panels
|
|
1006
|
+
for (let i = 1; i <= 3; i++) {
|
|
1007
|
+
const panel = document.getElementById('gsPanel' + i);
|
|
1008
|
+
if (panel) panel.style.display = (i === n) ? '' : 'none';
|
|
1009
|
+
}
|
|
1010
|
+
// Populate panel content
|
|
1011
|
+
populateStepPanel(n);
|
|
1012
|
+
// Update contextual info banner
|
|
1013
|
+
updateContextualBanner(n);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function populateStepPanel(n) {
|
|
1017
|
+
const d = analysisData;
|
|
1018
|
+
if (n === 1) {
|
|
1019
|
+
const panel = document.getElementById('gsPanel1');
|
|
1020
|
+
if (!panel) return;
|
|
1021
|
+
if (!d) {
|
|
1022
|
+
panel.innerHTML = '<p class="panel-empty">Run an analysis to see your usage summary here.</p>';
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const days = d.days_analyzed || 0;
|
|
1026
|
+
const tasks = d.total_tasks || 0;
|
|
1027
|
+
const current = d.monthly_projected_current || 0;
|
|
1028
|
+
const optimized = d.monthly_projected_optimized || 0;
|
|
1029
|
+
const models = (d.model_breakdown || []).length;
|
|
1030
|
+
panel.innerHTML = `
|
|
1031
|
+
<h4>Analysis Summary</h4>
|
|
1032
|
+
<p>${days} day${days !== 1 ? 's' : ''} analyzed · ${tasks} task${tasks !== 1 ? 's' : ''} · ${models} model${models !== 1 ? 's' : ''} detected</p>
|
|
1033
|
+
<table>
|
|
1034
|
+
<tr><td>Projected Monthly Cost</td><td style="text-align:right;font-weight:600">$${current.toFixed(2)}</td></tr>
|
|
1035
|
+
<tr><td>Optimized Projection</td><td style="text-align:right;font-weight:600;color:var(--green)">$${optimized.toFixed(2)}</td></tr>
|
|
1036
|
+
<tr><td>Potential Savings</td><td style="text-align:right;font-weight:600;color:var(--accent)">$${(current - optimized).toFixed(2)}</td></tr>
|
|
1037
|
+
</table>`;
|
|
1038
|
+
} else if (n === 2) {
|
|
1039
|
+
const panel = document.getElementById('gsPanel2');
|
|
1040
|
+
if (!panel) return;
|
|
1041
|
+
const models = d ? (d.model_breakdown || []) : [];
|
|
1042
|
+
if (models.length === 0) {
|
|
1043
|
+
panel.innerHTML = '<p class="panel-empty">No model data available yet. Run an analysis first.</p>';
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const totalCost = models.reduce((s, m) => s + m.cost, 0);
|
|
1047
|
+
panel.innerHTML = `
|
|
1048
|
+
<h4>Model Cost Breakdown</h4>
|
|
1049
|
+
<table>
|
|
1050
|
+
<thead><tr>
|
|
1051
|
+
<th>Model</th><th>Tasks</th><th>Cost</th><th>Avg/Task</th><th>Share</th>
|
|
1052
|
+
</tr></thead>
|
|
1053
|
+
<tbody>
|
|
1054
|
+
${models.map(m => {
|
|
1055
|
+
const share = totalCost > 0 ? ((m.cost / totalCost) * 100).toFixed(1) : '0.0';
|
|
1056
|
+
return `<tr>
|
|
1057
|
+
<td>${escHtml(m.model)}</td>
|
|
1058
|
+
<td>${m.tasks}</td>
|
|
1059
|
+
<td>$${m.cost.toFixed(2)}</td>
|
|
1060
|
+
<td>$${m.avg_cost_per_task.toFixed(3)}</td>
|
|
1061
|
+
<td>${share}%</td>
|
|
1062
|
+
</tr>`;
|
|
1063
|
+
}).join('')}
|
|
1064
|
+
</tbody>
|
|
1065
|
+
</table>`;
|
|
1066
|
+
} else if (n === 3) {
|
|
1067
|
+
const panel = document.getElementById('gsPanel3');
|
|
1068
|
+
if (!panel) return;
|
|
1069
|
+
const recs = d ? (d.recommendations || []) : [];
|
|
1070
|
+
if (recs.length === 0) {
|
|
1071
|
+
panel.innerHTML = '<p class="panel-empty">No recommendations available yet. Run an analysis first.</p>';
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
panel.innerHTML = `
|
|
1075
|
+
<h4>Recommendations</h4>
|
|
1076
|
+
<p>Toggle on the optimizations you want to activate, then hit Apply.</p>
|
|
1077
|
+
<div class="gs-rec-list">
|
|
1078
|
+
${recs.map((r, i) => {
|
|
1079
|
+
const impactClass = (r.impact || '').toLowerCase().includes('high') ? 'high'
|
|
1080
|
+
: (r.impact || '').toLowerCase().includes('medium') ? 'medium' : 'low';
|
|
1081
|
+
const checked = editedRecommendations[i] && editedRecommendations[i].selected ? 'checked' : '';
|
|
1082
|
+
return `<label class="gs-rec-row" data-index="${i}">
|
|
1083
|
+
<input type="checkbox" class="gs-rec-toggle" ${checked} onchange="toggleGuideRec(${i}, this.checked)">
|
|
1084
|
+
<div class="gs-rec-body">
|
|
1085
|
+
<div class="gs-rec-header">
|
|
1086
|
+
<span class="rec-title">${escHtml(r.title)}</span>
|
|
1087
|
+
${r.impact ? `<span class="rec-impact ${impactClass}">${escHtml(r.impact)}</span>` : ''}
|
|
1088
|
+
</div>
|
|
1089
|
+
<div class="rec-desc">${escHtml(r.description)}</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
</label>`;
|
|
1092
|
+
}).join('')}
|
|
1093
|
+
</div>
|
|
1094
|
+
<div class="gs-rec-actions">
|
|
1095
|
+
<button class="btn btn-primary" onclick="applyGuideRecommendations()">Apply Selected</button>
|
|
1096
|
+
<span class="gs-rec-count" id="gsRecCount">${Object.values(editedRecommendations).filter(v => v.selected).length} selected</span>
|
|
1097
|
+
</div>`;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function updateContextualBanner(n) {
|
|
1102
|
+
const notice = document.getElementById('costDataNotice');
|
|
1103
|
+
const content = document.getElementById('costDataNoticeContent');
|
|
1104
|
+
if (!notice || !content) return;
|
|
1105
|
+
|
|
1106
|
+
const messages = {
|
|
1107
|
+
1: {
|
|
1108
|
+
title: 'About Cost Tracking',
|
|
1109
|
+
text: 'Your OpenRouter API responses may not include cost data. SmartMeter still optimizes based on token usage.'
|
|
1110
|
+
},
|
|
1111
|
+
2: {
|
|
1112
|
+
title: 'Understanding Model Costs',
|
|
1113
|
+
text: 'Costs are based on token usage reported by OpenRouter. Compare models to find cheaper alternatives for similar tasks.'
|
|
1114
|
+
},
|
|
1115
|
+
3: {
|
|
1116
|
+
title: 'Applying Recommendations',
|
|
1117
|
+
text: 'Select recommendations below to apply them. Changes update your OpenClaw configuration for optimized model routing.'
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
const msg = messages[n] || messages[1];
|
|
1122
|
+
const d = analysisData;
|
|
1123
|
+
const hasCostData = d && (d.monthly_projected_current || 0) > 0;
|
|
1124
|
+
|
|
1125
|
+
// Show banner contextually: always for step 2/3 if data exists, or step 1 if no cost data
|
|
1126
|
+
if (n === 1 && hasCostData) {
|
|
1127
|
+
notice.style.display = 'none';
|
|
1128
|
+
} else {
|
|
1129
|
+
content.innerHTML = `<strong>${msg.title}</strong><p>${msg.text}</p>`;
|
|
1130
|
+
notice.style.display = 'flex';
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/* ─── Guide Panel Actions ─── */
|
|
1135
|
+
function toggleGuideRec(i, on) {
|
|
1136
|
+
if (!editedRecommendations[i]) editedRecommendations[i] = {};
|
|
1137
|
+
editedRecommendations[i].selected = on;
|
|
1138
|
+
// Update count
|
|
1139
|
+
const count = Object.values(editedRecommendations).filter(v => v.selected).length;
|
|
1140
|
+
const el = document.getElementById('gsRecCount');
|
|
1141
|
+
if (el) el.textContent = count + ' selected';
|
|
1142
|
+
// Sync main recommendation cards if they exist
|
|
1143
|
+
const card = document.querySelector(`.rec-card[data-index="${i}"]`);
|
|
1144
|
+
if (card) card.classList.toggle('selected', on);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function applyGuideRecommendations() {
|
|
1148
|
+
const selected = Object.entries(editedRecommendations)
|
|
1149
|
+
.filter(([_, v]) => v.selected)
|
|
1150
|
+
.map(([i]) => parseInt(i));
|
|
1151
|
+
|
|
1152
|
+
if (selected.length === 0) {
|
|
1153
|
+
showToast('Toggle at least one recommendation to apply');
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Build summary list for the modal
|
|
1158
|
+
const recs = analysisData ? (analysisData.recommendations || []) : [];
|
|
1159
|
+
const listEl = document.getElementById('applyModalList');
|
|
1160
|
+
if (listEl) {
|
|
1161
|
+
listEl.innerHTML = selected.map(i => {
|
|
1162
|
+
const r = recs[i];
|
|
1163
|
+
if (!r) return '';
|
|
1164
|
+
const impactClass = (r.impact || '').toLowerCase().includes('high') ? 'high'
|
|
1165
|
+
: (r.impact || '').toLowerCase().includes('medium') ? 'medium' : 'low';
|
|
1166
|
+
return `<div class="apply-modal-item">
|
|
1167
|
+
<span class="apply-modal-dot ${impactClass}"></span>
|
|
1168
|
+
<span>${escHtml(r.title)}</span>
|
|
1169
|
+
${r.impact ? `<span class="rec-impact ${impactClass}">${escHtml(r.impact)}</span>` : ''}
|
|
1170
|
+
</div>`;
|
|
1171
|
+
}).join('');
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Show modal
|
|
1175
|
+
const modal = document.getElementById('applyModal');
|
|
1176
|
+
if (modal) modal.style.display = 'flex';
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function closeApplyModal() {
|
|
1180
|
+
const modal = document.getElementById('applyModal');
|
|
1181
|
+
if (modal) modal.style.display = 'none';
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async function confirmApplyRecommendations() {
|
|
1185
|
+
const selected = Object.entries(editedRecommendations)
|
|
1186
|
+
.filter(([_, v]) => v.selected)
|
|
1187
|
+
.map(([i]) => parseInt(i));
|
|
1188
|
+
|
|
1189
|
+
const btn = document.getElementById('applyModalConfirmBtn');
|
|
1190
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Applying…'; }
|
|
1191
|
+
|
|
1192
|
+
try {
|
|
1193
|
+
const res = await fetch(`${API_BASE_URL}/api/apply`, {
|
|
1194
|
+
method: 'POST',
|
|
1195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1196
|
+
body: JSON.stringify({ confirm: true })
|
|
1197
|
+
});
|
|
1198
|
+
const json = await res.json();
|
|
1199
|
+
if (json.success) {
|
|
1200
|
+
closeApplyModal();
|
|
1201
|
+
showToast(`✅ ${selected.length} optimization(s) applied!`);
|
|
1202
|
+
selected.forEach(i => {
|
|
1203
|
+
if (editedRecommendations[i]) editedRecommendations[i].selected = false;
|
|
1204
|
+
});
|
|
1205
|
+
populateStepPanel(3);
|
|
1206
|
+
updateRecommendations();
|
|
1207
|
+
} else {
|
|
1208
|
+
showToast(`Error: ${json.error || 'Apply failed'}`);
|
|
1209
|
+
}
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
showToast(`Network error: ${err.message}`);
|
|
1212
|
+
} finally {
|
|
1213
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Apply Now'; }
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
987
1217
|
/* ─── OpenRouter Integration ─── */
|
|
988
1218
|
async function checkOpenRouterConfig() {
|
|
989
1219
|
try {
|
|
@@ -1005,26 +1235,35 @@ async function fetchOpenRouterUsage() {
|
|
|
1005
1235
|
if (!res.ok) return;
|
|
1006
1236
|
const json = await res.json();
|
|
1007
1237
|
if (json.success && json.configured) {
|
|
1008
|
-
const
|
|
1238
|
+
const raw = json.data || json;
|
|
1239
|
+
const credits = raw.credits || {};
|
|
1240
|
+
const account = raw.account || {};
|
|
1241
|
+
const used = credits.used ?? raw.usage ?? account.usageBalance ?? 0;
|
|
1242
|
+
const limit = credits.total ?? raw.limit ?? account.limit ?? 0;
|
|
1243
|
+
const remaining = credits.remaining ?? (limit - used);
|
|
1244
|
+
const rate = raw.rate_limit?.requests || raw.rate?.requests || '--';
|
|
1009
1245
|
container.innerHTML = `
|
|
1010
1246
|
<div class="or-stats-grid">
|
|
1011
1247
|
<div class="or-stat-card">
|
|
1012
1248
|
<div class="or-stat-label">Usage (USD)</div>
|
|
1013
|
-
<div class="or-stat-value">$${(
|
|
1249
|
+
<div class="or-stat-value">$${Number(used).toFixed(2)}</div>
|
|
1014
1250
|
</div>
|
|
1015
1251
|
<div class="or-stat-card">
|
|
1016
1252
|
<div class="or-stat-label">Limit</div>
|
|
1017
|
-
<div class="or-stat-value">$${(
|
|
1253
|
+
<div class="or-stat-value">$${Number(limit).toFixed(2)}</div>
|
|
1018
1254
|
</div>
|
|
1019
1255
|
<div class="or-stat-card">
|
|
1020
1256
|
<div class="or-stat-label">Remaining</div>
|
|
1021
|
-
<div class="or-stat-value">$${(
|
|
1257
|
+
<div class="or-stat-value">$${Number(remaining).toFixed(2)}</div>
|
|
1022
1258
|
</div>
|
|
1023
1259
|
<div class="or-stat-card">
|
|
1024
1260
|
<div class="or-stat-label">Rate Limit</div>
|
|
1025
|
-
<div class="or-stat-value">${
|
|
1261
|
+
<div class="or-stat-value">${typeof rate === 'number' ? rate + '/s' : rate}</div>
|
|
1026
1262
|
</div>
|
|
1027
1263
|
</div>`;
|
|
1264
|
+
|
|
1265
|
+
// Also update Get Started balance card if visible
|
|
1266
|
+
showBalanceDisplay(raw);
|
|
1028
1267
|
}
|
|
1029
1268
|
} catch {
|
|
1030
1269
|
// noop
|
|
@@ -1254,16 +1493,8 @@ async function refreshDashboard() {
|
|
|
1254
1493
|
|
|
1255
1494
|
/* ─── Cost Data Notice ─── */
|
|
1256
1495
|
function checkCostDataNotice() {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
const hasCostData = (d.monthly_projected_current || 0) > 0;
|
|
1260
|
-
const notice = document.getElementById('costDataNotice');
|
|
1261
|
-
if (notice) {
|
|
1262
|
-
// Only show when cost data is truly missing — keep hidden otherwise
|
|
1263
|
-
if (!hasCostData) {
|
|
1264
|
-
notice.style.display = 'flex';
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1496
|
+
// Delegate to contextual banner system
|
|
1497
|
+
updateContextualBanner(activeStep);
|
|
1267
1498
|
}
|
|
1268
1499
|
|
|
1269
1500
|
/* ─── Helpers ─── */
|
|
@@ -119,21 +119,21 @@
|
|
|
119
119
|
|
|
120
120
|
<!-- 3-Step Flow -->
|
|
121
121
|
<div class="gs-steps">
|
|
122
|
-
<div class="gs-step" id="gsStep1">
|
|
122
|
+
<div class="gs-step gs-step-active" id="gsStep1" onclick="activateStep(1)">
|
|
123
123
|
<div class="gs-step-num">1</div>
|
|
124
124
|
<div>
|
|
125
125
|
<div class="gs-step-title">Analyze</div>
|
|
126
126
|
<div class="gs-step-desc">Parse session logs to classify tasks, track costs, and break down model usage.</div>
|
|
127
127
|
</div>
|
|
128
128
|
</div>
|
|
129
|
-
<div class="gs-step" id="gsStep2">
|
|
129
|
+
<div class="gs-step" id="gsStep2" onclick="activateStep(2)">
|
|
130
130
|
<div class="gs-step-num">2</div>
|
|
131
131
|
<div>
|
|
132
132
|
<div class="gs-step-title">Evaluate</div>
|
|
133
133
|
<div class="gs-step-desc">Compare models side-by-side — see which cost the most and where cheaper alternatives save money.</div>
|
|
134
134
|
</div>
|
|
135
135
|
</div>
|
|
136
|
-
<div class="gs-step" id="gsStep3">
|
|
136
|
+
<div class="gs-step" id="gsStep3" onclick="activateStep(3)">
|
|
137
137
|
<div class="gs-step-num">3</div>
|
|
138
138
|
<div>
|
|
139
139
|
<div class="gs-step-title">Guide</div>
|
|
@@ -141,12 +141,23 @@
|
|
|
141
141
|
</div>
|
|
142
142
|
</div>
|
|
143
143
|
</div>
|
|
144
|
+
|
|
145
|
+
<!-- Step Detail Panels -->
|
|
146
|
+
<div class="gs-step-panel" id="gsPanel1">
|
|
147
|
+
<!-- Analyze: shown by default with analysis summary -->
|
|
148
|
+
</div>
|
|
149
|
+
<div class="gs-step-panel" id="gsPanel2" style="display:none">
|
|
150
|
+
<!-- Evaluate: model cost breakdown table -->
|
|
151
|
+
</div>
|
|
152
|
+
<div class="gs-step-panel" id="gsPanel3" style="display:none">
|
|
153
|
+
<!-- Guide: top recommendations -->
|
|
154
|
+
</div>
|
|
144
155
|
</div>
|
|
145
156
|
|
|
146
|
-
<!-- Info Banner -->
|
|
157
|
+
<!-- Contextual Info Banner -->
|
|
147
158
|
<div class="alert alert-info" id="costDataNotice" style="display:none;">
|
|
148
159
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
|
149
|
-
<div>
|
|
160
|
+
<div id="costDataNoticeContent">
|
|
150
161
|
<strong>About Cost Tracking</strong>
|
|
151
162
|
<p>Your OpenRouter API responses may not include cost data. SmartMeter still optimizes based on token usage.</p>
|
|
152
163
|
</div>
|
|
@@ -160,7 +171,7 @@
|
|
|
160
171
|
<div class="kpi-sub" id="savingsPercentage">--</div>
|
|
161
172
|
</div>
|
|
162
173
|
<div class="kpi-card">
|
|
163
|
-
<div class="kpi-label">
|
|
174
|
+
<div class="kpi-label">Projected Monthly Cost</div>
|
|
164
175
|
<div class="kpi-value" id="currentCost">$0.00<small>/mo</small></div>
|
|
165
176
|
</div>
|
|
166
177
|
<div class="kpi-card kpi-success">
|
|
@@ -402,6 +413,31 @@
|
|
|
402
413
|
</div>
|
|
403
414
|
</div>
|
|
404
415
|
|
|
416
|
+
<!-- Apply Recommendations Modal -->
|
|
417
|
+
<div id="applyModal" class="modal-overlay" style="display:none">
|
|
418
|
+
<div class="modal">
|
|
419
|
+
<div class="modal-header">
|
|
420
|
+
<h3>Apply Optimizations</h3>
|
|
421
|
+
<button class="btn-icon modal-close" onclick="closeApplyModal()">✕</button>
|
|
422
|
+
</div>
|
|
423
|
+
<div class="modal-body">
|
|
424
|
+
<p class="modal-desc">The following recommendations will be applied to your OpenClaw configuration at <code>~/.openclaw/openclaw.json</code>.</p>
|
|
425
|
+
<div id="applyModalList" class="apply-modal-list"></div>
|
|
426
|
+
<div class="apply-revert-info">
|
|
427
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
|
428
|
+
<div>
|
|
429
|
+
<strong>Easy to undo</strong>
|
|
430
|
+
<p>To revert, run <code>smartmeter reset</code> in your terminal, or edit <code>~/.openclaw/openclaw.json</code> directly. You can also toggle individual recommendations off here and re-apply.</p>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
<div class="modal-footer">
|
|
435
|
+
<button class="btn btn-ghost" onclick="closeApplyModal()">Cancel</button>
|
|
436
|
+
<button class="btn btn-primary" id="applyModalConfirmBtn" onclick="confirmApplyRecommendations()">Apply Now</button>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
405
441
|
<!-- Preview Modal -->
|
|
406
442
|
<div id="previewModal" class="modal-overlay" style="display:none">
|
|
407
443
|
<div class="modal modal-lg">
|
|
@@ -399,7 +399,12 @@ a:hover { color: var(--accent-hover); }
|
|
|
399
399
|
background: var(--bg-surface);
|
|
400
400
|
border-radius: var(--radius);
|
|
401
401
|
border: 1px solid var(--border);
|
|
402
|
-
transition: border-color .2s;
|
|
402
|
+
transition: border-color .2s, background .2s;
|
|
403
|
+
cursor: pointer;
|
|
404
|
+
}
|
|
405
|
+
.gs-step:hover {
|
|
406
|
+
border-color: var(--border-light);
|
|
407
|
+
background: var(--bg-card-hover);
|
|
403
408
|
}
|
|
404
409
|
.gs-step.gs-step-active {
|
|
405
410
|
border-color: var(--accent);
|
|
@@ -437,6 +442,167 @@ a:hover { color: var(--accent-hover); }
|
|
|
437
442
|
line-height: 1.5;
|
|
438
443
|
}
|
|
439
444
|
|
|
445
|
+
/* Step detail panels */
|
|
446
|
+
.gs-step-panel {
|
|
447
|
+
margin-top: 16px;
|
|
448
|
+
background: var(--bg-surface);
|
|
449
|
+
border: 1px solid var(--border);
|
|
450
|
+
border-radius: var(--radius);
|
|
451
|
+
padding: 18px;
|
|
452
|
+
animation: fadeIn .25s ease;
|
|
453
|
+
}
|
|
454
|
+
.gs-step-panel h4 {
|
|
455
|
+
font-size: 14px;
|
|
456
|
+
font-weight: 700;
|
|
457
|
+
color: var(--text-primary);
|
|
458
|
+
margin: 0 0 12px 0;
|
|
459
|
+
}
|
|
460
|
+
.gs-step-panel p {
|
|
461
|
+
font-size: 13px;
|
|
462
|
+
color: var(--text-secondary);
|
|
463
|
+
margin: 0 0 12px 0;
|
|
464
|
+
line-height: 1.5;
|
|
465
|
+
}
|
|
466
|
+
.gs-step-panel table {
|
|
467
|
+
width: 100%;
|
|
468
|
+
border-collapse: collapse;
|
|
469
|
+
font-size: 13px;
|
|
470
|
+
}
|
|
471
|
+
.gs-step-panel th {
|
|
472
|
+
text-align: left;
|
|
473
|
+
font-weight: 600;
|
|
474
|
+
color: var(--text-muted);
|
|
475
|
+
padding: 6px 10px;
|
|
476
|
+
border-bottom: 1px solid var(--border);
|
|
477
|
+
font-size: 11px;
|
|
478
|
+
text-transform: uppercase;
|
|
479
|
+
letter-spacing: .5px;
|
|
480
|
+
}
|
|
481
|
+
.gs-step-panel td {
|
|
482
|
+
padding: 8px 10px;
|
|
483
|
+
color: var(--text-primary);
|
|
484
|
+
border-bottom: 1px solid var(--border-subtle, rgba(255,255,255,0.04));
|
|
485
|
+
}
|
|
486
|
+
.gs-step-panel tr:last-child td {
|
|
487
|
+
border-bottom: none;
|
|
488
|
+
}
|
|
489
|
+
.gs-step-panel .panel-empty {
|
|
490
|
+
color: var(--text-muted);
|
|
491
|
+
font-style: italic;
|
|
492
|
+
font-size: 13px;
|
|
493
|
+
}
|
|
494
|
+
.gs-step-panel .rec-item {
|
|
495
|
+
padding: 10px 0;
|
|
496
|
+
border-bottom: 1px solid var(--border-subtle, rgba(255,255,255,0.04));
|
|
497
|
+
}
|
|
498
|
+
.gs-step-panel .rec-item:last-child {
|
|
499
|
+
border-bottom: none;
|
|
500
|
+
}
|
|
501
|
+
.gs-step-panel .rec-title {
|
|
502
|
+
font-weight: 600;
|
|
503
|
+
color: var(--text-primary);
|
|
504
|
+
font-size: 13px;
|
|
505
|
+
margin-bottom: 4px;
|
|
506
|
+
}
|
|
507
|
+
.gs-step-panel .rec-desc {
|
|
508
|
+
font-size: 12px;
|
|
509
|
+
color: var(--text-muted);
|
|
510
|
+
line-height: 1.5;
|
|
511
|
+
}
|
|
512
|
+
.gs-step-panel .rec-impact {
|
|
513
|
+
display: inline-block;
|
|
514
|
+
font-size: 11px;
|
|
515
|
+
font-weight: 600;
|
|
516
|
+
padding: 2px 8px;
|
|
517
|
+
border-radius: 10px;
|
|
518
|
+
margin-top: 4px;
|
|
519
|
+
}
|
|
520
|
+
.gs-step-panel .rec-impact.high {
|
|
521
|
+
background: var(--green-subtle);
|
|
522
|
+
color: var(--green);
|
|
523
|
+
}
|
|
524
|
+
.gs-step-panel .rec-impact.medium {
|
|
525
|
+
background: var(--accent-subtle);
|
|
526
|
+
color: var(--accent);
|
|
527
|
+
}
|
|
528
|
+
.gs-step-panel .rec-impact.low {
|
|
529
|
+
background: rgba(255,255,255,0.06);
|
|
530
|
+
color: var(--text-muted);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/* Guide panel – recommendation list */
|
|
534
|
+
.gs-rec-list {
|
|
535
|
+
display: flex;
|
|
536
|
+
flex-direction: column;
|
|
537
|
+
gap: 2px;
|
|
538
|
+
}
|
|
539
|
+
.gs-rec-row {
|
|
540
|
+
display: flex;
|
|
541
|
+
align-items: flex-start;
|
|
542
|
+
gap: 12px;
|
|
543
|
+
padding: 12px 14px;
|
|
544
|
+
border-radius: 8px;
|
|
545
|
+
cursor: pointer;
|
|
546
|
+
transition: background .15s;
|
|
547
|
+
}
|
|
548
|
+
.gs-rec-row:hover {
|
|
549
|
+
background: rgba(255,255,255,0.03);
|
|
550
|
+
}
|
|
551
|
+
.gs-rec-toggle {
|
|
552
|
+
appearance: none;
|
|
553
|
+
-webkit-appearance: none;
|
|
554
|
+
width: 38px;
|
|
555
|
+
min-width: 38px;
|
|
556
|
+
height: 22px;
|
|
557
|
+
border-radius: 11px;
|
|
558
|
+
background: var(--border);
|
|
559
|
+
position: relative;
|
|
560
|
+
cursor: pointer;
|
|
561
|
+
transition: background .2s;
|
|
562
|
+
margin-top: 2px;
|
|
563
|
+
}
|
|
564
|
+
.gs-rec-toggle::after {
|
|
565
|
+
content: '';
|
|
566
|
+
position: absolute;
|
|
567
|
+
top: 3px;
|
|
568
|
+
left: 3px;
|
|
569
|
+
width: 16px;
|
|
570
|
+
height: 16px;
|
|
571
|
+
border-radius: 50%;
|
|
572
|
+
background: var(--text-muted);
|
|
573
|
+
transition: transform .2s, background .2s;
|
|
574
|
+
}
|
|
575
|
+
.gs-rec-toggle:checked {
|
|
576
|
+
background: var(--accent);
|
|
577
|
+
}
|
|
578
|
+
.gs-rec-toggle:checked::after {
|
|
579
|
+
transform: translateX(16px);
|
|
580
|
+
background: #fff;
|
|
581
|
+
}
|
|
582
|
+
.gs-rec-body {
|
|
583
|
+
flex: 1;
|
|
584
|
+
min-width: 0;
|
|
585
|
+
}
|
|
586
|
+
.gs-rec-header {
|
|
587
|
+
display: flex;
|
|
588
|
+
align-items: center;
|
|
589
|
+
gap: 8px;
|
|
590
|
+
flex-wrap: wrap;
|
|
591
|
+
margin-bottom: 4px;
|
|
592
|
+
}
|
|
593
|
+
.gs-rec-actions {
|
|
594
|
+
display: flex;
|
|
595
|
+
align-items: center;
|
|
596
|
+
gap: 14px;
|
|
597
|
+
margin-top: 16px;
|
|
598
|
+
padding-top: 14px;
|
|
599
|
+
border-top: 1px solid var(--border);
|
|
600
|
+
}
|
|
601
|
+
.gs-rec-count {
|
|
602
|
+
font-size: 12px;
|
|
603
|
+
color: var(--text-muted);
|
|
604
|
+
}
|
|
605
|
+
|
|
440
606
|
@media (max-width: 768px) {
|
|
441
607
|
.gs-steps {
|
|
442
608
|
grid-template-columns: 1fr;
|
|
@@ -924,6 +1090,67 @@ a:hover { color: var(--accent-hover); }
|
|
|
924
1090
|
padding: 12px 24px 20px;
|
|
925
1091
|
}
|
|
926
1092
|
|
|
1093
|
+
/* Apply Modal */
|
|
1094
|
+
.apply-modal-list {
|
|
1095
|
+
display: flex;
|
|
1096
|
+
flex-direction: column;
|
|
1097
|
+
gap: 8px;
|
|
1098
|
+
margin-bottom: 16px;
|
|
1099
|
+
max-height: 200px;
|
|
1100
|
+
overflow-y: auto;
|
|
1101
|
+
}
|
|
1102
|
+
.apply-modal-item {
|
|
1103
|
+
display: flex;
|
|
1104
|
+
align-items: center;
|
|
1105
|
+
gap: 10px;
|
|
1106
|
+
padding: 10px 12px;
|
|
1107
|
+
background: var(--bg-card);
|
|
1108
|
+
border: 1px solid var(--border);
|
|
1109
|
+
border-radius: var(--radius-sm);
|
|
1110
|
+
font-size: 13px;
|
|
1111
|
+
color: var(--text-primary);
|
|
1112
|
+
}
|
|
1113
|
+
.apply-modal-dot {
|
|
1114
|
+
flex-shrink: 0;
|
|
1115
|
+
width: 8px;
|
|
1116
|
+
height: 8px;
|
|
1117
|
+
border-radius: 50%;
|
|
1118
|
+
}
|
|
1119
|
+
.apply-modal-dot.high { background: var(--green); }
|
|
1120
|
+
.apply-modal-dot.medium { background: var(--accent); }
|
|
1121
|
+
.apply-modal-dot.low { background: var(--text-muted); }
|
|
1122
|
+
.apply-revert-info {
|
|
1123
|
+
display: flex;
|
|
1124
|
+
gap: 10px;
|
|
1125
|
+
padding: 12px 14px;
|
|
1126
|
+
background: var(--accent-subtle);
|
|
1127
|
+
border: 1px solid rgba(99,102,241,.2);
|
|
1128
|
+
border-radius: var(--radius-sm);
|
|
1129
|
+
font-size: 12px;
|
|
1130
|
+
color: var(--text-secondary);
|
|
1131
|
+
line-height: 1.5;
|
|
1132
|
+
}
|
|
1133
|
+
.apply-revert-info svg {
|
|
1134
|
+
flex-shrink: 0;
|
|
1135
|
+
margin-top: 2px;
|
|
1136
|
+
}
|
|
1137
|
+
.apply-revert-info strong {
|
|
1138
|
+
display: block;
|
|
1139
|
+
color: var(--text-primary);
|
|
1140
|
+
font-size: 13px;
|
|
1141
|
+
margin-bottom: 4px;
|
|
1142
|
+
}
|
|
1143
|
+
.apply-revert-info p {
|
|
1144
|
+
margin: 0;
|
|
1145
|
+
}
|
|
1146
|
+
.apply-revert-info code {
|
|
1147
|
+
background: rgba(255,255,255,.06);
|
|
1148
|
+
padding: 1px 5px;
|
|
1149
|
+
border-radius: 3px;
|
|
1150
|
+
font-size: 11px;
|
|
1151
|
+
font-family: var(--font-mono);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
927
1154
|
/* Form */
|
|
928
1155
|
.form-label {
|
|
929
1156
|
display: block;
|