openclaw-smartmeter 0.4.0 → 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 +219 -32
- package/canvas-template/index.html +67 -23
- package/canvas-template/styles.css +155 -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) {
|
|
@@ -111,16 +112,17 @@ function normalizeApiData(api) {
|
|
|
111
112
|
/* ─── Render All ─── */
|
|
112
113
|
function renderAll() {
|
|
113
114
|
if (!analysisData) return;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
const safe = fn => { try { fn(); } catch (e) { console.error(`[SmartMeter] ${fn.name} error:`, e); } };
|
|
116
|
+
safe(updateKPIs);
|
|
117
|
+
safe(updateMetrics);
|
|
118
|
+
safe(updateCharts);
|
|
119
|
+
safe(updateRecommendations);
|
|
120
|
+
safe(updateModelRecommendations);
|
|
121
|
+
safe(updateOtherRecommendations);
|
|
122
|
+
safe(updateBudgetControls);
|
|
123
|
+
safe(updateModelDetails);
|
|
124
|
+
safe(updateLastUpdated);
|
|
125
|
+
safe(checkCostDataNotice);
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
/* ─── KPIs ─── */
|
|
@@ -440,7 +442,7 @@ function updateModelRecommendations() {
|
|
|
440
442
|
${alt.isRecommended ? '<span class="model-tag best-tag">RECOMMENDED</span>' : ''}
|
|
441
443
|
</div>
|
|
442
444
|
<div class="model-alt-meta">
|
|
443
|
-
Quality: ${renderQualityDots(alt.quality_score)} · ${escHtml(alt.speed)} · Best for: ${alt.best_for.join(', ')}
|
|
445
|
+
Quality: ${renderQualityDots(alt.quality_score)} · ${escHtml(alt.speed || '')} · Best for: ${(alt.best_for || []).join(', ')}
|
|
444
446
|
</div>
|
|
445
447
|
</div>
|
|
446
448
|
<div class="model-alt-cost">
|
|
@@ -827,6 +829,161 @@ function updateModelDetails() {
|
|
|
827
829
|
</table>`;
|
|
828
830
|
}
|
|
829
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
|
+
|
|
830
987
|
/* ─── OpenRouter Integration ─── */
|
|
831
988
|
async function checkOpenRouterConfig() {
|
|
832
989
|
try {
|
|
@@ -877,10 +1034,13 @@ async function fetchOpenRouterUsage() {
|
|
|
877
1034
|
/* ─── Config Modal ─── */
|
|
878
1035
|
function openConfigModal() {
|
|
879
1036
|
document.getElementById('configModal').style.display = 'flex';
|
|
880
|
-
document.getElementById('apiKeyInput')
|
|
1037
|
+
const input = document.getElementById('apiKeyInput');
|
|
1038
|
+
const stored = localStorage.getItem('smartmeter_openrouter_key');
|
|
1039
|
+
if (stored && !input.value) input.value = stored;
|
|
1040
|
+
input.focus();
|
|
881
1041
|
const status = document.getElementById('configStatus');
|
|
882
1042
|
status.className = 'config-status';
|
|
883
|
-
status.style.display
|
|
1043
|
+
status.style.removeProperty('display');
|
|
884
1044
|
}
|
|
885
1045
|
function closeConfigModal() {
|
|
886
1046
|
document.getElementById('configModal').style.display = 'none';
|
|
@@ -890,22 +1050,28 @@ async function saveApiKey() {
|
|
|
890
1050
|
const key = document.getElementById('apiKeyInput').value.trim();
|
|
891
1051
|
const status = document.getElementById('configStatus');
|
|
892
1052
|
|
|
1053
|
+
function showStatus(msg, type) {
|
|
1054
|
+
status.textContent = msg;
|
|
1055
|
+
status.className = 'config-status ' + type;
|
|
1056
|
+
status.style.removeProperty('display');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
893
1059
|
if (!key) {
|
|
894
|
-
|
|
895
|
-
status.className = 'config-status error';
|
|
1060
|
+
showStatus('Please enter an API key.', 'error');
|
|
896
1061
|
return;
|
|
897
1062
|
}
|
|
898
1063
|
|
|
899
1064
|
if (!key.startsWith('sk-or-')) {
|
|
900
|
-
|
|
901
|
-
status.className = 'config-status error';
|
|
1065
|
+
showStatus('Invalid format — key should start with sk-or-', 'error');
|
|
902
1066
|
return;
|
|
903
1067
|
}
|
|
904
1068
|
|
|
905
|
-
|
|
906
|
-
status.className = 'config-status';
|
|
907
|
-
status.style.display = 'block';
|
|
1069
|
+
showStatus('Validating…', 'validating');
|
|
908
1070
|
|
|
1071
|
+
let validated = false;
|
|
1072
|
+
let errorMsg = '';
|
|
1073
|
+
|
|
1074
|
+
// Try API server first
|
|
909
1075
|
try {
|
|
910
1076
|
const res = await fetch(`${API_BASE_URL}/api/config/openrouter-key`, {
|
|
911
1077
|
method: 'POST',
|
|
@@ -914,20 +1080,41 @@ async function saveApiKey() {
|
|
|
914
1080
|
});
|
|
915
1081
|
const json = await res.json();
|
|
916
1082
|
if (json.success) {
|
|
917
|
-
|
|
918
|
-
status.className = 'config-status success';
|
|
919
|
-
setTimeout(() => {
|
|
920
|
-
closeConfigModal();
|
|
921
|
-
fetchOpenRouterUsage();
|
|
922
|
-
navigateTo('openrouter');
|
|
923
|
-
}, 1200);
|
|
1083
|
+
validated = true;
|
|
924
1084
|
} else {
|
|
925
|
-
|
|
926
|
-
status.className = 'config-status error';
|
|
1085
|
+
errorMsg = json.error || 'Validation failed';
|
|
927
1086
|
}
|
|
928
|
-
} catch (
|
|
929
|
-
|
|
930
|
-
|
|
1087
|
+
} catch (_) {
|
|
1088
|
+
// API server not available — validate directly against OpenRouter
|
|
1089
|
+
try {
|
|
1090
|
+
const res = await fetch('https://openrouter.ai/api/v1/auth/key', {
|
|
1091
|
+
headers: { 'Authorization': `Bearer ${key}` }
|
|
1092
|
+
});
|
|
1093
|
+
if (res.ok) {
|
|
1094
|
+
const data = await res.json();
|
|
1095
|
+
validated = !!(data && data.data);
|
|
1096
|
+
if (!validated) errorMsg = 'Key not recognized by OpenRouter';
|
|
1097
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
1098
|
+
errorMsg = 'Invalid API key — authentication failed';
|
|
1099
|
+
} else {
|
|
1100
|
+
errorMsg = `OpenRouter returned status ${res.status}`;
|
|
1101
|
+
}
|
|
1102
|
+
} catch (e2) {
|
|
1103
|
+
errorMsg = 'Could not reach OpenRouter to validate — check your connection';
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (validated) {
|
|
1108
|
+
localStorage.setItem('smartmeter_openrouter_key', key);
|
|
1109
|
+
showStatus('✅ API key saved and validated!', 'success');
|
|
1110
|
+
setTimeout(() => {
|
|
1111
|
+
closeConfigModal();
|
|
1112
|
+
fetchOpenRouterUsage();
|
|
1113
|
+
// Also sync the Get Started card
|
|
1114
|
+
initGetStartedCard();
|
|
1115
|
+
}, 1200);
|
|
1116
|
+
} else {
|
|
1117
|
+
showStatus(`❌ ${errorMsg}`, 'error');
|
|
931
1118
|
}
|
|
932
1119
|
}
|
|
933
1120
|
|
|
@@ -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;
|
|
275
344
|
}
|
|
276
|
-
.
|
|
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;
|
|
351
|
+
}
|
|
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);
|
|
368
|
+
}
|
|
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;
|
|
278
377
|
}
|
|
279
|
-
.
|
|
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 ===== */
|
|
@@ -860,6 +970,11 @@ a:hover { color: var(--accent-hover); }
|
|
|
860
970
|
background: var(--green-subtle);
|
|
861
971
|
color: var(--green);
|
|
862
972
|
}
|
|
973
|
+
.config-status.validating {
|
|
974
|
+
display: block;
|
|
975
|
+
background: var(--indigo-subtle, rgba(99,102,241,.08));
|
|
976
|
+
color: var(--text-secondary);
|
|
977
|
+
}
|
|
863
978
|
|
|
864
979
|
/* Code block */
|
|
865
980
|
.code-block {
|
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
|
+
}
|