@chrysb/alphaclaw 0.4.6-beta.3 → 0.4.6-beta.5
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/lib/public/js/app.js +158 -1073
- package/lib/public/js/components/doctor/index.js +1 -2
- package/lib/public/js/components/general/index.js +155 -0
- package/lib/public/js/components/general/use-general-tab.js +233 -0
- package/lib/public/js/components/models-tab/index.js +286 -0
- package/lib/public/js/components/models-tab/provider-auth-card.js +369 -0
- package/lib/public/js/components/models-tab/use-models.js +262 -0
- package/lib/public/js/components/routes/browse-route.js +35 -0
- package/lib/public/js/components/routes/doctor-route.js +21 -0
- package/lib/public/js/components/routes/envars-route.js +11 -0
- package/lib/public/js/components/routes/general-route.js +45 -0
- package/lib/public/js/components/routes/index.js +11 -0
- package/lib/public/js/components/routes/models-route.js +11 -0
- package/lib/public/js/components/routes/providers-route.js +11 -0
- package/lib/public/js/components/routes/route-redirect.js +10 -0
- package/lib/public/js/components/routes/telegram-route.js +11 -0
- package/lib/public/js/components/routes/usage-route.js +15 -0
- package/lib/public/js/components/routes/watchdog-route.js +32 -0
- package/lib/public/js/components/routes/webhooks-route.js +43 -0
- package/lib/public/js/components/sidebar.js +2 -3
- package/lib/public/js/components/usage-tab/constants.js +1 -1
- package/lib/public/js/components/usage-tab/overview-section.js +124 -50
- package/lib/public/js/components/usage-tab/use-usage-tab.js +42 -11
- package/lib/public/js/hooks/use-app-shell-controller.js +230 -0
- package/lib/public/js/hooks/use-app-shell-ui.js +112 -0
- package/lib/public/js/hooks/use-browse-navigation.js +193 -0
- package/lib/public/js/hooks/use-hash-location.js +32 -0
- package/lib/public/js/lib/api.js +35 -0
- package/lib/public/js/lib/app-navigation.js +39 -0
- package/lib/public/js/lib/browse-restart-policy.js +28 -0
- package/lib/public/js/lib/browse-route.js +57 -0
- package/lib/public/js/lib/format.js +12 -0
- package/lib/server/auth-profiles.js +223 -52
- package/lib/server/doctor/prompt.js +4 -1
- package/lib/server/gateway.js +29 -9
- package/lib/server/routes/models.js +170 -2
- package/lib/server/watchdog.js +14 -1
- package/lib/server.js +1 -0
- package/package.json +1 -1
|
@@ -462,9 +462,8 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
462
462
|
? html`
|
|
463
463
|
<div class="ac-surface-inset rounded-xl p-4">
|
|
464
464
|
<div
|
|
465
|
-
class="
|
|
465
|
+
class="text-xs leading-5 text-gray-400"
|
|
466
466
|
>
|
|
467
|
-
<${LoadingSpinner} className="h-3.5 w-3.5" />
|
|
468
467
|
<span
|
|
469
468
|
>Run in progress. Findings will appear when analysis
|
|
470
469
|
completes.</span
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { Gateway } from "../gateway.js";
|
|
4
|
+
import { Channels } from "../channels.js";
|
|
5
|
+
import { Pairings } from "../pairings.js";
|
|
6
|
+
import { DevicePairings } from "../device-pairings.js";
|
|
7
|
+
import { Google } from "../google/index.js";
|
|
8
|
+
import { Features } from "../features.js";
|
|
9
|
+
import { GeneralDoctorWarning } from "../doctor/general-warning.js";
|
|
10
|
+
import { ChevronDownIcon } from "../icons.js";
|
|
11
|
+
import { UpdateActionButton } from "../update-action-button.js";
|
|
12
|
+
import { useGeneralTab } from "./use-general-tab.js";
|
|
13
|
+
|
|
14
|
+
const html = htm.bind(h);
|
|
15
|
+
|
|
16
|
+
export const GeneralTab = ({
|
|
17
|
+
statusData = null,
|
|
18
|
+
watchdogData = null,
|
|
19
|
+
doctorStatusData = null,
|
|
20
|
+
doctorWarningDismissedUntilMs = 0,
|
|
21
|
+
onRefreshStatuses = () => {},
|
|
22
|
+
onSwitchTab = () => {},
|
|
23
|
+
onNavigate = () => {},
|
|
24
|
+
onOpenGmailWebhook = () => {},
|
|
25
|
+
isActive = false,
|
|
26
|
+
restartingGateway = false,
|
|
27
|
+
onRestartGateway = () => {},
|
|
28
|
+
restartSignal = 0,
|
|
29
|
+
openclawUpdateInProgress = false,
|
|
30
|
+
onOpenclawVersionActionComplete = () => {},
|
|
31
|
+
onOpenclawUpdate = () => {},
|
|
32
|
+
onRestartRequired = () => {},
|
|
33
|
+
onDismissDoctorWarning = () => {},
|
|
34
|
+
}) => {
|
|
35
|
+
const { state, actions } = useGeneralTab({
|
|
36
|
+
statusData,
|
|
37
|
+
watchdogData,
|
|
38
|
+
doctorStatusData,
|
|
39
|
+
onRefreshStatuses,
|
|
40
|
+
isActive,
|
|
41
|
+
restartSignal,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return html`
|
|
45
|
+
<div class="space-y-4">
|
|
46
|
+
<${Gateway}
|
|
47
|
+
status=${state.gatewayStatus}
|
|
48
|
+
openclawVersion=${state.openclawVersion}
|
|
49
|
+
restarting=${restartingGateway}
|
|
50
|
+
onRestart=${onRestartGateway}
|
|
51
|
+
watchdogStatus=${state.watchdogStatus}
|
|
52
|
+
onOpenWatchdog=${() => onSwitchTab("watchdog")}
|
|
53
|
+
onRepair=${actions.handleWatchdogRepair}
|
|
54
|
+
repairing=${state.repairingWatchdog}
|
|
55
|
+
openclawUpdateInProgress=${openclawUpdateInProgress}
|
|
56
|
+
onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
|
|
57
|
+
onOpenclawUpdate=${onOpenclawUpdate}
|
|
58
|
+
/>
|
|
59
|
+
<${GeneralDoctorWarning}
|
|
60
|
+
doctorStatus=${state.doctorStatus}
|
|
61
|
+
dismissedUntilMs=${doctorWarningDismissedUntilMs}
|
|
62
|
+
onOpenDoctor=${() => onSwitchTab("doctor")}
|
|
63
|
+
onDismiss=${onDismissDoctorWarning}
|
|
64
|
+
/>
|
|
65
|
+
<${Channels}
|
|
66
|
+
channels=${state.channels}
|
|
67
|
+
onSwitchTab=${onSwitchTab}
|
|
68
|
+
onNavigate=${onNavigate}
|
|
69
|
+
/>
|
|
70
|
+
<${Pairings}
|
|
71
|
+
pending=${state.pending}
|
|
72
|
+
channels=${state.channels}
|
|
73
|
+
visible=${state.hasUnpaired}
|
|
74
|
+
onApprove=${actions.handleApprove}
|
|
75
|
+
onReject=${actions.handleReject}
|
|
76
|
+
/>
|
|
77
|
+
<${Features} onSwitchTab=${onSwitchTab} />
|
|
78
|
+
<${Google}
|
|
79
|
+
gatewayStatus=${state.gatewayStatus}
|
|
80
|
+
onRestartRequired=${onRestartRequired}
|
|
81
|
+
onOpenGmailWebhook=${onOpenGmailWebhook}
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
${state.repo &&
|
|
85
|
+
html`
|
|
86
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
87
|
+
<div class="flex items-center justify-between gap-3">
|
|
88
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
89
|
+
<svg
|
|
90
|
+
class="w-4 h-4 text-gray-400"
|
|
91
|
+
viewBox="0 0 16 16"
|
|
92
|
+
fill="currentColor"
|
|
93
|
+
>
|
|
94
|
+
<path
|
|
95
|
+
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
|
|
96
|
+
/>
|
|
97
|
+
</svg>
|
|
98
|
+
<a
|
|
99
|
+
href="https://github.com/${state.repo}"
|
|
100
|
+
target="_blank"
|
|
101
|
+
class="text-sm text-gray-400 hover:text-gray-200 transition-colors truncate"
|
|
102
|
+
>${state.repo}</a
|
|
103
|
+
>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
106
|
+
<span class="text-xs text-gray-400">Auto-sync</span>
|
|
107
|
+
<div class="relative">
|
|
108
|
+
<select
|
|
109
|
+
value=${state.syncCronChoice}
|
|
110
|
+
onchange=${(event) =>
|
|
111
|
+
actions.handleSyncCronChoiceChange(event.target.value)}
|
|
112
|
+
disabled=${state.savingSyncCron}
|
|
113
|
+
class="appearance-none bg-black/30 border border-border rounded-lg pl-2.5 pr-9 py-1.5 text-xs text-gray-300 ${state.savingSyncCron
|
|
114
|
+
? "opacity-50 cursor-not-allowed"
|
|
115
|
+
: ""}"
|
|
116
|
+
title=${state.syncCron?.installed === false
|
|
117
|
+
? "Not Installed Yet"
|
|
118
|
+
: state.syncCronStatusText}
|
|
119
|
+
>
|
|
120
|
+
<option value="disabled">Disabled</option>
|
|
121
|
+
<option value="*/30 * * * *">Every 30 min</option>
|
|
122
|
+
<option value="0 * * * *">Hourly</option>
|
|
123
|
+
<option value="0 0 * * *">Daily</option>
|
|
124
|
+
</select>
|
|
125
|
+
<${ChevronDownIcon}
|
|
126
|
+
className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
`}
|
|
133
|
+
|
|
134
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
135
|
+
<div class="flex items-center justify-between">
|
|
136
|
+
<div>
|
|
137
|
+
<h2 class="font-semibold text-sm">OpenClaw Gateway Dashboard</h2>
|
|
138
|
+
</div>
|
|
139
|
+
<${UpdateActionButton}
|
|
140
|
+
onClick=${actions.handleOpenDashboard}
|
|
141
|
+
loading=${state.dashboardLoading}
|
|
142
|
+
warning=${false}
|
|
143
|
+
idleLabel="Open"
|
|
144
|
+
loadingLabel="Opening..."
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<${DevicePairings}
|
|
148
|
+
pending=${state.devicePending}
|
|
149
|
+
onApprove=${actions.handleDeviceApprove}
|
|
150
|
+
onReject=${actions.handleDeviceReject}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
`;
|
|
155
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import {
|
|
3
|
+
approveDevice,
|
|
4
|
+
approvePairing,
|
|
5
|
+
fetchDashboardUrl,
|
|
6
|
+
fetchDevicePairings,
|
|
7
|
+
fetchPairings,
|
|
8
|
+
rejectDevice,
|
|
9
|
+
rejectPairing,
|
|
10
|
+
triggerWatchdogRepair,
|
|
11
|
+
updateSyncCron,
|
|
12
|
+
} from "../../lib/api.js";
|
|
13
|
+
import { usePolling } from "../../hooks/usePolling.js";
|
|
14
|
+
import { showToast } from "../toast.js";
|
|
15
|
+
import { ALL_CHANNELS } from "../channels.js";
|
|
16
|
+
|
|
17
|
+
const kDefaultSyncCronSchedule = "0 * * * *";
|
|
18
|
+
|
|
19
|
+
export const useGeneralTab = ({
|
|
20
|
+
statusData = null,
|
|
21
|
+
watchdogData = null,
|
|
22
|
+
doctorStatusData = null,
|
|
23
|
+
onRefreshStatuses = () => {},
|
|
24
|
+
isActive = false,
|
|
25
|
+
restartSignal = 0,
|
|
26
|
+
} = {}) => {
|
|
27
|
+
const [dashboardLoading, setDashboardLoading] = useState(false);
|
|
28
|
+
const [repairingWatchdog, setRepairingWatchdog] = useState(false);
|
|
29
|
+
const [syncCronEnabled, setSyncCronEnabled] = useState(true);
|
|
30
|
+
const [syncCronSchedule, setSyncCronSchedule] = useState(kDefaultSyncCronSchedule);
|
|
31
|
+
const [savingSyncCron, setSavingSyncCron] = useState(false);
|
|
32
|
+
const [syncCronChoice, setSyncCronChoice] = useState(kDefaultSyncCronSchedule);
|
|
33
|
+
|
|
34
|
+
const status = statusData;
|
|
35
|
+
const watchdogStatus = watchdogData;
|
|
36
|
+
const doctorStatus = doctorStatusData;
|
|
37
|
+
const gatewayStatus = status?.gateway ?? null;
|
|
38
|
+
const channels = status?.channels ?? null;
|
|
39
|
+
const repo = status?.repo || null;
|
|
40
|
+
const syncCron = status?.syncCron || null;
|
|
41
|
+
const openclawVersion = status?.openclawVersion || null;
|
|
42
|
+
|
|
43
|
+
const hasUnpaired = ALL_CHANNELS.some((channel) => {
|
|
44
|
+
const info = channels?.[channel];
|
|
45
|
+
return info && info.status !== "paired";
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const pairingsPoll = usePolling(
|
|
49
|
+
async () => {
|
|
50
|
+
const data = await fetchPairings();
|
|
51
|
+
return data.pending || [];
|
|
52
|
+
},
|
|
53
|
+
1000,
|
|
54
|
+
{ enabled: hasUnpaired && gatewayStatus === "running" },
|
|
55
|
+
);
|
|
56
|
+
const pending = pairingsPoll.data || [];
|
|
57
|
+
|
|
58
|
+
const devicePoll = usePolling(
|
|
59
|
+
async () => {
|
|
60
|
+
const data = await fetchDevicePairings();
|
|
61
|
+
return data.pending || [];
|
|
62
|
+
},
|
|
63
|
+
2000,
|
|
64
|
+
{ enabled: gatewayStatus === "running" },
|
|
65
|
+
);
|
|
66
|
+
const devicePending = devicePoll.data || [];
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!isActive) return;
|
|
70
|
+
onRefreshStatuses();
|
|
71
|
+
pairingsPoll.refresh();
|
|
72
|
+
devicePoll.refresh();
|
|
73
|
+
}, [devicePoll.refresh, isActive, onRefreshStatuses, pairingsPoll.refresh]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!restartSignal || !isActive) return;
|
|
77
|
+
onRefreshStatuses();
|
|
78
|
+
pairingsPoll.refresh();
|
|
79
|
+
devicePoll.refresh();
|
|
80
|
+
const t1 = setTimeout(() => {
|
|
81
|
+
onRefreshStatuses();
|
|
82
|
+
pairingsPoll.refresh();
|
|
83
|
+
devicePoll.refresh();
|
|
84
|
+
}, 1200);
|
|
85
|
+
const t2 = setTimeout(() => {
|
|
86
|
+
onRefreshStatuses();
|
|
87
|
+
pairingsPoll.refresh();
|
|
88
|
+
devicePoll.refresh();
|
|
89
|
+
}, 3500);
|
|
90
|
+
return () => {
|
|
91
|
+
clearTimeout(t1);
|
|
92
|
+
clearTimeout(t2);
|
|
93
|
+
};
|
|
94
|
+
}, [
|
|
95
|
+
devicePoll.refresh,
|
|
96
|
+
isActive,
|
|
97
|
+
onRefreshStatuses,
|
|
98
|
+
pairingsPoll.refresh,
|
|
99
|
+
restartSignal,
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!syncCron) return;
|
|
104
|
+
setSyncCronEnabled(syncCron.enabled !== false);
|
|
105
|
+
setSyncCronSchedule(syncCron.schedule || kDefaultSyncCronSchedule);
|
|
106
|
+
setSyncCronChoice(
|
|
107
|
+
syncCron.enabled === false ? "disabled" : syncCron.schedule || kDefaultSyncCronSchedule,
|
|
108
|
+
);
|
|
109
|
+
}, [syncCron?.enabled, syncCron?.schedule]);
|
|
110
|
+
|
|
111
|
+
const refreshAfterPairingAction = () => {
|
|
112
|
+
setTimeout(pairingsPoll.refresh, 500);
|
|
113
|
+
setTimeout(pairingsPoll.refresh, 2000);
|
|
114
|
+
setTimeout(onRefreshStatuses, 3000);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const saveSyncCronSettings = async ({
|
|
118
|
+
enabled = syncCronEnabled,
|
|
119
|
+
schedule = syncCronSchedule,
|
|
120
|
+
} = {}) => {
|
|
121
|
+
if (savingSyncCron) return;
|
|
122
|
+
setSavingSyncCron(true);
|
|
123
|
+
try {
|
|
124
|
+
const data = await updateSyncCron({ enabled, schedule });
|
|
125
|
+
if (!data.ok) {
|
|
126
|
+
throw new Error(data.error || "Could not save sync settings");
|
|
127
|
+
}
|
|
128
|
+
showToast("Sync schedule updated", "success");
|
|
129
|
+
onRefreshStatuses();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
showToast(err.message || "Could not save sync settings", "error");
|
|
132
|
+
} finally {
|
|
133
|
+
setSavingSyncCron(false);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleSyncCronChoiceChange = async (nextChoice) => {
|
|
138
|
+
setSyncCronChoice(nextChoice);
|
|
139
|
+
const nextEnabled = nextChoice !== "disabled";
|
|
140
|
+
const nextSchedule = nextEnabled ? nextChoice : syncCronSchedule;
|
|
141
|
+
setSyncCronEnabled(nextEnabled);
|
|
142
|
+
setSyncCronSchedule(nextSchedule);
|
|
143
|
+
await saveSyncCronSettings({
|
|
144
|
+
enabled: nextEnabled,
|
|
145
|
+
schedule: nextSchedule,
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleApprove = async (id, channel) => {
|
|
150
|
+
await approvePairing(id, channel);
|
|
151
|
+
refreshAfterPairingAction();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleReject = async (id, channel) => {
|
|
155
|
+
await rejectPairing(id, channel);
|
|
156
|
+
refreshAfterPairingAction();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleDeviceApprove = async (id) => {
|
|
160
|
+
await approveDevice(id);
|
|
161
|
+
setTimeout(devicePoll.refresh, 500);
|
|
162
|
+
setTimeout(devicePoll.refresh, 2000);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleDeviceReject = async (id) => {
|
|
166
|
+
await rejectDevice(id);
|
|
167
|
+
setTimeout(devicePoll.refresh, 500);
|
|
168
|
+
setTimeout(devicePoll.refresh, 2000);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleWatchdogRepair = async () => {
|
|
172
|
+
if (repairingWatchdog) return;
|
|
173
|
+
setRepairingWatchdog(true);
|
|
174
|
+
try {
|
|
175
|
+
const data = await triggerWatchdogRepair();
|
|
176
|
+
if (!data.ok) throw new Error(data.error || "Repair failed");
|
|
177
|
+
showToast("Repair triggered", "success");
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
onRefreshStatuses();
|
|
180
|
+
}, 800);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
showToast(err.message || "Could not run repair", "error");
|
|
183
|
+
} finally {
|
|
184
|
+
setRepairingWatchdog(false);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleOpenDashboard = async () => {
|
|
189
|
+
if (dashboardLoading) return;
|
|
190
|
+
setDashboardLoading(true);
|
|
191
|
+
try {
|
|
192
|
+
const data = await fetchDashboardUrl();
|
|
193
|
+
console.log("[dashboard] response:", JSON.stringify(data));
|
|
194
|
+
window.open(data.url || "/openclaw", "_blank");
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error("[dashboard] error:", err);
|
|
197
|
+
window.open("/openclaw", "_blank");
|
|
198
|
+
} finally {
|
|
199
|
+
setDashboardLoading(false);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
state: {
|
|
205
|
+
channels,
|
|
206
|
+
dashboardLoading,
|
|
207
|
+
devicePending,
|
|
208
|
+
doctorStatus,
|
|
209
|
+
gatewayStatus,
|
|
210
|
+
hasUnpaired,
|
|
211
|
+
openclawVersion,
|
|
212
|
+
pending,
|
|
213
|
+
repairingWatchdog,
|
|
214
|
+
repo,
|
|
215
|
+
savingSyncCron,
|
|
216
|
+
syncCron,
|
|
217
|
+
syncCronChoice,
|
|
218
|
+
syncCronEnabled,
|
|
219
|
+
syncCronSchedule,
|
|
220
|
+
syncCronStatusText: syncCronEnabled ? "Enabled" : "Disabled",
|
|
221
|
+
watchdogStatus,
|
|
222
|
+
},
|
|
223
|
+
actions: {
|
|
224
|
+
handleApprove,
|
|
225
|
+
handleDeviceApprove,
|
|
226
|
+
handleDeviceReject,
|
|
227
|
+
handleOpenDashboard,
|
|
228
|
+
handleReject,
|
|
229
|
+
handleSyncCronChoiceChange,
|
|
230
|
+
handleWatchdogRepair,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState, useMemo } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { PageHeader } from "../page-header.js";
|
|
5
|
+
import { LoadingSpinner } from "../loading-spinner.js";
|
|
6
|
+
import { ActionButton } from "../action-button.js";
|
|
7
|
+
import { Badge } from "../badge.js";
|
|
8
|
+
import { useModels } from "./use-models.js";
|
|
9
|
+
import { ProviderAuthCard } from "./provider-auth-card.js";
|
|
10
|
+
import { getModelProvider, getFeaturedModels } from "../../lib/model-config.js";
|
|
11
|
+
|
|
12
|
+
const html = htm.bind(h);
|
|
13
|
+
|
|
14
|
+
const deriveRequiredProviders = (configuredModels) => {
|
|
15
|
+
const providers = new Set();
|
|
16
|
+
for (const modelKey of Object.keys(configuredModels)) {
|
|
17
|
+
const provider = getModelProvider(modelKey);
|
|
18
|
+
if (provider) providers.add(provider);
|
|
19
|
+
}
|
|
20
|
+
return [...providers];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const kProviderDisplayOrder = [
|
|
24
|
+
"anthropic",
|
|
25
|
+
"openai",
|
|
26
|
+
"openai-codex",
|
|
27
|
+
"google",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const Models = ({ onRestartRequired = () => {} }) => {
|
|
31
|
+
const {
|
|
32
|
+
catalog,
|
|
33
|
+
primary,
|
|
34
|
+
configuredModels,
|
|
35
|
+
authProfiles,
|
|
36
|
+
authOrder,
|
|
37
|
+
codexStatus,
|
|
38
|
+
loading,
|
|
39
|
+
saving,
|
|
40
|
+
ready,
|
|
41
|
+
error,
|
|
42
|
+
isDirty,
|
|
43
|
+
addModel,
|
|
44
|
+
removeModel,
|
|
45
|
+
setPrimaryModel,
|
|
46
|
+
editProfile,
|
|
47
|
+
editAuthOrder,
|
|
48
|
+
getProfileValue,
|
|
49
|
+
getEffectiveOrder,
|
|
50
|
+
cancelChanges,
|
|
51
|
+
saveAll,
|
|
52
|
+
refreshCodexStatus,
|
|
53
|
+
} = useModels();
|
|
54
|
+
|
|
55
|
+
const [showAllModels, setShowAllModels] = useState(false);
|
|
56
|
+
|
|
57
|
+
const configuredKeys = useMemo(
|
|
58
|
+
() => new Set(Object.keys(configuredModels)),
|
|
59
|
+
[configuredModels],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const featuredModels = useMemo(() => getFeaturedModels(catalog), [catalog]);
|
|
63
|
+
|
|
64
|
+
const pickerModels = useMemo(() => {
|
|
65
|
+
const base = showAllModels
|
|
66
|
+
? catalog
|
|
67
|
+
: featuredModels.length > 0
|
|
68
|
+
? featuredModels
|
|
69
|
+
: catalog;
|
|
70
|
+
return base.filter((m) => !configuredKeys.has(m.key));
|
|
71
|
+
}, [catalog, featuredModels, showAllModels, configuredKeys]);
|
|
72
|
+
|
|
73
|
+
const canToggleFullCatalog =
|
|
74
|
+
featuredModels.length > 0 && catalog.length > featuredModels.length;
|
|
75
|
+
|
|
76
|
+
const requiredProviders = useMemo(
|
|
77
|
+
() => deriveRequiredProviders(configuredModels),
|
|
78
|
+
[configuredModels],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const sortedProviders = useMemo(() => {
|
|
82
|
+
const ordered = [];
|
|
83
|
+
for (const p of kProviderDisplayOrder) {
|
|
84
|
+
if (requiredProviders.includes(p)) ordered.push(p);
|
|
85
|
+
}
|
|
86
|
+
for (const p of requiredProviders) {
|
|
87
|
+
if (!ordered.includes(p)) ordered.push(p);
|
|
88
|
+
}
|
|
89
|
+
return ordered;
|
|
90
|
+
}, [requiredProviders]);
|
|
91
|
+
|
|
92
|
+
const providerHasAuth = useMemo(() => {
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const p of authProfiles) {
|
|
95
|
+
if (p.key || p.token || p.access) {
|
|
96
|
+
result[p.provider] = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (codexStatus?.connected) {
|
|
100
|
+
result["openai-codex"] = true;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}, [authProfiles, codexStatus]);
|
|
104
|
+
|
|
105
|
+
const configuredModelEntries = useMemo(
|
|
106
|
+
() =>
|
|
107
|
+
Object.keys(configuredModels).map((key) => {
|
|
108
|
+
const catalogEntry = catalog.find((m) => m.key === key);
|
|
109
|
+
const provider = getModelProvider(key);
|
|
110
|
+
const hasAuth = !!providerHasAuth[provider];
|
|
111
|
+
return {
|
|
112
|
+
key,
|
|
113
|
+
label: catalogEntry?.label || key,
|
|
114
|
+
isPrimary: key === primary,
|
|
115
|
+
hasAuth,
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
[configuredModels, catalog, primary, providerHasAuth],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (!ready) {
|
|
122
|
+
return html`
|
|
123
|
+
<div class="space-y-4">
|
|
124
|
+
<${PageHeader}
|
|
125
|
+
title="Models"
|
|
126
|
+
actions=${html`
|
|
127
|
+
<${ActionButton}
|
|
128
|
+
disabled=${true}
|
|
129
|
+
tone="primary"
|
|
130
|
+
size="sm"
|
|
131
|
+
idleLabel="Save changes"
|
|
132
|
+
className="transition-all"
|
|
133
|
+
/>
|
|
134
|
+
`}
|
|
135
|
+
/>
|
|
136
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
137
|
+
<div class="flex items-center gap-2 text-sm text-gray-400">
|
|
138
|
+
<${LoadingSpinner} className="h-4 w-4" />
|
|
139
|
+
Loading model settings...
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return html`
|
|
147
|
+
<div class="space-y-4">
|
|
148
|
+
<${PageHeader}
|
|
149
|
+
title="Models"
|
|
150
|
+
actions=${html`
|
|
151
|
+
<${ActionButton}
|
|
152
|
+
onClick=${cancelChanges}
|
|
153
|
+
disabled=${!isDirty || saving}
|
|
154
|
+
tone="secondary"
|
|
155
|
+
size="sm"
|
|
156
|
+
idleLabel="Cancel"
|
|
157
|
+
className="transition-all"
|
|
158
|
+
/>
|
|
159
|
+
<${ActionButton}
|
|
160
|
+
onClick=${saveAll}
|
|
161
|
+
disabled=${!isDirty || saving}
|
|
162
|
+
loading=${saving}
|
|
163
|
+
tone="primary"
|
|
164
|
+
size="sm"
|
|
165
|
+
idleLabel="Save changes"
|
|
166
|
+
loadingLabel="Saving..."
|
|
167
|
+
className="transition-all"
|
|
168
|
+
/>
|
|
169
|
+
`}
|
|
170
|
+
/>
|
|
171
|
+
|
|
172
|
+
<!-- Configured Models -->
|
|
173
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
174
|
+
<h2 class="font-semibold text-sm">Available Models</h2>
|
|
175
|
+
|
|
176
|
+
${configuredModelEntries.length === 0
|
|
177
|
+
? html`<p class="text-xs text-gray-500">
|
|
178
|
+
No models configured. Add a model below.
|
|
179
|
+
</p>`
|
|
180
|
+
: html`
|
|
181
|
+
<div class="space-y-1">
|
|
182
|
+
${configuredModelEntries.map(
|
|
183
|
+
(entry) => html`
|
|
184
|
+
<div
|
|
185
|
+
class="flex items-center justify-between py-1.5 px-2 rounded-lg hover:bg-white/5"
|
|
186
|
+
>
|
|
187
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
188
|
+
<span class="text-sm text-gray-200 truncate"
|
|
189
|
+
>${entry.label}</span
|
|
190
|
+
>
|
|
191
|
+
${entry.isPrimary
|
|
192
|
+
? html`<${Badge} tone="cyan">Primary</${Badge}>`
|
|
193
|
+
: entry.hasAuth
|
|
194
|
+
? html`
|
|
195
|
+
<button
|
|
196
|
+
onclick=${() => setPrimaryModel(entry.key)}
|
|
197
|
+
class="text-xs px-2 py-0.5 rounded-full text-gray-500 hover:text-gray-300 hover:bg-white/5"
|
|
198
|
+
>
|
|
199
|
+
Set primary
|
|
200
|
+
</button>
|
|
201
|
+
`
|
|
202
|
+
: html`<${Badge} tone="warning">Needs auth</${Badge}>`}
|
|
203
|
+
</div>
|
|
204
|
+
<button
|
|
205
|
+
onclick=${() => removeModel(entry.key)}
|
|
206
|
+
class="text-xs text-gray-600 hover:text-red-400 shrink-0 px-1"
|
|
207
|
+
>
|
|
208
|
+
Remove
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
`,
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
`}
|
|
215
|
+
|
|
216
|
+
<div class="pt-2 border-t border-border space-y-2">
|
|
217
|
+
<label class="text-xs font-medium text-gray-400">Add model</label>
|
|
218
|
+
<select
|
|
219
|
+
onInput=${(e) => {
|
|
220
|
+
const val = e.target.value;
|
|
221
|
+
if (val) {
|
|
222
|
+
addModel(val);
|
|
223
|
+
if (!primary) setPrimaryModel(val);
|
|
224
|
+
}
|
|
225
|
+
e.target.value = "";
|
|
226
|
+
}}
|
|
227
|
+
class="w-full bg-black/30 border border-border rounded-lg pl-3 pr-8 py-2 text-sm text-gray-200 outline-none focus:border-gray-500"
|
|
228
|
+
>
|
|
229
|
+
<option value="">Select a model to add...</option>
|
|
230
|
+
${pickerModels.map(
|
|
231
|
+
(m) =>
|
|
232
|
+
html`<option value=${m.key}>${m.label || m.key}</option>`,
|
|
233
|
+
)}
|
|
234
|
+
</select>
|
|
235
|
+
${canToggleFullCatalog
|
|
236
|
+
? html`
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
onclick=${() => setShowAllModels((prev) => !prev)}
|
|
240
|
+
class="text-xs text-gray-500 hover:text-gray-300"
|
|
241
|
+
>
|
|
242
|
+
${showAllModels
|
|
243
|
+
? "Show recommended models"
|
|
244
|
+
: "Show full model catalog"}
|
|
245
|
+
</button>
|
|
246
|
+
`
|
|
247
|
+
: null}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
${loading
|
|
251
|
+
? html`<p class="text-xs text-gray-600">
|
|
252
|
+
Loading model catalog...
|
|
253
|
+
</p>`
|
|
254
|
+
: error
|
|
255
|
+
? html`<p class="text-xs text-gray-600">${error}</p>`
|
|
256
|
+
: null}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<!-- Provider Auth -->
|
|
260
|
+
${sortedProviders.length > 0
|
|
261
|
+
? html`
|
|
262
|
+
<div class="space-y-3">
|
|
263
|
+
<h2 class="font-semibold text-sm text-gray-300">
|
|
264
|
+
Provider Authentication
|
|
265
|
+
</h2>
|
|
266
|
+
${sortedProviders.map(
|
|
267
|
+
(provider) => html`
|
|
268
|
+
<${ProviderAuthCard}
|
|
269
|
+
provider=${provider}
|
|
270
|
+
authProfiles=${authProfiles}
|
|
271
|
+
authOrder=${authOrder}
|
|
272
|
+
codexStatus=${codexStatus}
|
|
273
|
+
onEditProfile=${editProfile}
|
|
274
|
+
onEditAuthOrder=${editAuthOrder}
|
|
275
|
+
getProfileValue=${getProfileValue}
|
|
276
|
+
getEffectiveOrder=${getEffectiveOrder}
|
|
277
|
+
onRefreshCodex=${refreshCodexStatus}
|
|
278
|
+
/>
|
|
279
|
+
`,
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
`
|
|
283
|
+
: null}
|
|
284
|
+
</div>
|
|
285
|
+
`;
|
|
286
|
+
};
|