dsh-packycode-api 1.1.9 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.en.md +25 -2
- package/README.md +25 -2
- package/docs/research/image-api.md +75 -0
- package/lib/client.js +748 -108
- package/lib/index.js +1331 -290
- package/package.json +8 -4
package/lib/client.js
CHANGED
|
@@ -39,6 +39,39 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
nav: "PackyCode",
|
|
40
40
|
intro: "填入 PackyCode(PackyAPI)的 API Key 即可使用内置模型路由。多个 Key 按顺序使用:额度用尽或失效时自动切到下一个;某个 Key 的分组用不了所选模型时,自动改用分组合适的 Key。",
|
|
41
41
|
accountsTitle: "账户与 Key",
|
|
42
|
+
imagesTitle: "图片生成与编辑",
|
|
43
|
+
imagesSeparateKey: "独立图片 Key",
|
|
44
|
+
imagesBalanceLoading: "正在读取图片 Key 余额…",
|
|
45
|
+
imagesBalanceFailed: "图片余额读取失败",
|
|
46
|
+
imagesBalanceHint: "仅汇总图片 Key 的额度,每分钟自动更新;也可手动刷新。与对话 Key 的余额分开统计。",
|
|
47
|
+
imagesIntro: "使用独立图片 Key 调用 PackyCode 图片接口。生成新图或上传图片后描述修改要求;图片模型独立于对话模型。原始 PNG 保存在本机,聊天中显示图片预览。",
|
|
48
|
+
imagesEnabled: "图片生成与编辑",
|
|
49
|
+
imagesOn: "启用",
|
|
50
|
+
imagesOff: "停用",
|
|
51
|
+
imagesModel: "请求型号",
|
|
52
|
+
imagesQuality: "请求质量",
|
|
53
|
+
imagesQuality_auto: "自动",
|
|
54
|
+
imagesQuality_low: "低",
|
|
55
|
+
imagesQuality_medium: "中",
|
|
56
|
+
imagesQuality_high: "高",
|
|
57
|
+
imagesQuality_xhigh: "超高(xhigh)",
|
|
58
|
+
imagesQuality_max: "最高(max)",
|
|
59
|
+
imagesQualityHint: "2.5 支持额外的 xhigh / max;质量越高,耗时和费用通常越高。网关实际支持情况需用图片 Key 验证。不会自动换型号或降低质量。",
|
|
60
|
+
imagesAddKey: "添加图片 Key",
|
|
61
|
+
imagesKeyPlaceholder: "粘贴专用的图片 API Key",
|
|
62
|
+
imagesNoKey: "尚未添加图片 Key。添加后可使用图片工具;对话 Key 不会用于图片请求。",
|
|
63
|
+
imagesLoadFailed: "图片设置读取失败。若刚更新本地插件,请重新加载插件后再打开设置。",
|
|
64
|
+
imagesUnavailable: "当前 Host 未提供图片附件服务,图片工具暂不可用。",
|
|
65
|
+
imagesCreate: "创建图片",
|
|
66
|
+
imagesShortcutHint: "在草稿中加入 PackyCode 生图指令,再补充图片描述后发送。",
|
|
67
|
+
imagesDraft: "请使用 packycode_image_generate 工具,跟随 PackyCode 已保存的图片型号和质量。有参考图时编辑参考图,没有参考图时生成新图。图片要求:",
|
|
68
|
+
imagesDraftBusy: "输入框正忙或已有引用,请直接在草稿里描述图片要求。",
|
|
69
|
+
imagesGenerating: "正在生成…",
|
|
70
|
+
imagesFailed: "生成失败",
|
|
71
|
+
imagesDone: "已生成",
|
|
72
|
+
imagesOpen: "打开图片",
|
|
73
|
+
imagesRetryPreview: "预览加载失败,点击重试",
|
|
74
|
+
imagesLoadingPreview: "正在加载图片…",
|
|
42
75
|
refresh: "刷新",
|
|
43
76
|
refreshing: "刷新中…",
|
|
44
77
|
totalRemaining: "合计剩余",
|
|
@@ -68,6 +101,7 @@ window.__ModuleLoader__.load({
|
|
|
68
101
|
resetMonthly: "每月重置",
|
|
69
102
|
resetWeekly: "每周重置",
|
|
70
103
|
resetDaily: "每日重置",
|
|
104
|
+
resetNever: "不重置",
|
|
71
105
|
loadFailed: "读取失败",
|
|
72
106
|
removeAll: "清空全部",
|
|
73
107
|
addTitle: "添加 Key",
|
|
@@ -180,6 +214,39 @@ window.__ModuleLoader__.load({
|
|
|
180
214
|
modelsEmpty: "No models selected. PackyCode models are hidden from the picker.",
|
|
181
215
|
intro: "Paste a PackyCode (PackyAPI) API key to use the built-in model route. Keys are used in order: an exhausted or rejected key hands over to the next one, and a key whose groups cannot reach the selected model is passed over for one whose groups can.",
|
|
182
216
|
accountsTitle: "Accounts and keys",
|
|
217
|
+
imagesTitle: "Image generation and editing",
|
|
218
|
+
imagesSeparateKey: "Separate image key",
|
|
219
|
+
imagesBalanceLoading: "Reading image key balances…",
|
|
220
|
+
imagesBalanceFailed: "Could not read image balances",
|
|
221
|
+
imagesBalanceHint: "Only image key quotas are counted here. Updates every minute, or use Refresh. Conversation key balances are separate.",
|
|
222
|
+
imagesIntro: "Use a dedicated image key with PackyCode’s Images API. Describe a new image or attach images with editing instructions. The image engine is independent of the conversation model. Original PNGs are saved locally and previews appear in chat.",
|
|
223
|
+
imagesEnabled: "Generate and edit images",
|
|
224
|
+
imagesOn: "Enabled",
|
|
225
|
+
imagesOff: "Disabled",
|
|
226
|
+
imagesModel: "Image model",
|
|
227
|
+
imagesQuality: "Request quality",
|
|
228
|
+
imagesQuality_auto: "Auto",
|
|
229
|
+
imagesQuality_low: "Low",
|
|
230
|
+
imagesQuality_medium: "Medium",
|
|
231
|
+
imagesQuality_high: "High",
|
|
232
|
+
imagesQuality_xhigh: "Extra high (xhigh)",
|
|
233
|
+
imagesQuality_max: "Maximum (max)",
|
|
234
|
+
imagesQualityHint: "The 2.5 engines also support xhigh / max. Higher quality usually takes longer and costs more. Gateway support needs testing with an image key. Models and quality are never changed automatically.",
|
|
235
|
+
imagesAddKey: "Add an image key",
|
|
236
|
+
imagesKeyPlaceholder: "Paste the dedicated image API key",
|
|
237
|
+
imagesNoKey: "No image key yet. Add one to enable the image tool. Conversation keys are never used for images.",
|
|
238
|
+
imagesLoadFailed: "Could not read image settings. If you just updated the local plugin, reload it and reopen settings.",
|
|
239
|
+
imagesUnavailable: "This host does not provide image attachments; the image tool is unavailable.",
|
|
240
|
+
imagesCreate: "Create image",
|
|
241
|
+
imagesShortcutHint: "Add a PackyCode image instruction to the draft, describe the image, then send.",
|
|
242
|
+
imagesDraft: "Use packycode_image_generate with the saved PackyCode image model and quality. Edit the attached references when provided; otherwise generate a new image. Image requirements:",
|
|
243
|
+
imagesDraftBusy: "The composer is busy or contains references. Describe the image requirements directly in the draft.",
|
|
244
|
+
imagesGenerating: "Generating…",
|
|
245
|
+
imagesFailed: "Generation failed",
|
|
246
|
+
imagesDone: "Generated",
|
|
247
|
+
imagesOpen: "Open image",
|
|
248
|
+
imagesRetryPreview: "Preview failed; click to retry",
|
|
249
|
+
imagesLoadingPreview: "Loading image…",
|
|
183
250
|
refresh: "Refresh",
|
|
184
251
|
refreshing: "Refreshing…",
|
|
185
252
|
totalRemaining: "Total remaining",
|
|
@@ -209,6 +276,7 @@ window.__ModuleLoader__.load({
|
|
|
209
276
|
resetMonthly: "resets monthly",
|
|
210
277
|
resetWeekly: "resets weekly",
|
|
211
278
|
resetDaily: "resets daily",
|
|
279
|
+
resetNever: "does not reset",
|
|
212
280
|
loadFailed: "Read failed",
|
|
213
281
|
removeAll: "Remove all",
|
|
214
282
|
addTitle: "Add a key",
|
|
@@ -339,6 +407,11 @@ window.__ModuleLoader__.load({
|
|
|
339
407
|
.pkc-input:focus{border-color:var(--dsw-alias-state-business-primary)}
|
|
340
408
|
.pkc-input::placeholder{color:var(--dsw-alias-label-caption)}
|
|
341
409
|
.pkc-input:disabled{opacity:.5}
|
|
410
|
+
.pkc-image-choice{flex:none;width:100%;max-width:300px;cursor:pointer}
|
|
411
|
+
.pkc-image-shortcut{display:inline-flex;align-items:center;gap:8px}
|
|
412
|
+
.pkc-image-result,.pkc-image-chat{display:flex;flex-direction:column;gap:10px;padding:8px 0}
|
|
413
|
+
.pkc-generated-image{display:grid;place-items:center;align-self:flex-start;max-width:100%;width:320px;min-height:80px;padding:0;overflow:hidden;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);cursor:zoom-in}
|
|
414
|
+
.pkc-generated-image img{display:block;max-width:100%;max-height:480px;object-fit:contain}
|
|
342
415
|
.pkc-form{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
343
416
|
|
|
344
417
|
.pkc-chip{flex:none;padding:0 8px;border-radius:999px;font-size:11px;font-weight:500;line-height:18px;white-space:nowrap}
|
|
@@ -416,6 +489,68 @@ window.__ModuleLoader__.load({
|
|
|
416
489
|
.pkc-setting>.pkc-label{padding-top:0}
|
|
417
490
|
}
|
|
418
491
|
`;
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region src/image-conversation.js
|
|
494
|
+
/** Project this plugin's durable image results into chat without duplicating context. */
|
|
495
|
+
const IMAGE_CHAT_KIND = "packycode-image-output";
|
|
496
|
+
const IMAGE_TOOLS = /* @__PURE__ */ new Set(["packycode_image_generate", "claude_code"]);
|
|
497
|
+
const hasImage = (content) => Array.isArray(content) && content.some((part) => part.type === "image" && part.attachment);
|
|
498
|
+
function successfulImages(event) {
|
|
499
|
+
if (event?.type === "tool/result" && event.data?.meta?.kind === "packycode-image") return (event.data.message?.content ?? []).filter((block) => block.type === "tool-result" && !block.isError && hasImage(block.content)).map((block) => ({
|
|
500
|
+
...block,
|
|
501
|
+
meta: event.data.meta
|
|
502
|
+
}));
|
|
503
|
+
if (event?.type === "tool/ptc-dispatch" && IMAGE_TOOLS.has(event.data?.name) && event.data.isError === false && typeof event.data.subCallId === "string" && hasImage(event.data.content)) return [{
|
|
504
|
+
type: "tool-result",
|
|
505
|
+
toolCallId: event.data.subCallId,
|
|
506
|
+
content: event.data.content,
|
|
507
|
+
meta: { kind: "packycode-image" }
|
|
508
|
+
}];
|
|
509
|
+
const source = event?.data?.message?.source;
|
|
510
|
+
if (event?.type !== "assistant/message" || source?.kind !== "model" || source.provider !== "packycode" || !/^claude-/u.test(source.model ?? "")) return [];
|
|
511
|
+
return (event.data.message.content ?? []).flatMap((block, index) => block.type === "image" && block.attachment ? [{
|
|
512
|
+
type: "tool-result",
|
|
513
|
+
toolCallId: `packycode-cli-image-${event.seq}-${index}`,
|
|
514
|
+
content: [block]
|
|
515
|
+
}] : []);
|
|
516
|
+
}
|
|
517
|
+
const imageConversationNode = {
|
|
518
|
+
kind: IMAGE_CHAT_KIND,
|
|
519
|
+
target: "chat",
|
|
520
|
+
match(event) {
|
|
521
|
+
return event.type === "turn/end" || successfulImages(event).length > 0 ? {
|
|
522
|
+
id: String(event.data.turn),
|
|
523
|
+
role: "update"
|
|
524
|
+
} : null;
|
|
525
|
+
},
|
|
526
|
+
start: () => void 0,
|
|
527
|
+
update: (context) => context.state,
|
|
528
|
+
buildViewNode(context) {
|
|
529
|
+
const finished = context.matches.find((match) => match.event.type === "turn/end");
|
|
530
|
+
if (finished === void 0) return null;
|
|
531
|
+
const results = context.matches.filter((match) => successfulImages(match.event).length > 0);
|
|
532
|
+
if (results.length === 0) return null;
|
|
533
|
+
const blocks = [...new Map(results.flatMap(({ event }) => successfulImages(event).map((block) => [block.toolCallId, {
|
|
534
|
+
...block,
|
|
535
|
+
kind: "tool-result"
|
|
536
|
+
}]))).values()];
|
|
537
|
+
const answerSeq = finished.location.turn?.steps?.at(-1)?.data.get("assistant-step")?.finalNode?.seq;
|
|
538
|
+
const lastImageSeq = Math.max(...results.map((match) => match.event.seq));
|
|
539
|
+
return {
|
|
540
|
+
key: context.key,
|
|
541
|
+
id: context.id,
|
|
542
|
+
kind: IMAGE_CHAT_KIND,
|
|
543
|
+
target: "chat",
|
|
544
|
+
location: { kind: "session" },
|
|
545
|
+
visibility: "visible",
|
|
546
|
+
anchorSeq: answerSeq > lastImageSeq ? answerSeq + .025 : finished.event.seq - .025,
|
|
547
|
+
data: {
|
|
548
|
+
turn: finished.event.data.turn,
|
|
549
|
+
blocks
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
};
|
|
419
554
|
//#endregion
|
|
420
555
|
//#region src/totals.js
|
|
421
556
|
/**
|
|
@@ -468,6 +603,584 @@ window.__ModuleLoader__.load({
|
|
|
468
603
|
return total;
|
|
469
604
|
}
|
|
470
605
|
//#endregion
|
|
606
|
+
//#region src/client-balance.jsx
|
|
607
|
+
const fill = (text, values) => text.replace(/\{(\w+)\}/gu, (match, name) => values[name] === void 0 ? match : String(values[name]));
|
|
608
|
+
function formatUsd(value) {
|
|
609
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
610
|
+
try {
|
|
611
|
+
return new Intl.NumberFormat(void 0, {
|
|
612
|
+
style: "currency",
|
|
613
|
+
currency: "USD"
|
|
614
|
+
}).format(value);
|
|
615
|
+
} catch {
|
|
616
|
+
return `$${value.toFixed(2)}`;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function readTime(ms) {
|
|
620
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "—";
|
|
621
|
+
try {
|
|
622
|
+
return new Date(ms).toLocaleTimeString(void 0, {
|
|
623
|
+
hour: "2-digit",
|
|
624
|
+
minute: "2-digit"
|
|
625
|
+
});
|
|
626
|
+
} catch {
|
|
627
|
+
return new Date(ms).toISOString().slice(11, 16);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const RESET_KEYS = {
|
|
631
|
+
monthly: "resetMonthly",
|
|
632
|
+
weekly: "resetWeekly",
|
|
633
|
+
daily: "resetDaily",
|
|
634
|
+
never: "resetNever"
|
|
635
|
+
};
|
|
636
|
+
/** The same quota presentation for conversation and image credential records. */
|
|
637
|
+
function KeyBalance({ row, t }) {
|
|
638
|
+
const usage = row?.usage;
|
|
639
|
+
const figures = [
|
|
640
|
+
usage?.unlimitedQuota === true ? [t("keyRemaining"), t("keyUnlimited")] : usage?.availableUsd === void 0 ? void 0 : [t("keyRemaining"), formatUsd(usage.availableUsd)],
|
|
641
|
+
usage?.usedUsd === void 0 ? void 0 : [t("keyUsed"), formatUsd(usage.usedUsd)],
|
|
642
|
+
usage?.limitUsd === void 0 ? void 0 : [t("accountLimit"), formatUsd(usage.limitUsd)]
|
|
643
|
+
].filter(Boolean);
|
|
644
|
+
const meta = [usage?.name === void 0 ? t("keyNameUnknown") : `${t("keyName")} ${usage.name}`, RESET_KEYS[usage?.resetPeriod] === void 0 ? void 0 : t(RESET_KEYS[usage.resetPeriod])].filter(Boolean).join(" · ");
|
|
645
|
+
const share = usage?.unlimitedQuota !== true && typeof usage?.limitUsd === "number" && usage.limitUsd > 0 && typeof usage.availableUsd === "number" ? Math.max(0, Math.min(1, usage.availableUsd / usage.limitUsd)) : void 0;
|
|
646
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
647
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
648
|
+
className: "pkc-meta",
|
|
649
|
+
children: meta
|
|
650
|
+
}),
|
|
651
|
+
figures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
652
|
+
className: "pkc-figures",
|
|
653
|
+
children: figures.map(([label, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: value })] }, label))
|
|
654
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
655
|
+
className: "pkc-meta",
|
|
656
|
+
children: row?.error === void 0 ? t("keyNoBalance") : `${t("loadFailed")}: ${row.error}`
|
|
657
|
+
}),
|
|
658
|
+
share !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
659
|
+
className: "pkc-bar",
|
|
660
|
+
"data-level": share < .1 ? "low" : share < .3 ? "warn" : "ok",
|
|
661
|
+
role: "presentation",
|
|
662
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round(share * 100)}%` } })
|
|
663
|
+
}),
|
|
664
|
+
row?.stale !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
665
|
+
className: "pkc-warn",
|
|
666
|
+
children: fill(t("staleNote"), {
|
|
667
|
+
time: readTime(row.stale.fetchedAt),
|
|
668
|
+
error: row.stale.error ?? ""
|
|
669
|
+
})
|
|
670
|
+
})
|
|
671
|
+
] });
|
|
672
|
+
}
|
|
673
|
+
/** Only this card's keys contribute; account-wide billing is never added. */
|
|
674
|
+
function BalanceTotals({ rows, t }) {
|
|
675
|
+
const totals = sumBalances(rows);
|
|
676
|
+
const stale = rows.filter((row) => row.stale !== void 0).length;
|
|
677
|
+
const known = (field) => rows.some((row) => row.usage?.unlimitedQuota !== true && Number.isFinite(row.usage?.[field]));
|
|
678
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
679
|
+
className: "pkc-stats",
|
|
680
|
+
children: [
|
|
681
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
682
|
+
className: "pkc-stat",
|
|
683
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
684
|
+
className: "pkc-stat-label",
|
|
685
|
+
children: t("totalRemaining")
|
|
686
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
687
|
+
className: "pkc-stat-value",
|
|
688
|
+
children: [known("availableUsd") ? formatUsd(totals.remainingUsd) : totals.unlimited > 0 ? t("keyUnlimited") : "—", known("availableUsd") && totals.unlimited > 0 && ` + ${t("keyUnlimited")}`]
|
|
689
|
+
})]
|
|
690
|
+
}),
|
|
691
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
692
|
+
className: "pkc-stat",
|
|
693
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
694
|
+
className: "pkc-stat-label",
|
|
695
|
+
children: t("totalUsed")
|
|
696
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
697
|
+
className: "pkc-stat-value",
|
|
698
|
+
children: known("usedUsd") ? formatUsd(totals.usedUsd) : "—"
|
|
699
|
+
})]
|
|
700
|
+
}),
|
|
701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
702
|
+
className: "pkc-stat",
|
|
703
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
704
|
+
className: "pkc-stat-label",
|
|
705
|
+
children: t("totalLimit")
|
|
706
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
707
|
+
className: "pkc-stat-value",
|
|
708
|
+
children: totals.unlimited > 0 ? t("keyUnlimited") : known("limitUsd") ? formatUsd(totals.limitUsd) : "—"
|
|
709
|
+
})]
|
|
710
|
+
})
|
|
711
|
+
]
|
|
712
|
+
}), (totals.unread > 0 || stale > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
713
|
+
className: "pkc-note",
|
|
714
|
+
children: [totals.unread > 0 ? fill(t("totalPartial"), { count: totals.unread }) : void 0, stale > 0 ? fill(t("totalStale"), { count: stale }) : void 0].filter(Boolean).join(" ")
|
|
715
|
+
})] });
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/client-images.jsx
|
|
719
|
+
const imageModelLabel = (id) => id.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
|
|
720
|
+
/** Only projections come back from the host; the password draft is cleared on success. */
|
|
721
|
+
function PackyCodeImagesCard({ call, t }) {
|
|
722
|
+
const [state, setState] = react.useState();
|
|
723
|
+
const [draft, setDraft] = react.useState({
|
|
724
|
+
key: "",
|
|
725
|
+
label: ""
|
|
726
|
+
});
|
|
727
|
+
const [busy, setBusy] = react.useState(false);
|
|
728
|
+
const [error, setError] = react.useState();
|
|
729
|
+
const [balance, setBalance] = react.useState();
|
|
730
|
+
const [balanceError, setBalanceError] = react.useState();
|
|
731
|
+
const [balanceLoading, setBalanceLoading] = react.useState(true);
|
|
732
|
+
const balanceGeneration = react.useRef(0);
|
|
733
|
+
const mounted = react.useRef(false);
|
|
734
|
+
const readBalance = async (force = false) => {
|
|
735
|
+
if (!mounted.current) return;
|
|
736
|
+
const generation = ++balanceGeneration.current;
|
|
737
|
+
setBalanceLoading(true);
|
|
738
|
+
setBalanceError(void 0);
|
|
739
|
+
try {
|
|
740
|
+
const value = await call("images/balance", { force });
|
|
741
|
+
if (mounted.current && balanceGeneration.current === generation) setBalance(value);
|
|
742
|
+
} catch (failure) {
|
|
743
|
+
if (mounted.current && balanceGeneration.current === generation) setBalanceError(String(failure?.message ?? t("imagesBalanceFailed")).slice(0, 200));
|
|
744
|
+
} finally {
|
|
745
|
+
if (mounted.current && balanceGeneration.current === generation) setBalanceLoading(false);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
react.useEffect(() => {
|
|
749
|
+
mounted.current = true;
|
|
750
|
+
call("images/status").then((value) => {
|
|
751
|
+
if (mounted.current) setState(value);
|
|
752
|
+
}).catch(() => {
|
|
753
|
+
if (mounted.current) setError(t("imagesLoadFailed"));
|
|
754
|
+
});
|
|
755
|
+
readBalance();
|
|
756
|
+
const timer = window.setInterval(() => {
|
|
757
|
+
readBalance();
|
|
758
|
+
}, 6e4);
|
|
759
|
+
return () => {
|
|
760
|
+
mounted.current = false;
|
|
761
|
+
balanceGeneration.current += 1;
|
|
762
|
+
window.clearInterval(timer);
|
|
763
|
+
};
|
|
764
|
+
}, [call, t]);
|
|
765
|
+
const mutate = async (endpoint, payload = {}) => {
|
|
766
|
+
setBusy(true);
|
|
767
|
+
setError(void 0);
|
|
768
|
+
try {
|
|
769
|
+
const value = await call(endpoint, payload);
|
|
770
|
+
if (mounted.current) setState(value);
|
|
771
|
+
if (mounted.current && endpoint.startsWith("images/keys/")) {
|
|
772
|
+
setBalance(void 0);
|
|
773
|
+
readBalance(true);
|
|
774
|
+
}
|
|
775
|
+
window.dispatchEvent(new Event("packycode-images-updated"));
|
|
776
|
+
return true;
|
|
777
|
+
} catch (failure) {
|
|
778
|
+
if (mounted.current) setError(String(failure?.message ?? t("imagesLoadFailed")).slice(0, 200));
|
|
779
|
+
return false;
|
|
780
|
+
} finally {
|
|
781
|
+
if (mounted.current) setBusy(false);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
const disabled = busy || state?.writable !== true;
|
|
785
|
+
const models = state?.models ?? [];
|
|
786
|
+
const qualities = models.find((model) => model.id === state?.imageModel)?.qualities ?? [];
|
|
787
|
+
const rows = (state?.keys ?? []).map((entry) => balance?.keys?.find((row) => row.id === entry.id) ?? { id: entry.id });
|
|
788
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
789
|
+
className: "pkc-card",
|
|
790
|
+
children: [
|
|
791
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
792
|
+
className: "pkc-head",
|
|
793
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
794
|
+
className: "pkc-title",
|
|
795
|
+
children: t("imagesTitle")
|
|
796
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
797
|
+
className: "pkc-actions",
|
|
798
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
799
|
+
className: "pkc-chip",
|
|
800
|
+
"data-tone": "neutral",
|
|
801
|
+
children: t("imagesSeparateKey")
|
|
802
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
803
|
+
className: "pkc-btn",
|
|
804
|
+
type: "button",
|
|
805
|
+
disabled: busy || balanceLoading,
|
|
806
|
+
onClick: () => {
|
|
807
|
+
readBalance(true);
|
|
808
|
+
},
|
|
809
|
+
children: balanceLoading ? t("refreshing") : t("refresh")
|
|
810
|
+
})]
|
|
811
|
+
})]
|
|
812
|
+
}),
|
|
813
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
814
|
+
className: "pkc-note",
|
|
815
|
+
children: t("imagesIntro")
|
|
816
|
+
}),
|
|
817
|
+
state !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
818
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
819
|
+
className: "pkc-setting",
|
|
820
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
821
|
+
className: "pkc-label",
|
|
822
|
+
children: t("imagesEnabled")
|
|
823
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
824
|
+
className: "pkc-input pkc-image-choice",
|
|
825
|
+
value: String(state.imageEnabled),
|
|
826
|
+
disabled,
|
|
827
|
+
onChange: (event) => {
|
|
828
|
+
mutate("images/update", { imageEnabled: event.target.value === "true" });
|
|
829
|
+
},
|
|
830
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
831
|
+
value: "true",
|
|
832
|
+
children: t("imagesOn")
|
|
833
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
834
|
+
value: "false",
|
|
835
|
+
children: t("imagesOff")
|
|
836
|
+
})]
|
|
837
|
+
})]
|
|
838
|
+
}),
|
|
839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
840
|
+
className: "pkc-setting",
|
|
841
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
842
|
+
className: "pkc-label",
|
|
843
|
+
children: t("imagesModel")
|
|
844
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
845
|
+
className: "pkc-input pkc-image-choice",
|
|
846
|
+
value: state.imageModel,
|
|
847
|
+
disabled: disabled || !state.imageEnabled,
|
|
848
|
+
onChange: (event) => {
|
|
849
|
+
mutate("images/update", {
|
|
850
|
+
imageModel: event.target.value,
|
|
851
|
+
imageQuality: "auto"
|
|
852
|
+
});
|
|
853
|
+
},
|
|
854
|
+
children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
855
|
+
value: model.id,
|
|
856
|
+
children: imageModelLabel(model.id)
|
|
857
|
+
}, model.id))
|
|
858
|
+
})]
|
|
859
|
+
}),
|
|
860
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
861
|
+
className: "pkc-setting",
|
|
862
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
863
|
+
className: "pkc-label",
|
|
864
|
+
children: t("imagesQuality")
|
|
865
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
866
|
+
className: "pkc-input pkc-image-choice",
|
|
867
|
+
value: state.imageQuality,
|
|
868
|
+
disabled: disabled || !state.imageEnabled,
|
|
869
|
+
onChange: (event) => {
|
|
870
|
+
mutate("images/update", { imageQuality: event.target.value });
|
|
871
|
+
},
|
|
872
|
+
children: qualities.map((quality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
873
|
+
value: quality,
|
|
874
|
+
children: t(`imagesQuality_${quality}`)
|
|
875
|
+
}, quality))
|
|
876
|
+
})]
|
|
877
|
+
}),
|
|
878
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
879
|
+
className: "pkc-note",
|
|
880
|
+
children: t("imagesQualityHint")
|
|
881
|
+
}),
|
|
882
|
+
(state.keys ?? []).length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BalanceTotals, {
|
|
883
|
+
rows,
|
|
884
|
+
t
|
|
885
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
886
|
+
className: "pkc-note",
|
|
887
|
+
children: balanceLoading ? t("imagesBalanceLoading") : t("imagesBalanceHint")
|
|
888
|
+
})] }),
|
|
889
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
890
|
+
className: "pkc-list",
|
|
891
|
+
children: (state.keys ?? []).map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
892
|
+
className: "pkc-row",
|
|
893
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
894
|
+
className: "pkc-row-top",
|
|
895
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
896
|
+
className: "pkc-row-name",
|
|
897
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
898
|
+
className: "pkc-name",
|
|
899
|
+
children: entry.label
|
|
900
|
+
}), entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
901
|
+
className: "pkc-chip",
|
|
902
|
+
"data-tone": "success",
|
|
903
|
+
children: t("keyActive")
|
|
904
|
+
})]
|
|
905
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
906
|
+
className: "pkc-actions",
|
|
907
|
+
children: [!entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
908
|
+
className: "pkc-btn",
|
|
909
|
+
type: "button",
|
|
910
|
+
disabled: busy,
|
|
911
|
+
onClick: () => {
|
|
912
|
+
mutate("images/keys/select", { id: entry.id });
|
|
913
|
+
},
|
|
914
|
+
children: t("keyUse")
|
|
915
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
916
|
+
className: "pkc-btn",
|
|
917
|
+
"data-variant": "danger",
|
|
918
|
+
type: "button",
|
|
919
|
+
disabled: busy,
|
|
920
|
+
onClick: () => {
|
|
921
|
+
mutate("images/keys/remove", { id: entry.id });
|
|
922
|
+
},
|
|
923
|
+
children: t("keyRemove")
|
|
924
|
+
})]
|
|
925
|
+
})]
|
|
926
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyBalance, {
|
|
927
|
+
row: rows.find((row) => row.id === entry.id),
|
|
928
|
+
t
|
|
929
|
+
})]
|
|
930
|
+
}, entry.id))
|
|
931
|
+
}),
|
|
932
|
+
(state.keys ?? []).length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
933
|
+
className: "pkc-note",
|
|
934
|
+
children: t("imagesNoKey")
|
|
935
|
+
}),
|
|
936
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
937
|
+
className: "pkc-add",
|
|
938
|
+
onSubmit: (event) => {
|
|
939
|
+
event.preventDefault();
|
|
940
|
+
if (busy || draft.key.trim() === "") return;
|
|
941
|
+
mutate("images/keys/add", draft).then((saved) => {
|
|
942
|
+
if (saved && mounted.current) setDraft({
|
|
943
|
+
key: "",
|
|
944
|
+
label: ""
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
},
|
|
948
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
949
|
+
className: "pkc-label",
|
|
950
|
+
children: t("imagesAddKey")
|
|
951
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
952
|
+
className: "pkc-form",
|
|
953
|
+
children: [
|
|
954
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
955
|
+
className: "pkc-input",
|
|
956
|
+
type: "password",
|
|
957
|
+
autoComplete: "off",
|
|
958
|
+
"aria-label": t("imagesAddKey"),
|
|
959
|
+
placeholder: t("imagesKeyPlaceholder"),
|
|
960
|
+
value: draft.key,
|
|
961
|
+
disabled: busy,
|
|
962
|
+
onChange: (event) => setDraft((current) => ({
|
|
963
|
+
...current,
|
|
964
|
+
key: event.target.value
|
|
965
|
+
}))
|
|
966
|
+
}),
|
|
967
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
968
|
+
className: "pkc-input",
|
|
969
|
+
type: "text",
|
|
970
|
+
maxLength: 60,
|
|
971
|
+
"aria-label": t("addLabelPlaceholder"),
|
|
972
|
+
placeholder: t("addLabelPlaceholder"),
|
|
973
|
+
value: draft.label,
|
|
974
|
+
disabled: busy,
|
|
975
|
+
onChange: (event) => setDraft((current) => ({
|
|
976
|
+
...current,
|
|
977
|
+
label: event.target.value
|
|
978
|
+
}))
|
|
979
|
+
}),
|
|
980
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
981
|
+
className: "pkc-btn",
|
|
982
|
+
"data-variant": "primary",
|
|
983
|
+
"data-size": "md",
|
|
984
|
+
type: "submit",
|
|
985
|
+
disabled: busy || draft.key.trim() === "",
|
|
986
|
+
children: t("add")
|
|
987
|
+
})
|
|
988
|
+
]
|
|
989
|
+
})]
|
|
990
|
+
}),
|
|
991
|
+
!state.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
992
|
+
className: "pkc-note",
|
|
993
|
+
children: t("imagesUnavailable")
|
|
994
|
+
}),
|
|
995
|
+
!state.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
996
|
+
className: "pkc-note",
|
|
997
|
+
children: t("searchNotWritable")
|
|
998
|
+
})
|
|
999
|
+
] }),
|
|
1000
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1001
|
+
className: "pkc-error",
|
|
1002
|
+
role: "alert",
|
|
1003
|
+
children: error
|
|
1004
|
+
}),
|
|
1005
|
+
balanceError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1006
|
+
className: "pkc-error",
|
|
1007
|
+
role: "alert",
|
|
1008
|
+
children: `${t("imagesBalanceFailed")}: ${balanceError}`
|
|
1009
|
+
})
|
|
1010
|
+
]
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
/** A draft shortcut names this provider explicitly when other image tools exist. */
|
|
1014
|
+
function PackyCodeImageShortcut({ call, t, directory, prepare }) {
|
|
1015
|
+
const active = react.useSyncExternalStore((listener) => directory.subscribe(listener), () => directory.getSnapshot())?.current?.provider === "packycode";
|
|
1016
|
+
const [ready, setReady] = react.useState(false);
|
|
1017
|
+
const [error, setError] = react.useState();
|
|
1018
|
+
react.useEffect(() => {
|
|
1019
|
+
let live = true;
|
|
1020
|
+
const refresh = () => {
|
|
1021
|
+
if (active) call("images/status").then((value) => {
|
|
1022
|
+
if (live) setReady(value.imageEnabled && value.available && value.keys.some((key) => !key.disabled));
|
|
1023
|
+
}).catch(() => {
|
|
1024
|
+
if (live) setReady(false);
|
|
1025
|
+
});
|
|
1026
|
+
};
|
|
1027
|
+
setReady(false);
|
|
1028
|
+
refresh();
|
|
1029
|
+
window.addEventListener("packycode-images-updated", refresh);
|
|
1030
|
+
return () => {
|
|
1031
|
+
live = false;
|
|
1032
|
+
window.removeEventListener("packycode-images-updated", refresh);
|
|
1033
|
+
};
|
|
1034
|
+
}, [call, active]);
|
|
1035
|
+
if (!active || !ready) return null;
|
|
1036
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1037
|
+
className: "pkc-image-shortcut",
|
|
1038
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1039
|
+
className: "pkc-btn",
|
|
1040
|
+
type: "button",
|
|
1041
|
+
title: t("imagesShortcutHint"),
|
|
1042
|
+
onClick: () => {
|
|
1043
|
+
try {
|
|
1044
|
+
prepare(t("imagesDraft"));
|
|
1045
|
+
setError(void 0);
|
|
1046
|
+
} catch {
|
|
1047
|
+
setError(t("imagesDraftBusy"));
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
children: t("imagesCreate")
|
|
1051
|
+
}), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1052
|
+
className: "pkc-error",
|
|
1053
|
+
role: "alert",
|
|
1054
|
+
children: error
|
|
1055
|
+
})]
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
function GeneratedImage({ attachment, loadImage, openImage, t }) {
|
|
1059
|
+
const [url, setUrl] = react.useState();
|
|
1060
|
+
const [failed, setFailed] = react.useState(false);
|
|
1061
|
+
const [attempt, setAttempt] = react.useState(0);
|
|
1062
|
+
react.useEffect(() => {
|
|
1063
|
+
let live = true;
|
|
1064
|
+
setUrl(void 0);
|
|
1065
|
+
setFailed(false);
|
|
1066
|
+
Promise.resolve().then(() => loadImage(attachment)).then((value) => {
|
|
1067
|
+
if (live) setUrl(value);
|
|
1068
|
+
}, () => {
|
|
1069
|
+
if (live) setFailed(true);
|
|
1070
|
+
});
|
|
1071
|
+
return () => {
|
|
1072
|
+
live = false;
|
|
1073
|
+
};
|
|
1074
|
+
}, [
|
|
1075
|
+
attachment,
|
|
1076
|
+
loadImage,
|
|
1077
|
+
attempt
|
|
1078
|
+
]);
|
|
1079
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1080
|
+
className: "pkc-generated-image",
|
|
1081
|
+
type: "button",
|
|
1082
|
+
title: t("imagesOpen"),
|
|
1083
|
+
"aria-label": t("imagesOpen"),
|
|
1084
|
+
disabled: !url && !failed,
|
|
1085
|
+
onClick: (event) => {
|
|
1086
|
+
if (failed) {
|
|
1087
|
+
setAttempt((value) => value + 1);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
openImage(url, attachment, event.currentTarget);
|
|
1091
|
+
},
|
|
1092
|
+
children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1093
|
+
src: url,
|
|
1094
|
+
alt: attachment.name ?? t("imagesTitle")
|
|
1095
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: failed ? t("imagesRetryPreview") : t("imagesLoadingPreview") })
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
function PackyCodeImageResult({ block, t, loadImage, openImage }) {
|
|
1099
|
+
const settled = block?.kind === "tool-result";
|
|
1100
|
+
const failed = settled && block.isError === true;
|
|
1101
|
+
const images = settled ? (block.content ?? []).filter((part) => part.type === "image" && part.attachment) : [];
|
|
1102
|
+
const error = failed ? block.content?.find((part) => part.type === "text")?.text : void 0;
|
|
1103
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1104
|
+
className: "pkc-image-result",
|
|
1105
|
+
children: [
|
|
1106
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1107
|
+
className: "pkc-status",
|
|
1108
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("imagesTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1109
|
+
className: "pkc-meta",
|
|
1110
|
+
children: ["PackyCode · ", t(!settled ? "imagesGenerating" : failed ? "imagesFailed" : "imagesDone")]
|
|
1111
|
+
})]
|
|
1112
|
+
}),
|
|
1113
|
+
images.map((part) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GeneratedImage, {
|
|
1114
|
+
attachment: part.attachment,
|
|
1115
|
+
t,
|
|
1116
|
+
loadImage,
|
|
1117
|
+
openImage
|
|
1118
|
+
}, part.attachment.attachmentId)),
|
|
1119
|
+
typeof block?.meta?.requestedModel === "string" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1120
|
+
className: "pkc-meta",
|
|
1121
|
+
children: [
|
|
1122
|
+
block.meta.requestedModel,
|
|
1123
|
+
" · ",
|
|
1124
|
+
String(block.meta.requestedQuality ?? "auto"),
|
|
1125
|
+
" · ",
|
|
1126
|
+
block.meta.width,
|
|
1127
|
+
" × ",
|
|
1128
|
+
block.meta.height
|
|
1129
|
+
]
|
|
1130
|
+
}),
|
|
1131
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1132
|
+
className: "pkc-error",
|
|
1133
|
+
role: "alert",
|
|
1134
|
+
children: error
|
|
1135
|
+
})
|
|
1136
|
+
]
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
function ImageChatOutput({ node, ...props }) {
|
|
1140
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1141
|
+
className: "pkc-image-chat",
|
|
1142
|
+
children: node.data.blocks.map((block) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackyCodeImageResult, {
|
|
1143
|
+
block,
|
|
1144
|
+
...props
|
|
1145
|
+
}, block.toolCallId))
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
function installImagePresentation(ctx, t) {
|
|
1149
|
+
ctx.inject(["uiConversation"], (scope) => {
|
|
1150
|
+
const conversation = scope.get("uiConversation");
|
|
1151
|
+
if (typeof conversation?.events?.register !== "function" || typeof conversation?.imageUrl !== "function") return;
|
|
1152
|
+
const imageProps = (sessionId) => ({
|
|
1153
|
+
t,
|
|
1154
|
+
loadImage: (attachment) => conversation.imageUrl(sessionId, attachment),
|
|
1155
|
+
openImage: (src, attachment, opener) => {
|
|
1156
|
+
if (scope.get("nativeImageViewer")?.open?.({
|
|
1157
|
+
items: [{
|
|
1158
|
+
id: attachment.attachmentId,
|
|
1159
|
+
src,
|
|
1160
|
+
name: attachment.name ?? "packycode-generated.png",
|
|
1161
|
+
width: attachment.width,
|
|
1162
|
+
height: attachment.height
|
|
1163
|
+
}],
|
|
1164
|
+
opener,
|
|
1165
|
+
source: "packycode-generated"
|
|
1166
|
+
}) === true) return;
|
|
1167
|
+
window.open(src, "_blank", "noopener,noreferrer");
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
scope.effect(() => conversation.events.register(imageConversationNode), "packycode-api: image results in chat");
|
|
1171
|
+
scope.slots.inject("conversation.chat.node", () => scope.slots.register({
|
|
1172
|
+
name: "conversation.chat.node",
|
|
1173
|
+
key: IMAGE_CHAT_KIND,
|
|
1174
|
+
inject: imageProps
|
|
1175
|
+
}, ImageChatOutput));
|
|
1176
|
+
scope.slots.inject("tool.call.toolview", () => scope.slots.register({
|
|
1177
|
+
name: "tool.call.toolview",
|
|
1178
|
+
key: "packycode_image_generate",
|
|
1179
|
+
inject: imageProps
|
|
1180
|
+
}, PackyCodeImageResult));
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
//#endregion
|
|
471
1184
|
//#region src/client.jsx
|
|
472
1185
|
/**
|
|
473
1186
|
* Browser half: the PackyCode settings page and the composer balance chip.
|
|
@@ -517,52 +1230,15 @@ window.__ModuleLoader__.load({
|
|
|
517
1230
|
});
|
|
518
1231
|
}
|
|
519
1232
|
const useCaller = (rpc) => react.useMemo(() => channelCaller(rpc), [rpc]);
|
|
520
|
-
/** Substitute `{name}` placeholders in a copy string. */
|
|
521
|
-
const fill = (text, values) => text.replace(/\{(\w+)\}/gu, (match, name) => values[name] === void 0 ? match : String(values[name]));
|
|
522
|
-
function formatUsd(value) {
|
|
523
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
524
|
-
try {
|
|
525
|
-
return new Intl.NumberFormat(void 0, {
|
|
526
|
-
style: "currency",
|
|
527
|
-
currency: "USD"
|
|
528
|
-
}).format(value);
|
|
529
|
-
} catch {
|
|
530
|
-
return `$${value.toFixed(2)}`;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
/** The clock time of an epoch-millisecond reading, for a "last read at" note. */
|
|
534
|
-
function readTime(ms) {
|
|
535
|
-
if (typeof ms !== "number" || !Number.isFinite(ms)) return "—";
|
|
536
|
-
try {
|
|
537
|
-
return new Date(ms).toLocaleTimeString(void 0, {
|
|
538
|
-
hour: "2-digit",
|
|
539
|
-
minute: "2-digit"
|
|
540
|
-
});
|
|
541
|
-
} catch {
|
|
542
|
-
return new Date(ms).toISOString().slice(11, 16);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
1233
|
function clearMessage(error) {
|
|
546
1234
|
const text = typeof error?.message === "string" && error.message.length > 0 ? error.message : String(error);
|
|
547
1235
|
return text.length > 200 ? text.slice(0, 200) : text;
|
|
548
1236
|
}
|
|
549
|
-
const RESET_KEYS = {
|
|
550
|
-
monthly: "resetMonthly",
|
|
551
|
-
weekly: "resetWeekly",
|
|
552
|
-
daily: "resetDaily"
|
|
553
|
-
};
|
|
554
|
-
const resetLabel = (period, t) => RESET_KEYS[period] === void 0 ? void 0 : t(RESET_KEYS[period]);
|
|
555
1237
|
/** Active key's usage row, preferring the key the ring currently points at. */
|
|
556
1238
|
function activeUsage(balance) {
|
|
557
1239
|
if (balance === void 0) return void 0;
|
|
558
1240
|
return balance.keys?.find((entry) => entry.id === balance.activeId) ?? balance.keys?.[0];
|
|
559
1241
|
}
|
|
560
|
-
/** How much of a key's limit is left, for its bar; undefined without a limit. */
|
|
561
|
-
function remainingShare(usage) {
|
|
562
|
-
if (usage?.unlimitedQuota === true || typeof usage?.limitUsd !== "number" || usage.limitUsd <= 0) return void 0;
|
|
563
|
-
if (typeof usage.availableUsd !== "number") return void 0;
|
|
564
|
-
return Math.max(0, Math.min(1, usage.availableUsd / usage.limitUsd));
|
|
565
|
-
}
|
|
566
1242
|
/** Which catalog families a key reaches, from its per-model coverage. */
|
|
567
1243
|
function reachOf(coverage) {
|
|
568
1244
|
return MODEL_FAMILIES.flatMap(([name, models]) => {
|
|
@@ -661,14 +1337,6 @@ window.__ModuleLoader__.load({
|
|
|
661
1337
|
] });
|
|
662
1338
|
}
|
|
663
1339
|
function KeyRow({ entry, row, group, descriptions, groupsLoading, busy, renaming, onRename, onAction, t }) {
|
|
664
|
-
const usage = row?.usage;
|
|
665
|
-
const share = remainingShare(usage);
|
|
666
|
-
const figures = [
|
|
667
|
-
usage?.unlimitedQuota === true ? [t("keyRemaining"), t("keyUnlimited")] : usage?.availableUsd === void 0 ? void 0 : [t("keyRemaining"), formatUsd(usage.availableUsd)],
|
|
668
|
-
usage?.usedUsd === void 0 ? void 0 : [t("keyUsed"), formatUsd(usage.usedUsd)],
|
|
669
|
-
usage?.limitUsd === void 0 ? void 0 : [t("accountLimit"), formatUsd(usage.limitUsd)]
|
|
670
|
-
].filter(Boolean);
|
|
671
|
-
const meta = [usage?.name === void 0 ? t("keyNameUnknown") : `${t("keyName")} ${usage.name}`, resetLabel(usage?.resetPeriod, t)].filter(Boolean).join(" · ");
|
|
672
1340
|
const editing = renaming?.id === entry.id;
|
|
673
1341
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
674
1342
|
className: "pkc-row",
|
|
@@ -750,29 +1418,9 @@ window.__ModuleLoader__.load({
|
|
|
750
1418
|
]
|
|
751
1419
|
})]
|
|
752
1420
|
}),
|
|
753
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}),
|
|
757
|
-
figures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
758
|
-
className: "pkc-figures",
|
|
759
|
-
children: figures.map(([label, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: value })] }, label))
|
|
760
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
761
|
-
className: "pkc-meta",
|
|
762
|
-
children: row?.error === void 0 ? t("keyNoBalance") : `${t("loadFailed")}: ${row.error}`
|
|
763
|
-
}),
|
|
764
|
-
share !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
765
|
-
className: "pkc-bar",
|
|
766
|
-
"data-level": share < .1 ? "low" : share < .3 ? "warn" : "ok",
|
|
767
|
-
role: "presentation",
|
|
768
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round(share * 100)}%` } })
|
|
769
|
-
}),
|
|
770
|
-
row?.stale !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
771
|
-
className: "pkc-warn",
|
|
772
|
-
children: fill(t("staleNote"), {
|
|
773
|
-
time: readTime(row.stale.fetchedAt),
|
|
774
|
-
error: row.stale.error ?? ""
|
|
775
|
-
})
|
|
1421
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyBalance, {
|
|
1422
|
+
row,
|
|
1423
|
+
t
|
|
776
1424
|
}),
|
|
777
1425
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyGroups, {
|
|
778
1426
|
group,
|
|
@@ -861,8 +1509,6 @@ window.__ModuleLoader__.load({
|
|
|
861
1509
|
};
|
|
862
1510
|
const keys = status?.keys ?? [];
|
|
863
1511
|
const rows = balance?.keys ?? [];
|
|
864
|
-
const totals = sumBalances(rows);
|
|
865
|
-
const staleCount = rows.filter((row) => row.stale !== void 0).length;
|
|
866
1512
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
867
1513
|
title: t("accountsTitle"),
|
|
868
1514
|
aside: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -883,44 +1529,9 @@ window.__ModuleLoader__.load({
|
|
|
883
1529
|
className: "pkc-note",
|
|
884
1530
|
children: t("empty")
|
|
885
1531
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
886
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
890
|
-
className: "pkc-stat",
|
|
891
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
892
|
-
className: "pkc-stat-label",
|
|
893
|
-
children: t("totalRemaining")
|
|
894
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
895
|
-
className: "pkc-stat-value",
|
|
896
|
-
children: [totals.counted === 0 ? "—" : formatUsd(totals.remainingUsd) ?? "—", totals.unlimited > 0 && ` + ${t("keyUnlimited")}`]
|
|
897
|
-
})]
|
|
898
|
-
}),
|
|
899
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
900
|
-
className: "pkc-stat",
|
|
901
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
902
|
-
className: "pkc-stat-label",
|
|
903
|
-
children: t("totalUsed")
|
|
904
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
905
|
-
className: "pkc-stat-value",
|
|
906
|
-
children: totals.counted === 0 ? "—" : formatUsd(totals.usedUsd) ?? "—"
|
|
907
|
-
})]
|
|
908
|
-
}),
|
|
909
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
910
|
-
className: "pkc-stat",
|
|
911
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
912
|
-
className: "pkc-stat-label",
|
|
913
|
-
children: t("totalLimit")
|
|
914
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
915
|
-
className: "pkc-stat-value",
|
|
916
|
-
children: totals.counted === 0 ? "—" : totals.unlimited > 0 ? t("keyUnlimited") : formatUsd(totals.limitUsd) ?? "—"
|
|
917
|
-
})]
|
|
918
|
-
})
|
|
919
|
-
]
|
|
920
|
-
}),
|
|
921
|
-
(totals.unread > 0 || staleCount > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
922
|
-
className: "pkc-note",
|
|
923
|
-
children: [totals.unread > 0 ? fill(t("totalPartial"), { count: totals.unread }) : void 0, staleCount > 0 ? fill(t("totalStale"), { count: staleCount }) : void 0].filter(Boolean).join(" ")
|
|
1532
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BalanceTotals, {
|
|
1533
|
+
rows,
|
|
1534
|
+
t
|
|
924
1535
|
}),
|
|
925
1536
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
926
1537
|
className: "pkc-list",
|
|
@@ -1584,6 +2195,10 @@ window.__ModuleLoader__.load({
|
|
|
1584
2195
|
call,
|
|
1585
2196
|
t
|
|
1586
2197
|
}),
|
|
2198
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackyCodeImagesCard, {
|
|
2199
|
+
call,
|
|
2200
|
+
t
|
|
2201
|
+
}),
|
|
1587
2202
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsCard, {
|
|
1588
2203
|
call,
|
|
1589
2204
|
t
|
|
@@ -1661,6 +2276,8 @@ window.__ModuleLoader__.load({
|
|
|
1661
2276
|
}, "packycode-api: style");
|
|
1662
2277
|
const connection = ctx.get("connection");
|
|
1663
2278
|
const t = ctx.locale.bind(NS);
|
|
2279
|
+
installImagePresentation(ctx, t);
|
|
2280
|
+
const call = channelCaller(connection.rpc);
|
|
1664
2281
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1665
2282
|
name: "settings.section",
|
|
1666
2283
|
id: "packycode-api",
|
|
@@ -1683,6 +2300,29 @@ window.__ModuleLoader__.load({
|
|
|
1683
2300
|
directory: ctx.modelDirectories.directoryFor(sessionId).store
|
|
1684
2301
|
})
|
|
1685
2302
|
}, PackyCodeComposerQuota));
|
|
2303
|
+
ctx.inject(["sessions", "conversation"], (scope) => {
|
|
2304
|
+
const sessions = scope.get("sessions");
|
|
2305
|
+
const conversation = scope.get("conversation");
|
|
2306
|
+
if (typeof sessions?.scope !== "function" || typeof conversation?.input?.for !== "function") return;
|
|
2307
|
+
scope.slots.inject("conversation.input.left", () => scope.slots.register({
|
|
2308
|
+
name: "conversation.input.left",
|
|
2309
|
+
id: "packycode-image-create",
|
|
2310
|
+
order: 31,
|
|
2311
|
+
inject: (sessionId) => ({
|
|
2312
|
+
call,
|
|
2313
|
+
t,
|
|
2314
|
+
directory: ctx.modelDirectories.directoryFor(sessionId).store,
|
|
2315
|
+
prepare: (text) => {
|
|
2316
|
+
const session = sessions.scope(sessionId);
|
|
2317
|
+
if (session === void 0) throw new Error("Composer unavailable");
|
|
2318
|
+
const input = conversation.input.for(session);
|
|
2319
|
+
const draft = input.state.getSnapshot();
|
|
2320
|
+
if (draft.phase !== "plain" || draft.occurrences?.length) throw new Error("Composer busy");
|
|
2321
|
+
input.setDraft([draft.draft, text].filter(Boolean).join("\n\n"));
|
|
2322
|
+
}
|
|
2323
|
+
})
|
|
2324
|
+
}, PackyCodeImageShortcut));
|
|
2325
|
+
});
|
|
1686
2326
|
}
|
|
1687
2327
|
//#endregion
|
|
1688
2328
|
exports.apply = apply;
|