dsh-packycode-api 1.1.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.en.md +32 -4
- package/README.md +29 -5
- package/docs/research/image-api.md +75 -0
- package/lib/client.js +727 -108
- package/lib/index.js +1037 -216
- 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,46 @@ 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 successfulImages = (event) => event?.type === "tool/result" && event.data?.meta?.kind === "packycode-image" ? (event.data.message?.content ?? []).filter((block) => block.type === "tool-result" && !block.isError && block.content?.some((part) => part.type === "image" && part.attachment)) : [];
|
|
497
|
+
const imageConversationNode = {
|
|
498
|
+
kind: IMAGE_CHAT_KIND,
|
|
499
|
+
target: "chat",
|
|
500
|
+
match(event) {
|
|
501
|
+
return event.type === "turn/end" || successfulImages(event).length > 0 ? {
|
|
502
|
+
id: String(event.data.turn),
|
|
503
|
+
role: "update"
|
|
504
|
+
} : null;
|
|
505
|
+
},
|
|
506
|
+
start: () => void 0,
|
|
507
|
+
update: (context) => context.state,
|
|
508
|
+
buildViewNode(context) {
|
|
509
|
+
const finished = context.matches.find((match) => match.event.type === "turn/end");
|
|
510
|
+
if (finished === void 0) return null;
|
|
511
|
+
const results = context.matches.filter((match) => successfulImages(match.event).length > 0);
|
|
512
|
+
if (results.length === 0) return null;
|
|
513
|
+
const blocks = results.flatMap(({ event }) => successfulImages(event).map((block) => ({
|
|
514
|
+
...block,
|
|
515
|
+
kind: "tool-result",
|
|
516
|
+
meta: event.data.meta
|
|
517
|
+
})));
|
|
518
|
+
const answerSeq = finished.location.turn?.steps?.at(-1)?.data.get("assistant-step")?.finalNode?.seq;
|
|
519
|
+
const lastImageSeq = Math.max(...results.map((match) => match.event.seq));
|
|
520
|
+
return {
|
|
521
|
+
key: context.key,
|
|
522
|
+
id: context.id,
|
|
523
|
+
kind: IMAGE_CHAT_KIND,
|
|
524
|
+
target: "chat",
|
|
525
|
+
location: finished.location,
|
|
526
|
+
visibility: "visible",
|
|
527
|
+
anchorSeq: answerSeq > lastImageSeq ? answerSeq + .025 : finished.event.seq - .025,
|
|
528
|
+
data: { blocks }
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
};
|
|
419
532
|
//#endregion
|
|
420
533
|
//#region src/totals.js
|
|
421
534
|
/**
|
|
@@ -468,6 +581,584 @@ window.__ModuleLoader__.load({
|
|
|
468
581
|
return total;
|
|
469
582
|
}
|
|
470
583
|
//#endregion
|
|
584
|
+
//#region src/client-balance.jsx
|
|
585
|
+
const fill = (text, values) => text.replace(/\{(\w+)\}/gu, (match, name) => values[name] === void 0 ? match : String(values[name]));
|
|
586
|
+
function formatUsd(value) {
|
|
587
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
588
|
+
try {
|
|
589
|
+
return new Intl.NumberFormat(void 0, {
|
|
590
|
+
style: "currency",
|
|
591
|
+
currency: "USD"
|
|
592
|
+
}).format(value);
|
|
593
|
+
} catch {
|
|
594
|
+
return `$${value.toFixed(2)}`;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function readTime(ms) {
|
|
598
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "—";
|
|
599
|
+
try {
|
|
600
|
+
return new Date(ms).toLocaleTimeString(void 0, {
|
|
601
|
+
hour: "2-digit",
|
|
602
|
+
minute: "2-digit"
|
|
603
|
+
});
|
|
604
|
+
} catch {
|
|
605
|
+
return new Date(ms).toISOString().slice(11, 16);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const RESET_KEYS = {
|
|
609
|
+
monthly: "resetMonthly",
|
|
610
|
+
weekly: "resetWeekly",
|
|
611
|
+
daily: "resetDaily",
|
|
612
|
+
never: "resetNever"
|
|
613
|
+
};
|
|
614
|
+
/** The same quota presentation for conversation and image credential records. */
|
|
615
|
+
function KeyBalance({ row, t }) {
|
|
616
|
+
const usage = row?.usage;
|
|
617
|
+
const figures = [
|
|
618
|
+
usage?.unlimitedQuota === true ? [t("keyRemaining"), t("keyUnlimited")] : usage?.availableUsd === void 0 ? void 0 : [t("keyRemaining"), formatUsd(usage.availableUsd)],
|
|
619
|
+
usage?.usedUsd === void 0 ? void 0 : [t("keyUsed"), formatUsd(usage.usedUsd)],
|
|
620
|
+
usage?.limitUsd === void 0 ? void 0 : [t("accountLimit"), formatUsd(usage.limitUsd)]
|
|
621
|
+
].filter(Boolean);
|
|
622
|
+
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(" · ");
|
|
623
|
+
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;
|
|
624
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
625
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
626
|
+
className: "pkc-meta",
|
|
627
|
+
children: meta
|
|
628
|
+
}),
|
|
629
|
+
figures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
630
|
+
className: "pkc-figures",
|
|
631
|
+
children: figures.map(([label, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: value })] }, label))
|
|
632
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
633
|
+
className: "pkc-meta",
|
|
634
|
+
children: row?.error === void 0 ? t("keyNoBalance") : `${t("loadFailed")}: ${row.error}`
|
|
635
|
+
}),
|
|
636
|
+
share !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
637
|
+
className: "pkc-bar",
|
|
638
|
+
"data-level": share < .1 ? "low" : share < .3 ? "warn" : "ok",
|
|
639
|
+
role: "presentation",
|
|
640
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round(share * 100)}%` } })
|
|
641
|
+
}),
|
|
642
|
+
row?.stale !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
643
|
+
className: "pkc-warn",
|
|
644
|
+
children: fill(t("staleNote"), {
|
|
645
|
+
time: readTime(row.stale.fetchedAt),
|
|
646
|
+
error: row.stale.error ?? ""
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
] });
|
|
650
|
+
}
|
|
651
|
+
/** Only this card's keys contribute; account-wide billing is never added. */
|
|
652
|
+
function BalanceTotals({ rows, t }) {
|
|
653
|
+
const totals = sumBalances(rows);
|
|
654
|
+
const stale = rows.filter((row) => row.stale !== void 0).length;
|
|
655
|
+
const known = (field) => rows.some((row) => row.usage?.unlimitedQuota !== true && Number.isFinite(row.usage?.[field]));
|
|
656
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
657
|
+
className: "pkc-stats",
|
|
658
|
+
children: [
|
|
659
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
660
|
+
className: "pkc-stat",
|
|
661
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
662
|
+
className: "pkc-stat-label",
|
|
663
|
+
children: t("totalRemaining")
|
|
664
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
665
|
+
className: "pkc-stat-value",
|
|
666
|
+
children: [known("availableUsd") ? formatUsd(totals.remainingUsd) : totals.unlimited > 0 ? t("keyUnlimited") : "—", known("availableUsd") && totals.unlimited > 0 && ` + ${t("keyUnlimited")}`]
|
|
667
|
+
})]
|
|
668
|
+
}),
|
|
669
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
670
|
+
className: "pkc-stat",
|
|
671
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
672
|
+
className: "pkc-stat-label",
|
|
673
|
+
children: t("totalUsed")
|
|
674
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
675
|
+
className: "pkc-stat-value",
|
|
676
|
+
children: known("usedUsd") ? formatUsd(totals.usedUsd) : "—"
|
|
677
|
+
})]
|
|
678
|
+
}),
|
|
679
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
680
|
+
className: "pkc-stat",
|
|
681
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
682
|
+
className: "pkc-stat-label",
|
|
683
|
+
children: t("totalLimit")
|
|
684
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
685
|
+
className: "pkc-stat-value",
|
|
686
|
+
children: totals.unlimited > 0 ? t("keyUnlimited") : known("limitUsd") ? formatUsd(totals.limitUsd) : "—"
|
|
687
|
+
})]
|
|
688
|
+
})
|
|
689
|
+
]
|
|
690
|
+
}), (totals.unread > 0 || stale > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
691
|
+
className: "pkc-note",
|
|
692
|
+
children: [totals.unread > 0 ? fill(t("totalPartial"), { count: totals.unread }) : void 0, stale > 0 ? fill(t("totalStale"), { count: stale }) : void 0].filter(Boolean).join(" ")
|
|
693
|
+
})] });
|
|
694
|
+
}
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/client-images.jsx
|
|
697
|
+
const imageModelLabel = (id) => id.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
|
|
698
|
+
/** Only projections come back from the host; the password draft is cleared on success. */
|
|
699
|
+
function PackyCodeImagesCard({ call, t }) {
|
|
700
|
+
const [state, setState] = react.useState();
|
|
701
|
+
const [draft, setDraft] = react.useState({
|
|
702
|
+
key: "",
|
|
703
|
+
label: ""
|
|
704
|
+
});
|
|
705
|
+
const [busy, setBusy] = react.useState(false);
|
|
706
|
+
const [error, setError] = react.useState();
|
|
707
|
+
const [balance, setBalance] = react.useState();
|
|
708
|
+
const [balanceError, setBalanceError] = react.useState();
|
|
709
|
+
const [balanceLoading, setBalanceLoading] = react.useState(true);
|
|
710
|
+
const balanceGeneration = react.useRef(0);
|
|
711
|
+
const mounted = react.useRef(false);
|
|
712
|
+
const readBalance = async (force = false) => {
|
|
713
|
+
if (!mounted.current) return;
|
|
714
|
+
const generation = ++balanceGeneration.current;
|
|
715
|
+
setBalanceLoading(true);
|
|
716
|
+
setBalanceError(void 0);
|
|
717
|
+
try {
|
|
718
|
+
const value = await call("images/balance", { force });
|
|
719
|
+
if (mounted.current && balanceGeneration.current === generation) setBalance(value);
|
|
720
|
+
} catch (failure) {
|
|
721
|
+
if (mounted.current && balanceGeneration.current === generation) setBalanceError(String(failure?.message ?? t("imagesBalanceFailed")).slice(0, 200));
|
|
722
|
+
} finally {
|
|
723
|
+
if (mounted.current && balanceGeneration.current === generation) setBalanceLoading(false);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
react.useEffect(() => {
|
|
727
|
+
mounted.current = true;
|
|
728
|
+
call("images/status").then((value) => {
|
|
729
|
+
if (mounted.current) setState(value);
|
|
730
|
+
}).catch(() => {
|
|
731
|
+
if (mounted.current) setError(t("imagesLoadFailed"));
|
|
732
|
+
});
|
|
733
|
+
readBalance();
|
|
734
|
+
const timer = window.setInterval(() => {
|
|
735
|
+
readBalance();
|
|
736
|
+
}, 6e4);
|
|
737
|
+
return () => {
|
|
738
|
+
mounted.current = false;
|
|
739
|
+
balanceGeneration.current += 1;
|
|
740
|
+
window.clearInterval(timer);
|
|
741
|
+
};
|
|
742
|
+
}, [call, t]);
|
|
743
|
+
const mutate = async (endpoint, payload = {}) => {
|
|
744
|
+
setBusy(true);
|
|
745
|
+
setError(void 0);
|
|
746
|
+
try {
|
|
747
|
+
const value = await call(endpoint, payload);
|
|
748
|
+
if (mounted.current) setState(value);
|
|
749
|
+
if (mounted.current && endpoint.startsWith("images/keys/")) {
|
|
750
|
+
setBalance(void 0);
|
|
751
|
+
readBalance(true);
|
|
752
|
+
}
|
|
753
|
+
window.dispatchEvent(new Event("packycode-images-updated"));
|
|
754
|
+
return true;
|
|
755
|
+
} catch (failure) {
|
|
756
|
+
if (mounted.current) setError(String(failure?.message ?? t("imagesLoadFailed")).slice(0, 200));
|
|
757
|
+
return false;
|
|
758
|
+
} finally {
|
|
759
|
+
if (mounted.current) setBusy(false);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
const disabled = busy || state?.writable !== true;
|
|
763
|
+
const models = state?.models ?? [];
|
|
764
|
+
const qualities = models.find((model) => model.id === state?.imageModel)?.qualities ?? [];
|
|
765
|
+
const rows = (state?.keys ?? []).map((entry) => balance?.keys?.find((row) => row.id === entry.id) ?? { id: entry.id });
|
|
766
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
767
|
+
className: "pkc-card",
|
|
768
|
+
children: [
|
|
769
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
770
|
+
className: "pkc-head",
|
|
771
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
772
|
+
className: "pkc-title",
|
|
773
|
+
children: t("imagesTitle")
|
|
774
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
775
|
+
className: "pkc-actions",
|
|
776
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
777
|
+
className: "pkc-chip",
|
|
778
|
+
"data-tone": "neutral",
|
|
779
|
+
children: t("imagesSeparateKey")
|
|
780
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
781
|
+
className: "pkc-btn",
|
|
782
|
+
type: "button",
|
|
783
|
+
disabled: busy || balanceLoading,
|
|
784
|
+
onClick: () => {
|
|
785
|
+
readBalance(true);
|
|
786
|
+
},
|
|
787
|
+
children: balanceLoading ? t("refreshing") : t("refresh")
|
|
788
|
+
})]
|
|
789
|
+
})]
|
|
790
|
+
}),
|
|
791
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
792
|
+
className: "pkc-note",
|
|
793
|
+
children: t("imagesIntro")
|
|
794
|
+
}),
|
|
795
|
+
state !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
797
|
+
className: "pkc-setting",
|
|
798
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
799
|
+
className: "pkc-label",
|
|
800
|
+
children: t("imagesEnabled")
|
|
801
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
802
|
+
className: "pkc-input pkc-image-choice",
|
|
803
|
+
value: String(state.imageEnabled),
|
|
804
|
+
disabled,
|
|
805
|
+
onChange: (event) => {
|
|
806
|
+
mutate("images/update", { imageEnabled: event.target.value === "true" });
|
|
807
|
+
},
|
|
808
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
809
|
+
value: "true",
|
|
810
|
+
children: t("imagesOn")
|
|
811
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
812
|
+
value: "false",
|
|
813
|
+
children: t("imagesOff")
|
|
814
|
+
})]
|
|
815
|
+
})]
|
|
816
|
+
}),
|
|
817
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
818
|
+
className: "pkc-setting",
|
|
819
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
820
|
+
className: "pkc-label",
|
|
821
|
+
children: t("imagesModel")
|
|
822
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
823
|
+
className: "pkc-input pkc-image-choice",
|
|
824
|
+
value: state.imageModel,
|
|
825
|
+
disabled: disabled || !state.imageEnabled,
|
|
826
|
+
onChange: (event) => {
|
|
827
|
+
mutate("images/update", {
|
|
828
|
+
imageModel: event.target.value,
|
|
829
|
+
imageQuality: "auto"
|
|
830
|
+
});
|
|
831
|
+
},
|
|
832
|
+
children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
833
|
+
value: model.id,
|
|
834
|
+
children: imageModelLabel(model.id)
|
|
835
|
+
}, model.id))
|
|
836
|
+
})]
|
|
837
|
+
}),
|
|
838
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
839
|
+
className: "pkc-setting",
|
|
840
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
841
|
+
className: "pkc-label",
|
|
842
|
+
children: t("imagesQuality")
|
|
843
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
844
|
+
className: "pkc-input pkc-image-choice",
|
|
845
|
+
value: state.imageQuality,
|
|
846
|
+
disabled: disabled || !state.imageEnabled,
|
|
847
|
+
onChange: (event) => {
|
|
848
|
+
mutate("images/update", { imageQuality: event.target.value });
|
|
849
|
+
},
|
|
850
|
+
children: qualities.map((quality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
851
|
+
value: quality,
|
|
852
|
+
children: t(`imagesQuality_${quality}`)
|
|
853
|
+
}, quality))
|
|
854
|
+
})]
|
|
855
|
+
}),
|
|
856
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
857
|
+
className: "pkc-note",
|
|
858
|
+
children: t("imagesQualityHint")
|
|
859
|
+
}),
|
|
860
|
+
(state.keys ?? []).length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BalanceTotals, {
|
|
861
|
+
rows,
|
|
862
|
+
t
|
|
863
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
864
|
+
className: "pkc-note",
|
|
865
|
+
children: balanceLoading ? t("imagesBalanceLoading") : t("imagesBalanceHint")
|
|
866
|
+
})] }),
|
|
867
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
868
|
+
className: "pkc-list",
|
|
869
|
+
children: (state.keys ?? []).map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
870
|
+
className: "pkc-row",
|
|
871
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
872
|
+
className: "pkc-row-top",
|
|
873
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
874
|
+
className: "pkc-row-name",
|
|
875
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
876
|
+
className: "pkc-name",
|
|
877
|
+
children: entry.label
|
|
878
|
+
}), entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
879
|
+
className: "pkc-chip",
|
|
880
|
+
"data-tone": "success",
|
|
881
|
+
children: t("keyActive")
|
|
882
|
+
})]
|
|
883
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
884
|
+
className: "pkc-actions",
|
|
885
|
+
children: [!entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
886
|
+
className: "pkc-btn",
|
|
887
|
+
type: "button",
|
|
888
|
+
disabled: busy,
|
|
889
|
+
onClick: () => {
|
|
890
|
+
mutate("images/keys/select", { id: entry.id });
|
|
891
|
+
},
|
|
892
|
+
children: t("keyUse")
|
|
893
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
894
|
+
className: "pkc-btn",
|
|
895
|
+
"data-variant": "danger",
|
|
896
|
+
type: "button",
|
|
897
|
+
disabled: busy,
|
|
898
|
+
onClick: () => {
|
|
899
|
+
mutate("images/keys/remove", { id: entry.id });
|
|
900
|
+
},
|
|
901
|
+
children: t("keyRemove")
|
|
902
|
+
})]
|
|
903
|
+
})]
|
|
904
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyBalance, {
|
|
905
|
+
row: rows.find((row) => row.id === entry.id),
|
|
906
|
+
t
|
|
907
|
+
})]
|
|
908
|
+
}, entry.id))
|
|
909
|
+
}),
|
|
910
|
+
(state.keys ?? []).length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
911
|
+
className: "pkc-note",
|
|
912
|
+
children: t("imagesNoKey")
|
|
913
|
+
}),
|
|
914
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
915
|
+
className: "pkc-add",
|
|
916
|
+
onSubmit: (event) => {
|
|
917
|
+
event.preventDefault();
|
|
918
|
+
if (busy || draft.key.trim() === "") return;
|
|
919
|
+
mutate("images/keys/add", draft).then((saved) => {
|
|
920
|
+
if (saved && mounted.current) setDraft({
|
|
921
|
+
key: "",
|
|
922
|
+
label: ""
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
},
|
|
926
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
927
|
+
className: "pkc-label",
|
|
928
|
+
children: t("imagesAddKey")
|
|
929
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
930
|
+
className: "pkc-form",
|
|
931
|
+
children: [
|
|
932
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
933
|
+
className: "pkc-input",
|
|
934
|
+
type: "password",
|
|
935
|
+
autoComplete: "off",
|
|
936
|
+
"aria-label": t("imagesAddKey"),
|
|
937
|
+
placeholder: t("imagesKeyPlaceholder"),
|
|
938
|
+
value: draft.key,
|
|
939
|
+
disabled: busy,
|
|
940
|
+
onChange: (event) => setDraft((current) => ({
|
|
941
|
+
...current,
|
|
942
|
+
key: event.target.value
|
|
943
|
+
}))
|
|
944
|
+
}),
|
|
945
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
946
|
+
className: "pkc-input",
|
|
947
|
+
type: "text",
|
|
948
|
+
maxLength: 60,
|
|
949
|
+
"aria-label": t("addLabelPlaceholder"),
|
|
950
|
+
placeholder: t("addLabelPlaceholder"),
|
|
951
|
+
value: draft.label,
|
|
952
|
+
disabled: busy,
|
|
953
|
+
onChange: (event) => setDraft((current) => ({
|
|
954
|
+
...current,
|
|
955
|
+
label: event.target.value
|
|
956
|
+
}))
|
|
957
|
+
}),
|
|
958
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
959
|
+
className: "pkc-btn",
|
|
960
|
+
"data-variant": "primary",
|
|
961
|
+
"data-size": "md",
|
|
962
|
+
type: "submit",
|
|
963
|
+
disabled: busy || draft.key.trim() === "",
|
|
964
|
+
children: t("add")
|
|
965
|
+
})
|
|
966
|
+
]
|
|
967
|
+
})]
|
|
968
|
+
}),
|
|
969
|
+
!state.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
970
|
+
className: "pkc-note",
|
|
971
|
+
children: t("imagesUnavailable")
|
|
972
|
+
}),
|
|
973
|
+
!state.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
974
|
+
className: "pkc-note",
|
|
975
|
+
children: t("searchNotWritable")
|
|
976
|
+
})
|
|
977
|
+
] }),
|
|
978
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
979
|
+
className: "pkc-error",
|
|
980
|
+
role: "alert",
|
|
981
|
+
children: error
|
|
982
|
+
}),
|
|
983
|
+
balanceError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
984
|
+
className: "pkc-error",
|
|
985
|
+
role: "alert",
|
|
986
|
+
children: `${t("imagesBalanceFailed")}: ${balanceError}`
|
|
987
|
+
})
|
|
988
|
+
]
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
/** A draft shortcut names this provider explicitly when other image tools exist. */
|
|
992
|
+
function PackyCodeImageShortcut({ call, t, directory, prepare }) {
|
|
993
|
+
const active = react.useSyncExternalStore((listener) => directory.subscribe(listener), () => directory.getSnapshot())?.current?.provider === "packycode";
|
|
994
|
+
const [ready, setReady] = react.useState(false);
|
|
995
|
+
const [error, setError] = react.useState();
|
|
996
|
+
react.useEffect(() => {
|
|
997
|
+
let live = true;
|
|
998
|
+
const refresh = () => {
|
|
999
|
+
if (active) call("images/status").then((value) => {
|
|
1000
|
+
if (live) setReady(value.imageEnabled && value.available && value.keys.some((key) => !key.disabled));
|
|
1001
|
+
}).catch(() => {
|
|
1002
|
+
if (live) setReady(false);
|
|
1003
|
+
});
|
|
1004
|
+
};
|
|
1005
|
+
setReady(false);
|
|
1006
|
+
refresh();
|
|
1007
|
+
window.addEventListener("packycode-images-updated", refresh);
|
|
1008
|
+
return () => {
|
|
1009
|
+
live = false;
|
|
1010
|
+
window.removeEventListener("packycode-images-updated", refresh);
|
|
1011
|
+
};
|
|
1012
|
+
}, [call, active]);
|
|
1013
|
+
if (!active || !ready) return null;
|
|
1014
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1015
|
+
className: "pkc-image-shortcut",
|
|
1016
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1017
|
+
className: "pkc-btn",
|
|
1018
|
+
type: "button",
|
|
1019
|
+
title: t("imagesShortcutHint"),
|
|
1020
|
+
onClick: () => {
|
|
1021
|
+
try {
|
|
1022
|
+
prepare(t("imagesDraft"));
|
|
1023
|
+
setError(void 0);
|
|
1024
|
+
} catch {
|
|
1025
|
+
setError(t("imagesDraftBusy"));
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
children: t("imagesCreate")
|
|
1029
|
+
}), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1030
|
+
className: "pkc-error",
|
|
1031
|
+
role: "alert",
|
|
1032
|
+
children: error
|
|
1033
|
+
})]
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
function GeneratedImage({ attachment, loadImage, openImage, t }) {
|
|
1037
|
+
const [url, setUrl] = react.useState();
|
|
1038
|
+
const [failed, setFailed] = react.useState(false);
|
|
1039
|
+
const [attempt, setAttempt] = react.useState(0);
|
|
1040
|
+
react.useEffect(() => {
|
|
1041
|
+
let live = true;
|
|
1042
|
+
setUrl(void 0);
|
|
1043
|
+
setFailed(false);
|
|
1044
|
+
Promise.resolve().then(() => loadImage(attachment)).then((value) => {
|
|
1045
|
+
if (live) setUrl(value);
|
|
1046
|
+
}, () => {
|
|
1047
|
+
if (live) setFailed(true);
|
|
1048
|
+
});
|
|
1049
|
+
return () => {
|
|
1050
|
+
live = false;
|
|
1051
|
+
};
|
|
1052
|
+
}, [
|
|
1053
|
+
attachment,
|
|
1054
|
+
loadImage,
|
|
1055
|
+
attempt
|
|
1056
|
+
]);
|
|
1057
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1058
|
+
className: "pkc-generated-image",
|
|
1059
|
+
type: "button",
|
|
1060
|
+
title: t("imagesOpen"),
|
|
1061
|
+
"aria-label": t("imagesOpen"),
|
|
1062
|
+
disabled: !url && !failed,
|
|
1063
|
+
onClick: (event) => {
|
|
1064
|
+
if (failed) {
|
|
1065
|
+
setAttempt((value) => value + 1);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
openImage(url, attachment, event.currentTarget);
|
|
1069
|
+
},
|
|
1070
|
+
children: url ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1071
|
+
src: url,
|
|
1072
|
+
alt: attachment.name ?? t("imagesTitle")
|
|
1073
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: failed ? t("imagesRetryPreview") : t("imagesLoadingPreview") })
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
function PackyCodeImageResult({ block, t, loadImage, openImage }) {
|
|
1077
|
+
const settled = block?.kind === "tool-result";
|
|
1078
|
+
const failed = settled && block.isError === true;
|
|
1079
|
+
const images = settled ? (block.content ?? []).filter((part) => part.type === "image" && part.attachment) : [];
|
|
1080
|
+
const error = failed ? block.content?.find((part) => part.type === "text")?.text : void 0;
|
|
1081
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1082
|
+
className: "pkc-image-result",
|
|
1083
|
+
children: [
|
|
1084
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1085
|
+
className: "pkc-status",
|
|
1086
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("imagesTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1087
|
+
className: "pkc-meta",
|
|
1088
|
+
children: ["PackyCode · ", t(!settled ? "imagesGenerating" : failed ? "imagesFailed" : "imagesDone")]
|
|
1089
|
+
})]
|
|
1090
|
+
}),
|
|
1091
|
+
images.map((part) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GeneratedImage, {
|
|
1092
|
+
attachment: part.attachment,
|
|
1093
|
+
t,
|
|
1094
|
+
loadImage,
|
|
1095
|
+
openImage
|
|
1096
|
+
}, part.attachment.attachmentId)),
|
|
1097
|
+
typeof block?.meta?.requestedModel === "string" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1098
|
+
className: "pkc-meta",
|
|
1099
|
+
children: [
|
|
1100
|
+
block.meta.requestedModel,
|
|
1101
|
+
" · ",
|
|
1102
|
+
String(block.meta.requestedQuality ?? "auto"),
|
|
1103
|
+
" · ",
|
|
1104
|
+
block.meta.width,
|
|
1105
|
+
" × ",
|
|
1106
|
+
block.meta.height
|
|
1107
|
+
]
|
|
1108
|
+
}),
|
|
1109
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1110
|
+
className: "pkc-error",
|
|
1111
|
+
role: "alert",
|
|
1112
|
+
children: error
|
|
1113
|
+
})
|
|
1114
|
+
]
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
function ImageChatOutput({ node, ...props }) {
|
|
1118
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1119
|
+
className: "pkc-image-chat",
|
|
1120
|
+
children: node.data.blocks.map((block) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackyCodeImageResult, {
|
|
1121
|
+
block,
|
|
1122
|
+
...props
|
|
1123
|
+
}, block.toolCallId))
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
function installImagePresentation(ctx, t) {
|
|
1127
|
+
ctx.inject(["uiConversation"], (scope) => {
|
|
1128
|
+
const conversation = scope.get("uiConversation");
|
|
1129
|
+
if (typeof conversation?.events?.register !== "function" || typeof conversation?.imageUrl !== "function") return;
|
|
1130
|
+
const imageProps = (sessionId) => ({
|
|
1131
|
+
t,
|
|
1132
|
+
loadImage: (attachment) => conversation.imageUrl(sessionId, attachment),
|
|
1133
|
+
openImage: (src, attachment, opener) => {
|
|
1134
|
+
if (scope.get("nativeImageViewer")?.open?.({
|
|
1135
|
+
items: [{
|
|
1136
|
+
id: attachment.attachmentId,
|
|
1137
|
+
src,
|
|
1138
|
+
name: attachment.name ?? "packycode-generated.png",
|
|
1139
|
+
width: attachment.width,
|
|
1140
|
+
height: attachment.height
|
|
1141
|
+
}],
|
|
1142
|
+
opener,
|
|
1143
|
+
source: "packycode-generated"
|
|
1144
|
+
}) === true) return;
|
|
1145
|
+
window.open(src, "_blank", "noopener,noreferrer");
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
scope.effect(() => conversation.events.register(imageConversationNode), "packycode-api: image results in chat");
|
|
1149
|
+
scope.slots.inject("conversation.chat.node", () => scope.slots.register({
|
|
1150
|
+
name: "conversation.chat.node",
|
|
1151
|
+
key: IMAGE_CHAT_KIND,
|
|
1152
|
+
inject: imageProps
|
|
1153
|
+
}, ImageChatOutput));
|
|
1154
|
+
scope.slots.inject("tool.call.toolview", () => scope.slots.register({
|
|
1155
|
+
name: "tool.call.toolview",
|
|
1156
|
+
key: "packycode_image_generate",
|
|
1157
|
+
inject: imageProps
|
|
1158
|
+
}, PackyCodeImageResult));
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
//#endregion
|
|
471
1162
|
//#region src/client.jsx
|
|
472
1163
|
/**
|
|
473
1164
|
* Browser half: the PackyCode settings page and the composer balance chip.
|
|
@@ -498,6 +1189,7 @@ window.__ModuleLoader__.load({
|
|
|
498
1189
|
["DeepSeek", ["deepseek-flash"]],
|
|
499
1190
|
["GPT", ["gpt-6.1-sol", "gpt-6-astra"]],
|
|
500
1191
|
["Grok", ["grok-4.7"]],
|
|
1192
|
+
["Gemini", ["gemini-4-argon", "gemini-3.8-flash"]],
|
|
501
1193
|
["Claude", [
|
|
502
1194
|
"claude-opus-5-5",
|
|
503
1195
|
"claude-sonnet-5-5",
|
|
@@ -516,52 +1208,15 @@ window.__ModuleLoader__.load({
|
|
|
516
1208
|
});
|
|
517
1209
|
}
|
|
518
1210
|
const useCaller = (rpc) => react.useMemo(() => channelCaller(rpc), [rpc]);
|
|
519
|
-
/** Substitute `{name}` placeholders in a copy string. */
|
|
520
|
-
const fill = (text, values) => text.replace(/\{(\w+)\}/gu, (match, name) => values[name] === void 0 ? match : String(values[name]));
|
|
521
|
-
function formatUsd(value) {
|
|
522
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
523
|
-
try {
|
|
524
|
-
return new Intl.NumberFormat(void 0, {
|
|
525
|
-
style: "currency",
|
|
526
|
-
currency: "USD"
|
|
527
|
-
}).format(value);
|
|
528
|
-
} catch {
|
|
529
|
-
return `$${value.toFixed(2)}`;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
/** The clock time of an epoch-millisecond reading, for a "last read at" note. */
|
|
533
|
-
function readTime(ms) {
|
|
534
|
-
if (typeof ms !== "number" || !Number.isFinite(ms)) return "—";
|
|
535
|
-
try {
|
|
536
|
-
return new Date(ms).toLocaleTimeString(void 0, {
|
|
537
|
-
hour: "2-digit",
|
|
538
|
-
minute: "2-digit"
|
|
539
|
-
});
|
|
540
|
-
} catch {
|
|
541
|
-
return new Date(ms).toISOString().slice(11, 16);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
1211
|
function clearMessage(error) {
|
|
545
1212
|
const text = typeof error?.message === "string" && error.message.length > 0 ? error.message : String(error);
|
|
546
1213
|
return text.length > 200 ? text.slice(0, 200) : text;
|
|
547
1214
|
}
|
|
548
|
-
const RESET_KEYS = {
|
|
549
|
-
monthly: "resetMonthly",
|
|
550
|
-
weekly: "resetWeekly",
|
|
551
|
-
daily: "resetDaily"
|
|
552
|
-
};
|
|
553
|
-
const resetLabel = (period, t) => RESET_KEYS[period] === void 0 ? void 0 : t(RESET_KEYS[period]);
|
|
554
1215
|
/** Active key's usage row, preferring the key the ring currently points at. */
|
|
555
1216
|
function activeUsage(balance) {
|
|
556
1217
|
if (balance === void 0) return void 0;
|
|
557
1218
|
return balance.keys?.find((entry) => entry.id === balance.activeId) ?? balance.keys?.[0];
|
|
558
1219
|
}
|
|
559
|
-
/** How much of a key's limit is left, for its bar; undefined without a limit. */
|
|
560
|
-
function remainingShare(usage) {
|
|
561
|
-
if (usage?.unlimitedQuota === true || typeof usage?.limitUsd !== "number" || usage.limitUsd <= 0) return void 0;
|
|
562
|
-
if (typeof usage.availableUsd !== "number") return void 0;
|
|
563
|
-
return Math.max(0, Math.min(1, usage.availableUsd / usage.limitUsd));
|
|
564
|
-
}
|
|
565
1220
|
/** Which catalog families a key reaches, from its per-model coverage. */
|
|
566
1221
|
function reachOf(coverage) {
|
|
567
1222
|
return MODEL_FAMILIES.flatMap(([name, models]) => {
|
|
@@ -660,14 +1315,6 @@ window.__ModuleLoader__.load({
|
|
|
660
1315
|
] });
|
|
661
1316
|
}
|
|
662
1317
|
function KeyRow({ entry, row, group, descriptions, groupsLoading, busy, renaming, onRename, onAction, t }) {
|
|
663
|
-
const usage = row?.usage;
|
|
664
|
-
const share = remainingShare(usage);
|
|
665
|
-
const figures = [
|
|
666
|
-
usage?.unlimitedQuota === true ? [t("keyRemaining"), t("keyUnlimited")] : usage?.availableUsd === void 0 ? void 0 : [t("keyRemaining"), formatUsd(usage.availableUsd)],
|
|
667
|
-
usage?.usedUsd === void 0 ? void 0 : [t("keyUsed"), formatUsd(usage.usedUsd)],
|
|
668
|
-
usage?.limitUsd === void 0 ? void 0 : [t("accountLimit"), formatUsd(usage.limitUsd)]
|
|
669
|
-
].filter(Boolean);
|
|
670
|
-
const meta = [usage?.name === void 0 ? t("keyNameUnknown") : `${t("keyName")} ${usage.name}`, resetLabel(usage?.resetPeriod, t)].filter(Boolean).join(" · ");
|
|
671
1318
|
const editing = renaming?.id === entry.id;
|
|
672
1319
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
673
1320
|
className: "pkc-row",
|
|
@@ -749,29 +1396,9 @@ window.__ModuleLoader__.load({
|
|
|
749
1396
|
]
|
|
750
1397
|
})]
|
|
751
1398
|
}),
|
|
752
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}),
|
|
756
|
-
figures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
757
|
-
className: "pkc-figures",
|
|
758
|
-
children: figures.map(([label, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: value })] }, label))
|
|
759
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
760
|
-
className: "pkc-meta",
|
|
761
|
-
children: row?.error === void 0 ? t("keyNoBalance") : `${t("loadFailed")}: ${row.error}`
|
|
762
|
-
}),
|
|
763
|
-
share !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
764
|
-
className: "pkc-bar",
|
|
765
|
-
"data-level": share < .1 ? "low" : share < .3 ? "warn" : "ok",
|
|
766
|
-
role: "presentation",
|
|
767
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round(share * 100)}%` } })
|
|
768
|
-
}),
|
|
769
|
-
row?.stale !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
770
|
-
className: "pkc-warn",
|
|
771
|
-
children: fill(t("staleNote"), {
|
|
772
|
-
time: readTime(row.stale.fetchedAt),
|
|
773
|
-
error: row.stale.error ?? ""
|
|
774
|
-
})
|
|
1399
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyBalance, {
|
|
1400
|
+
row,
|
|
1401
|
+
t
|
|
775
1402
|
}),
|
|
776
1403
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyGroups, {
|
|
777
1404
|
group,
|
|
@@ -860,8 +1487,6 @@ window.__ModuleLoader__.load({
|
|
|
860
1487
|
};
|
|
861
1488
|
const keys = status?.keys ?? [];
|
|
862
1489
|
const rows = balance?.keys ?? [];
|
|
863
|
-
const totals = sumBalances(rows);
|
|
864
|
-
const staleCount = rows.filter((row) => row.stale !== void 0).length;
|
|
865
1490
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
866
1491
|
title: t("accountsTitle"),
|
|
867
1492
|
aside: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -882,44 +1507,9 @@ window.__ModuleLoader__.load({
|
|
|
882
1507
|
className: "pkc-note",
|
|
883
1508
|
children: t("empty")
|
|
884
1509
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
885
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
889
|
-
className: "pkc-stat",
|
|
890
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
891
|
-
className: "pkc-stat-label",
|
|
892
|
-
children: t("totalRemaining")
|
|
893
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
894
|
-
className: "pkc-stat-value",
|
|
895
|
-
children: [totals.counted === 0 ? "—" : formatUsd(totals.remainingUsd) ?? "—", totals.unlimited > 0 && ` + ${t("keyUnlimited")}`]
|
|
896
|
-
})]
|
|
897
|
-
}),
|
|
898
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
899
|
-
className: "pkc-stat",
|
|
900
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
901
|
-
className: "pkc-stat-label",
|
|
902
|
-
children: t("totalUsed")
|
|
903
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
904
|
-
className: "pkc-stat-value",
|
|
905
|
-
children: totals.counted === 0 ? "—" : formatUsd(totals.usedUsd) ?? "—"
|
|
906
|
-
})]
|
|
907
|
-
}),
|
|
908
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
909
|
-
className: "pkc-stat",
|
|
910
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
911
|
-
className: "pkc-stat-label",
|
|
912
|
-
children: t("totalLimit")
|
|
913
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
914
|
-
className: "pkc-stat-value",
|
|
915
|
-
children: totals.counted === 0 ? "—" : totals.unlimited > 0 ? t("keyUnlimited") : formatUsd(totals.limitUsd) ?? "—"
|
|
916
|
-
})]
|
|
917
|
-
})
|
|
918
|
-
]
|
|
919
|
-
}),
|
|
920
|
-
(totals.unread > 0 || staleCount > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
921
|
-
className: "pkc-note",
|
|
922
|
-
children: [totals.unread > 0 ? fill(t("totalPartial"), { count: totals.unread }) : void 0, staleCount > 0 ? fill(t("totalStale"), { count: staleCount }) : void 0].filter(Boolean).join(" ")
|
|
1510
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BalanceTotals, {
|
|
1511
|
+
rows,
|
|
1512
|
+
t
|
|
923
1513
|
}),
|
|
924
1514
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
925
1515
|
className: "pkc-list",
|
|
@@ -1583,6 +2173,10 @@ window.__ModuleLoader__.load({
|
|
|
1583
2173
|
call,
|
|
1584
2174
|
t
|
|
1585
2175
|
}),
|
|
2176
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PackyCodeImagesCard, {
|
|
2177
|
+
call,
|
|
2178
|
+
t
|
|
2179
|
+
}),
|
|
1586
2180
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsCard, {
|
|
1587
2181
|
call,
|
|
1588
2182
|
t
|
|
@@ -1660,6 +2254,8 @@ window.__ModuleLoader__.load({
|
|
|
1660
2254
|
}, "packycode-api: style");
|
|
1661
2255
|
const connection = ctx.get("connection");
|
|
1662
2256
|
const t = ctx.locale.bind(NS);
|
|
2257
|
+
installImagePresentation(ctx, t);
|
|
2258
|
+
const call = channelCaller(connection.rpc);
|
|
1663
2259
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1664
2260
|
name: "settings.section",
|
|
1665
2261
|
id: "packycode-api",
|
|
@@ -1682,6 +2278,29 @@ window.__ModuleLoader__.load({
|
|
|
1682
2278
|
directory: ctx.modelDirectories.directoryFor(sessionId).store
|
|
1683
2279
|
})
|
|
1684
2280
|
}, PackyCodeComposerQuota));
|
|
2281
|
+
ctx.inject(["sessions", "conversation"], (scope) => {
|
|
2282
|
+
const sessions = scope.get("sessions");
|
|
2283
|
+
const conversation = scope.get("conversation");
|
|
2284
|
+
if (typeof sessions?.scope !== "function" || typeof conversation?.input?.for !== "function") return;
|
|
2285
|
+
scope.slots.inject("conversation.input.left", () => scope.slots.register({
|
|
2286
|
+
name: "conversation.input.left",
|
|
2287
|
+
id: "packycode-image-create",
|
|
2288
|
+
order: 31,
|
|
2289
|
+
inject: (sessionId) => ({
|
|
2290
|
+
call,
|
|
2291
|
+
t,
|
|
2292
|
+
directory: ctx.modelDirectories.directoryFor(sessionId).store,
|
|
2293
|
+
prepare: (text) => {
|
|
2294
|
+
const session = sessions.scope(sessionId);
|
|
2295
|
+
if (session === void 0) throw new Error("Composer unavailable");
|
|
2296
|
+
const input = conversation.input.for(session);
|
|
2297
|
+
const draft = input.state.getSnapshot();
|
|
2298
|
+
if (draft.phase !== "plain" || draft.occurrences?.length) throw new Error("Composer busy");
|
|
2299
|
+
input.setDraft([draft.draft, text].filter(Boolean).join("\n\n"));
|
|
2300
|
+
}
|
|
2301
|
+
})
|
|
2302
|
+
}, PackyCodeImageShortcut));
|
|
2303
|
+
});
|
|
1685
2304
|
}
|
|
1686
2305
|
//#endregion
|
|
1687
2306
|
exports.apply = apply;
|