dsh-coding-subscription-oauth 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -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 +4 -4
- package/README.pt-BR.md +4 -4
- package/README.ru.md +4 -4
- package/README.zh-CN.md +4 -4
- 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/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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-coding-subscription-oauth",
|
|
3
3
|
"description": "DeepSeek Harness coding-subscription OAuth: SuperGrok/Grok Build, ChatGPT Plus Codex, Kimi Code, Claude Code. Fixes AUTH API key is invalid, INVALID_REPLAY_STATE, grok-4.6 xhigh, Kimi Bearer vs x-api-key.",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.5",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/** Plugin-owned coding subscription account section inside the dsh Settings shell. */
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cancelPreviewTicket, copyText, isConflictError, isConsumedPreviewError, jsonRequest } from "./api.ts";
|
|
5
5
|
import { AboutTab } from "./components/AboutTab.tsx";
|
|
6
6
|
import { AccountsTab } from "./components/AccountsTab.tsx";
|
|
7
7
|
import { CapabilitiesTab } from "./components/CapabilitiesTab.tsx";
|
|
8
8
|
import { GatewayTab } from "./components/GatewayTab.tsx";
|
|
9
|
+
import type { SettingsTabHint } from "./components/SettingsTabs.tsx";
|
|
9
10
|
import { SettingsTabs } from "./components/SettingsTabs.tsx";
|
|
10
11
|
import {
|
|
11
12
|
CAPABILITIES_PATH,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
STATUS_PATH,
|
|
29
30
|
} from "./constants.ts";
|
|
30
31
|
import { isLikelyRemoteHost } from "./display.ts";
|
|
32
|
+
import { ensureMicroStyles } from "./microStyles.ts";
|
|
31
33
|
import {
|
|
32
34
|
emptyCapabilitySettings,
|
|
33
35
|
mergeSources,
|
|
@@ -185,6 +187,7 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
|
|
|
185
187
|
|
|
186
188
|
// Accounts: status immediately; sources shortly after (non-blocking for first paint).
|
|
187
189
|
useEffect(() => {
|
|
190
|
+
ensureMicroStyles();
|
|
188
191
|
void refresh();
|
|
189
192
|
const timer = window.setTimeout(() => {
|
|
190
193
|
void refreshSources();
|
|
@@ -441,6 +444,22 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
|
|
|
441
444
|
|
|
442
445
|
const showUsage = capabilities?.value.codexUsage === true && status?.providers.codex.status === "signed-in";
|
|
443
446
|
|
|
447
|
+
const tabHints = useMemo((): readonly SettingsTabHint[] => {
|
|
448
|
+
const hints: SettingsTabHint[] = [];
|
|
449
|
+
if (status !== undefined) {
|
|
450
|
+
const signedInCount = Object.values(status.providers).filter(
|
|
451
|
+
(provider) => provider.status === "signed-in",
|
|
452
|
+
).length;
|
|
453
|
+
if (signedInCount > 0) {
|
|
454
|
+
hints.push({ id: "accounts", suffix: String(signedInCount) });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (gateway?.running === true) {
|
|
458
|
+
hints.push({ id: "gateway", suffix: t("tabGatewayActive") });
|
|
459
|
+
}
|
|
460
|
+
return hints;
|
|
461
|
+
}, [gateway?.running, status, t]);
|
|
462
|
+
|
|
444
463
|
const markCopied = (field: CopyField): void => {
|
|
445
464
|
if (copiedTimerRef.current !== undefined) window.clearTimeout(copiedTimerRef.current);
|
|
446
465
|
setCopiedField(field);
|
|
@@ -570,7 +589,7 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
|
|
|
570
589
|
{requestError}
|
|
571
590
|
</p>
|
|
572
591
|
)}
|
|
573
|
-
<SettingsTabs t={t} activeTab={activeTab} onChange={setActiveTab} />
|
|
592
|
+
<SettingsTabs t={t} activeTab={activeTab} onChange={setActiveTab} hints={tabHints} />
|
|
574
593
|
<div
|
|
575
594
|
id={`coding-oauth-panel-${activeTab}`}
|
|
576
595
|
role="tabpanel"
|
|
@@ -635,6 +654,9 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
|
|
|
635
654
|
onRefreshSources={() => {
|
|
636
655
|
void refreshSources();
|
|
637
656
|
}}
|
|
657
|
+
onDismissSourcesNotice={() => {
|
|
658
|
+
setSourcesNotice(undefined);
|
|
659
|
+
}}
|
|
638
660
|
/>
|
|
639
661
|
) : null}
|
|
640
662
|
{activeTab === "capabilities" ? (
|
|
@@ -5,16 +5,13 @@ import { allOfficialCliMissing, anyOfficialCliAvailable } from "../display.ts";
|
|
|
5
5
|
import {
|
|
6
6
|
accountGridStyle,
|
|
7
7
|
bodyStyle,
|
|
8
|
-
buttonStyle,
|
|
9
8
|
cardStyle,
|
|
10
9
|
dotStyle,
|
|
11
|
-
errorStyle,
|
|
12
10
|
hintStyle,
|
|
13
11
|
monoStyle,
|
|
14
12
|
rowStyle,
|
|
15
13
|
skeletonStyle,
|
|
16
14
|
statusStyle,
|
|
17
|
-
tipStyle,
|
|
18
15
|
titleStyle,
|
|
19
16
|
} from "../styles.ts";
|
|
20
17
|
import type {
|
|
@@ -26,7 +23,9 @@ import type {
|
|
|
26
23
|
SourceStatus,
|
|
27
24
|
UsageView,
|
|
28
25
|
} from "../types.ts";
|
|
26
|
+
import { Badge } from "./Badge.tsx";
|
|
29
27
|
import { CliPullPreview } from "./CliPullPreview.tsx";
|
|
28
|
+
import { NoticeBanner } from "./NoticeBanner.tsx";
|
|
30
29
|
import { ProviderCard } from "./ProviderCard.tsx";
|
|
31
30
|
|
|
32
31
|
export interface AccountsTabProps {
|
|
@@ -61,6 +60,7 @@ export interface AccountsTabProps {
|
|
|
61
60
|
onCommitSource: () => void;
|
|
62
61
|
onCancelSourcePreview: () => void;
|
|
63
62
|
onRefreshSources: () => void;
|
|
63
|
+
onDismissSourcesNotice: () => void;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function AccountsTab({
|
|
@@ -95,6 +95,7 @@ export function AccountsTab({
|
|
|
95
95
|
onCommitSource,
|
|
96
96
|
onCancelSourcePreview,
|
|
97
97
|
onRefreshSources,
|
|
98
|
+
onDismissSourcesNotice,
|
|
98
99
|
}: AccountsTabProps) {
|
|
99
100
|
if (status === undefined) {
|
|
100
101
|
return (
|
|
@@ -110,37 +111,35 @@ export function AccountsTab({
|
|
|
110
111
|
return (
|
|
111
112
|
<>
|
|
112
113
|
{remote && !remoteTipDismissed ? (
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{t("remoteTipDismiss")}
|
|
119
|
-
</button>
|
|
120
|
-
</div>
|
|
114
|
+
<NoticeBanner
|
|
115
|
+
message={t("remoteAccountsTip")}
|
|
116
|
+
dismissLabel={t("remoteTipDismiss")}
|
|
117
|
+
onDismiss={onDismissRemoteTip}
|
|
118
|
+
/>
|
|
121
119
|
) : null}
|
|
122
120
|
{allOfficialCliMissing(sources) ? (
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
{t("sourcesCheckAgain")}
|
|
131
|
-
</button>
|
|
132
|
-
</div>
|
|
121
|
+
<NoticeBanner
|
|
122
|
+
message={t("sourcesAllMissingHint")}
|
|
123
|
+
dismissLabel={t("sourcesCheckAgain")}
|
|
124
|
+
onDismiss={() => {
|
|
125
|
+
onRefreshSources();
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
133
128
|
) : null}
|
|
134
129
|
{anyOfficialCliAvailable(sources) ? <p style={hintStyle}>{t("sourcesAvailableHint")}</p> : null}
|
|
135
130
|
{sourcesError === undefined ? null : (
|
|
136
|
-
<p style={
|
|
131
|
+
<p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
|
|
137
132
|
{sourcesError}
|
|
138
133
|
</p>
|
|
139
134
|
)}
|
|
140
135
|
{sourcesNotice === undefined ? null : (
|
|
141
|
-
<
|
|
142
|
-
{sourcesNotice}
|
|
143
|
-
|
|
136
|
+
<NoticeBanner
|
|
137
|
+
key={sourcesNotice}
|
|
138
|
+
message={sourcesNotice}
|
|
139
|
+
tone="success"
|
|
140
|
+
autoHideMs={5000}
|
|
141
|
+
onDismiss={onDismissSourcesNotice}
|
|
142
|
+
/>
|
|
144
143
|
)}
|
|
145
144
|
<div style={accountGridStyle}>
|
|
146
145
|
{PROVIDERS.map((definition) => {
|
|
@@ -199,10 +198,11 @@ export function AccountsTab({
|
|
|
199
198
|
<span style={monoStyle}>{status.antigravity.route}</span>
|
|
200
199
|
</p>
|
|
201
200
|
</div>
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
<Badge
|
|
202
|
+
label={status.antigravity.installed ? t("antigravityInstalled") : t("antigravityMissing")}
|
|
203
|
+
tone={status.antigravity.installed ? "success" : "neutral"}
|
|
204
|
+
installed={status.antigravity.installed}
|
|
205
|
+
/>
|
|
206
206
|
</div>
|
|
207
207
|
<p style={bodyStyle}>{t("antigravityCliHint")}</p>
|
|
208
208
|
<code style={{ ...monoStyle, fontSize: 12, overflowWrap: "anywhere" }}>{t("antigravityCliCommand")}</code>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Status capsule badge with semantic tone colors. */
|
|
2
|
+
|
|
3
|
+
import { badgeStyle, dotStyle, providerStatusTone, type StatusTone } from "../styles.ts";
|
|
4
|
+
import type { ProviderStatus } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
export interface BadgeProps {
|
|
7
|
+
label: string;
|
|
8
|
+
tone?: StatusTone;
|
|
9
|
+
/** When set, derives tone from provider login status. */
|
|
10
|
+
providerStatus?: ProviderStatus["status"];
|
|
11
|
+
installed?: boolean;
|
|
12
|
+
showDot?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Badge({ label, tone, providerStatus, installed = true, showDot = true }: BadgeProps) {
|
|
16
|
+
const resolvedTone =
|
|
17
|
+
tone ?? (providerStatus !== undefined ? providerStatusTone(providerStatus, installed) : "neutral");
|
|
18
|
+
return (
|
|
19
|
+
<span style={badgeStyle(resolvedTone)} role="status">
|
|
20
|
+
{showDot ? (
|
|
21
|
+
<span
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
style={{
|
|
24
|
+
...dotStyle(providerStatus ?? (resolvedTone === "success" ? "signed-in" : "signed-out"), installed),
|
|
25
|
+
width: 7,
|
|
26
|
+
height: 7,
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
) : null}
|
|
30
|
+
<span>{label}</span>
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -5,7 +5,6 @@ import { imagineSourceLabel } from "../parsers.ts";
|
|
|
5
5
|
import {
|
|
6
6
|
bodyStyle,
|
|
7
7
|
cardStyle,
|
|
8
|
-
checkRowStyle,
|
|
9
8
|
dotStyle,
|
|
10
9
|
errorStyle,
|
|
11
10
|
hintStyle,
|
|
@@ -23,6 +22,8 @@ import type {
|
|
|
23
22
|
GrokBuildSettingsInjected,
|
|
24
23
|
ImagineCredentialView,
|
|
25
24
|
} from "../types.ts";
|
|
25
|
+
import { Badge } from "./Badge.tsx";
|
|
26
|
+
import { ToggleSwitch } from "./ToggleSwitch.tsx";
|
|
26
27
|
|
|
27
28
|
export interface CapabilitiesTabProps {
|
|
28
29
|
t: GrokBuildSettingsInjected["t"];
|
|
@@ -68,10 +69,10 @@ export function CapabilitiesTab({
|
|
|
68
69
|
</div>
|
|
69
70
|
) : imagine === undefined ? null : (
|
|
70
71
|
<div style={nestedStyle}>
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
<Badge
|
|
73
|
+
label={imagine.configured ? t("imagineConfigured") : t("imagineNotConfigured")}
|
|
74
|
+
tone={imagine.configured ? "success" : "neutral"}
|
|
75
|
+
/>
|
|
75
76
|
<p style={hintStyle}>{t("imagineSource", { source: imagineSourceLabel(imagine.source, t) })}</p>
|
|
76
77
|
</div>
|
|
77
78
|
)}
|
|
@@ -98,25 +99,34 @@ export function CapabilitiesTab({
|
|
|
98
99
|
const checked = capabilities.value[item.key];
|
|
99
100
|
const imagesOff = item.requiresImages === true && !capabilities.value.codexImages;
|
|
100
101
|
const disabled = capabilitiesBusy || !capabilities.writable || imagesOff;
|
|
102
|
+
const switchId = `cap-switch-${item.key}`;
|
|
101
103
|
return (
|
|
102
|
-
<li
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
<li
|
|
105
|
+
key={item.key}
|
|
106
|
+
style={{
|
|
107
|
+
opacity: imagesOff ? 0.5 : 1,
|
|
108
|
+
transition: "opacity 0.15s ease",
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<div style={{ ...rowStyle, alignItems: "flex-start" }}>
|
|
112
|
+
<label htmlFor={switchId} style={{ flex: "1 1 360px", cursor: disabled ? "default" : "pointer" }}>
|
|
113
|
+
<span style={{ display: "block", fontSize: 14, color: "var(--dsw-alias-label-primary)" }}>
|
|
114
|
+
{t(item.label)}
|
|
115
|
+
</span>
|
|
116
|
+
<span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
|
|
117
|
+
{imagesOff ? t("requiresCodexImages") : t(item.hint)}
|
|
118
|
+
</span>
|
|
119
|
+
</label>
|
|
120
|
+
<ToggleSwitch
|
|
121
|
+
id={switchId}
|
|
106
122
|
checked={checked}
|
|
107
123
|
disabled={disabled}
|
|
108
|
-
|
|
109
|
-
onChange={(
|
|
110
|
-
void onPatchCapability(item.key,
|
|
124
|
+
ariaLabel={t(item.label)}
|
|
125
|
+
onChange={(next) => {
|
|
126
|
+
void onPatchCapability(item.key, next);
|
|
111
127
|
}}
|
|
112
128
|
/>
|
|
113
|
-
|
|
114
|
-
<span style={{ display: "block" }}>{t(item.label)}</span>
|
|
115
|
-
<span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
|
|
116
|
-
{t(item.hint)}
|
|
117
|
-
</span>
|
|
118
|
-
</span>
|
|
119
|
-
</label>
|
|
129
|
+
</div>
|
|
120
130
|
</li>
|
|
121
131
|
);
|
|
122
132
|
})}
|
|
@@ -126,25 +136,28 @@ export function CapabilitiesTab({
|
|
|
126
136
|
{imagineToggles.map((item) => {
|
|
127
137
|
const checked = capabilities.value[item.key];
|
|
128
138
|
const disabled = capabilitiesBusy || !capabilities.writable;
|
|
139
|
+
const switchId = `cap-switch-${item.key}`;
|
|
129
140
|
return (
|
|
130
141
|
<li key={item.key}>
|
|
131
|
-
<
|
|
132
|
-
<
|
|
133
|
-
|
|
142
|
+
<div style={{ ...rowStyle, alignItems: "flex-start" }}>
|
|
143
|
+
<label htmlFor={switchId} style={{ flex: "1 1 360px", cursor: disabled ? "default" : "pointer" }}>
|
|
144
|
+
<span style={{ display: "block", fontSize: 14, color: "var(--dsw-alias-label-primary)" }}>
|
|
145
|
+
{t(item.label)}
|
|
146
|
+
</span>
|
|
147
|
+
<span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
|
|
148
|
+
{t(item.hint)}
|
|
149
|
+
</span>
|
|
150
|
+
</label>
|
|
151
|
+
<ToggleSwitch
|
|
152
|
+
id={switchId}
|
|
134
153
|
checked={checked}
|
|
135
154
|
disabled={disabled}
|
|
136
|
-
|
|
137
|
-
onChange={(
|
|
138
|
-
void onPatchCapability(item.key,
|
|
155
|
+
ariaLabel={t(item.label)}
|
|
156
|
+
onChange={(next) => {
|
|
157
|
+
void onPatchCapability(item.key, next);
|
|
139
158
|
}}
|
|
140
159
|
/>
|
|
141
|
-
|
|
142
|
-
<span style={{ display: "block" }}>{t(item.label)}</span>
|
|
143
|
-
<span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
|
|
144
|
-
{t(item.hint)}
|
|
145
|
-
</span>
|
|
146
|
-
</span>
|
|
147
|
-
</label>
|
|
160
|
+
</div>
|
|
148
161
|
</li>
|
|
149
162
|
);
|
|
150
163
|
})}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Compact copy button with copied / failed feedback. */
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { copyText } from "../api.ts";
|
|
5
|
+
import { compactButtonStyle, primaryButtonStyle } from "../styles.ts";
|
|
6
|
+
|
|
7
|
+
export interface CopyButtonProps {
|
|
8
|
+
text: string;
|
|
9
|
+
idleLabel: string;
|
|
10
|
+
copiedLabel: string;
|
|
11
|
+
failedLabel: string;
|
|
12
|
+
primary?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function CopyButton({
|
|
17
|
+
text,
|
|
18
|
+
idleLabel,
|
|
19
|
+
copiedLabel,
|
|
20
|
+
failedLabel,
|
|
21
|
+
primary = false,
|
|
22
|
+
disabled = false,
|
|
23
|
+
}: CopyButtonProps) {
|
|
24
|
+
const [state, setState] = useState<"idle" | "copied" | "failed">("idle");
|
|
25
|
+
const timerRef = useRef<number | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
return () => {
|
|
29
|
+
if (timerRef.current !== undefined) window.clearTimeout(timerRef.current);
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const handleClick = useCallback(async () => {
|
|
34
|
+
const ok = await copyText(text);
|
|
35
|
+
setState(ok ? "copied" : "failed");
|
|
36
|
+
if (timerRef.current !== undefined) window.clearTimeout(timerRef.current);
|
|
37
|
+
timerRef.current = window.setTimeout(() => {
|
|
38
|
+
setState("idle");
|
|
39
|
+
timerRef.current = undefined;
|
|
40
|
+
}, 2000);
|
|
41
|
+
}, [text]);
|
|
42
|
+
|
|
43
|
+
const label = state === "copied" ? copiedLabel : state === "failed" ? failedLabel : idleLabel;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
style={primary ? primaryButtonStyle : compactButtonStyle}
|
|
49
|
+
disabled={disabled || text.length === 0}
|
|
50
|
+
onClick={() => {
|
|
51
|
+
void handleClick();
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{label}
|
|
55
|
+
</button>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Local API gateway settings tab. */
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
4
|
import { GATEWAY_PORT_MAX, GATEWAY_PORT_MIN } from "../constants.ts";
|
|
5
|
+
import { buildGatewaySnippets, type GatewaySnippetId } from "../gatewaySnippets.ts";
|
|
5
6
|
import { formatGatewayBaseUrl, parseGatewayPort, randomGatewayPort } from "../parsers.ts";
|
|
6
7
|
import {
|
|
7
8
|
bodyStyle,
|
|
@@ -16,12 +17,18 @@ import {
|
|
|
16
17
|
monoStyle,
|
|
17
18
|
nestedStyle,
|
|
18
19
|
primaryButtonStyle,
|
|
20
|
+
segmentedNavStyle,
|
|
21
|
+
segmentedTabActiveStyle,
|
|
22
|
+
segmentedTabStyle,
|
|
19
23
|
skeletonStyle,
|
|
24
|
+
snippetStyle,
|
|
20
25
|
statusStyle,
|
|
21
26
|
titleStyle,
|
|
22
27
|
warningStyle,
|
|
23
28
|
} from "../styles.ts";
|
|
24
29
|
import type { CopyField, GatewayView, GrokBuildSettingsInjected } from "../types.ts";
|
|
30
|
+
import { Badge } from "./Badge.tsx";
|
|
31
|
+
import { CopyButton } from "./CopyButton.tsx";
|
|
25
32
|
|
|
26
33
|
export interface GatewayTabProps {
|
|
27
34
|
t: GrokBuildSettingsInjected["t"];
|
|
@@ -47,6 +54,15 @@ export interface GatewayTabProps {
|
|
|
47
54
|
onRotate: () => void;
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
const SNIPPET_TABS: readonly {
|
|
58
|
+
id: GatewaySnippetId;
|
|
59
|
+
labelKey: "gatewaySnippetCurl" | "gatewaySnippetPython" | "gatewaySnippetIde";
|
|
60
|
+
}[] = [
|
|
61
|
+
{ id: "curl", labelKey: "gatewaySnippetCurl" },
|
|
62
|
+
{ id: "python", labelKey: "gatewaySnippetPython" },
|
|
63
|
+
{ id: "ide", labelKey: "gatewaySnippetIde" },
|
|
64
|
+
];
|
|
65
|
+
|
|
50
66
|
export function GatewayTab({
|
|
51
67
|
t,
|
|
52
68
|
gateway,
|
|
@@ -71,6 +87,23 @@ export function GatewayTab({
|
|
|
71
87
|
onRotate,
|
|
72
88
|
}: GatewayTabProps) {
|
|
73
89
|
const [enableConfirm, setEnableConfirm] = useState(false);
|
|
90
|
+
const [activeSnippet, setActiveSnippet] = useState<GatewaySnippetId>("curl");
|
|
91
|
+
|
|
92
|
+
const portValid = parseGatewayPort(portDraft) !== undefined;
|
|
93
|
+
const portChanged = gateway !== undefined && portDraft !== String(gateway.port);
|
|
94
|
+
|
|
95
|
+
const snippets = useMemo(() => {
|
|
96
|
+
if (gateway === undefined) return undefined;
|
|
97
|
+
const openAi = `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`;
|
|
98
|
+
const anthropic = formatGatewayBaseUrl(gateway.bind, gateway.port);
|
|
99
|
+
const key =
|
|
100
|
+
gatewayKeyVisible && gatewayOnceKey !== undefined
|
|
101
|
+
? gatewayOnceKey
|
|
102
|
+
: gateway.keyHint.length > 0
|
|
103
|
+
? gateway.keyHint
|
|
104
|
+
: "";
|
|
105
|
+
return buildGatewaySnippets(openAi, anthropic, key);
|
|
106
|
+
}, [gateway, gatewayKeyVisible, gatewayOnceKey]);
|
|
74
107
|
|
|
75
108
|
const copyLabel = (field: CopyField, idle?: string): string => {
|
|
76
109
|
if (copyFailedField === field) return t("copyFailed");
|
|
@@ -107,10 +140,10 @@ export function GatewayTab({
|
|
|
107
140
|
</div>
|
|
108
141
|
) : gateway === undefined ? null : (
|
|
109
142
|
<div style={nestedStyle}>
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
143
|
+
<Badge
|
|
144
|
+
label={gateway.running ? t("gatewayRunning") : t("gatewayStopped")}
|
|
145
|
+
tone={gateway.running ? "success" : "neutral"}
|
|
146
|
+
/>
|
|
114
147
|
{enableConfirm ? (
|
|
115
148
|
<div style={nestedStyle}>
|
|
116
149
|
<p style={bodyStyle}>{t("gatewayEnableConfirm")}</p>
|
|
@@ -166,6 +199,11 @@ export function GatewayTab({
|
|
|
166
199
|
<p id="coding-oauth-gateway-port-hint" style={hintStyle}>
|
|
167
200
|
{t("gatewayPortHint")}
|
|
168
201
|
</p>
|
|
202
|
+
{!portValid && portDraft.length > 0 ? (
|
|
203
|
+
<p style={{ ...hintStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
|
|
204
|
+
{t("gatewayPortInvalid")}
|
|
205
|
+
</p>
|
|
206
|
+
) : null}
|
|
169
207
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 8 }}>
|
|
170
208
|
<input
|
|
171
209
|
id="coding-oauth-gateway-port"
|
|
@@ -177,7 +215,16 @@ export function GatewayTab({
|
|
|
177
215
|
value={portDraft}
|
|
178
216
|
disabled={gatewayBusy}
|
|
179
217
|
aria-describedby="coding-oauth-gateway-port-hint"
|
|
180
|
-
|
|
218
|
+
aria-invalid={!portValid && portDraft.length > 0}
|
|
219
|
+
style={{
|
|
220
|
+
...inputStyle,
|
|
221
|
+
width: 112,
|
|
222
|
+
flex: "0 0 112px",
|
|
223
|
+
borderColor:
|
|
224
|
+
!portValid && portDraft.length > 0
|
|
225
|
+
? "var(--dsw-alias-state-error-primary)"
|
|
226
|
+
: "var(--dsw-alias-border-l2)",
|
|
227
|
+
}}
|
|
181
228
|
onChange={(event) => {
|
|
182
229
|
onPortDraftChange(event.target.value);
|
|
183
230
|
}}
|
|
@@ -194,9 +241,7 @@ export function GatewayTab({
|
|
|
194
241
|
<button
|
|
195
242
|
type="button"
|
|
196
243
|
style={primaryButtonStyle}
|
|
197
|
-
disabled={
|
|
198
|
-
gatewayBusy || portDraft === String(gateway.port) || parseGatewayPort(portDraft) === undefined
|
|
199
|
-
}
|
|
244
|
+
disabled={gatewayBusy || !portChanged || !portValid}
|
|
200
245
|
onClick={onApplyPort}
|
|
201
246
|
>
|
|
202
247
|
{t("gatewayPortApply")}
|
|
@@ -262,6 +307,38 @@ export function GatewayTab({
|
|
|
262
307
|
</span>
|
|
263
308
|
</p>
|
|
264
309
|
<p style={hintStyle}>{t("gatewayKeyCopyHint")}</p>
|
|
310
|
+
{gateway.enabled && snippets !== undefined ? (
|
|
311
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
312
|
+
<h4 style={{ ...titleStyle, fontSize: 14 }}>{t("gatewaySnippetsTitle")}</h4>
|
|
313
|
+
<div role="tablist" aria-label={t("gatewaySnippetsTitle")} style={segmentedNavStyle}>
|
|
314
|
+
{SNIPPET_TABS.map((tab) => {
|
|
315
|
+
const selected = activeSnippet === tab.id;
|
|
316
|
+
return (
|
|
317
|
+
<button
|
|
318
|
+
key={tab.id}
|
|
319
|
+
type="button"
|
|
320
|
+
role="tab"
|
|
321
|
+
aria-selected={selected}
|
|
322
|
+
style={selected ? segmentedTabActiveStyle : segmentedTabStyle}
|
|
323
|
+
onClick={() => {
|
|
324
|
+
setActiveSnippet(tab.id);
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
{t(tab.labelKey)}
|
|
328
|
+
</button>
|
|
329
|
+
);
|
|
330
|
+
})}
|
|
331
|
+
</div>
|
|
332
|
+
<code style={snippetStyle}>{snippets[activeSnippet]}</code>
|
|
333
|
+
<CopyButton
|
|
334
|
+
text={snippets[activeSnippet]}
|
|
335
|
+
idleLabel={t("copy")}
|
|
336
|
+
copiedLabel={t("copied")}
|
|
337
|
+
failedLabel={t("copyFailed")}
|
|
338
|
+
primary
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
) : null}
|
|
265
342
|
{gatewayRotateConfirm ? (
|
|
266
343
|
<div style={nestedStyle}>
|
|
267
344
|
<p style={bodyStyle}>{t("gatewayRotateConfirm")}</p>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Dismissible notice banner with optional auto-hide. */
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { bodyStyle, buttonStyle, noticeStyle } from "../styles.ts";
|
|
5
|
+
|
|
6
|
+
export interface NoticeBannerProps {
|
|
7
|
+
message: string;
|
|
8
|
+
dismissLabel?: string;
|
|
9
|
+
onDismiss?: () => void;
|
|
10
|
+
autoHideMs?: number;
|
|
11
|
+
tone?: "info" | "success";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function NoticeBanner({ message, dismissLabel, onDismiss, autoHideMs, tone = "info" }: NoticeBannerProps) {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (autoHideMs === undefined || onDismiss === undefined) return;
|
|
17
|
+
const timer = window.setTimeout(onDismiss, autoHideMs);
|
|
18
|
+
return () => {
|
|
19
|
+
window.clearTimeout(timer);
|
|
20
|
+
};
|
|
21
|
+
}, [autoHideMs, onDismiss]);
|
|
22
|
+
|
|
23
|
+
const borderColor =
|
|
24
|
+
tone === "success" ? "var(--dsw-alias-state-success-primary, #22a06b)" : "var(--dsw-alias-brand-primary, #1677ff)";
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
style={{
|
|
29
|
+
...noticeStyle,
|
|
30
|
+
display: "flex",
|
|
31
|
+
alignItems: "flex-start",
|
|
32
|
+
justifyContent: "space-between",
|
|
33
|
+
gap: 12,
|
|
34
|
+
borderLeft: `3px solid ${borderColor}`,
|
|
35
|
+
}}
|
|
36
|
+
role="status"
|
|
37
|
+
>
|
|
38
|
+
<p style={{ ...bodyStyle, margin: 0, color: "var(--dsw-alias-label-primary)" }}>{message}</p>
|
|
39
|
+
{onDismiss === undefined || dismissLabel === undefined ? null : (
|
|
40
|
+
<button type="button" style={buttonStyle} onClick={onDismiss}>
|
|
41
|
+
{dismissLabel}
|
|
42
|
+
</button>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Usage quota progress bar with threshold-based color. */
|
|
2
|
+
|
|
3
|
+
import { hintStyle } from "../styles.ts";
|
|
4
|
+
|
|
5
|
+
export interface ProgressBarProps {
|
|
6
|
+
/** 0–100 used percentage. */
|
|
7
|
+
value: number;
|
|
8
|
+
label?: string;
|
|
9
|
+
meta?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function barColor(percent: number): string {
|
|
13
|
+
if (percent >= 90) return "var(--dsw-alias-state-error-primary, #d92d20)";
|
|
14
|
+
if (percent >= 75) return "var(--dsw-alias-state-warning-primary, #e06c00)";
|
|
15
|
+
return "var(--dsw-alias-brand-primary, #1677ff)";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ProgressBar({ value, label, meta }: ProgressBarProps) {
|
|
19
|
+
const clamped = Math.max(0, Math.min(100, value));
|
|
20
|
+
return (
|
|
21
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
22
|
+
{label === undefined ? null : (
|
|
23
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap" }}>
|
|
24
|
+
<span style={{ ...hintStyle, color: "var(--dsw-alias-label-primary)" }}>{label}</span>
|
|
25
|
+
{meta === undefined ? null : <span style={hintStyle}>{meta}</span>}
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
<div
|
|
29
|
+
role="progressbar"
|
|
30
|
+
aria-valuenow={clamped}
|
|
31
|
+
aria-valuemin={0}
|
|
32
|
+
aria-valuemax={100}
|
|
33
|
+
style={{
|
|
34
|
+
height: 8,
|
|
35
|
+
borderRadius: 4,
|
|
36
|
+
background: "var(--dsw-alias-border-l2)",
|
|
37
|
+
overflow: "hidden",
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
width: `${String(clamped)}%`,
|
|
43
|
+
height: "100%",
|
|
44
|
+
borderRadius: 4,
|
|
45
|
+
background: barColor(clamped),
|
|
46
|
+
transition: "width 0.3s ease, background 0.3s ease",
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|