dsh-coding-subscription-oauth 0.5.4 → 0.5.6
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/CHANGELOG.md +18 -0
- package/INSTALL.md +4 -4
- package/README.de.md +4 -4
- package/README.es.md +4 -4
- package/README.fr.md +4 -4
- package/README.ja.md +4 -4
- package/README.ko.md +4 -4
- package/README.md +5 -5
- package/README.pt-BR.md +4 -4
- package/README.ru.md +4 -4
- package/README.zh-CN.md +5 -5
- package/docs/02-architecture.md +1 -1
- package/docs/02-architecture.zh-CN.md +1 -1
- package/lib/client.js +34 -1
- package/lib/client.js.map +4 -4
- package/media/settings_accounts.png +0 -0
- package/media/settings_capabilities.png +0 -0
- package/media/settings_gateway.png +0 -0
- package/package.json +1 -1
- package/src/client/GrokBuildSettings.tsx +24 -2
- package/src/client/components/AccountsTab.tsx +29 -29
- package/src/client/components/Badge.tsx +33 -0
- package/src/client/components/CapabilitiesTab.tsx +45 -32
- package/src/client/components/CopyButton.tsx +57 -0
- package/src/client/components/GatewayTab.tsx +86 -9
- package/src/client/components/NoticeBanner.tsx +46 -0
- package/src/client/components/ProgressBar.tsx +52 -0
- package/src/client/components/ProviderCard.tsx +244 -64
- package/src/client/components/SettingsTabs.tsx +16 -6
- package/src/client/components/ToggleSwitch.tsx +60 -0
- package/src/client/constants.ts +1 -1
- package/src/client/gatewaySnippets.ts +36 -0
- package/src/client/locales.ts +34 -0
- package/src/client/microStyles.ts +33 -0
- package/src/client/styles.ts +136 -10
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Single provider account card for the Accounts tab. */
|
|
2
2
|
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
3
4
|
import { SOURCE_REASON_KEY } from "../constants.ts";
|
|
4
5
|
import { methodLabel, orderedLoginMethods, shouldShowPerCardSourceReason } from "../display.ts";
|
|
5
6
|
import { formatEpoch, modelFields, usageHasVisibleFields } from "../parsers.ts";
|
|
@@ -9,17 +10,18 @@ import {
|
|
|
9
10
|
cardStyle,
|
|
10
11
|
checkRowStyle,
|
|
11
12
|
codeStyle,
|
|
12
|
-
|
|
13
|
-
errorStyle,
|
|
13
|
+
compactButtonStyle,
|
|
14
14
|
hintStyle,
|
|
15
15
|
inputStyle,
|
|
16
|
-
linkStyle,
|
|
17
16
|
listStyle,
|
|
18
17
|
monoStyle,
|
|
19
18
|
nestedStyle,
|
|
20
19
|
primaryButtonStyle,
|
|
21
20
|
rowStyle,
|
|
22
|
-
|
|
21
|
+
stepActiveStyle,
|
|
22
|
+
stepNumberActiveStyle,
|
|
23
|
+
stepNumberStyle,
|
|
24
|
+
stepRowStyle,
|
|
23
25
|
titleStyle,
|
|
24
26
|
} from "../styles.ts";
|
|
25
27
|
import type {
|
|
@@ -31,6 +33,9 @@ import type {
|
|
|
31
33
|
SourceStatus,
|
|
32
34
|
UsageView,
|
|
33
35
|
} from "../types.ts";
|
|
36
|
+
import { Badge } from "./Badge.tsx";
|
|
37
|
+
import { CopyButton } from "./CopyButton.tsx";
|
|
38
|
+
import { ProgressBar } from "./ProgressBar.tsx";
|
|
34
39
|
|
|
35
40
|
export interface ProviderCardProps {
|
|
36
41
|
t: GrokBuildSettingsInjected["t"];
|
|
@@ -57,6 +62,88 @@ export interface ProviderCardProps {
|
|
|
57
62
|
onSaveModels: (selected: string[]) => void;
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
function SignInSteps({
|
|
66
|
+
t,
|
|
67
|
+
activeMethod,
|
|
68
|
+
userCode,
|
|
69
|
+
url,
|
|
70
|
+
popupBlocked,
|
|
71
|
+
}: {
|
|
72
|
+
t: GrokBuildSettingsInjected["t"];
|
|
73
|
+
activeMethod: LoginMethod;
|
|
74
|
+
userCode: string | undefined;
|
|
75
|
+
url: string | undefined;
|
|
76
|
+
popupBlocked: boolean;
|
|
77
|
+
}) {
|
|
78
|
+
const hasCode = userCode !== undefined && userCode.length > 0;
|
|
79
|
+
const hasUrl = url !== undefined && url.length > 0;
|
|
80
|
+
const needsPaste = activeMethod !== "device";
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
84
|
+
<div style={hasUrl ? stepActiveStyle : stepRowStyle}>
|
|
85
|
+
<span style={hasUrl ? stepNumberActiveStyle : stepNumberStyle} aria-hidden="true">
|
|
86
|
+
1
|
|
87
|
+
</span>
|
|
88
|
+
<span>{t("signInStepOpen")}</span>
|
|
89
|
+
</div>
|
|
90
|
+
{hasUrl ? (
|
|
91
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, paddingLeft: 32 }}>
|
|
92
|
+
<a href={url} target="_blank" rel="noreferrer" style={primaryButtonStyle}>
|
|
93
|
+
{t("openAuthUrl")}
|
|
94
|
+
</a>
|
|
95
|
+
<CopyButton text={url} idleLabel={t("copy")} copiedLabel={t("copied")} failedLabel={t("copyFailed")} />
|
|
96
|
+
</div>
|
|
97
|
+
) : null}
|
|
98
|
+
{popupBlocked && hasUrl ? <p style={hintStyle}>{t("popupBlocked")}</p> : null}
|
|
99
|
+
{hasCode ? (
|
|
100
|
+
<>
|
|
101
|
+
<div style={stepActiveStyle}>
|
|
102
|
+
<span style={stepNumberActiveStyle} aria-hidden="true">
|
|
103
|
+
2
|
|
104
|
+
</span>
|
|
105
|
+
<span>{t("signInStepCode")}</span>
|
|
106
|
+
</div>
|
|
107
|
+
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8, paddingLeft: 32 }}>
|
|
108
|
+
<span style={codeStyle}>{userCode}</span>
|
|
109
|
+
<CopyButton
|
|
110
|
+
text={userCode}
|
|
111
|
+
idleLabel={t("copyUserCode")}
|
|
112
|
+
copiedLabel={t("copied")}
|
|
113
|
+
failedLabel={t("copyFailed")}
|
|
114
|
+
primary
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
</>
|
|
118
|
+
) : null}
|
|
119
|
+
<div style={stepActiveStyle}>
|
|
120
|
+
<span style={stepNumberActiveStyle} aria-hidden="true">
|
|
121
|
+
{hasCode ? 3 : 2}
|
|
122
|
+
</span>
|
|
123
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
|
|
124
|
+
<span
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
style={{
|
|
127
|
+
width: 14,
|
|
128
|
+
height: 14,
|
|
129
|
+
border: "2px solid var(--dsw-alias-brand-primary, #1677ff)",
|
|
130
|
+
borderTopColor: "transparent",
|
|
131
|
+
borderRadius: "50%",
|
|
132
|
+
animation: "dsh-coding-oauth-spin 0.8s linear infinite",
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
{t("signInStepWait")}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
{needsPaste ? (
|
|
139
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8, paddingLeft: 32 }}>
|
|
140
|
+
<p style={bodyStyle}>{t(activeMethod === "browser" ? "pasteBrowserCodeHint" : "pasteCodeHint")}</p>
|
|
141
|
+
</div>
|
|
142
|
+
) : null}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
60
147
|
export function ProviderCard({
|
|
61
148
|
t,
|
|
62
149
|
definition,
|
|
@@ -81,7 +168,13 @@ export function ProviderCard({
|
|
|
81
168
|
onPreviewSource,
|
|
82
169
|
onSaveModels,
|
|
83
170
|
}: ProviderCardProps) {
|
|
171
|
+
const [showAltMethods, setShowAltMethods] = useState(false);
|
|
172
|
+
const [modelFilter, setModelFilter] = useState("");
|
|
173
|
+
|
|
84
174
|
const ordered = orderedLoginMethods(definition, remote);
|
|
175
|
+
const primaryMethod: LoginMethod = ordered[0] ?? definition.recommended;
|
|
176
|
+
const altMethods = ordered.filter((method) => method !== primaryMethod);
|
|
177
|
+
|
|
85
178
|
const statusLabel =
|
|
86
179
|
providerStatus.status === "signed-in"
|
|
87
180
|
? t("signedIn")
|
|
@@ -90,10 +183,17 @@ export function ProviderCard({
|
|
|
90
183
|
: providerStatus.status === "error"
|
|
91
184
|
? t("requestFailed")
|
|
92
185
|
: t("signedOut");
|
|
93
|
-
const activeMethod = providerStatus.status === "signing-in" ? providerStatus.method :
|
|
186
|
+
const activeMethod = providerStatus.status === "signing-in" ? providerStatus.method : primaryMethod;
|
|
94
187
|
const { available, selected } = modelFields(providerStatus);
|
|
95
188
|
const grokProviderStatus = definition.slug === "grok" ? (providerStatus as GrokStatus) : undefined;
|
|
96
189
|
const showSourceReason = shouldShowPerCardSourceReason(source);
|
|
190
|
+
|
|
191
|
+
const filteredModels = useMemo(() => {
|
|
192
|
+
const query = modelFilter.trim().toLowerCase();
|
|
193
|
+
if (query.length === 0) return available;
|
|
194
|
+
return available.filter((id) => id.toLowerCase().includes(query));
|
|
195
|
+
}, [available, modelFilter]);
|
|
196
|
+
|
|
97
197
|
const usagePercent =
|
|
98
198
|
definition.slug === "codex" && showUsage
|
|
99
199
|
? usage?.individualRemainingPercent === undefined
|
|
@@ -121,10 +221,7 @@ export function ProviderCard({
|
|
|
121
221
|
</>
|
|
122
222
|
)}
|
|
123
223
|
</div>
|
|
124
|
-
<
|
|
125
|
-
<span aria-hidden="true" style={dotStyle(providerStatus.status)} />
|
|
126
|
-
<span>{statusLabel}</span>
|
|
127
|
-
</div>
|
|
224
|
+
<Badge label={statusLabel} providerStatus={providerStatus.status} />
|
|
128
225
|
</div>
|
|
129
226
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
|
|
130
227
|
{providerStatus.status === "signed-in" ? (
|
|
@@ -143,40 +240,76 @@ export function ProviderCard({
|
|
|
143
240
|
</>
|
|
144
241
|
) : providerStatus.status === "signing-in" ? (
|
|
145
242
|
<>
|
|
146
|
-
{ordered
|
|
147
|
-
.filter((method) => method !== activeMethod)
|
|
148
|
-
.map((method) => (
|
|
149
|
-
<button
|
|
150
|
-
key={method}
|
|
151
|
-
type="button"
|
|
152
|
-
style={buttonStyle}
|
|
153
|
-
disabled={busy}
|
|
154
|
-
onClick={() => {
|
|
155
|
-
onSignIn(method);
|
|
156
|
-
}}
|
|
157
|
-
>
|
|
158
|
-
{methodLabel(method, t, { remote, primary: method === ordered[0] })}
|
|
159
|
-
</button>
|
|
160
|
-
))}
|
|
161
243
|
<button type="button" style={buttonStyle} disabled={busy} onClick={onCancelLogin}>
|
|
162
244
|
{t("cancelLogin")}
|
|
163
245
|
</button>
|
|
246
|
+
{altMethods.length > 0 ? (
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
style={compactButtonStyle}
|
|
250
|
+
disabled={busy}
|
|
251
|
+
onClick={() => {
|
|
252
|
+
setShowAltMethods((current) => !current);
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
{showAltMethods ? t("hideOtherLoginMethods") : t("otherLoginMethods")}
|
|
256
|
+
</button>
|
|
257
|
+
) : null}
|
|
258
|
+
{showAltMethods
|
|
259
|
+
? altMethods.map((method) => (
|
|
260
|
+
<button
|
|
261
|
+
key={method}
|
|
262
|
+
type="button"
|
|
263
|
+
style={compactButtonStyle}
|
|
264
|
+
disabled={busy}
|
|
265
|
+
onClick={() => {
|
|
266
|
+
onSignIn(method);
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
{methodLabel(method, t, { remote, primary: false })}
|
|
270
|
+
</button>
|
|
271
|
+
))
|
|
272
|
+
: null}
|
|
164
273
|
</>
|
|
165
274
|
) : (
|
|
166
275
|
<>
|
|
167
|
-
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
style={primaryButtonStyle}
|
|
279
|
+
disabled={busy}
|
|
280
|
+
onClick={() => {
|
|
281
|
+
onSignIn(primaryMethod);
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
{busy ? t("working") : methodLabel(primaryMethod, t, { remote, primary: true })}
|
|
285
|
+
</button>
|
|
286
|
+
{altMethods.length > 0 ? (
|
|
168
287
|
<button
|
|
169
|
-
key={method}
|
|
170
288
|
type="button"
|
|
171
|
-
style={
|
|
289
|
+
style={compactButtonStyle}
|
|
172
290
|
disabled={busy}
|
|
173
291
|
onClick={() => {
|
|
174
|
-
|
|
292
|
+
setShowAltMethods((current) => !current);
|
|
175
293
|
}}
|
|
176
294
|
>
|
|
177
|
-
{
|
|
295
|
+
{showAltMethods ? t("hideOtherLoginMethods") : t("otherLoginMethods")}
|
|
178
296
|
</button>
|
|
179
|
-
)
|
|
297
|
+
) : null}
|
|
298
|
+
{showAltMethods
|
|
299
|
+
? altMethods.map((method) => (
|
|
300
|
+
<button
|
|
301
|
+
key={method}
|
|
302
|
+
type="button"
|
|
303
|
+
style={compactButtonStyle}
|
|
304
|
+
disabled={busy}
|
|
305
|
+
onClick={() => {
|
|
306
|
+
onSignIn(method);
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
309
|
+
{methodLabel(method, t, { remote, primary: false })}
|
|
310
|
+
</button>
|
|
311
|
+
))
|
|
312
|
+
: null}
|
|
180
313
|
{source?.available === true ? (
|
|
181
314
|
<button type="button" style={buttonStyle} disabled={sourcesBusy} onClick={onPreviewSource}>
|
|
182
315
|
{t("sourcesPullCopy")}
|
|
@@ -187,23 +320,20 @@ export function ProviderCard({
|
|
|
187
320
|
</>
|
|
188
321
|
)}
|
|
189
322
|
</div>
|
|
190
|
-
{providerStatus.status === "error" ?
|
|
191
|
-
|
|
192
|
-
<p style={bodyStyle}>
|
|
193
|
-
{t("userCode")} <span style={codeStyle}>{providerStatus.userCode}</span>
|
|
194
|
-
</p>
|
|
323
|
+
{providerStatus.status === "error" ? (
|
|
324
|
+
<p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }}>{providerStatus.message}</p>
|
|
195
325
|
) : null}
|
|
196
|
-
{providerStatus.status === "signing-in"
|
|
197
|
-
<
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
326
|
+
{providerStatus.status === "signing-in" ? (
|
|
327
|
+
<SignInSteps
|
|
328
|
+
t={t}
|
|
329
|
+
activeMethod={activeMethod}
|
|
330
|
+
userCode={providerStatus.userCode}
|
|
331
|
+
url={providerStatus.url}
|
|
332
|
+
popupBlocked={popupBlocked}
|
|
333
|
+
/>
|
|
203
334
|
) : null}
|
|
204
335
|
{providerStatus.status === "signing-in" && activeMethod !== "device" ? (
|
|
205
336
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
206
|
-
<p style={bodyStyle}>{t(activeMethod === "browser" ? "pasteBrowserCodeHint" : "pasteCodeHint")}</p>
|
|
207
337
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
|
|
208
338
|
<input
|
|
209
339
|
style={{ ...inputStyle, flex: "1 1 360px" }}
|
|
@@ -235,17 +365,49 @@ export function ProviderCard({
|
|
|
235
365
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
236
366
|
<div style={rowStyle}>
|
|
237
367
|
<h4 style={{ ...titleStyle, fontSize: 14 }}>{t("models")}</h4>
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
368
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
style={compactButtonStyle}
|
|
372
|
+
disabled={busy}
|
|
373
|
+
onClick={() => {
|
|
374
|
+
onSaveModels([]);
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
{t("deselectAll")}
|
|
378
|
+
</button>
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
style={compactButtonStyle}
|
|
382
|
+
disabled={busy}
|
|
383
|
+
onClick={() => {
|
|
384
|
+
onSaveModels([...available]);
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
{t("selectAll")}
|
|
388
|
+
</button>
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
style={compactButtonStyle}
|
|
392
|
+
disabled={busy}
|
|
393
|
+
onClick={() => {
|
|
394
|
+
onSaveModels(available.length > 0 ? [available[0]!] : []);
|
|
395
|
+
}}
|
|
396
|
+
>
|
|
397
|
+
{t("resetModelsDefault")}
|
|
398
|
+
</button>
|
|
399
|
+
</div>
|
|
248
400
|
</div>
|
|
401
|
+
<input
|
|
402
|
+
type="search"
|
|
403
|
+
style={inputStyle}
|
|
404
|
+
value={modelFilter}
|
|
405
|
+
placeholder={t("modelFilterPlaceholder")}
|
|
406
|
+
disabled={busy}
|
|
407
|
+
onChange={(event) => {
|
|
408
|
+
setModelFilter(event.target.value);
|
|
409
|
+
}}
|
|
410
|
+
/>
|
|
249
411
|
{grokProviderStatus?.status === "signed-in" ? (
|
|
250
412
|
<p style={bodyStyle}>
|
|
251
413
|
{grokProviderStatus.catalogSource === "live"
|
|
@@ -259,7 +421,7 @@ export function ProviderCard({
|
|
|
259
421
|
{t("modelHint")} <span style={monoStyle}>{definition.route}/<id></span>
|
|
260
422
|
</p>
|
|
261
423
|
<ul style={listStyle}>
|
|
262
|
-
{
|
|
424
|
+
{filteredModels.map((id) => {
|
|
263
425
|
const checked = selected.includes(id);
|
|
264
426
|
return (
|
|
265
427
|
<li key={id}>
|
|
@@ -281,14 +443,15 @@ export function ProviderCard({
|
|
|
281
443
|
);
|
|
282
444
|
})}
|
|
283
445
|
</ul>
|
|
446
|
+
{filteredModels.length === 0 ? <p style={hintStyle}>{t("modelFilterPlaceholder")}</p> : null}
|
|
284
447
|
{grokProviderStatus?.status === "signed-in" && grokProviderStatus.catalogError !== undefined ? (
|
|
285
|
-
<p style={
|
|
448
|
+
<p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }}>{t("catalogError")}</p>
|
|
286
449
|
) : null}
|
|
287
450
|
{definition.slug === "codex" && showUsage ? (
|
|
288
451
|
<div style={nestedStyle}>
|
|
289
452
|
<p style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}>{t("usageTitle")}</p>
|
|
290
453
|
{usageError === undefined ? null : (
|
|
291
|
-
<p style={
|
|
454
|
+
<p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
|
|
292
455
|
{usageError}
|
|
293
456
|
</p>
|
|
294
457
|
)}
|
|
@@ -299,16 +462,33 @@ export function ProviderCard({
|
|
|
299
462
|
) : (
|
|
300
463
|
<>
|
|
301
464
|
{fetchedAt === undefined ? null : <p style={hintStyle}>{t("usageFetchedAt", { time: fetchedAt })}</p>}
|
|
465
|
+
{usagePercent !== undefined ? (
|
|
466
|
+
<ProgressBar
|
|
467
|
+
value={usagePercent}
|
|
468
|
+
label={t("usageRateLimit")}
|
|
469
|
+
meta={t("usageUsed", { value: `${String(usagePercent)}%` })}
|
|
470
|
+
/>
|
|
471
|
+
) : null}
|
|
302
472
|
{usage.rateLimits.map((limit) => {
|
|
303
|
-
const
|
|
473
|
+
const window = limit.windows[0];
|
|
474
|
+
const used = window?.usedPercent;
|
|
475
|
+
const resetsAt = formatEpoch(window?.resetsAt);
|
|
304
476
|
return (
|
|
305
|
-
<
|
|
306
|
-
{
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
477
|
+
<div key={limit.id} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
478
|
+
{used === undefined ? (
|
|
479
|
+
<p style={hintStyle}>{limit.name ?? t("usageRateLimit")}</p>
|
|
480
|
+
) : (
|
|
481
|
+
<ProgressBar
|
|
482
|
+
value={used}
|
|
483
|
+
label={limit.name ?? t("usageRateLimit")}
|
|
484
|
+
meta={
|
|
485
|
+
resetsAt === undefined
|
|
486
|
+
? t("usageUsed", { value: `${String(used)}%` })
|
|
487
|
+
: `${t("usageUsed", { value: `${String(used)}%` })} · ${t("usageResets", { time: resetsAt })}`
|
|
488
|
+
}
|
|
489
|
+
/>
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
312
492
|
);
|
|
313
493
|
})}
|
|
314
494
|
</>
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
/** Accessible tablist for Coding OAuth settings. */
|
|
1
|
+
/** Accessible segmented tablist for Coding OAuth settings. */
|
|
2
2
|
|
|
3
3
|
import type { KeyboardEvent } from "react";
|
|
4
4
|
import { SETTINGS_TABS } from "../constants.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { segmentedNavStyle, segmentedTabActiveStyle, segmentedTabStyle } from "../styles.ts";
|
|
6
6
|
import type { GrokBuildSettingsInjected, SettingsTabId } from "../types.ts";
|
|
7
7
|
|
|
8
|
+
export interface SettingsTabHint {
|
|
9
|
+
id: SettingsTabId;
|
|
10
|
+
suffix?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export interface SettingsTabsProps {
|
|
9
14
|
t: GrokBuildSettingsInjected["t"];
|
|
10
15
|
activeTab: SettingsTabId;
|
|
11
16
|
onChange: (tab: SettingsTabId) => void;
|
|
17
|
+
hints?: readonly SettingsTabHint[];
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
export function SettingsTabs({ t, activeTab, onChange }: SettingsTabsProps) {
|
|
20
|
+
export function SettingsTabs({ t, activeTab, onChange, hints }: SettingsTabsProps) {
|
|
21
|
+
const hintFor = (id: SettingsTabId): string | undefined => hints?.find((entry) => entry.id === id)?.suffix;
|
|
22
|
+
|
|
15
23
|
const focusTab = (index: number): void => {
|
|
16
24
|
const tab = SETTINGS_TABS[index];
|
|
17
25
|
if (tab === undefined) return;
|
|
@@ -39,9 +47,11 @@ export function SettingsTabs({ t, activeTab, onChange }: SettingsTabsProps) {
|
|
|
39
47
|
};
|
|
40
48
|
|
|
41
49
|
return (
|
|
42
|
-
<div role="tablist" aria-label={t("title")} style={
|
|
50
|
+
<div role="tablist" aria-label={t("title")} style={segmentedNavStyle} onKeyDown={onKeyDown}>
|
|
43
51
|
{SETTINGS_TABS.map((tab) => {
|
|
44
52
|
const selected = activeTab === tab.id;
|
|
53
|
+
const suffix = hintFor(tab.id);
|
|
54
|
+
const label = suffix === undefined ? t(tab.label) : `${t(tab.label)} (${suffix})`;
|
|
45
55
|
return (
|
|
46
56
|
<button
|
|
47
57
|
key={tab.id}
|
|
@@ -51,12 +61,12 @@ export function SettingsTabs({ t, activeTab, onChange }: SettingsTabsProps) {
|
|
|
51
61
|
aria-selected={selected}
|
|
52
62
|
aria-controls={`coding-oauth-panel-${tab.id}`}
|
|
53
63
|
tabIndex={selected ? 0 : -1}
|
|
54
|
-
style={selected ?
|
|
64
|
+
style={selected ? segmentedTabActiveStyle : segmentedTabStyle}
|
|
55
65
|
onClick={() => {
|
|
56
66
|
onChange(tab.id);
|
|
57
67
|
}}
|
|
58
68
|
>
|
|
59
|
-
{
|
|
69
|
+
{label}
|
|
60
70
|
</button>
|
|
61
71
|
);
|
|
62
72
|
})}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Accessible toggle switch replacing native checkboxes for capability settings. */
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties } from "react";
|
|
4
|
+
import { TRANSITION } from "../styles.ts";
|
|
5
|
+
|
|
6
|
+
export interface ToggleSwitchProps {
|
|
7
|
+
checked: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onChange: (checked: boolean) => void;
|
|
10
|
+
/** Accessible label (visually hidden when label prop is rendered externally). */
|
|
11
|
+
ariaLabel?: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const trackStyle = (checked: boolean, disabled: boolean): CSSProperties => ({
|
|
16
|
+
position: "relative",
|
|
17
|
+
width: 40,
|
|
18
|
+
height: 22,
|
|
19
|
+
borderRadius: 11,
|
|
20
|
+
flex: "0 0 auto",
|
|
21
|
+
background: checked
|
|
22
|
+
? "var(--dsw-alias-brand-primary, #315fc7)"
|
|
23
|
+
: "var(--dsw-alias-border-l4, rgba(127, 127, 127, 0.45))",
|
|
24
|
+
opacity: disabled ? 0.5 : 1,
|
|
25
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
26
|
+
transition: TRANSITION,
|
|
27
|
+
border: "none",
|
|
28
|
+
padding: 0,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const thumbStyle = (checked: boolean): CSSProperties => ({
|
|
32
|
+
position: "absolute",
|
|
33
|
+
top: 2,
|
|
34
|
+
left: checked ? 20 : 2,
|
|
35
|
+
width: 18,
|
|
36
|
+
height: 18,
|
|
37
|
+
borderRadius: "50%",
|
|
38
|
+
background: "#ffffff",
|
|
39
|
+
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.25)",
|
|
40
|
+
transition: TRANSITION,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export function ToggleSwitch({ checked, disabled = false, onChange, ariaLabel, id }: ToggleSwitchProps) {
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
id={id}
|
|
47
|
+
type="button"
|
|
48
|
+
role="switch"
|
|
49
|
+
aria-checked={checked}
|
|
50
|
+
aria-label={ariaLabel}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
style={trackStyle(checked, disabled)}
|
|
53
|
+
onClick={() => {
|
|
54
|
+
if (!disabled) onChange(!checked);
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<span aria-hidden="true" style={thumbStyle(checked)} />
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/client/constants.ts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Quick-setup code snippets for the local API gateway. */
|
|
2
|
+
|
|
3
|
+
export type GatewaySnippetId = "curl" | "python" | "ide";
|
|
4
|
+
|
|
5
|
+
export function buildGatewaySnippets(
|
|
6
|
+
openAiBaseUrl: string,
|
|
7
|
+
anthropicBaseUrl: string,
|
|
8
|
+
apiKeyPlaceholder: string,
|
|
9
|
+
): Record<GatewaySnippetId, string> {
|
|
10
|
+
const key = apiKeyPlaceholder.length > 0 ? apiKeyPlaceholder : "<your-gateway-key>";
|
|
11
|
+
return {
|
|
12
|
+
curl: `curl ${openAiBaseUrl}/chat/completions \\
|
|
13
|
+
-H "Authorization: Bearer ${key}" \\
|
|
14
|
+
-H "Content-Type: application/json" \\
|
|
15
|
+
-d '{"model":"codex-oauth/gpt-4.1","messages":[{"role":"user","content":"Hello"}]}'`,
|
|
16
|
+
python: `from openai import OpenAI
|
|
17
|
+
|
|
18
|
+
client = OpenAI(
|
|
19
|
+
base_url="${openAiBaseUrl}",
|
|
20
|
+
api_key="${key}",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
response = client.chat.completions.create(
|
|
24
|
+
model="codex-oauth/gpt-4.1",
|
|
25
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
26
|
+
)
|
|
27
|
+
print(response.choices[0].message.content)`,
|
|
28
|
+
ide: `# OpenAI-compatible client
|
|
29
|
+
base_url: ${openAiBaseUrl}
|
|
30
|
+
api_key: ${key}
|
|
31
|
+
|
|
32
|
+
# Anthropic-compatible client
|
|
33
|
+
base_url: ${anthropicBaseUrl}
|
|
34
|
+
api_key: ${key}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
package/src/client/locales.ts
CHANGED
|
@@ -228,6 +228,23 @@ export const en = {
|
|
|
228
228
|
pluginVersion: "Plugin version {version}",
|
|
229
229
|
aboutDocsHint:
|
|
230
230
|
"See the plugin README and INSTALL docs in the repository for setup, supported providers, and gateway safety notes.",
|
|
231
|
+
otherLoginMethods: "Other sign-in methods",
|
|
232
|
+
hideOtherLoginMethods: "Hide other methods",
|
|
233
|
+
signInStepOpen: "Open the provider authorization page",
|
|
234
|
+
signInStepCode: "Enter the device code when prompted",
|
|
235
|
+
signInStepWait: "Waiting for authorization to complete",
|
|
236
|
+
copyUserCode: "Copy code",
|
|
237
|
+
openAuthUrl: "Open authorization page",
|
|
238
|
+
modelFilterPlaceholder: "Filter models…",
|
|
239
|
+
deselectAll: "Hide all",
|
|
240
|
+
resetModelsDefault: "Reset selection",
|
|
241
|
+
requiresCodexImages: "Turn on Codex images first.",
|
|
242
|
+
gatewaySnippetsTitle: "Quick setup snippets",
|
|
243
|
+
gatewaySnippetCurl: "cURL",
|
|
244
|
+
gatewaySnippetPython: "Python (OpenAI SDK)",
|
|
245
|
+
gatewaySnippetIde: "IDE / client",
|
|
246
|
+
tabAccountsSignedIn: "{count} signed in",
|
|
247
|
+
tabGatewayActive: "Running",
|
|
231
248
|
};
|
|
232
249
|
|
|
233
250
|
export type GrokBuildSettingsKey = keyof typeof en;
|
|
@@ -450,4 +467,21 @@ export const zh: { [Key in GrokBuildSettingsKey]: string } = {
|
|
|
450
467
|
"在远程 DSH 主机上,打开此处 Settings,对每个供应商使用设备码登录。在能访问供应商的任意浏览器完成验证码,再回到聊天选择对应路由模型。",
|
|
451
468
|
pluginVersion: "插件版本 {version}",
|
|
452
469
|
aboutDocsHint: "仓库中的 README 与 INSTALL 文档介绍了安装步骤、支持的供应商以及网关安全注意事项。",
|
|
470
|
+
otherLoginMethods: "其他登录方式",
|
|
471
|
+
hideOtherLoginMethods: "收起其他方式",
|
|
472
|
+
signInStepOpen: "打开供应商授权页面",
|
|
473
|
+
signInStepCode: "在提示处输入设备码",
|
|
474
|
+
signInStepWait: "等待授权完成",
|
|
475
|
+
copyUserCode: "复制设备码",
|
|
476
|
+
openAuthUrl: "打开授权页面",
|
|
477
|
+
modelFilterPlaceholder: "筛选模型…",
|
|
478
|
+
deselectAll: "全部隐藏",
|
|
479
|
+
resetModelsDefault: "重置选择",
|
|
480
|
+
requiresCodexImages: "需先开启 Codex 出图。",
|
|
481
|
+
gatewaySnippetsTitle: "快速配置片段",
|
|
482
|
+
gatewaySnippetCurl: "cURL",
|
|
483
|
+
gatewaySnippetPython: "Python (OpenAI SDK)",
|
|
484
|
+
gatewaySnippetIde: "IDE / 客户端",
|
|
485
|
+
tabAccountsSignedIn: "已登录 {count} 个",
|
|
486
|
+
tabGatewayActive: "运行中",
|
|
453
487
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Injects global keyframes once for skeleton pulse and spinner animations. */
|
|
2
|
+
|
|
3
|
+
const STYLE_ID = "dsh-coding-oauth-micro-styles";
|
|
4
|
+
|
|
5
|
+
const CSS = `
|
|
6
|
+
@keyframes dsh-coding-oauth-skeleton-pulse {
|
|
7
|
+
0% { background-position: 200% 0; }
|
|
8
|
+
100% { background-position: -200% 0; }
|
|
9
|
+
}
|
|
10
|
+
@keyframes dsh-coding-oauth-spin {
|
|
11
|
+
from { transform: rotate(0deg); }
|
|
12
|
+
to { transform: rotate(360deg); }
|
|
13
|
+
}
|
|
14
|
+
@keyframes dsh-coding-oauth-fade-in {
|
|
15
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
16
|
+
to { opacity: 1; transform: translateY(0); }
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
let injected = false;
|
|
21
|
+
|
|
22
|
+
export function ensureMicroStyles(): void {
|
|
23
|
+
if (injected || typeof document === "undefined") return;
|
|
24
|
+
if (document.getElementById(STYLE_ID) !== null) {
|
|
25
|
+
injected = true;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const style = document.createElement("style");
|
|
29
|
+
style.id = STYLE_ID;
|
|
30
|
+
style.textContent = CSS;
|
|
31
|
+
document.head.appendChild(style);
|
|
32
|
+
injected = true;
|
|
33
|
+
}
|