@jxgame2020/dsh-token-quota 0.1.39 → 0.1.41
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/client.js +466 -133
- package/lib/index.js +184 -29
- package/lib/types/client/TokenQuotaPanel.d.ts +4 -2
- package/lib/types/client/TokenQuotaPanel.js +10 -8
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/index.js +50 -1
- package/lib/types/client/locales.d.ts +26 -0
- package/lib/types/client/locales.js +26 -0
- package/lib/types/client/mic.d.ts +19 -0
- package/lib/types/client/mic.js +158 -0
- package/lib/types/client/store.d.ts +5 -1
- package/lib/types/client/store.js +2 -0
- package/lib/types/index.d.ts +19 -1
- package/lib/types/index.js +192 -28
- package/lib/types/types.d.ts +20 -4
- package/lib/types/types.js +6 -6
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -27,6 +27,12 @@ const TOKEN_QUOTA_DEFAULT_BALANCE = {
|
|
|
27
27
|
enabled: true,
|
|
28
28
|
pollMinutes: 5
|
|
29
29
|
};
|
|
30
|
+
const TOKEN_QUOTA_DEFAULT_TRANSCRIBE = {
|
|
31
|
+
enabled: false,
|
|
32
|
+
baseURL: "https://api.siliconflow.cn/v1",
|
|
33
|
+
apiKeyEnv: "SILICONFLOW_API_KEY",
|
|
34
|
+
model: "XingChenAGI/XingChenASR-V3.2-Ultra"
|
|
35
|
+
};
|
|
30
36
|
/** Default music settings: silent until the user flips the panel switch. */
|
|
31
37
|
const TOKEN_QUOTA_DEFAULT_MUSIC = {
|
|
32
38
|
enabled: false,
|
|
@@ -244,7 +250,13 @@ const TOKEN_QUOTA_SETTINGS_SCHEMA = z.object({
|
|
|
244
250
|
balance: z.object({
|
|
245
251
|
enabled: z.boolean().default(TOKEN_QUOTA_DEFAULT_BALANCE.enabled),
|
|
246
252
|
pollMinutes: z.number().min(1).max(120).default(TOKEN_QUOTA_DEFAULT_BALANCE.pollMinutes)
|
|
247
|
-
}).default({ ...TOKEN_QUOTA_DEFAULT_BALANCE })
|
|
253
|
+
}).default({ ...TOKEN_QUOTA_DEFAULT_BALANCE }),
|
|
254
|
+
transcribe: z.object({
|
|
255
|
+
enabled: z.boolean().default(TOKEN_QUOTA_DEFAULT_TRANSCRIBE.enabled),
|
|
256
|
+
baseURL: z.string().default(TOKEN_QUOTA_DEFAULT_TRANSCRIBE.baseURL),
|
|
257
|
+
apiKeyEnv: z.string().default(TOKEN_QUOTA_DEFAULT_TRANSCRIBE.apiKeyEnv),
|
|
258
|
+
model: z.string().default(TOKEN_QUOTA_DEFAULT_TRANSCRIBE.model)
|
|
259
|
+
}).default({ ...TOKEN_QUOTA_DEFAULT_TRANSCRIBE })
|
|
248
260
|
});
|
|
249
261
|
/** Two-digit zero-pad helper. */
|
|
250
262
|
function pad2(value) {
|
|
@@ -276,23 +288,8 @@ function defaultStoragePath() {
|
|
|
276
288
|
function usageTokens(usage) {
|
|
277
289
|
return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0) + usage.outputTokens;
|
|
278
290
|
}
|
|
279
|
-
/**
|
|
280
|
-
|
|
281
|
-
*
|
|
282
|
-
* Mount it beside the other rows (`@jxgame2020/dsh-token-quota`) and write
|
|
283
|
-
* per-model limits through the `token-quota` settings namespace.
|
|
284
|
-
*/
|
|
285
|
-
/**
|
|
286
|
-
* Providers the host can query for account balance, keyed by route key. Each
|
|
287
|
-
* entry names the credential reference (environment-variable name), the
|
|
288
|
-
* balance endpoint base, and the optional `*_BASE_URL` override variable.
|
|
289
|
-
* Directory providers outside this map render as "not integrated for query".
|
|
290
|
-
*/
|
|
291
|
-
const BALANCE_SOURCES = { deepseek: {
|
|
292
|
-
keyEnv: "DEEPSEEK_API_KEY",
|
|
293
|
-
baseURL: "https://api.deepseek.com",
|
|
294
|
-
baseEnv: "DEEPSEEK_BASE_URL"
|
|
295
|
-
} };
|
|
291
|
+
/** DeepSeek's public API host; used to recognise DeepSeek-family providers. */
|
|
292
|
+
const DEEPSEEK_BALANCE_HOST = "api.deepseek.com";
|
|
296
293
|
var TokenQuotaService = class TokenQuotaService extends Service {
|
|
297
294
|
static Config = z.object({ storagePath: z.string().default("") });
|
|
298
295
|
storagePath;
|
|
@@ -315,7 +312,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
315
312
|
music: { ...TOKEN_QUOTA_DEFAULT_MUSIC },
|
|
316
313
|
order: [],
|
|
317
314
|
notes: [],
|
|
318
|
-
balance: { ...TOKEN_QUOTA_DEFAULT_BALANCE }
|
|
315
|
+
balance: { ...TOKEN_QUOTA_DEFAULT_BALANCE },
|
|
316
|
+
transcribe: { ...TOKEN_QUOTA_DEFAULT_TRANSCRIBE }
|
|
319
317
|
});
|
|
320
318
|
/** Whether update checks are enabled (mirrors the settings document). */
|
|
321
319
|
checkUpdates = true;
|
|
@@ -353,6 +351,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
353
351
|
disposeRouteClear;
|
|
354
352
|
/** Disposer for the optional balance-refresh route (`POST /token-quota/refresh-balance`). */
|
|
355
353
|
disposeRouteBalance;
|
|
354
|
+
/** Disposer for the voice transcription route (`POST /token-quota/transcribe`). */
|
|
355
|
+
disposeRouteTranscribe;
|
|
356
356
|
/** Disposer for the optional music-event route (`GET /token-quota/events`, SSE). */
|
|
357
357
|
disposeRouteEvents;
|
|
358
358
|
/** Live SSE subscribers receiving streamed music actions. */
|
|
@@ -383,6 +383,12 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
383
383
|
enabled: doc.balance?.enabled ?? TOKEN_QUOTA_DEFAULT_BALANCE.enabled,
|
|
384
384
|
pollMinutes: typeof doc.balance?.pollMinutes === "number" ? Math.max(1, Math.min(120, Math.round(doc.balance.pollMinutes))) : TOKEN_QUOTA_DEFAULT_BALANCE.pollMinutes
|
|
385
385
|
},
|
|
386
|
+
transcribe: {
|
|
387
|
+
enabled: doc.transcribe?.enabled ?? TOKEN_QUOTA_DEFAULT_TRANSCRIBE.enabled,
|
|
388
|
+
baseURL: typeof doc.transcribe?.baseURL === "string" && doc.transcribe.baseURL !== "" ? doc.transcribe.baseURL.replace(/\/+$/, "") : TOKEN_QUOTA_DEFAULT_TRANSCRIBE.baseURL,
|
|
389
|
+
apiKeyEnv: typeof doc.transcribe?.apiKeyEnv === "string" && doc.transcribe.apiKeyEnv !== "" ? doc.transcribe.apiKeyEnv : TOKEN_QUOTA_DEFAULT_TRANSCRIBE.apiKeyEnv,
|
|
390
|
+
model: typeof doc.transcribe?.model === "string" && doc.transcribe.model !== "" ? doc.transcribe.model : TOKEN_QUOTA_DEFAULT_TRANSCRIBE.model
|
|
391
|
+
},
|
|
386
392
|
notes: Array.isArray(doc.notes) ? doc.notes.slice(0, 5).flatMap((note) => {
|
|
387
393
|
if (typeof note.id !== "string") return [];
|
|
388
394
|
return [{
|
|
@@ -525,7 +531,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
525
531
|
if (typeof parsed === "object" && parsed !== null && typeof parsed.provider === "string") provider = parsed.provider;
|
|
526
532
|
}
|
|
527
533
|
} catch {}
|
|
528
|
-
(provider !== void 0
|
|
534
|
+
(provider !== void 0 ? this.refreshProviderBalance(provider) : this.refreshAllBalances()).finally(() => {
|
|
529
535
|
const body = JSON.stringify(this.readSnapshot());
|
|
530
536
|
res.writeHead(200, {
|
|
531
537
|
"content-type": "application/json",
|
|
@@ -536,6 +542,32 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
536
542
|
});
|
|
537
543
|
}
|
|
538
544
|
});
|
|
545
|
+
this.disposeRouteTranscribe = server.register({
|
|
546
|
+
kind: "exact",
|
|
547
|
+
path: "/token-quota/transcribe",
|
|
548
|
+
handler: (req, res) => {
|
|
549
|
+
if (req.method !== "POST") {
|
|
550
|
+
res.writeHead(405);
|
|
551
|
+
res.end();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this.handleTranscribe(req).then((result) => {
|
|
555
|
+
if (result.ok) {
|
|
556
|
+
res.writeHead(200, {
|
|
557
|
+
"content-type": "application/json",
|
|
558
|
+
"cache-control": "no-store"
|
|
559
|
+
});
|
|
560
|
+
res.end(JSON.stringify({ text: result.text }));
|
|
561
|
+
} else {
|
|
562
|
+
res.writeHead(result.status);
|
|
563
|
+
res.end(result.message);
|
|
564
|
+
}
|
|
565
|
+
}, (error) => {
|
|
566
|
+
res.writeHead(500);
|
|
567
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
});
|
|
539
571
|
this.disposeRouteClear = server.register({
|
|
540
572
|
kind: "exact",
|
|
541
573
|
path: "/token-quota/clear-log",
|
|
@@ -619,7 +651,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
619
651
|
return this.upgradeCheck;
|
|
620
652
|
}
|
|
621
653
|
async doRefreshUpgrade() {
|
|
622
|
-
const current = "0.1.
|
|
654
|
+
const current = "0.1.41";
|
|
623
655
|
let latest;
|
|
624
656
|
try {
|
|
625
657
|
const response = await fetch(REGISTRY_LATEST_URL, {
|
|
@@ -692,9 +724,46 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
692
724
|
};
|
|
693
725
|
schedule();
|
|
694
726
|
}
|
|
695
|
-
/** Refresh every provider
|
|
727
|
+
/** Refresh every DeepSeek-family provider found in the model directory. */
|
|
696
728
|
refreshAllBalances() {
|
|
697
|
-
return Promise.all(
|
|
729
|
+
return Promise.all([...this.discoverBalanceSources().values()].map((source) => this.refreshProviderBalance(source.provider, source))).then(() => void 0);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Discover every provider in the model directory that looks like DeepSeek,
|
|
733
|
+
* together with its own credential reference and endpoint base. This is what
|
|
734
|
+
* decides which providers are queried: a provider qualifies when its base URL
|
|
735
|
+
* points at DeepSeek's API host or its credential reference is named with the
|
|
736
|
+
* `DEEPSEEK` prefix, and it must declare an `apiKeyEnv`.
|
|
737
|
+
*/
|
|
738
|
+
discoverBalanceSources() {
|
|
739
|
+
const found = /* @__PURE__ */ new Map();
|
|
740
|
+
const llm = this.ctx.get("llm");
|
|
741
|
+
const settings = this.ctx.get("settings");
|
|
742
|
+
const fallback = () => ({
|
|
743
|
+
provider: "deepseek",
|
|
744
|
+
keyEnv: "DEEPSEEK_API_KEY",
|
|
745
|
+
baseURL: process.env.DEEPSEEK_BASE_URL ?? `https://${DEEPSEEK_BALANCE_HOST}`
|
|
746
|
+
});
|
|
747
|
+
if (llm?.listConfigurableProviders === void 0) {
|
|
748
|
+
found.set("deepseek", fallback());
|
|
749
|
+
return found;
|
|
750
|
+
}
|
|
751
|
+
for (const entry of llm.listConfigurableProviders()) try {
|
|
752
|
+
let profile = settings?.get(entry.settingsNs);
|
|
753
|
+
for (const segment of entry.settingsPath ?? []) profile = profile !== null && typeof profile === "object" ? profile[segment] : void 0;
|
|
754
|
+
if (profile === null || typeof profile !== "object") continue;
|
|
755
|
+
const record = profile;
|
|
756
|
+
const keyEnv = typeof record.apiKeyEnv === "string" && record.apiKeyEnv !== "" ? record.apiKeyEnv : void 0;
|
|
757
|
+
const baseURL = typeof record.baseURL === "string" && record.baseURL !== "" ? record.baseURL : void 0;
|
|
758
|
+
if (!(baseURL !== void 0 && baseURL.includes(DEEPSEEK_BALANCE_HOST) || keyEnv !== void 0 && keyEnv.startsWith("DEEPSEEK") || entry.provider.startsWith("deepseek")) || keyEnv === void 0) continue;
|
|
759
|
+
found.set(entry.provider, {
|
|
760
|
+
provider: entry.provider,
|
|
761
|
+
keyEnv,
|
|
762
|
+
baseURL: baseURL ?? `https://${DEEPSEEK_BALANCE_HOST}`
|
|
763
|
+
});
|
|
764
|
+
} catch {}
|
|
765
|
+
if (found.size === 0) found.set("deepseek", fallback());
|
|
766
|
+
return found;
|
|
698
767
|
}
|
|
699
768
|
/**
|
|
700
769
|
* Refresh one provider's cached account balance. The API key is resolved
|
|
@@ -702,9 +771,9 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
702
771
|
* environment as a fallback, so the Models page key is used with nothing to
|
|
703
772
|
* type. A failed fetch keeps the previous value and marks it as an error.
|
|
704
773
|
*/
|
|
705
|
-
async refreshProviderBalance(provider) {
|
|
706
|
-
const
|
|
707
|
-
if (
|
|
774
|
+
async refreshProviderBalance(provider, source) {
|
|
775
|
+
const resolved = source ?? this.discoverBalanceSources().get(provider);
|
|
776
|
+
if (resolved === void 0) return;
|
|
708
777
|
const existing = this.balanceFetches.get(provider);
|
|
709
778
|
if (existing !== void 0) return existing;
|
|
710
779
|
const run = async () => {
|
|
@@ -712,9 +781,9 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
712
781
|
const credentials = this.ctx.get("credentials");
|
|
713
782
|
let key;
|
|
714
783
|
if (credentials !== void 0) try {
|
|
715
|
-
key = (await credentials.resolve(
|
|
784
|
+
key = (await credentials.resolve(resolved.keyEnv))?.value;
|
|
716
785
|
} catch {}
|
|
717
|
-
if (key === void 0 || key === "") key = process.env[
|
|
786
|
+
if (key === void 0 || key === "") key = process.env[resolved.keyEnv];
|
|
718
787
|
if (key === void 0 || key === "") {
|
|
719
788
|
this.balances.set(provider, {
|
|
720
789
|
provider,
|
|
@@ -726,7 +795,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
726
795
|
});
|
|
727
796
|
return;
|
|
728
797
|
}
|
|
729
|
-
const base =
|
|
798
|
+
const base = resolved.baseURL;
|
|
730
799
|
try {
|
|
731
800
|
const controller = new AbortController();
|
|
732
801
|
const timer = setTimeout(() => {
|
|
@@ -783,6 +852,91 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
783
852
|
this.balanceFetches.set(provider, promise);
|
|
784
853
|
return promise;
|
|
785
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* Forward a recorded audio blob to the configured OpenAI-compatible
|
|
857
|
+
* `/audio/transcriptions` endpoint and return the recognised text. The API
|
|
858
|
+
* key resolves through the credential seam (Models page) with an environment
|
|
859
|
+
* fallback; the request body is the raw audio and the content type carries
|
|
860
|
+
* the filename for the multipart form.
|
|
861
|
+
*/
|
|
862
|
+
async handleTranscribe(req) {
|
|
863
|
+
const settings = this.settingsSource().transcribe;
|
|
864
|
+
if (!settings.enabled) return {
|
|
865
|
+
ok: false,
|
|
866
|
+
status: 403,
|
|
867
|
+
message: "transcription is disabled"
|
|
868
|
+
};
|
|
869
|
+
const chunks = [];
|
|
870
|
+
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
871
|
+
const audio = Buffer.concat(chunks);
|
|
872
|
+
if (audio.length === 0) return {
|
|
873
|
+
ok: false,
|
|
874
|
+
status: 400,
|
|
875
|
+
message: "empty audio payload"
|
|
876
|
+
};
|
|
877
|
+
const credentials = this.ctx.get("credentials");
|
|
878
|
+
let key;
|
|
879
|
+
if (credentials !== void 0) try {
|
|
880
|
+
key = (await credentials.resolve(settings.apiKeyEnv))?.value;
|
|
881
|
+
} catch {}
|
|
882
|
+
if (key === void 0 || key === "") key = process.env[settings.apiKeyEnv];
|
|
883
|
+
if (key === void 0 || key === "") return {
|
|
884
|
+
ok: false,
|
|
885
|
+
status: 400,
|
|
886
|
+
message: `no API key for credential reference "${settings.apiKeyEnv}"`
|
|
887
|
+
};
|
|
888
|
+
const contentType = req.headers["content-type"] ?? "application/octet-stream";
|
|
889
|
+
const disposition = typeof req.headers["content-disposition"] === "string" ? req.headers["content-disposition"] : "";
|
|
890
|
+
const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? "recording.webm";
|
|
891
|
+
const form = new FormData();
|
|
892
|
+
form.append("model", settings.model);
|
|
893
|
+
form.append("file", new Blob([new Uint8Array(audio)], { type: contentType }), filename);
|
|
894
|
+
try {
|
|
895
|
+
const controller = new AbortController();
|
|
896
|
+
const timer = setTimeout(() => {
|
|
897
|
+
controller.abort();
|
|
898
|
+
}, 6e4);
|
|
899
|
+
let res;
|
|
900
|
+
try {
|
|
901
|
+
res = await fetch(`${settings.baseURL}/audio/transcriptions`, {
|
|
902
|
+
method: "POST",
|
|
903
|
+
headers: {
|
|
904
|
+
authorization: `Bearer ${key}`,
|
|
905
|
+
accept: "application/json"
|
|
906
|
+
},
|
|
907
|
+
body: form,
|
|
908
|
+
signal: controller.signal
|
|
909
|
+
});
|
|
910
|
+
} finally {
|
|
911
|
+
clearTimeout(timer);
|
|
912
|
+
}
|
|
913
|
+
if (!res.ok) {
|
|
914
|
+
const detail = await res.text().catch(() => "");
|
|
915
|
+
return {
|
|
916
|
+
ok: false,
|
|
917
|
+
status: res.status,
|
|
918
|
+
message: `transcription endpoint HTTP ${res.status}${detail !== "" ? `: ${detail.slice(0, 300)}` : ""}`
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const data = await res.json();
|
|
922
|
+
const text = typeof data?.text === "string" ? data.text : "";
|
|
923
|
+
if (text === "") return {
|
|
924
|
+
ok: false,
|
|
925
|
+
status: 502,
|
|
926
|
+
message: "transcription endpoint returned no text"
|
|
927
|
+
};
|
|
928
|
+
return {
|
|
929
|
+
ok: true,
|
|
930
|
+
text
|
|
931
|
+
};
|
|
932
|
+
} catch (error) {
|
|
933
|
+
return {
|
|
934
|
+
ok: false,
|
|
935
|
+
status: 502,
|
|
936
|
+
message: error instanceof Error ? error.message : String(error)
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
}
|
|
786
940
|
/** Read the full per-cycle usage history (log dialog data). */
|
|
787
941
|
readLog() {
|
|
788
942
|
this.rollCycleIfNeeded();
|
|
@@ -1087,6 +1241,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
1087
1241
|
if (this.disposeRouteCheck !== void 0) this.disposeRouteCheck();
|
|
1088
1242
|
if (this.disposeRouteClear !== void 0) this.disposeRouteClear();
|
|
1089
1243
|
if (this.disposeRouteBalance !== void 0) this.disposeRouteBalance();
|
|
1244
|
+
if (this.disposeRouteTranscribe !== void 0) this.disposeRouteTranscribe();
|
|
1090
1245
|
if (this.balanceTimer !== void 0) {
|
|
1091
1246
|
clearTimeout(this.balanceTimer);
|
|
1092
1247
|
this.balanceTimer = void 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
2
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
3
|
-
import type { TokenQuotaFullAction, TokenQuotaMusicStyle, TokenQuotaNote, TokenQuotaReset } from '@jxgame2020/dsh-token-quota/types';
|
|
3
|
+
import type { TokenQuotaFullAction, TokenQuotaMusicStyle, TokenQuotaNote, TokenQuotaReset, TokenQuotaTranscribeSettings } from '@jxgame2020/dsh-token-quota/types';
|
|
4
4
|
import type { createTokenQuotaPanelStore } from './store.ts';
|
|
5
5
|
/** Injected business face: data loading and mutations wired in `apply`. */
|
|
6
6
|
export interface TokenQuotaPanelInjected {
|
|
@@ -36,6 +36,8 @@ export interface TokenQuotaPanelInjected {
|
|
|
36
36
|
setMusicStyle: (style: TokenQuotaMusicStyle) => void;
|
|
37
37
|
/** Toggle whether the soundtrack follows only the current session. */
|
|
38
38
|
setMusicOnlyCurrentSession: (only: boolean) => void;
|
|
39
|
+
/** Persist the voice transcription settings (endpoint, key ref, model). */
|
|
40
|
+
setTranscribeSettings: (settings: TokenQuotaTranscribeSettings) => void;
|
|
39
41
|
/** Ask the Host to check the registry right now, then refresh the snapshot. */
|
|
40
42
|
checkUpdatesNow: () => void;
|
|
41
43
|
/** Ask the Host to clear log entries before a date, then refresh the log. */
|
|
@@ -48,5 +50,5 @@ export type TokenQuotaPanelComponentProps = PropsRuntime<'shell.overlay'> & Prop
|
|
|
48
50
|
* @param props - composed slot props.
|
|
49
51
|
* @returns the panel element tree.
|
|
50
52
|
*/
|
|
51
|
-
export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, saveNotes, setBalanceSettings, refreshBalance, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
|
|
53
|
+
export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, saveNotes, setBalanceSettings, refreshBalance, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, setTranscribeSettings, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
|
|
52
54
|
//# sourceMappingURL=TokenQuotaPanel.d.ts.map
|
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
* callbacks.
|
|
11
11
|
*/
|
|
12
12
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
-
import {
|
|
13
|
+
import { TOKEN_QUOTA_MAX_NOTES } from '@jxgame2020/dsh-token-quota/types';
|
|
14
14
|
import { mergeModelRows, reorderKeys } from "./store.js";
|
|
15
15
|
import css from './TokenQuotaPanel.module.css';
|
|
16
16
|
/** Compact a token count for display. */
|
|
@@ -149,7 +149,7 @@ function loadPanelPos() {
|
|
|
149
149
|
* @param props - composed slot props.
|
|
150
150
|
* @returns the panel element tree.
|
|
151
151
|
*/
|
|
152
|
-
export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, saveNotes, setBalanceSettings, refreshBalance, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }) {
|
|
152
|
+
export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, saveNotes, setBalanceSettings, refreshBalance, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, setTranscribeSettings, useStore, actions, useSessions, }) {
|
|
153
153
|
const [collapsed, setCollapsed] = useState(false);
|
|
154
154
|
const [drafts, setDrafts] = useState({});
|
|
155
155
|
const [editingKey, setEditingKey] = useState(null);
|
|
@@ -357,6 +357,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
357
357
|
const musicVolume = useStore(s => s.musicVolume);
|
|
358
358
|
const musicStyle = useStore(s => s.musicStyle);
|
|
359
359
|
const musicOnlyCurrentSession = useStore(s => s.musicOnlyCurrentSession);
|
|
360
|
+
const transcribe = useStore(s => s.transcribe);
|
|
360
361
|
const upgrade = useStore(s => s.upgrade);
|
|
361
362
|
const upgradeDismissed = useStore(s => s.upgradeDismissed);
|
|
362
363
|
const checkingUpdates = useStore(s => s.checkingUpdates);
|
|
@@ -543,9 +544,10 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
543
544
|
list.push('deepseek');
|
|
544
545
|
return list;
|
|
545
546
|
}, [groups]);
|
|
546
|
-
// Balance by provider key
|
|
547
|
-
//
|
|
548
|
-
//
|
|
547
|
+
// Balance by provider key. The Host returns a balances entry for every
|
|
548
|
+
// provider it can query (DeepSeek-family), so a directory provider with no
|
|
549
|
+
// entry is "not integrated for query" — except during the very first fetch,
|
|
550
|
+
// when the whole list is still empty and reads as "loading" instead.
|
|
549
551
|
const balanceByProvider = useMemo(() => {
|
|
550
552
|
const map = new Map();
|
|
551
553
|
for (const provider of providers) {
|
|
@@ -553,7 +555,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
553
555
|
if (bal !== undefined) {
|
|
554
556
|
map.set(provider, bal);
|
|
555
557
|
}
|
|
556
|
-
else if (
|
|
558
|
+
else if (balances.length === 0) {
|
|
557
559
|
map.set(provider, 'loading');
|
|
558
560
|
}
|
|
559
561
|
else {
|
|
@@ -789,7 +791,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
789
791
|
: _jsx(_Fragment, { children: quotaText(row) });
|
|
790
792
|
})(), _jsx("button", { type: "button", className: css.gearBtn, title: t('setLimitHint'), onClick: () => { setEditingKey(editing ? null : row.key); }, children: "\u2699" })] })] }), _jsx("div", { className: css.bar, children: _jsx("div", { className: barClass, style: { width: `${pct}%` } }) }), editing && (_jsxs("div", { className: css.controls, children: [_jsx("input", { className: css.input, type: "number", min: 0, step: 10000, placeholder: t('limitPlaceholder'), value: drafts[row.key] ?? '', onChange: (event) => { setDrafts(prev => ({ ...prev, [row.key]: event.target.value })); }, onKeyDown: (event) => { if (event.key === 'Enter')
|
|
791
793
|
commitLimit(row); } }), _jsx("button", { type: "button", className: css.save, onClick: () => { commitLimit(row); }, children: t('save') })] }))] }, row.key));
|
|
792
|
-
})] })] }), dialogOpen && (_jsxs("div", { ref: settingsRef, className: `${css.dialog} ${css.settingsDialog}`, style: settingsPos !== null ? { left: settingsPos.x, top: settingsPos.y, transform: 'none' } : undefined, children: [_jsxs("div", { className: css.dialogHeader, onPointerDown: (event) => { beginDrag(event, settingsRef.current, setSettingsPos); }, children: [_jsx("div", { className: css.dialogTitle, children: t('settingsTitle') }), _jsx("button", { type: "button", className: css.dialogClose, title: t('close'), onClick: () => { actions.setDialogOpen(false); }, onPointerDown: (event) => { event.stopPropagation(); }, children: "\u00D7" })] }), _jsxs("div", { className: css.dialogTabs, role: "tablist", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'quota', className: `${css.dialogTab}${settingsTab === 'quota' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('quota'); }, children: t('settingsTabQuota') }), _jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'music', className: `${css.dialogTab}${settingsTab === 'music' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('music'); }, children: t('settingsTabMusic') }), _jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'other', className: `${css.dialogTab}${settingsTab === 'other' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('other'); }, children: t('settingsTabOther') })] }), settingsTab === 'quota' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.dialogSection, children: [_jsx("div", { className: css.dialogLabel, children: t('monitorLabel') }), _jsx("div", { className: css.monitorHint, children: t('monitorHint') }), _jsx("div", { className: css.monitorList, children: allModels.map(model => (_jsxs("label", { className: css.monitorRow, children: [_jsx("input", { type: "checkbox", checked: monitored === null || monitored.includes(model.key), onChange: (event) => {
|
|
794
|
+
})] })] }), dialogOpen && (_jsxs("div", { ref: settingsRef, className: `${css.dialog} ${css.settingsDialog}`, style: settingsPos !== null ? { left: settingsPos.x, top: settingsPos.y, transform: 'none' } : undefined, children: [_jsxs("div", { className: css.dialogHeader, onPointerDown: (event) => { beginDrag(event, settingsRef.current, setSettingsPos); }, children: [_jsx("div", { className: css.dialogTitle, children: t('settingsTitle') }), _jsx("button", { type: "button", className: css.dialogClose, title: t('close'), onClick: () => { actions.setDialogOpen(false); }, onPointerDown: (event) => { event.stopPropagation(); }, children: "\u00D7" })] }), _jsxs("div", { className: css.dialogTabs, role: "tablist", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'quota', className: `${css.dialogTab}${settingsTab === 'quota' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('quota'); }, children: t('settingsTabQuota') }), _jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'music', className: `${css.dialogTab}${settingsTab === 'music' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('music'); }, children: t('settingsTabMusic') }), _jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'transcribe', className: `${css.dialogTab}${settingsTab === 'transcribe' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('transcribe'); }, children: t('settingsTabTranscribe') }), _jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'other', className: `${css.dialogTab}${settingsTab === 'other' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('other'); }, children: t('settingsTabOther') })] }), settingsTab === 'quota' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.dialogSection, children: [_jsx("div", { className: css.dialogLabel, children: t('monitorLabel') }), _jsx("div", { className: css.monitorHint, children: t('monitorHint') }), _jsx("div", { className: css.monitorList, children: allModels.map(model => (_jsxs("label", { className: css.monitorRow, children: [_jsx("input", { type: "checkbox", checked: monitored === null || monitored.includes(model.key), onChange: (event) => {
|
|
793
795
|
const base = monitored === null ? allModels.map(m => m.key) : monitored;
|
|
794
796
|
const next = new Set(base);
|
|
795
797
|
if (event.target.checked)
|
|
@@ -816,7 +818,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
816
818
|
hour: reset?.hour ?? 0,
|
|
817
819
|
minute: Number(event.target.value),
|
|
818
820
|
});
|
|
819
|
-
}, children: Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (_jsxs("option", { value: minute, children: [String(minute).padStart(2, '0'), " \u5206"] }, minute))) })] })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: dimWhenIdle, onChange: (event) => { setDimWhenIdle(event.target.checked); } }), _jsx("span", { children: t('dimWhenIdleLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('dimWhenIdleHint') })] })] })), settingsTab === 'music' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: musicEnabled, onChange: (event) => { setMusicEnabled(event.target.checked); } }), _jsx("span", { children: t('musicLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('musicHint') })] }), musicEnabled && (_jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicVolume') }), _jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: musicVolume, onChange: (event) => { setMusicVolume(Number(event.target.value)); } })] }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicStyle') }), _jsxs("select", { value: musicStyle, onChange: (event) => { setMusicStyle(event.target.value); }, children: [_jsx("option", { value: "major", children: t('musicStyleMajor') }), _jsx("option", { value: "minor", children: t('musicStyleMinor') }), _jsx("option", { value: "pentatonic", children: t('musicStylePentatonic') })] })] }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: musicOnlyCurrentSession, onChange: (event) => { setMusicOnlyCurrentSession(event.target.checked); } }), _jsx("span", { children: t('musicOnlyCurrentSessionLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('musicOnlyCurrentSessionHint') })] }))] })), settingsTab === 'other' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.dialogSection, children: [_jsxs("div", { className: css.checkUpdatesRow, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: checkUpdates, onChange: (event) => { setCheckUpdates(event.target.checked); } }), _jsx("span", { children: t('checkUpdatesLabel') })] }), _jsx("button", { type: "button", className: css.checkBtn, disabled: checkingUpdates, onClick: checkUpdatesNow, children: checkingUpdates
|
|
821
|
+
}, children: Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (_jsxs("option", { value: minute, children: [String(minute).padStart(2, '0'), " \u5206"] }, minute))) })] })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: dimWhenIdle, onChange: (event) => { setDimWhenIdle(event.target.checked); } }), _jsx("span", { children: t('dimWhenIdleLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('dimWhenIdleHint') })] })] })), settingsTab === 'music' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: musicEnabled, onChange: (event) => { setMusicEnabled(event.target.checked); } }), _jsx("span", { children: t('musicLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('musicHint') })] }), musicEnabled && (_jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicVolume') }), _jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: musicVolume, onChange: (event) => { setMusicVolume(Number(event.target.value)); } })] }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicStyle') }), _jsxs("select", { value: musicStyle, onChange: (event) => { setMusicStyle(event.target.value); }, children: [_jsx("option", { value: "major", children: t('musicStyleMajor') }), _jsx("option", { value: "minor", children: t('musicStyleMinor') }), _jsx("option", { value: "pentatonic", children: t('musicStylePentatonic') })] })] }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: musicOnlyCurrentSession, onChange: (event) => { setMusicOnlyCurrentSession(event.target.checked); } }), _jsx("span", { children: t('musicOnlyCurrentSessionLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('musicOnlyCurrentSessionHint') })] }))] })), settingsTab === 'transcribe' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: transcribe.enabled, onChange: (event) => { setTranscribeSettings({ ...transcribe, enabled: event.target.checked }); } }), _jsx("span", { children: t('transcribeEnabledLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('transcribeEnabledHint') })] }), transcribe.enabled && (_jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('transcribeBaseURLLabel') }), _jsx("input", { className: css.resetSelect, type: "text", value: transcribe.baseURL, onChange: (event) => { setTranscribeSettings({ ...transcribe, baseURL: event.target.value }); } })] }), _jsx("div", { className: css.dialogHint, children: t('transcribeBaseURLHint') }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('transcribeApiKeyEnvLabel') }), _jsx("input", { className: css.resetSelect, type: "text", value: transcribe.apiKeyEnv, onChange: (event) => { setTranscribeSettings({ ...transcribe, apiKeyEnv: event.target.value }); } })] }), _jsx("div", { className: css.dialogHint, children: t('transcribeApiKeyEnvHint') }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('transcribeModelLabel') }), _jsx("input", { className: css.resetSelect, type: "text", value: transcribe.model, onChange: (event) => { setTranscribeSettings({ ...transcribe, model: event.target.value }); } })] }), _jsx("div", { className: css.dialogHint, children: t('transcribeModelHint') })] }))] })), settingsTab === 'other' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.dialogSection, children: [_jsxs("div", { className: css.checkUpdatesRow, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: checkUpdates, onChange: (event) => { setCheckUpdates(event.target.checked); } }), _jsx("span", { children: t('checkUpdatesLabel') })] }), _jsx("button", { type: "button", className: css.checkBtn, disabled: checkingUpdates, onClick: checkUpdatesNow, children: checkingUpdates
|
|
820
822
|
? t('checkingUpdates')
|
|
821
823
|
: lastCheckResult === 'up-to-date'
|
|
822
824
|
? t('checkUpToDate')
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
16
16
|
import { type TokenQuotaKey } from './locales.ts';
|
|
17
17
|
export type { TokenQuotaPanelInjected } from './TokenQuotaPanel.tsx';
|
|
18
|
+
export type { TokenQuotaMicInjected } from './mic.tsx';
|
|
18
19
|
export type { ModelQuotaRow, TokenQuotaPanelState } from './store.ts';
|
|
19
20
|
export { mergeModelRows } from './store.ts';
|
|
20
21
|
export type { TokenQuotaKey } from './locales.ts';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { TOKEN_QUOTA_MAX_NOTES } from '@jxgame2020/dsh-token-quota/types';
|
|
1
|
+
import { TOKEN_QUOTA_MAX_NOTES, TOKEN_QUOTA_DEFAULT_TRANSCRIBE } from '@jxgame2020/dsh-token-quota/types';
|
|
2
2
|
import { createTokenQuotaPanelStore } from "./store.js";
|
|
3
3
|
import { TokenQuotaPanel } from "./TokenQuotaPanel.js";
|
|
4
4
|
import { TokenQuotaMusic } from "./music.js";
|
|
5
|
+
import { TokenQuotaMic } from "./mic.js";
|
|
5
6
|
import { en, zh } from "./locales.js";
|
|
6
7
|
export { mergeModelRows } from "./store.js";
|
|
7
8
|
/** Dictionary namespace owning the panel copy. */
|
|
@@ -121,6 +122,13 @@ export function apply(ctx) {
|
|
|
121
122
|
style: raw?.style ?? 'pentatonic',
|
|
122
123
|
onlyCurrentSession: raw?.onlyCurrentSession ?? true,
|
|
123
124
|
});
|
|
125
|
+
/** Build a writable transcription-settings clone with defaults applied. */
|
|
126
|
+
const transcribeDefaults = (raw) => ({
|
|
127
|
+
enabled: raw?.enabled ?? TOKEN_QUOTA_DEFAULT_TRANSCRIBE.enabled,
|
|
128
|
+
baseURL: typeof raw?.baseURL === 'string' && raw.baseURL !== '' ? raw.baseURL : TOKEN_QUOTA_DEFAULT_TRANSCRIBE.baseURL,
|
|
129
|
+
apiKeyEnv: typeof raw?.apiKeyEnv === 'string' && raw.apiKeyEnv !== '' ? raw.apiKeyEnv : TOKEN_QUOTA_DEFAULT_TRANSCRIBE.apiKeyEnv,
|
|
130
|
+
model: typeof raw?.model === 'string' && raw.model !== '' ? raw.model : TOKEN_QUOTA_DEFAULT_TRANSCRIBE.model,
|
|
131
|
+
});
|
|
124
132
|
const keyOf = (selection) => {
|
|
125
133
|
if (selection === null)
|
|
126
134
|
return undefined;
|
|
@@ -291,6 +299,7 @@ export function apply(ctx) {
|
|
|
291
299
|
bound?.setMusicVolume(musicSettings.volume);
|
|
292
300
|
bound?.setMusicStyle(musicSettings.style);
|
|
293
301
|
bound?.setMusicOnlyCurrentSession(musicSettings.onlyCurrentSession);
|
|
302
|
+
bound?.setTranscribeSettings(transcribeDefaults(doc?.transcribe));
|
|
294
303
|
void fetch('/token-quota', { headers: { accept: 'application/json' } }).then((response) => {
|
|
295
304
|
if (!response.ok) {
|
|
296
305
|
bound?.setError(`quota route: ${String(response.status)}`);
|
|
@@ -378,6 +387,29 @@ export function apply(ctx) {
|
|
|
378
387
|
bound?.setBalanceSettings(enabled, pollMinutes);
|
|
379
388
|
void scope.set('balance', { enabled, pollMinutes });
|
|
380
389
|
};
|
|
390
|
+
/** Persist the voice transcription settings and mirror them into the store. */
|
|
391
|
+
const setTranscribeSettings = (settings) => {
|
|
392
|
+
bound?.setTranscribeSettings(settings);
|
|
393
|
+
void scope.set('transcribe', settings);
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* Send a recorded audio blob to the Host, which forwards it to the
|
|
397
|
+
* configured OpenAI-compatible transcription endpoint. Resolves with the
|
|
398
|
+
* recognised text.
|
|
399
|
+
*/
|
|
400
|
+
const transcribe = async (blob) => {
|
|
401
|
+
const res = await fetch('/token-quota/transcribe', {
|
|
402
|
+
method: 'POST',
|
|
403
|
+
headers: { 'content-type': blob.type !== '' ? blob.type : 'application/octet-stream' },
|
|
404
|
+
body: blob,
|
|
405
|
+
});
|
|
406
|
+
if (!res.ok) {
|
|
407
|
+
const detail = await res.text().catch(() => '');
|
|
408
|
+
throw new Error(detail !== '' ? detail : `transcription HTTP ${res.status}`);
|
|
409
|
+
}
|
|
410
|
+
const data = await res.json();
|
|
411
|
+
return typeof data.text === 'string' ? data.text : '';
|
|
412
|
+
};
|
|
381
413
|
/**
|
|
382
414
|
* Ask the Host to refresh account balances now and adopt the result.
|
|
383
415
|
* With a provider key only that provider is refreshed; otherwise all of
|
|
@@ -514,6 +546,7 @@ export function apply(ctx) {
|
|
|
514
546
|
setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle,
|
|
515
547
|
setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession,
|
|
516
548
|
setModelOrder, saveNotes, setBalanceSettings, refreshBalance,
|
|
549
|
+
setTranscribeSettings,
|
|
517
550
|
};
|
|
518
551
|
};
|
|
519
552
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
@@ -525,5 +558,21 @@ export function apply(ctx) {
|
|
|
525
558
|
locale: NS,
|
|
526
559
|
inject: injected,
|
|
527
560
|
}, TokenQuotaPanel));
|
|
561
|
+
// Mic button inside the chat composer (top-right corner). Only renders when
|
|
562
|
+
// transcription is enabled in settings; a press starts recording, a second
|
|
563
|
+
// press stops and sends the audio to the Host for transcription, then the
|
|
564
|
+
// recognised text is appended to the composer.
|
|
565
|
+
ctx.slots.inject('conversation.input.overlay', () => ctx.slots.register({
|
|
566
|
+
name: 'conversation.input.overlay',
|
|
567
|
+
id: 'token-quota-mic',
|
|
568
|
+
order: 100,
|
|
569
|
+
label: () => t('transcribeTitle'),
|
|
570
|
+
store,
|
|
571
|
+
locale: NS,
|
|
572
|
+
inject: (_sessionId, micActions) => {
|
|
573
|
+
bound = micActions;
|
|
574
|
+
return { transcribe, setTranscribeSettings };
|
|
575
|
+
},
|
|
576
|
+
}, TokenQuotaMic));
|
|
528
577
|
}
|
|
529
578
|
//# sourceMappingURL=index.js.map
|
|
@@ -27,7 +27,20 @@ export declare const zh: {
|
|
|
27
27
|
settingsTitle: string;
|
|
28
28
|
settingsTabQuota: string;
|
|
29
29
|
settingsTabMusic: string;
|
|
30
|
+
settingsTabTranscribe: string;
|
|
30
31
|
settingsTabOther: string;
|
|
32
|
+
transcribeTitle: string;
|
|
33
|
+
transcribeHint: string;
|
|
34
|
+
transcribeMicDenied: string;
|
|
35
|
+
transcribeEmpty: string;
|
|
36
|
+
transcribeEnabledLabel: string;
|
|
37
|
+
transcribeEnabledHint: string;
|
|
38
|
+
transcribeBaseURLLabel: string;
|
|
39
|
+
transcribeBaseURLHint: string;
|
|
40
|
+
transcribeApiKeyEnvLabel: string;
|
|
41
|
+
transcribeApiKeyEnvHint: string;
|
|
42
|
+
transcribeModelLabel: string;
|
|
43
|
+
transcribeModelHint: string;
|
|
31
44
|
reorderHint: string;
|
|
32
45
|
balanceLabel: string;
|
|
33
46
|
balanceUnconfigured: string;
|
|
@@ -145,7 +158,20 @@ export declare const en: {
|
|
|
145
158
|
settingsTitle: string;
|
|
146
159
|
settingsTabQuota: string;
|
|
147
160
|
settingsTabMusic: string;
|
|
161
|
+
settingsTabTranscribe: string;
|
|
148
162
|
settingsTabOther: string;
|
|
163
|
+
transcribeTitle: string;
|
|
164
|
+
transcribeHint: string;
|
|
165
|
+
transcribeMicDenied: string;
|
|
166
|
+
transcribeEmpty: string;
|
|
167
|
+
transcribeEnabledLabel: string;
|
|
168
|
+
transcribeEnabledHint: string;
|
|
169
|
+
transcribeBaseURLLabel: string;
|
|
170
|
+
transcribeBaseURLHint: string;
|
|
171
|
+
transcribeApiKeyEnvLabel: string;
|
|
172
|
+
transcribeApiKeyEnvHint: string;
|
|
173
|
+
transcribeModelLabel: string;
|
|
174
|
+
transcribeModelHint: string;
|
|
149
175
|
reorderHint: string;
|
|
150
176
|
balanceLabel: string;
|
|
151
177
|
balanceUnconfigured: string;
|
|
@@ -27,7 +27,20 @@ export const zh = {
|
|
|
27
27
|
settingsTitle: '设置',
|
|
28
28
|
settingsTabQuota: '限额设置',
|
|
29
29
|
settingsTabMusic: '音乐设置',
|
|
30
|
+
settingsTabTranscribe: '转录',
|
|
30
31
|
settingsTabOther: '其它',
|
|
32
|
+
transcribeTitle: '语音转录',
|
|
33
|
+
transcribeHint: '按下麦克风录音,再按一次停止并转录为文字',
|
|
34
|
+
transcribeMicDenied: '未获得麦克风权限',
|
|
35
|
+
transcribeEmpty: '录音为空',
|
|
36
|
+
transcribeEnabledLabel: '启用语音转录',
|
|
37
|
+
transcribeEnabledHint: '在对话框右上角显示麦克风按钮',
|
|
38
|
+
transcribeBaseURLLabel: '接口地址',
|
|
39
|
+
transcribeBaseURLHint: 'OpenAI 兼容的转录接口 base URL',
|
|
40
|
+
transcribeApiKeyEnvLabel: 'API Key 环境变量',
|
|
41
|
+
transcribeApiKeyEnvHint: '存有 API Key 的凭证变量名(在 Models 页或 credentials 配置)',
|
|
42
|
+
transcribeModelLabel: '模型',
|
|
43
|
+
transcribeModelHint: '语音转文本模型名',
|
|
31
44
|
reorderHint: '拖动排序',
|
|
32
45
|
balanceLabel: '余额',
|
|
33
46
|
balanceUnconfigured: '未配置 DeepSeek API Key',
|
|
@@ -143,7 +156,20 @@ export const en = {
|
|
|
143
156
|
settingsTitle: 'Settings',
|
|
144
157
|
settingsTabQuota: 'Quota',
|
|
145
158
|
settingsTabMusic: 'Music',
|
|
159
|
+
settingsTabTranscribe: 'Transcribe',
|
|
146
160
|
settingsTabOther: 'Other',
|
|
161
|
+
transcribeTitle: 'Voice transcription',
|
|
162
|
+
transcribeHint: 'Press the mic to record; press again to stop and transcribe to text',
|
|
163
|
+
transcribeMicDenied: 'Microphone access denied',
|
|
164
|
+
transcribeEmpty: 'Recording was empty',
|
|
165
|
+
transcribeEnabledLabel: 'Enable voice transcription',
|
|
166
|
+
transcribeEnabledHint: 'Show a mic button in the top-right of the composer',
|
|
167
|
+
transcribeBaseURLLabel: 'Base URL',
|
|
168
|
+
transcribeBaseURLHint: 'OpenAI-compatible transcription endpoint base URL',
|
|
169
|
+
transcribeApiKeyEnvLabel: 'API key env var',
|
|
170
|
+
transcribeApiKeyEnvHint: 'Credential variable name holding the API key (Models page or credentials)',
|
|
171
|
+
transcribeModelLabel: 'Model',
|
|
172
|
+
transcribeModelHint: 'Speech-to-text model name',
|
|
147
173
|
reorderHint: 'Drag to reorder',
|
|
148
174
|
balanceLabel: 'Balance',
|
|
149
175
|
balanceUnconfigured: 'No DeepSeek API key',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import { TOKEN_QUOTA_NAMESPACE } from '@jxgame2020/dsh-token-quota/types';
|
|
3
|
+
import type { TokenQuotaTranscribeSettings } from '@jxgame2020/dsh-token-quota/types';
|
|
4
|
+
import type { createTokenQuotaPanelStore } from './store.ts';
|
|
5
|
+
/** Injected business face: transcription send + settings persistence. */
|
|
6
|
+
export interface TokenQuotaMicInjected {
|
|
7
|
+
/** Send a recorded audio blob to the Host for transcription. */
|
|
8
|
+
transcribe: (blob: Blob) => Promise<string>;
|
|
9
|
+
/** Persist the transcription settings (the panel owns the settings UI). */
|
|
10
|
+
setTranscribeSettings: (settings: TokenQuotaTranscribeSettings) => void;
|
|
11
|
+
}
|
|
12
|
+
/** Full mic component props: runtime + store + locale + injected face. */
|
|
13
|
+
export type TokenQuotaMicComponentProps = PropsRuntime<'conversation.input.overlay'> & PropsStore<ReturnType<typeof createTokenQuotaPanelStore>> & PropsLocale<typeof TOKEN_QUOTA_NAMESPACE> & TokenQuotaMicInjected;
|
|
14
|
+
/**
|
|
15
|
+
* Mic button rendered inside the composer card. Manages its own MediaRecorder
|
|
16
|
+
* lifecycle; only the button is shown — no floating panel UI.
|
|
17
|
+
*/
|
|
18
|
+
export declare function TokenQuotaMic({ t, useStore, transcribe, }: TokenQuotaMicComponentProps): JSX.Element | null;
|
|
19
|
+
//# sourceMappingURL=mic.d.ts.map
|