@dsh-plus/image-studio 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +558 -113
- package/lib/index.d.ts +171 -9
- package/lib/index.js +530 -55
- package/package.json +1 -1
- package/src/api.ts +83 -3
- package/src/client/api.ts +25 -0
- package/src/client/card.tsx +12 -0
- package/src/client/i18n.ts +17 -0
- package/src/client/panel/controller.ts +4 -2
- package/src/client/panel/gallery.tsx +7 -2
- package/src/client/panel/generator.tsx +89 -26
- package/src/client/panel/panel.tsx +4 -1
- package/src/client/panel/params-form.tsx +50 -6
- package/src/client/panel/picker.tsx +124 -40
- package/src/client/panel/uploads.tsx +124 -0
- package/src/client/styles.ts +50 -0
- package/src/config.ts +6 -0
- package/src/dto.ts +17 -3
- package/src/errors.ts +6 -0
- package/src/gallery/store.ts +16 -15
- package/src/images/id.ts +12 -0
- package/src/images/mime.ts +64 -0
- package/src/index.ts +12 -5
- package/src/limits.ts +12 -0
- package/src/params/catalog.ts +9 -3
- package/src/params/size.ts +98 -0
- package/src/params/spec.ts +17 -2
- package/src/service.ts +157 -28
- package/src/uploads/store.ts +168 -0
package/lib/client.js
CHANGED
|
@@ -637,10 +637,11 @@ ${extra}
|
|
|
637
637
|
{
|
|
638
638
|
key: "size",
|
|
639
639
|
kind: "string",
|
|
640
|
+
rules: "image-size",
|
|
640
641
|
applies: ["generation", "edit"],
|
|
641
642
|
generation: "current",
|
|
642
643
|
advanced: false,
|
|
643
|
-
description: "
|
|
644
|
+
description: "尺寸:auto 或「宽x高」。推荐 1024x1024 / 1536x1024 / 1024x1536;自定义需宽高为 16 的倍数、长边 ≤3840、比例 ≤3:1、总像素 655360-8294400"
|
|
644
645
|
},
|
|
645
646
|
{
|
|
646
647
|
key: "quality",
|
|
@@ -649,12 +650,14 @@ ${extra}
|
|
|
649
650
|
"auto",
|
|
650
651
|
"low",
|
|
651
652
|
"medium",
|
|
652
|
-
"high"
|
|
653
|
+
"high",
|
|
654
|
+
"xhigh",
|
|
655
|
+
"max"
|
|
653
656
|
],
|
|
654
657
|
applies: ["generation", "edit"],
|
|
655
658
|
generation: "current",
|
|
656
659
|
advanced: false,
|
|
657
|
-
description: "
|
|
660
|
+
description: "质量档位(xhigh/max 为 image 2.5 新增高档位)"
|
|
658
661
|
},
|
|
659
662
|
{
|
|
660
663
|
key: "background",
|
|
@@ -764,6 +767,90 @@ ${extra}
|
|
|
764
767
|
return key === "model";
|
|
765
768
|
}
|
|
766
769
|
//#endregion
|
|
770
|
+
//#region src/params/size.ts
|
|
771
|
+
/** GPT Image 代自定义尺寸规则。 */
|
|
772
|
+
const IMAGE_SIZE_RULE = Object.freeze({
|
|
773
|
+
multiple: 16,
|
|
774
|
+
maxEdge: 3840,
|
|
775
|
+
minPixels: 655360,
|
|
776
|
+
maxPixels: 8294400,
|
|
777
|
+
maxAspect: 3
|
|
778
|
+
});
|
|
779
|
+
/** 免校验取值:实际尺寸由模型按提示词决定。 */
|
|
780
|
+
const SIZE_AUTO = "auto";
|
|
781
|
+
/** 预设尺寸表(顺序即展示序:常用在前;全部满足 16 倍数约束)。 */
|
|
782
|
+
const SIZE_PRESETS = Object.freeze([
|
|
783
|
+
Object.freeze({
|
|
784
|
+
value: SIZE_AUTO,
|
|
785
|
+
label: "auto(由模型决定)"
|
|
786
|
+
}),
|
|
787
|
+
Object.freeze({
|
|
788
|
+
value: "1024x1024",
|
|
789
|
+
label: "1024×1024 方形 1:1"
|
|
790
|
+
}),
|
|
791
|
+
Object.freeze({
|
|
792
|
+
value: "1536x1024",
|
|
793
|
+
label: "1536×1024 横向 3:2"
|
|
794
|
+
}),
|
|
795
|
+
Object.freeze({
|
|
796
|
+
value: "1024x1536",
|
|
797
|
+
label: "1024×1536 纵向 2:3"
|
|
798
|
+
}),
|
|
799
|
+
Object.freeze({
|
|
800
|
+
value: "2048x1152",
|
|
801
|
+
label: "2048×1152 宽屏 16:9"
|
|
802
|
+
}),
|
|
803
|
+
Object.freeze({
|
|
804
|
+
value: "1152x2048",
|
|
805
|
+
label: "1152×2048 竖屏 9:16"
|
|
806
|
+
}),
|
|
807
|
+
Object.freeze({
|
|
808
|
+
value: "3840x2160",
|
|
809
|
+
label: "3840×2160 4K 16:9"
|
|
810
|
+
}),
|
|
811
|
+
Object.freeze({
|
|
812
|
+
value: "2160x3840",
|
|
813
|
+
label: "2160×3840 4K 9:16"
|
|
814
|
+
})
|
|
815
|
+
]);
|
|
816
|
+
const OK = { ok: true };
|
|
817
|
+
/** 「宽x高」规范形态(小写 x、无空白;`auto` 原样)。 */
|
|
818
|
+
function normalizeImageSize(value) {
|
|
819
|
+
const text = value.trim();
|
|
820
|
+
if (text === "auto") return SIZE_AUTO;
|
|
821
|
+
return text.replace(/[×✕✖]/g, "x").replace(/\s+/g, "").toLowerCase();
|
|
822
|
+
}
|
|
823
|
+
/** 规范化后的「宽x高」形态。 */
|
|
824
|
+
const SIZE_PATTERN = /^(\d{2,5})x(\d{2,5})$/;
|
|
825
|
+
/**
|
|
826
|
+
* 校验一个尺寸取值(`auto` 或「宽x高」,容忍大小写/全角乘号/空白)。
|
|
827
|
+
* @param value - 用户输入或预设值(提交/保存前校验)。
|
|
828
|
+
* @param rule - 尺寸约束(缺省 GPT Image 代规则)。
|
|
829
|
+
*/
|
|
830
|
+
function checkImageSize(value, rule = IMAGE_SIZE_RULE) {
|
|
831
|
+
const text = normalizeImageSize(value);
|
|
832
|
+
if (text === "auto") return OK;
|
|
833
|
+
const match = SIZE_PATTERN.exec(text);
|
|
834
|
+
if (match === null) return fail("需为「宽x高」(如 1024x1024)或 auto");
|
|
835
|
+
const width = Number(match[1]);
|
|
836
|
+
const height = Number(match[2]);
|
|
837
|
+
if (width % rule.multiple !== 0 || height % rule.multiple !== 0) return fail(`宽高需为 ${rule.multiple} 的倍数(当前 ${width}x${height})`);
|
|
838
|
+
const long = Math.max(width, height);
|
|
839
|
+
const short = Math.min(width, height);
|
|
840
|
+
if (long > rule.maxEdge) return fail(`长边不能超过 ${rule.maxEdge}px(当前 ${long}px)`);
|
|
841
|
+
if (long / short > rule.maxAspect) return fail(`宽高比不能超过 ${rule.maxAspect}:1`);
|
|
842
|
+
const pixels = width * height;
|
|
843
|
+
if (pixels < rule.minPixels) return fail(`总像素不能少于 ${rule.minPixels}(当前 ${pixels})`);
|
|
844
|
+
if (pixels > rule.maxPixels) return fail(`总像素不能超过 ${rule.maxPixels}(当前 ${pixels})`);
|
|
845
|
+
return OK;
|
|
846
|
+
}
|
|
847
|
+
function fail(message) {
|
|
848
|
+
return {
|
|
849
|
+
ok: false,
|
|
850
|
+
message
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
//#endregion
|
|
767
854
|
//#region src/params/spec.ts
|
|
768
855
|
/** 归一化失败原因(边界校验用,端点转 invalid-request)。 */
|
|
769
856
|
var ParamValidationError = class extends Error {
|
|
@@ -781,7 +868,7 @@ ${extra}
|
|
|
781
868
|
switch (entry.kind) {
|
|
782
869
|
case "string":
|
|
783
870
|
if (typeof value !== "string" || value.length === 0) throw new ParamValidationError(key, "必须是非空字符串");
|
|
784
|
-
return value;
|
|
871
|
+
return validateRule(entry.rules, key, value);
|
|
785
872
|
case "enum":
|
|
786
873
|
if (typeof value !== "string" || !entry.values?.includes(value)) throw new ParamValidationError(key, `必须是 ${entry.values?.join(" | ") ?? "枚举值"} 之一`);
|
|
787
874
|
return value;
|
|
@@ -795,6 +882,18 @@ ${extra}
|
|
|
795
882
|
return value;
|
|
796
883
|
}
|
|
797
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* 结构化值域规则分派(目录声明 → 校验实现单一入口)。
|
|
887
|
+
* 无规则的参数原样返回;规则拒绝时错误消息即为面向用户的原因。
|
|
888
|
+
*/
|
|
889
|
+
function validateRule(rule, key, value) {
|
|
890
|
+
if (rule === "image-size") {
|
|
891
|
+
const checked = checkImageSize(value);
|
|
892
|
+
if (!checked.ok) throw new ParamValidationError(key, checked.message);
|
|
893
|
+
return normalizeImageSize(value);
|
|
894
|
+
}
|
|
895
|
+
return value;
|
|
896
|
+
}
|
|
798
897
|
/** 校验预设里的参数表(结构合法但可跨端点;应用时再按端点归一化)。 */
|
|
799
898
|
function validateParamSpecs(specs) {
|
|
800
899
|
if (specs === null || typeof specs !== "object" || Array.isArray(specs)) throw new ParamValidationError("(整体)", "必须是对象");
|
|
@@ -845,6 +944,27 @@ ${extra}
|
|
|
845
944
|
function imageUrl(imageId) {
|
|
846
945
|
return `${BASE}/images/${imageId}`;
|
|
847
946
|
}
|
|
947
|
+
/** 上传原图 URL(缩略图直连,非 fetch)。 */
|
|
948
|
+
function uploadImageUrl(uploadId) {
|
|
949
|
+
return `${BASE}/uploads/${uploadId}`;
|
|
950
|
+
}
|
|
951
|
+
/** 上传本地图片:raw body 流(query 携带原文件名,仅展示用)。 */
|
|
952
|
+
async function uploadImage(file) {
|
|
953
|
+
const query = new URLSearchParams({ name: file.name });
|
|
954
|
+
const res = await fetch(`${BASE}/uploads?${query.toString()}`, {
|
|
955
|
+
method: "POST",
|
|
956
|
+
credentials: "same-origin",
|
|
957
|
+
headers: { "content-type": "application/octet-stream" },
|
|
958
|
+
body: file
|
|
959
|
+
});
|
|
960
|
+
const body = await res.json();
|
|
961
|
+
if (!res.ok) throw new Error(body.message ?? body.error ?? `HTTP ${res.status}`);
|
|
962
|
+
return body;
|
|
963
|
+
}
|
|
964
|
+
/** 上传列表(最新在后;界面自行倒序)。 */
|
|
965
|
+
async function fetchUploads() {
|
|
966
|
+
return getJson(`${BASE}/uploads`);
|
|
967
|
+
}
|
|
848
968
|
/** 凭据状态(describe,永不回传值)。 */
|
|
849
969
|
function fetchCredentialStatus(refName) {
|
|
850
970
|
return getJson(`${BASE}/credentials/${refName}`);
|
|
@@ -903,7 +1023,8 @@ ${extra}
|
|
|
903
1023
|
maxConcurrent: String(value.maxConcurrent),
|
|
904
1024
|
requestTimeoutMs: String(value.requestTimeoutMs),
|
|
905
1025
|
proxy: value.proxy,
|
|
906
|
-
galleryMaxItems: String(value.galleryMaxItems)
|
|
1026
|
+
galleryMaxItems: String(value.galleryMaxItems),
|
|
1027
|
+
uploadTtlHours: String(value.uploadTtlHours)
|
|
907
1028
|
};
|
|
908
1029
|
}
|
|
909
1030
|
function parseJson(text, label) {
|
|
@@ -944,7 +1065,8 @@ ${extra}
|
|
|
944
1065
|
maxConcurrent: Math.max(0, Math.floor(Number(draft.maxConcurrent) || 0)),
|
|
945
1066
|
requestTimeoutMs: Math.max(1e4, Math.floor(Number(draft.requestTimeoutMs) || 3e5)),
|
|
946
1067
|
proxy: draft.proxy,
|
|
947
|
-
galleryMaxItems: Math.max(0, Math.floor(Number(draft.galleryMaxItems) || 0))
|
|
1068
|
+
galleryMaxItems: Math.max(0, Math.floor(Number(draft.galleryMaxItems) || 0)),
|
|
1069
|
+
uploadTtlHours: Math.max(1, Math.floor(Number(draft.uploadTtlHours) || 24))
|
|
948
1070
|
};
|
|
949
1071
|
}
|
|
950
1072
|
/** 单个提供商预设的 API Key 行(describe 徽标 + 设置/删除,值不回显)。 */
|
|
@@ -1385,6 +1507,15 @@ ${extra}
|
|
|
1385
1507
|
disabled,
|
|
1386
1508
|
onEdit: (v) => patch({ galleryMaxItems: v })
|
|
1387
1509
|
})]
|
|
1510
|
+
}),
|
|
1511
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
1512
|
+
prefix: "imsc",
|
|
1513
|
+
id: "imsc-upload-ttl",
|
|
1514
|
+
label: t("card.uploadTtlHours"),
|
|
1515
|
+
numeric: true,
|
|
1516
|
+
value: draft.uploadTtlHours,
|
|
1517
|
+
disabled,
|
|
1518
|
+
onEdit: (v) => patch({ uploadTtlHours: v })
|
|
1388
1519
|
})
|
|
1389
1520
|
]
|
|
1390
1521
|
});
|
|
@@ -1444,8 +1575,15 @@ ${extra}
|
|
|
1444
1575
|
"sources.mask": "遮罩(可选)",
|
|
1445
1576
|
"sources.maskPick": "选择遮罩",
|
|
1446
1577
|
"sources.maskClear": "清除",
|
|
1578
|
+
"sources.maskHint": "遮罩必须是与源图同尺寸的 PNG(透明区域为可重绘范围)",
|
|
1579
|
+
"sources.uploaded": "本地文件上传",
|
|
1580
|
+
"upload.pick": "上传图片",
|
|
1581
|
+
"upload.empty": "暂无上传图片,点上方「上传图片」从本地选择",
|
|
1582
|
+
"upload.failed": "上传失败:",
|
|
1447
1583
|
"picker.empty": "画廊暂无图片",
|
|
1448
1584
|
"picker.confirm": "使用所选({n})",
|
|
1585
|
+
"picker.section.gallery": "画廊",
|
|
1586
|
+
"picker.section.uploads": "本地上传",
|
|
1449
1587
|
"generate.submit": "生成",
|
|
1450
1588
|
"generate.submitted": "已提交任务 {id},进度见下方任务条",
|
|
1451
1589
|
"generate.failed": "提交失败:",
|
|
@@ -1496,6 +1634,7 @@ ${extra}
|
|
|
1496
1634
|
"card.requestTimeoutMs": "单请求超时(毫秒)",
|
|
1497
1635
|
"card.proxy": "上游代理(空 = 直连)",
|
|
1498
1636
|
"card.galleryMaxItems": "画廊保留上限(0 = 不清理)",
|
|
1637
|
+
"card.uploadTtlHours": "上传原图保留时长(小时)",
|
|
1499
1638
|
"card.specsInvalid": "参数表非法:",
|
|
1500
1639
|
"card.idInvalid": "ID 非法:",
|
|
1501
1640
|
"card.save": "保存",
|
|
@@ -1552,8 +1691,15 @@ ${extra}
|
|
|
1552
1691
|
"sources.mask": "Mask (optional)",
|
|
1553
1692
|
"sources.maskPick": "Pick mask",
|
|
1554
1693
|
"sources.maskClear": "Clear",
|
|
1694
|
+
"sources.maskHint": "The mask must be a PNG the same size as the source (transparent area is repainted)",
|
|
1695
|
+
"sources.uploaded": "Uploaded from disk",
|
|
1696
|
+
"upload.pick": "Upload image",
|
|
1697
|
+
"upload.empty": "No uploads yet — use \"Upload image\" above to pick a local file",
|
|
1698
|
+
"upload.failed": "Upload failed: ",
|
|
1555
1699
|
"picker.empty": "No images in the gallery yet",
|
|
1556
1700
|
"picker.confirm": "Use selected ({n})",
|
|
1701
|
+
"picker.section.gallery": "Gallery",
|
|
1702
|
+
"picker.section.uploads": "Uploads",
|
|
1557
1703
|
"generate.submit": "Generate",
|
|
1558
1704
|
"generate.submitted": "Task {id} submitted; progress in the task bar below",
|
|
1559
1705
|
"generate.failed": "Submit failed: ",
|
|
@@ -1604,6 +1750,7 @@ ${extra}
|
|
|
1604
1750
|
"card.requestTimeoutMs": "Request timeout (ms)",
|
|
1605
1751
|
"card.proxy": "Upstream proxy (empty = direct)",
|
|
1606
1752
|
"card.galleryMaxItems": "Gallery retention (0 = keep all)",
|
|
1753
|
+
"card.uploadTtlHours": "Upload retention (hours)",
|
|
1607
1754
|
"card.specsInvalid": "Invalid param specs: ",
|
|
1608
1755
|
"card.idInvalid": "Invalid ID: ",
|
|
1609
1756
|
"card.save": "Save",
|
|
@@ -1790,18 +1937,25 @@ ${extra}
|
|
|
1790
1937
|
className: "ims-detailPrompt",
|
|
1791
1938
|
children: item.prompt
|
|
1792
1939
|
}),
|
|
1793
|
-
item.sourceIds.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
1940
|
+
item.sourceIds.length + item.sourceUploads.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
1794
1941
|
className: "ims-detailLabel",
|
|
1795
1942
|
children: t("gallery.detail.sources")
|
|
1796
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1943
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1797
1944
|
className: "ims-sources",
|
|
1798
|
-
children: item.sourceIds.map((imageId) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1945
|
+
children: [item.sourceIds.map((imageId) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1799
1946
|
className: "ims-thumb",
|
|
1800
1947
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1801
1948
|
src: imageUrl(imageId),
|
|
1802
1949
|
alt: ""
|
|
1803
1950
|
})
|
|
1804
|
-
}, imageId))
|
|
1951
|
+
}, imageId)), item.sourceUploads.map((uploadId) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1952
|
+
className: "ims-thumb",
|
|
1953
|
+
title: t("sources.uploaded"),
|
|
1954
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1955
|
+
src: uploadImageUrl(uploadId),
|
|
1956
|
+
alt: ""
|
|
1957
|
+
})
|
|
1958
|
+
}, uploadId))]
|
|
1805
1959
|
})] }) : null,
|
|
1806
1960
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
1807
1961
|
className: "ims-detailLabel",
|
|
@@ -2042,7 +2196,13 @@ ${extra}
|
|
|
2042
2196
|
}
|
|
2043
2197
|
//#endregion
|
|
2044
2198
|
//#region src/client/panel/params-form.tsx
|
|
2045
|
-
/**
|
|
2199
|
+
/** 结构化规则的即时提示:规则通过或取值未填时不占位。 */
|
|
2200
|
+
function ruleHint(entry, value) {
|
|
2201
|
+
if (entry.rules !== "image-size" || value.trim() === "") return null;
|
|
2202
|
+
const checked = checkImageSize(value);
|
|
2203
|
+
return checked.ok ? null : checked.message;
|
|
2204
|
+
}
|
|
2205
|
+
/** 单参数行:开关 + 名称/说明 + 值控件(+ 预设与校验提示)。 */
|
|
2046
2206
|
function ParamRow(props) {
|
|
2047
2207
|
const { t, entry, form, onChange } = props;
|
|
2048
2208
|
const field = form[entry.key] ?? {
|
|
@@ -2056,11 +2216,15 @@ ${extra}
|
|
|
2056
2216
|
...next
|
|
2057
2217
|
}
|
|
2058
2218
|
});
|
|
2219
|
+
const text = String(field.value);
|
|
2220
|
+
const hint = field.enabled ? ruleHint(entry, text) : null;
|
|
2221
|
+
/** size 预设:快捷填写,仍可自由改动输入框。 */
|
|
2222
|
+
const presets = entry.rules === "image-size" ? SIZE_PRESETS : [];
|
|
2059
2223
|
const control = (() => {
|
|
2060
2224
|
if (entry.kind === "boolean") return null;
|
|
2061
2225
|
if (entry.kind === "enum") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2062
2226
|
className: "ims-input ims-paramControl",
|
|
2063
|
-
value:
|
|
2227
|
+
value: text,
|
|
2064
2228
|
disabled: !field.enabled,
|
|
2065
2229
|
onChange: (event) => patch({ value: event.target.value }),
|
|
2066
2230
|
children: (entry.values ?? []).map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
@@ -2074,15 +2238,17 @@ ${extra}
|
|
|
2074
2238
|
inputMode: "numeric",
|
|
2075
2239
|
min: entry.min,
|
|
2076
2240
|
max: entry.max,
|
|
2077
|
-
value:
|
|
2241
|
+
value: text,
|
|
2078
2242
|
disabled: !field.enabled,
|
|
2079
2243
|
onChange: (event) => patch({ value: event.target.value })
|
|
2080
2244
|
});
|
|
2081
2245
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2082
|
-
className:
|
|
2246
|
+
className: `ims-input ims-paramControl${hint !== null ? " ims-inputInvalid" : ""}`,
|
|
2083
2247
|
type: "text",
|
|
2084
|
-
|
|
2248
|
+
list: presets.length > 0 ? `ims-size-options-${entry.key}` : void 0,
|
|
2249
|
+
value: text,
|
|
2085
2250
|
disabled: !field.enabled,
|
|
2251
|
+
"aria-invalid": hint !== null,
|
|
2086
2252
|
onChange: (event) => patch({ value: event.target.value })
|
|
2087
2253
|
});
|
|
2088
2254
|
})();
|
|
@@ -2109,18 +2275,45 @@ ${extra}
|
|
|
2109
2275
|
}),
|
|
2110
2276
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2111
2277
|
className: "ims-paramText",
|
|
2112
|
-
children: [
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2278
|
+
children: [
|
|
2279
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2280
|
+
className: "ims-paramKey",
|
|
2281
|
+
children: [entry.key, entry.generation !== "current" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2282
|
+
className: "ims-paramGen",
|
|
2283
|
+
children: entry.generation
|
|
2284
|
+
}) : null]
|
|
2285
|
+
}),
|
|
2286
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2287
|
+
className: "ims-paramDesc",
|
|
2288
|
+
children: entry.description
|
|
2289
|
+
}),
|
|
2290
|
+
hint !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2291
|
+
className: "ims-paramWarn",
|
|
2292
|
+
role: "alert",
|
|
2293
|
+
children: hint
|
|
2294
|
+
}) : null
|
|
2295
|
+
]
|
|
2296
|
+
}),
|
|
2297
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2298
|
+
className: "ims-paramValue",
|
|
2299
|
+
children: [control, presets.length > 0 && field.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2300
|
+
className: "ims-paramPresets",
|
|
2301
|
+
children: presets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2302
|
+
type: "button",
|
|
2303
|
+
title: preset.label,
|
|
2304
|
+
className: `ims-chip${preset.value === text ? " ims-chipOn" : ""}`,
|
|
2305
|
+
onClick: () => patch({ value: preset.value }),
|
|
2306
|
+
children: preset.value
|
|
2307
|
+
}, preset.value))
|
|
2308
|
+
}) : null]
|
|
2122
2309
|
}),
|
|
2123
|
-
|
|
2310
|
+
presets.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
|
|
2311
|
+
id: `ims-size-options-${entry.key}`,
|
|
2312
|
+
children: presets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2313
|
+
value: preset.value,
|
|
2314
|
+
label: preset.label
|
|
2315
|
+
}, preset.value))
|
|
2316
|
+
}) : null
|
|
2124
2317
|
]
|
|
2125
2318
|
});
|
|
2126
2319
|
}
|
|
@@ -2161,10 +2354,84 @@ ${extra}
|
|
|
2161
2354
|
});
|
|
2162
2355
|
}
|
|
2163
2356
|
//#endregion
|
|
2357
|
+
//#region src/client/panel/uploads.tsx
|
|
2358
|
+
/**
|
|
2359
|
+
* 本地文件上传的浏览器半:列表/上传/错误态封装为 hook,控件供生图表单与
|
|
2360
|
+
* 图片选择器复用。上传为串行(逐个文件),首个失败即中断并展示原因;
|
|
2361
|
+
* 成功后条目即时并入本地列表(选择器无需刷新即可见)。
|
|
2362
|
+
* @module image-studio/client/panel/uploads
|
|
2363
|
+
*/
|
|
2364
|
+
/** 可接受的上传类型(宿主端另有魔数嗅探兜底)。 */
|
|
2365
|
+
const ACCEPT = "image/png,image/jpeg,image/webp,image/gif";
|
|
2366
|
+
/** 上传暂存区读写 hook(每个消费组件一份,互不共享状态)。 */
|
|
2367
|
+
function useUploads() {
|
|
2368
|
+
const [items, setItems] = (0, react.useState)([]);
|
|
2369
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2370
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2371
|
+
const reload = (0, react.useCallback)(() => {
|
|
2372
|
+
setBusy(true);
|
|
2373
|
+
fetchUploads().then((res) => setItems(res.items)).catch(() => setItems([])).finally(() => setBusy(false));
|
|
2374
|
+
}, []);
|
|
2375
|
+
return {
|
|
2376
|
+
items,
|
|
2377
|
+
busy,
|
|
2378
|
+
error,
|
|
2379
|
+
add: (0, react.useCallback)(async (files) => {
|
|
2380
|
+
const list = Array.from(files);
|
|
2381
|
+
if (list.length === 0) return [];
|
|
2382
|
+
setBusy(true);
|
|
2383
|
+
setError(null);
|
|
2384
|
+
const saved = [];
|
|
2385
|
+
try {
|
|
2386
|
+
for (const file of list) {
|
|
2387
|
+
const entry = await uploadImage(file);
|
|
2388
|
+
saved.push(entry);
|
|
2389
|
+
}
|
|
2390
|
+
setItems((current) => [...current, ...saved]);
|
|
2391
|
+
return saved;
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2394
|
+
return saved;
|
|
2395
|
+
} finally {
|
|
2396
|
+
setBusy(false);
|
|
2397
|
+
}
|
|
2398
|
+
}, []),
|
|
2399
|
+
reload
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
/** 隐藏 file input + 触发按钮(移动端显式 accept 图像类型,避免退化为相册单选)。 */
|
|
2403
|
+
function UploadButton(props) {
|
|
2404
|
+
const { t, uploads, onUploaded, multiple, disabled, className, children } = props;
|
|
2405
|
+
const inputRef = (0, react.useRef)(null);
|
|
2406
|
+
const onChange = (event) => {
|
|
2407
|
+
const files = event.target.files;
|
|
2408
|
+
if (files !== null && files.length > 0) uploads.add(files).then((entries) => {
|
|
2409
|
+
if (entries.length > 0) onUploaded(entries);
|
|
2410
|
+
});
|
|
2411
|
+
event.target.value = "";
|
|
2412
|
+
};
|
|
2413
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2414
|
+
type: "button",
|
|
2415
|
+
className: className ?? "ims-btn ims-btnGhost",
|
|
2416
|
+
disabled: disabled === true || uploads.busy,
|
|
2417
|
+
onClick: () => inputRef.current?.click(),
|
|
2418
|
+
children
|
|
2419
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2420
|
+
ref: inputRef,
|
|
2421
|
+
type: "file",
|
|
2422
|
+
hidden: true,
|
|
2423
|
+
multiple,
|
|
2424
|
+
accept: props.accept ?? ACCEPT,
|
|
2425
|
+
"aria-label": t("upload.pick"),
|
|
2426
|
+
onChange
|
|
2427
|
+
})] });
|
|
2428
|
+
}
|
|
2429
|
+
//#endregion
|
|
2164
2430
|
//#region src/client/panel/picker.tsx
|
|
2165
2431
|
/**
|
|
2166
|
-
*
|
|
2167
|
-
*
|
|
2432
|
+
* 源图选择器(模态):源图多选(≤MAX_SOURCE_IMAGES)与遮罩单选共用。
|
|
2433
|
+
* 两个来源分区——画廊(已生成图片)与本地上传(暂存区),可混合选择;
|
|
2434
|
+
* 选中态以 SourceRef 记录(kind 区分 id 空间),底部确认条显示已选数量。
|
|
2168
2435
|
* @module image-studio/client/panel/picker
|
|
2169
2436
|
*/
|
|
2170
2437
|
function flattenImages(items) {
|
|
@@ -2176,17 +2443,66 @@ ${extra}
|
|
|
2176
2443
|
});
|
|
2177
2444
|
return out;
|
|
2178
2445
|
}
|
|
2446
|
+
/** 引用键(选中判定与去重;两个 id 空间不重叠但不互相假设)。 */
|
|
2447
|
+
function refKey(ref) {
|
|
2448
|
+
return `${ref.kind}:${ref.id}`;
|
|
2449
|
+
}
|
|
2450
|
+
/** 缩略图地址(画廊 / 上传两套端点)。 */
|
|
2451
|
+
function refUrl(ref) {
|
|
2452
|
+
return ref.kind === "gallery" ? imageUrl(ref.id) : uploadImageUrl(ref.id);
|
|
2453
|
+
}
|
|
2179
2454
|
function ImagePicker(props) {
|
|
2180
|
-
const { t, title, items, multiple, initial, onConfirm, onClose } = props;
|
|
2455
|
+
const { t, title, items, uploads, multiple, initial, onConfirm, onClose } = props;
|
|
2181
2456
|
const [selected, setSelected] = (0, react.useState)(initial);
|
|
2457
|
+
const [section, setSection] = (0, react.useState)(uploads.items.length > 0 && items.length === 0 ? "uploads" : "gallery");
|
|
2182
2458
|
const images = flattenImages(items);
|
|
2183
|
-
const
|
|
2459
|
+
const uploadItems = [...uploads.items].reverse();
|
|
2460
|
+
const selectedKeys = selected.map(refKey);
|
|
2461
|
+
const toggle = (ref) => {
|
|
2462
|
+
const key = refKey(ref);
|
|
2184
2463
|
if (multiple) {
|
|
2185
|
-
setSelected((current) => current.
|
|
2464
|
+
setSelected((current) => current.some((item) => refKey(item) === key) ? current.filter((item) => refKey(item) !== key) : current.length >= 16 ? current : [...current, ref]);
|
|
2186
2465
|
return;
|
|
2187
2466
|
}
|
|
2188
|
-
setSelected((current) => current.
|
|
2467
|
+
setSelected((current) => current.some((item) => refKey(item) === key) ? [] : [ref]);
|
|
2189
2468
|
};
|
|
2469
|
+
/** 新上传条目直接选中(少一次点击;超限按剩余额度截断)。 */
|
|
2470
|
+
const selectUploaded = (entries) => {
|
|
2471
|
+
const refs = entries.map((entry) => ({
|
|
2472
|
+
kind: "upload",
|
|
2473
|
+
id: entry.id
|
|
2474
|
+
}));
|
|
2475
|
+
setSelected((current) => multiple ? [...current, ...refs].slice(0, 16) : refs);
|
|
2476
|
+
setSection("uploads");
|
|
2477
|
+
};
|
|
2478
|
+
const grid = (cells, empty) => cells.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2479
|
+
className: "ims-empty",
|
|
2480
|
+
children: empty
|
|
2481
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2482
|
+
className: "ims-pickerGrid",
|
|
2483
|
+
role: "listbox",
|
|
2484
|
+
"aria-multiselectable": multiple,
|
|
2485
|
+
children: cells.map((cell) => {
|
|
2486
|
+
const index = selectedKeys.indexOf(refKey(cell.ref));
|
|
2487
|
+
const on = index >= 0;
|
|
2488
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2489
|
+
type: "button",
|
|
2490
|
+
role: "option",
|
|
2491
|
+
"aria-selected": on,
|
|
2492
|
+
className: `ims-pickerCell${on ? " ims-pickerCellOn" : ""}`,
|
|
2493
|
+
title: cell.title,
|
|
2494
|
+
onClick: () => toggle(cell.ref),
|
|
2495
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
2496
|
+
src: refUrl(cell.ref),
|
|
2497
|
+
alt: cell.title,
|
|
2498
|
+
loading: "lazy"
|
|
2499
|
+
}), on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2500
|
+
className: "ims-pickerMark",
|
|
2501
|
+
children: index + 1
|
|
2502
|
+
}) : null]
|
|
2503
|
+
}, refKey(cell.ref));
|
|
2504
|
+
})
|
|
2505
|
+
});
|
|
2190
2506
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2191
2507
|
open: true,
|
|
2192
2508
|
onClose,
|
|
@@ -2198,43 +2514,69 @@ ${extra}
|
|
|
2198
2514
|
children: [
|
|
2199
2515
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2200
2516
|
className: "ims-subHead",
|
|
2201
|
-
children: [
|
|
2202
|
-
|
|
2203
|
-
|
|
2517
|
+
children: [
|
|
2518
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2519
|
+
className: "ims-subTitle",
|
|
2520
|
+
children: title
|
|
2521
|
+
}),
|
|
2522
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(UploadButton, {
|
|
2523
|
+
t,
|
|
2524
|
+
uploads,
|
|
2525
|
+
multiple,
|
|
2526
|
+
accept: multiple ? void 0 : "image/png",
|
|
2527
|
+
onUploaded: selectUploaded,
|
|
2528
|
+
className: "ims-btn ims-btnPrimary ims-btnSmall",
|
|
2529
|
+
children: [
|
|
2530
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 14 }),
|
|
2531
|
+
" ",
|
|
2532
|
+
t("upload.pick")
|
|
2533
|
+
]
|
|
2534
|
+
}),
|
|
2535
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2536
|
+
type: "button",
|
|
2537
|
+
className: "ims-btn ims-btnGhost",
|
|
2538
|
+
onClick: onClose,
|
|
2539
|
+
children: t("common.close")
|
|
2540
|
+
})
|
|
2541
|
+
]
|
|
2542
|
+
}),
|
|
2543
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2544
|
+
className: "ims-seg",
|
|
2545
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2546
|
+
type: "button",
|
|
2547
|
+
className: `ims-segItem${section === "gallery" ? " ims-segItemOn" : ""}`,
|
|
2548
|
+
"aria-pressed": section === "gallery",
|
|
2549
|
+
onClick: () => setSection("gallery"),
|
|
2550
|
+
children: t("picker.section.gallery")
|
|
2204
2551
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2205
2552
|
type: "button",
|
|
2206
|
-
className:
|
|
2207
|
-
|
|
2208
|
-
|
|
2553
|
+
className: `ims-segItem${section === "uploads" ? " ims-segItemOn" : ""}`,
|
|
2554
|
+
"aria-pressed": section === "uploads",
|
|
2555
|
+
onClick: () => {
|
|
2556
|
+
uploads.reload();
|
|
2557
|
+
setSection("uploads");
|
|
2558
|
+
},
|
|
2559
|
+
children: t("picker.section.uploads")
|
|
2209
2560
|
})]
|
|
2210
2561
|
}),
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
alt: image.prompt,
|
|
2230
|
-
loading: "lazy"
|
|
2231
|
-
}), on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2232
|
-
className: "ims-pickerMark",
|
|
2233
|
-
children: selected.indexOf(image.imageId) + 1
|
|
2234
|
-
}) : null]
|
|
2235
|
-
}, image.imageId);
|
|
2236
|
-
})
|
|
2237
|
-
}),
|
|
2562
|
+
section === "gallery" ? grid(images.map((image) => ({
|
|
2563
|
+
ref: {
|
|
2564
|
+
kind: "gallery",
|
|
2565
|
+
id: image.imageId
|
|
2566
|
+
},
|
|
2567
|
+
title: image.prompt
|
|
2568
|
+
})), t("picker.empty")) : grid(uploadItems.map((entry) => ({
|
|
2569
|
+
ref: {
|
|
2570
|
+
kind: "upload",
|
|
2571
|
+
id: entry.id
|
|
2572
|
+
},
|
|
2573
|
+
title: entry.name
|
|
2574
|
+
})), t("upload.empty")),
|
|
2575
|
+
uploads.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2576
|
+
className: "ims-status ims-statusError",
|
|
2577
|
+
role: "alert",
|
|
2578
|
+
children: [t("upload.failed"), uploads.error]
|
|
2579
|
+
}) : null,
|
|
2238
2580
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2239
2581
|
className: "ims-subFoot",
|
|
2240
2582
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -2261,7 +2603,7 @@ ${extra}
|
|
|
2261
2603
|
* - 提供商预设选择 + 凭据状态徽标 + inline 覆盖(凭据沿用预设);
|
|
2262
2604
|
* - 提示词编辑 + 预设插入/另存;
|
|
2263
2605
|
* - 目录驱动的参数表单 + 参数预设应用/另存;
|
|
2264
|
-
* - 图生图追加源图(≤16
|
|
2606
|
+
* - 图生图追加源图(≤16,画廊选择与本地文件上传混用)与可选遮罩;
|
|
2265
2607
|
* - 提交 generate → 202 taskId 交由面板任务条轮询。
|
|
2266
2608
|
* 预设另存走 settings RPC(scope/api 注入),成功后经 onPresetSaved 刷新快照。
|
|
2267
2609
|
* @module image-studio/client/panel/generator
|
|
@@ -2274,6 +2616,14 @@ ${extra}
|
|
|
2274
2616
|
function presetId(prefix) {
|
|
2275
2617
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
2276
2618
|
}
|
|
2619
|
+
/** 单选上传结果 → 遮罩引用(无成功条目时保持原值)。 */
|
|
2620
|
+
function uploadRef(entries) {
|
|
2621
|
+
const first = entries[0];
|
|
2622
|
+
return first === void 0 ? null : {
|
|
2623
|
+
kind: "upload",
|
|
2624
|
+
id: first.id
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2277
2627
|
function GeneratorForm(props) {
|
|
2278
2628
|
const { t, endpoint, catalog, protocols, presets, gallery, scope, api } = props;
|
|
2279
2629
|
const [providerId, setProviderId] = (0, react.useState)("");
|
|
@@ -2286,8 +2636,8 @@ ${extra}
|
|
|
2286
2636
|
const [credentialOk, setCredentialOk] = (0, react.useState)(null);
|
|
2287
2637
|
const [prompt, setPrompt] = (0, react.useState)("");
|
|
2288
2638
|
const [form, setForm] = (0, react.useState)({});
|
|
2289
|
-
const [
|
|
2290
|
-
const [
|
|
2639
|
+
const [sources, setSources] = (0, react.useState)([]);
|
|
2640
|
+
const [mask, setMask] = (0, react.useState)(null);
|
|
2291
2641
|
const [picker, setPicker] = (0, react.useState)(null);
|
|
2292
2642
|
const [naming, setNaming] = (0, react.useState)(null);
|
|
2293
2643
|
const [status, setStatus] = (0, react.useState)(IDLE);
|
|
@@ -2296,6 +2646,7 @@ ${extra}
|
|
|
2296
2646
|
const formReadyRef = (0, react.useRef)(false);
|
|
2297
2647
|
/** 已消费的种子 nonce(防重复回填)。 */
|
|
2298
2648
|
const seedRef = (0, react.useRef)(0);
|
|
2649
|
+
const uploads = useUploads();
|
|
2299
2650
|
(0, react.useEffect)(() => {
|
|
2300
2651
|
if (formReadyRef.current || catalog.length === 0) return;
|
|
2301
2652
|
formReadyRef.current = true;
|
|
@@ -2306,7 +2657,7 @@ ${extra}
|
|
|
2306
2657
|
if (seed === null || seed.nonce === seedRef.current || !formReadyRef.current) return;
|
|
2307
2658
|
seedRef.current = seed.nonce;
|
|
2308
2659
|
setPrompt(seed.prompt);
|
|
2309
|
-
|
|
2660
|
+
setSources(seed.sources);
|
|
2310
2661
|
setForm(applyParams(catalog, endpoint, seed.params));
|
|
2311
2662
|
if (seed.providerPresetId !== null) setProviderId(seed.providerPresetId);
|
|
2312
2663
|
setStatus(IDLE);
|
|
@@ -2400,7 +2751,7 @@ ${extra}
|
|
|
2400
2751
|
});
|
|
2401
2752
|
return;
|
|
2402
2753
|
}
|
|
2403
|
-
if (endpoint === "edit" &&
|
|
2754
|
+
if (endpoint === "edit" && sources.length === 0) {
|
|
2404
2755
|
setStatus({
|
|
2405
2756
|
kind: "error",
|
|
2406
2757
|
text: t("generate.needSources")
|
|
@@ -2429,8 +2780,8 @@ ${extra}
|
|
|
2429
2780
|
model: inline.model.trim()
|
|
2430
2781
|
};
|
|
2431
2782
|
if (endpoint === "edit") {
|
|
2432
|
-
request.
|
|
2433
|
-
if (
|
|
2783
|
+
request.sources = sources;
|
|
2784
|
+
if (mask !== null) request.mask = mask;
|
|
2434
2785
|
}
|
|
2435
2786
|
setSubmitting(true);
|
|
2436
2787
|
setStatus(IDLE);
|
|
@@ -2598,28 +2949,44 @@ ${extra}
|
|
|
2598
2949
|
}),
|
|
2599
2950
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2600
2951
|
className: "ims-sources",
|
|
2601
|
-
children: [
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2952
|
+
children: [
|
|
2953
|
+
sources.map((ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2954
|
+
className: "ims-thumb",
|
|
2955
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
2956
|
+
src: refUrl(ref),
|
|
2957
|
+
alt: ""
|
|
2958
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2959
|
+
type: "button",
|
|
2960
|
+
className: "ims-thumbRemove",
|
|
2961
|
+
"aria-label": t("common.delete"),
|
|
2962
|
+
onClick: () => setSources((current) => current.filter((item) => !(item.kind === ref.kind && item.id === ref.id))),
|
|
2963
|
+
children: "×"
|
|
2964
|
+
})]
|
|
2965
|
+
}, `${ref.kind}:${ref.id}`)),
|
|
2966
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2607
2967
|
type: "button",
|
|
2608
|
-
className: "ims-
|
|
2609
|
-
|
|
2610
|
-
onClick: () =>
|
|
2611
|
-
children: "
|
|
2612
|
-
})
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2968
|
+
className: "ims-thumbAdd",
|
|
2969
|
+
disabled: sources.length >= 16,
|
|
2970
|
+
onClick: () => setPicker("sources"),
|
|
2971
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconLayers, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sources.pick") })]
|
|
2972
|
+
}),
|
|
2973
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(UploadButton, {
|
|
2974
|
+
t,
|
|
2975
|
+
uploads,
|
|
2976
|
+
multiple: true,
|
|
2977
|
+
disabled: sources.length >= 16,
|
|
2978
|
+
className: "ims-thumbAdd",
|
|
2979
|
+
onUploaded: (entries) => setSources((current) => [...current, ...entries.map((entry) => ({
|
|
2980
|
+
kind: "upload",
|
|
2981
|
+
id: entry.id
|
|
2982
|
+
}))].slice(0, 16)),
|
|
2983
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("upload.pick") })]
|
|
2984
|
+
})
|
|
2985
|
+
]
|
|
2619
2986
|
}),
|
|
2620
2987
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2621
2988
|
className: "ims-hint",
|
|
2622
|
-
children: t("sources.count").replace("{n}", String(
|
|
2989
|
+
children: t("sources.count").replace("{n}", String(sources.length))
|
|
2623
2990
|
}),
|
|
2624
2991
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2625
2992
|
className: "ims-blockTitle",
|
|
@@ -2627,25 +2994,46 @@ ${extra}
|
|
|
2627
2994
|
}),
|
|
2628
2995
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2629
2996
|
className: "ims-sources",
|
|
2630
|
-
children: [
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2997
|
+
children: [
|
|
2998
|
+
mask !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2999
|
+
className: "ims-thumb",
|
|
3000
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
3001
|
+
src: refUrl(mask),
|
|
3002
|
+
alt: ""
|
|
3003
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3004
|
+
type: "button",
|
|
3005
|
+
className: "ims-thumbRemove",
|
|
3006
|
+
"aria-label": t("sources.maskClear"),
|
|
3007
|
+
onClick: () => setMask(null),
|
|
3008
|
+
children: "×"
|
|
3009
|
+
})]
|
|
3010
|
+
}) : null,
|
|
3011
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2636
3012
|
type: "button",
|
|
2637
|
-
className: "ims-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
3013
|
+
className: "ims-thumbAdd",
|
|
3014
|
+
onClick: () => setPicker("mask"),
|
|
3015
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconLayers, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sources.maskPick") })]
|
|
3016
|
+
}),
|
|
3017
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(UploadButton, {
|
|
3018
|
+
t,
|
|
3019
|
+
uploads,
|
|
3020
|
+
multiple: false,
|
|
3021
|
+
accept: "image/png",
|
|
3022
|
+
className: "ims-thumbAdd",
|
|
3023
|
+
onUploaded: (entries) => setMask(uploadRef(entries)),
|
|
3024
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("upload.pick") })]
|
|
3025
|
+
})
|
|
3026
|
+
]
|
|
3027
|
+
}),
|
|
3028
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3029
|
+
className: "ims-hint",
|
|
3030
|
+
children: t("sources.maskHint")
|
|
3031
|
+
}),
|
|
3032
|
+
uploads.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3033
|
+
className: "ims-status ims-statusError",
|
|
3034
|
+
role: "alert",
|
|
3035
|
+
children: [t("upload.failed"), uploads.error]
|
|
3036
|
+
}) : null
|
|
2649
3037
|
]
|
|
2650
3038
|
}) : null
|
|
2651
3039
|
]
|
|
@@ -2746,11 +3134,12 @@ ${extra}
|
|
|
2746
3134
|
t,
|
|
2747
3135
|
title: t(picker === "sources" ? "sources.label" : "sources.mask"),
|
|
2748
3136
|
items: gallery,
|
|
3137
|
+
uploads,
|
|
2749
3138
|
multiple: picker === "sources",
|
|
2750
|
-
initial: picker === "sources" ?
|
|
2751
|
-
onConfirm: (
|
|
2752
|
-
if (picker === "sources")
|
|
2753
|
-
else
|
|
3139
|
+
initial: picker === "sources" ? sources : mask === null ? [] : [mask],
|
|
3140
|
+
onConfirm: (refs) => {
|
|
3141
|
+
if (picker === "sources") setSources(refs);
|
|
3142
|
+
else setMask(refs[0] ?? null);
|
|
2754
3143
|
setPicker(null);
|
|
2755
3144
|
},
|
|
2756
3145
|
onClose: () => setPicker(null)
|
|
@@ -2980,7 +3369,13 @@ ${extra}
|
|
|
2980
3369
|
failed: galleryFailed,
|
|
2981
3370
|
onRefresh: reloadGallery,
|
|
2982
3371
|
onEdit: (item) => studio.seedEdit({
|
|
2983
|
-
|
|
3372
|
+
sources: [...item.imageIds.map((id) => ({
|
|
3373
|
+
kind: "gallery",
|
|
3374
|
+
id
|
|
3375
|
+
})), ...item.sourceUploads.map((id) => ({
|
|
3376
|
+
kind: "upload",
|
|
3377
|
+
id
|
|
3378
|
+
}))],
|
|
2984
3379
|
prompt: item.prompt,
|
|
2985
3380
|
params: item.params,
|
|
2986
3381
|
providerPresetId: item.providerPresetId
|
|
@@ -3243,6 +3638,32 @@ ${extra}
|
|
|
3243
3638
|
white-space: nowrap;
|
|
3244
3639
|
}
|
|
3245
3640
|
.ims-paramControl { width: 132px; flex: none; min-height: 30px; padding: 4px 8px; }
|
|
3641
|
+
.ims-inputInvalid {
|
|
3642
|
+
border-color: var(--dsw-alias-state-error-primary);
|
|
3643
|
+
}
|
|
3644
|
+
.ims-paramValue { display: grid; gap: 4px; justify-items: end; }
|
|
3645
|
+
.ims-paramPresets { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
|
|
3646
|
+
.ims-chip {
|
|
3647
|
+
padding: 1px 6px;
|
|
3648
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
3649
|
+
border-radius: 999px;
|
|
3650
|
+
background: transparent;
|
|
3651
|
+
color: var(--dsw-alias-label-secondary);
|
|
3652
|
+
font-family: var(--ds-font-family-code);
|
|
3653
|
+
font-size: 10px;
|
|
3654
|
+
line-height: 16px;
|
|
3655
|
+
cursor: pointer;
|
|
3656
|
+
}
|
|
3657
|
+
.ims-chip:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
3658
|
+
.ims-chipOn {
|
|
3659
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
3660
|
+
color: var(--dsw-alias-brand-primary);
|
|
3661
|
+
}
|
|
3662
|
+
.ims-paramWarn {
|
|
3663
|
+
margin-top: 2px;
|
|
3664
|
+
color: var(--dsw-alias-state-error-primary);
|
|
3665
|
+
font-size: 11px;
|
|
3666
|
+
}
|
|
3246
3667
|
.ims-adv { margin-top: 8px; }
|
|
3247
3668
|
.ims-advSummary {
|
|
3248
3669
|
cursor: pointer;
|
|
@@ -3369,6 +3790,30 @@ ${extra}
|
|
|
3369
3790
|
}
|
|
3370
3791
|
.ims-subFoot .ims-status { margin: 0 auto 0 0; }
|
|
3371
3792
|
|
|
3793
|
+
/* 选择器来源切换(画廊 / 本地上传) */
|
|
3794
|
+
.ims-seg {
|
|
3795
|
+
display: flex;
|
|
3796
|
+
flex: none;
|
|
3797
|
+
gap: 4px;
|
|
3798
|
+
padding: 8px 14px 0;
|
|
3799
|
+
}
|
|
3800
|
+
.ims-segItem {
|
|
3801
|
+
padding: 4px 10px;
|
|
3802
|
+
border: 1px solid transparent;
|
|
3803
|
+
border-radius: 999px;
|
|
3804
|
+
background: transparent;
|
|
3805
|
+
color: var(--dsw-alias-label-secondary);
|
|
3806
|
+
font-size: 12px;
|
|
3807
|
+
cursor: pointer;
|
|
3808
|
+
}
|
|
3809
|
+
.ims-segItem:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
3810
|
+
.ims-segItemOn {
|
|
3811
|
+
border-color: var(--dsw-alias-border-l2);
|
|
3812
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
3813
|
+
color: var(--dsw-alias-label-primary);
|
|
3814
|
+
font-weight: 600;
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3372
3817
|
/* 选择器网格 */
|
|
3373
3818
|
.ims-pickerGrid {
|
|
3374
3819
|
flex: 1;
|