@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.1-beta.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/bin/alphaclaw.js +1 -31
- package/lib/public/assets/icons/google_icon.svg +8 -0
- package/lib/public/css/explorer.css +53 -0
- package/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +205 -109
- package/lib/public/js/components/credentials-modal.js +36 -8
- package/lib/public/js/components/file-tree.js +212 -22
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +47 -6
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/status-banners.js +11 -6
- package/lib/public/js/components/file-viewer/toolbar.js +56 -1
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
- package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
- package/lib/public/js/components/google/account-row.js +131 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
- package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
- package/lib/public/js/components/google/index.js +553 -0
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/google/use-google-accounts.js +41 -0
- package/lib/public/js/components/icons.js +26 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +48 -20
- package/lib/public/js/components/sidebar.js +93 -75
- package/lib/public/js/components/toast.js +11 -7
- package/lib/public/js/components/usage-tab/constants.js +31 -0
- package/lib/public/js/components/usage-tab/formatters.js +24 -0
- package/lib/public/js/components/usage-tab/index.js +72 -0
- package/lib/public/js/components/usage-tab/overview-section.js +147 -0
- package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
- package/lib/public/js/components/webhooks.js +182 -129
- package/lib/public/js/lib/api.js +178 -9
- package/lib/public/js/lib/browse-file-policies.js +29 -11
- package/lib/public/js/lib/format.js +71 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
- package/lib/public/shared/browse-file-policies.json +13 -0
- package/lib/server/constants.js +47 -7
- package/lib/server/gmail-push.js +109 -0
- package/lib/server/gmail-serve.js +254 -0
- package/lib/server/gmail-watch.js +725 -0
- package/lib/server/google-state.js +317 -0
- package/lib/server/helpers.js +17 -11
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/onboarding/github.js +21 -2
- package/lib/server/onboarding/index.js +1 -3
- package/lib/server/onboarding/openclaw.js +3 -0
- package/lib/server/onboarding/workspace.js +40 -0
- package/lib/server/routes/browse/index.js +90 -2
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +433 -213
- package/lib/server/routes/system.js +107 -0
- package/lib/server/routes/usage.js +29 -2
- package/lib/server/routes/webhooks.js +52 -17
- package/lib/server/usage-db.js +283 -15
- package/lib/server/watchdog.js +66 -0
- package/lib/server/webhook-middleware.js +99 -1
- package/lib/server/webhooks.js +214 -65
- package/lib/server.js +27 -0
- package/lib/setup/gitignore +6 -0
- package/lib/setup/hourly-git-sync.sh +29 -2
- package/package.json +1 -1
- package/lib/public/js/components/google.js +0 -228
- package/lib/public/js/components/usage-tab.js +0 -531
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { fetchGoogleAccounts } from "../../lib/api.js";
|
|
3
|
+
|
|
4
|
+
export const useGoogleAccounts = ({ gatewayStatus }) => {
|
|
5
|
+
const [accounts, setAccounts] = useState([]);
|
|
6
|
+
const [loading, setLoading] = useState(true);
|
|
7
|
+
const [hasCompanyCredentials, setHasCompanyCredentials] = useState(false);
|
|
8
|
+
const [hasPersonalCredentials, setHasPersonalCredentials] = useState(false);
|
|
9
|
+
|
|
10
|
+
const refreshAccounts = useCallback(async () => {
|
|
11
|
+
setLoading(true);
|
|
12
|
+
try {
|
|
13
|
+
const data = await fetchGoogleAccounts();
|
|
14
|
+
if (data.ok) {
|
|
15
|
+
setAccounts(Array.isArray(data.accounts) ? data.accounts : []);
|
|
16
|
+
setHasCompanyCredentials(Boolean(data.hasCompanyCredentials));
|
|
17
|
+
setHasPersonalCredentials(Boolean(data.hasPersonalCredentials));
|
|
18
|
+
}
|
|
19
|
+
} finally {
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
refreshAccounts();
|
|
26
|
+
}, [refreshAccounts]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (gatewayStatus === "running") {
|
|
30
|
+
refreshAccounts();
|
|
31
|
+
}
|
|
32
|
+
}, [gatewayStatus, refreshAccounts]);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
accounts,
|
|
36
|
+
loading,
|
|
37
|
+
hasCompanyCredentials,
|
|
38
|
+
hasPersonalCredentials,
|
|
39
|
+
refreshAccounts,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -248,3 +248,29 @@ export const LockLineIcon = ({ className = "" }) => html`
|
|
|
248
248
|
/>
|
|
249
249
|
</svg>
|
|
250
250
|
`;
|
|
251
|
+
|
|
252
|
+
export const DeleteBinLineIcon = ({ className = "" }) => html`
|
|
253
|
+
<svg
|
|
254
|
+
class=${className}
|
|
255
|
+
viewBox="0 0 24 24"
|
|
256
|
+
fill="currentColor"
|
|
257
|
+
aria-hidden="true"
|
|
258
|
+
>
|
|
259
|
+
<path
|
|
260
|
+
d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z"
|
|
261
|
+
/>
|
|
262
|
+
</svg>
|
|
263
|
+
`;
|
|
264
|
+
|
|
265
|
+
export const RestartLineIcon = ({ className = "" }) => html`
|
|
266
|
+
<svg
|
|
267
|
+
class=${className}
|
|
268
|
+
viewBox="0 0 24 24"
|
|
269
|
+
fill="currentColor"
|
|
270
|
+
aria-hidden="true"
|
|
271
|
+
>
|
|
272
|
+
<path
|
|
273
|
+
d="M18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"
|
|
274
|
+
/>
|
|
275
|
+
</svg>
|
|
276
|
+
`;
|
|
@@ -68,7 +68,7 @@ export function ScopePicker({ scopes, onToggle, apiStatus, loading }) {
|
|
|
68
68
|
<button
|
|
69
69
|
type="button"
|
|
70
70
|
onclick=${() => setShowAll((prev) => !prev)}
|
|
71
|
-
class="text-xs text-gray-500 hover:text-gray-300"
|
|
71
|
+
class="ml-3 text-xs text-gray-500 hover:text-gray-300"
|
|
72
72
|
>
|
|
73
73
|
${showAll ? 'Show fewer services' : `Show more services (${SERVICES.length - kVisibleCount})`}
|
|
74
74
|
</button>
|
|
@@ -2,6 +2,7 @@ import { h } from "https://esm.sh/preact";
|
|
|
2
2
|
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { fetchBrowseGitSummary, syncBrowseChanges } from "../lib/api.js";
|
|
5
|
+
import { formatLocaleDateTime } from "../lib/format.js";
|
|
5
6
|
import { ActionButton } from "./action-button.js";
|
|
6
7
|
import { GitBranchLineIcon, GithubFillIcon } from "./icons.js";
|
|
7
8
|
import { LoadingSpinner } from "./loading-spinner.js";
|
|
@@ -13,12 +14,10 @@ const kSyncCommitFileNameLimit = 4;
|
|
|
13
14
|
const kCommitHistoryLimit = 12;
|
|
14
15
|
|
|
15
16
|
const formatCommitTime = (unixSeconds) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
return "";
|
|
21
|
-
}
|
|
17
|
+
return formatLocaleDateTime(unixSeconds, {
|
|
18
|
+
fallback: "",
|
|
19
|
+
valueIsUnixSeconds: true,
|
|
20
|
+
});
|
|
22
21
|
};
|
|
23
22
|
|
|
24
23
|
const getRepoName = (summary) => {
|
|
@@ -43,8 +42,8 @@ const getChangedFilePresentation = (changedFile) => {
|
|
|
43
42
|
return {
|
|
44
43
|
statusLabel: "D",
|
|
45
44
|
statusClass: "is-deleted",
|
|
46
|
-
rowClass: "",
|
|
47
|
-
canOpen:
|
|
45
|
+
rowClass: "is-clickable",
|
|
46
|
+
canOpen: true,
|
|
48
47
|
};
|
|
49
48
|
}
|
|
50
49
|
return {
|
|
@@ -62,6 +61,14 @@ const formatDelta = (value, prefix) => {
|
|
|
62
61
|
return `${prefix}${numericValue}`;
|
|
63
62
|
};
|
|
64
63
|
|
|
64
|
+
const isDirectoryChangePath = (changedPath, statusKind) => {
|
|
65
|
+
const safePath = String(changedPath || "").trim();
|
|
66
|
+
const safeStatusKind = String(statusKind || "").toUpperCase();
|
|
67
|
+
if (!safePath) return false;
|
|
68
|
+
if (safePath.endsWith("/")) return true;
|
|
69
|
+
return safeStatusKind === "U" && safePath.endsWith("\\");
|
|
70
|
+
};
|
|
71
|
+
|
|
65
72
|
const getRemoteSyncPresentation = (summary) => {
|
|
66
73
|
const safeState = String(summary?.syncState || "").trim();
|
|
67
74
|
const aheadCount = Number(summary?.aheadCount) || 0;
|
|
@@ -108,31 +115,41 @@ const getRemoteSyncPresentation = (summary) => {
|
|
|
108
115
|
};
|
|
109
116
|
};
|
|
110
117
|
|
|
111
|
-
const buildSyncCommitMessage = (
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const totalCount = filePaths.length;
|
|
118
|
+
const buildSyncCommitMessage = (summary) => {
|
|
119
|
+
const changedFiles = Array.isArray(summary?.changedFiles) ? summary.changedFiles : [];
|
|
120
|
+
const changedFilesCount = Number(summary?.changedFilesCount) || 0;
|
|
121
|
+
const filePaths = changedFiles
|
|
122
|
+
.map((file) => String(file?.path || "").trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
const totalCount = changedFilesCount || filePaths.length;
|
|
118
125
|
if (totalCount <= 0) return "sync changes";
|
|
119
126
|
|
|
120
|
-
const fileNames = filePaths
|
|
127
|
+
const fileNames = filePaths
|
|
128
|
+
.map((filePath) => filePath.split("/").filter(Boolean).pop() || filePath);
|
|
121
129
|
const uniqueFileNames = Array.from(new Set(fileNames));
|
|
130
|
+
if (uniqueFileNames.length <= 0) {
|
|
131
|
+
const noun = totalCount === 1 ? "file" : "files";
|
|
132
|
+
return `Edited ${totalCount} ${noun}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
const shownFileNames = uniqueFileNames.slice(0, kSyncCommitFileNameLimit);
|
|
123
|
-
const remainingCount = Math.max(0,
|
|
136
|
+
const remainingCount = Math.max(0, totalCount - shownFileNames.length);
|
|
124
137
|
const noun = totalCount === 1 ? "file" : "files";
|
|
125
138
|
const suffix = remainingCount > 0 ? ` +${remainingCount} more` : "";
|
|
126
139
|
return `Edited ${totalCount} ${noun} - ${shownFileNames.join(", ")}${suffix}`;
|
|
127
140
|
};
|
|
128
141
|
|
|
129
|
-
export const SidebarGitPanel = ({
|
|
142
|
+
export const SidebarGitPanel = ({
|
|
143
|
+
onSelectFile = () => {},
|
|
144
|
+
isActive = true,
|
|
145
|
+
}) => {
|
|
130
146
|
const [loading, setLoading] = useState(true);
|
|
131
147
|
const [syncing, setSyncing] = useState(false);
|
|
132
148
|
const [error, setError] = useState("");
|
|
133
149
|
const [summary, setSummary] = useState(null);
|
|
134
150
|
|
|
135
151
|
useEffect(() => {
|
|
152
|
+
if (!isActive) return () => {};
|
|
136
153
|
let active = true;
|
|
137
154
|
let intervalId = null;
|
|
138
155
|
|
|
@@ -164,7 +181,7 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
164
181
|
if (intervalId) window.clearInterval(intervalId);
|
|
165
182
|
window.removeEventListener("alphaclaw:browse-file-saved", handleFileSaved);
|
|
166
183
|
};
|
|
167
|
-
}, []);
|
|
184
|
+
}, [isActive]);
|
|
168
185
|
|
|
169
186
|
if (loading) {
|
|
170
187
|
return html`
|
|
@@ -194,7 +211,7 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
194
211
|
if (!canSyncChanges || syncing) return;
|
|
195
212
|
try {
|
|
196
213
|
setSyncing(true);
|
|
197
|
-
const commitMessage = buildSyncCommitMessage(summary
|
|
214
|
+
const commitMessage = buildSyncCommitMessage(summary);
|
|
198
215
|
const syncResult = await syncBrowseChanges(commitMessage);
|
|
199
216
|
if (syncResult?.committed || syncResult?.pushed) {
|
|
200
217
|
window.dispatchEvent(new CustomEvent("alphaclaw:browse-git-synced"));
|
|
@@ -270,6 +287,17 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
270
287
|
title=${changedPath}
|
|
271
288
|
onclick=${() => {
|
|
272
289
|
if (!presentation.canOpen || !changedPath) return;
|
|
290
|
+
const directorySelection = isDirectoryChangePath(
|
|
291
|
+
changedPath,
|
|
292
|
+
changedFile?.statusKind,
|
|
293
|
+
);
|
|
294
|
+
if (directorySelection) {
|
|
295
|
+
onSelectFile(changedPath, {
|
|
296
|
+
directory: true,
|
|
297
|
+
preservePreview: true,
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
273
301
|
onSelectFile(changedPath, { view: "diff" });
|
|
274
302
|
}}
|
|
275
303
|
>
|
|
@@ -89,6 +89,8 @@ export const AppSidebar = ({
|
|
|
89
89
|
const layoutElement = browseLayoutRef.current;
|
|
90
90
|
if (!layoutElement || typeof ResizeObserver === "undefined") return () => {};
|
|
91
91
|
const observer = new ResizeObserver(() => {
|
|
92
|
+
const layoutRect = layoutElement.getBoundingClientRect();
|
|
93
|
+
if (layoutRect.height <= 0) return;
|
|
92
94
|
setBrowseBottomPanelHeightPx((currentHeight) =>
|
|
93
95
|
getClampedBrowseBottomPanelHeight(currentHeight),
|
|
94
96
|
);
|
|
@@ -172,83 +174,99 @@ export const AppSidebar = ({
|
|
|
172
174
|
<${FolderLineIcon} className="sidebar-tab-icon" />
|
|
173
175
|
</button>
|
|
174
176
|
</div>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
177
|
+
<div
|
|
178
|
+
style=${{
|
|
179
|
+
display: sidebarTab === "menu" ? "flex" : "none",
|
|
180
|
+
flexDirection: "column",
|
|
181
|
+
flex: "1 1 auto",
|
|
182
|
+
minHeight: 0,
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
${navSections.map(
|
|
186
|
+
(section) => html`
|
|
187
|
+
<div class="sidebar-label">${section.label}</div>
|
|
188
|
+
<nav class="sidebar-nav">
|
|
189
|
+
${section.items.map(
|
|
190
|
+
(item) => html`
|
|
191
|
+
<a
|
|
192
|
+
class=${selectedNavId === item.id ? "active" : ""}
|
|
193
|
+
onclick=${() => onSelectNavItem(item.id)}
|
|
194
|
+
>
|
|
195
|
+
${item.label}
|
|
196
|
+
</a>
|
|
197
|
+
`,
|
|
198
|
+
)}
|
|
199
|
+
</nav>
|
|
200
|
+
`,
|
|
201
|
+
)}
|
|
202
|
+
<div class="sidebar-footer">
|
|
203
|
+
${acHasUpdate && acLatest && !acDismissed
|
|
204
|
+
? html`
|
|
205
|
+
<${UpdateActionButton}
|
|
206
|
+
onClick=${onAcUpdate}
|
|
207
|
+
loading=${acUpdating}
|
|
208
|
+
warning=${true}
|
|
209
|
+
idleLabel=${`Update to v${acLatest}`}
|
|
210
|
+
loadingLabel="Updating..."
|
|
211
|
+
className="w-full justify-center"
|
|
202
212
|
/>
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
213
|
+
`
|
|
214
|
+
: null}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div
|
|
218
|
+
style=${{
|
|
219
|
+
display: sidebarTab === "browse" ? "flex" : "none",
|
|
220
|
+
flexDirection: "column",
|
|
221
|
+
flex: "1 1 auto",
|
|
222
|
+
minHeight: 0,
|
|
223
|
+
overflow: "hidden",
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<div class="sidebar-browse-layout" ref=${browseLayoutRef}>
|
|
227
|
+
<div
|
|
228
|
+
class="sidebar-browse-panel"
|
|
229
|
+
>
|
|
230
|
+
<${FileTree}
|
|
231
|
+
onSelectFile=${onSelectBrowseFile}
|
|
232
|
+
selectedPath=${selectedBrowsePath}
|
|
233
|
+
onPreviewFile=${onPreviewBrowseFile}
|
|
234
|
+
isActive=${sidebarTab === "browse"}
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
<div
|
|
238
|
+
class=${`sidebar-browse-resizer ${isResizingBrowsePanels ? "is-resizing" : ""}`}
|
|
239
|
+
onpointerdown=${onBrowsePanelResizerPointerDown}
|
|
240
|
+
role="separator"
|
|
241
|
+
aria-orientation="horizontal"
|
|
242
|
+
aria-label="Resize browse and git panels"
|
|
243
|
+
></div>
|
|
244
|
+
<div class="sidebar-browse-bottom">
|
|
245
|
+
<div
|
|
246
|
+
class="sidebar-browse-bottom-inner"
|
|
247
|
+
ref=${browseBottomPanelRef}
|
|
248
|
+
style=${{ height: `${browseBottomPanelHeightPx}px` }}
|
|
249
|
+
>
|
|
250
|
+
<${SidebarGitPanel}
|
|
251
|
+
onSelectFile=${onSelectBrowseFile}
|
|
252
|
+
isActive=${sidebarTab === "browse"}
|
|
253
|
+
/>
|
|
254
|
+
${acHasUpdate && acLatest && !acDismissed
|
|
255
|
+
? html`
|
|
256
|
+
<${UpdateActionButton}
|
|
257
|
+
onClick=${onAcUpdate}
|
|
258
|
+
loading=${acUpdating}
|
|
259
|
+
warning=${true}
|
|
260
|
+
idleLabel=${`Update to v${acLatest}`}
|
|
261
|
+
loadingLabel="Updating..."
|
|
262
|
+
className="w-full justify-center"
|
|
263
|
+
/>
|
|
264
|
+
`
|
|
265
|
+
: null}
|
|
249
266
|
</div>
|
|
250
|
-
|
|
251
|
-
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
252
270
|
</div>
|
|
253
271
|
`;
|
|
254
272
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h } from 'https://esm.sh/preact';
|
|
2
2
|
import { useState, useEffect } from 'https://esm.sh/preact/hooks';
|
|
3
|
+
import { createPortal } from 'https://esm.sh/preact/compat';
|
|
3
4
|
import htm from 'https://esm.sh/htm';
|
|
4
5
|
const html = htm.bind(h);
|
|
5
6
|
|
|
@@ -50,11 +51,14 @@ export function ToastContainer({
|
|
|
50
51
|
|
|
51
52
|
if (toasts.length === 0) return null;
|
|
52
53
|
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
${t.text
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
return createPortal(
|
|
55
|
+
html`<div class=${className} style=${{ zIndex: 70 }}>
|
|
56
|
+
${toasts.map(t => html`
|
|
57
|
+
<div key=${t.id} class="${kToastClassByType[normalizeToastType(t.type)]} px-4 py-2 rounded-lg text-sm">
|
|
58
|
+
${t.text}
|
|
59
|
+
</div>
|
|
60
|
+
`)}
|
|
61
|
+
</div>`,
|
|
62
|
+
document.body,
|
|
63
|
+
);
|
|
60
64
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const kColorPalette = [
|
|
2
|
+
"#7dd3fc",
|
|
3
|
+
"#22d3ee",
|
|
4
|
+
"#34d399",
|
|
5
|
+
"#fbbf24",
|
|
6
|
+
"#fb7185",
|
|
7
|
+
"#a78bfa",
|
|
8
|
+
"#f472b6",
|
|
9
|
+
"#60a5fa",
|
|
10
|
+
"#4ade80",
|
|
11
|
+
"#f97316",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const kBadgeToneClass = {
|
|
15
|
+
cyan: "border-cyan-400/30 text-cyan-300 bg-cyan-400/10",
|
|
16
|
+
blue: "border-blue-400/30 text-blue-300 bg-blue-400/10",
|
|
17
|
+
purple: "border-purple-400/30 text-purple-300 bg-purple-400/10",
|
|
18
|
+
gray: "border-gray-400/30 text-gray-400 bg-gray-400/10",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const kRangeOptions = [
|
|
22
|
+
{ label: "7d", value: 7 },
|
|
23
|
+
{ label: "30d", value: 30 },
|
|
24
|
+
{ label: "90d", value: 90 },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const kDefaultUsageDays = 30;
|
|
28
|
+
export const kDefaultUsageMetric = "tokens";
|
|
29
|
+
export const kUsageDaysUiSettingKey = "usageDays";
|
|
30
|
+
export const kUsageMetricUiSettingKey = "usageMetric";
|
|
31
|
+
export const kUsageSourceOrder = ["chat", "hooks", "cron"];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { kColorPalette } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
export const toLocalDayKey = (value) => {
|
|
4
|
+
const d = value instanceof Date ? value : new Date(value ?? Date.now());
|
|
5
|
+
const year = d.getFullYear();
|
|
6
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
7
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
8
|
+
return `${year}-${month}-${day}`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const toChartColor = (key) => {
|
|
12
|
+
const raw = String(key || "");
|
|
13
|
+
let hash = 0;
|
|
14
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
15
|
+
hash = ((hash << 5) - hash + raw.charCodeAt(index)) | 0;
|
|
16
|
+
}
|
|
17
|
+
return kColorPalette[Math.abs(hash) % kColorPalette.length];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const renderSourceLabel = (source) => {
|
|
21
|
+
if (source === "hooks") return "Hooks";
|
|
22
|
+
if (source === "cron") return "Cron";
|
|
23
|
+
return "Chat";
|
|
24
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { ActionButton } from "../action-button.js";
|
|
4
|
+
import { PageHeader } from "../page-header.js";
|
|
5
|
+
import { OverviewSection } from "./overview-section.js";
|
|
6
|
+
import { SessionsSection } from "./sessions-section.js";
|
|
7
|
+
import { useUsageTab } from "./use-usage-tab.js";
|
|
8
|
+
|
|
9
|
+
const html = htm.bind(h);
|
|
10
|
+
|
|
11
|
+
export const UsageTab = ({ sessionId = "" }) => {
|
|
12
|
+
const { state, actions } = useUsageTab({ sessionId });
|
|
13
|
+
|
|
14
|
+
const handleToggleSession = (itemSessionId, isOpen) => {
|
|
15
|
+
if (isOpen) {
|
|
16
|
+
actions.setExpandedSessionIds((currentValue) =>
|
|
17
|
+
currentValue.includes(itemSessionId) ? currentValue : [...currentValue, itemSessionId],
|
|
18
|
+
);
|
|
19
|
+
if (!state.sessionDetailById[itemSessionId] && !state.loadingDetailById[itemSessionId]) {
|
|
20
|
+
actions.loadSessionDetail(itemSessionId);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
actions.setExpandedSessionIds((currentValue) =>
|
|
25
|
+
currentValue.filter((value) => value !== itemSessionId),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return html`
|
|
30
|
+
<div class="space-y-4">
|
|
31
|
+
<${PageHeader}
|
|
32
|
+
title="Usage"
|
|
33
|
+
actions=${html`
|
|
34
|
+
<${ActionButton}
|
|
35
|
+
onClick=${actions.loadSummary}
|
|
36
|
+
loading=${state.loadingSummary}
|
|
37
|
+
tone="secondary"
|
|
38
|
+
size="sm"
|
|
39
|
+
idleLabel="Refresh"
|
|
40
|
+
loadingMode="inline"
|
|
41
|
+
/>
|
|
42
|
+
`}
|
|
43
|
+
/>
|
|
44
|
+
${state.error
|
|
45
|
+
? html`<div class="text-xs text-red-300 bg-red-950/30 border border-red-900 rounded px-3 py-2">
|
|
46
|
+
${state.error}
|
|
47
|
+
</div>`
|
|
48
|
+
: null}
|
|
49
|
+
${state.loadingSummary && !state.summary
|
|
50
|
+
? html`<div class="text-sm text-[var(--text-muted)]">Loading usage summary...</div>`
|
|
51
|
+
: html`
|
|
52
|
+
<${OverviewSection}
|
|
53
|
+
summary=${state.summary}
|
|
54
|
+
periodSummary=${state.periodSummary}
|
|
55
|
+
metric=${state.metric}
|
|
56
|
+
days=${state.days}
|
|
57
|
+
overviewCanvasRef=${state.overviewCanvasRef}
|
|
58
|
+
onDaysChange=${actions.setDays}
|
|
59
|
+
onMetricChange=${actions.setMetric}
|
|
60
|
+
/>
|
|
61
|
+
`}
|
|
62
|
+
<${SessionsSection}
|
|
63
|
+
sessions=${state.sessions}
|
|
64
|
+
loadingSessions=${state.loadingSessions}
|
|
65
|
+
expandedSessionIds=${state.expandedSessionIds}
|
|
66
|
+
loadingDetailById=${state.loadingDetailById}
|
|
67
|
+
sessionDetailById=${state.sessionDetailById}
|
|
68
|
+
onToggleSession=${handleToggleSession}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
`;
|
|
72
|
+
};
|