dsh-workbuddy-connect 0.4.0 → 0.5.1
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/README.en.md +27 -9
- package/README.md +28 -9
- package/lib/bin.js +71 -23
- package/lib/client.js +1129 -971
- package/lib/index.d.ts +494 -19
- package/lib/index.js +733 -183
- package/lib/{host-heartbeat-0UAWqWac.js → variants-CE_2IvEG.js} +2137 -1022
- package/package.json +3 -1
package/lib/client.js
CHANGED
|
@@ -20,581 +20,214 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
* document.
|
|
21
21
|
*/
|
|
22
22
|
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/client/WorkBuddyProbeControl.tsx
|
|
25
23
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* Interaction follows the Fast Mode control `dsh-codex-connect` ships in this
|
|
29
|
-
* same seat, which is the established shape for composer chrome here:
|
|
30
|
-
*
|
|
31
|
-
* - a **static inline label** next to the icon names the feature ("Reasoning
|
|
32
|
-
* levels"), set smaller and dimmer than the surrounding chrome so it reads as
|
|
33
|
-
* an annotation on the icon. It never carries state: the verified levels
|
|
34
|
-
* already appear in the model dropdown (the adapter exposes them as
|
|
35
|
-
* selectable efforts), so repeating them here would duplicate the real answer
|
|
36
|
-
* and make the label's width jump as results change.
|
|
37
|
-
* - a **hover/focus tooltip** carries the state and the click's purpose, the way
|
|
38
|
-
* Fast Mode's tooltip explains its current speed.
|
|
39
|
-
* - the **confirmation** is a small bubble anchored to the control, not a
|
|
40
|
-
* `window.confirm`. Probing spends real credit, so a confirmation stays — but
|
|
41
|
-
* it belongs next to the thing it acts on, sized to one line plus two small
|
|
42
|
-
* buttons.
|
|
24
|
+
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
43
25
|
*
|
|
44
|
-
*
|
|
26
|
+
* Kept as separate constants rather than a computed suffix so both halves
|
|
27
|
+
* reference literal strings: the browser bundle and the host bundle are built
|
|
28
|
+
* independently, and a shared expression is one build-config drift away from
|
|
29
|
+
* the desk asking a route the host never mounted.
|
|
45
30
|
*/
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
31
|
+
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
32
|
+
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/client/WorkBuddyPluginCard.tsx
|
|
35
|
+
/** WorkBuddy status card contributed to Harness Plugin configuration. */
|
|
36
|
+
/** CN WorkBuddy; the plugin's long-standing card and default. */
|
|
37
|
+
const CN_CARD_VARIANT = {
|
|
38
|
+
id: "workbuddy",
|
|
39
|
+
titleKey: "title",
|
|
40
|
+
introKey: "intro",
|
|
41
|
+
signedOutKey: "signedOutHint",
|
|
42
|
+
statusPath: WORKBUDDY_STATUS_PATH,
|
|
43
|
+
probePath: WORKBUDDY_PROBE_PATH
|
|
54
44
|
};
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
/** Both cards, in display order. */
|
|
46
|
+
const CARD_VARIANTS = [CN_CARD_VARIANT, {
|
|
47
|
+
id: "workbuddy-ai",
|
|
48
|
+
titleKey: "titleAI",
|
|
49
|
+
introKey: "introAI",
|
|
50
|
+
signedOutKey: "signedOutHintAI",
|
|
51
|
+
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
52
|
+
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
53
|
+
}];
|
|
54
|
+
const POLL_INTERVAL_MS = 6e4;
|
|
55
|
+
const cardStyle = {
|
|
56
|
+
overflow: "hidden",
|
|
57
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
58
|
+
borderRadius: 10,
|
|
59
|
+
background: "var(--dsw-alias-bg-module-platform)"
|
|
60
|
+
};
|
|
61
|
+
const headerStyle = {
|
|
62
|
+
boxSizing: "border-box",
|
|
63
|
+
width: "100%",
|
|
64
|
+
display: "flex",
|
|
57
65
|
alignItems: "center",
|
|
58
|
-
justifyContent: "
|
|
59
|
-
gap:
|
|
60
|
-
height: 30,
|
|
61
|
-
padding: "0 6px",
|
|
66
|
+
justifyContent: "space-between",
|
|
67
|
+
gap: 16,
|
|
62
68
|
border: 0,
|
|
63
|
-
|
|
69
|
+
padding: "13px 14px",
|
|
64
70
|
background: "transparent",
|
|
65
|
-
color: "var(--dsw-alias-label-
|
|
71
|
+
color: "var(--dsw-alias-label-primary)",
|
|
66
72
|
font: "inherit",
|
|
67
|
-
|
|
73
|
+
textAlign: "left",
|
|
68
74
|
cursor: "pointer"
|
|
69
75
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const labelStyle = {
|
|
76
|
-
fontSize: 11,
|
|
77
|
-
lineHeight: "16px",
|
|
78
|
-
color: "var(--dsw-alias-label-tertiary)"
|
|
76
|
+
const headTextStyle = {
|
|
77
|
+
display: "flex",
|
|
78
|
+
minWidth: 0,
|
|
79
|
+
flexDirection: "column",
|
|
80
|
+
gap: 3
|
|
79
81
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
padding: "4px 8px",
|
|
88
|
-
borderRadius: 6,
|
|
89
|
-
background: "var(--dsw-specific-tip, #1f2329)",
|
|
90
|
-
boxShadow: "var(--dsw-shadow-lv2)",
|
|
91
|
-
color: "var(--dsw-alias-label-primary, #fff)",
|
|
92
|
-
fontSize: 12,
|
|
82
|
+
const nameStyle = {
|
|
83
|
+
fontSize: 14,
|
|
84
|
+
lineHeight: "20px",
|
|
85
|
+
fontWeight: 600
|
|
86
|
+
};
|
|
87
|
+
const descriptionStyle = {
|
|
88
|
+
fontSize: 13,
|
|
93
89
|
lineHeight: "18px",
|
|
94
|
-
|
|
95
|
-
pointerEvents: "none"
|
|
90
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
96
91
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
const chevronStyle = {
|
|
93
|
+
flex: "0 0 auto",
|
|
94
|
+
fontSize: 18,
|
|
95
|
+
lineHeight: 1,
|
|
96
|
+
transition: "transform 120ms ease"
|
|
97
|
+
};
|
|
98
|
+
const cardBodyStyle = {
|
|
99
|
+
borderTop: "1px solid var(--dsw-alias-border-l2)",
|
|
100
|
+
padding: "16px 14px 18px"
|
|
101
|
+
};
|
|
102
|
+
const bodyStyle = {
|
|
103
|
+
margin: 0,
|
|
104
|
+
fontSize: 14,
|
|
105
|
+
lineHeight: "22px",
|
|
106
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
107
|
+
};
|
|
108
|
+
const rowStyle = {
|
|
103
109
|
display: "flex",
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
border: "1px solid var(--dsw-alias-border-l2)",
|
|
109
|
-
borderRadius: 8,
|
|
110
|
-
background: "var(--dsw-alias-bg-layer-1, #fff)",
|
|
111
|
-
boxShadow: "var(--dsw-shadow-lv2)",
|
|
112
|
-
color: "var(--dsw-alias-label-primary)",
|
|
113
|
-
fontSize: 12,
|
|
114
|
-
lineHeight: "18px"
|
|
110
|
+
alignItems: "center",
|
|
111
|
+
justifyContent: "space-between",
|
|
112
|
+
flexWrap: "wrap",
|
|
113
|
+
gap: 12
|
|
115
114
|
};
|
|
116
|
-
const
|
|
115
|
+
const statusStyle = {
|
|
117
116
|
display: "flex",
|
|
118
|
-
|
|
119
|
-
gap:
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
gap: 9,
|
|
119
|
+
fontSize: 15,
|
|
120
|
+
fontWeight: 500,
|
|
121
|
+
color: "var(--dsw-alias-label-primary)"
|
|
120
122
|
};
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
+
const buttonStyle$1 = {
|
|
124
|
+
boxSizing: "border-box",
|
|
125
|
+
minHeight: 34,
|
|
126
|
+
padding: "6px 14px",
|
|
123
127
|
border: "1px solid var(--dsw-alias-border-l2)",
|
|
124
|
-
borderRadius:
|
|
125
|
-
background: "
|
|
126
|
-
color: "
|
|
128
|
+
borderRadius: 18,
|
|
129
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
130
|
+
color: "var(--dsw-alias-label-primary)",
|
|
127
131
|
font: "inherit",
|
|
128
|
-
fontSize:
|
|
132
|
+
fontSize: 14,
|
|
129
133
|
cursor: "pointer"
|
|
130
134
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
* The fill and its text colour must come as a pair: `brand-primary` resolves to
|
|
135
|
-
* a light accent in this theme, so hardcoding `color: #fff` on top of it renders
|
|
136
|
-
* white-on-white. `button-primary-fill` + `label-primary-foreground` is the
|
|
137
|
-
* theme's own pair for exactly this, and is what `dsh-codex-connect` uses for
|
|
138
|
-
* the same job.
|
|
139
|
-
*/
|
|
140
|
-
const primaryButtonStyle$1 = {
|
|
141
|
-
...confirmButtonStyle,
|
|
142
|
-
border: "1px solid var(--dsw-alias-button-primary-fill)",
|
|
143
|
-
background: "var(--dsw-alias-button-primary-fill)",
|
|
144
|
-
color: "var(--dsw-alias-label-primary-foreground)"
|
|
135
|
+
const errorStyle = {
|
|
136
|
+
...bodyStyle,
|
|
137
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
145
138
|
};
|
|
146
|
-
|
|
147
|
-
* Result note: a single line + a dismiss button, anchored to the control's
|
|
148
|
-
* right side. Smaller than the confirmation bubble because it carries an
|
|
149
|
-
* *outcome*, not a *decision* — the work is done, the user only has to read
|
|
150
|
-
* and dismiss.
|
|
151
|
-
*/
|
|
152
|
-
const noteStyle = {
|
|
153
|
-
position: "absolute",
|
|
154
|
-
right: 0,
|
|
155
|
-
bottom: "calc(100% + 8px)",
|
|
156
|
-
zIndex: 1001,
|
|
139
|
+
const quotaListStyle = {
|
|
157
140
|
display: "flex",
|
|
158
|
-
|
|
141
|
+
flexDirection: "column",
|
|
142
|
+
gap: 18,
|
|
143
|
+
paddingTop: 2
|
|
144
|
+
};
|
|
145
|
+
const quotaGroupStyle = {
|
|
146
|
+
display: "flex",
|
|
147
|
+
flexDirection: "column",
|
|
148
|
+
gap: 10
|
|
149
|
+
};
|
|
150
|
+
const quotaTitleStyle = {
|
|
151
|
+
margin: 0,
|
|
152
|
+
fontSize: 14,
|
|
153
|
+
lineHeight: "20px",
|
|
154
|
+
fontWeight: 600,
|
|
155
|
+
color: "var(--dsw-alias-label-primary)"
|
|
156
|
+
};
|
|
157
|
+
const quotaLabelStyle = {
|
|
158
|
+
display: "flex",
|
|
159
|
+
justifyContent: "space-between",
|
|
159
160
|
gap: 12,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
fontSize: 13,
|
|
162
|
+
lineHeight: "20px",
|
|
163
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
164
|
+
};
|
|
165
|
+
const modelBadgeStyle = {
|
|
166
|
+
display: "flex",
|
|
167
|
+
alignItems: "center",
|
|
168
|
+
gap: 6,
|
|
169
|
+
flexWrap: "wrap"
|
|
170
|
+
};
|
|
171
|
+
const modelOfferStyle = {
|
|
172
|
+
display: "flex",
|
|
173
|
+
flexDirection: "column",
|
|
174
|
+
gap: 2
|
|
175
|
+
};
|
|
176
|
+
const modelRateStyle = {
|
|
166
177
|
fontSize: 12,
|
|
167
178
|
lineHeight: "18px",
|
|
168
|
-
|
|
179
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
169
180
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*/
|
|
175
|
-
const noteDismissStyle = {
|
|
176
|
-
padding: "2px 8px",
|
|
177
|
-
border: "1px solid var(--dsw-alias-border-l2)",
|
|
178
|
-
borderRadius: 6,
|
|
179
|
-
background: "transparent",
|
|
180
|
-
color: "var(--dsw-alias-label-secondary)",
|
|
181
|
-
font: "inherit",
|
|
182
|
-
fontSize: 12,
|
|
181
|
+
const modelBadgeChipStyle = {
|
|
182
|
+
padding: "1px 8px",
|
|
183
|
+
borderRadius: 999,
|
|
184
|
+
fontSize: 11,
|
|
183
185
|
lineHeight: "18px",
|
|
184
|
-
|
|
186
|
+
background: "var(--dsw-alias-state-success-subtle, rgba(34, 160, 107, 0.12))",
|
|
187
|
+
color: "var(--dsw-alias-state-success-primary, #22a06b)"
|
|
185
188
|
};
|
|
186
189
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
190
|
+
* Localize an upstream promotional badge label, with an unknown-badge fallback.
|
|
191
|
+
*
|
|
192
|
+
* The CN catalog spells badges in Chinese (`限时免费`, `夜间折扣`); the
|
|
193
|
+
* international document's `modelPromotions` carries English (`Free now`). Both
|
|
194
|
+
* are mapped so the same promotion reads consistently in either UI language,
|
|
195
|
+
* and anything else passes through verbatim — an unrecognized badge is still
|
|
196
|
+
* information the upstream chose to show.
|
|
189
197
|
*/
|
|
190
|
-
function
|
|
191
|
-
return t("
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (status.status !== "signed-in") return void 0;
|
|
196
|
-
return status.probe?.results.find((result) => result.id === model);
|
|
198
|
+
function modelBadgeLabel(badge, t) {
|
|
199
|
+
if (badge === "限时免费") return t("badgeLimitedFree");
|
|
200
|
+
if (badge === "夜间折扣") return t("badgeNightDiscount");
|
|
201
|
+
if (badge === "Free now") return t("badgeFreeNow");
|
|
202
|
+
return badge;
|
|
197
203
|
}
|
|
204
|
+
const progressTrackStyle = {
|
|
205
|
+
height: 8,
|
|
206
|
+
overflow: "hidden",
|
|
207
|
+
borderRadius: 999,
|
|
208
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))"
|
|
209
|
+
};
|
|
198
210
|
/**
|
|
199
|
-
*
|
|
200
|
-
* two
|
|
211
|
+
* Inline confirmation box for a paid detection. Replaces the previous
|
|
212
|
+
* `window.confirm`: the decision is one line plus two buttons, and a modal
|
|
213
|
+
* alert for that is heavier than the action it guards.
|
|
201
214
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
|
|
208
|
-
if (result.validation === "non-validating") return t("probeTooltipNotValidating");
|
|
209
|
-
return t("probeTooltipRetry");
|
|
210
|
-
}
|
|
211
|
-
/** Model-independent shell: resolves the selection, then delegates per model. */
|
|
212
|
-
function WorkBuddyProbeControl({ directory, t }) {
|
|
213
|
-
const subscribe = (0, react.useCallback)((listener) => directory.subscribe(listener), [directory]);
|
|
214
|
-
const snapshot = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
|
|
215
|
-
const selection = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).current;
|
|
216
|
-
const model = selection?.provider === "workbuddy" ? selection.model : void 0;
|
|
217
|
-
const label = useLabel(t);
|
|
218
|
-
return model === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelProbe, {
|
|
219
|
-
model,
|
|
220
|
-
label,
|
|
221
|
-
t
|
|
222
|
-
}, model);
|
|
223
|
-
}
|
|
224
|
-
function ModelProbe({ model, label, t }) {
|
|
225
|
-
const [status, setStatus] = (0, react.useState)();
|
|
226
|
-
const [busy, setBusy] = (0, react.useState)(false);
|
|
227
|
-
const [confirming, setConfirming] = (0, react.useState)(false);
|
|
228
|
-
const [tooltipVisible, setTooltipVisible] = (0, react.useState)(false);
|
|
229
|
-
const [failed, setFailed] = (0, react.useState)(false);
|
|
230
|
-
const [note, setNote] = (0, react.useState)();
|
|
231
|
-
const inFlight = (0, react.useRef)(false);
|
|
232
|
-
const mounted = (0, react.useRef)(false);
|
|
233
|
-
const tooltipId = (0, react.useId)();
|
|
234
|
-
const refresh = (0, react.useCallback)(async (signal) => {
|
|
235
|
-
const response = await fetch(WORKBUDDY_STATUS_PATH, {
|
|
236
|
-
credentials: "same-origin",
|
|
237
|
-
headers: { accept: "application/json" },
|
|
238
|
-
...signal === void 0 ? {} : { signal }
|
|
239
|
-
});
|
|
240
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
241
|
-
const value = await response.json();
|
|
242
|
-
if (mounted.current && !signal?.aborted) setStatus(value);
|
|
243
|
-
}, []);
|
|
244
|
-
(0, react.useEffect)(() => {
|
|
245
|
-
mounted.current = true;
|
|
246
|
-
const controller = new AbortController();
|
|
247
|
-
const load = () => {
|
|
248
|
-
refresh(controller.signal).catch(() => {});
|
|
249
|
-
};
|
|
250
|
-
load();
|
|
251
|
-
const timer = window.setInterval(load, RECONCILE_MS);
|
|
252
|
-
window.addEventListener("focus", load);
|
|
253
|
-
return () => {
|
|
254
|
-
mounted.current = false;
|
|
255
|
-
controller.abort();
|
|
256
|
-
window.clearInterval(timer);
|
|
257
|
-
window.removeEventListener("focus", load);
|
|
258
|
-
};
|
|
259
|
-
}, [refresh]);
|
|
260
|
-
const probe = status?.status === "signed-in" ? status.probe : void 0;
|
|
261
|
-
const key = status?.status === "signed-in" ? status.probeKey : void 0;
|
|
262
|
-
const result = status === void 0 ? void 0 : resultFor(status, model);
|
|
263
|
-
const visible = probe?.candidates.includes(model) === true || result !== void 0;
|
|
264
|
-
(0, react.useEffect)(() => {
|
|
265
|
-
setConfirming(false);
|
|
266
|
-
setNote(void 0);
|
|
267
|
-
}, [model]);
|
|
268
|
-
const detect = async () => {
|
|
269
|
-
if (key === void 0 || inFlight.current || probe?.running === true) return;
|
|
270
|
-
inFlight.current = true;
|
|
271
|
-
setNote(void 0);
|
|
272
|
-
setConfirming(false);
|
|
273
|
-
setBusy(true);
|
|
274
|
-
setFailed(false);
|
|
275
|
-
try {
|
|
276
|
-
const response = await fetch(WORKBUDDY_PROBE_PATH, {
|
|
277
|
-
method: "POST",
|
|
278
|
-
credentials: "same-origin",
|
|
279
|
-
headers: {
|
|
280
|
-
"Content-Type": "application/json",
|
|
281
|
-
"X-WorkBuddy-Probe-Key": key
|
|
282
|
-
},
|
|
283
|
-
body: JSON.stringify({
|
|
284
|
-
action: "probe",
|
|
285
|
-
model
|
|
286
|
-
})
|
|
287
|
-
});
|
|
288
|
-
const body = await response.json();
|
|
289
|
-
if (!response.ok || body.state !== "ok" || body.validation !== "validating" && body.validation !== "non-validating" || !Array.isArray(body.efforts) || !body.efforts.every((effort) => typeof effort === "string")) throw new Error("probe failed");
|
|
290
|
-
if (mounted.current) {
|
|
291
|
-
const completed = {
|
|
292
|
-
id: model,
|
|
293
|
-
name: model,
|
|
294
|
-
validation: body.validation,
|
|
295
|
-
efforts: body.efforts,
|
|
296
|
-
probedAt: Date.now()
|
|
297
|
-
};
|
|
298
|
-
setNote(completed);
|
|
299
|
-
}
|
|
300
|
-
refresh().catch(() => {});
|
|
301
|
-
} catch {
|
|
302
|
-
if (mounted.current) setFailed(true);
|
|
303
|
-
} finally {
|
|
304
|
-
inFlight.current = false;
|
|
305
|
-
if (mounted.current) setBusy(false);
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
if (!visible) return null;
|
|
309
|
-
const text = tooltipText(t, model, {
|
|
310
|
-
busy,
|
|
311
|
-
result,
|
|
312
|
-
failed
|
|
313
|
-
});
|
|
314
|
-
const disabled = busy || probe?.running === true || key === void 0;
|
|
315
|
-
const showTooltip = tooltipVisible && !confirming && note === void 0;
|
|
316
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
317
|
-
style: wrapperStyle,
|
|
318
|
-
onMouseEnter: () => {
|
|
319
|
-
setTooltipVisible(true);
|
|
320
|
-
},
|
|
321
|
-
onMouseLeave: () => {
|
|
322
|
-
setTooltipVisible(false);
|
|
323
|
-
},
|
|
324
|
-
children: [
|
|
325
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
326
|
-
type: "button",
|
|
327
|
-
"aria-label": text,
|
|
328
|
-
"aria-describedby": showTooltip ? tooltipId : void 0,
|
|
329
|
-
"aria-busy": busy,
|
|
330
|
-
"aria-expanded": confirming,
|
|
331
|
-
disabled,
|
|
332
|
-
onClick: () => {
|
|
333
|
-
setConfirming(true);
|
|
334
|
-
},
|
|
335
|
-
onFocus: () => {
|
|
336
|
-
setTooltipVisible(true);
|
|
337
|
-
},
|
|
338
|
-
onBlur: () => {
|
|
339
|
-
setTooltipVisible(false);
|
|
340
|
-
},
|
|
341
|
-
style: {
|
|
342
|
-
...buttonStyle$1,
|
|
343
|
-
opacity: disabled && !confirming ? .6 : 1,
|
|
344
|
-
cursor: disabled ? "default" : "pointer"
|
|
345
|
-
},
|
|
346
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
347
|
-
width: "16",
|
|
348
|
-
height: "16",
|
|
349
|
-
viewBox: "0 0 24 24",
|
|
350
|
-
fill: "none",
|
|
351
|
-
stroke: "currentColor",
|
|
352
|
-
strokeWidth: "1.6",
|
|
353
|
-
"aria-hidden": "true",
|
|
354
|
-
focusable: "false",
|
|
355
|
-
children: [
|
|
356
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
357
|
-
cx: "12",
|
|
358
|
-
cy: "12",
|
|
359
|
-
r: "9"
|
|
360
|
-
}),
|
|
361
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
362
|
-
cx: "12",
|
|
363
|
-
cy: "12",
|
|
364
|
-
r: "4"
|
|
365
|
-
}),
|
|
366
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 12 20 4" }),
|
|
367
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
368
|
-
cx: "12",
|
|
369
|
-
cy: "12",
|
|
370
|
-
r: "1"
|
|
371
|
-
})
|
|
372
|
-
]
|
|
373
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
374
|
-
style: labelStyle,
|
|
375
|
-
children: label
|
|
376
|
-
})]
|
|
377
|
-
}),
|
|
378
|
-
showTooltip && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
379
|
-
id: tooltipId,
|
|
380
|
-
role: "tooltip",
|
|
381
|
-
style: tooltipStyle,
|
|
382
|
-
children: text
|
|
383
|
-
}),
|
|
384
|
-
confirming && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
385
|
-
style: confirmStyle,
|
|
386
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("probeBubbleBody") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
387
|
-
style: confirmRowStyle$1,
|
|
388
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
389
|
-
type: "button",
|
|
390
|
-
style: confirmButtonStyle,
|
|
391
|
-
onClick: () => {
|
|
392
|
-
setConfirming(false);
|
|
393
|
-
},
|
|
394
|
-
children: t("cancel")
|
|
395
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
396
|
-
type: "button",
|
|
397
|
-
style: primaryButtonStyle$1,
|
|
398
|
-
onClick: () => {
|
|
399
|
-
detect();
|
|
400
|
-
},
|
|
401
|
-
children: t("probeConfirmAction")
|
|
402
|
-
})]
|
|
403
|
-
})]
|
|
404
|
-
}),
|
|
405
|
-
note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
406
|
-
role: "status",
|
|
407
|
-
"aria-live": "polite",
|
|
408
|
-
style: noteStyle,
|
|
409
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: noteText(t, note) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
410
|
-
type: "button",
|
|
411
|
-
style: noteDismissStyle,
|
|
412
|
-
onClick: () => {
|
|
413
|
-
setNote(void 0);
|
|
414
|
-
},
|
|
415
|
-
children: t("probeNoteDismiss")
|
|
416
|
-
})]
|
|
417
|
-
})
|
|
418
|
-
]
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
/** Compose the one-line outcome string the note bubble shows. */
|
|
422
|
-
function noteText(t, result) {
|
|
423
|
-
if (result.validation === "validating" && result.efforts.length > 0) return t("probeNoteVerified", { levels: result.efforts.join(" / ") });
|
|
424
|
-
if (result.validation === "non-validating") return t("probeNoteNotValidating");
|
|
425
|
-
return t("probeNoteUnknown");
|
|
426
|
-
}
|
|
427
|
-
//#endregion
|
|
428
|
-
//#region src/client/WorkBuddyPluginCard.tsx
|
|
429
|
-
/** WorkBuddy status card contributed to Harness Plugin configuration. */
|
|
430
|
-
const POLL_INTERVAL_MS = 6e4;
|
|
431
|
-
const cardStyle = {
|
|
432
|
-
overflow: "hidden",
|
|
215
|
+
const confirmBoxStyle = {
|
|
216
|
+
display: "flex",
|
|
217
|
+
flexDirection: "column",
|
|
218
|
+
gap: 10,
|
|
219
|
+
padding: "10px 12px",
|
|
433
220
|
border: "1px solid var(--dsw-alias-border-l2)",
|
|
434
|
-
borderRadius:
|
|
435
|
-
background: "var(--dsw-alias-bg-
|
|
221
|
+
borderRadius: 8,
|
|
222
|
+
background: "var(--dsw-alias-bg-layer-1)"
|
|
436
223
|
};
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
border: 0,
|
|
445
|
-
padding: "13px 14px",
|
|
446
|
-
background: "transparent",
|
|
447
|
-
color: "var(--dsw-alias-label-primary)",
|
|
448
|
-
font: "inherit",
|
|
449
|
-
textAlign: "left",
|
|
450
|
-
cursor: "pointer"
|
|
451
|
-
};
|
|
452
|
-
const headTextStyle = {
|
|
453
|
-
display: "flex",
|
|
454
|
-
minWidth: 0,
|
|
455
|
-
flexDirection: "column",
|
|
456
|
-
gap: 3
|
|
457
|
-
};
|
|
458
|
-
const nameStyle = {
|
|
459
|
-
fontSize: 14,
|
|
460
|
-
lineHeight: "20px",
|
|
461
|
-
fontWeight: 600
|
|
462
|
-
};
|
|
463
|
-
const descriptionStyle = {
|
|
464
|
-
fontSize: 13,
|
|
465
|
-
lineHeight: "18px",
|
|
466
|
-
color: "var(--dsw-alias-label-tertiary)"
|
|
467
|
-
};
|
|
468
|
-
const chevronStyle = {
|
|
469
|
-
flex: "0 0 auto",
|
|
470
|
-
fontSize: 18,
|
|
471
|
-
lineHeight: 1,
|
|
472
|
-
transition: "transform 120ms ease"
|
|
473
|
-
};
|
|
474
|
-
const cardBodyStyle = {
|
|
475
|
-
borderTop: "1px solid var(--dsw-alias-border-l2)",
|
|
476
|
-
padding: "16px 14px 18px"
|
|
477
|
-
};
|
|
478
|
-
const bodyStyle = {
|
|
479
|
-
margin: 0,
|
|
480
|
-
fontSize: 14,
|
|
481
|
-
lineHeight: "22px",
|
|
482
|
-
color: "var(--dsw-alias-label-secondary)"
|
|
483
|
-
};
|
|
484
|
-
const rowStyle = {
|
|
485
|
-
display: "flex",
|
|
486
|
-
alignItems: "center",
|
|
487
|
-
justifyContent: "space-between",
|
|
488
|
-
flexWrap: "wrap",
|
|
489
|
-
gap: 12
|
|
490
|
-
};
|
|
491
|
-
const statusStyle = {
|
|
492
|
-
display: "flex",
|
|
493
|
-
alignItems: "center",
|
|
494
|
-
gap: 9,
|
|
495
|
-
fontSize: 15,
|
|
496
|
-
fontWeight: 500,
|
|
497
|
-
color: "var(--dsw-alias-label-primary)"
|
|
498
|
-
};
|
|
499
|
-
const buttonStyle = {
|
|
500
|
-
boxSizing: "border-box",
|
|
501
|
-
minHeight: 34,
|
|
502
|
-
padding: "6px 14px",
|
|
503
|
-
border: "1px solid var(--dsw-alias-border-l2)",
|
|
504
|
-
borderRadius: 18,
|
|
505
|
-
background: "var(--dsw-alias-bg-layer-1)",
|
|
506
|
-
color: "var(--dsw-alias-label-primary)",
|
|
507
|
-
font: "inherit",
|
|
508
|
-
fontSize: 14,
|
|
509
|
-
cursor: "pointer"
|
|
510
|
-
};
|
|
511
|
-
const errorStyle = {
|
|
512
|
-
...bodyStyle,
|
|
513
|
-
color: "var(--dsw-alias-state-error-primary)"
|
|
514
|
-
};
|
|
515
|
-
const quotaListStyle = {
|
|
516
|
-
display: "flex",
|
|
517
|
-
flexDirection: "column",
|
|
518
|
-
gap: 18,
|
|
519
|
-
paddingTop: 2
|
|
520
|
-
};
|
|
521
|
-
const quotaGroupStyle = {
|
|
522
|
-
display: "flex",
|
|
523
|
-
flexDirection: "column",
|
|
524
|
-
gap: 10
|
|
525
|
-
};
|
|
526
|
-
const quotaTitleStyle = {
|
|
527
|
-
margin: 0,
|
|
528
|
-
fontSize: 14,
|
|
529
|
-
lineHeight: "20px",
|
|
530
|
-
fontWeight: 600,
|
|
531
|
-
color: "var(--dsw-alias-label-primary)"
|
|
532
|
-
};
|
|
533
|
-
const quotaLabelStyle = {
|
|
534
|
-
display: "flex",
|
|
535
|
-
justifyContent: "space-between",
|
|
536
|
-
gap: 12,
|
|
537
|
-
fontSize: 13,
|
|
538
|
-
lineHeight: "20px",
|
|
539
|
-
color: "var(--dsw-alias-label-secondary)"
|
|
540
|
-
};
|
|
541
|
-
const modelBadgeStyle = {
|
|
542
|
-
display: "flex",
|
|
543
|
-
alignItems: "center",
|
|
544
|
-
gap: 6,
|
|
545
|
-
flexWrap: "wrap"
|
|
546
|
-
};
|
|
547
|
-
const modelOfferStyle = {
|
|
548
|
-
display: "flex",
|
|
549
|
-
flexDirection: "column",
|
|
550
|
-
gap: 2
|
|
551
|
-
};
|
|
552
|
-
const modelRateStyle = {
|
|
553
|
-
fontSize: 12,
|
|
554
|
-
lineHeight: "18px",
|
|
555
|
-
color: "var(--dsw-alias-label-tertiary)"
|
|
556
|
-
};
|
|
557
|
-
const modelBadgeChipStyle = {
|
|
558
|
-
padding: "1px 8px",
|
|
559
|
-
borderRadius: 999,
|
|
560
|
-
fontSize: 11,
|
|
561
|
-
lineHeight: "18px",
|
|
562
|
-
background: "var(--dsw-alias-state-success-subtle, rgba(34, 160, 107, 0.12))",
|
|
563
|
-
color: "var(--dsw-alias-state-success-primary, #22a06b)"
|
|
564
|
-
};
|
|
565
|
-
/** Localize an upstream promotional badge label, with an unknown-badge fallback. */
|
|
566
|
-
function modelBadgeLabel(badge, t) {
|
|
567
|
-
if (badge === "限时免费") return t("badgeLimitedFree");
|
|
568
|
-
if (badge === "夜间折扣") return t("badgeNightDiscount");
|
|
569
|
-
return badge;
|
|
570
|
-
}
|
|
571
|
-
const progressTrackStyle = {
|
|
572
|
-
height: 8,
|
|
573
|
-
overflow: "hidden",
|
|
574
|
-
borderRadius: 999,
|
|
575
|
-
background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))"
|
|
576
|
-
};
|
|
577
|
-
/**
|
|
578
|
-
* Inline confirmation box for a paid detection. Replaces the previous
|
|
579
|
-
* `window.confirm`: the decision is one line plus two buttons, and a modal
|
|
580
|
-
* alert for that is heavier than the action it guards.
|
|
581
|
-
*/
|
|
582
|
-
const confirmBoxStyle = {
|
|
583
|
-
display: "flex",
|
|
584
|
-
flexDirection: "column",
|
|
585
|
-
gap: 10,
|
|
586
|
-
padding: "10px 12px",
|
|
587
|
-
border: "1px solid var(--dsw-alias-border-l2)",
|
|
588
|
-
borderRadius: 8,
|
|
589
|
-
background: "var(--dsw-alias-bg-layer-1)"
|
|
590
|
-
};
|
|
591
|
-
const confirmRowStyle = {
|
|
592
|
-
display: "flex",
|
|
593
|
-
justifyContent: "flex-end",
|
|
594
|
-
gap: 8
|
|
595
|
-
};
|
|
596
|
-
/** One probeable model's row: name on the left, state and action on the right. */
|
|
597
|
-
const probeRowStyle = {
|
|
224
|
+
const confirmRowStyle$1 = {
|
|
225
|
+
display: "flex",
|
|
226
|
+
justifyContent: "flex-end",
|
|
227
|
+
gap: 8
|
|
228
|
+
};
|
|
229
|
+
/** One probeable model's row: name on the left, state and action on the right. */
|
|
230
|
+
const probeRowStyle = {
|
|
598
231
|
display: "flex",
|
|
599
232
|
alignItems: "center",
|
|
600
233
|
justifyContent: "space-between",
|
|
@@ -644,8 +277,8 @@ window.__ModuleLoader__.load({
|
|
|
644
277
|
* theme as a pair: `brand-primary` is a light accent here, so pairing it with a
|
|
645
278
|
* hardcoded white would render white-on-white.
|
|
646
279
|
*/
|
|
647
|
-
const primaryButtonStyle = {
|
|
648
|
-
...buttonStyle,
|
|
280
|
+
const primaryButtonStyle$1 = {
|
|
281
|
+
...buttonStyle$1,
|
|
649
282
|
border: "1px solid var(--dsw-alias-button-primary-fill)",
|
|
650
283
|
background: "var(--dsw-alias-button-primary-fill)",
|
|
651
284
|
color: "var(--dsw-alias-label-primary-foreground)"
|
|
@@ -708,474 +341,970 @@ window.__ModuleLoader__.load({
|
|
|
708
341
|
});
|
|
709
342
|
}
|
|
710
343
|
/**
|
|
711
|
-
* One model offer row: name, promotional badges, and the billing rate.
|
|
344
|
+
* One model offer row: name, promotional badges, and the billing rate.
|
|
345
|
+
*
|
|
346
|
+
* The rate sits under the name rather than beside it because the row already
|
|
347
|
+
* spends its horizontal budget on badges; stacking keeps long model names and
|
|
348
|
+
* several badges from squeezing the rate into an ellipsis.
|
|
349
|
+
*/
|
|
350
|
+
function ModelOfferRow({ model, t }) {
|
|
351
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
352
|
+
style: modelOfferStyle,
|
|
353
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
354
|
+
style: quotaLabelStyle,
|
|
355
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
356
|
+
style: modelBadgeStyle,
|
|
357
|
+
children: [model.badges?.map((badge) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
358
|
+
style: modelBadgeChipStyle,
|
|
359
|
+
children: modelBadgeLabel(badge, t)
|
|
360
|
+
}, badge)), model.free === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
361
|
+
style: modelBadgeChipStyle,
|
|
362
|
+
children: t("freeModel")
|
|
363
|
+
}) : null]
|
|
364
|
+
})]
|
|
365
|
+
}), model.credits === void 0 ? model.rateUnknown === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
366
|
+
style: modelRateStyle,
|
|
367
|
+
children: t("rateUnknown")
|
|
368
|
+
}) : null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
369
|
+
style: modelRateStyle,
|
|
370
|
+
children: t("rate", { rate: model.credits })
|
|
371
|
+
})]
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Context capacity, listed in full.
|
|
376
|
+
*
|
|
377
|
+
* Every model the upstream reports a capacity for, largest first. A one-line
|
|
378
|
+
* summary with the exceptions on hover was tried and rejected: capacity is
|
|
379
|
+
* reference data you scan by model, and hiding most of it behind a hover made
|
|
380
|
+
* the common case (a model you already have in mind) the hard one to look up.
|
|
381
|
+
*
|
|
382
|
+
* Purely a report of the upstream's own numbers. The plugin offers no tier
|
|
383
|
+
* picker: the CN catalog declares one capacity per model and publishes no
|
|
384
|
+
* alternatives, so a menu there would mean inventing client-side policy. The
|
|
385
|
+
* international document does declare alternatives (`supportedLengths`), and
|
|
386
|
+
* they are shown as a secondary figure rather than merged into one number —
|
|
387
|
+
* the default is the budget actually requested, while the larger value is a
|
|
388
|
+
* ceiling the upstream would accept.
|
|
389
|
+
*/
|
|
390
|
+
function ContextTable({ models, t }) {
|
|
391
|
+
const known = (models ?? []).filter((model) => model.contextWindow !== void 0).sort((a, b) => b.contextWindow - a.contextWindow);
|
|
392
|
+
if (known.length === 0) return null;
|
|
393
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
394
|
+
style: quotaListStyle,
|
|
395
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
396
|
+
style: quotaTitleStyle,
|
|
397
|
+
children: t("contextHeading")
|
|
398
|
+
}), known.map((model) => {
|
|
399
|
+
const capacity = model.contextWindow;
|
|
400
|
+
const alternative = model.maxContextWindow !== void 0 && model.maxContextWindow > capacity ? model.maxContextWindow : void 0;
|
|
401
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
402
|
+
style: quotaLabelStyle,
|
|
403
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
404
|
+
style: modelOfferStyle,
|
|
405
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
406
|
+
style: { textAlign: "right" },
|
|
407
|
+
children: formatTokens(capacity)
|
|
408
|
+
}), alternative === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
409
|
+
style: modelRateStyle,
|
|
410
|
+
children: t("contextUpTo", { size: formatTokens(alternative) })
|
|
411
|
+
})]
|
|
412
|
+
})]
|
|
413
|
+
}, model.id);
|
|
414
|
+
})]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Compact token count for display: the catalog's own round numbers (`200000`,
|
|
419
|
+
* `1000000`) read better as `200K` / `1M`, and no precision is lost because
|
|
420
|
+
* these values are always whole thousands.
|
|
421
|
+
*/
|
|
422
|
+
function formatTokens(tokens) {
|
|
423
|
+
if (tokens >= 1e6 && tokens % 1e6 === 0) return `${tokens / 1e6}M`;
|
|
424
|
+
if (tokens >= 1e3 && tokens % 1e3 === 0) return `${tokens / 1e3}K`;
|
|
425
|
+
return String(tokens);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Reasoning-effort detection section: consent switches, per-model detection,
|
|
429
|
+
* and the recorded observations.
|
|
430
|
+
*
|
|
431
|
+
* Two deliberate UX rules from the plan (§3.1, §3.2):
|
|
432
|
+
* - the confirmation is shown *before* any request, and its copy states the
|
|
433
|
+
* credit caveat;
|
|
434
|
+
* - a `non-validating` result is presented as an observation about the
|
|
435
|
+
* parameter ("this model does not check it"), never as a statement that a
|
|
436
|
+
* level is unsupported.
|
|
437
|
+
*/
|
|
438
|
+
function ProbeSection({ probe, t, onDetect, onClear, busy }) {
|
|
439
|
+
const [pending, setPending] = (0, react.useState)();
|
|
440
|
+
const [runningModel, setRunningModel] = (0, react.useState)();
|
|
441
|
+
(0, react.useEffect)(() => {
|
|
442
|
+
if (pending !== void 0 && !probe.candidates.includes(pending)) setPending(void 0);
|
|
443
|
+
}, [pending, probe.candidates]);
|
|
444
|
+
const runningArmed = (0, react.useRef)(false);
|
|
445
|
+
(0, react.useEffect)(() => {
|
|
446
|
+
if (runningModel === void 0) return;
|
|
447
|
+
if (busy || probe.running) {
|
|
448
|
+
runningArmed.current = true;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (!runningArmed.current) return;
|
|
452
|
+
runningArmed.current = false;
|
|
453
|
+
setRunningModel(void 0);
|
|
454
|
+
}, [
|
|
455
|
+
runningModel,
|
|
456
|
+
busy,
|
|
457
|
+
probe.running
|
|
458
|
+
]);
|
|
459
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
460
|
+
style: quotaListStyle,
|
|
461
|
+
children: [
|
|
462
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
463
|
+
style: quotaTitleStyle,
|
|
464
|
+
children: t("probeHeading")
|
|
465
|
+
}),
|
|
466
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
467
|
+
style: bodyStyle,
|
|
468
|
+
children: t("probeIntro")
|
|
469
|
+
}),
|
|
470
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
471
|
+
style: bodyStyle,
|
|
472
|
+
children: t("probeConsentHint")
|
|
473
|
+
}),
|
|
474
|
+
probe.running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
475
|
+
style: bodyStyle,
|
|
476
|
+
children: t("probeRunningGeneric")
|
|
477
|
+
}) : null,
|
|
478
|
+
probe.candidates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
479
|
+
style: bodyStyle,
|
|
480
|
+
children: t("probeResultEmpty")
|
|
481
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
482
|
+
style: quotaGroupStyle,
|
|
483
|
+
children: probe.candidates.map((id) => {
|
|
484
|
+
const result = probe.results.find((entry) => entry.id === id);
|
|
485
|
+
const name = result?.name ?? id;
|
|
486
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
487
|
+
style: modelOfferStyle,
|
|
488
|
+
children: [
|
|
489
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
490
|
+
style: probeRowStyle,
|
|
491
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
492
|
+
style: probeRowEndStyle,
|
|
493
|
+
children: [result === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
494
|
+
style: modelBadgeChipStyle,
|
|
495
|
+
children: result.validation === "validating" && result.efforts.length > 0 ? result.efforts.join(" / ") : t(result.validation === "non-validating" ? "probeResultNotValidating" : "probeResultUnknown")
|
|
496
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
497
|
+
type: "button",
|
|
498
|
+
style: buttonStyle$1,
|
|
499
|
+
disabled: probe.running || busy,
|
|
500
|
+
onClick: () => {
|
|
501
|
+
setPending(id);
|
|
502
|
+
},
|
|
503
|
+
children: runningModel === id ? t("probeRunning", { model: id }) : t(result === void 0 ? "probeStart" : "probeRedetect")
|
|
504
|
+
})]
|
|
505
|
+
})]
|
|
506
|
+
}),
|
|
507
|
+
result === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
508
|
+
style: modelRateStyle,
|
|
509
|
+
children: t("probeResultAt", { time: formatTime(result.probedAt) })
|
|
510
|
+
}),
|
|
511
|
+
pending === id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
512
|
+
style: confirmBoxStyle,
|
|
513
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
514
|
+
style: bodyStyle,
|
|
515
|
+
children: t("probeConfirmBody", { model: name })
|
|
516
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
517
|
+
style: confirmRowStyle$1,
|
|
518
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
519
|
+
type: "button",
|
|
520
|
+
style: buttonStyle$1,
|
|
521
|
+
onClick: () => {
|
|
522
|
+
setPending(void 0);
|
|
523
|
+
},
|
|
524
|
+
children: t("cancel")
|
|
525
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
526
|
+
type: "button",
|
|
527
|
+
style: primaryButtonStyle$1,
|
|
528
|
+
disabled: probe.running || busy,
|
|
529
|
+
onClick: () => {
|
|
530
|
+
setRunningModel(id);
|
|
531
|
+
setPending(void 0);
|
|
532
|
+
onDetect(id);
|
|
533
|
+
},
|
|
534
|
+
children: t("probeConfirmAction")
|
|
535
|
+
})]
|
|
536
|
+
})]
|
|
537
|
+
}) : null
|
|
538
|
+
]
|
|
539
|
+
}, id);
|
|
540
|
+
})
|
|
541
|
+
}),
|
|
542
|
+
probe.results.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
543
|
+
type: "button",
|
|
544
|
+
style: buttonStyle$1,
|
|
545
|
+
disabled: busy,
|
|
546
|
+
onClick: () => {
|
|
547
|
+
onClear();
|
|
548
|
+
},
|
|
549
|
+
children: t("probeClear")
|
|
550
|
+
})
|
|
551
|
+
]
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
/** Render WorkBuddy sign-in state and credit as one expandable card. */
|
|
555
|
+
function WorkBuddyPluginCard({ t, variant = CN_CARD_VARIANT }) {
|
|
556
|
+
if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
|
|
557
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
558
|
+
const [status, setStatus] = (0, react.useState)({ status: "signed-out" });
|
|
559
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
560
|
+
const [tab, setTab] = (0, react.useState)("status");
|
|
561
|
+
const mounted = (0, react.useRef)(true);
|
|
562
|
+
(0, react.useEffect)(() => {
|
|
563
|
+
mounted.current = true;
|
|
564
|
+
return () => {
|
|
565
|
+
mounted.current = false;
|
|
566
|
+
};
|
|
567
|
+
}, []);
|
|
568
|
+
const refresh = (0, react.useCallback)(async (signal) => {
|
|
569
|
+
try {
|
|
570
|
+
const response = await fetch(variant.statusPath, {
|
|
571
|
+
headers: { accept: "application/json" },
|
|
572
|
+
credentials: "same-origin",
|
|
573
|
+
...signal === void 0 ? {} : { signal }
|
|
574
|
+
});
|
|
575
|
+
const value = await response.json().catch(() => void 0);
|
|
576
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
577
|
+
if (mounted.current && signal?.aborted !== true) setStatus(value);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (mounted.current && signal?.aborted !== true) setStatus({
|
|
580
|
+
status: "error",
|
|
581
|
+
message: error instanceof Error ? error.message : t("requestFailed")
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}, [t, variant.statusPath]);
|
|
585
|
+
(0, react.useEffect)(() => {
|
|
586
|
+
if (!open) return;
|
|
587
|
+
const controller = new AbortController();
|
|
588
|
+
refresh(controller.signal);
|
|
589
|
+
return () => {
|
|
590
|
+
controller.abort();
|
|
591
|
+
};
|
|
592
|
+
}, [open, refresh]);
|
|
593
|
+
(0, react.useEffect)(() => {
|
|
594
|
+
if (!open || status.status !== "signed-in") return;
|
|
595
|
+
const controller = new AbortController();
|
|
596
|
+
const timer = window.setInterval(() => {
|
|
597
|
+
refresh(controller.signal);
|
|
598
|
+
}, POLL_INTERVAL_MS);
|
|
599
|
+
return () => {
|
|
600
|
+
window.clearInterval(timer);
|
|
601
|
+
controller.abort();
|
|
602
|
+
};
|
|
603
|
+
}, [
|
|
604
|
+
open,
|
|
605
|
+
refresh,
|
|
606
|
+
status.status
|
|
607
|
+
]);
|
|
608
|
+
const manualRefresh = async () => {
|
|
609
|
+
setBusy(true);
|
|
610
|
+
try {
|
|
611
|
+
await refresh();
|
|
612
|
+
} finally {
|
|
613
|
+
if (mounted.current) setBusy(false);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
/**
|
|
617
|
+
* Ask the host to re-read the credential and re-fetch this variant's catalog.
|
|
618
|
+
*
|
|
619
|
+
* Shares the probe route's key and guards: it is a write that spends an
|
|
620
|
+
* upstream request, so it does not belong on the read-only status GET. A
|
|
621
|
+
* failure is surfaced through the refreshed document's `catalog.error` rather
|
|
622
|
+
* than thrown away, so the reason survives the round trip.
|
|
623
|
+
*/
|
|
624
|
+
const refreshModels = (0, react.useCallback)(async () => {
|
|
625
|
+
const key = status.status === "signed-in" ? status.probeKey : void 0;
|
|
626
|
+
if (key === void 0) return;
|
|
627
|
+
setBusy(true);
|
|
628
|
+
try {
|
|
629
|
+
const response = await fetch(variant.probePath, {
|
|
630
|
+
method: "POST",
|
|
631
|
+
headers: {
|
|
632
|
+
"Content-Type": "application/json",
|
|
633
|
+
"X-WorkBuddy-Probe-Key": key
|
|
634
|
+
},
|
|
635
|
+
credentials: "same-origin",
|
|
636
|
+
body: JSON.stringify({ action: "refresh" })
|
|
637
|
+
});
|
|
638
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (mounted.current) setStatus((previous) => ({
|
|
641
|
+
status: "error",
|
|
642
|
+
message: error instanceof Error ? error.message : t("requestFailed")
|
|
643
|
+
}));
|
|
644
|
+
return;
|
|
645
|
+
} finally {
|
|
646
|
+
if (mounted.current) setBusy(false);
|
|
647
|
+
}
|
|
648
|
+
await refresh();
|
|
649
|
+
}, [
|
|
650
|
+
refresh,
|
|
651
|
+
status,
|
|
652
|
+
t,
|
|
653
|
+
variant.probePath
|
|
654
|
+
]);
|
|
655
|
+
/**
|
|
656
|
+
* Run one control action and refresh the card's state afterwards.
|
|
657
|
+
*
|
|
658
|
+
* The key travels in a header, not the body: it authorizes the write, and
|
|
659
|
+
* the host never accepts a prompt, a sentinel, or a model outside its own
|
|
660
|
+
* catalog from here.
|
|
661
|
+
*/
|
|
662
|
+
const control = (0, react.useCallback)(async (action) => {
|
|
663
|
+
const key = status.status === "signed-in" ? status.probeKey : void 0;
|
|
664
|
+
if (key === void 0) return;
|
|
665
|
+
setBusy(true);
|
|
666
|
+
try {
|
|
667
|
+
const response = await fetch(variant.probePath, {
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers: {
|
|
670
|
+
"Content-Type": "application/json",
|
|
671
|
+
"X-WorkBuddy-Probe-Key": key
|
|
672
|
+
},
|
|
673
|
+
credentials: "same-origin",
|
|
674
|
+
body: JSON.stringify(action)
|
|
675
|
+
});
|
|
676
|
+
const value = await response.json().catch(() => void 0);
|
|
677
|
+
if (!response.ok) {
|
|
678
|
+
const message = typeof value === "object" && value !== null && "error" in value ? String(value["error"]) : `HTTP ${response.status}`;
|
|
679
|
+
throw new Error(message);
|
|
680
|
+
}
|
|
681
|
+
await refresh();
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (mounted.current) setStatus((previous) => ({
|
|
684
|
+
status: "error",
|
|
685
|
+
message: error instanceof Error ? error.message : t("requestFailed")
|
|
686
|
+
}));
|
|
687
|
+
} finally {
|
|
688
|
+
if (mounted.current) setBusy(false);
|
|
689
|
+
}
|
|
690
|
+
}, [
|
|
691
|
+
refresh,
|
|
692
|
+
status,
|
|
693
|
+
t,
|
|
694
|
+
variant.probePath
|
|
695
|
+
]);
|
|
696
|
+
/**
|
|
697
|
+
* Start a detection. Confirmation happens inline in the section, so this is
|
|
698
|
+
* only ever called after the user has already agreed.
|
|
699
|
+
*/
|
|
700
|
+
const confirmDetect = (0, react.useCallback)((modelId) => {
|
|
701
|
+
control({
|
|
702
|
+
action: "probe",
|
|
703
|
+
model: modelId
|
|
704
|
+
});
|
|
705
|
+
}, [control]);
|
|
706
|
+
const title = t(variant.titleKey);
|
|
707
|
+
const label = status.status === "signed-in" ? status.nickname === void 0 ? t("signedInAs", { nickname: "" }).trimEnd().replace(/[::]$/, "") : t("signedInAs", { nickname: status.nickname }) : status.status === "error" ? t("requestFailed") : t("signedOut");
|
|
708
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
709
|
+
style: cardStyle,
|
|
710
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
711
|
+
type: "button",
|
|
712
|
+
style: headerStyle,
|
|
713
|
+
"aria-expanded": open,
|
|
714
|
+
"aria-label": `${t(open ? "collapse" : "expand")}: ${title}`,
|
|
715
|
+
onClick: () => {
|
|
716
|
+
setOpen(!open);
|
|
717
|
+
},
|
|
718
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
719
|
+
style: headTextStyle,
|
|
720
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
721
|
+
style: nameStyle,
|
|
722
|
+
children: title
|
|
723
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
724
|
+
style: descriptionStyle,
|
|
725
|
+
children: t(variant.introKey)
|
|
726
|
+
})]
|
|
727
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
728
|
+
"aria-hidden": "true",
|
|
729
|
+
style: {
|
|
730
|
+
...chevronStyle,
|
|
731
|
+
transform: open ? "rotate(180deg)" : "none"
|
|
732
|
+
},
|
|
733
|
+
children: "⌄"
|
|
734
|
+
})]
|
|
735
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
736
|
+
style: cardBodyStyle,
|
|
737
|
+
children: [
|
|
738
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
739
|
+
style: quotaTitleStyle,
|
|
740
|
+
children: t("accountHeading")
|
|
741
|
+
}),
|
|
742
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
743
|
+
style: rowStyle,
|
|
744
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
745
|
+
style: statusStyle,
|
|
746
|
+
role: "status",
|
|
747
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
748
|
+
"aria-hidden": "true",
|
|
749
|
+
style: dotStyle(status.status)
|
|
750
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
751
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
752
|
+
type: "button",
|
|
753
|
+
style: buttonStyle$1,
|
|
754
|
+
disabled: busy,
|
|
755
|
+
onClick: () => {
|
|
756
|
+
manualRefresh();
|
|
757
|
+
},
|
|
758
|
+
children: busy ? t("refreshing") : t("refresh")
|
|
759
|
+
})]
|
|
760
|
+
}),
|
|
761
|
+
status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
762
|
+
status.expiresAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
763
|
+
style: bodyStyle,
|
|
764
|
+
children: t("accessTokenExpires", { time: formatTime(status.expiresAt) })
|
|
765
|
+
}),
|
|
766
|
+
status.catalog === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
767
|
+
style: rowStyle,
|
|
768
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
769
|
+
style: bodyStyle,
|
|
770
|
+
children: [status.catalog.source === "live" && status.catalog.fetchedAt !== void 0 ? t("catalogLive", { time: formatTime(status.catalog.fetchedAt) }) : status.catalog.source === "saved" && status.catalog.fetchedAt !== void 0 ? t("catalogSaved", { time: formatTime(status.catalog.fetchedAt) }) : t("catalogFallback"), status.catalog.appVersion === void 0 ? "" : ` · ${t("catalogAppVersion", { version: status.catalog.appVersion })}`]
|
|
771
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
772
|
+
type: "button",
|
|
773
|
+
style: buttonStyle$1,
|
|
774
|
+
disabled: busy,
|
|
775
|
+
onClick: () => {
|
|
776
|
+
refreshModels();
|
|
777
|
+
},
|
|
778
|
+
children: busy ? t("refreshingModels") : t("refreshModels")
|
|
779
|
+
})]
|
|
780
|
+
}),
|
|
781
|
+
status.catalog?.error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
782
|
+
style: errorStyle,
|
|
783
|
+
children: t("catalogError", { message: status.catalog.error })
|
|
784
|
+
}),
|
|
785
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
786
|
+
role: "tablist",
|
|
787
|
+
style: tabBarStyle,
|
|
788
|
+
children: [
|
|
789
|
+
"status",
|
|
790
|
+
"context",
|
|
791
|
+
"details"
|
|
792
|
+
].map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
793
|
+
type: "button",
|
|
794
|
+
role: "tab",
|
|
795
|
+
"aria-selected": tab === id,
|
|
796
|
+
onClick: () => {
|
|
797
|
+
setTab(id);
|
|
798
|
+
},
|
|
799
|
+
style: {
|
|
800
|
+
...tabStyle,
|
|
801
|
+
...tab === id ? tabActiveStyle : {}
|
|
802
|
+
},
|
|
803
|
+
children: t(id === "status" ? "tabStatus" : id === "context" ? "tabContext" : "tabDetails")
|
|
804
|
+
}, id))
|
|
805
|
+
}),
|
|
806
|
+
tab === "status" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
807
|
+
style: tabPanelStyle,
|
|
808
|
+
children: [
|
|
809
|
+
status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
810
|
+
style: quotaListStyle,
|
|
811
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
812
|
+
style: rowStyle,
|
|
813
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
814
|
+
style: quotaTitleStyle,
|
|
815
|
+
children: t("creditsHeading")
|
|
816
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
817
|
+
style: bodyStyle,
|
|
818
|
+
children: t("creditsTotal", { total: formatNumber(status.credits.total) })
|
|
819
|
+
})]
|
|
820
|
+
})
|
|
821
|
+
}),
|
|
822
|
+
status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
823
|
+
style: errorStyle,
|
|
824
|
+
children: t("creditsError", { message: status.creditsError })
|
|
825
|
+
}),
|
|
826
|
+
status.probe === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeSection, {
|
|
827
|
+
probe: status.probe,
|
|
828
|
+
t,
|
|
829
|
+
busy,
|
|
830
|
+
onDetect: confirmDetect,
|
|
831
|
+
onClear: () => {
|
|
832
|
+
control({ action: "clear" });
|
|
833
|
+
}
|
|
834
|
+
})
|
|
835
|
+
]
|
|
836
|
+
}) : tab === "context" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
837
|
+
style: tabPanelStyle,
|
|
838
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextTable, {
|
|
839
|
+
models: status.models,
|
|
840
|
+
t
|
|
841
|
+
})
|
|
842
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
843
|
+
style: tabPanelStyle,
|
|
844
|
+
children: [status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
845
|
+
style: quotaListStyle,
|
|
846
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
847
|
+
style: quotaTitleStyle,
|
|
848
|
+
children: t("creditsDetailHeading")
|
|
849
|
+
}), status.credits.accounts.filter((account) => account.remain > 0).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
|
|
850
|
+
label: account.packageName,
|
|
851
|
+
remain: account.remain,
|
|
852
|
+
size: account.size,
|
|
853
|
+
t
|
|
854
|
+
}, `${account.packageName}-${String(index)}`))]
|
|
855
|
+
}), status.models === void 0 || status.models.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
856
|
+
style: quotaListStyle,
|
|
857
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
858
|
+
style: quotaTitleStyle,
|
|
859
|
+
children: t("modelsHeading")
|
|
860
|
+
}), status.models.filter((model) => model.free === true || (model.badges?.length ?? 0) > 0).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelOfferRow, {
|
|
861
|
+
model,
|
|
862
|
+
t
|
|
863
|
+
}, model.id))]
|
|
864
|
+
})]
|
|
865
|
+
})
|
|
866
|
+
] }) : null,
|
|
867
|
+
status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
868
|
+
style: status.reason === void 0 ? bodyStyle : errorStyle,
|
|
869
|
+
children: status.reason ?? t(variant.signedOutKey)
|
|
870
|
+
}) : null,
|
|
871
|
+
status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
872
|
+
style: errorStyle,
|
|
873
|
+
children: status.message
|
|
874
|
+
}) : null
|
|
875
|
+
]
|
|
876
|
+
}) : null]
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/client/WorkBuddyProbeControl.tsx
|
|
881
|
+
/**
|
|
882
|
+
* Per-model reasoning-effort entry beside the Composer's model selector.
|
|
712
883
|
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
* several badges from squeezing the rate into an ellipsis.
|
|
716
|
-
*/
|
|
717
|
-
function ModelOfferRow({ model, t }) {
|
|
718
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
719
|
-
style: modelOfferStyle,
|
|
720
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
721
|
-
style: quotaLabelStyle,
|
|
722
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
723
|
-
style: modelBadgeStyle,
|
|
724
|
-
children: [model.badges?.map((badge) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
725
|
-
style: modelBadgeChipStyle,
|
|
726
|
-
children: modelBadgeLabel(badge, t)
|
|
727
|
-
}, badge)), model.free === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
728
|
-
style: modelBadgeChipStyle,
|
|
729
|
-
children: t("freeModel")
|
|
730
|
-
}) : null]
|
|
731
|
-
})]
|
|
732
|
-
}), model.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
733
|
-
style: modelRateStyle,
|
|
734
|
-
children: t("rate", { rate: model.credits })
|
|
735
|
-
})]
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Context capacity, listed in full.
|
|
884
|
+
* Interaction follows the Fast Mode control `dsh-codex-connect` ships in this
|
|
885
|
+
* same seat, which is the established shape for composer chrome here:
|
|
740
886
|
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
887
|
+
* - a **static inline label** next to the icon names the feature ("Reasoning
|
|
888
|
+
* levels"), set smaller and dimmer than the surrounding chrome so it reads as
|
|
889
|
+
* an annotation on the icon. It never carries state: the verified levels
|
|
890
|
+
* already appear in the model dropdown (the adapter exposes them as
|
|
891
|
+
* selectable efforts), so repeating them here would duplicate the real answer
|
|
892
|
+
* and make the label's width jump as results change.
|
|
893
|
+
* - a **hover/focus tooltip** carries the state and the click's purpose, the way
|
|
894
|
+
* Fast Mode's tooltip explains its current speed.
|
|
895
|
+
* - the **confirmation** is a small bubble anchored to the control, not a
|
|
896
|
+
* `window.confirm`. Probing spends real credit, so a confirmation stays — but
|
|
897
|
+
* it belongs next to the thing it acts on, sized to one line plus two small
|
|
898
|
+
* buttons.
|
|
745
899
|
*
|
|
746
|
-
*
|
|
747
|
-
* picker: the catalog declares one capacity per model and publishes no
|
|
748
|
-
* alternatives, so a menu would mean inventing client-side policy. (The
|
|
749
|
-
* desktop app's "300K / 1M" selector appears nowhere in the API.)
|
|
900
|
+
* @module dsh-workbuddy-connect/client/probe-control
|
|
750
901
|
*/
|
|
751
|
-
function ContextTable({ models, t }) {
|
|
752
|
-
const known = (models ?? []).filter((model) => model.contextWindow !== void 0).sort((a, b) => b.contextWindow - a.contextWindow);
|
|
753
|
-
if (known.length === 0) return null;
|
|
754
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
755
|
-
style: quotaListStyle,
|
|
756
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
757
|
-
style: quotaTitleStyle,
|
|
758
|
-
children: t("contextHeading")
|
|
759
|
-
}), known.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
760
|
-
style: quotaLabelStyle,
|
|
761
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTokens(model.contextWindow) })]
|
|
762
|
-
}, model.id))]
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
902
|
/**
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
903
|
+
* The card (and therefore the routes) a selected provider belongs to.
|
|
904
|
+
*
|
|
905
|
+
* The control serves both WorkBuddy providers from one seat, so the provider id
|
|
906
|
+
* is what selects the status and probe endpoints. Returning `undefined` for any
|
|
907
|
+
* other provider is what keeps the icon off every non-WorkBuddy model.
|
|
769
908
|
*/
|
|
770
|
-
function
|
|
771
|
-
|
|
772
|
-
if (tokens >= 1e3 && tokens % 1e3 === 0) return `${tokens / 1e3}K`;
|
|
773
|
-
return String(tokens);
|
|
909
|
+
function cardVariantFor(provider) {
|
|
910
|
+
return CARD_VARIANTS.find((card) => card.id === provider);
|
|
774
911
|
}
|
|
912
|
+
/** How often the control re-checks state when the window regains focus. */
|
|
913
|
+
const RECONCILE_MS = 6e4;
|
|
914
|
+
const wrapperStyle = {
|
|
915
|
+
display: "inline-flex",
|
|
916
|
+
position: "relative",
|
|
917
|
+
alignItems: "center",
|
|
918
|
+
transform: "translateY(2px)",
|
|
919
|
+
marginRight: -8
|
|
920
|
+
};
|
|
921
|
+
const buttonStyle = {
|
|
922
|
+
display: "inline-flex",
|
|
923
|
+
alignItems: "center",
|
|
924
|
+
justifyContent: "center",
|
|
925
|
+
gap: 2,
|
|
926
|
+
height: 30,
|
|
927
|
+
padding: "0 6px",
|
|
928
|
+
border: 0,
|
|
929
|
+
borderRadius: 8,
|
|
930
|
+
background: "transparent",
|
|
931
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
932
|
+
font: "inherit",
|
|
933
|
+
whiteSpace: "nowrap",
|
|
934
|
+
cursor: "pointer"
|
|
935
|
+
};
|
|
775
936
|
/**
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
* Two deliberate UX rules from the plan (§3.1, §3.2):
|
|
780
|
-
* - the confirmation is shown *before* any request, and its copy states the
|
|
781
|
-
* credit caveat;
|
|
782
|
-
* - a `non-validating` result is presented as an observation about the
|
|
783
|
-
* parameter ("this model does not check it"), never as a statement that a
|
|
784
|
-
* level is unsupported.
|
|
937
|
+
* The inline label. Smaller and dimmer than the surrounding chrome on purpose:
|
|
938
|
+
* it names the feature, so it should read as an annotation attached to the icon
|
|
939
|
+
* rather than compete with the adjacent model selector.
|
|
785
940
|
*/
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
941
|
+
const labelStyle = {
|
|
942
|
+
fontSize: 11,
|
|
943
|
+
lineHeight: "16px",
|
|
944
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
945
|
+
};
|
|
946
|
+
/** Tooltip bubble: the Fast Mode shape (nowrap, one line, above the control). */
|
|
947
|
+
const tooltipStyle = {
|
|
948
|
+
position: "absolute",
|
|
949
|
+
left: "50%",
|
|
950
|
+
bottom: "calc(100% + 8px)",
|
|
951
|
+
zIndex: 1e3,
|
|
952
|
+
transform: "translateX(-50%)",
|
|
953
|
+
padding: "4px 8px",
|
|
954
|
+
borderRadius: 6,
|
|
955
|
+
background: "var(--dsw-specific-tip, #1f2329)",
|
|
956
|
+
boxShadow: "var(--dsw-shadow-lv2)",
|
|
957
|
+
color: "var(--dsw-alias-label-primary, #fff)",
|
|
958
|
+
fontSize: 12,
|
|
959
|
+
lineHeight: "18px",
|
|
960
|
+
whiteSpace: "nowrap",
|
|
961
|
+
pointerEvents: "none"
|
|
962
|
+
};
|
|
963
|
+
/** Confirmation bubble: same anchor, but interactive and allowed to wrap. */
|
|
964
|
+
const confirmStyle = {
|
|
965
|
+
position: "absolute",
|
|
966
|
+
right: 0,
|
|
967
|
+
bottom: "calc(100% + 8px)",
|
|
968
|
+
zIndex: 1001,
|
|
969
|
+
display: "flex",
|
|
970
|
+
flexDirection: "column",
|
|
971
|
+
gap: 8,
|
|
972
|
+
width: 260,
|
|
973
|
+
padding: "10px 12px",
|
|
974
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
975
|
+
borderRadius: 8,
|
|
976
|
+
background: "var(--dsw-alias-bg-layer-1, #fff)",
|
|
977
|
+
boxShadow: "var(--dsw-shadow-lv2)",
|
|
978
|
+
color: "var(--dsw-alias-label-primary)",
|
|
979
|
+
fontSize: 12,
|
|
980
|
+
lineHeight: "18px"
|
|
981
|
+
};
|
|
982
|
+
const confirmRowStyle = {
|
|
983
|
+
display: "flex",
|
|
984
|
+
justifyContent: "flex-end",
|
|
985
|
+
gap: 8
|
|
986
|
+
};
|
|
987
|
+
const confirmButtonStyle = {
|
|
988
|
+
padding: "3px 10px",
|
|
989
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
990
|
+
borderRadius: 6,
|
|
991
|
+
background: "transparent",
|
|
992
|
+
color: "inherit",
|
|
993
|
+
font: "inherit",
|
|
994
|
+
fontSize: 12,
|
|
995
|
+
cursor: "pointer"
|
|
996
|
+
};
|
|
997
|
+
/**
|
|
998
|
+
* Primary action inside the confirmation bubble.
|
|
999
|
+
*
|
|
1000
|
+
* The fill and its text colour must come as a pair: `brand-primary` resolves to
|
|
1001
|
+
* a light accent in this theme, so hardcoding `color: #fff` on top of it renders
|
|
1002
|
+
* white-on-white. `button-primary-fill` + `label-primary-foreground` is the
|
|
1003
|
+
* theme's own pair for exactly this, and is what `dsh-codex-connect` uses for
|
|
1004
|
+
* the same job.
|
|
1005
|
+
*/
|
|
1006
|
+
const primaryButtonStyle = {
|
|
1007
|
+
...confirmButtonStyle,
|
|
1008
|
+
border: "1px solid var(--dsw-alias-button-primary-fill)",
|
|
1009
|
+
background: "var(--dsw-alias-button-primary-fill)",
|
|
1010
|
+
color: "var(--dsw-alias-label-primary-foreground)"
|
|
1011
|
+
};
|
|
1012
|
+
/**
|
|
1013
|
+
* Result note: a single line + a dismiss button, anchored to the control's
|
|
1014
|
+
* right side. Smaller than the confirmation bubble because it carries an
|
|
1015
|
+
* *outcome*, not a *decision* — the work is done, the user only has to read
|
|
1016
|
+
* and dismiss.
|
|
1017
|
+
*/
|
|
1018
|
+
const noteStyle = {
|
|
1019
|
+
position: "absolute",
|
|
1020
|
+
right: 0,
|
|
1021
|
+
bottom: "calc(100% + 8px)",
|
|
1022
|
+
zIndex: 1001,
|
|
1023
|
+
display: "flex",
|
|
1024
|
+
alignItems: "center",
|
|
1025
|
+
gap: 12,
|
|
1026
|
+
padding: "6px 10px",
|
|
1027
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1028
|
+
borderRadius: 8,
|
|
1029
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
1030
|
+
boxShadow: "var(--dsw-shadow-lv2)",
|
|
1031
|
+
color: "var(--dsw-alias-label-primary)",
|
|
1032
|
+
fontSize: 12,
|
|
1033
|
+
lineHeight: "18px",
|
|
1034
|
+
whiteSpace: "nowrap"
|
|
1035
|
+
};
|
|
1036
|
+
/**
|
|
1037
|
+
* The note's dismiss action. Outlined rather than bare text: inside an already
|
|
1038
|
+
* bordered bubble, an unbordered word does not read as something you can click.
|
|
1039
|
+
* Matches the outlined pill convention the plugin's other secondary actions use.
|
|
1040
|
+
*/
|
|
1041
|
+
const noteDismissStyle = {
|
|
1042
|
+
padding: "2px 8px",
|
|
1043
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1044
|
+
borderRadius: 6,
|
|
1045
|
+
background: "transparent",
|
|
1046
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1047
|
+
font: "inherit",
|
|
1048
|
+
fontSize: 12,
|
|
1049
|
+
lineHeight: "18px",
|
|
1050
|
+
cursor: "pointer"
|
|
1051
|
+
};
|
|
1052
|
+
/**
|
|
1053
|
+
* The feature's static inline label. Deliberately not a state readout — see the
|
|
1054
|
+
* module comment.
|
|
1055
|
+
*/
|
|
1056
|
+
function useLabel(t) {
|
|
1057
|
+
return t("probeLabel");
|
|
901
1058
|
}
|
|
902
|
-
/**
|
|
903
|
-
function
|
|
904
|
-
if (
|
|
905
|
-
|
|
906
|
-
|
|
1059
|
+
/** Pick the model's recorded observation out of the probe section. */
|
|
1060
|
+
function resultFor(status, model) {
|
|
1061
|
+
if (status.status !== "signed-in") return void 0;
|
|
1062
|
+
return status.probe?.results.find((result) => result.id === model);
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* The one-line tooltip: current state first, then what a click does — the same
|
|
1066
|
+
* two-part shape Fast Mode uses.
|
|
1067
|
+
*/
|
|
1068
|
+
function tooltipText(t, model, state) {
|
|
1069
|
+
if (state.busy) return t("probeRunning", { model });
|
|
1070
|
+
if (state.failed) return t("probeTooltipRetry");
|
|
1071
|
+
const result = state.result;
|
|
1072
|
+
if (result === void 0) return t("probeTooltipIdle", { model });
|
|
1073
|
+
if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
|
|
1074
|
+
if (result.validation === "non-validating") return t("probeTooltipNotValidating");
|
|
1075
|
+
return t("probeTooltipRetry");
|
|
1076
|
+
}
|
|
1077
|
+
/** Model-independent shell: resolves the selection, then delegates per model. */
|
|
1078
|
+
function WorkBuddyProbeControl({ directory, t }) {
|
|
1079
|
+
const subscribe = (0, react.useCallback)((listener) => directory.subscribe(listener), [directory]);
|
|
1080
|
+
const snapshot = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
|
|
1081
|
+
const selection = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).current;
|
|
1082
|
+
const card = selection === void 0 ? void 0 : cardVariantFor(selection.provider);
|
|
1083
|
+
const key = card === void 0 || selection === void 0 ? void 0 : `${card.id}:${selection.model}`;
|
|
1084
|
+
return card === void 0 || selection === void 0 || key === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelProbe, {
|
|
1085
|
+
model: selection.model,
|
|
1086
|
+
card,
|
|
1087
|
+
label: useLabel(t),
|
|
1088
|
+
t
|
|
1089
|
+
}, key);
|
|
1090
|
+
}
|
|
1091
|
+
function ModelProbe({ model, card, label, t }) {
|
|
1092
|
+
const [status, setStatus] = (0, react.useState)();
|
|
907
1093
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
908
|
-
const [
|
|
909
|
-
const
|
|
910
|
-
(0, react.
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}, []);
|
|
1094
|
+
const [confirming, setConfirming] = (0, react.useState)(false);
|
|
1095
|
+
const [tooltipVisible, setTooltipVisible] = (0, react.useState)(false);
|
|
1096
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
1097
|
+
const [note, setNote] = (0, react.useState)();
|
|
1098
|
+
const inFlight = (0, react.useRef)(false);
|
|
1099
|
+
const mounted = (0, react.useRef)(false);
|
|
1100
|
+
const tooltipId = (0, react.useId)();
|
|
916
1101
|
const refresh = (0, react.useCallback)(async (signal) => {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
} catch (error) {
|
|
927
|
-
if (mounted.current && signal?.aborted !== true) setStatus({
|
|
928
|
-
status: "error",
|
|
929
|
-
message: error instanceof Error ? error.message : t("requestFailed")
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
}, [t]);
|
|
1102
|
+
const response = await fetch(card.statusPath, {
|
|
1103
|
+
credentials: "same-origin",
|
|
1104
|
+
headers: { accept: "application/json" },
|
|
1105
|
+
...signal === void 0 ? {} : { signal }
|
|
1106
|
+
});
|
|
1107
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1108
|
+
const value = await response.json();
|
|
1109
|
+
if (mounted.current && !signal?.aborted) setStatus(value);
|
|
1110
|
+
}, [card.statusPath]);
|
|
933
1111
|
(0, react.useEffect)(() => {
|
|
934
|
-
|
|
1112
|
+
mounted.current = true;
|
|
935
1113
|
const controller = new AbortController();
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
controller.abort();
|
|
1114
|
+
const load = () => {
|
|
1115
|
+
refresh(controller.signal).catch(() => {});
|
|
939
1116
|
};
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const controller = new AbortController();
|
|
944
|
-
const timer = window.setInterval(() => {
|
|
945
|
-
refresh(controller.signal);
|
|
946
|
-
}, POLL_INTERVAL_MS);
|
|
1117
|
+
load();
|
|
1118
|
+
const timer = window.setInterval(load, RECONCILE_MS);
|
|
1119
|
+
window.addEventListener("focus", load);
|
|
947
1120
|
return () => {
|
|
948
|
-
|
|
1121
|
+
mounted.current = false;
|
|
949
1122
|
controller.abort();
|
|
1123
|
+
window.clearInterval(timer);
|
|
1124
|
+
window.removeEventListener("focus", load);
|
|
950
1125
|
};
|
|
951
|
-
}, [
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
* Run one control action and refresh the card's state afterwards.
|
|
966
|
-
*
|
|
967
|
-
* The key travels in a header, not the body: it authorizes the write, and
|
|
968
|
-
* the host never accepts a prompt, a sentinel, or a model outside its own
|
|
969
|
-
* catalog from here.
|
|
970
|
-
*/
|
|
971
|
-
const control = (0, react.useCallback)(async (action) => {
|
|
972
|
-
const key = status.status === "signed-in" ? status.probeKey : void 0;
|
|
973
|
-
if (key === void 0) return;
|
|
1126
|
+
}, [refresh]);
|
|
1127
|
+
const probe = status?.status === "signed-in" ? status.probe : void 0;
|
|
1128
|
+
const key = status?.status === "signed-in" ? status.probeKey : void 0;
|
|
1129
|
+
const result = status === void 0 ? void 0 : resultFor(status, model);
|
|
1130
|
+
const visible = probe?.candidates.includes(model) === true || result !== void 0;
|
|
1131
|
+
(0, react.useEffect)(() => {
|
|
1132
|
+
setConfirming(false);
|
|
1133
|
+
setNote(void 0);
|
|
1134
|
+
}, [model]);
|
|
1135
|
+
const detect = async () => {
|
|
1136
|
+
if (key === void 0 || inFlight.current || probe?.running === true) return;
|
|
1137
|
+
inFlight.current = true;
|
|
1138
|
+
setNote(void 0);
|
|
1139
|
+
setConfirming(false);
|
|
974
1140
|
setBusy(true);
|
|
1141
|
+
setFailed(false);
|
|
975
1142
|
try {
|
|
976
|
-
const response = await fetch(
|
|
1143
|
+
const response = await fetch(card.probePath, {
|
|
977
1144
|
method: "POST",
|
|
1145
|
+
credentials: "same-origin",
|
|
978
1146
|
headers: {
|
|
979
1147
|
"Content-Type": "application/json",
|
|
980
1148
|
"X-WorkBuddy-Probe-Key": key
|
|
981
1149
|
},
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1150
|
+
body: JSON.stringify({
|
|
1151
|
+
action: "probe",
|
|
1152
|
+
model
|
|
1153
|
+
})
|
|
1154
|
+
});
|
|
1155
|
+
const body = await response.json();
|
|
1156
|
+
if (!response.ok || body.state !== "ok" || body.validation !== "validating" && body.validation !== "non-validating" || !Array.isArray(body.efforts) || !body.efforts.every((effort) => typeof effort === "string")) throw new Error("probe failed");
|
|
1157
|
+
if (mounted.current) {
|
|
1158
|
+
const completed = {
|
|
1159
|
+
id: model,
|
|
1160
|
+
name: model,
|
|
1161
|
+
validation: body.validation,
|
|
1162
|
+
efforts: body.efforts,
|
|
1163
|
+
probedAt: Date.now()
|
|
1164
|
+
};
|
|
1165
|
+
setNote(completed);
|
|
1166
|
+
}
|
|
1167
|
+
refresh().catch(() => {});
|
|
1168
|
+
} catch {
|
|
1169
|
+
if (mounted.current) setFailed(true);
|
|
1170
|
+
} finally {
|
|
1171
|
+
inFlight.current = false;
|
|
1172
|
+
if (mounted.current) setBusy(false);
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
if (!visible) return null;
|
|
1176
|
+
const text = tooltipText(t, model, {
|
|
1177
|
+
busy,
|
|
1178
|
+
result,
|
|
1179
|
+
failed
|
|
1180
|
+
});
|
|
1181
|
+
const disabled = busy || probe?.running === true || key === void 0;
|
|
1182
|
+
const showTooltip = tooltipVisible && !confirming && note === void 0;
|
|
1183
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1184
|
+
style: wrapperStyle,
|
|
1185
|
+
onMouseEnter: () => {
|
|
1186
|
+
setTooltipVisible(true);
|
|
1187
|
+
},
|
|
1188
|
+
onMouseLeave: () => {
|
|
1189
|
+
setTooltipVisible(false);
|
|
1190
|
+
},
|
|
1191
|
+
children: [
|
|
1192
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1193
|
+
type: "button",
|
|
1194
|
+
"aria-label": text,
|
|
1195
|
+
"aria-describedby": showTooltip ? tooltipId : void 0,
|
|
1196
|
+
"aria-busy": busy,
|
|
1197
|
+
"aria-expanded": confirming,
|
|
1198
|
+
disabled,
|
|
1199
|
+
onClick: () => {
|
|
1200
|
+
setConfirming(true);
|
|
1201
|
+
},
|
|
1202
|
+
onFocus: () => {
|
|
1203
|
+
setTooltipVisible(true);
|
|
1204
|
+
},
|
|
1205
|
+
onBlur: () => {
|
|
1206
|
+
setTooltipVisible(false);
|
|
1207
|
+
},
|
|
1037
1208
|
style: {
|
|
1038
|
-
...
|
|
1039
|
-
|
|
1209
|
+
...buttonStyle,
|
|
1210
|
+
opacity: disabled && !confirming ? .6 : 1,
|
|
1211
|
+
cursor: disabled ? "default" : "pointer"
|
|
1040
1212
|
},
|
|
1041
|
-
children: "
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1213
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1214
|
+
width: "16",
|
|
1215
|
+
height: "16",
|
|
1216
|
+
viewBox: "0 0 24 24",
|
|
1217
|
+
fill: "none",
|
|
1218
|
+
stroke: "currentColor",
|
|
1219
|
+
strokeWidth: "1.6",
|
|
1220
|
+
"aria-hidden": "true",
|
|
1221
|
+
focusable: "false",
|
|
1222
|
+
children: [
|
|
1223
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1224
|
+
cx: "12",
|
|
1225
|
+
cy: "12",
|
|
1226
|
+
r: "9"
|
|
1227
|
+
}),
|
|
1228
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1229
|
+
cx: "12",
|
|
1230
|
+
cy: "12",
|
|
1231
|
+
r: "4"
|
|
1232
|
+
}),
|
|
1233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 12 20 4" }),
|
|
1234
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1235
|
+
cx: "12",
|
|
1236
|
+
cy: "12",
|
|
1237
|
+
r: "1"
|
|
1238
|
+
})
|
|
1239
|
+
]
|
|
1240
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1241
|
+
style: labelStyle,
|
|
1242
|
+
children: label
|
|
1243
|
+
})]
|
|
1244
|
+
}),
|
|
1245
|
+
showTooltip && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1246
|
+
id: tooltipId,
|
|
1247
|
+
role: "tooltip",
|
|
1248
|
+
style: tooltipStyle,
|
|
1249
|
+
children: text
|
|
1250
|
+
}),
|
|
1251
|
+
confirming && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1252
|
+
style: confirmStyle,
|
|
1253
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("probeBubbleBody") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1254
|
+
style: confirmRowStyle,
|
|
1255
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1256
|
+
type: "button",
|
|
1257
|
+
style: confirmButtonStyle,
|
|
1258
|
+
onClick: () => {
|
|
1259
|
+
setConfirming(false);
|
|
1260
|
+
},
|
|
1261
|
+
children: t("cancel")
|
|
1059
1262
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1060
1263
|
type: "button",
|
|
1061
|
-
style:
|
|
1062
|
-
disabled: busy,
|
|
1264
|
+
style: primaryButtonStyle,
|
|
1063
1265
|
onClick: () => {
|
|
1064
|
-
|
|
1266
|
+
detect();
|
|
1065
1267
|
},
|
|
1066
|
-
children:
|
|
1268
|
+
children: t("probeConfirmAction")
|
|
1067
1269
|
})]
|
|
1068
|
-
})
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
"aria-selected": tab === id,
|
|
1085
|
-
onClick: () => {
|
|
1086
|
-
setTab(id);
|
|
1087
|
-
},
|
|
1088
|
-
style: {
|
|
1089
|
-
...tabStyle,
|
|
1090
|
-
...tab === id ? tabActiveStyle : {}
|
|
1091
|
-
},
|
|
1092
|
-
children: t(id === "status" ? "tabStatus" : id === "context" ? "tabContext" : "tabDetails")
|
|
1093
|
-
}, id))
|
|
1094
|
-
}),
|
|
1095
|
-
tab === "status" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1096
|
-
style: tabPanelStyle,
|
|
1097
|
-
children: [
|
|
1098
|
-
status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1099
|
-
style: quotaListStyle,
|
|
1100
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1101
|
-
style: rowStyle,
|
|
1102
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1103
|
-
style: quotaTitleStyle,
|
|
1104
|
-
children: t("creditsHeading")
|
|
1105
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1106
|
-
style: bodyStyle,
|
|
1107
|
-
children: t("creditsTotal", { total: formatNumber(status.credits.total) })
|
|
1108
|
-
})]
|
|
1109
|
-
})
|
|
1110
|
-
}),
|
|
1111
|
-
status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1112
|
-
style: errorStyle,
|
|
1113
|
-
children: t("creditsError", { message: status.creditsError })
|
|
1114
|
-
}),
|
|
1115
|
-
status.probe === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeSection, {
|
|
1116
|
-
probe: status.probe,
|
|
1117
|
-
t,
|
|
1118
|
-
busy,
|
|
1119
|
-
onDetect: confirmDetect,
|
|
1120
|
-
onClear: () => {
|
|
1121
|
-
control({ action: "clear" });
|
|
1122
|
-
}
|
|
1123
|
-
})
|
|
1124
|
-
]
|
|
1125
|
-
}) : tab === "context" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1126
|
-
style: tabPanelStyle,
|
|
1127
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextTable, {
|
|
1128
|
-
models: status.models,
|
|
1129
|
-
t
|
|
1130
|
-
})
|
|
1131
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1132
|
-
style: tabPanelStyle,
|
|
1133
|
-
children: [status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1134
|
-
style: quotaListStyle,
|
|
1135
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1136
|
-
style: quotaTitleStyle,
|
|
1137
|
-
children: t("creditsDetailHeading")
|
|
1138
|
-
}), status.credits.accounts.filter((account) => account.remain > 0).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
|
|
1139
|
-
label: account.packageName,
|
|
1140
|
-
remain: account.remain,
|
|
1141
|
-
size: account.size,
|
|
1142
|
-
t
|
|
1143
|
-
}, `${account.packageName}-${String(index)}`))]
|
|
1144
|
-
}), status.models === void 0 || status.models.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1145
|
-
style: quotaListStyle,
|
|
1146
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1147
|
-
style: quotaTitleStyle,
|
|
1148
|
-
children: t("modelsHeading")
|
|
1149
|
-
}), status.models.filter((model) => model.free === true || (model.badges?.length ?? 0) > 0).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelOfferRow, {
|
|
1150
|
-
model,
|
|
1151
|
-
t
|
|
1152
|
-
}, model.id))]
|
|
1153
|
-
})]
|
|
1154
|
-
})
|
|
1155
|
-
] }) : null,
|
|
1156
|
-
status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1157
|
-
style: bodyStyle,
|
|
1158
|
-
children: t("signedOutHint")
|
|
1159
|
-
}) : null,
|
|
1160
|
-
status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1161
|
-
style: errorStyle,
|
|
1162
|
-
children: status.message
|
|
1163
|
-
}) : null
|
|
1164
|
-
]
|
|
1165
|
-
}) : null]
|
|
1270
|
+
})]
|
|
1271
|
+
}),
|
|
1272
|
+
note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1273
|
+
role: "status",
|
|
1274
|
+
"aria-live": "polite",
|
|
1275
|
+
style: noteStyle,
|
|
1276
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: noteText(t, note) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1277
|
+
type: "button",
|
|
1278
|
+
style: noteDismissStyle,
|
|
1279
|
+
onClick: () => {
|
|
1280
|
+
setNote(void 0);
|
|
1281
|
+
},
|
|
1282
|
+
children: t("probeNoteDismiss")
|
|
1283
|
+
})]
|
|
1284
|
+
})
|
|
1285
|
+
]
|
|
1166
1286
|
});
|
|
1167
1287
|
}
|
|
1288
|
+
/** Compose the one-line outcome string the note bubble shows. */
|
|
1289
|
+
function noteText(t, result) {
|
|
1290
|
+
if (result.validation === "validating" && result.efforts.length > 0) return t("probeNoteVerified", { levels: result.efforts.join(" / ") });
|
|
1291
|
+
if (result.validation === "non-validating") return t("probeNoteNotValidating");
|
|
1292
|
+
return t("probeNoteUnknown");
|
|
1293
|
+
}
|
|
1168
1294
|
//#endregion
|
|
1169
1295
|
//#region src/client/locales.ts
|
|
1170
1296
|
/** Plugin-card copy registered under the settings.workbuddy locale namespace. */
|
|
1171
1297
|
const en = {
|
|
1172
1298
|
title: "DSH WorkBuddy Connect",
|
|
1173
1299
|
intro: "Use the models in the WorkBuddy desktop app directly in DSH — zero configuration, ready out of the box.",
|
|
1300
|
+
titleAI: "DSH WorkBuddy AI Connect",
|
|
1301
|
+
introAI: "Use the models in the WorkBuddy AI international desktop app directly in DSH — zero configuration, ready out of the box.",
|
|
1174
1302
|
expand: "Expand",
|
|
1175
1303
|
collapse: "Collapse",
|
|
1176
1304
|
loading: "Loading account…",
|
|
1177
1305
|
signedOut: "Not signed in",
|
|
1178
1306
|
signedOutHint: "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
|
|
1307
|
+
signedOutHintAI: "Sign in once in the WorkBuddy AI desktop app; this plugin follows that sign-in automatically.",
|
|
1179
1308
|
signedInAs: "Signed in as {nickname}",
|
|
1180
1309
|
accessTokenExpires: "Access token expires {time} (refresh is automatic)",
|
|
1181
1310
|
creditsHeading: "Remaining credit",
|
|
@@ -1190,14 +1319,24 @@ window.__ModuleLoader__.load({
|
|
|
1190
1319
|
creditsError: "Credit unavailable: {message}",
|
|
1191
1320
|
refresh: "Refresh",
|
|
1192
1321
|
refreshing: "Refreshing…",
|
|
1322
|
+
refreshModels: "Refresh model list",
|
|
1323
|
+
refreshingModels: "Refreshing models…",
|
|
1324
|
+
catalogLive: "Model list updated {time}",
|
|
1325
|
+
catalogSaved: "Showing the saved model list from {time}",
|
|
1326
|
+
catalogFallback: "Showing the built-in model list (not yet updated from WorkBuddy)",
|
|
1327
|
+
catalogError: "Last update failed: {message}",
|
|
1328
|
+
catalogAppVersion: "App version {version}",
|
|
1193
1329
|
requestFailed: "Request failed",
|
|
1194
1330
|
accountHeading: "Account",
|
|
1195
1331
|
modelsHeading: "Model offers",
|
|
1196
1332
|
contextHeading: "Context window",
|
|
1333
|
+
contextUpTo: "up to {size}",
|
|
1197
1334
|
freeModel: "Free",
|
|
1198
1335
|
badgeLimitedFree: "Limited-time free",
|
|
1199
1336
|
badgeNightDiscount: "Night discount",
|
|
1337
|
+
badgeFreeNow: "Free now",
|
|
1200
1338
|
rate: "{rate} credits per message",
|
|
1339
|
+
rateUnknown: "Price unavailable — refresh to update",
|
|
1201
1340
|
probeLabel: "Reasoning levels",
|
|
1202
1341
|
probeTooltipIdle: "Detect the reasoning levels {model} accepts",
|
|
1203
1342
|
probeTooltipVerified: "Accepted levels: {levels} · click to detect again",
|
|
@@ -1212,7 +1351,6 @@ window.__ModuleLoader__.load({
|
|
|
1212
1351
|
probeHeading: "Reasoning effort detection",
|
|
1213
1352
|
probeResultNoLevels: "No tested levels were accepted.",
|
|
1214
1353
|
probeIntro: "Some models reason but declare no selectable effort levels. Detecting which levels a model accepts sends a few real requests that may consume credit.",
|
|
1215
|
-
probeConsent: "Authorize detection",
|
|
1216
1354
|
probeConsentHint: "Each detection sends test requests to one model to confirm its available reasoning levels, and may consume a small amount of credit.",
|
|
1217
1355
|
probeStart: "Detect",
|
|
1218
1356
|
probeRedetect: "Detect again",
|
|
@@ -1232,11 +1370,14 @@ window.__ModuleLoader__.load({
|
|
|
1232
1370
|
const zh = {
|
|
1233
1371
|
title: "DSH WorkBuddy Connect",
|
|
1234
1372
|
intro: "在 DSH 中直接使用 WorkBuddy 桌面 App 包含的模型,开箱即用,无需额外配置。",
|
|
1373
|
+
titleAI: "DSH WorkBuddy AI Connect",
|
|
1374
|
+
introAI: "在 DSH 中直接使用 WorkBuddy AI 国际版桌面 App 包含的模型,开箱即用,无需额外配置。",
|
|
1235
1375
|
expand: "展开",
|
|
1236
1376
|
collapse: "收起",
|
|
1237
1377
|
loading: "正在读取账号…",
|
|
1238
1378
|
signedOut: "未登录",
|
|
1239
1379
|
signedOutHint: "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
|
|
1380
|
+
signedOutHintAI: "在 WorkBuddy AI 国际版桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
|
|
1240
1381
|
signedInAs: "已登录:{nickname}",
|
|
1241
1382
|
accessTokenExpires: "访问令牌 {time} 过期(自动续期)",
|
|
1242
1383
|
creditsHeading: "剩余积分",
|
|
@@ -1251,14 +1392,24 @@ window.__ModuleLoader__.load({
|
|
|
1251
1392
|
creditsError: "积分查询失败:{message}",
|
|
1252
1393
|
refresh: "刷新",
|
|
1253
1394
|
refreshing: "正在刷新…",
|
|
1395
|
+
refreshModels: "刷新模型列表",
|
|
1396
|
+
refreshingModels: "正在刷新模型…",
|
|
1397
|
+
catalogLive: "模型列表更新于 {time}",
|
|
1398
|
+
catalogSaved: "当前显示已保存的模型列表,更新于 {time}",
|
|
1399
|
+
catalogFallback: "当前显示内置模型列表(尚未从 WorkBuddy 更新)",
|
|
1400
|
+
catalogError: "上次更新失败:{message}",
|
|
1401
|
+
catalogAppVersion: "App 版本 {version}",
|
|
1254
1402
|
requestFailed: "请求失败",
|
|
1255
1403
|
accountHeading: "账号",
|
|
1256
1404
|
modelsHeading: "模型优惠",
|
|
1257
1405
|
contextHeading: "上下文窗口",
|
|
1406
|
+
contextUpTo: "最高 {size}",
|
|
1258
1407
|
freeModel: "免费",
|
|
1259
1408
|
badgeLimitedFree: "限时免费",
|
|
1260
1409
|
badgeNightDiscount: "夜间折扣",
|
|
1410
|
+
badgeFreeNow: "限时免费",
|
|
1261
1411
|
rate: "{rate} 积分/次",
|
|
1412
|
+
rateUnknown: "价格未知 — 刷新后更新",
|
|
1262
1413
|
probeLabel: "推理等级",
|
|
1263
1414
|
probeTooltipIdle: "检测 {model} 可用的推理档位",
|
|
1264
1415
|
probeTooltipVerified: "已接受:{levels} · 点击可重新检测",
|
|
@@ -1273,7 +1424,6 @@ window.__ModuleLoader__.load({
|
|
|
1273
1424
|
probeHeading: "推理档位检测",
|
|
1274
1425
|
probeResultNoLevels: "本次测试的档位均未被接受。",
|
|
1275
1426
|
probeIntro: "部分模型具备思考能力,但没有声明可选档位。检测会发送少量真实请求,可能消耗积分。",
|
|
1276
|
-
probeConsent: "授权检测",
|
|
1277
1427
|
probeConsentHint: "每次检测会向该模型发送探测请求,以确认可用推理档位,可能消耗少量积分。",
|
|
1278
1428
|
probeStart: "开始检测",
|
|
1279
1429
|
probeRedetect: "重新检测",
|
|
@@ -1306,7 +1456,12 @@ window.__ModuleLoader__.load({
|
|
|
1306
1456
|
* named in the package's `dsh.client.inject` list, so cordis has activated
|
|
1307
1457
|
* them before this plugin's fiber starts.
|
|
1308
1458
|
*/
|
|
1309
|
-
const inject = [
|
|
1459
|
+
const inject = [
|
|
1460
|
+
"slots",
|
|
1461
|
+
"locale",
|
|
1462
|
+
"remote",
|
|
1463
|
+
"remote.session"
|
|
1464
|
+
];
|
|
1310
1465
|
/**
|
|
1311
1466
|
* Register card copy and the WorkBuddy card under Plugin configuration.
|
|
1312
1467
|
*
|
|
@@ -1333,11 +1488,14 @@ window.__ModuleLoader__.load({
|
|
|
1333
1488
|
en
|
|
1334
1489
|
}), "dsh-workbuddy-connect: settings copy");
|
|
1335
1490
|
const t = ctx.locale.bind(namespace);
|
|
1336
|
-
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
1491
|
+
for (const [index, variant] of CARD_VARIANTS.entries()) ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
1337
1492
|
name: "settings.plugin.item",
|
|
1338
|
-
key:
|
|
1339
|
-
priority: 30,
|
|
1340
|
-
inject: () => ({
|
|
1493
|
+
key: variant.id,
|
|
1494
|
+
priority: 30 - index,
|
|
1495
|
+
inject: () => ({
|
|
1496
|
+
t,
|
|
1497
|
+
variant
|
|
1498
|
+
})
|
|
1341
1499
|
}, WorkBuddyPluginCard));
|
|
1342
1500
|
ctx.inject(["modelDirectories"], (scope) => {
|
|
1343
1501
|
scope.slots.inject("conversation.input.right", () => scope.slots.register({
|