PageSourceSearch

https://illumin-ai.io/app/build/assets/Generate-JbLM7DQM.js

js illumin-ai.io collected 2026-10-04 13:22:08 UTC 66,149 bytes, 1,472 lines download raw bytes

1import { j as jsxRuntimeExports, r as reactExports, a as router3 } from './app-DcfwjElx.js';
2import { C as CircleQuestionMark, A as AuthenticatedLayout } from './AuthenticatedLayout-DywEqxU_.js';
3import { s } from './index-CiWNx9kG.js';
4import { G as GenerationResultTitle, T as Thumbnail, W as WarningModal } from './GenerationResultTitle-CVozX48j.js';
5import { W as WalletGenerateButtons, n as newIdempotencyKey, c as createImageTask, h as createParallelImageTasks, j as getImageTask, G as GenerationDetailModal, g as deleteMediaByTaskId, v as validateImageItem, b as useGenerationCredit } from './mediaApi-B1pP7RD7.js';
6import { D as DeleteConfirmModal } from './DeleteConfirmModal-BoUnuZRd.js';
7import { T as Toaster, t as toast } from './index-CfJ5oEMN.js';
8import { F as FilePickerModal } from './AddFileMenuTile-skspJIlE.js';
9import { u as uploadImage, G as GenerateResult, I as IMAGE_VALIDATION_OPTIONS } from './uploadImageValidation-BgPyghc_.js';
10import { p as promptLength, m as mergeModes, M as ModelMultiSelect, a as ModeSelect, P as PromptInput, S as SelectMenu, G as GenerateSize, b as GenerateSeed, u as useTaskPollingPool, c as useParallelProgress, f as flattenImages, d as createParallelTask, e as patchTask, i as isSettled, t as totalCount, g as progressSummary, h as progressBreakdown, j as ModelResultCard, k as StatusChip, l as defaultMode, n as findModel, o as modesOf, q as findMode, r as mergeRatios, s as mergeResolutions, v as SLOT, w as mergeMaxCount, x as mergePromptRequired, y as mergePromptMaxLength, z as mergeParams, A as mergeSlots, B as SINGLE_ONLY_LIMITS, C as hasSlot, D as slotMax, E as keepOrDefaultMode, F as keepOrFirst, H as findModelByAnyId, I as useMultiModelCredit, J as modeFromQuery } from './useMultiModelCredit-BeVZMQRL.js';
11import { C as CompactImageUpload, u as useImageUpload } from './useImageUpload-DHoVcxXH.js';
12import { X } from './x-Hr0nmDWA.js';
13import { U as Upload } from './upload-DpZtQ1KH.js';
14import { P as ProjectPostPicker, a as ProjectPostModal } from './ProjectPostModal-Cbyk-oMj.js';
15import { L as LoaderCircle } from './loader-circle-CYWCs6PF.js';
16import { D as Download } from './download-DAwV5Buz.js';
17import { c as consumeReusePrompt } from './reuseOriginalCharacter-CdJErSgd.js';
18import './createLucideIcon-DdfqdVik.js';
19import './search-TiiXekoU.js';
20import './users-lQI8w5Xn.js';
21import './Logo-BVNRcjAq.js';
22import './gift-CBaDsIlA.js';
23import './transition-B9acLIsI.js';
24import './user-BAoeAiea.js';
25import './log-out-C427prMT.js';
26import './heart-1wQw6Yxc.js';
27import './external-link-DGZAgNbN.js';
28import './pencil-CFuV0xlC.js';
29import './trash-2-DSZuUwBI.js';
30import './chevron-left-BhY4ZaaN.js';
31import './chevron-right-bDYPGnsn.js';
32import './music-C8ZZaTCu.js';
33import './link-BU3mQUjy.js';
34import './index-nIruWruQ.js';
35import './fileApi-D_CekV5D.js';
36import './play-CmyQeS9Y.js';
37import './image-plus-CZHyJOfn.js';
38import './useReadymadePrompt-C_IAzFIv.js';
39import './triangle-alert-CVtr3DZy.js';
40import './chevron-down-BWSmdTsA.js';
41import './formatSize-GgmYFGEy.js';
42import './folder-plus-BSCIjgw-.js';
43import './circle-x-BCXNFL-V.js';
44
45function GenerationCount({ selectedCount, setSelectedCount, counts, onlySingleGenerateModel }) {
46  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
47    /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-foreground flex items-center gap-1", children: "生成枚数" }),
48    /* @__PURE__ */ jsxRuntimeExports.jsx(
49      "select",
50      {
51        value: selectedCount,
52        onChange: (e) => setSelectedCount(e.target.value),
53        className: "w-full px-3 py-1 border border-gray-600 bg-input text-foreground rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent",
54        disabled: onlySingleGenerateModel,
55        "aria-disabled": onlySingleGenerateModel,
56        title: onlySingleGenerateModel ? "このモデルは生成枚数を変更できません" : void 0,
57        children: (counts || []).map((c) => /* @__PURE__ */ jsxRuntimeExports.jsx(
58          "option",
59          {
60            value: c.id ?? String(c),
61            className: "bg-[#2a3a52] text-foreground",
62            children: c.name ?? String(c)
63          },
64          c.id ?? String(c)
65        ))
66      }
67    ),
68    onlySingleGenerateModel && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-red-500 mt-1", children: "このモデルは生成枚数を選択できません。" })
69  ] });
70}
71
72const BACKGROUND_OPTIONS = [
73  { id: "opaque", name: "透過なし" },
74  { id: "transparent", name: "背景透過" }
75];
76function BackgroundSelect({ selectedBackground, setSelectedBackground }) {
77  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
78    /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-foreground flex items-center gap-1", children: "背景透過" }),
79    /* @__PURE__ */ jsxRuntimeExports.jsx(
80      "select",
81      {
82        value: selectedBackground,
83        onChange: (e) => setSelectedBackground(e.target.value),
84        className: "w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent border bg-input text-foreground",
85        children: BACKGROUND_OPTIONS.map((opt) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: opt.id, className: "bg-[#2a3a52] text-foreground", children: opt.name }, opt.id))
86      }
87    )
88  ] });
89}
90
91const INPUT_FIDELITY_OPTIONS = [
92  { id: "high", name: "元画像を保持して編集" },
93  { id: "low", name: "プロンプト重視で編集" }
94];
95function InputFidelitySelect({ selectedInputFidelity, setSelectedInputFidelity }) {
96  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
97    /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-foreground flex items-center gap-1", children: "編集モード" }),
98    /* @__PURE__ */ jsxRuntimeExports.jsx(
99      "select",
100      {
101        value: selectedInputFidelity,
102        onChange: (e) => setSelectedInputFidelity(e.target.value),
103        className: "w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent border bg-input text-foreground",
104        children: INPUT_FIDELITY_OPTIONS.map((opt) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: opt.id, className: "bg-[#2a3a52] text-foreground", children: opt.name }, opt.id))
105      }
106    )
107  ] });
108}
109
110function MaskUpload({
111  maskPreview,
112  setMaskPreview,
113  maskUrl,
114  setMaskUrl,
115  redirectToLogout
116}) {
117  const [isUploading, setIsUploading] = reactExports.useState(false);
118  const handleFile = async (file) => {
119    if (!file.type.startsWith("image/")) {
120      alert("画像ファイルのみアップロードできます。");
121      return;
122    }
123    const reader = new FileReader();
124    reader.onload = (ev) => setMaskPreview(String(ev.target?.result || ""));
125    reader.readAsDataURL(file);
126    setIsUploading(true);
127    try {
128      const res = await uploadImage(file);
129      const text = await res.text();
130      if (!res.ok) {
131        if (res.status === 401) {
132          redirectToLogout();
133          return;
134        }
135        throw new Error(`アップロードに失敗しました(${res.status})`);
136      }
137      const data = JSON.parse(text);
138      if (data?.image_url) {
139        setMaskUrl(data.image_url);
140      }
141    } catch (err) {
142      console.error("マスクアップロードエラー:", err);
143      alert(err.message || "マスク画像のアップロードに失敗しました");
144      setMaskPreview(null);
145      setMaskUrl(null);
146    } finally {
147      setIsUploading(false);
148    }
149  };
150  const handleClear = () => {
151    setMaskPreview(null);
152    setMaskUrl(null);
153  };
154  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
155    /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "text-xs font-medium text-foreground flex items-center gap-1", children: [
156      "マスク画像(任意)",
157      /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "relative group cursor-pointer", children: [
158        /* @__PURE__ */ jsxRuntimeExports.jsxs(
159          "span",
160          {
161            className: "absolute left-1/2 -translate-x-[30%] bottom-full mb-2 w-max max-w-xs px-3 py-2 rounded-lg bg-muted text-foreground text-xs shadow-lg border border-gray-700 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-10",
162            style: { whiteSpace: "pre-line" },
163            role: "tooltip",
164            children: [
165              "アップロード画像に同じサイズのファイルをアップロードする必要があります。",
166              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute left-1/2 top-full -translate-x-1/2 mt-1 w-3 h-3 overflow-hidden leading-none pointer-events-none z-10", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "block w-3 h-3 bg-muted border border-gray-700 rotate-45 -translate-y-2" }) })
167            ]
168          }
169        ),
170        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground flex items-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleQuestionMark, { className: "w-4 h-4" }) })
171      ] })
172    ] }),
173    maskPreview ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative inline-block", children: [
174      /* @__PURE__ */ jsxRuntimeExports.jsx(
175        "img",
176        {
177          src: maskPreview,
178          alt: "マスクプレビュー",
179          className: "w-20 h-20 object-cover rounded-lg border border-gray-600"
180        }
181      ),
182      /* @__PURE__ */ jsxRuntimeExports.jsx(
183        "button",
184        {
185          onClick: handleClear,
186          className: "absolute -top-2 -right-2 bg-gray-600 text-white rounded-full w-6 h-6 flex items-center justify-center hover:bg-gray-700 transition-opacity",
187          title: "削除",
188          children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 14 })
189        }
190      )
191    ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
192      /* @__PURE__ */ jsxRuntimeExports.jsx(
193        "input",
194        {
195          type: "file",
196          accept: "image/*",
197          onChange: (e) => {
198            const file = e.target.files?.[0];
199            if (file) handleFile(file);
200            e.target.value = "";
201          },
202          style: { display: "none" },
203          id: "mask-upload"
204        }
205      ),
206      /* @__PURE__ */ jsxRuntimeExports.jsxs(
207        "label",
208        {
209          htmlFor: "mask-upload",
210          className: `inline-flex items-center gap-2 px-3 py-2 bg-card text-foreground border bg-add-media-hover rounded cursor-pointer text-sm ${isUploading ? "opacity-60 pointer-events-none" : ""}`,
211          children: [
212            /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 14 }),
213            isUploading ? "アップロード中..." : "マスク画像を選択"
214          ]
215        }
216      )
217    ] })
218  ] });
219}
220
221function PromptEffectBar({
222  promptStrength,
223  setPromptStrength
224}) {
225  const min = 1;
226  const max = 10;
227  const thumb = 14;
228  const half = thumb / 2;
229  const adjust = -3;
230  const trackRef = reactExports.useRef(null);
231  const percent = (promptStrength - min) / (max - min) * 100;
232  const handleMove = (clientX) => {
233    if (!trackRef.current) return;
234    const rect = trackRef.current.getBoundingClientRect();
235    const x = Math.min(Math.max(clientX - rect.left, 0), rect.width);
236    const ratio = x / rect.width;
237    const value = min + ratio * (max - min);
238    setPromptStrength(Number(value.toFixed(1)));
239  };
240  const handleMouseDown = (e) => {
241    handleMove(e.clientX);
242    const move = (ev) => handleMove(ev.clientX);
243    const up = () => {
244      window.removeEventListener("mousemove", move);
245      window.removeEventListener("mouseup", up);
246    };
247    window.addEventListener("mousemove", move);
248    window.addEventListener("mouseup", up);
249  };
250  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
251    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center", children: [
252      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-medium text-foreground", children: "プロンプト影響度" }),
253      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-muted-foreground font-semibold", children: promptStrength.toFixed(1) })
254    ] }),
255    /* @__PURE__ */ jsxRuntimeExports.jsxs(
256      "div",
257      {
258        ref: trackRef,
259        onMouseDown: handleMouseDown,
260        className: "relative w-full h-[14px] rounded-full bg-muted cursor-pointer select-none",
261        children: [
262          /* @__PURE__ */ jsxRuntimeExports.jsx(
263            "div",
264            {
265              className: "absolute left-0 top-0 h-full rounded-full bg-[var(--text-color)]",
266              style: { width: `${percent}%` }
267            }
268          ),
269          /* @__PURE__ */ jsxRuntimeExports.jsx(
270            "div",
271            {
272              className: "absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-[14px] h-[14px] rounded-full border-2",
273              style: {
274                left: `clamp(${half}px, calc(${percent}% + ${adjust}px), calc(100% - ${half}px))`,
275                backgroundColor: "var(--bg-background-color)",
276                borderColor: "var(--text-color)"
277              }
278            }
279          )
280        ]
281      }
282    )
283  ] });
284}
285
286function ImageSettingCard({
287  iconComponent: IconComponent,
288  models,
289  selectedModel,
290  selectedModels,
291  setSelectedModels,
292  limits,
293  creditBreakdown,
294  slotNotices,
295  setSelectedModel,
296  selectedMode,
297  setSelectedMode,
298  capability,
299  showUpload,
300  uploadedImages,
301  setUploadedImages,
302  uploadedImageUrls,
303  setUploadedImageUrls,
304  isDragOver,
305  handleDragOver,
306  handleDragLeave,
307  handleDrop,
308  safeProcessFiles,
309  uploadValidationError,
310  setUploadValidationError,
311  maxUploads,
312  onOpenFilePicker,
313  prompt,
314  setPrompt,
315  placeholders,
316  promptLanguage,
317  selectedResolution,
318  setSelectedResolution,
319  selectedSize,
320  setSelectedSize,
321  sizeOptions,
322  selectedCount,
323  setSelectedCount,
324  counts,
325  seed,
326  setSeed,
327  selectedBackground,
328  setSelectedBackground,
329  selectedInputFidelity,
330  setSelectedInputFidelity,
331  maskPreview,
332  setMaskPreview,
333  maskUrl,
334  setMaskUrl,
335  promptStrength,
336  setPromptStrength,
337  redirectToLogout,
338  isGenerating,
339  remainingCredits,
340  requiredCredits,
341  handleGenerate,
342  canUseTeamWallet = false,
343  teamRemainingCredits = null,
344  consumeCredits,
345  generationProgress
346}) {
347  const params = capability?.params ?? [];
348  const promptMaxLength = capability?.promptMaxLength ?? 1e3;
349  const promptOverLimit = promptLength(prompt) > promptMaxLength;
350  const inputImagesRequired = Boolean(capability?.slots.find((slot) => slot.slot === "input_images")?.required);
351  const isUploadingImages = uploadedImages.length > uploadedImageUrls.length;
352  const selectedModelObjects = models.filter((m) => selectedModels.includes(m.id));
353  const sharedModeValues = mergeModes(selectedModelObjects);
354  const sharedModes = (selectedModelObjects[0]?.modes ?? []).filter((m) => sharedModeValues.includes(m.mode));
355  const resolutionOptions = (capability?.resolutions ?? []).map((id) => ({ id, name: id === "512" ? "512px" : id }));
356  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-card rounded-lg border shadow-sm", children: [
357    /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pt-5 px-5 pb-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "flex items-center space-x-2 text-sm font-medium text-foreground", children: [
358      /* @__PURE__ */ jsxRuntimeExports.jsx(IconComponent, {}),
359      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "画像生成設定" })
360    ] }) }),
361    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-5 space-y-4", children: [
362      /* @__PURE__ */ jsxRuntimeExports.jsx(
363        ModelMultiSelect,
364        {
365          models,
366          selected: selectedModels,
367          onChange: setSelectedModels,
368          mode: selectedMode,
369          limits
370        }
371      ),
372      /* @__PURE__ */ jsxRuntimeExports.jsx(
373        ModeSelect,
374        {
375          selectedMode,
376          setSelectedMode,
377          modes: sharedModes
378        }
379      ),
380      showUpload && /* @__PURE__ */ jsxRuntimeExports.jsx(
381        CompactImageUpload,
382        {
383          uploadedImages,
384          setUploadedImages,
385          uploadedImageUrls,
386          setUploadedImageUrls,
387          isDragOver,
388          handleDragOver,
389          handleDragLeave,
390          handleDrop,
391          safeProcessFiles,
392          uploadValidationError,
393          setUploadValidationError,
394          maxUploads,
395          onOpenFilePicker,
396          label: inputImagesRequired ? "入力画像" : "入力画像(任意)"
397        }
398      ),
399      /* @__PURE__ */ jsxRuntimeExports.jsx(
400        PromptInput,
401        {
402          prompt,
403          setPrompt,
404          placeholders,
405          promptLanguage,
406          maxLength: promptMaxLength,
407          surface: "image",
408          targetModel: selectedModel
409        }
410      ),
411      capability?.resolutionSelectable && resolutionOptions.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
412        SelectMenu,
413        {
414          label: "解像度",
415          value: selectedResolution,
416          onChange: setSelectedResolution,
417          items: resolutionOptions
418        }
419      ),
420      params.includes("background") && /* @__PURE__ */ jsxRuntimeExports.jsx(
421        BackgroundSelect,
422        {
423          selectedBackground,
424          setSelectedBackground
425        }
426      ),
427      params.includes("input_fidelity") && showUpload && /* @__PURE__ */ jsxRuntimeExports.jsx(
428        InputFidelitySelect,
429        {
430          selectedInputFidelity,
431          setSelectedInputFidelity
432        }
433      ),
434      capability?.slots.some((slot) =>
434 slot.slot === "mask") && /* @__PURE__ */ jsxRuntimeExports.jsx(
435        MaskUpload,
436        {
437          maskPreview,
438          setMaskPreview,
439          maskUrl,
440          setMaskUrl,
441          redirectToLogout
442        }
443      ),
444      sizeOptions.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
445        GenerateSize,
446        {
447          selectedSize,
448          setSelectedSize,
449          sizeOptions,
450          compact: true
451        }
452      ),
453      params.includes("guidance_scale") && /* @__PURE__ */ jsxRuntimeExports.jsx(
454        PromptEffectBar,
455        {
456          promptStrength,
457          setPromptStrength
458        }
459      ),
460      /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-2 mt-2", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-[1fr_2fr] gap-4", children: [
461        /* @__PURE__ */ jsxRuntimeExports.jsx(
462          GenerationCount,
463          {
464            selectedCount,
465            setSelectedCount,
466            counts,
467            onlySingleGenerateModel: (capability?.maxCount ?? 1) <= 1
468          }
469        ),
470        /* @__PURE__ */ jsxRuntimeExports.jsx(
471          GenerateSeed,
472          {
473            seed,
474            setSeed,
475            minValue: capability?.seedMin ?? -1
476          }
477        )
478      ] }) }) }),
479      slotNotices && slotNotices.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3 space-y-1 bg-draft rounded-lg", children: [
480        /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] font-semibold text-foreground", children: "次の入力は、一部のモデルでは使われません" }),
481        /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "space-y-0.5", children: slotNotices.map((notice) => /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "text-[11px] text-muted-foreground", children: [
482          "・",
483          notice
484        ] }, notice)) })
485      ] }),
486      creditBreakdown && creditBreakdown.length > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3 space-y-1.5 bg-muted/60 rounded-lg", children: [
487        /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] font-semibold text-foreground", children: "モデル別の消費クレジット" }),
488        creditBreakdown.map((row) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-[11px] text-muted-foreground", children: [
489          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 min-w-0 truncate", children: row.label }),
490          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "tabular-nums", children: row.credits === null ? "試算できませんでした" : `${row.credits} クレジット` })
491        ] }, row.id)),
492        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 pt-1 border-t-theme text-[11px] font-semibold text-foreground", children: [
493          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1", children: "合計" }),
494          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "tabular-nums", children: creditBreakdown.every((row) => typeof row.credits === "number") ? `${creditBreakdown.reduce((sum, row) => sum + row.credits, 0)} クレジット` : "—" })
495        ] })
496      ] }),
497      /* @__PURE__ */ jsxRuntimeExports.jsx(
498        WalletGenerateButtons,
499        {
500          canUseTeamWallet,
501          teamRemainingCredits,
502          isGenerating,
503          prompt: capability?.promptRequired ? prompt : void 0,
504          disabled: showUpload && (uploadedImageUrls.length === 0 || isUploadingImages) || promptOverLimit || Boolean(creditBreakdown && (creditBreakdown.length !== selectedModels.length || creditBreakdown.some((row) => row.credits === null))),
505          remainingCredits,
506          requiredCreditsGenerate: requiredCredits,
507          consumeCredits,
508          generationProgress,
509          handleGenerate,
510          singleLabel: "画像を生成",
511          progressLabel: "AI画像生成中"
512        }
513      )
514    ] })
515  ] });
516}
517
518const TIMEOUT_MS = 1e3 * 60 * 5;
519function useParallelImageGeneration(callbacks) {
520  const [tasks, setTasks] = reactExports.useState([]);
521  const [isSubmitting, setIsSubmitting] = reactExports.useState(false);
522  const singleModelRef = reactExports.useRef(false);
523  const pendingAttemptRef = reactExports.useRef(null);
524  const pool = useTaskPollingPool({
525    fetchTask: getImageTask,
526    timeoutMs: TIMEOUT_MS,
527    onUnauthorized: () => callbacks.redirectToLogout(),
528    onSucceeded: (taskId, data) => {
529      const images = (data.images || []).map((img) => ({
530        ...img,
531        type: "image",
532        is_favorite: img.is_favorite || false
533      }));
534      setTasks((prev) => patchTask(prev, taskId, { status: "succeeded", progress: 100, images }));
535      callbacks.router.reload({ only: ["auth"] });
536    },
537    onFailed: (taskId, message) => {
538      if (singleModelRef.current) callbacks.onError(message);
539      setTasks((prev) => patchTask(prev, taskId, { status: "failed", progress: 100, error: message }));
540      callbacks.router.reload({ only: ["auth"] });
541    },
542    onTimeout: (taskId) => {
543      const message = "生成に時間がかかっています。完了するとマイギャラリーに反映されます。";
544      if (singleModelRef.current) callbacks.onError(message);
545      setTasks((prev) => patchTask(prev, taskId, { status: "timeout", progress: 100, error: message }));
546    }
547  });
548  const progress = useParallelProgress(tasks);
549  const setGeneratedImages = reactExports.useCallback((value) => {
550    setTasks((prev) => {
551      const flat = flattenImages(prev);
552      const next = typeof value === "function" ? value(flat) : value;
553      return prev.map((task) => ({
554        ...task,
555        // 残っているものだけを、更新後の内容で持ち直す(削除は落ち、お気に入りは反映される)
556        images: task.images.map((image) => next.find((n) => n.id === image.id)).filter((image) => image !== void 0)
557      }));
558    });
559  }, []);
560  const submit = reactExports.useCallback(async (params) => {
561    if (params.models.length === 0) return;
562    if (params.storageUsage?.generation_usage_rate !== null && Number(params.storageUsage?.generation_usage_rate) >= 100) {
563      callbacks.setShowStorageModal(true);
564      return;
565    }
566    pool.cancelAll();
567    setTasks([]);
568    setIsSubmitting(true);
569    const wallet = params.wallet ?? "personal";
570    const body = {
571      prompt: params.prompt,
572      size: params.size,
573      ...params.resolution ? { resolution: params.resolution } : {},
574      count: Number(params.count),
575      seed: params.seed,
576      watermark: false,
577      ...params.extraParams
578    };
579    const parallelPayload = {
580      models: params.models.map((m) => m.id),
581      mode: params.mode,
582      params: body,
583      slots: params.slots,
584      wallet
585    };
586    const signature = JSON.stringify(parallelPayload);
587    const idempotencyKey = pendingAttemptRef.current?.signature === signature ? pendingAttemptRef.current.key : newIdempotencyKey();
588    pendingAttemptRef.current = { signature, key: idempotencyKey };
589    try {
590      const res = params.models.length === 1 ? await createImageTask({
591        model: params.models[0].id,
592        mode: params.mode,
593        slots: params.slots,
594        ...body,
595        count: params.count,
596        wallet
597      }) : await createParallelImageTasks(parallelPayload, idempotencyKey);
598      const raw = await res.text();
599      if (res.status === 401) {
600        callbacks.redirectToLogout();
601        return;
602      }
603      if (!res.ok) {
604        const message = errorMessageOf(res.status, raw, callbacks.setShowStorageModal);
605        if (message === "") return;
606        throw new Error(message);
607      }
608      pendingAttemptRef.current = null;
609      const data = JSON.parse(raw);
610      const created = Array.isArray(data.items) ? data.items.map((item) => createParallelTask({
611        index: Number(item.index ?? 0),
612        modelId: String(item.model),
613        modelLabel: params.models.find((m) => m.id === item.model)?.label ?? String(item.model),
614        taskId: String(item.task_id),
615        count: Number(item.count ?? params.count),
616        credits: Number(item.credits ?? 0)
617      })) : [createParallelTask({
618        index: 0,
619        modelId: params.models[0].id,
620        modelLabel: params.models[0].label,
621        taskId: String(data.id),
622        count: Number(params.count),
623        credits: 0
624      })];
625      if (created.some((task) => !task.taskId || task.taskId === "undefined")) {
626        throw new Error("サーバーから不正な応答が返されました");
627      }
628      singleModelRef.current = created.length === 1;
629      setTasks(created);
630      callbacks.router.reload({ only: ["auth"] });
631      for (const task of created) {
632        setTasks((prev) => patchTask(prev, task.taskId, { status: "generating" }));
633        pool.start(task.taskId);
634      }
635    } catch (error) {
636      console.error("画像生成エラー:", error);
637      setTasks([]);
638      callbacks.router.reload({ only: ["auth"] });
639      callbacks.onError(error?.message || "画像生成に失敗しました。もう一度お試しください。");
640    } finally {
641      setIsSubmitting(false);
642    }
643  }, [callbacks, pool]);
644  const generatedImages = reactExports.useMemo(() => flattenImages(tasks), [tasks]);
645  const isGenerating = isSubmitting || tasks.length > 0 && tasks.some((task) => !isSettled(task));
646  return {
647    tasks,
648    setTasks,
649    submit,
650    setGeneratedImages,
651    generatedImages,
652    isGenerating,
653    progress,
654    /** 画面に出す「N / M 枚」の分母。全モデルのクランプ後の合計 */
655    expectedCount: totalCount(tasks)
656  };
657}
658function errorMessageOf(status, raw, setShowStorageModal) {
659  let parsed = null;
660  try {
661    parsed = JSON.parse(raw);
662  } catch {
663    if (raw?.startsWith("<!DOCTYPE") || raw?.startsWith("<html")) {
664      return "画像を生成できません。しばらくしてからもう一度お試しください。";
665    }
666  }
667  if (status === 422) {
668    const first = parsed?.errors ? Object.values(parsed.errors)[0] : null;
669    return (Array.isArray(first) ? first[0] : first) || parsed?.message || "入力内容をご確認ください。";
670  }
671  if (status === 403 && parsed?.error === "Storage limit exceeded") {
672    setShowStorageModal(true);
673    return "";
674  }
675  if (status === 402 || status === 409) {
676    return parsed?.message ?? (status === 409 ? "他に進行中の生成タスクがあるため、現在は新しい生成を開始できません。完了をお待ちください。" : "クレジットが不足しています。");
677  }
678  return parsed?.message ? `キュー投入に失敗しました(${status}): ${parsed.message}` : `キュー投入に失敗しました(${status})`;
679}
680
681const VIEW_STORAGE_KEY = "parallel-result-view";
682function ParallelResultPanel({
683  tasks,
684  models,
685  setImages,
686  onImageClick,
687  setDeleteTarget,
688  showFavoriteButton,
689  onDownload,
690  galleryHref
691}) {
692  const [view, setView] = reactExports.useState("by-model");
693  const [selectedPostIds, setSelectedPostIds] = reactExports.useState(/* @__PURE__ */ new Set());
694  const [postResults, setPostResults] = reactExports.useState(null);
695  const postableIds = reactExports.useMemo(
696    () => tasks.flatMap((task) => task.images.map((image) => image.id)),
697    [tasks]
698  );
699  const postableKey = postableIds.join(",");
700  reactExports.useEffect(() => {
701    setSelectedPostIds((prev) => {
702      const available = new Set(postableIds);
703      const next = new Set([...prev].filter((id) => available.has(id)));
704      return next.size === prev.size ? prev : next;
705    });
706  }, [postableKey]);
707  const allSelected = postableIds.length > 0 && selectedPostIds.size === postableIds.length;
708  const toggleOne = (id) => {
709    setSelectedPostIds((prev) => {
710      const next = new Set(prev);
711      next.has(id) ? next.delete(id) : next.add(id);
712      return next;
713    });
714  };
715  const toggleAll = () => {
716    setSelectedPostIds(allSelected ? /* @__PURE__ */ new Set() : new Set(postableIds));
717  };
718  const selectedPostItems = postableIds.filter((id) => selectedPostIds.has(id)).map((id) => ({ type: "image", id }));
719  const generationIds = reactExports.useMemo(
720    () => Array.from(new Set(
721      tasks.flatMap((task) => task.images.map((image) => image.generation_id)).filter((id) => typeof id === "number")
722    )),
723    [tasks]
724  );
725  const maxItemCount = reactExports.useMemo(
726    () => tasks.reduce((max, task) => Math.max(max, task.images.length), 0),
727    [tasks]
728  );
729  const initialTitle = reactExports.useMemo(() => {
730    const first = tasks.flatMap((task) => task.images).find((image) => image.generation_id === generationIds[0]);
731    return String(first?.title ?? "").replace(/\s*\(\d+\)\s*$/, "");
732  }, [tasks, generationIds]);
733  const applyResolvedTitles = (items) => {
734    setImages((prev) => prev.map((image) => {
735      const found = items.find((it) => it.type === "image" && it.id === image.id);
736      return found ? { ...image, title: found.title, title_source: found.title_source } : image;
737    }));
738  };
739  reactExports.useEffect(() => {
740    try {
741      const stored = window.localStorage.getItem(VIEW_STORAGE_KEY);
742      if (stored === "flat" || stored === "by-model") setView(stored);
743    } catch {
744    }
745  }, []);
746  const changeView = (next) => {
747    setView(next);
748    try {
749      window.localStorage.setItem(VIEW_STORAGE_KEY, next);
750    } catch {
751    }
752  };
753  const allSettled = tasks.length > 0 && tasks.every(isSettled);
754  const doneImages = tasks.reduce((sum, task) => sum + task.images.length, 0);
755  const expected = totalCount(tasks);
756  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-card rounded-lg border shadow-sm", children: [
757    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 px-6 pt-5 pb-3 border-b-theme", children: [
758      /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "flex-1 min-w-0 text-sm font-medium text-foreground", children: [
759        "生成結果:",
760        doneImages,
761        " / ",
762        expected,
763        " 枚"
764      ] }),
765      postableIds.length > 1 && /* @__PURE__ */ jsxRuntimeExports.jsx(
766        "button",
767        {
768          type: "button",
769          onClick: toggleAll,
770          "aria-pressed": allSelected,
771          className: "text-xs font-semibold text-blue-500 hover:text-blue-400 transition-colors",
772          children: allSelected ? "選択を解除" : "すべて選択"
773        }
774      ),
775      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 p-0.5 bg-muted rounded-md", role: "group", "aria-label": "表示の切り替え", children: [
776        /* @__PURE__ */ jsxRuntimeExports.jsx(
777          "button",
778          {
779            type: "button",
780            onClick: () => changeView("by-model"),
781            "aria-pressed": view === "by-model",
782            className: `px-2 py-1 text-[11px] font-semibold rounded focus:outline-none focus:ring-2 focus:ring-blue-500 ${view === "by-model" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground"}`,
783            children: "モデル別表示"
784          }
785        ),
786        /* @__PURE__ */ jsxRuntimeExports.jsx(
787          "button",
788          {
789            type: "button",
790            onClick: () => changeView("flat"),
791            "aria-pressed": view === "flat",
792            className: `px-2 py-1 text-[11px] font-semibold rounded focus:outline-none focus:ring-2 focus:ring-blue-500 ${view === "flat" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground"}`,
793            children: "並べて表示"
794          }
795        )
796      ] })
797    ] }),
798    /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-6 pt-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-muted-foreground", children: allSettled ? progressSummary(tasks) : progressBreakdown(tasks) }) }),
799    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-4", children: [
800      allSettled && generationIds.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
801        GenerationResultTitle,
802        {
803          generationId: generationIds[0],
804          additionalGenerationIds: generationIds.slice(1),
805          itemCount: maxItemCount,
806          initialTitle,
807          numberingNote: "モデルごとに、各画像へ (1)(2)… の連番を付けて保存します。",
808          pollForAuto: true,
809          onResolved: applyResolvedTitles
810        }
811      ),
812      view === "by-model" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: tasks.map((task) => /* @__PURE__ */ jsxRuntimeExports.jsx(
813        ModelResultCard,
814        {
815          task,
816          provider: models.find((m) => m.id === task.modelId)?.provider,
817          setImages,
818          onImageClick,
819          setDeleteTarget,
820          showFavoriteButton,
821          onDownload,
822          galleryHref,
823          selectedPostIds: postableIds.length > 1 ? selectedPostIds : void 0,
824          onTogglePost: postableIds.length > 1 ? toggleOne : void 0
825        },
826        task.taskId ?? task.modelId
827      )) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-2 lg:grid-cols-4 gap-4", children: tasks.flatMap((task) => {
828        if (task.images.length === 0) {
829          if (task.status === "succeeded") {
830            return [/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
831              /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "aspect-square grid place-items-center rounded-md bg-muted/60 text-center px-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-muted-foreground", children: "生成結果をすべて削除しました" }) }),
832              /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-muted-foreground truncate", children: task.modelLabel })
833            ] }, `deleted-${task.taskId ?? task.modelId}`)];
834          }
835          if (task.status !== "failed" && task.status !== "timeout") {
836            return Array.from({ length: Math.max(1, task.count) }, (_, slot) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
837              /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "aspect-square grid place-items-center rounded-md bg-muted animate-pulse", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-5 h-5 text-muted-foreground animate-spin" }) }),
838              /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-muted-foreground truncate", children: task.modelLabel })
839            ] }, `pending-${task.taskId ?? task.modelId}-${slot}`));
840          }
841          return [/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
842            /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "aspect-square grid place-items-center rounded-md bg-muted/60 text-center px-3", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
843              /* @__PURE__ */ jsxRuntimeExports.jsx(StatusChip, { status: task.status }),
844              /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-muted-foreground break-words", children: task.error }),
845              task.status === "timeout" && galleryHref && /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: galleryHref, className: "inline-block text-[11px] font-semibold text-blue-500 hover:underline", children: "マイギャラリーで確認する" })
846            ] }) }),
847            /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-muted-foreground truncate", children: task.modelLabel })
848          ] }, `failed-${task.taskId ?? task.modelId}`)];
849        }
850        return task.images.map((image, index) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
851          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `relative rounded-lg ${selectedPostIds.has(image.id) ? "ring-2 ring-blue-500" : ""}`, children: [
852            postableIds.length > 1 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-2 left-2 z-20", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsx(
853              "input",
854              {
855                type: "checkbox",
856                checked: selectedPostIds.has(image.id),
857                onChange: () => toggleOne(image.id),
858                className: "w-4 h-4 rounded border-white/60 bg-black/40 accent-blue-500 cursor-pointer",
859                "aria-label": `${task.modelLabel}の画像${index + 1}を投稿対象に選択`
860              }
861            ) }),
862            /* @__PURE__ */ jsxRuntimeExports.jsx(
863              Thumbnail,
864              {
865                image,
866                setImages,
867                handleImageClick: onImageClick,
868                setDeleteTarget,
869                showFavoriteButton
870              }
871            ),
872            /* @__PURE__ */ jsxRuntimeExports.jsx(
873              "button",
874              {
875                type: "button",
876                onClick: (e) => onDownload(image.id, e),
877                "aria-label": `${task.modelLabel} の画像をダウンロード`,
878                className: "absolute bottom-2 right-2 z-10 p-1.5 rounded-full bg-black/45 text-white hover:bg-black/65 focus:outline-none focus:ring-2 focus:ring-white/70",
879                children: /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { className: "w-3.5 h-3.5" })
880              }
881            )
882          ] }),
883          /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[11px] text-muted-foreground truncate", children: [
884            task.modelLabel,
885            task.images.length > 1 ? `(${index + 1})` : ""
886          ] })
887        ] }, image.id));
888      }) }),
889      postableIds.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-2 pt-2", children: [
890        postableIds.length > 1 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-semibold text-muted-foreground", children: selectedPostItems.length > 0 ? `${selectedPostItems.length}件を選択中` : "投稿する画像を選択してください" }),
891        (postableIds.length === 1 || selectedPostItems.length > 0) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: postableIds.length > 1 ? "ml-auto" : "", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
892          ProjectPostPicker,
893          {
894            items: postableIds.length === 1 ? [{ type: "image", id: postableIds[0] }] : selectedPostItems,
895            onPosted: () => setSelectedPostIds(/* @__PURE__ */ new Set()),
896            onFailures: setPostResults
897          }
898        ) })
899      ] })
900    ] }),
901    /* @__PURE__ */ jsxRuntimeExports.jsx(ProjectPostModal, { results: postResults, onClose: () => setPostResults(null) })
902  ] });
903}
904
905const PLACEHOLDERS = {
906  ja: "例:桜咲く春の公園で遊ぶ子供たち、暖かい午後の光、パステル調の優しい色合い"
907};
908const WandIcon = () => /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: [
909  /* @__PURE__ */ jsxRuntimeExports.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }),
910  /* @__PURE__ */ jsxRuntimeExports.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" })
911] });
912function ImageGenerationPanel({
913  capabilities,
914  parallel,
915  sizes,
916  counts,
917  auth,
918  isFreePlan,
919  storage_usage,
920  initialMode,
921  setShowStorageModal,
922  consumeCredits,
923  fetchImageCredits
924}) {
925  const limits = parallel ?? SINGLE_ONLY_LIMITS;
926  const [selectedModels, setSelectedModels] = reactExports.useState(() => capabilities[0] ? [capabilities[0].id] : []);
927  const selectedModel = selectedModels[0] ?? "";
928  const setSelectedModel = (id) => setSelectedModels([id]);
929  const isParallel = selectedModels.length > 1;
930  const [selectedMode, setSelectedMode] = reactExports.useState(() => initialMode ?? defaultMode(capabilities[0] ?? null));
931  const [prompt, setPrompt] = reactExports.useState("");
932  const [selectedSize, setSelectedSize] = reactExports.useState(() => Array.isArray(sizes) && sizes.length > 0 ? String(sizes[0].id) : "");
933  const [selectedResolution, setSelectedResolution] = reactExports.useState("");
934  const [selectedCount, setSelectedCount] = reactExports.useState("2");
935  const [seed, setSeed] = reactExports.useState(-1);
936  const [promptStrength, setPromptStrength] = reactExports.useState(4);
937  const [selectedBackground, setSelectedBackground] = reactExports.useState("opaque");
938  const [selectedInputFidelity, setSelectedInputFidelity] = reactExports.useState("high");
939  const [maskPreview, setMaskPreview] = reactExports.useState(null);
940  const [maskUrl, setMaskUrl] = reactExports.useState(null);
941  const [uploadedImages, setUploadedImages] = reactExports.useState([]);
942  const [uploadedImageUrls, setUploadedImageUrls] = reactExports.useState([]);
943  const [uploadValidationError, setUploadValidationError] = reactExports.useState(null);
944  const [isDragOver, setIsDragOver] = reactExports.useState(false);
945  const [showFilePicker, setShowFilePicker] = reactExports.useState(false);
946  const [selectedImage, setSelectedImage] = reactExports.useState(null);
947  const [showModal, setShowModal] = reactExports.useState(false);
948  const [deleteTarget, setDeleteTarget] = reactExports.useState(null);
949  const [isDeleting, setIsDeleting] = reactExports.useState(false);
950  const [deleteError, setDeleteError] = reactExports.useState(null);
951  const model = findModel(capabilities, selectedModel);
952  const selectedModelObjects = reactExports.useMemo(
953    () => capabilities.filter((m) => selectedModels.includes(m.id)),
954    [capabilities, selectedModels]
955  );
956  const capability = reactExports.useMemo(() => {
957    const modes = modesOf(selectedModelObjects, selectedMode);
958    const base = modes[0] ?? findMode(model, selectedMode);
959    if (!base) return null;
960    if (modes.length <= 1) return base;
961    const ratios = mergeRatios(modes);
962    const resolutions = mergeResolutions(modes);
963    const allowMask = modes.every((m) => m.slots.some((slot) => slot.slot === SLOT.mask));
964    return {
965      ...base,
966      slots: mergeSlots(selectedModelObjects, selectedMode).filter((slot) => slot.slot !== SLOT.mask || allowMask),
967      ratios,
968      ratioSelectable: ratios.length > 0,
969      resolutions,
970      resolutionSelectable: resolutions.length > 0,
971      params: mergeParams(modes),
972      promptMaxLength: mergePromptMaxLength(modes) ?? base.promptMaxLength,
973      promptRequired: mergePromptRequired(modes),
974      maxCount: mergeMaxCount(modes, limits, selectedModels.length),
975      // シードは全モデルで0以上に揃える(サーバの受付も同じ範囲で見る)。
976      // 宣言は -1(ランダム)を許すモデルが多いが、複数選択は比較が目的で
977      // モデルごとに違う値が当たっては条件が揃わないため、下限を 0 で固定する
978      seedMin: Math.max(0, ...modes.map((m) => m.seedMin)),
979      seedSupported: modes.every((m) => m.seedSupported)
980    };
981  }, [selectedModelObjects, selectedMode, model, limits, selectedModels.length]);
982  const showUpload = hasSlot(capability, SLOT.inputImages);
983  const uploadLimit = slotMax(capability, SLOT.inputImages);
984  const maxUploads = uploadLimit ?? 10;
985  const {
986    tasks,
987    submit,
988    setGeneratedImages,
989    generatedImages,
990    isGenerating,
991    progress: generationProgress,
992    expectedCount
993  } = useParallelImageGeneration({
994    setShowStorageModal,
995    redirectToLogout,
996    router: router3,
997    onError: (message) => toast.error(message)
998  });
999  const activeCount = String(expectedCount || selectedCount);
1000  function redirectToLogout() {
1001    router3.visit(s("logout.success", { title: "セッションが切れました", message: "再度ログインしてください。" }), {
1002      preserveScroll: false,
1003      preserveState: false
1004    });
1005  }
1006  const {
1007    safeProcessFiles,
1008    handleDragOver,
1009    handleDragLeave,
1010    handleDrop
1011  } = useImageUpload(uploadedImages, maxUploads, {
1012    setUploadedImages,
1013    setUploadedImageUrls,
1014    setUploadValidationError,
1015    setIsDragOver,
1016    redirectToLogout
1017  });
1018  reactExports.useEffect(() => {
1019    setSelectedMode((current) => {
1020      if (selectedModelObjects.length <= 1) return keepOrDefaultMode(model, current);
1021      const shared = mergeModes(selectedModelObjects);
1022      if (shared.includes(current)) return current;
1023      return shared[0] ?? keepOrDefaultMode(model, current);
1024    });
1025  }, [selectedModels.join(",")]);
1026  const prevSingleCountRef = reactExports.useRef((capability?.maxCount ?? 1) <= 1);
1027  reactExports.useEffect(() => {
1028    const onlySingle = (capability?.maxCount ?? 1) <= 1;
1029    if (onlySingle) {
1030      setSelectedCount("1");
1031    } else if (prevSingleCountRef.current) {
1032      setSelectedCount("2");
1033    }
1034    prevSingleCountRef.current = onlySingle;
1035  }, [capability]);
1036  reactExports.useEffect(() => {
1037    if (uploadLimit === null) return;
1038    setUploadedImages((prev) => prev.length > uploadLimit ? prev.slice(0, uploadLimit) : prev);
1039    setUploadedImageUrls((prev) => prev.length > uploadLimit ? prev.slice(0, uploadLimit) : prev);
1040  }, [uploadLimit]);
1041  reactExports.useEffect(() => {
1042    if (!capability) return;
1043    setSelectedResolution((current) => capability.resolutions.length === 0 ? "" : keepOrFirst(capability.resolutions, current, "2K"));
1044    setSelectedCount((current) => String(Math.min(Number(current) || 1, capability.maxCount)));
1045    setSeed((current) => current < capability.seedMin ? capability.seedMin : current);
1046  }, [selectedModel, selectedMode, capability]);
1047  const sizeOptions = reactExports.useMemo(() => {
1048    const allowed = capability?.ratios ?? [];
1049    if (allowed.length === 0) return [];
1050    return (sizes || []).filter((size) => allowed.includes(String(size.id))).map((size) => ({
1051      id: String(size.id),
1052      name: size.name ?? size.id,
1053      disabled: false
1054    }));
1055  }, [sizes, capability]);
1056  reactExports.useEffect(() => {
1057    if (sizeOptions.some((size) => size.id === selectedSize)) return;
1058    const fallback = sizeOptions.find((size) => size.name === "1:1") ?? sizeOptions[0];
1059    if (fallback) setSelectedSize(fallback.id);
1060  }, [sizeOptions, selectedSize]);
1061  const filteredCounts = reactExports.useMemo(
1062    () => (counts || []).filter((count) => Number(count.id) <= (capability?.maxCount ?? 1)),
1063    [counts, capability]
1064  );
1065  const effectiveResolution = reactExports.useMemo(
1066    () => (capability?.resolutions.length ?? 0) === 0 ? "" : keepOrFirst(capability.resolutions, selectedResolution, "2K"),
1067    [capability, selectedResolution]
1068  );
1069  const effectiveSize = reactExports.useMemo(() => {
1070    const allowed = capability?.ratios ?? [];
1071    if (allowed.length === 0 || allowed.includes(String(selectedSize))) return selectedSize;
1072    const fallback = (sizes || []).find((size) => allowed.includes(String(size.id)) && size.name === "1:1") ?? (sizes || []).find((size) => allowed.includes(String(size.id)));
1073    return fallback ? String(fallback.id) : selectedSize;
1074  }, [capability, selectedSize, sizes]);
1075  const effectiveCount = reactExports.useMemo(
1076    () => String(Math.min(Number(selectedCount) || 1, capability?.maxCount ?? 1)),
1077    [capability, selectedCount]
1078  );
1079  reactExports.useEffect(() => {
1080    if (!selectedModel || !selectedMode || !selectedSize || !capability) return;
1081    if (isParallel) return;
1082    fetchImageCredits({
1083      model: selectedModel,
1084      mode: selectedMode,
1085      size: effectiveSize,
1086      ...effectiveResolution ? { resolution: effectiveResolution } : {},
1087      count: Number(effectiveCount)
1088    });
1089  }, [selectedModel, selectedMode, effectiveSize, effectiveResolution, effectiveCount, capability, isParallel, fetchImageCredits]);
1090  reactExports.useEffect(() => {
1091    if (typeof window === "undefined") return;
1092    const stored = consumeReusePrompt();
1093    if (stored) {
1094      setPrompt(stored);
1095      return;
1096    }
1097    const params = new URLSearchParams(window.location.search);
1098    const queryPrompt = params.get("prompt");
1099    if (queryPrompt) setPrompt(queryPrompt);
1100  }, []);
1101  reactExports.useEffect(() => {
1102    if (typeof window === "undefined") return;
1103    const params = new URLSearchParams(window.location.search);
1104    const queryModel = params.get("model");
1105    if (!queryModel) return;
1106    const matched = findModelByAnyId(capabilities, queryModel);
1107    if (matched) setSelectedModel(matched.id);
1108    params.delete("model");
1109    const newUrl = params.toString() ? `${window.location.pathname}?${params.toString()}` : window.location.pathname;
1110    window.history.replaceState({}, "", newUrl);
1111  }, []);
1112  reactExports.useEffect(() => {
1113    if (!deleteError) return;
1114    const timer = setTimeout(() => setDeleteError(null), 5e3);
1115    return () => clearTimeout(timer);
1116  }, [deleteError]);
1117  const slotNotices = reactExports.useMemo(() => {
1118    if (!isParallel) return [];
1119    const filled = {
1120      [SLOT.inputImages]: uploadedImageUrls.length,
1121      [SLOT.mask]: maskUrl ? 1 : 0
1122    };
1123    return mergeSlots(selectedModelObjects, selectedMode).filter((slot) => (filled[slot.slot] ?? 0) > 0).flatMap((slot) => {
1124      const messages = [];
1125      if (slot.unsupportedBy.length > 0) {
1126        messages.push(slot.slot === SLOT.mask ? `${slot.label}は、対応していないモデルが含まれるため使われません` : `${slot.label}は ${slot.unsupportedBy.join("・")} では使われません`);
1127      }
1128      for (const row of slot.clampedBy) {
1129        if ((filled[slot.slot] ?? 0) > row.max) {
1130          messages.push(`${slot.label}は ${row.label} では先頭${row.max}件のみ使われます`);
1131        }
1132      }
1133      return messages;
1134    });
1135  }, [isParallel, selectedModelObjects, selectedMode, uploadedImageUrls.length, maskUrl]);
1136  const multiCredit = useMultiModelCredit({
1137    // 枚数はモデルごとの上限まで丸める。サーバも同じ丸めをしてから課金するので、
1138    // 揃えないと上限の小さいモデルの内訳と合計が実際より高く出る
1139    models: selectedModelObjects.map((m) => {
1140      const modeCapability = m.modes.find((x) => x.mode === selectedMode);
1141      return {
1142        id: m.id,
1143        label: m.label,
1144        count: Math.min(Number(effectiveCount), modeCapability?.maxCount ?? 1)
1145      };
1146    }),
1147    mode: selectedMode,
1148    size: effectiveSize,
1149    resolution: effectiveResolution || null,
1150    // 比率が決まらないうちは試算しない
1151    enabled: isParallel && Boolean(effectiveSize)
1152  });
1153  const remainingCredits = Number(auth?.available_credits ?? auth?.remaining_credits ?? 0);
1154  const singleRequiredCredits = typeof consumeCredits === "number" && !Number.isNaN(consumeCredits) ? consumeCredits : null;
1155  const requiredCredits = isParallel ? multiCredit.total : singleRequiredCredits;
1156  const canUseTeamWallet = Boolean(auth?.team?.can_use_team_wallet);
1157  const teamRemainingCredits = typeof auth?.team?.team_remaining_credits === "number" ? auth.team.team_remaining_credits : null;
1158  const handleGenerate = async (wallet = "personal") => {
1159    const params = capability?.params ?? [];
1160    await submit(
1161      {
1162        models: selectedModelObjects.map((m) => ({ id: m.id, label: m.label })),
1163        mode: selectedMode,
1164        slots: {
1165          ...showUpload && uploadedImageUrls.length > 0 ? { [SLOT.inputImages]: uploadedImageUrls } : {},
1166          ...maskUrl && hasSlot(capability, SLOT.mask) ? { [SLOT.mask]: maskUrl } : {}
1167        },
1168        prompt,
1169        size: effectiveSize,
1170        resolution: effectiveResolution || null,
1171        count: effectiveCount,
1172        seed,
1173        extraParams: {
1174          ...params.includes("background") ? { background: selectedBackground } : {},
1175          ...params.includes("input_fidelity") && showUpload ? { input_fidelity: selectedInputFidelity } : {},
1176          ...params.includes("guidance_scale") ? { guidance_scale: promptStrength } : {}
1177        },
1178        storageUsage: storage_usage,
1179        wallet: wallet === "team" ? "team" : "personal"
1180      }
1181    );
1182  };
1183  const handleDownload = (imageId, e) => {
1184    e.stopPropagation();
1185    window.location.href = s("images.download", { id: imageId });
1186  };
1187  const handleDeleteConfirm = async () => {
1188    if (!deleteTarget?.id) {
1189      setDeleteTarget(null);
1190      return;
1191    }
1192    setIsDeleting(true);
1193    setDeleteError(null);
1194    try {
1195      await deleteMediaByTaskId("image", deleteTarget.id);
1196      setGeneratedImages((prev) => prev.filter((image) => image.id !== deleteTarget.id));
1197      setDeleteTarget(null);
1198      setShowModal(false);
1199    } catch (error) {
1200      console.error("画像の削除に失敗しました:", error);
1201      setDeleteError(error.message || "画像の削除に失敗しました。もう一度お試しください。");
1202    } finally {
1203      setIsDeleting(false);
1204    }
1205  };
1206  const detailModalItem = reactExports.useMemo(() => {
1207    if (!selectedImage) return null;
1208    const latest = generatedImages.find((image) => image.id === selectedImage.id) || selectedImage;
1209    const saved = latest?.meta?.parameters || {};
1210    const owner = tasks.find((task) => task.images.some((image) => image.id === latest.id));
1211    const sizeLabel = sizes.find((size) => String(size.id) === String(saved.size ?? selectedSize))?.name ?? selectedSize;
1212    return {
1213      ...latest,
1214      type: "image",
1215      prompt: latest.prompt ?? prompt,
1216      generation_type: latest.generation_type ?? (showUpload ? "image_editing" : "image_generation"),
1217      // size/resolution/count/seed と同じく saved を優先する。生成後にモデル選択を変えても
1218      // 表示中の結果のモデル名がズレない。saved.model は ImageTaskController::show が
1219      // ModelType::label() で上書き済みなのでそのまま表示ラベルとして使える。
1220      model_label: saved.model ?? owner?.modelLabel ?? model?.label ?? selectedModel,
1221      size_label: sizeLabel,
1222      parameters: {
1223        model: saved.model ?? owner?.modelId ?? selectedModel,
1224        size: sizeLabel,
1225        resolution: saved.resolution ?? selectedResolution,
1226        count: saved.count ?? owner?.count ?? activeCount,
1227        // seed は -1(ランダム)も送信され generations.parameters にそのまま保存さã
1227‚Œã‚‹ãŸã‚éš ã•ない。
1228        // saved 優先は維持(生成後にバックエンドが返す実シードのほうが正確)。
1229        seed: "seed" in saved ? saved.seed : seed
1230      }
1231    };
1232  }, [selectedImage, generatedImages, tasks, selectedModel, selectedSize, selectedResolution, activeCount, seed, sizes, prompt, showUpload, model]);
1233  const pickerRoom = Math.max(0, maxUploads - uploadedImages.length);
1234  const handleFilePicked = async (files) => {
1235    setShowFilePicker(false);
1236    const picked = files.filter((file) => !!file.url);
1237    if (picked.length === 0) return;
1238    const overLimit = picked.length > pickerRoom;
1239    const target = picked.slice(0, pickerRoom);
1240    const valid = [];
1241    let lastError = null;
1242    for (const file of target) {
1243      const message = await validateImageItem(file, IMAGE_VALIDATION_OPTIONS);
1244      if (message) {
1245        lastError = message;
1246        continue;
1247      }
1248      valid.push(file);
1249    }
1250    if (valid.length > 0) {
1251      setUploadedImages((prev) => [...prev, ...valid.map((file) => file.thumbnail || file.url)]);
1252      setUploadedImageUrls((prev) => [...prev, ...valid.map((file) => file.url)]);
1253    }
1254    if (lastError) {
1255      setUploadValidationError(target.length === 1 ? lastError : `一部画像を追加できませんでした。${lastError}`);
1256    } else {
1257      setUploadValidationError(overLimit ? "入力画像の上限を超える分は追加していません。" : null);
1258    }
1259  };
1260  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-10 gap-6 pb-8", children: [
1261    /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-6 lg:col-span-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1262      ImageSettingCard,
1263      {
1264        iconComponent: WandIcon,
1265        models: capabilities,
1266        selectedModel,
1267        setSelectedModel,
1268        selectedModels,
1269        setSelectedModels,
1270        limits,
1271        selectedMode,
1272        setSelectedMode,
1273        capability,
1274        showUpload,
1275        uploadedImages,
1276        setUploadedImages,
1277        uploadedImageUrls,
1278        setUploadedImageUrls,
1279        isDragOver,
1280        handleDragOver,
1281        handleDragLeave,
1282        handleDrop,
1283        safeProcessFiles,
1284        uploadValidationError,
1285        setUploadValidationError,
1286        maxUploads,
1287        onOpenFilePicker: () => setShowFilePicker(true),
1288        prompt,
1289        setPrompt,
1290        placeholders: PLACEHOLDERS,
1291        promptLanguage: "ja",
1292        selectedResolution,
1293        setSelectedResolution,
1294        selectedSize,
1295        setSelectedSize,
1296        sizeOptions,
1297        selectedCount,
1298        setSelectedCount,
1299        counts: filteredCounts,
1300        seed,
1301        setSeed,
1302        selectedBackground,
1303        setSelectedBackground,
1304        selectedInputFidelity,
1305        setSelectedInputFidelity,
1306        maskPreview,
1307        setMaskPreview,
1308        maskUrl,
1309        setMaskUrl,
1310        promptStrength,
1311        setPromptStrength,
1312        redirectToLogout,
1313        isGenerating,
1314        remainingCredits,
1315        requiredCredits,
1316        handleGenerate,
1317        canUseTeamWallet,
1318        teamRemainingCredits,
1319        consumeCredits: requiredCredits,
1320        creditBreakdown: isParallel ? multiCredit.breakdown : void 0,
1321        slotNotices,
1322        generationProgress
1323      }
1324    ) }),
1325    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6 lg:col-span-7", children: [
1326      tasks.length > 1 ? /* @__PURE__ */ jsxRuntimeExports.jsx(
1327        ParallelResultPanel,
1328        {
1329          tasks,
1330          models: capabilities,
1331          setImages: setGeneratedImages,
1332          onImageClick: (image) => {
1333            setSelectedImage(image);
1334            setShowModal(true);
1335          },
1336          setDeleteTarget,
1337          showFavoriteButton: !isFreePlan,
1338          onDownload: handleDownload,
1339          galleryHref: s("my-gallery.index")
1340        }
1341      ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
1342        GenerateResult,
1343        {
1344          projectPostItems: generatedImages.map((image) => ({
1345            type: "image",
1346            id: image.id
1347          })),
1348          generatedImages,
1349          setGeneratedImages,
1350          handleImageClick: (image) => {
1351            setSelectedImage(image);
1352            setShowModal(true);
1353          },
1354          setDeleteTarget,
1355          showFavoriteButton: !isFreePlan,
1356          isGenerating,
1357          activeCount,
1358          prompt,
1359          handleDownload,
1360          iconComponent: WandIcon,
1361          children: "プロンプトを入力して画像を生成してください"
1362        }
1363      ),
1364      deleteError && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30 animate-fade-in", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-w-md w-full p-4 bg-red-50 border-2 border-red-50 rounded-lg shadow-2xl animate-scale-in", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-red-800 font-medium", children: deleteError }) }) })
1365    ] }),
1366    !showModal && /* @__PURE__ */ jsxRuntimeExports.jsx(Toaster, { position: "top-center", richColors: true }),
1367    /* @__PURE__ */ jsxRuntimeExports.jsx(
1368      GenerationDetailModal,
1369      {
1370        isOpen: showModal,
1371        onClose: () => {
1372          setShowModal(false);
1373          setSelectedImage(null);
1374        },
1375        item: detailModalItem,
1376        items: tasks.length > 1 ? generatedImages : void 0,
1377        onNavigateItem: tasks.length > 1 ? (next) => setSelectedImage(next) : void 0,
1378        onDelete: () => setDeleteTarget(selectedImage),
1379        setGeneration: setGeneratedImages,
1380        showFavoriteButton: !isFreePlan,
1381        source: "other",
1382        onBeforeUnfavorite: void 0
1383      }
1384    ),
1385    /* @__PURE__ */ jsxRuntimeExports.jsx(
1386      DeleteConfirmModal,
1387      {
1388        open: !!deleteTarget,
1389        title: "削除確認",
1390        description: "選択したコンテンツを削除しますか?",
1391        onCancel: () => setDeleteTarget(null),
1392        onConfirm: handleDeleteConfirm,
1393        isDeleting
1394      }
1395    ),
1396    /* @__PURE__ */ jsxRuntimeExports.jsx(
1397      FilePickerModal,
1398      {
1399        isOpen: showFilePicker,
1400        onClose: () => setShowFilePicker(false),
1401        onSelect: handleFilePicked,
1402        allowedTypes: ["image"],
1403        multiple: pickerRoom > 1,
1404        maxSelectable: pickerRoom,
1405        title: "ファイルから画像を選択"
1406      }
1407    )
1408  ] });
1409}
1410
1411function ImageGenerate(props) {
1412  const { auth, sizes, counts, capabilities, parallel } = props;
1413  const { storage_usage, user } = auth;
1414  const PLAN_TYPE_FREE = 0;
1415  const isFreePlan = user?.plan_id === PLAN_TYPE_FREE;
1416  const initialMode = (() => {
1417    if (typeof window === "undefined") return null;
1418    return modeFromQuery(new URLSearchParams(window.location.search), capabilities ?? []);
1419  })();
1420  const [showStorageModal, setShowStorageModal] = reactExports.useState(false);
1421  const { consumeCredits, fetchImageCredits } = useGenerationCredit();
1422  return /* @__PURE__ */ jsxRuntimeExports.jsxs(AuthenticatedLayout, { children: [
1423    /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "min-h-screen", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full px-6 pt-6", children: [
1424      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-6", children: [
1425        /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-xl md:text-2xl font-semibold mb-2 text-foreground", children: "画像生成" }),
1426        /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-muted-foreground text-xs md:text-sm", children: "AIを使って画像を生成します" })
1427      ] }),
1428      /* @__PURE__ */ jsxRuntimeExports.jsx(
1429        ImageGenerationPanel,
1430        {
1431          capabilities: capabilities ?? [],
1432          parallel,
1433          sizes,
1434          counts,
1435          auth,
1436          isFreePlan,
1437          storage_usage,
1438          initialMode,
1439          setShowStorageModal,
1440          consumeCredits,
1441          fetchImageCredits
1442        }
1443      )
1444    ] }) }),
1445    /* @__PURE__ */ jsxRuntimeExports.jsx(
1446      WarningModal,
1447      {
1448        show: showStorageModal,
1449        onClose: () => setShowStorageModal(false),
1450        title: "ストレージ容量超過",
1451        message: storage_usage ? `生成コンテンツの上限:${storage_usage.generation_limit_gb} GB / 使用量:${storage_usage.generation_storage_gb.toFixed(2)} GB` : "ストレージ情報を取得できませんでした",
1452        warningText: /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1453          "ストレージ使用量が加入プランの上限を超えています。",
1454          /* @__PURE__ */ jsxRuntimeExports.jsx("br", {}),
1455          "不要なコンテンツを削除する、またはプランをアップグレードしてください。"
1456        ] }),
1457        secondaryButtonText: "プラン変更",
1458        onSecondary: () => {
1459          setShowStorageModal(false);
1460          router3.visit(s("payment.settings"));
1461        },
1462        confirmButtonText: "マイギャラリー",
1463        onConfirm: () => {
1464          setShowStorageModal(false);
1465          router3.visit(s("my-gallery.index"));
1466        }
1467      }
1468    )
1469  ] });
1470}
1471
1472export { ImageGenerate as default };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.