dsh-plugin-subscription-oauth 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/auth/mimo-flow.d.ts +9 -0
- package/lib/auth/mimo-flow.js +171 -0
- package/lib/auth/registry.d.ts +10 -1
- package/lib/auth/registry.js +11 -1
- package/lib/auth/rpc.js +6 -0
- package/lib/client/SubscriptionsSection.js +37 -17
- package/lib/client.js +108 -135
- package/lib/client.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +13 -1
- package/lib/protocol.js +1 -0
- package/lib/providers/auto.js +3 -0
- package/lib/providers/grok.d.ts +2 -0
- package/lib/providers/grok.js +31 -21
- package/lib/providers/kimi-provider.js +2 -1
- package/lib/providers/kimi.js +18 -13
- package/lib/providers/mimo.d.ts +10 -0
- package/lib/providers/mimo.js +65 -1
- package/lib/settings.d.ts +8 -0
- package/lib/settings.js +19 -5
- package/package.json +1 -1
package/lib/auth/mimo-flow.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface MimoSessionCookies {
|
|
|
63
63
|
/** The Xiaomi sign-in has lapsed and the account must sign in again. */
|
|
64
64
|
export declare class MimoSignInLapsed extends Error {
|
|
65
65
|
}
|
|
66
|
+
/** The open platform's origin (the Token Plan lives here, not at a MiMo server). */
|
|
67
|
+
export declare const MIMO_PLATFORM = "https://platform.xiaomimimo.com";
|
|
66
68
|
/**
|
|
67
69
|
* Sign the account on at a MiMo server, following the serviceLogin redirects.
|
|
68
70
|
*
|
|
@@ -118,6 +120,8 @@ export declare function refreshMimoSession(creds: MimoCredentials, fetchFn: type
|
|
|
118
120
|
export interface MimoUsageWindow {
|
|
119
121
|
label: string;
|
|
120
122
|
usedPercent?: number;
|
|
123
|
+
used?: number;
|
|
124
|
+
limit?: number;
|
|
121
125
|
resetsAt?: number;
|
|
122
126
|
}
|
|
123
127
|
/** The account's plan and weekly allowance, for the Settings card and chip. */
|
|
@@ -126,4 +130,9 @@ export declare function readMimoUsage(session: MimoSessionCookies, creds: MimoCr
|
|
|
126
130
|
renewsAt?: number;
|
|
127
131
|
windows: MimoUsageWindow[];
|
|
128
132
|
}>;
|
|
133
|
+
/** The account's Token Plan as a usable API credential, or null when it has none. */
|
|
134
|
+
export declare function readMimoPlanKey(creds: MimoCredentials, fetchFn: typeof fetch): Promise<{
|
|
135
|
+
key: string;
|
|
136
|
+
base: string;
|
|
137
|
+
} | null>;
|
|
129
138
|
export {};
|
package/lib/auth/mimo-flow.js
CHANGED
|
@@ -144,6 +144,10 @@ export class Jar {
|
|
|
144
144
|
/** The Xiaomi sign-in has lapsed and the account must sign in again. */
|
|
145
145
|
export class MimoSignInLapsed extends Error {
|
|
146
146
|
}
|
|
147
|
+
/** The open platform's origin (the Token Plan lives here, not at a MiMo server). */
|
|
148
|
+
export const MIMO_PLATFORM = 'https://platform.xiaomimimo.com';
|
|
149
|
+
/** A plan the app refused covers for this long before its endpoint is asked again. */
|
|
150
|
+
const TP_LIFE_MS = 60 * 60 * 1000;
|
|
147
151
|
const appHeaders = () => ({
|
|
148
152
|
'User-Agent': USER_AGENT,
|
|
149
153
|
'X-Client-Version': APP_VERSION,
|
|
@@ -457,5 +461,172 @@ export async function readMimoUsage(session, creds, fetchFn) {
|
|
|
457
461
|
catch {
|
|
458
462
|
// The plan alone, when the week's allowance can't be read.
|
|
459
463
|
}
|
|
464
|
+
// The Token Plan at the open platform, an aside the app's requests don't spend.
|
|
465
|
+
const tokenPlan = await readMimoTokenPlan(creds, fetchFn);
|
|
466
|
+
if (tokenPlan !== null) {
|
|
467
|
+
if (current === undefined) {
|
|
468
|
+
plan = tokenPlan.plan;
|
|
469
|
+
if (tokenPlan.until !== undefined)
|
|
470
|
+
renewsAt = tokenPlan.until;
|
|
471
|
+
}
|
|
472
|
+
if (tokenPlan.window !== undefined)
|
|
473
|
+
windows.push(tokenPlan.window);
|
|
474
|
+
}
|
|
460
475
|
return { plan, ...renewsAt === undefined ? {} : { renewsAt }, windows };
|
|
461
476
|
}
|
|
477
|
+
/** A short count of the Token Plan's credits, "6.81M" / "4.1B" / "12K". */
|
|
478
|
+
function mimoCredits(value) {
|
|
479
|
+
for (const [divisor, unit] of [[1e9, 'B'], [1e6, 'M'], [1e3, 'K']]) {
|
|
480
|
+
if (value >= divisor)
|
|
481
|
+
return `${Number((value / divisor).toPrecision(3))}${unit}`;
|
|
482
|
+
}
|
|
483
|
+
return String(value);
|
|
484
|
+
}
|
|
485
|
+
/** Read one of the platform's /api/v1 pages, signing on afresh when the session is gone. */
|
|
486
|
+
async function mimoPlatformPage(creds, path, cookies, fetchFn) {
|
|
487
|
+
const url = `${MIMO_PLATFORM}/api/v1${path}`;
|
|
488
|
+
const ask = async (cookie) => {
|
|
489
|
+
const response = await fetchFn(url, {
|
|
490
|
+
headers: cookie === '' ? {} : { Cookie: cookie },
|
|
491
|
+
redirect: 'manual',
|
|
492
|
+
signal: AbortSignal.timeout(20_000),
|
|
493
|
+
});
|
|
494
|
+
return { status: response.status, text: await response.text() };
|
|
495
|
+
};
|
|
496
|
+
const parse = (text) => {
|
|
497
|
+
try {
|
|
498
|
+
const value = JSON.parse(text);
|
|
499
|
+
return value?.code === 0 ? (isRecord(value.data) ? value.data : null) : undefined;
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
if (cookies !== undefined && Object.keys(cookies).length > 0) {
|
|
506
|
+
const { status, text } = await ask(cookieHeaderOf(cookies));
|
|
507
|
+
const data = status === 200 ? parse(text) : undefined;
|
|
508
|
+
if (data !== undefined)
|
|
509
|
+
return { cookies, data };
|
|
510
|
+
}
|
|
511
|
+
let { status, text } = await ask('');
|
|
512
|
+
let login;
|
|
513
|
+
try {
|
|
514
|
+
login = JSON.parse(text).loginUrl;
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
login = undefined;
|
|
518
|
+
}
|
|
519
|
+
if (status !== 401 || typeof login !== 'string' || !login.startsWith('https://account.xiaomi.com/')) {
|
|
520
|
+
throw new Error(`Xiaomi MiMo platform ${path}: HTTP ${status}`);
|
|
521
|
+
}
|
|
522
|
+
// Follow the serviceLogin redirect with the passToken, collecting the platform's cookies.
|
|
523
|
+
const jar = new Jar();
|
|
524
|
+
for (const [key, value] of [
|
|
525
|
+
['userId', creds.userId], ['passToken', creds.passToken], ['cUserId', creds.cUserId],
|
|
526
|
+
['deviceId', creds.deviceId], ['pass_ua', 'pc'], ['uLocale', 'zh_CN'],
|
|
527
|
+
]) {
|
|
528
|
+
if (value)
|
|
529
|
+
jar.seed('https://account.xiaomi.com', key, value);
|
|
530
|
+
}
|
|
531
|
+
let at = login;
|
|
532
|
+
for (let hop = 0;; hop++) {
|
|
533
|
+
if (hop > 10)
|
|
534
|
+
throw new Error('Xiaomi MiMo platform sign-in: too many redirects');
|
|
535
|
+
const cookie = jar.header(at);
|
|
536
|
+
const response = await fetchFn(at, {
|
|
537
|
+
headers: cookie === '' ? {} : { Cookie: cookie },
|
|
538
|
+
redirect: 'manual',
|
|
539
|
+
signal: AbortSignal.timeout(20_000),
|
|
540
|
+
});
|
|
541
|
+
jar.take(response, at);
|
|
542
|
+
const location = response.headers.get('location');
|
|
543
|
+
if (response.status < 300 || response.status >= 400 || !location) {
|
|
544
|
+
status = response.status;
|
|
545
|
+
text = await response.text();
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
await response.arrayBuffer().catch(() => { });
|
|
549
|
+
const next = new URL(location, at);
|
|
550
|
+
if (next.protocol === 'http:' && next.host === new URL(MIMO_PLATFORM).host)
|
|
551
|
+
next.protocol = 'https:';
|
|
552
|
+
at = next.href;
|
|
553
|
+
}
|
|
554
|
+
const data = status === 200 ? parse(text) : undefined;
|
|
555
|
+
if (data === undefined)
|
|
556
|
+
throw new Error(`Xiaomi MiMo platform ${path}: the Xiaomi account didn't sign on (HTTP ${status})`);
|
|
557
|
+
return { cookies: jar.forOrigin(`${MIMO_PLATFORM}/`), data };
|
|
558
|
+
}
|
|
559
|
+
/** Read the account's Token Plan, or null when it has none or the platform can't be read. */
|
|
560
|
+
async function readMimoTokenPlan(creds, fetchFn) {
|
|
561
|
+
try {
|
|
562
|
+
const detail = await mimoPlatformPage(creds, '/tokenPlan/detail', undefined, fetchFn);
|
|
563
|
+
const p = detail.data;
|
|
564
|
+
const name = p !== null && typeof p.planName === 'string' ? p.planName.trim() : '';
|
|
565
|
+
if (name === '' || p?.expired !== false)
|
|
566
|
+
return null;
|
|
567
|
+
const out = { plan: `Token Plan ${name}` };
|
|
568
|
+
const end = typeof p.currentPeriodEnd === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(p.currentPeriodEnd)
|
|
569
|
+
? Date.parse(p.currentPeriodEnd.replace(' ', 'T') + 'Z')
|
|
570
|
+
: NaN;
|
|
571
|
+
if (!Number.isNaN(end))
|
|
572
|
+
out.until = end;
|
|
573
|
+
try {
|
|
574
|
+
const usage = await mimoPlatformPage(creds, '/tokenPlan/usage', detail.cookies, fetchFn);
|
|
575
|
+
const u = usage.data;
|
|
576
|
+
const items = [
|
|
577
|
+
...(isRecord(u) && isRecord(u.monthUsage) && Array.isArray(u.monthUsage.items) ? u.monthUsage.items : []),
|
|
578
|
+
...(isRecord(u) && isRecord(u.usage) && Array.isArray(u.usage.items) ? u.usage.items : []),
|
|
579
|
+
].filter(isRecord);
|
|
580
|
+
const it = items.find((x) => x.name === 'month_total_token') ?? items.find((x) => x.name === 'plan_total_token');
|
|
581
|
+
const used = typeof it?.used === 'number' ? it.used : NaN;
|
|
582
|
+
const limit = typeof it?.limit === 'number' ? it.limit : NaN;
|
|
583
|
+
if (Number.isFinite(used) && Number.isFinite(limit) && limit > 0) {
|
|
584
|
+
const window = {
|
|
585
|
+
label: 'Token Plan',
|
|
586
|
+
usedPercent: Math.max(0, Math.min(100, (100 * used) / limit)),
|
|
587
|
+
used,
|
|
588
|
+
limit,
|
|
589
|
+
};
|
|
590
|
+
if (out.until !== undefined)
|
|
591
|
+
window.resetsAt = out.until;
|
|
592
|
+
out.window = window;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// The plan alone, when its usage can't be read.
|
|
597
|
+
}
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/** The account's Token Plan as a usable API credential, or null when it has none. */
|
|
605
|
+
export async function readMimoPlanKey(creds, fetchFn) {
|
|
606
|
+
try {
|
|
607
|
+
let cookies;
|
|
608
|
+
const read = async (path) => {
|
|
609
|
+
const result = await mimoPlatformPage(creds, path, cookies, fetchFn);
|
|
610
|
+
cookies = result.cookies;
|
|
611
|
+
return result.data;
|
|
612
|
+
};
|
|
613
|
+
const detail = await read('/tokenPlan/detail');
|
|
614
|
+
if (detail === null || typeof detail.planName !== 'string' || detail.planName.trim() === '' || detail.expired !== false) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
const keyInfo = await read('/tokenPlan/apiKey');
|
|
618
|
+
const base = typeof keyInfo?.openaiBaseUrl === 'string' && /^https:\/\/[^/]+\.xiaomimimo\.com\//.test(keyInfo.openaiBaseUrl)
|
|
619
|
+
? keyInfo.openaiBaseUrl.replace(/\/+$/, '')
|
|
620
|
+
: '';
|
|
621
|
+
if (base === '')
|
|
622
|
+
return null;
|
|
623
|
+
const raw = await read('/tokenPlan/apiKey/raw');
|
|
624
|
+
const rawKey = typeof raw === 'string' ? raw : '';
|
|
625
|
+
if (!rawKey.startsWith('tp-'))
|
|
626
|
+
return null;
|
|
627
|
+
return { key: rawKey, base };
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
package/lib/auth/registry.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module dsh-plugin-subscription-oauth/auth/registry
|
|
10
10
|
*/
|
|
11
|
+
import type { ModelPreferences } from '../settings.js';
|
|
11
12
|
import type { LoginStart, ModelVariants, ProviderStatus, RefreshResult, SubscriptionStatus, UsageResult } from '../protocol.js';
|
|
12
13
|
import type { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
13
14
|
import type { ProviderLoginStart, ProviderRefreshResult, ProviderUsageResult } from './provider-controller.js';
|
|
@@ -41,8 +42,9 @@ export interface ProviderRoute {
|
|
|
41
42
|
* failure: the message names the routes that do exist.
|
|
42
43
|
*/
|
|
43
44
|
export declare class SubscriptionRegistry {
|
|
45
|
+
private readonly preferences;
|
|
44
46
|
private readonly byId;
|
|
45
|
-
constructor(routes: readonly ProviderRoute[]);
|
|
47
|
+
constructor(routes: readonly ProviderRoute[], preferences: ModelPreferences);
|
|
46
48
|
/** Route keys this registry serves, in display order. */
|
|
47
49
|
ids(): readonly string[];
|
|
48
50
|
/** Every route, in registration order. */
|
|
@@ -92,4 +94,11 @@ export declare class SubscriptionRegistry {
|
|
|
92
94
|
reload(): Promise<SubscriptionStatus>;
|
|
93
95
|
/** The fast and image variants for one model, as the route's discovery reported them. */
|
|
94
96
|
modelVariants(provider: string, id: string): Promise<ModelVariants | undefined>;
|
|
97
|
+
/** Whether the `auto` provider is offered; flips it. */
|
|
98
|
+
autoState(): Promise<{
|
|
99
|
+
enabled: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
setAutoOn(enabled: boolean): Promise<{
|
|
102
|
+
enabled: boolean;
|
|
103
|
+
}>;
|
|
95
104
|
}
|
package/lib/auth/registry.js
CHANGED
|
@@ -29,8 +29,10 @@ function displayRank(id) {
|
|
|
29
29
|
* failure: the message names the routes that do exist.
|
|
30
30
|
*/
|
|
31
31
|
export class SubscriptionRegistry {
|
|
32
|
+
preferences;
|
|
32
33
|
byId;
|
|
33
|
-
constructor(routes) {
|
|
34
|
+
constructor(routes, preferences) {
|
|
35
|
+
this.preferences = preferences;
|
|
34
36
|
this.byId = new Map(routes.map((route) => [route.id, route]));
|
|
35
37
|
}
|
|
36
38
|
/** Route keys this registry serves, in display order. */
|
|
@@ -151,4 +153,12 @@ export class SubscriptionRegistry {
|
|
|
151
153
|
const route = this.route(provider);
|
|
152
154
|
return route.modelVariants?.(id);
|
|
153
155
|
}
|
|
156
|
+
/** Whether the `auto` provider is offered; flips it. */
|
|
157
|
+
async autoState() {
|
|
158
|
+
return { enabled: (await this.preferences.autoPreferences()).enabled };
|
|
159
|
+
}
|
|
160
|
+
async setAutoOn(enabled) {
|
|
161
|
+
await this.preferences.setAutoOn(enabled);
|
|
162
|
+
return { enabled };
|
|
163
|
+
}
|
|
154
164
|
}
|
package/lib/auth/rpc.js
CHANGED
|
@@ -155,6 +155,12 @@ async function runEndpoint(registry, endpoint, payload, balance) {
|
|
|
155
155
|
}
|
|
156
156
|
case 'usage':
|
|
157
157
|
return registry.usage(requiredProvider(payload, endpoint), booleanField(payload, 'force'));
|
|
158
|
+
case 'setAutoOn': {
|
|
159
|
+
const enabled = booleanField(payload, 'enabled');
|
|
160
|
+
if (enabled === undefined)
|
|
161
|
+
throw new ProviderError('setAutoOn requires an enabled flag', BAD_REQUEST);
|
|
162
|
+
return registry.setAutoOn(enabled);
|
|
163
|
+
}
|
|
158
164
|
case 'deepseek-balance': {
|
|
159
165
|
// The DeepSeek account's balance, read on the node half through the host's own
|
|
160
166
|
// account service. The browser cannot call that service directly (it sits behind
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Subscriptions settings page.
|
|
4
4
|
*
|
|
@@ -134,13 +134,21 @@ const styles = {
|
|
|
134
134
|
cardBody: { display: 'flex', flexDirection: 'column', gap: 10, marginTop: 2 },
|
|
135
135
|
/** The one-line state a folded card shows in place of its body. */
|
|
136
136
|
cardSummary: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
/** The status indicator: filled when live, a hollow ring when not. */
|
|
138
|
+
dot: {
|
|
139
|
+
width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
|
|
140
|
+
border: '1.5px solid var(--dsw-alias-label-dimmed)',
|
|
141
|
+
background: 'transparent',
|
|
142
|
+
},
|
|
143
|
+
dotLive: {
|
|
144
|
+
borderColor: 'var(--dsw-alias-state-success-primary, #067647)',
|
|
145
|
+
background: 'var(--dsw-alias-state-success-primary, #067647)',
|
|
143
146
|
},
|
|
147
|
+
dotBusy: {
|
|
148
|
+
borderColor: 'var(--dsw-alias-state-warn-primary, #f5a524)',
|
|
149
|
+
background: 'var(--dsw-alias-state-warn-primary, #f5a524)',
|
|
150
|
+
},
|
|
151
|
+
name: { fontWeight: 600, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
|
|
144
152
|
statusLine: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
|
|
145
153
|
errorLine: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' },
|
|
146
154
|
successLine: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-success-primary)' },
|
|
@@ -234,6 +242,8 @@ const styles = {
|
|
|
234
242
|
padding: '8px 10px', borderRadius: 'var(--dsw-radius-md, 10px)',
|
|
235
243
|
background: 'var(--dsw-alias-bg-layer-1)', border: '0.5px solid var(--dsw-alias-border-l2)',
|
|
236
244
|
},
|
|
245
|
+
/** The metered windows under one account row: no border, just the meters. */
|
|
246
|
+
usageInline: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 },
|
|
237
247
|
modelRow: {
|
|
238
248
|
display: 'flex', alignItems: 'flex-start', gap: 8,
|
|
239
249
|
cursor: 'pointer', fontSize: 13, lineHeight: '20px',
|
|
@@ -275,13 +285,13 @@ const styles = {
|
|
|
275
285
|
modalActions: { display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 2 },
|
|
276
286
|
divider: { borderTop: '0.5px solid var(--dsw-alias-border-l2)' },
|
|
277
287
|
};
|
|
278
|
-
/** Status dot
|
|
279
|
-
function
|
|
288
|
+
/** Status dot style for one provider's connection state. */
|
|
289
|
+
function dotStyle(status) {
|
|
280
290
|
if (status.busy)
|
|
281
|
-
return
|
|
291
|
+
return styles.dotBusy;
|
|
282
292
|
if (status.loggedIn)
|
|
283
|
-
return
|
|
284
|
-
return
|
|
293
|
+
return styles.dotLive;
|
|
294
|
+
return {};
|
|
285
295
|
}
|
|
286
296
|
/** Localized label for one provider-owned detail row. */
|
|
287
297
|
function detailLabel(t, row) {
|
|
@@ -301,7 +311,14 @@ function windowAmount(window) {
|
|
|
301
311
|
return `${String(window.used)}/${String(window.limit)}`;
|
|
302
312
|
return '—';
|
|
303
313
|
}
|
|
304
|
-
/** One account's
|
|
314
|
+
/** One account's metered windows, rendered under its account row. */
|
|
315
|
+
function AccountUsage(props) {
|
|
316
|
+
const { entry, t } = props;
|
|
317
|
+
if (entry === undefined)
|
|
318
|
+
return null;
|
|
319
|
+
return (_jsxs("div", { style: styles.usageInline, children: [entry.windows.map((window) => (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [windowLabel(t, window), ' · ', windowAmount(window)] }), _jsx("span", { style: styles.modelMeta, children: window.resetsAt === undefined ? '' : formatReset(window.resetsAt, t) })] }), window.usedPercent !== undefined && (_jsx("div", { className: "dsh-subscription-meter", children: _jsx("span", { style: { width: `${String(Math.min(100, Math.max(0, window.usedPercent)))}%` }, "data-exhausted": window.usedPercent >= 90 ? 'true' : 'false' }) }))] }, `${entry.account}-${window.label}`))), entry.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageNone') }))] }));
|
|
320
|
+
}
|
|
321
|
+
/** The plan-and-usage block for the whole route. */
|
|
305
322
|
function UsageBlock(props) {
|
|
306
323
|
const { usage, error, t, busyAction, onRefresh } = props;
|
|
307
324
|
return (_jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.catalogTitle, children: t('usageTitle') }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: onRefresh, children: busyAction ? t('refreshing') : t('usageRefresh') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: t('usageError', { message: error }) }), usage.length === 0 && error === undefined && _jsx("p", { style: styles.statusLine, children: t('usageNone') }), usage.map((entry) => (_jsxs("div", { style: styles.usageAccount, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelMeta, children: [entry.plan === undefined ? entry.account : t('usagePlan', { plan: entry.plan }), entry.renewsAt === undefined ? '' : ` · ${t('usageRenews', { time: formatTime(entry.renewsAt) })}`] }), _jsx("span", { style: styles.modelMeta, children: t('usageFetched', { time: formatTime(entry.fetchedAt) }) })] }), entry.windows.map((window) => (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [windowLabel(t, window), ' · ', windowAmount(window)] }), _jsx("span", { style: styles.modelMeta, children: window.resetsAt === undefined ? '' : formatReset(window.resetsAt, t) })] }), window.usedPercent !== undefined && (_jsx("div", { className: "dsh-subscription-meter", children: _jsx("span", { style: { width: `${String(Math.min(100, Math.max(0, window.usedPercent)))}%` }, "data-exhausted": window.usedPercent >= 90 ? 'true' : 'false' }) }))] }, `${entry.account}-${window.label}`)))] }, entry.account)))] }));
|
|
@@ -500,9 +517,12 @@ function ProviderCard(props) {
|
|
|
500
517
|
return (_jsxs("div", { style: cardOpen ? { ...styles.card, ...styles.cardOpen } : styles.card, children: [_jsxs("div", { style: styles.cardHeader, role: "button", tabIndex: 0, "aria-expanded": cardOpen, onClick: toggleCardOpen, onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') {
|
|
501
518
|
event.preventDefault();
|
|
502
519
|
toggleCardOpen();
|
|
503
|
-
} }, children: [_jsx("span", { style: { ...styles.dot,
|
|
504
|
-
|
|
505
|
-
|
|
520
|
+
} }, children: [_jsx("span", { style: { ...styles.dot, ...dotStyle(status) }, "aria-hidden": "true" }), _jsx("span", { style: styles.name, children: status.displayName }), status.loggedIn && _jsx("span", { style: styles.badgeDefault, children: t('badgeLive') }), _jsx("span", { style: styles.cardSummary, children: summaryLine }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, "aria-expanded": cardOpen, "aria-controls": `dsh-subscription-card-${status.provider}`, onClick: (event) => { event.stopPropagation(); toggleCardOpen(); }, children: cardOpen ? t('collapse') : t('edit') })] }), _jsxs("div", { id: `dsh-subscription-card-${status.provider}`, style: cardOpen ? styles.cardBody : { ...styles.cardBody, display: 'none' }, children: [connectable && !status.loggedIn && !busy && _jsx("p", { style: styles.statusLine, children: t('statusSignIn') }), status.lastError !== undefined && (_jsx("p", { className: "dsh-subscription-error", children: t('lastError', { message: status.lastError }) })), message?.tone === 'error' && _jsx("p", { className: "dsh-subscription-error", children: message.text }), message?.tone === 'success' && _jsx("p", { className: "dsh-subscription-success", children: message.text }), accounts.map((account) => {
|
|
521
|
+
const usageEntry = (status.usage ?? []).find((entry) => entry.account === account.account);
|
|
522
|
+
return (_jsxs("div", { style: styles.accountRow, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("button", { type: "button", style: styles.starButton, title: account.isDefault ? t('defaultBadge') : t('makeDefault'), "aria-label": account.isDefault ? t('defaultBadge') : t('makeDefault'), disabled: busyAction || account.isDefault, onClick: () => { props.onSetDefault(account.account); }, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: account.label }), account.isDefault && _jsx("span", { style: styles.badgeDefault, children: t('defaultBadge') }), account.needsLogin === true && (_jsx("span", { style: styles.badgeWarn, title: t('accountNeedsLoginHint'), children: t('accountNeedsLogin') })), usageEntry?.plan !== undefined && (_jsx("span", { style: styles.badge, children: t('usagePlan', { plan: usageEntry.plan }) })), _jsx("span", { style: styles.statusLine, children: account.expired
|
|
523
|
+
? t('accountExpired')
|
|
524
|
+
: t('accountExpires', { time: formatTime(account.expiresAt) }) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: () => { props.onLogout(account.account); }, children: t('logout') })] }), _jsx(AccountUsage, { entry: usageEntry, t: t })] }, account.account));
|
|
525
|
+
}), status.loggedIn && (_jsxs("div", { style: styles.catalog, children: [_jsxs("div", { style: styles.catalogHeader, children: [_jsxs("button", { type: "button", className: "dsh-subscription-disclosure", style: styles.disclosure, "aria-expanded": modelsOpen, "aria-controls": `dsh-subscription-models-${status.provider}`, onClick: toggleModelsOpen, children: [_jsx("span", { style: styles.chevron, "aria-hidden": "true", children: modelsOpen ? '▾' : '▸' }), _jsx("span", { style: styles.catalogTitle, children: t('modelsTitle') })] }), _jsxs("span", { style: styles.catalogMeta, children: [t('modelsCount', { count: models.length }), ' · ', liveCatalog ? t('modelsLive') : t('modelsBuiltIn'), hiddenCount === 0 ? '' : ` ${t('modelsHiddenCount', { count: hiddenCount })}`] }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: props.onRefreshModels, children: busyAction ? t('refreshing') : t('refresh') }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busyAction || !connectable, title: t('modelsAddHint'), onClick: () => { setAdding((open) => !open); }, children: t('modelsAdd') })] }), adding && (_jsxs("form", { style: styles.addRow, onSubmit: (event) => {
|
|
506
526
|
event.preventDefault();
|
|
507
527
|
const id = draft.trim();
|
|
508
528
|
if (id.length === 0)
|
|
@@ -525,7 +545,7 @@ function ProviderCard(props) {
|
|
|
525
545
|
? { ...styles.modelRowGrow, cursor: 'pointer' }
|
|
526
546
|
: { ...styles.modelRowGrow, cursor: 'pointer', color: 'var(--dsw-alias-label-dimmed)' }, title: model.visible ? t('modelsHidden') : t('modelsShow'), children: [_jsx("input", { type: "checkbox", className: "dsh-subscription-switch", role: "switch", checked: model.visible, disabled: busyAction, "aria-label": `${t('modelsShow')} · ${model.name}`, onChange: (event) => { props.onSetModelVisible(model.id, event.target.checked); } }), _jsxs("span", { style: styles.modelBody, children: [_jsxs("span", { style: styles.modelName, children: [model.name, model.source === undefined ? null : _jsx("span", { style: styles.badge, children: model.source })] }), _jsxs("span", { style: styles.modelMeta, children: [model.id, ' · ', t('modelsContext', { tokens: formatTokens(model.contextWindow) }), model.defaultEffort === undefined ? '' : ` · ${model.defaultEffort}`, model.inputModalities === undefined || model.inputModalities.length === 0
|
|
527
547
|
? ''
|
|
528
|
-
: ` · ${model.inputModalities.join('/')}`] })] })] }), model.userAdded === true && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.removeButton, disabled: busyAction, title: t('modelsRemove'), "aria-label": `${t('modelsRemove')} · ${model.name}`, onClick: () => { props.onRemoveModel(model.id); }, children: "\u00D7" }))] }, model.id))), models.length === 0 && (_jsx("p", { style: styles.statusLine, children: status.provider === 'auto' ? t('modelsAutoChainEmpty') : t('modelsEmpty') }))] }))] })),
|
|
548
|
+
: ` · ${model.inputModalities.join('/')}`] })] })] }), model.userAdded === true && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.removeButton, disabled: busyAction, title: t('modelsRemove'), "aria-label": `${t('modelsRemove')} · ${model.name}`, onClick: () => { props.onRemoveModel(model.id); }, children: "\u00D7" }))] }, model.id))), models.length === 0 && (_jsx("p", { style: styles.statusLine, children: status.provider === 'auto' ? t('modelsAutoChainEmpty') : t('modelsEmpty') }))] }))] })), connectable && (_jsxs("div", { style: styles.actions, children: [!busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: props.onLogin, children: accounts.length === 0 ? t('connect') : t('addAccount') })), !busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: () => { setManageOpen(true); }, children: t('manage') })), busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: props.onCancel, children: t('cancel') }))] })), connectable && accounts.length > 0 && _jsx("p", { style: styles.statusLine, children: t('hint') }), busy && status.pending !== undefined && (_jsx(PendingLogin, { prompt: status.pending, t: t, copied: copied, onCopy: (code) => { void copyCode(code); } }))] }), _jsxs("dialog", { ref: dialog, className: "dsh-subscription-manager", "aria-labelledby": `dsh-subscription-manage-${status.provider}`,
|
|
529
549
|
// A closed `<dialog>` is hidden by the user-agent stylesheet, and an
|
|
530
550
|
// inline `display` would override that rule — so the open state has to
|
|
531
551
|
// be restated here rather than left to `showModal()` alone.
|