openclaw-smartmeter 0.4.1 → 0.5.0
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 +158 -1
- package/canvas-template/index.html +67 -23
- package/canvas-template/styles.css +150 -40
- package/package.json +2 -2
package/canvas-template/app.js
CHANGED
|
@@ -23,6 +23,7 @@ async function initializeDashboard() {
|
|
|
23
23
|
// Try to load data from local file first (works when served statically)
|
|
24
24
|
await loadAnalysisData();
|
|
25
25
|
hideLoading();
|
|
26
|
+
initGetStartedCard();
|
|
26
27
|
checkOpenRouterConfig();
|
|
27
28
|
startAutoRefresh();
|
|
28
29
|
} catch (err) {
|
|
@@ -828,6 +829,161 @@ function updateModelDetails() {
|
|
|
828
829
|
</table>`;
|
|
829
830
|
}
|
|
830
831
|
|
|
832
|
+
/* ─── Inline API Key (Get Started Card) ─── */
|
|
833
|
+
async function validateInlineApiKey() {
|
|
834
|
+
const input = document.getElementById('gsApiKeyInput');
|
|
835
|
+
const key = input.value.trim();
|
|
836
|
+
const status = document.getElementById('gsKeyStatus');
|
|
837
|
+
const btn = document.getElementById('gsValidateBtn');
|
|
838
|
+
|
|
839
|
+
function showStatus(msg, type) {
|
|
840
|
+
status.textContent = msg;
|
|
841
|
+
status.className = 'config-status ' + type;
|
|
842
|
+
status.style.removeProperty('display');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (!key) { showStatus('Please enter an API key.', 'error'); return; }
|
|
846
|
+
if (!key.startsWith('sk-or-')) { showStatus('Invalid format — key should start with sk-or-', 'error'); return; }
|
|
847
|
+
|
|
848
|
+
showStatus('Validating…', 'validating');
|
|
849
|
+
btn.disabled = true;
|
|
850
|
+
btn.textContent = 'Validating…';
|
|
851
|
+
|
|
852
|
+
let validated = false;
|
|
853
|
+
let errorMsg = '';
|
|
854
|
+
let usageData = null;
|
|
855
|
+
|
|
856
|
+
// Try API server first
|
|
857
|
+
try {
|
|
858
|
+
const res = await fetch(`${API_BASE_URL}/api/config/openrouter-key`, {
|
|
859
|
+
method: 'POST',
|
|
860
|
+
headers: { 'Content-Type': 'application/json' },
|
|
861
|
+
body: JSON.stringify({ apiKey: key })
|
|
862
|
+
});
|
|
863
|
+
const json = await res.json();
|
|
864
|
+
if (json.success) {
|
|
865
|
+
validated = true;
|
|
866
|
+
// Fetch usage via API server
|
|
867
|
+
try {
|
|
868
|
+
const ur = await fetch(`${API_BASE_URL}/api/openrouter-usage`);
|
|
869
|
+
const uj = await ur.json();
|
|
870
|
+
if (uj.success && uj.configured) usageData = uj.data || uj;
|
|
871
|
+
} catch {}
|
|
872
|
+
} else {
|
|
873
|
+
errorMsg = json.error || 'Validation failed';
|
|
874
|
+
}
|
|
875
|
+
} catch (_) {
|
|
876
|
+
// API server not available — validate directly against OpenRouter
|
|
877
|
+
try {
|
|
878
|
+
const res = await fetch('https://openrouter.ai/api/v1/auth/key', {
|
|
879
|
+
headers: { 'Authorization': `Bearer ${key}` }
|
|
880
|
+
});
|
|
881
|
+
if (res.ok) {
|
|
882
|
+
const data = await res.json();
|
|
883
|
+
if (data && data.data) {
|
|
884
|
+
validated = true;
|
|
885
|
+
usageData = data.data;
|
|
886
|
+
} else {
|
|
887
|
+
errorMsg = 'Key not recognized by OpenRouter';
|
|
888
|
+
}
|
|
889
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
890
|
+
errorMsg = 'Invalid API key — authentication failed';
|
|
891
|
+
} else {
|
|
892
|
+
errorMsg = `OpenRouter returned status ${res.status}`;
|
|
893
|
+
}
|
|
894
|
+
} catch (e2) {
|
|
895
|
+
errorMsg = 'Could not reach OpenRouter to validate — check your connection';
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
btn.disabled = false;
|
|
900
|
+
btn.textContent = 'Save & Validate';
|
|
901
|
+
|
|
902
|
+
if (validated) {
|
|
903
|
+
localStorage.setItem('smartmeter_openrouter_key', key);
|
|
904
|
+
showStatus('✅ API key saved and validated!', 'success');
|
|
905
|
+
|
|
906
|
+
// Show balance section, hide key input
|
|
907
|
+
setTimeout(() => {
|
|
908
|
+
showBalanceDisplay(usageData);
|
|
909
|
+
// Also sync the modal key input
|
|
910
|
+
const modalInput = document.getElementById('apiKeyInput');
|
|
911
|
+
if (modalInput) modalInput.value = key;
|
|
912
|
+
// Also update the OpenRouter sidebar section
|
|
913
|
+
fetchOpenRouterUsage();
|
|
914
|
+
}, 600);
|
|
915
|
+
} else {
|
|
916
|
+
showStatus(`❌ ${errorMsg}`, 'error');
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function showBalanceDisplay(usageData) {
|
|
921
|
+
const keySection = document.getElementById('gsKeySection');
|
|
922
|
+
const balanceSection = document.getElementById('gsBalance');
|
|
923
|
+
|
|
924
|
+
keySection.style.display = 'none';
|
|
925
|
+
balanceSection.style.display = 'block';
|
|
926
|
+
|
|
927
|
+
if (usageData) {
|
|
928
|
+
const usage = usageData.usage || 0;
|
|
929
|
+
const limit = usageData.limit || usageData.credit || 0;
|
|
930
|
+
const remaining = limit - usage;
|
|
931
|
+
const rate = usageData.rate_limit?.requests || usageData.rateLimit?.requests || '--';
|
|
932
|
+
|
|
933
|
+
setText('gsBalanceCredits', `$${limit.toFixed(2)}`);
|
|
934
|
+
setText('gsBalanceUsage', `$${usage.toFixed(2)}`);
|
|
935
|
+
setText('gsBalanceRemaining', `$${remaining.toFixed(2)}`);
|
|
936
|
+
setText('gsBalanceRate', typeof rate === 'number' ? `${rate}/s` : rate);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Highlight step 1 as done
|
|
940
|
+
const step1 = document.getElementById('gsStep1');
|
|
941
|
+
if (step1) { step1.classList.add('gs-step-done'); }
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function showApiKeyInput() {
|
|
945
|
+
const keySection = document.getElementById('gsKeySection');
|
|
946
|
+
const balanceSection = document.getElementById('gsBalance');
|
|
947
|
+
keySection.style.display = 'block';
|
|
948
|
+
balanceSection.style.display = 'none';
|
|
949
|
+
document.getElementById('gsApiKeyInput').focus();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** On init, check if key is already stored and auto-show balance */
|
|
953
|
+
async function initGetStartedCard() {
|
|
954
|
+
const stored = localStorage.getItem('smartmeter_openrouter_key');
|
|
955
|
+
if (!stored) return;
|
|
956
|
+
|
|
957
|
+
// Pre-fill the input
|
|
958
|
+
const input = document.getElementById('gsApiKeyInput');
|
|
959
|
+
if (input) input.value = stored;
|
|
960
|
+
|
|
961
|
+
// Try to fetch balance
|
|
962
|
+
let usageData = null;
|
|
963
|
+
try {
|
|
964
|
+
const res = await fetch(`${API_BASE_URL}/api/openrouter-usage`);
|
|
965
|
+
const json = await res.json();
|
|
966
|
+
if (json.success && json.configured) {
|
|
967
|
+
usageData = json.data || json;
|
|
968
|
+
}
|
|
969
|
+
} catch {
|
|
970
|
+
// Try direct OpenRouter
|
|
971
|
+
try {
|
|
972
|
+
const res = await fetch('https://openrouter.ai/api/v1/auth/key', {
|
|
973
|
+
headers: { 'Authorization': `Bearer ${stored}` }
|
|
974
|
+
});
|
|
975
|
+
if (res.ok) {
|
|
976
|
+
const data = await res.json();
|
|
977
|
+
if (data && data.data) usageData = data.data;
|
|
978
|
+
}
|
|
979
|
+
} catch {}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (usageData) {
|
|
983
|
+
showBalanceDisplay(usageData);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
831
987
|
/* ─── OpenRouter Integration ─── */
|
|
832
988
|
async function checkOpenRouterConfig() {
|
|
833
989
|
try {
|
|
@@ -954,7 +1110,8 @@ async function saveApiKey() {
|
|
|
954
1110
|
setTimeout(() => {
|
|
955
1111
|
closeConfigModal();
|
|
956
1112
|
fetchOpenRouterUsage();
|
|
957
|
-
|
|
1113
|
+
// Also sync the Get Started card
|
|
1114
|
+
initGetStartedCard();
|
|
958
1115
|
}, 1200);
|
|
959
1116
|
} else {
|
|
960
1117
|
showStatus(`❌ ${errorMsg}`, 'error');
|
|
@@ -63,40 +63,84 @@
|
|
|
63
63
|
|
|
64
64
|
<!-- Section: Overview -->
|
|
65
65
|
<section class="page-section active" id="section-overview">
|
|
66
|
-
<!--
|
|
67
|
-
<div class="
|
|
68
|
-
<div class="
|
|
69
|
-
<
|
|
70
|
-
|
|
66
|
+
<!-- Get Started / Analyze Card -->
|
|
67
|
+
<div class="get-started-card" id="getStartedCard">
|
|
68
|
+
<div class="gs-header">
|
|
69
|
+
<div class="gs-header-left">
|
|
70
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
71
|
+
<div>
|
|
72
|
+
<strong class="gs-title">Get Started — Connect OpenRouter</strong>
|
|
73
|
+
<p class="gs-subtitle">Enter your API key to analyze usage, view your balance, and get cost-saving recommendations.</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
71
76
|
</div>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
|
|
78
|
+
<!-- API Key Input Row -->
|
|
79
|
+
<div class="gs-key-section" id="gsKeySection">
|
|
80
|
+
<div class="gs-key-row">
|
|
81
|
+
<div class="gs-key-input-wrap">
|
|
82
|
+
<label class="gs-key-label" for="gsApiKeyInput">OpenRouter API Key</label>
|
|
83
|
+
<div class="gs-key-field">
|
|
84
|
+
<input type="password" id="gsApiKeyInput" placeholder="sk-or-v1-..." class="form-input gs-input">
|
|
85
|
+
<button class="btn btn-primary gs-validate-btn" id="gsValidateBtn" onclick="validateInlineApiKey()">Save & Validate</button>
|
|
86
|
+
</div>
|
|
87
|
+
<small class="form-hint">Your key is stored locally and never shared. <a href="https://openrouter.ai/keys" target="_blank">Get a key →</a></small>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div id="gsKeyStatus" class="config-status"></div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- Balance Display (shown after validation) -->
|
|
94
|
+
<div class="gs-balance" id="gsBalance" style="display:none">
|
|
95
|
+
<div class="gs-balance-grid">
|
|
96
|
+
<div class="gs-balance-item">
|
|
97
|
+
<span class="gs-balance-label">Balance</span>
|
|
98
|
+
<span class="gs-balance-value" id="gsBalanceCredits">--</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="gs-balance-item">
|
|
101
|
+
<span class="gs-balance-label">Usage</span>
|
|
102
|
+
<span class="gs-balance-value" id="gsBalanceUsage">--</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="gs-balance-item">
|
|
105
|
+
<span class="gs-balance-label">Remaining</span>
|
|
106
|
+
<span class="gs-balance-value gs-balance-green" id="gsBalanceRemaining">--</span>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="gs-balance-item">
|
|
109
|
+
<span class="gs-balance-label">Rate Limit</span>
|
|
110
|
+
<span class="gs-balance-value" id="gsBalanceRate">--</span>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="gs-connected-badge">
|
|
114
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--green)" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
|
115
|
+
<span>Connected</span>
|
|
116
|
+
<button class="btn-link gs-change-key" onclick="showApiKeyInput()">Change key</button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<!-- 3-Step Flow -->
|
|
121
|
+
<div class="gs-steps">
|
|
122
|
+
<div class="gs-step" id="gsStep1">
|
|
123
|
+
<div class="gs-step-num">1</div>
|
|
76
124
|
<div>
|
|
77
|
-
<div class="
|
|
78
|
-
<div class="
|
|
125
|
+
<div class="gs-step-title">Analyze</div>
|
|
126
|
+
<div class="gs-step-desc">Parse session logs to classify tasks, track costs, and break down model usage.</div>
|
|
79
127
|
</div>
|
|
80
128
|
</div>
|
|
81
|
-
<div class="
|
|
82
|
-
<div class="
|
|
129
|
+
<div class="gs-step" id="gsStep2">
|
|
130
|
+
<div class="gs-step-num">2</div>
|
|
83
131
|
<div>
|
|
84
|
-
<div class="
|
|
85
|
-
<div class="
|
|
132
|
+
<div class="gs-step-title">Evaluate</div>
|
|
133
|
+
<div class="gs-step-desc">Compare models side-by-side — see which cost the most and where cheaper alternatives save money.</div>
|
|
86
134
|
</div>
|
|
87
135
|
</div>
|
|
88
|
-
<div class="
|
|
89
|
-
<div class="
|
|
136
|
+
<div class="gs-step" id="gsStep3">
|
|
137
|
+
<div class="gs-step-num">3</div>
|
|
90
138
|
<div>
|
|
91
|
-
<div class="
|
|
92
|
-
<div class="
|
|
139
|
+
<div class="gs-step-title">Guide</div>
|
|
140
|
+
<div class="gs-step-desc">Get actionable recommendations — model switches, caching strategies, and budget controls.</div>
|
|
93
141
|
</div>
|
|
94
142
|
</div>
|
|
95
143
|
</div>
|
|
96
|
-
<div class="hiw-note">
|
|
97
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
|
|
98
|
-
<span>Requires an <strong>OpenRouter API key</strong>. Go to <a href="#" onclick="navigateTo('openrouter'); return false;">OpenRouter settings</a> to connect your account.</span>
|
|
99
|
-
</div>
|
|
100
144
|
</div>
|
|
101
145
|
|
|
102
146
|
<!-- Info Banner -->
|
|
@@ -252,45 +252,163 @@ a:hover { color: var(--accent-hover); }
|
|
|
252
252
|
.alert-info p { color: var(--text-secondary); margin-top: 2px; }
|
|
253
253
|
|
|
254
254
|
/* ===== How It Works Banner ===== */
|
|
255
|
-
|
|
255
|
+
/* ===== Get Started Card ===== */
|
|
256
|
+
.get-started-card {
|
|
256
257
|
background: var(--bg-card);
|
|
257
258
|
border: 1px solid var(--border);
|
|
258
259
|
border-radius: var(--radius-lg);
|
|
259
|
-
padding:
|
|
260
|
+
padding: 24px;
|
|
260
261
|
margin-bottom: 20px;
|
|
261
262
|
}
|
|
262
|
-
.
|
|
263
|
+
.gs-header {
|
|
263
264
|
display: flex;
|
|
264
|
-
align-items:
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
align-items: flex-start;
|
|
266
|
+
justify-content: space-between;
|
|
267
|
+
margin-bottom: 18px;
|
|
268
|
+
}
|
|
269
|
+
.gs-header-left {
|
|
270
|
+
display: flex;
|
|
271
|
+
gap: 12px;
|
|
272
|
+
align-items: flex-start;
|
|
273
|
+
}
|
|
274
|
+
.gs-header-left svg {
|
|
275
|
+
flex-shrink: 0;
|
|
276
|
+
margin-top: 2px;
|
|
277
|
+
}
|
|
278
|
+
.gs-title {
|
|
279
|
+
font-size: 16px;
|
|
267
280
|
color: var(--text-primary);
|
|
268
|
-
|
|
281
|
+
display: block;
|
|
282
|
+
margin-bottom: 4px;
|
|
269
283
|
}
|
|
270
|
-
.
|
|
284
|
+
.gs-subtitle {
|
|
271
285
|
font-size: 13px;
|
|
272
286
|
color: var(--text-secondary);
|
|
273
|
-
line-height: 1.
|
|
274
|
-
|
|
287
|
+
line-height: 1.5;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* API Key Section */
|
|
291
|
+
.gs-key-section {
|
|
292
|
+
background: var(--bg-surface);
|
|
293
|
+
border: 1px solid var(--border);
|
|
294
|
+
border-radius: var(--radius);
|
|
295
|
+
padding: 18px 20px;
|
|
296
|
+
margin-bottom: 18px;
|
|
297
|
+
}
|
|
298
|
+
.gs-key-row {
|
|
299
|
+
display: flex;
|
|
300
|
+
gap: 12px;
|
|
301
|
+
align-items: flex-end;
|
|
302
|
+
}
|
|
303
|
+
.gs-key-input-wrap {
|
|
304
|
+
flex: 1;
|
|
305
|
+
}
|
|
306
|
+
.gs-key-label {
|
|
307
|
+
display: block;
|
|
308
|
+
font-size: 12px;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
color: var(--text-secondary);
|
|
311
|
+
margin-bottom: 6px;
|
|
312
|
+
}
|
|
313
|
+
.gs-key-field {
|
|
314
|
+
display: flex;
|
|
315
|
+
gap: 10px;
|
|
316
|
+
}
|
|
317
|
+
.gs-input {
|
|
318
|
+
flex: 1;
|
|
319
|
+
min-width: 0;
|
|
320
|
+
}
|
|
321
|
+
.gs-validate-btn {
|
|
322
|
+
white-space: nowrap;
|
|
323
|
+
flex-shrink: 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* Balance Display */
|
|
327
|
+
.gs-balance {
|
|
328
|
+
background: var(--bg-surface);
|
|
329
|
+
border: 1px solid var(--border);
|
|
330
|
+
border-radius: var(--radius);
|
|
331
|
+
padding: 18px 20px;
|
|
332
|
+
margin-bottom: 18px;
|
|
333
|
+
}
|
|
334
|
+
.gs-balance-grid {
|
|
335
|
+
display: grid;
|
|
336
|
+
grid-template-columns: repeat(4, 1fr);
|
|
337
|
+
gap: 16px;
|
|
338
|
+
margin-bottom: 12px;
|
|
339
|
+
}
|
|
340
|
+
.gs-balance-item {
|
|
341
|
+
display: flex;
|
|
342
|
+
flex-direction: column;
|
|
343
|
+
gap: 4px;
|
|
344
|
+
}
|
|
345
|
+
.gs-balance-label {
|
|
346
|
+
font-size: 11px;
|
|
347
|
+
font-weight: 600;
|
|
348
|
+
color: var(--text-muted);
|
|
349
|
+
text-transform: uppercase;
|
|
350
|
+
letter-spacing: .5px;
|
|
275
351
|
}
|
|
276
|
-
.
|
|
352
|
+
.gs-balance-value {
|
|
353
|
+
font-size: 18px;
|
|
354
|
+
font-weight: 700;
|
|
277
355
|
color: var(--text-primary);
|
|
356
|
+
font-family: var(--font-mono);
|
|
357
|
+
}
|
|
358
|
+
.gs-balance-green {
|
|
359
|
+
color: var(--green);
|
|
360
|
+
}
|
|
361
|
+
.gs-connected-badge {
|
|
362
|
+
display: flex;
|
|
363
|
+
align-items: center;
|
|
364
|
+
gap: 6px;
|
|
365
|
+
font-size: 12px;
|
|
366
|
+
font-weight: 600;
|
|
367
|
+
color: var(--green);
|
|
278
368
|
}
|
|
279
|
-
.
|
|
369
|
+
.gs-change-key {
|
|
370
|
+
margin-left: 8px;
|
|
371
|
+
color: var(--text-muted);
|
|
372
|
+
font-size: 11px;
|
|
373
|
+
font-weight: 400;
|
|
374
|
+
cursor: pointer;
|
|
375
|
+
text-decoration: underline;
|
|
376
|
+
text-underline-offset: 2px;
|
|
377
|
+
}
|
|
378
|
+
.gs-change-key:hover {
|
|
379
|
+
color: var(--accent);
|
|
380
|
+
}
|
|
381
|
+
.btn-link {
|
|
382
|
+
background: none;
|
|
383
|
+
border: none;
|
|
384
|
+
padding: 0;
|
|
385
|
+
font-family: inherit;
|
|
386
|
+
cursor: pointer;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* 3-Step Flow */
|
|
390
|
+
.gs-steps {
|
|
280
391
|
display: grid;
|
|
281
392
|
grid-template-columns: repeat(3, 1fr);
|
|
282
393
|
gap: 14px;
|
|
283
|
-
margin-bottom: 14px;
|
|
284
394
|
}
|
|
285
|
-
.
|
|
395
|
+
.gs-step {
|
|
286
396
|
display: flex;
|
|
287
397
|
gap: 12px;
|
|
288
398
|
padding: 14px;
|
|
289
399
|
background: var(--bg-surface);
|
|
290
400
|
border-radius: var(--radius);
|
|
291
401
|
border: 1px solid var(--border);
|
|
402
|
+
transition: border-color .2s;
|
|
403
|
+
}
|
|
404
|
+
.gs-step.gs-step-active {
|
|
405
|
+
border-color: var(--accent);
|
|
406
|
+
background: var(--accent-subtle);
|
|
292
407
|
}
|
|
293
|
-
.
|
|
408
|
+
.gs-step.gs-step-done {
|
|
409
|
+
border-color: var(--green);
|
|
410
|
+
}
|
|
411
|
+
.gs-step-num {
|
|
294
412
|
flex-shrink: 0;
|
|
295
413
|
width: 28px;
|
|
296
414
|
height: 28px;
|
|
@@ -303,40 +421,32 @@ a:hover { color: var(--accent-hover); }
|
|
|
303
421
|
font-size: 13px;
|
|
304
422
|
font-weight: 700;
|
|
305
423
|
}
|
|
306
|
-
.
|
|
424
|
+
.gs-step-done .gs-step-num {
|
|
425
|
+
background: var(--green-subtle);
|
|
426
|
+
color: var(--green);
|
|
427
|
+
}
|
|
428
|
+
.gs-step-title {
|
|
307
429
|
font-size: 13px;
|
|
308
430
|
font-weight: 700;
|
|
309
431
|
color: var(--text-primary);
|
|
310
432
|
margin-bottom: 4px;
|
|
311
433
|
}
|
|
312
|
-
.
|
|
434
|
+
.gs-step-desc {
|
|
313
435
|
font-size: 12px;
|
|
314
436
|
color: var(--text-muted);
|
|
315
437
|
line-height: 1.5;
|
|
316
438
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
.hiw-note strong {
|
|
329
|
-
color: var(--amber);
|
|
330
|
-
}
|
|
331
|
-
.hiw-note a {
|
|
332
|
-
color: var(--amber);
|
|
333
|
-
font-weight: 600;
|
|
334
|
-
text-decoration: underline;
|
|
335
|
-
text-underline-offset: 2px;
|
|
336
|
-
}
|
|
337
|
-
.hiw-note svg {
|
|
338
|
-
flex-shrink: 0;
|
|
339
|
-
color: var(--amber);
|
|
439
|
+
|
|
440
|
+
@media (max-width: 768px) {
|
|
441
|
+
.gs-steps {
|
|
442
|
+
grid-template-columns: 1fr;
|
|
443
|
+
}
|
|
444
|
+
.gs-balance-grid {
|
|
445
|
+
grid-template-columns: repeat(2, 1fr);
|
|
446
|
+
}
|
|
447
|
+
.gs-key-field {
|
|
448
|
+
flex-direction: column;
|
|
449
|
+
}
|
|
340
450
|
}
|
|
341
451
|
|
|
342
452
|
/* ===== Model Rec Explainer ===== */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-smartmeter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "AI cost optimization for OpenClaw - analyze usage and reduce costs by 48%",
|
|
5
5
|
"main": "src/cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -54,4 +54,4 @@
|
|
|
54
54
|
"fs-extra": "^11.2.0",
|
|
55
55
|
"open": "^9.1.0"
|
|
56
56
|
}
|
|
57
|
-
}
|
|
57
|
+
}
|