@mevdragon/vidfarm-devcli 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/editor-capabilities/SKILL.md +166 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/vidfarm-media/SKILL.md +2 -0
- package/SKILL.director.md +174 -16
- package/SKILL.platform.md +7 -3
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +77 -75
- package/dist/src/app.js +1513 -145
- package/dist/src/cli.js +1365 -105
- package/dist/src/config.js +7 -0
- package/dist/src/devcli/clips.js +3 -0
- package/dist/src/devcli/composition-edit.js +396 -8
- package/dist/src/devcli/local-backend.js +123 -0
- package/dist/src/devcli/migrate-local.js +140 -0
- package/dist/src/devcli/sync.js +311 -0
- package/dist/src/devcli/timeline-edit.js +208 -1
- package/dist/src/editor-chat.js +33 -17
- package/dist/src/frontend/file-directory.js +271 -20
- package/dist/src/frontend/homepage-view.js +3 -3
- package/dist/src/frontend/template-editor-chat.js +6 -3
- package/dist/src/page-shell.js +1 -1
- package/dist/src/primitive-registry.js +409 -4
- package/dist/src/reskin/chat-page.js +103 -15
- package/dist/src/reskin/discover-page.js +247 -10
- package/dist/src/reskin/document.js +147 -10
- package/dist/src/reskin/inpaint-clipper-page.js +649 -0
- package/dist/src/reskin/inpaint-page.js +2459 -452
- package/dist/src/reskin/inpaint-video-page.js +1339 -0
- package/dist/src/reskin/library-page.js +324 -82
- package/dist/src/reskin/theme.js +36 -11
- package/dist/src/services/billing.js +4 -0
- package/dist/src/services/clip-curation/hunt.js +2 -0
- package/dist/src/services/clip-records.js +28 -0
- package/dist/src/services/file-directory.js +6 -3
- package/dist/src/services/hyperframes.js +283 -3
- package/dist/src/services/serverless-records.js +43 -0
- package/dist/src/services/storage.js +24 -2
- package/package.json +1 -1
- package/public/assets/file-directory-app.js +2 -2
- package/public/assets/homepage-client-app.js +1 -1
- package/public/assets/page-runtime-client-app.js +2 -2
- package/public/assets/placeholders/scene-placeholder.png +0 -0
|
@@ -1,543 +1,2550 @@
|
|
|
1
|
-
// /
|
|
2
|
-
// farmville design system.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// • create job → POST /api/v1/primitives/images/inpaint
|
|
9
|
-
// body { tracer, payload:{ source_image_url, mask_url,
|
|
10
|
-
// instruction, reference_attachments, provider?, size,
|
|
11
|
-
// aspect_ratio?, output_format } }
|
|
12
|
-
// • poll job → GET /api/v1/primitives/image_inpaint/jobs/:jobId
|
|
1
|
+
// /tools/image — "Create Image" masked image editor, ported into the
|
|
2
|
+
// farmville design system. It restores the FULL legacy inpaint workbench
|
|
3
|
+
// (multi-color masks, a per-mask-color prompt for every color, a separate
|
|
4
|
+
// whole-image prompt, the intelligent front-end merge that composes them into
|
|
5
|
+
// one provider instruction, reference attachments, and a real tracer-grouped
|
|
6
|
+
// History strip) AND keeps the newer text-to-image "Create Image" empty-state
|
|
7
|
+
// composer + useAsSource refinement flow.
|
|
13
8
|
//
|
|
14
|
-
// The
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
9
|
+
// The proven legacy control/mask/merge/history logic (from renderCustomInpaintPage
|
|
10
|
+
// in src/template-editor-pages.ts) is embedded verbatim below and only reskinned;
|
|
11
|
+
// the text-to-image composer is layered on top inside the same IIFE so it can
|
|
12
|
+
// reuse the legacy helpers (setStatus, loadSourceUrl, uploadTemporaryBlob, …).
|
|
13
|
+
//
|
|
14
|
+
// Endpoints (all same-origin, session cookie):
|
|
15
|
+
// • temp upload → POST /api/v1/user/me/temporary-files/presign
|
|
16
|
+
// → (server|direct) upload → POST /api/v1/user/me/temporary-files
|
|
17
|
+
// • providers → GET /api/v1/user/me/provider-keys
|
|
18
|
+
// • inpaint job → POST /api/v1/primitives/images/inpaint
|
|
19
|
+
// body { tracer, payload:{ source_image_url, mask_url,
|
|
20
|
+
// instruction, reference_attachments, provider?, size,
|
|
21
|
+
// aspect_ratio?, output_format } }
|
|
22
|
+
// • poll inpaint → GET /api/v1/primitives/image_inpaint/jobs/:jobId
|
|
23
|
+
// • text→image → POST /api/v1/primitives/images/generate → poll image_generate
|
|
24
|
+
// • history → GET /job-runs/history?template_id=primitive:image_inpaint&tracer=…
|
|
25
|
+
//
|
|
26
|
+
// The single provider mask is white with alpha holes where the user painted
|
|
27
|
+
// (destination-out); the colors are UI labels only, surfaced textually per
|
|
28
|
+
// region inside the merged instruction. Each completed output is also mirrored
|
|
29
|
+
// into /temp/inpaints/<tracer>/ so a session's outputs are browsable as files.
|
|
19
30
|
import { reskinDocument } from "./document.js";
|
|
20
31
|
// Functional SVG glyphs (no decorative emoji).
|
|
21
32
|
const UPLOAD_ICON = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>`;
|
|
22
|
-
const UNDO_ICON = `<svg width="
|
|
23
|
-
const
|
|
33
|
+
const UNDO_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7.2 7H3.8V3.6"/><path d="M4 7a7 7 0 1 1 1.9 6.6"/></svg>`;
|
|
34
|
+
const REDO_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12.8 7h3.4V3.6"/><path d="M16 7a7 7 0 1 0-1.9 6.6"/></svg>`;
|
|
35
|
+
const CLEAR_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10M15 5 5 15"/></svg>`;
|
|
36
|
+
const GEAR_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M8.8 3.4 9.4 2h1.2l.6 1.4 1.2.5 1.4-.6.9.9-.6 1.4.5 1.2 1.4.6v1.2l-1.4.6-.5 1.2.6 1.4-.9.9-1.4-.6-1.2.5-.6 1.4H9.4l-.6-1.4-1.2-.5-1.4.6-.9-.9.6-1.4-.5-1.2L4 8.6V7.4l1.4-.6.5-1.2-.6-1.4.9-.9 1.4.6 1.2-.5Z"/><circle cx="10" cy="8" r="2.4"/></svg>`;
|
|
37
|
+
const REFRESH_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16.5 10a6.5 6.5 0 1 1-1.9-4.6"/><path d="M16.5 4.2v4.2h-4.2"/></svg>`;
|
|
24
38
|
const SPARK_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l1.9 5.1 5.1 1.9-5.1 1.9L12 16.5l-1.9-5.1L5 9.5l5.1-1.9z"/></svg>`;
|
|
25
39
|
const DL_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3v9"/><path d="m6.5 8.5 3.5 3.5 3.5-3.5"/><path d="M4 16h12"/></svg>`;
|
|
40
|
+
const CLIP_ICON = `<svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m8.1 10.7 4.8-4.8a2.5 2.5 0 0 1 3.5 3.5l-6.1 6.1a4 4 0 0 1-5.7-5.7l6.3-6.3"/><path d="m10 8.8-4.1 4.1a1.6 1.6 0 0 0 2.3 2.3l5.6-5.6"/></svg>`;
|
|
41
|
+
const SEND_ICON = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg>`;
|
|
42
|
+
const COPY_ICON = `<svg width="13" height="13" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1"/><path d="M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2"/></svg>`;
|
|
43
|
+
const CLOSE_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10"/><path d="M15 5 5 15"/></svg>`;
|
|
44
|
+
const HISTORY_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3.2 10a6.8 6.8 0 1 1 2 4.8"/><path d="M3.2 14.4v-3.6h3.6"/><path d="M10 6.2V10l2.6 1.6"/></svg>`;
|
|
45
|
+
const PLUS_ICON = `<svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4v12"/><path d="M4 10h12"/></svg>`;
|
|
46
|
+
// Image / Video / Clipper mode switch — the creative tool is three twins under
|
|
47
|
+
// /tools (masked image edits at /tools/image, start/end-frame + time-scoped
|
|
48
|
+
// video edits at /tools/video, subrange video clipping at /tools/clipper).
|
|
49
|
+
// Shared here so all three pages render an identical segmented tab.
|
|
50
|
+
const TAB_IMAGE_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9" r="1.5"/><path d="m4 17 4.5-4.5 3 3L16 11l4 4"/></svg>`;
|
|
51
|
+
const TAB_VIDEO_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="14" height="14" rx="2"/><path d="m17 9 4-2v10l-4-2"/></svg>`;
|
|
52
|
+
const TAB_CLIPPER_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M8.1 8.1 20 18M8.1 15.9 20 6"/></svg>`;
|
|
53
|
+
export function renderInpaintModeTabs(active) {
|
|
54
|
+
const tab = (mode, href, icon, label) => `<a class="rk-ip-tab${active === mode ? " is-active" : ""}" href="${href}" role="tab" aria-selected="${active === mode ? "true" : "false"}">${icon}<span>${label}</span></a>`;
|
|
55
|
+
return `<div class="rk-ip-tabs" role="tablist" aria-label="Editor mode">${tab("image", "/tools/image", TAB_IMAGE_ICON, "Image")}${tab("video", "/tools/video", TAB_VIDEO_ICON, "Video")}${tab("clipper", "/tools/clipper", TAB_CLIPPER_ICON, "Clipper")}</div>`;
|
|
56
|
+
}
|
|
57
|
+
export const INPAINT_TABS_CSS = `
|
|
58
|
+
.rk-ip-tabs{display:inline-flex;gap:3px;padding:3px;background:var(--rk-n-100);border:1px solid var(--rk-border);border-radius:999px;margin-bottom:12px}
|
|
59
|
+
.rk-ip-tab{display:inline-flex;align-items:center;gap:6px;padding:6px 15px;border-radius:999px;font-size:13px;font-weight:700;color:var(--rk-text-muted);text-decoration:none;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
60
|
+
.rk-ip-tab:hover{color:var(--rk-ink)}
|
|
61
|
+
.rk-ip-tab.is-active{background:var(--rk-surface);color:var(--rk-ink);box-shadow:var(--rk-shadow-sm)}
|
|
62
|
+
.rk-ip-tab svg{flex:none}
|
|
63
|
+
`;
|
|
26
64
|
export function renderReskinInpaint(input = {}) {
|
|
27
65
|
const boot = { accountId: input.accountId || "", isLoggedIn: Boolean(input.isLoggedIn) };
|
|
28
66
|
const body = `
|
|
29
67
|
<main class="rk-container-wide rk-page rk-inpaint-page">
|
|
30
|
-
<div class="rk-page-head">
|
|
31
|
-
<
|
|
32
|
-
|
|
68
|
+
<div class="rk-page-head rk-inpaint-head">
|
|
69
|
+
<div class="rk-inpaint-head-text">
|
|
70
|
+
${renderInpaintModeTabs("image")}
|
|
71
|
+
<h1 class="rk-h1">Create Image</h1>
|
|
72
|
+
<p class="rk-sub">Upload an image (or generate one), paint mask regions in different colors, give each color its own instruction, add an optional whole-image prompt, and generate — everything outside your mask stays untouched.</p>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="rk-inpaint-toolbar">
|
|
75
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-inpaint-startnew" data-custom-inpaint-start-new>${PLUS_ICON} Start New</button>
|
|
76
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-settings-button aria-label="Settings" title="Settings">${GEAR_ICON}</button>
|
|
77
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-history-toggle aria-label="Past sessions" title="Past sessions">${HISTORY_ICON}</button>
|
|
78
|
+
</div>
|
|
33
79
|
</div>
|
|
34
80
|
|
|
35
81
|
<div class="rk-inpaint-workbench">
|
|
36
|
-
<!-- LEFT:
|
|
82
|
+
<!-- LEFT: source + prompts + brush/color controls -->
|
|
37
83
|
<section class="rk-inpaint-panel" aria-label="Controls">
|
|
38
84
|
<div class="rk-inpaint-block">
|
|
39
|
-
<label class="rk-label"
|
|
40
|
-
<
|
|
85
|
+
<label class="rk-label">Source image</label>
|
|
86
|
+
<button type="button" class="rk-inpaint-drop" data-custom-inpaint-upload>
|
|
41
87
|
<span class="rk-inpaint-drop-ic" aria-hidden="true">${UPLOAD_ICON}</span>
|
|
42
|
-
<span class="rk-inpaint-drop-main"><strong>
|
|
43
|
-
<span class="rk-inpaint-drop-name"
|
|
44
|
-
|
|
45
|
-
|
|
88
|
+
<span class="rk-inpaint-drop-main"><strong>Add source image</strong> or drop it here</span>
|
|
89
|
+
<span class="rk-inpaint-drop-name" data-custom-inpaint-source-name>No image selected</span>
|
|
90
|
+
</button>
|
|
91
|
+
<input data-custom-inpaint-file type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" hidden />
|
|
46
92
|
</div>
|
|
47
93
|
|
|
48
94
|
<div class="rk-inpaint-block">
|
|
49
|
-
<label class="rk-label"
|
|
50
|
-
<
|
|
51
|
-
|
|
95
|
+
<label class="rk-label">Whole image prompt</label>
|
|
96
|
+
<div class="rk-ip-promptfield">
|
|
97
|
+
<textarea class="rk-textarea rk-ip-global" data-editor-inpaint-global-prompt placeholder="Describe the overall edit, style, constraints, or preservation notes (optional)."></textarea>
|
|
98
|
+
<button type="button" class="rk-ip-attach" data-editor-inpaint-attach-scope="global" aria-label="Attach reference image" title="Attach reference image">${CLIP_ICON}</button>
|
|
99
|
+
</div>
|
|
100
|
+
<span class="editor-inpaint-attachment-chips rk-ip-attach-chips" data-editor-inpaint-attachments="global"></span>
|
|
101
|
+
<input data-editor-inpaint-attachment-input type="file" accept="image/jpeg,image/png,image/webp" multiple hidden />
|
|
102
|
+
<span class="rk-hint">Guidance for the masked edit and overall consistency. It never permits changing unmasked pixels.</span>
|
|
52
103
|
</div>
|
|
53
104
|
|
|
54
105
|
<div class="rk-inpaint-block">
|
|
55
106
|
<div class="rk-inpaint-brushrow">
|
|
56
|
-
<label class="rk-label"
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" id="rk-ip-clear">${CLEAR_ICON} Clear mask</button>
|
|
107
|
+
<label class="rk-label">Brush <span data-editor-inpaint-brush-label>70</span> px</label>
|
|
108
|
+
<div class="rk-ip-toolbtns">
|
|
109
|
+
<button type="button" class="rk-ip-iconbtn" data-editor-inpaint-undo aria-label="Undo stroke" title="Undo">${UNDO_ICON}</button>
|
|
110
|
+
<button type="button" class="rk-ip-iconbtn" data-editor-inpaint-redo aria-label="Redo stroke" title="Redo">${REDO_ICON}</button>
|
|
111
|
+
<button type="button" class="rk-ip-iconbtn" data-editor-inpaint-clear aria-label="Clear mask" title="Clear mask">${CLEAR_ICON}</button>
|
|
112
|
+
</div>
|
|
63
113
|
</div>
|
|
114
|
+
<input data-editor-inpaint-brush class="rk-inpaint-range" type="range" min="6" max="180" step="1" value="70" />
|
|
64
115
|
</div>
|
|
65
116
|
|
|
66
|
-
<div class="rk-inpaint-block
|
|
67
|
-
<
|
|
68
|
-
|
|
117
|
+
<div class="rk-inpaint-block">
|
|
118
|
+
<div class="rk-inpaint-brushrow">
|
|
119
|
+
<label class="rk-label">Mask colors</label>
|
|
120
|
+
<input class="rk-ip-colorpicker" data-editor-inpaint-color-picker type="color" value="#ff2d55" aria-label="Mask color" />
|
|
121
|
+
</div>
|
|
122
|
+
<span class="rk-hint">Paint regions in different colors, then give each color its own replacement instruction below.</span>
|
|
123
|
+
<div class="editor-inpaint-swatches" data-editor-inpaint-swatches></div>
|
|
124
|
+
<div class="editor-inpaint-prompts" data-editor-inpaint-prompts></div>
|
|
125
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-ip-addcolor" data-editor-inpaint-add-color>+ Add color</button>
|
|
69
126
|
</div>
|
|
70
127
|
|
|
71
|
-
<div class="rk-inpaint-
|
|
72
|
-
|
|
73
|
-
<
|
|
128
|
+
<div class="rk-inpaint-block">
|
|
129
|
+
<button type="button" class="rk-btn rk-btn-gold rk-inpaint-generate" data-editor-inpaint-submit>${SPARK_ICON} <span data-editor-inpaint-submit-label>Generate edit</span></button>
|
|
130
|
+
<p class="custom-inpaint-status rk-inpaint-status" data-custom-inpaint-status data-tone="" aria-live="polite"></p>
|
|
74
131
|
</div>
|
|
75
|
-
|
|
76
|
-
<button type="button" class="rk-btn rk-btn-gold rk-inpaint-generate" id="rk-ip-generate" disabled>${SPARK_ICON} Generate edit</button>
|
|
77
|
-
<p class="rk-inpaint-status" id="rk-ip-status" data-tone="" aria-live="polite"></p>
|
|
78
132
|
</section>
|
|
79
133
|
|
|
80
|
-
<!-- CENTER: canvas stage -->
|
|
134
|
+
<!-- CENTER: canvas stage (with text-to-image empty state) -->
|
|
81
135
|
<section class="rk-inpaint-stage" aria-label="Canvas">
|
|
82
|
-
<div class="rk-inpaint-empty"
|
|
136
|
+
<div class="rk-inpaint-empty" data-custom-inpaint-empty>
|
|
83
137
|
<span class="rk-inpaint-empty-ic" aria-hidden="true">${UPLOAD_ICON}</span>
|
|
84
138
|
<p class="rk-inpaint-empty-title">Upload an image to start</p>
|
|
85
|
-
<p class="rk-inpaint-empty-sub">Then
|
|
139
|
+
<p class="rk-inpaint-empty-sub">Then paint mask regions in one or more colors.</p>
|
|
140
|
+
|
|
141
|
+
<!-- Or generate an image from scratch (text-to-image) -->
|
|
142
|
+
<div class="rk-ip-gen" id="rk-ip-gen">
|
|
143
|
+
<span class="rk-ip-gen-or">or</span>
|
|
144
|
+
<div class="rk-ip-gen-box">
|
|
145
|
+
<textarea id="rk-ip-genprompt" class="rk-ip-gen-input" rows="2" placeholder="Or type your image prompt"></textarea>
|
|
146
|
+
<div class="rk-ip-gen-refs" id="rk-ip-genrefs" hidden></div>
|
|
147
|
+
<div class="rk-ip-gen-bar">
|
|
148
|
+
<button type="button" class="rk-ip-gen-clip" id="rk-ip-genclip" aria-label="Attach reference images from your files" title="Attach reference images from your files">${CLIP_ICON}</button>
|
|
149
|
+
<span class="rk-ip-gen-hint">Generate a brand-new image, then paint to refine it.</span>
|
|
150
|
+
<button type="button" class="rk-ip-gen-send" id="rk-ip-gensend" aria-label="Generate image">${SEND_ICON}</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<p class="rk-ip-gen-status" id="rk-ip-genstatus" data-tone="" aria-live="polite"></p>
|
|
154
|
+
</div>
|
|
86
155
|
</div>
|
|
87
|
-
<div class="rk-inpaint-canvaswrap
|
|
88
|
-
<img
|
|
89
|
-
<canvas
|
|
90
|
-
<
|
|
156
|
+
<div class="rk-inpaint-canvaswrap editor-inpaint-canvas-wrap" data-custom-inpaint-canvas-wrap hidden>
|
|
157
|
+
<img data-editor-inpaint-image class="rk-inpaint-img" alt="" />
|
|
158
|
+
<canvas data-editor-inpaint-canvas class="rk-inpaint-canvas"></canvas>
|
|
159
|
+
<div class="editor-inpaint-brush-cursor" data-editor-inpaint-brush-cursor aria-hidden="true"></div>
|
|
91
160
|
</div>
|
|
92
161
|
</section>
|
|
93
162
|
|
|
94
|
-
<!-- RIGHT: result + history -->
|
|
95
|
-
<section class="rk-inpaint-output" aria-label="Result">
|
|
96
|
-
<div class="
|
|
163
|
+
<!-- RIGHT: result + tracer-grouped history -->
|
|
164
|
+
<section class="rk-inpaint-output" aria-label="Result and history">
|
|
165
|
+
<div class="custom-inpaint-output rk-inpaint-result" data-custom-inpaint-output hidden>
|
|
97
166
|
<div class="rk-inpaint-result-head">
|
|
98
167
|
<h2 class="rk-inpaint-h2">Result</h2>
|
|
99
|
-
<
|
|
168
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-custom-inpaint-save>Save to Files</button>
|
|
100
169
|
</div>
|
|
101
|
-
<
|
|
102
|
-
<img
|
|
103
|
-
</
|
|
170
|
+
<button type="button" class="rk-inpaint-result-imgwrap custom-inpaint-preview" data-custom-inpaint-result-open aria-label="View image edit result">
|
|
171
|
+
<img class="rk-inpaint-result-img" data-custom-inpaint-result alt="Image edit result" />
|
|
172
|
+
</button>
|
|
104
173
|
</div>
|
|
174
|
+
|
|
105
175
|
<div class="rk-inpaint-history">
|
|
106
|
-
<
|
|
107
|
-
|
|
176
|
+
<div class="rk-inpaint-history-head">
|
|
177
|
+
<h2 class="rk-inpaint-h2">This session</h2>
|
|
178
|
+
<div class="rk-ip-histtools">
|
|
179
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-history-refresh aria-label="Refresh history" title="Refresh history">${REFRESH_ICON}</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="custom-inpaint-history-list rk-inpaint-historylist" data-custom-inpaint-history-list>
|
|
183
|
+
<div class="custom-inpaint-history-empty">Loading history…</div>
|
|
184
|
+
</div>
|
|
108
185
|
</div>
|
|
109
186
|
</section>
|
|
110
187
|
</div>
|
|
111
188
|
|
|
189
|
+
<!-- settings modal (tracer + AI model) -->
|
|
190
|
+
<div class="rk-ip-settingsmodal" data-custom-inpaint-settings hidden>
|
|
191
|
+
<div class="rk-ip-settingsmodal-panel" role="dialog" aria-modal="true" aria-label="Image edit settings">
|
|
192
|
+
<div class="rk-ip-settingsmodal-head">
|
|
193
|
+
<span class="rk-ip-settingsmodal-title">Settings</span>
|
|
194
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-settings-close aria-label="Close settings" title="Close">${CLOSE_ICON}</button>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="rk-ip-settingsmodal-body">
|
|
197
|
+
<label class="rk-ip-setting editor-inpaint-provider rk-ip-provider" data-editor-inpaint-provider-wrap>
|
|
198
|
+
<span class="rk-label">AI model</span>
|
|
199
|
+
<select data-editor-inpaint-provider class="rk-select rk-inpaint-select"></select>
|
|
200
|
+
</label>
|
|
201
|
+
<label class="rk-ip-setting">
|
|
202
|
+
<span class="rk-label">Session name <span class="rk-ip-setting-note">(tracer — groups this session's History)</span></span>
|
|
203
|
+
<input data-custom-inpaint-tracer class="rk-ip-input" type="text" autocomplete="off" spellcheck="false" placeholder="Auto-named from your first prompt" />
|
|
204
|
+
</label>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<!-- past-sessions (tracers) left slide-in panel -->
|
|
210
|
+
<aside class="rk-ip-sessions" data-custom-inpaint-sessions aria-label="Past sessions">
|
|
211
|
+
<div class="rk-ip-sessions-head">
|
|
212
|
+
<span class="rk-ip-sessions-title">Past sessions</span>
|
|
213
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-sessions-close aria-label="Close past sessions" title="Close">${CLOSE_ICON}</button>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="rk-ip-sessions-list" data-custom-inpaint-sessions-list>
|
|
216
|
+
<div class="rk-ip-sessions-empty">Loading…</div>
|
|
217
|
+
</div>
|
|
218
|
+
</aside>
|
|
219
|
+
<div class="rk-ip-sessions-backdrop" data-custom-inpaint-sessions-backdrop hidden></div>
|
|
220
|
+
|
|
221
|
+
<!-- image preview lightbox (result + history) -->
|
|
222
|
+
<div class="custom-inpaint-lightbox rk-ip-lightbox" data-custom-inpaint-lightbox data-open="false" hidden>
|
|
223
|
+
<div class="custom-inpaint-lightbox-dialog" role="dialog" aria-modal="true" aria-labelledby="custom-inpaint-lightbox-title">
|
|
224
|
+
<div class="custom-inpaint-lightbox-head">
|
|
225
|
+
<p class="custom-inpaint-lightbox-title" id="custom-inpaint-lightbox-title" data-custom-inpaint-lightbox-title>Image edit output</p>
|
|
226
|
+
<div class="custom-inpaint-lightbox-actions">
|
|
227
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-lightbox-copy aria-label="Copy image URL" title="Copy URL">${COPY_ICON}</button>
|
|
228
|
+
<a class="rk-btn rk-btn-ink rk-btn-sm" data-custom-inpaint-lightbox-download href="#" target="_blank" rel="noopener noreferrer" download>${DL_ICON} Download</a>
|
|
229
|
+
<button type="button" class="rk-ip-iconbtn" data-custom-inpaint-lightbox-close aria-label="Close preview" title="Close">${CLOSE_ICON}</button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="custom-inpaint-lightbox-stage">
|
|
233
|
+
<img data-custom-inpaint-lightbox-image alt="Image edit output preview" />
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
112
238
|
<script type="application/json" data-rk-inpaint-boot>${JSON.stringify(boot).replace(/</g, "\\u003c")}</script>
|
|
113
239
|
</main>`;
|
|
114
240
|
const pageCss = `
|
|
241
|
+
${INPAINT_TABS_CSS}
|
|
115
242
|
.rk-inpaint-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter)}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.rk-inpaint-workbench{display:grid;grid-template-columns:280px minmax(0,1fr) 300px;gap:20px;align-items:start}
|
|
120
|
-
@media(max-width:1200px){.rk-inpaint-workbench{grid-template-columns:280px minmax(0,1fr)}.rk-inpaint-output{grid-column:1 / -1;grid-row:2}}
|
|
121
|
-
@media(max-width:820px){.rk-inpaint-workbench{grid-template-columns:1fr}.rk-inpaint-output,.rk-inpaint-stage,.rk-inpaint-panel{grid-column:auto}
|
|
122
|
-
/* stack canvas ABOVE the controls so you can paint without scrolling past Generate */
|
|
123
|
-
.rk-inpaint-stage{order:-1}}
|
|
124
|
-
@media(max-height:520px){.rk-inpaint-stage{min-height:0}}
|
|
243
|
+
.rk-inpaint-workbench{display:grid;grid-template-columns:320px minmax(0,1fr) 320px;gap:20px;align-items:start}
|
|
244
|
+
@media(max-width:1200px){.rk-inpaint-workbench{grid-template-columns:320px minmax(0,1fr)}.rk-inpaint-output{grid-column:1 / -1;grid-row:2}}
|
|
245
|
+
@media(max-width:820px){.rk-inpaint-workbench{grid-template-columns:1fr}.rk-inpaint-output,.rk-inpaint-stage,.rk-inpaint-panel{grid-column:auto}.rk-inpaint-stage{order:-1}}
|
|
125
246
|
|
|
126
247
|
/* panels (no side borders — uniform hairline) */
|
|
127
248
|
.rk-inpaint-panel,.rk-inpaint-stage,.rk-inpaint-output{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card)}
|
|
128
|
-
.rk-inpaint-panel{padding:
|
|
249
|
+
.rk-inpaint-panel{padding:18px;display:grid;gap:16px;align-content:start}
|
|
129
250
|
.rk-inpaint-block{display:grid;gap:9px}
|
|
130
251
|
.rk-inpaint-h2{font-family:var(--rk-font-display);font-size:var(--rk-text-lg);font-weight:800;color:var(--rk-ink);letter-spacing:-.01em}
|
|
131
252
|
|
|
132
|
-
/* upload dropzone */
|
|
133
|
-
.rk-inpaint-drop{display:grid;justify-items:center;gap:6px;text-align:center;padding:
|
|
134
|
-
border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-2xl);background:var(--rk-n-50);color:var(--rk-text-muted);
|
|
253
|
+
/* upload dropzone (button) */
|
|
254
|
+
.rk-inpaint-drop{display:grid;justify-items:center;gap:6px;text-align:center;padding:20px 16px;cursor:pointer;width:100%;
|
|
255
|
+
border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-2xl);background:var(--rk-n-50);color:var(--rk-text-muted);font:inherit;
|
|
135
256
|
transition:border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
136
257
|
.rk-inpaint-drop:hover,.rk-inpaint-drop.is-drag{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
|
|
137
258
|
.rk-inpaint-drop-ic{color:var(--rk-gold-700);display:grid;place-items:center}
|
|
138
259
|
.rk-inpaint-drop-main{font-size:13px}
|
|
139
260
|
.rk-inpaint-drop-main strong{color:var(--rk-ink);font-weight:700}
|
|
140
261
|
.rk-inpaint-drop-name{font-size:12px;color:var(--rk-text-faint);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
141
|
-
.rk-inpaint-file{display:none}
|
|
142
|
-
.rk-inpaint-prompt{min-height:88px}
|
|
143
262
|
|
|
144
|
-
/*
|
|
263
|
+
/* whole-image + per-color prompt fields */
|
|
264
|
+
.rk-ip-promptfield{position:relative}
|
|
265
|
+
.rk-ip-global{min-height:74px;width:100%;padding-right:44px}
|
|
266
|
+
.editor-inpaint-prompt{width:100%;min-height:60px;resize:vertical;padding:9px 40px 9px 11px;font:inherit;font-size:13px;line-height:1.45;color:var(--rk-ink);
|
|
267
|
+
border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-xl);background:#fff;outline:0;transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
268
|
+
.editor-inpaint-prompt:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
269
|
+
.rk-ip-attach,.editor-inpaint-attach-button{position:absolute;right:7px;bottom:7px;display:grid;place-items:center;width:30px;height:30px;border-radius:50%;cursor:pointer;
|
|
270
|
+
border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600);transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
271
|
+
.rk-ip-attach:hover,.editor-inpaint-attach-button:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
272
|
+
.editor-inpaint-prompt-stack .editor-inpaint-prompt-field{position:relative}
|
|
273
|
+
.rk-ip-attach-chips,.editor-inpaint-attachment-chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
274
|
+
.editor-inpaint-attachment-tools{margin-top:6px}
|
|
275
|
+
.editor-inpaint-attachment-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-50);font-size:11.5px;color:var(--rk-n-700);max-width:170px}
|
|
276
|
+
.editor-inpaint-attachment-chip img{width:22px;height:22px;border-radius:4px;object-fit:cover;flex:none}
|
|
277
|
+
.editor-inpaint-attachment-chip span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
278
|
+
.editor-inpaint-attachment-chip button{flex:none;border:0;background:transparent;cursor:pointer;color:var(--rk-text-faint);font-size:15px;line-height:1;padding:0 2px}
|
|
279
|
+
.editor-inpaint-attachment-chip button:hover{color:var(--rk-ink)}
|
|
280
|
+
|
|
281
|
+
/* brush / tools row */
|
|
145
282
|
.rk-inpaint-brushrow{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
146
|
-
.rk-
|
|
283
|
+
.rk-ip-toolbtns{display:flex;gap:6px}
|
|
284
|
+
.rk-ip-iconbtn{display:grid;place-items:center;width:32px;height:32px;border-radius:var(--rk-r-lg);border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600);cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
285
|
+
.rk-ip-iconbtn:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
147
286
|
.rk-inpaint-range{width:100%;accent-color:var(--rk-gold-700);cursor:pointer}
|
|
148
|
-
.rk-
|
|
149
|
-
.rk-
|
|
150
|
-
.rk-
|
|
151
|
-
.rk-
|
|
152
|
-
.rk-
|
|
153
|
-
.rk-
|
|
287
|
+
.rk-ip-settings{margin-top:2px;padding:10px;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);background:var(--rk-n-50);display:grid;gap:12px}
|
|
288
|
+
.rk-ip-settings[hidden]{display:none}
|
|
289
|
+
.rk-ip-setting{display:grid;gap:6px}
|
|
290
|
+
.rk-ip-setting-note{font-weight:600;color:var(--rk-text-faint)}
|
|
291
|
+
.rk-ip-input{width:100%;padding:8px 10px;font:inherit;font-size:13px;color:var(--rk-ink);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;outline:0}
|
|
292
|
+
.rk-ip-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
293
|
+
|
|
294
|
+
/* mask colors */
|
|
295
|
+
.rk-ip-colorpicker{width:42px;height:32px;padding:2px;border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;cursor:pointer}
|
|
296
|
+
.editor-inpaint-swatches{display:none}
|
|
297
|
+
.editor-inpaint-prompts{display:grid;align-content:start;gap:10px}
|
|
298
|
+
.editor-inpaint-color-row{position:relative;display:flex;align-items:flex-start;gap:10px;padding:6px;border-radius:var(--rk-r-xl);border:1px solid transparent}
|
|
299
|
+
.editor-inpaint-color-row[data-active="true"]{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
|
|
300
|
+
.editor-inpaint-color-tools{flex:0 0 auto;display:grid;justify-items:center;gap:6px}
|
|
301
|
+
.editor-inpaint-color-dot{width:26px;height:26px;padding:0;border:2px solid #fff;border-radius:50%;cursor:pointer;background:var(--inpaint-color,#ff2d55);box-shadow:0 0 0 1px var(--rk-border-strong)}
|
|
302
|
+
.editor-inpaint-color-row[data-active="true"] .editor-inpaint-color-dot{box-shadow:0 0 0 2px var(--rk-gold-700)}
|
|
303
|
+
.editor-inpaint-color-remove{display:grid;place-items:center;width:22px;height:22px;padding:0;border:1px solid var(--rk-border);border-radius:50%;background:#fff;color:var(--rk-text-muted);cursor:pointer}
|
|
304
|
+
.editor-inpaint-color-remove:hover{color:#b42318;border-color:#e6a8a0}
|
|
305
|
+
.editor-inpaint-color-remove svg{width:12px;height:12px}
|
|
306
|
+
.editor-inpaint-prompt-stack{flex:1;min-width:0;display:grid;gap:6px}
|
|
307
|
+
.rk-ip-addcolor{justify-self:start}
|
|
308
|
+
|
|
309
|
+
/* provider */
|
|
310
|
+
.editor-inpaint-provider{display:none;gap:6px}
|
|
311
|
+
.editor-inpaint-provider[data-visible="true"]{display:grid}
|
|
312
|
+
.rk-inpaint-select{width:100%}
|
|
313
|
+
|
|
314
|
+
/* generate + status */
|
|
154
315
|
.rk-inpaint-generate{width:100%;justify-content:center;gap:8px;padding:13px}
|
|
155
|
-
.rk-inpaint-generate
|
|
316
|
+
.rk-inpaint-generate[data-loading="true"]{opacity:.7;cursor:wait}
|
|
156
317
|
.rk-inpaint-status{font-size:12.5px;font-weight:600;line-height:1.5;min-height:18px;color:var(--rk-text-muted)}
|
|
157
318
|
.rk-inpaint-status[data-tone="error"]{color:#b42318}
|
|
158
319
|
.rk-inpaint-status[data-tone="success"]{color:#1f7a44}
|
|
159
320
|
|
|
160
|
-
/*
|
|
321
|
+
/* locked session — inputs frozen after an edit is generated (Start New to edit again) */
|
|
322
|
+
.rk-inpaint-workbench.is-locked .rk-inpaint-panel{position:relative}
|
|
323
|
+
.rk-inpaint-workbench.is-locked .rk-inpaint-block:not(:last-child){opacity:.55}
|
|
324
|
+
.rk-inpaint-workbench.is-locked .editor-inpaint-prompt,
|
|
325
|
+
.rk-inpaint-workbench.is-locked .rk-ip-global{cursor:default;background:var(--rk-n-50)}
|
|
326
|
+
.rk-inpaint-workbench.is-locked .rk-inpaint-canvas{cursor:not-allowed}
|
|
327
|
+
.rk-inpaint-workbench.is-locked .rk-inpaint-generate{opacity:1;cursor:default;background:var(--rk-n-50);color:var(--rk-text-muted);border-color:var(--rk-border);box-shadow:none;font-size:12.5px}
|
|
328
|
+
.rk-inpaint-workbench.is-locked .rk-inpaint-generate svg{display:none}
|
|
329
|
+
|
|
330
|
+
/* center stage + canvas */
|
|
161
331
|
.rk-inpaint-stage{min-height:420px;display:grid;place-items:center;padding:20px;overflow:hidden}
|
|
162
|
-
.rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:
|
|
332
|
+
.rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:34px 20px}
|
|
163
333
|
.rk-inpaint-empty[hidden]{display:none}
|
|
164
334
|
.rk-inpaint-empty-ic{color:var(--rk-n-400)}
|
|
165
335
|
.rk-inpaint-empty-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);color:var(--rk-ink)}
|
|
166
336
|
.rk-inpaint-empty-sub{font-size:13px}
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
.
|
|
170
|
-
.
|
|
171
|
-
.
|
|
172
|
-
|
|
173
|
-
|
|
337
|
+
.editor-inpaint-canvas-wrap{position:relative;display:inline-block;line-height:0;max-width:100%;border-radius:var(--rk-r-2xl);overflow:hidden;box-shadow:var(--rk-shadow-md);
|
|
338
|
+
--inpaint-brush-cursor-size:70px;--inpaint-brush-cursor-x:0px;--inpaint-brush-cursor-y:0px}
|
|
339
|
+
.editor-inpaint-canvas-wrap[hidden]{display:none}
|
|
340
|
+
.editor-inpaint-canvas-wrap img{display:block;max-width:100%;max-height:min(64vh,620px);width:auto;height:auto;object-fit:contain;user-select:none}
|
|
341
|
+
.editor-inpaint-canvas-wrap canvas{position:absolute;inset:0;width:100%;height:100%;opacity:.86;cursor:none;touch-action:none}
|
|
342
|
+
.editor-inpaint-brush-cursor{position:absolute;z-index:2;left:0;top:0;width:var(--inpaint-brush-cursor-size);height:var(--inpaint-brush-cursor-size);
|
|
343
|
+
border:2px solid rgba(255,255,255,.95);border-radius:50%;box-shadow:0 0 0 1px rgba(28,25,23,.7),0 0 18px rgba(28,25,23,.25);opacity:0;pointer-events:none;
|
|
344
|
+
transform:translate(var(--inpaint-brush-cursor-x),var(--inpaint-brush-cursor-y)) translate(-50%,-50%)}
|
|
345
|
+
.editor-inpaint-brush-cursor[data-visible="true"]{opacity:1}
|
|
346
|
+
|
|
347
|
+
/* text-to-image composer (empty state) */
|
|
348
|
+
.rk-ip-gen{width:100%;max-width:460px;margin-top:16px;display:grid;gap:10px;justify-items:stretch}
|
|
349
|
+
.rk-ip-gen-or{position:relative;font-size:11.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--rk-text-faint)}
|
|
350
|
+
.rk-ip-gen-or::before,.rk-ip-gen-or::after{content:"";position:absolute;top:50%;width:calc(50% - 26px);height:1px;background:var(--rk-border)}
|
|
351
|
+
.rk-ip-gen-or::before{left:0}.rk-ip-gen-or::after{right:0}
|
|
352
|
+
.rk-ip-gen-box{background:#fff;border:1.5px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);padding:10px 10px 8px;display:grid;gap:8px;
|
|
353
|
+
box-shadow:var(--rk-shadow-sm);transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease);text-align:left}
|
|
354
|
+
.rk-ip-gen-box:focus-within{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
|
|
355
|
+
.rk-ip-gen.is-busy .rk-ip-gen-box{opacity:.75}
|
|
356
|
+
.rk-ip-gen-input{width:100%;border:0;outline:0;resize:none;background:transparent;font:inherit;font-size:14px;line-height:1.45;color:var(--rk-ink);min-height:44px;padding:2px 4px}
|
|
357
|
+
.rk-ip-gen-input::placeholder{color:var(--rk-text-faint)}
|
|
358
|
+
.rk-ip-gen-input:disabled{cursor:not-allowed;color:var(--rk-text-muted)}
|
|
359
|
+
.rk-ip-gen-clip:disabled{opacity:.5;cursor:not-allowed}
|
|
360
|
+
.rk-ip-gen-refs{display:flex;flex-wrap:wrap;gap:6px}
|
|
361
|
+
.rk-ip-gen-refs[hidden]{display:none}
|
|
362
|
+
.rk-ip-gen-ref{display:inline-flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-50);font-size:11.5px;color:var(--rk-n-700);max-width:180px}
|
|
363
|
+
.rk-ip-gen-ref img{width:22px;height:22px;border-radius:4px;object-fit:cover;flex:none}
|
|
364
|
+
.rk-ip-gen-ref span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
365
|
+
.rk-ip-gen-ref button{flex:none;border:0;background:transparent;cursor:pointer;color:var(--rk-text-faint);font-size:15px;line-height:1;padding:0 2px}
|
|
366
|
+
.rk-ip-gen-ref button:hover{color:var(--rk-ink)}
|
|
367
|
+
.rk-ip-gen-bar{display:flex;align-items:center;gap:8px}
|
|
368
|
+
.rk-ip-gen-hint{flex:1;font-size:11.5px;color:var(--rk-text-faint);line-height:1.35}
|
|
369
|
+
.rk-ip-gen-clip,.rk-ip-gen-send{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),opacity var(--rk-dur) var(--rk-ease)}
|
|
370
|
+
.rk-ip-gen-clip{border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600)}
|
|
371
|
+
.rk-ip-gen-clip:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
372
|
+
.rk-ip-gen-send{border:0;background:var(--rk-gold-500,var(--rk-gold-600));color:var(--rk-ink)}
|
|
373
|
+
.rk-ip-gen-send:hover:not(:disabled){background:var(--rk-gold-600)}
|
|
374
|
+
.rk-ip-gen-send:disabled{opacity:.5;cursor:default}
|
|
375
|
+
.rk-ip-gen-send.is-busy{background:var(--rk-gold-tint);pointer-events:none}
|
|
376
|
+
.rk-ip-gen-spin,.rk-ip-gen-send .rk-ip-gen-spin{width:16px;height:16px;border-radius:50%;border:2.5px solid var(--rk-gold-tint2,rgba(252,185,0,.35));border-top-color:var(--rk-gold-700);animation:rk-ip-spin .8s linear infinite}
|
|
377
|
+
.rk-ip-gen-status{font-size:12px;font-weight:600;min-height:16px;color:var(--rk-text-muted);text-align:left;padding-left:2px}
|
|
378
|
+
.rk-ip-gen-status[data-tone="error"]{color:#b42318}
|
|
379
|
+
.rk-ip-gen-status[data-tone="success"]{color:#1f7a44}
|
|
380
|
+
|
|
381
|
+
/* file-directory pop panel (reference picker) */
|
|
382
|
+
.rk-ip-filemodal{position:fixed;inset:0;z-index:60;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px;animation:rk-ip-fade .16s var(--rk-ease)}
|
|
383
|
+
@keyframes rk-ip-fade{from{opacity:0}to{opacity:1}}
|
|
384
|
+
.rk-ip-filemodal-panel{width:min(560px,100%);max-height:min(80vh,680px);display:flex;flex-direction:column;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card));overflow:hidden}
|
|
385
|
+
.rk-ip-filemodal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;border-bottom:1px solid var(--rk-border)}
|
|
386
|
+
.rk-ip-filemodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
|
|
387
|
+
.rk-ip-filemodal-close{border:0;background:transparent;font-size:22px;line-height:1;color:var(--rk-text-muted);cursor:pointer;padding:0 4px}
|
|
388
|
+
.rk-ip-filemodal-close:hover{color:var(--rk-ink)}
|
|
389
|
+
.rk-ip-filemodal-mount{padding:12px 14px;overflow:auto;flex:1;min-height:0}
|
|
174
390
|
|
|
175
391
|
/* right: result + history */
|
|
176
392
|
.rk-inpaint-output{padding:18px;display:grid;gap:18px;align-content:start}
|
|
177
|
-
.
|
|
393
|
+
.custom-inpaint-output[hidden]{display:none}
|
|
178
394
|
.rk-inpaint-result-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
|
179
|
-
.rk-inpaint-
|
|
180
|
-
.rk-inpaint-result-imgwrap{display:block;border-radius:var(--rk-r-2xl);overflow:hidden;background:var(--rk-n-100);box-shadow:var(--rk-shadow-sm)}
|
|
395
|
+
.rk-inpaint-result-imgwrap{display:block;width:100%;padding:0;border:0;border-radius:var(--rk-r-2xl);overflow:hidden;background:var(--rk-n-100);box-shadow:var(--rk-shadow-sm);cursor:zoom-in}
|
|
181
396
|
.rk-inpaint-result-img{display:block;width:100%;height:auto}
|
|
182
|
-
.rk-inpaint-
|
|
183
|
-
.rk-
|
|
184
|
-
.rk-inpaint-
|
|
185
|
-
.
|
|
186
|
-
.
|
|
397
|
+
.rk-inpaint-history-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
|
398
|
+
.rk-ip-histtools{display:flex;align-items:center;gap:8px}
|
|
399
|
+
.rk-inpaint-historylist{display:grid;gap:10px}
|
|
400
|
+
.custom-inpaint-history-empty{font-size:12.5px;color:var(--rk-text-faint);padding:14px 4px;text-align:center;line-height:1.4}
|
|
401
|
+
.custom-inpaint-history-item{position:relative;display:grid;grid-template-columns:54px minmax(0,1fr);align-items:center;gap:10px;padding:8px;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);background:var(--rk-n-50)}
|
|
402
|
+
.custom-inpaint-history-item[data-kind="source"]{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
403
|
+
.custom-inpaint-history-thumb-button{width:54px;height:54px;display:grid;padding:0;border:0;border-radius:var(--rk-r-lg);background:transparent;cursor:zoom-in}
|
|
404
|
+
.custom-inpaint-history-thumb{width:54px;height:54px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);object-fit:cover;background:var(--rk-n-200)}
|
|
405
|
+
.custom-inpaint-history-generating{width:54px;height:54px;display:grid;place-items:center;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-100)}
|
|
406
|
+
.custom-inpaint-history-generating::before{content:"";width:22px;height:22px;border-radius:50%;border:2.5px solid var(--rk-gold-tint2,rgba(252,185,0,.35));border-top-color:var(--rk-gold-700);animation:rk-ip-spin .8s linear infinite}
|
|
407
|
+
.custom-inpaint-history-meta{min-width:0;display:grid;gap:5px}
|
|
408
|
+
.custom-inpaint-history-title{min-width:0;color:var(--rk-ink);font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
409
|
+
.custom-inpaint-history-subtitle{min-width:0;color:var(--rk-text-muted);font-size:11.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
410
|
+
.custom-inpaint-history-actions{display:flex;align-items:center;gap:5px}
|
|
411
|
+
.custom-inpaint-history-icon{width:26px;height:26px;display:inline-grid;place-items:center;padding:0;border:1px solid var(--rk-border);border-radius:50%;background:#fff;color:var(--rk-n-700);cursor:pointer;text-decoration:none}
|
|
412
|
+
.custom-inpaint-history-icon:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
|
|
413
|
+
.custom-inpaint-history-icon svg{width:13px;height:13px}
|
|
414
|
+
.custom-inpaint-history-check{min-height:26px;padding:0 10px;border:1px solid var(--rk-border);border-radius:999px;background:#fff;color:var(--rk-ink);font:inherit;font-size:11.5px;font-weight:800;cursor:pointer}
|
|
415
|
+
.custom-inpaint-history-check[disabled]{cursor:wait;opacity:.6}
|
|
416
|
+
.custom-inpaint-history-popconfirm{position:absolute;right:6px;top:6px;z-index:6;width:160px;display:grid;gap:7px;padding:9px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-surface);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card))}
|
|
417
|
+
.custom-inpaint-history-popconfirm[hidden]{display:none}
|
|
418
|
+
.custom-inpaint-history-popconfirm p{margin:0;color:var(--rk-ink);font-size:12px;font-weight:700;line-height:1.3}
|
|
419
|
+
.custom-inpaint-history-popconfirm-actions{display:flex;gap:6px}
|
|
420
|
+
.custom-inpaint-history-popconfirm-actions button{flex:1 1 0;min-height:26px;padding:0 8px;border-radius:var(--rk-r-md,8px);border:1px solid var(--rk-border);background:#fff;color:var(--rk-ink);font:inherit;font-size:11.5px;font-weight:700;cursor:pointer}
|
|
421
|
+
.custom-inpaint-history-popconfirm-actions button[data-danger="true"]{border-color:#e6a8a0;color:#b42318}
|
|
422
|
+
|
|
423
|
+
/* lightbox */
|
|
424
|
+
.custom-inpaint-lightbox{position:fixed;inset:0;z-index:80;display:none;place-items:center;padding:22px;background:rgba(28,25,23,.66);backdrop-filter:blur(8px)}
|
|
425
|
+
.custom-inpaint-lightbox[data-open="true"]{display:grid}
|
|
426
|
+
.custom-inpaint-lightbox[hidden]{display:none}
|
|
427
|
+
.custom-inpaint-lightbox-dialog{width:min(1100px,calc(100vw - 44px));max-height:94svh;display:grid;grid-template-rows:auto minmax(0,1fr);gap:12px;padding:14px;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);background:var(--rk-surface);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card))}
|
|
428
|
+
.custom-inpaint-lightbox-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
429
|
+
.custom-inpaint-lightbox-title{min-width:0;margin:0;color:var(--rk-ink);font-size:14px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
430
|
+
.custom-inpaint-lightbox-actions{display:flex;align-items:center;gap:8px}
|
|
431
|
+
.custom-inpaint-lightbox-stage{min-height:0;display:grid;place-items:center;overflow:auto}
|
|
432
|
+
.custom-inpaint-lightbox-stage img{max-width:100%;max-height:calc(94svh - 92px);object-fit:contain;border-radius:var(--rk-r-xl);background:var(--rk-n-100)}
|
|
433
|
+
|
|
187
434
|
@keyframes rk-ip-spin{to{transform:rotate(360deg)}}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.rk-inpaint-
|
|
191
|
-
.rk-inpaint-
|
|
192
|
-
.rk-inpaint-
|
|
193
|
-
.rk-inpaint-
|
|
435
|
+
|
|
436
|
+
/* page-head toolbar (Start New + gear + past-sessions) */
|
|
437
|
+
.rk-inpaint-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
|
|
438
|
+
.rk-inpaint-head-text{min-width:0;flex:1 1 320px}
|
|
439
|
+
.rk-inpaint-toolbar{flex:none;display:flex;align-items:center;gap:8px;padding-top:2px}
|
|
440
|
+
.rk-inpaint-startnew{gap:6px}
|
|
441
|
+
|
|
442
|
+
/* settings modal */
|
|
443
|
+
.rk-ip-settingsmodal{position:fixed;inset:0;z-index:70;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px;animation:rk-ip-fade .16s var(--rk-ease)}
|
|
444
|
+
.rk-ip-settingsmodal[hidden]{display:none}
|
|
445
|
+
.rk-ip-settingsmodal-panel{width:min(440px,100%);display:flex;flex-direction:column;gap:0;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card));overflow:hidden}
|
|
446
|
+
.rk-ip-settingsmodal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;border-bottom:1px solid var(--rk-border)}
|
|
447
|
+
.rk-ip-settingsmodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
|
|
448
|
+
.rk-ip-settingsmodal-body{padding:16px 18px;display:grid;gap:14px}
|
|
449
|
+
|
|
450
|
+
/* past-sessions left slide-in panel (like /chat history) */
|
|
451
|
+
.rk-ip-sessions{position:fixed;left:0;top:0;bottom:0;z-index:96;width:min(320px,86vw);display:flex;flex-direction:column;
|
|
452
|
+
background:var(--rk-surface);border-right:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xl,var(--rk-shadow-card));
|
|
453
|
+
transform:translateX(-100%);transition:transform var(--rk-dur) var(--rk-ease)}
|
|
454
|
+
.rk-ip-sessions.is-open{transform:none}
|
|
455
|
+
.rk-ip-sessions-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:16px 14px 10px;border-bottom:1px solid var(--rk-border)}
|
|
456
|
+
.rk-ip-sessions-title{font-family:var(--rk-font-display);font-weight:800;font-size:14px;color:var(--rk-ink)}
|
|
457
|
+
.rk-ip-sessions-list{flex:1;min-height:0;overflow-y:auto;padding:8px;display:grid;gap:4px;align-content:start}
|
|
458
|
+
.rk-ip-sessions-empty{font-size:12.5px;color:var(--rk-text-faint);padding:16px 8px;text-align:center;line-height:1.4}
|
|
459
|
+
.rk-ip-session{display:grid;grid-template-columns:44px minmax(0,1fr);align-items:center;gap:10px;padding:7px;border:1px solid transparent;border-radius:var(--rk-r-lg);background:transparent;cursor:pointer;text-align:left;font:inherit;width:100%}
|
|
460
|
+
.rk-ip-session:hover{background:var(--rk-n-50)}
|
|
461
|
+
.rk-ip-session.is-active{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
|
|
462
|
+
.rk-ip-session-thumb{width:44px;height:44px;border:1px solid var(--rk-border);border-radius:var(--rk-r-md,8px);object-fit:cover;background:var(--rk-n-200)}
|
|
463
|
+
.rk-ip-session-thumb.is-empty{display:grid;place-items:center;color:var(--rk-text-faint)}
|
|
464
|
+
.rk-ip-session-meta{min-width:0;display:grid;gap:3px}
|
|
465
|
+
.rk-ip-session-title{min-width:0;color:var(--rk-ink);font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
466
|
+
.rk-ip-session-sub{min-width:0;color:var(--rk-text-muted);font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
467
|
+
.rk-ip-sessions-backdrop{position:fixed;inset:0;z-index:95;background:rgba(28,25,23,.34)}
|
|
468
|
+
.rk-ip-sessions-backdrop[hidden]{display:none}
|
|
194
469
|
`;
|
|
195
470
|
const script = `
|
|
196
471
|
(function(){
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
472
|
+
const uploadButton = document.querySelector("[data-custom-inpaint-upload]");
|
|
473
|
+
const startNewButton = document.querySelector("[data-custom-inpaint-start-new]");
|
|
474
|
+
const fileInput = document.querySelector("[data-custom-inpaint-file]");
|
|
475
|
+
const sourceName = document.querySelector("[data-custom-inpaint-source-name]");
|
|
476
|
+
const statusEl = document.querySelector("[data-custom-inpaint-status]");
|
|
477
|
+
const emptyState = document.querySelector("[data-custom-inpaint-empty]");
|
|
478
|
+
const canvasWrap = document.querySelector("[data-custom-inpaint-canvas-wrap]");
|
|
479
|
+
const outputPanel = document.querySelector("[data-custom-inpaint-output]");
|
|
480
|
+
const resultOpenButton = document.querySelector("[data-custom-inpaint-result-open]");
|
|
481
|
+
const resultImage = document.querySelector("[data-custom-inpaint-result]");
|
|
482
|
+
const historyList = document.querySelector("[data-custom-inpaint-history-list]");
|
|
483
|
+
const historyRefresh = document.querySelector("[data-custom-inpaint-history-refresh]");
|
|
484
|
+
const inpaintImage = document.querySelector("[data-editor-inpaint-image]");
|
|
485
|
+
const inpaintCanvas = document.querySelector("[data-editor-inpaint-canvas]");
|
|
486
|
+
const inpaintBrushCursor = document.querySelector("[data-editor-inpaint-brush-cursor]");
|
|
487
|
+
const inpaintUndo = document.querySelector("[data-editor-inpaint-undo]");
|
|
488
|
+
const inpaintRedo = document.querySelector("[data-editor-inpaint-redo]");
|
|
489
|
+
const inpaintClear = document.querySelector("[data-editor-inpaint-clear]");
|
|
490
|
+
const inpaintBrush = document.querySelector("[data-editor-inpaint-brush]");
|
|
491
|
+
const inpaintBrushLabel = document.querySelector("[data-editor-inpaint-brush-label]");
|
|
492
|
+
const inpaintColorPicker = document.querySelector("[data-editor-inpaint-color-picker]");
|
|
493
|
+
const inpaintAddColor = document.querySelector("[data-editor-inpaint-add-color]");
|
|
494
|
+
const inpaintProviderWrap = document.querySelector("[data-editor-inpaint-provider-wrap]");
|
|
495
|
+
const inpaintProviderSelect = document.querySelector("[data-editor-inpaint-provider]");
|
|
496
|
+
const inpaintGlobalPrompt = document.querySelector("[data-editor-inpaint-global-prompt]");
|
|
497
|
+
const inpaintAttachmentInput = document.querySelector("[data-editor-inpaint-attachment-input]");
|
|
498
|
+
const inpaintGlobalAttachments = document.querySelector('[data-editor-inpaint-attachments="global"]');
|
|
499
|
+
const inpaintSwatches = document.querySelector("[data-editor-inpaint-swatches]");
|
|
500
|
+
const inpaintPrompts = document.querySelector("[data-editor-inpaint-prompts]");
|
|
501
|
+
const inpaintSubmit = document.querySelector("[data-editor-inpaint-submit]");
|
|
502
|
+
const inpaintSubmitLabel = document.querySelector("[data-editor-inpaint-submit-label]");
|
|
503
|
+
const settingsButton = document.querySelector("[data-custom-inpaint-settings-button]");
|
|
504
|
+
const settingsPopover = document.querySelector("[data-custom-inpaint-settings]");
|
|
505
|
+
const settingsClose = document.querySelector("[data-custom-inpaint-settings-close]");
|
|
506
|
+
const tracerInput = document.querySelector("[data-custom-inpaint-tracer]");
|
|
507
|
+
const sessionsToggle = document.querySelector("[data-custom-inpaint-history-toggle]");
|
|
508
|
+
const sessionsPanel = document.querySelector("[data-custom-inpaint-sessions]");
|
|
509
|
+
const sessionsList = document.querySelector("[data-custom-inpaint-sessions-list]");
|
|
510
|
+
const sessionsClose = document.querySelector("[data-custom-inpaint-sessions-close]");
|
|
511
|
+
const sessionsBackdrop = document.querySelector("[data-custom-inpaint-sessions-backdrop]");
|
|
512
|
+
const lightbox = document.querySelector("[data-custom-inpaint-lightbox]");
|
|
513
|
+
const lightboxTitle = document.querySelector("[data-custom-inpaint-lightbox-title]");
|
|
514
|
+
const lightboxImage = document.querySelector("[data-custom-inpaint-lightbox-image]");
|
|
515
|
+
const lightboxCopy = document.querySelector("[data-custom-inpaint-lightbox-copy]");
|
|
516
|
+
const lightboxDownload = document.querySelector("[data-custom-inpaint-lightbox-download]");
|
|
517
|
+
const lightboxClose = document.querySelector("[data-custom-inpaint-lightbox-close]");
|
|
518
|
+
if (!(uploadButton instanceof HTMLButtonElement)
|
|
519
|
+
|| !(startNewButton instanceof HTMLButtonElement)
|
|
520
|
+
|| !(fileInput instanceof HTMLInputElement)
|
|
521
|
+
|| !(sourceName instanceof HTMLElement)
|
|
522
|
+
|| !(statusEl instanceof HTMLElement)
|
|
523
|
+
|| !(emptyState instanceof HTMLElement)
|
|
524
|
+
|| !(canvasWrap instanceof HTMLElement)
|
|
525
|
+
|| !(outputPanel instanceof HTMLElement)
|
|
526
|
+
|| !(resultOpenButton instanceof HTMLButtonElement)
|
|
527
|
+
|| !(resultImage instanceof HTMLImageElement)
|
|
528
|
+
|| !(historyList instanceof HTMLElement)
|
|
529
|
+
|| !(historyRefresh instanceof HTMLButtonElement)
|
|
530
|
+
|| !(inpaintImage instanceof HTMLImageElement)
|
|
531
|
+
|| !(inpaintCanvas instanceof HTMLCanvasElement)
|
|
532
|
+
|| !(inpaintBrushCursor instanceof HTMLElement)
|
|
533
|
+
|| !(inpaintUndo instanceof HTMLButtonElement)
|
|
534
|
+
|| !(inpaintRedo instanceof HTMLButtonElement)
|
|
535
|
+
|| !(inpaintClear instanceof HTMLButtonElement)
|
|
536
|
+
|| !(inpaintBrush instanceof HTMLInputElement)
|
|
537
|
+
|| !(inpaintBrushLabel instanceof HTMLElement)
|
|
538
|
+
|| !(inpaintColorPicker instanceof HTMLInputElement)
|
|
539
|
+
|| !(inpaintAddColor instanceof HTMLButtonElement)
|
|
540
|
+
|| !(inpaintProviderWrap instanceof HTMLElement)
|
|
541
|
+
|| !(inpaintProviderSelect instanceof HTMLSelectElement)
|
|
542
|
+
|| !(inpaintGlobalPrompt instanceof HTMLTextAreaElement)
|
|
543
|
+
|| !(inpaintAttachmentInput instanceof HTMLInputElement)
|
|
544
|
+
|| !(inpaintGlobalAttachments instanceof HTMLElement)
|
|
545
|
+
|| !(inpaintSwatches instanceof HTMLElement)
|
|
546
|
+
|| !(inpaintPrompts instanceof HTMLElement)
|
|
547
|
+
|| !(inpaintSubmit instanceof HTMLButtonElement)
|
|
548
|
+
|| !(inpaintSubmitLabel instanceof HTMLElement)
|
|
549
|
+
|| !(settingsButton instanceof HTMLButtonElement)
|
|
550
|
+
|| !(settingsPopover instanceof HTMLElement)
|
|
551
|
+
|| !(tracerInput instanceof HTMLInputElement)
|
|
552
|
+
|| !(lightbox instanceof HTMLElement)
|
|
553
|
+
|| !(lightboxTitle instanceof HTMLElement)
|
|
554
|
+
|| !(lightboxImage instanceof HTMLImageElement)
|
|
555
|
+
|| !(lightboxCopy instanceof HTMLButtonElement)
|
|
556
|
+
|| !(lightboxDownload instanceof HTMLAnchorElement)
|
|
557
|
+
|| !(lightboxClose instanceof HTMLButtonElement)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let sourceImageUrl = "";
|
|
562
|
+
let sourceImageName = "";
|
|
563
|
+
let inpaintMaskColors = [{ color: "#ff2d55", prompt: "" }];
|
|
564
|
+
let activeInpaintColor = "#ff2d55";
|
|
565
|
+
let inpaintStrokes = [];
|
|
566
|
+
let redoStrokes = [];
|
|
567
|
+
let currentInpaintStroke = null;
|
|
568
|
+
let isInpaintPainting = false;
|
|
569
|
+
let inpaintAttachments = [];
|
|
570
|
+
let pendingInpaintAttachmentScope = null;
|
|
571
|
+
let inpaintHistoryItems = [];
|
|
572
|
+
// Local "starting image" history entries (uploaded / generated / deep-linked
|
|
573
|
+
// sources). They are not job runs, so they survive server-history reloads by
|
|
574
|
+
// being re-injected after each rebuild — this lets users see where a session
|
|
575
|
+
// started from.
|
|
576
|
+
let sourceHistoryItems = [];
|
|
577
|
+
let historyTracerRefreshTimer = 0;
|
|
578
|
+
const hiddenInpaintHistoryKeys = new Set();
|
|
579
|
+
const maxInpaintAttachments = 8;
|
|
580
|
+
const allowedInpaintAttachmentTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
581
|
+
|
|
582
|
+
// Session tracer. A fresh session starts "auto-pending": the tracer is left
|
|
583
|
+
// unnamed until the first prompt / file upload, then it is auto-named from
|
|
584
|
+
// that seed (readable slug + random suffix) so History groups per session.
|
|
585
|
+
let sessionTracer = "";
|
|
586
|
+
let tracerAutoPending = true;
|
|
587
|
+
|
|
588
|
+
function tracerSuffix() {
|
|
589
|
+
return globalThis.crypto && typeof globalThis.crypto.randomUUID === "function"
|
|
590
|
+
? globalThis.crypto.randomUUID().replace(/-/g, "").slice(0, 6)
|
|
591
|
+
: Math.random().toString(16).slice(2, 8).padEnd(6, "0").slice(0, 6);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function makeTracerId() {
|
|
595
|
+
return "image-edit-" + tracerSuffix();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function slugifyTracer(value) {
|
|
599
|
+
const normalized = String(value || "").trim().toLowerCase()
|
|
600
|
+
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
601
|
+
return (normalized || "image-edit").slice(0, 24);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Turn the first prompt / filename into the session's tracer name.
|
|
605
|
+
function nameTracerFromSeed(seed) {
|
|
606
|
+
const tracer = slugifyTracer(seed) + "-" + tracerSuffix();
|
|
607
|
+
sessionTracer = tracer;
|
|
608
|
+
tracerInput.value = tracer;
|
|
609
|
+
tracerAutoPending = false;
|
|
610
|
+
return tracer;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function commitRandomTracer() {
|
|
614
|
+
const tracer = makeTracerId();
|
|
615
|
+
sessionTracer = tracer;
|
|
616
|
+
tracerInput.value = tracer;
|
|
617
|
+
tracerAutoPending = false;
|
|
618
|
+
return tracer;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Effective tracer without forcing a name — "" while still auto-pending.
|
|
622
|
+
function resolveTracer() {
|
|
623
|
+
const typed = tracerInput.value.trim();
|
|
624
|
+
if (typed) return typed;
|
|
625
|
+
return sessionTracer;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Used at submit/persist time — must always return a concrete tracer.
|
|
629
|
+
function getTracerValue() {
|
|
630
|
+
const typed = tracerInput.value.trim();
|
|
631
|
+
if (typed) { sessionTracer = typed; tracerAutoPending = false; return typed; }
|
|
632
|
+
if (sessionTracer) return sessionTracer;
|
|
633
|
+
return commitRandomTracer();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function queueInpaintHistoryReload() {
|
|
637
|
+
window.clearTimeout(historyTracerRefreshTimer);
|
|
638
|
+
historyTracerRefreshTimer = window.setTimeout(() => {
|
|
639
|
+
void loadInpaintHistory();
|
|
640
|
+
}, 300);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function setSubmitLoading(isLoading) {
|
|
644
|
+
inpaintSubmit.disabled = isLoading || sessionLocked;
|
|
645
|
+
inpaintSubmit.setAttribute("data-loading", isLoading ? "true" : "false");
|
|
646
|
+
inpaintSubmitLabel.textContent = isLoading
|
|
647
|
+
? "Generating\\u2026"
|
|
648
|
+
: (sessionLocked ? "Edit generated \\u2014 Start New to edit again" : "Generate edit");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Once an edit is generated the inputs that produced it are frozen (prompts,
|
|
652
|
+
// masks, brush, colors, references, source) so the record stays truthful —
|
|
653
|
+
// to make another edit the user hits Start New (or opens/loads another image).
|
|
654
|
+
let sessionLocked = false;
|
|
655
|
+
function applySessionLock() {
|
|
656
|
+
const locked = sessionLocked;
|
|
657
|
+
const workbench = document.querySelector(".rk-inpaint-workbench");
|
|
658
|
+
if (workbench instanceof HTMLElement) workbench.classList.toggle("is-locked", locked);
|
|
659
|
+
inpaintGlobalPrompt.readOnly = locked;
|
|
660
|
+
inpaintBrush.disabled = locked;
|
|
661
|
+
inpaintUndo.disabled = locked;
|
|
662
|
+
inpaintRedo.disabled = locked;
|
|
663
|
+
inpaintClear.disabled = locked;
|
|
664
|
+
inpaintColorPicker.disabled = locked;
|
|
665
|
+
inpaintAddColor.disabled = locked;
|
|
666
|
+
uploadButton.disabled = locked;
|
|
667
|
+
const globalAttach = document.querySelector('[data-editor-inpaint-attach-scope="global"]');
|
|
668
|
+
if (globalAttach instanceof HTMLButtonElement) globalAttach.disabled = locked;
|
|
669
|
+
inpaintPrompts.querySelectorAll("textarea.editor-inpaint-prompt").forEach((el) => { el.readOnly = locked; });
|
|
670
|
+
inpaintPrompts.querySelectorAll(".editor-inpaint-color-remove, .editor-inpaint-attach-button").forEach((el) => { el.disabled = locked; });
|
|
671
|
+
// Keep the submit button + label consistent with the lock while not loading.
|
|
672
|
+
if (inpaintSubmit.getAttribute("data-loading") !== "true") setSubmitLoading(false);
|
|
673
|
+
}
|
|
674
|
+
function setSessionLocked(locked) {
|
|
675
|
+
sessionLocked = Boolean(locked);
|
|
676
|
+
applySessionLock();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function setStatus(message, tone) {
|
|
680
|
+
statusEl.textContent = message || "";
|
|
681
|
+
if (tone) {
|
|
682
|
+
statusEl.setAttribute("data-tone", tone);
|
|
683
|
+
} else {
|
|
684
|
+
statusEl.removeAttribute("data-tone");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function imageDownloadName(prefix, createdAt) {
|
|
689
|
+
const stamp = String(createdAt || new Date().toISOString()).replace(/[^0-9a-z]+/gi, "-");
|
|
690
|
+
return prefix + "-" + stamp + ".png";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function openImagePreview(url, label, downloadName) {
|
|
694
|
+
if (!url) return;
|
|
695
|
+
lightboxImage.src = url;
|
|
696
|
+
lightboxImage.alt = label || "Image edit output preview";
|
|
697
|
+
lightboxTitle.textContent = label || "Image edit output";
|
|
698
|
+
lightboxCopy.dataset.copyUrl = url;
|
|
699
|
+
lightboxCopy.setAttribute("title", "Copy URL");
|
|
700
|
+
lightboxDownload.href = url;
|
|
701
|
+
lightboxDownload.setAttribute("download", downloadName || imageDownloadName("inpaint-output"));
|
|
702
|
+
lightbox.hidden = false;
|
|
703
|
+
lightbox.setAttribute("data-open", "true");
|
|
704
|
+
document.body.style.overflow = "hidden";
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function closeImagePreview() {
|
|
708
|
+
lightbox.setAttribute("data-open", "false");
|
|
709
|
+
lightbox.hidden = true;
|
|
710
|
+
lightboxImage.removeAttribute("src");
|
|
711
|
+
lightboxCopy.dataset.copyUrl = "";
|
|
712
|
+
lightboxCopy.setAttribute("title", "Copy URL");
|
|
713
|
+
lightboxDownload.href = "#";
|
|
714
|
+
document.body.style.overflow = "";
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function updateBrushCursorSize() {
|
|
718
|
+
const rect = inpaintCanvas.getBoundingClientRect();
|
|
719
|
+
const brushSize = Math.max(1, Number(inpaintBrush.value) || 40);
|
|
720
|
+
const displayScale = inpaintCanvas.width && rect.width ? rect.width / inpaintCanvas.width : 1;
|
|
721
|
+
canvasWrap.style.setProperty("--inpaint-brush-cursor-size", Math.max(6, brushSize * displayScale) + "px");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function updateBrushCursorPosition(event) {
|
|
725
|
+
const rect = inpaintCanvas.getBoundingClientRect();
|
|
726
|
+
canvasWrap.style.setProperty("--inpaint-brush-cursor-x", Math.max(0, Math.min(rect.width, event.clientX - rect.left)) + "px");
|
|
727
|
+
canvasWrap.style.setProperty("--inpaint-brush-cursor-y", Math.max(0, Math.min(rect.height, event.clientY - rect.top)) + "px");
|
|
728
|
+
updateBrushCursorSize();
|
|
729
|
+
inpaintBrushCursor.dataset.visible = "true";
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function hideBrushCursor() {
|
|
733
|
+
inpaintBrushCursor.dataset.visible = "false";
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function normalizeHexColor(value) {
|
|
737
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
738
|
+
if (/^#[0-9a-f]{6}$/i.test(raw)) return raw;
|
|
739
|
+
if (/^#[0-9a-f]{3}$/i.test(raw)) {
|
|
740
|
+
return "#" + raw.slice(1).split("").map((part) => part + part).join("");
|
|
741
|
+
}
|
|
742
|
+
return "#ff2d55";
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const inpaintColorPalette = ["#ff2d55", "#3b82f6", "#22c55e", "#f59e0b", "#a855f7", "#06b6d4", "#f97316", "#ec4899"];
|
|
746
|
+
|
|
747
|
+
function syncInpaintPromptValuesFromDom() {
|
|
748
|
+
const fields = Array.from(inpaintPrompts.querySelectorAll("[data-color]"));
|
|
749
|
+
for (const field of fields) {
|
|
750
|
+
if (field instanceof HTMLTextAreaElement) {
|
|
751
|
+
updateInpaintPrompt(field.dataset.color || "", field.value);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function attachmentId() {
|
|
757
|
+
return "att_" + Date.now().toString(36) + "_" + Math.random().toString(16).slice(2, 8);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function attachmentsForScope(scope, color) {
|
|
761
|
+
const normalizedColor = color ? normalizeHexColor(color) : "";
|
|
762
|
+
return inpaintAttachments.filter((attachment) => (
|
|
763
|
+
attachment.scope === scope && (!normalizedColor || attachment.color === normalizedColor)
|
|
764
|
+
));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function renderAttachmentChips(container, scope, color) {
|
|
768
|
+
container.innerHTML = "";
|
|
769
|
+
for (const attachment of attachmentsForScope(scope, color)) {
|
|
770
|
+
const chip = document.createElement("span");
|
|
771
|
+
chip.className = "editor-inpaint-attachment-chip";
|
|
772
|
+
const preview = document.createElement("img");
|
|
773
|
+
preview.src = attachment.url;
|
|
774
|
+
preview.alt = "";
|
|
775
|
+
preview.loading = "lazy";
|
|
776
|
+
const label = document.createElement("span");
|
|
777
|
+
label.textContent = attachment.fileName || "reference image";
|
|
778
|
+
const remove = document.createElement("button");
|
|
779
|
+
remove.type = "button";
|
|
780
|
+
remove.textContent = "×";
|
|
781
|
+
remove.dataset.editorInpaintRemoveAttachment = attachment.id;
|
|
782
|
+
remove.setAttribute("aria-label", "Remove " + (attachment.fileName || "reference image"));
|
|
783
|
+
chip.append(preview, label, remove);
|
|
784
|
+
container.append(chip);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function renderAllAttachmentChips() {
|
|
789
|
+
renderAttachmentChips(inpaintGlobalAttachments, "global");
|
|
790
|
+
for (const container of Array.from(inpaintPrompts.querySelectorAll("[data-editor-inpaint-attachments]"))) {
|
|
791
|
+
if (container instanceof HTMLElement && container.dataset.color) {
|
|
792
|
+
renderAttachmentChips(container, "color", container.dataset.color);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function openAttachmentPicker(scope, color) {
|
|
798
|
+
pendingInpaintAttachmentScope = { scope, color: color ? normalizeHexColor(color) : "" };
|
|
799
|
+
inpaintAttachmentInput.click();
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function isAllowedInpaintAttachment(file) {
|
|
803
|
+
return file instanceof File && allowedInpaintAttachmentTypes.has(file.type);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function imageFilesFromPaste(event) {
|
|
807
|
+
const clipboard = event.clipboardData;
|
|
808
|
+
if (!clipboard) return [];
|
|
809
|
+
const files = [];
|
|
810
|
+
for (const item of Array.from(clipboard.items || [])) {
|
|
811
|
+
if (item.kind !== "file") continue;
|
|
812
|
+
const file = item.getAsFile();
|
|
813
|
+
if (file) files.push(file);
|
|
814
|
+
}
|
|
815
|
+
if (!files.length) {
|
|
816
|
+
for (const file of Array.from(clipboard.files || [])) {
|
|
817
|
+
files.push(file);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return files;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function handleAttachmentPaste(event, scope, color) {
|
|
824
|
+
const files = imageFilesFromPaste(event);
|
|
825
|
+
if (!files.length) return;
|
|
826
|
+
event.preventDefault();
|
|
827
|
+
const allowed = files.filter(isAllowedInpaintAttachment);
|
|
828
|
+
if (!allowed.length) {
|
|
829
|
+
setStatus("Reference attachments must be JPEG, PNG, or WebP images.", "error");
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
void addAttachmentFilesForScope(allowed, {
|
|
833
|
+
scope,
|
|
834
|
+
color: scope === "color" ? normalizeHexColor(color) : ""
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function pickNextInpaintColor(requestedColor) {
|
|
839
|
+
const requested = normalizeHexColor(requestedColor);
|
|
840
|
+
const used = new Set(inpaintMaskColors.map((entry) => entry.color));
|
|
841
|
+
if (!used.has(requested)) {
|
|
842
|
+
return requested;
|
|
843
|
+
}
|
|
844
|
+
for (const color of inpaintColorPalette) {
|
|
845
|
+
if (!used.has(color)) {
|
|
846
|
+
return color;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
for (let index = 0; index < 32; index += 1) {
|
|
850
|
+
const color = "#" + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, "0");
|
|
851
|
+
if (!used.has(color)) {
|
|
852
|
+
return color;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return requested;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function renderInpaintControls() {
|
|
859
|
+
syncInpaintPromptValuesFromDom();
|
|
860
|
+
inpaintBrushLabel.textContent = inpaintBrush.value;
|
|
861
|
+
inpaintSwatches.innerHTML = "";
|
|
862
|
+
inpaintPrompts.innerHTML = "";
|
|
863
|
+
for (const entry of inpaintMaskColors) {
|
|
864
|
+
const row = document.createElement("div");
|
|
865
|
+
row.className = "editor-inpaint-color-row";
|
|
866
|
+
row.dataset.color = entry.color;
|
|
867
|
+
row.dataset.active = entry.color === activeInpaintColor ? "true" : "false";
|
|
868
|
+
const dot = document.createElement("button");
|
|
869
|
+
dot.type = "button";
|
|
870
|
+
dot.className = "editor-inpaint-color-dot";
|
|
871
|
+
dot.style.setProperty("--inpaint-color", entry.color);
|
|
872
|
+
dot.dataset.color = entry.color;
|
|
873
|
+
dot.setAttribute("aria-label", "Use mask color " + entry.color);
|
|
874
|
+
dot.setAttribute("title", entry.color);
|
|
875
|
+
const colorTools = document.createElement("div");
|
|
876
|
+
colorTools.className = "editor-inpaint-color-tools";
|
|
877
|
+
colorTools.append(dot);
|
|
878
|
+
if (inpaintMaskColors.length > 1) {
|
|
879
|
+
const removeColor = document.createElement("button");
|
|
880
|
+
removeColor.type = "button";
|
|
881
|
+
removeColor.className = "editor-inpaint-color-remove";
|
|
882
|
+
removeColor.dataset.editorInpaintRemoveColor = entry.color;
|
|
883
|
+
removeColor.setAttribute("aria-label", "Remove mask color " + entry.color);
|
|
884
|
+
removeColor.setAttribute("title", "Remove color");
|
|
885
|
+
removeColor.innerHTML = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 5l10 10"></path><path d="M15 5 5 15"></path></svg>';
|
|
886
|
+
removeColor.addEventListener("click", (event) => {
|
|
887
|
+
event.preventDefault();
|
|
888
|
+
event.stopPropagation();
|
|
889
|
+
removeInpaintColor(entry.color);
|
|
890
|
+
});
|
|
891
|
+
colorTools.append(removeColor);
|
|
892
|
+
}
|
|
893
|
+
const prompt = document.createElement("textarea");
|
|
894
|
+
prompt.className = "editor-inpaint-prompt";
|
|
895
|
+
prompt.dataset.color = entry.color;
|
|
896
|
+
prompt.value = entry.prompt || "";
|
|
897
|
+
prompt.placeholder = "In color " + entry.color + " replace with...";
|
|
898
|
+
const stack = document.createElement("div");
|
|
899
|
+
stack.className = "editor-inpaint-prompt-stack";
|
|
900
|
+
const field = document.createElement("div");
|
|
901
|
+
field.className = "editor-inpaint-prompt-field";
|
|
902
|
+
const tools = document.createElement("div");
|
|
903
|
+
tools.className = "editor-inpaint-attachment-tools";
|
|
904
|
+
const attach = document.createElement("button");
|
|
905
|
+
attach.type = "button";
|
|
906
|
+
attach.className = "editor-inpaint-attach-button";
|
|
907
|
+
attach.dataset.editorInpaintAttachScope = "color";
|
|
908
|
+
attach.dataset.color = entry.color;
|
|
909
|
+
attach.setAttribute("aria-label", "Attach reference image for " + entry.color);
|
|
910
|
+
attach.setAttribute("title", "Attach reference image");
|
|
911
|
+
attach.innerHTML = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8.1 10.7 4.8-4.8a2.5 2.5 0 0 1 3.5 3.5l-6.1 6.1a4 4 0 0 1-5.7-5.7l6.3-6.3"></path><path d="m10 8.8-4.1 4.1a1.6 1.6 0 0 0 2.3 2.3l5.6-5.6"></path></svg>';
|
|
912
|
+
const chips = document.createElement("span");
|
|
913
|
+
chips.className = "editor-inpaint-attachment-chips";
|
|
914
|
+
chips.dataset.editorInpaintAttachments = "color";
|
|
915
|
+
chips.dataset.color = entry.color;
|
|
916
|
+
field.append(prompt, attach);
|
|
917
|
+
tools.append(chips);
|
|
918
|
+
stack.append(field, tools);
|
|
919
|
+
row.append(colorTools, stack);
|
|
920
|
+
inpaintPrompts.append(row);
|
|
921
|
+
}
|
|
922
|
+
renderAllAttachmentChips();
|
|
923
|
+
applySessionLock();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function redrawInpaintMask() {
|
|
927
|
+
const ctx = inpaintCanvas.getContext("2d");
|
|
928
|
+
if (!ctx) return;
|
|
929
|
+
ctx.clearRect(0, 0, inpaintCanvas.width, inpaintCanvas.height);
|
|
930
|
+
ctx.lineCap = "round";
|
|
931
|
+
ctx.lineJoin = "round";
|
|
932
|
+
for (const stroke of inpaintStrokes) {
|
|
933
|
+
if (!stroke.points || stroke.points.length < 1) continue;
|
|
934
|
+
ctx.strokeStyle = stroke.color;
|
|
935
|
+
ctx.lineWidth = stroke.size;
|
|
936
|
+
ctx.globalAlpha = 1;
|
|
937
|
+
ctx.beginPath();
|
|
938
|
+
ctx.moveTo(stroke.points[0].x, stroke.points[0].y);
|
|
939
|
+
if (stroke.points.length === 1) {
|
|
940
|
+
ctx.lineTo(stroke.points[0].x + 0.01, stroke.points[0].y + 0.01);
|
|
941
|
+
} else {
|
|
942
|
+
for (let index = 1; index < stroke.points.length; index += 1) {
|
|
943
|
+
ctx.lineTo(stroke.points[index].x, stroke.points[index].y);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
ctx.stroke();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function syncInpaintCanvasSize() {
|
|
951
|
+
if (!inpaintImage.naturalWidth || !inpaintImage.naturalHeight) return;
|
|
952
|
+
if (inpaintCanvas.width !== inpaintImage.naturalWidth || inpaintCanvas.height !== inpaintImage.naturalHeight) {
|
|
953
|
+
inpaintCanvas.width = inpaintImage.naturalWidth;
|
|
954
|
+
inpaintCanvas.height = inpaintImage.naturalHeight;
|
|
955
|
+
redrawInpaintMask();
|
|
956
|
+
}
|
|
957
|
+
updateBrushCursorSize();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function getInpaintPoint(event) {
|
|
961
|
+
const rect = inpaintCanvas.getBoundingClientRect();
|
|
962
|
+
return {
|
|
963
|
+
x: Math.max(0, Math.min(inpaintCanvas.width, (event.clientX - rect.left) * (inpaintCanvas.width / rect.width))),
|
|
964
|
+
y: Math.max(0, Math.min(inpaintCanvas.height, (event.clientY - rect.top) * (inpaintCanvas.height / rect.height)))
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function beginInpaintStroke(event) {
|
|
969
|
+
if (!sourceImageUrl || !inpaintCanvas.width || inpaintSubmit.disabled || sessionLocked) return;
|
|
970
|
+
event.preventDefault();
|
|
971
|
+
inpaintCanvas.setPointerCapture(event.pointerId);
|
|
972
|
+
isInpaintPainting = true;
|
|
973
|
+
currentInpaintStroke = {
|
|
974
|
+
color: activeInpaintColor,
|
|
975
|
+
size: Number(inpaintBrush.value) || 40,
|
|
976
|
+
points: [getInpaintPoint(event)]
|
|
977
|
+
};
|
|
978
|
+
inpaintStrokes.push(currentInpaintStroke);
|
|
979
|
+
redoStrokes = [];
|
|
980
|
+
redrawInpaintMask();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function continueInpaintStroke(event) {
|
|
984
|
+
if (!isInpaintPainting || !currentInpaintStroke) return;
|
|
985
|
+
event.preventDefault();
|
|
986
|
+
currentInpaintStroke.points.push(getInpaintPoint(event));
|
|
987
|
+
redrawInpaintMask();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function endInpaintStroke(event) {
|
|
991
|
+
if (!isInpaintPainting) return;
|
|
992
|
+
event.preventDefault();
|
|
993
|
+
isInpaintPainting = false;
|
|
994
|
+
currentInpaintStroke = null;
|
|
995
|
+
try {
|
|
996
|
+
inpaintCanvas.releasePointerCapture(event.pointerId);
|
|
997
|
+
} catch {}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function addInpaintColor(color) {
|
|
1001
|
+
syncInpaintPromptValuesFromDom();
|
|
1002
|
+
const normalized = pickNextInpaintColor(color);
|
|
1003
|
+
if (!inpaintMaskColors.some((entry) => entry.color === normalized)) {
|
|
1004
|
+
inpaintMaskColors.push({ color: normalized, prompt: "" });
|
|
1005
|
+
}
|
|
1006
|
+
activeInpaintColor = normalized;
|
|
1007
|
+
inpaintColorPicker.value = normalized;
|
|
1008
|
+
renderInpaintControls();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function removeInpaintColor(color) {
|
|
1012
|
+
syncInpaintPromptValuesFromDom();
|
|
1013
|
+
const normalized = normalizeHexColor(color);
|
|
1014
|
+
if (inpaintMaskColors.length <= 1 || !inpaintMaskColors.some((entry) => entry.color === normalized)) {
|
|
1015
|
+
setStatus("Keep at least one mask color.", "error");
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
inpaintMaskColors = inpaintMaskColors.filter((entry) => entry.color !== normalized);
|
|
1019
|
+
inpaintAttachments = inpaintAttachments.filter((attachment) => attachment.scope !== "color" || attachment.color !== normalized);
|
|
1020
|
+
inpaintStrokes = inpaintStrokes.filter((stroke) => normalizeHexColor(stroke.color) !== normalized);
|
|
1021
|
+
redoStrokes = redoStrokes.filter((stroke) => normalizeHexColor(stroke.color) !== normalized);
|
|
1022
|
+
if (activeInpaintColor === normalized) {
|
|
1023
|
+
activeInpaintColor = inpaintMaskColors[0]?.color || "#ff2d55";
|
|
1024
|
+
inpaintColorPicker.value = activeInpaintColor;
|
|
1025
|
+
}
|
|
1026
|
+
redrawInpaintMask();
|
|
1027
|
+
renderInpaintControls();
|
|
1028
|
+
setStatus("", "");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function selectInpaintColor(color) {
|
|
1032
|
+
activeInpaintColor = normalizeHexColor(color);
|
|
1033
|
+
inpaintColorPicker.value = activeInpaintColor;
|
|
1034
|
+
for (const row of Array.from(inpaintPrompts.querySelectorAll(".editor-inpaint-color-row"))) {
|
|
1035
|
+
if (row instanceof HTMLElement) {
|
|
1036
|
+
row.dataset.active = row.dataset.color === activeInpaintColor ? "true" : "false";
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function updateInpaintPrompt(color, prompt) {
|
|
1042
|
+
const normalized = normalizeHexColor(color);
|
|
1043
|
+
inpaintMaskColors = inpaintMaskColors.map((entry) => (
|
|
1044
|
+
entry.color === normalized ? { ...entry, prompt: String(prompt || "") } : entry
|
|
1045
|
+
));
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function buildInpaintInstruction() {
|
|
1049
|
+
const globalPrompt = inpaintGlobalPrompt.value.trim();
|
|
1050
|
+
const regions = inpaintMaskColors
|
|
1051
|
+
.map((entry) => ({ color: entry.color.toUpperCase(), prompt: String(entry.prompt || "").trim() }))
|
|
1052
|
+
.filter((entry) => entry.prompt);
|
|
1053
|
+
if (!globalPrompt && !regions.length) return "";
|
|
1054
|
+
const attachmentNotes = inpaintAttachments.map((attachment, index) => {
|
|
1055
|
+
const label = "reference image " + (index + 1) + " (" + (attachment.fileName || "attachment") + ")";
|
|
1056
|
+
return attachment.scope === "global"
|
|
1057
|
+
? label + " applies to the whole image instruction"
|
|
1058
|
+
: label + " applies only to the UI mask color " + String(attachment.color || "").toUpperCase();
|
|
1059
|
+
});
|
|
1060
|
+
return [
|
|
1061
|
+
"INPAINTING SYSTEM WRAPPER:",
|
|
1062
|
+
"You are editing an existing image with an alpha mask. Treat transparent mask pixels as the only editable region. Everything outside the editable mask must remain the same image: preserve identity, layout, pose, camera angle, framing, background, lighting, colors, textures, text, logos, and all unmasked details.",
|
|
1063
|
+
"Do not redraw, reinterpret, beautify, crop, zoom, rotate, expand, stylize, or otherwise alter unmasked content. Do not render the UI mask colors; they are labels only.",
|
|
1064
|
+
"The final image must keep the original dimensions and aspect ratio unless the user explicitly asks for a dimension change.",
|
|
1065
|
+
globalPrompt ? "Whole image prompt: " + globalPrompt + ". Apply this only as guidance for the masked edit and overall consistency; it does not permit changing unmasked content." : "",
|
|
1066
|
+
regions.map((entry) => (
|
|
1067
|
+
"Masked region " + entry.color + ": replace only that marked area with " + entry.prompt + ". Match the surrounding perspective, lighting, grain, shadows, and edge blending."
|
|
1068
|
+
)).join(" "),
|
|
1069
|
+
attachmentNotes.length ? "Reference image scope: " + attachmentNotes.join("; ") + ". Use each reference only for its stated scope and do not copy unrelated reference details into unmasked areas." : ""
|
|
1070
|
+
].filter(Boolean).join(" ");
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function getInpaintAspectRatio() {
|
|
1074
|
+
const width = inpaintCanvas.width || inpaintImage.naturalWidth || 0;
|
|
1075
|
+
const height = inpaintCanvas.height || inpaintImage.naturalHeight || 0;
|
|
1076
|
+
if (!width || !height) return undefined;
|
|
1077
|
+
const ratio = width / height;
|
|
1078
|
+
if (Math.abs(ratio - 1) < 0.08) return "1:1";
|
|
1079
|
+
if (ratio > 1) return ratio >= 1.55 ? "16:9" : "4:3";
|
|
1080
|
+
return ratio <= 0.65 ? "9:16" : "3:4";
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function canvasToPngBlob(canvas) {
|
|
1084
|
+
return new Promise((resolve, reject) => {
|
|
1085
|
+
canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("Unable to prepare image.")), "image/png");
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function createProviderMaskBlob() {
|
|
1090
|
+
const maskCanvas = document.createElement("canvas");
|
|
1091
|
+
maskCanvas.width = inpaintCanvas.width;
|
|
1092
|
+
maskCanvas.height = inpaintCanvas.height;
|
|
1093
|
+
const ctx = maskCanvas.getContext("2d");
|
|
1094
|
+
if (!ctx) throw new Error("Unable to prepare mask image.");
|
|
1095
|
+
ctx.fillStyle = "#ffffff";
|
|
1096
|
+
ctx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
|
|
1097
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1098
|
+
ctx.lineCap = "round";
|
|
1099
|
+
ctx.lineJoin = "round";
|
|
1100
|
+
for (const stroke of inpaintStrokes) {
|
|
1101
|
+
if (!stroke.points || stroke.points.length < 1) continue;
|
|
1102
|
+
ctx.strokeStyle = "rgba(0, 0, 0, 1)";
|
|
1103
|
+
ctx.lineWidth = stroke.size;
|
|
1104
|
+
ctx.beginPath();
|
|
1105
|
+
ctx.moveTo(stroke.points[0].x, stroke.points[0].y);
|
|
1106
|
+
if (stroke.points.length === 1) {
|
|
1107
|
+
ctx.lineTo(stroke.points[0].x + 0.01, stroke.points[0].y + 0.01);
|
|
1108
|
+
} else {
|
|
1109
|
+
for (let index = 1; index < stroke.points.length; index += 1) {
|
|
1110
|
+
ctx.lineTo(stroke.points[index].x, stroke.points[index].y);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
ctx.stroke();
|
|
1114
|
+
}
|
|
1115
|
+
return canvasToPngBlob(maskCanvas);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function parseJsonResponse(response) {
|
|
1119
|
+
return response.json().catch(() => ({}));
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function loadInpaintProviders() {
|
|
1123
|
+
try {
|
|
1124
|
+
const response = await fetch("/api/v1/user/me/provider-keys", { credentials: "same-origin" });
|
|
1125
|
+
const payload = await parseJsonResponse(response);
|
|
1126
|
+
if (!response.ok) return;
|
|
1127
|
+
const supported = new Set(["openai", "gemini", "openrouter"]);
|
|
1128
|
+
const providers = [];
|
|
1129
|
+
const seen = new Set();
|
|
1130
|
+
for (const key of Array.isArray(payload.provider_keys) ? payload.provider_keys : []) {
|
|
1131
|
+
const provider = String(key.provider || "");
|
|
1132
|
+
const status = String(key.status || "active");
|
|
1133
|
+
const cooldownUntil = key.cooldown_until ? Date.parse(String(key.cooldown_until)) : 0;
|
|
1134
|
+
if (!supported.has(provider) || status !== "active" || (cooldownUntil && cooldownUntil > Date.now()) || seen.has(provider)) {
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
seen.add(provider);
|
|
1138
|
+
providers.push(provider);
|
|
1139
|
+
}
|
|
1140
|
+
inpaintProviderSelect.innerHTML = '<option value="">Auto</option>' + providers.map((provider) => (
|
|
1141
|
+
'<option value="' + provider + '">' + provider.charAt(0).toUpperCase() + provider.slice(1) + '</option>'
|
|
1142
|
+
)).join("");
|
|
1143
|
+
inpaintProviderWrap.setAttribute("data-visible", providers.length > 1 ? "true" : "false");
|
|
1144
|
+
} catch {}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
async function uploadTemporaryBlob(blob, fileName, contentType, folderPath) {
|
|
1148
|
+
// All inpaint temp artifacts live under a single top-level "inpaints"
|
|
1149
|
+
// folder so the file explorer shows ONE session tree, never a confusing
|
|
1150
|
+
// "inpaint" (inputs) + "inpaints" (outputs) pair. Intermediate inputs
|
|
1151
|
+
// (source uploads, alpha masks, reference images) go to inpaints/_inputs;
|
|
1152
|
+
// finished outputs are mirrored into inpaints/<tracer>/ by their callers.
|
|
1153
|
+
const folder = folderPath || "inpaints/_inputs";
|
|
1154
|
+
const presignResponse = await fetch("/api/v1/user/me/temporary-files/presign", {
|
|
1155
|
+
method: "POST",
|
|
1156
|
+
credentials: "same-origin",
|
|
1157
|
+
headers: { "content-type": "application/json" },
|
|
1158
|
+
body: JSON.stringify({
|
|
1159
|
+
file_name: fileName,
|
|
1160
|
+
content_type: contentType,
|
|
1161
|
+
size_bytes: blob.size,
|
|
1162
|
+
folder_path: folder
|
|
1163
|
+
})
|
|
1164
|
+
});
|
|
1165
|
+
const presignPayload = await parseJsonResponse(presignResponse);
|
|
1166
|
+
if (!presignResponse.ok || !presignPayload || !presignPayload.transport) {
|
|
1167
|
+
throw new Error(presignPayload && presignPayload.error ? presignPayload.error : "Unable to prepare upload.");
|
|
1168
|
+
}
|
|
1169
|
+
if (presignPayload.transport === "server") {
|
|
1170
|
+
const formData = new FormData();
|
|
1171
|
+
formData.append(presignPayload.upload?.form_field || "file", new File([blob], fileName, { type: contentType }));
|
|
1172
|
+
formData.append("folder_path", folder);
|
|
1173
|
+
const uploadResponse = await fetch(presignPayload.upload?.url || "/api/v1/user/me/temporary-files/upload", {
|
|
1174
|
+
method: presignPayload.upload?.method || "POST",
|
|
1175
|
+
credentials: "same-origin",
|
|
1176
|
+
body: formData
|
|
1177
|
+
});
|
|
1178
|
+
const uploadPayload = await parseJsonResponse(uploadResponse);
|
|
1179
|
+
if (!uploadResponse.ok || !uploadPayload.file) {
|
|
1180
|
+
throw new Error(uploadPayload.error || "Unable to upload file.");
|
|
1181
|
+
}
|
|
1182
|
+
return uploadPayload.file.viewUrl || uploadPayload.file.view_url || uploadPayload.file.s3Url || uploadPayload.file.s3_url;
|
|
1183
|
+
}
|
|
1184
|
+
const uploadResponse = await fetch(presignPayload.upload.url, {
|
|
1185
|
+
method: presignPayload.upload.method,
|
|
1186
|
+
headers: presignPayload.upload.headers,
|
|
1187
|
+
body: blob
|
|
1188
|
+
});
|
|
1189
|
+
if (!uploadResponse.ok) throw new Error("Upload failed with status " + uploadResponse.status + ".");
|
|
1190
|
+
const finalizeResponse = await fetch("/api/v1/user/me/temporary-files", {
|
|
1191
|
+
method: "POST",
|
|
1192
|
+
credentials: "same-origin",
|
|
1193
|
+
headers: { "content-type": "application/json" },
|
|
1194
|
+
body: JSON.stringify({
|
|
1195
|
+
file_id: presignPayload.file_id,
|
|
1196
|
+
file_name: presignPayload.file_name,
|
|
1197
|
+
content_type: presignPayload.content_type,
|
|
1198
|
+
size_bytes: blob.size,
|
|
1199
|
+
storage_key: presignPayload.storage_key,
|
|
1200
|
+
folder_path: presignPayload.folder_path || folder
|
|
1201
|
+
})
|
|
1202
|
+
});
|
|
1203
|
+
const finalizePayload = await parseJsonResponse(finalizeResponse);
|
|
1204
|
+
if (!finalizeResponse.ok || !finalizePayload.file) {
|
|
1205
|
+
throw new Error(finalizePayload.error || "Unable to save file.");
|
|
1206
|
+
}
|
|
1207
|
+
return finalizePayload.file.viewUrl || finalizePayload.file.view_url || finalizePayload.file.s3Url || finalizePayload.file.s3_url;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function absoluteUrl(value) {
|
|
1211
|
+
return new URL(value, window.location.href).toString();
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Save a finished media output into My Files: pick a destination folder
|
|
1215
|
+
// with the shared file-directory pop-panel, then ingest the URL
|
|
1216
|
+
// server-side (handles cross-origin S3 objects). "/files" only.
|
|
1217
|
+
function saveMediaToFiles(mediaUrl, defaultName) {
|
|
1218
|
+
if (!mediaUrl) return;
|
|
1219
|
+
var dir = window.__vidfarmDirectory;
|
|
1220
|
+
if (!dir || !dir.pick) { setStatus("File browser isn't available right now.", "error"); return; }
|
|
1221
|
+
dir.pick({
|
|
1222
|
+
select: "folder",
|
|
1223
|
+
roots: ["files"],
|
|
1224
|
+
initialPath: "/files",
|
|
1225
|
+
title: "Save to My Files",
|
|
1226
|
+
hint: "Open a folder in your Files, then choose it to save this output there.",
|
|
1227
|
+
confirmLabel: "Save here",
|
|
1228
|
+
onPick: function(folder){
|
|
1229
|
+
setStatus("Saving to " + (folder && folder.path ? folder.path : "My Files") + "\\u2026", "");
|
|
1230
|
+
fetch("/api/v1/user/me/attachments/from-url", {
|
|
1231
|
+
method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" },
|
|
1232
|
+
body: JSON.stringify({ source_url: mediaUrl, folder_path: (folder && folder.folderPath) || "", file_name: defaultName || "" })
|
|
1233
|
+
}).then(function(r){ return r.json().then(function(j){ return { ok: r.ok, j: j }; }); })
|
|
1234
|
+
.then(function(res){
|
|
1235
|
+
if(!res.ok || !res.j || res.j.error){ throw new Error((res.j && res.j.error) || "Couldn't save to My Files."); }
|
|
1236
|
+
setStatus("Saved to " + (folder && folder.path ? folder.path : "My Files") + ".", "success");
|
|
1237
|
+
})
|
|
1238
|
+
.catch(function(err){ setStatus(err instanceof Error ? err.message : "Couldn't save to My Files.", "error"); });
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function firstSearchParam(names) {
|
|
1244
|
+
const params = new URLSearchParams(window.location.search);
|
|
1245
|
+
for (const name of names) {
|
|
1246
|
+
const value = params.get(name);
|
|
1247
|
+
if (value && value.trim()) return value.trim();
|
|
1248
|
+
}
|
|
1249
|
+
return "";
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function sourceNameFromUrl(value) {
|
|
1253
|
+
try {
|
|
1254
|
+
const pathName = new URL(value, window.location.href).pathname;
|
|
1255
|
+
const fileName = decodeURIComponent(pathName.split("/").filter(Boolean).pop() || "");
|
|
1256
|
+
return fileName || "Source image";
|
|
1257
|
+
} catch {
|
|
1258
|
+
return "Source image";
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function loadSourceUrl(value, origin) {
|
|
1263
|
+
if (!value) return;
|
|
1264
|
+
let url = "";
|
|
1265
|
+
try {
|
|
1266
|
+
url = absoluteUrl(value);
|
|
1267
|
+
} catch {
|
|
1268
|
+
setStatus("The source image URL is invalid.", "error");
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
setSessionLocked(false);
|
|
1272
|
+
sourceImageUrl = url;
|
|
1273
|
+
sourceImageName = firstSearchParam(["source_image_name", "image_name", "file_name"]) || sourceNameFromUrl(url);
|
|
1274
|
+
sourceName.textContent = sourceImageName;
|
|
1275
|
+
emptyState.hidden = true;
|
|
1276
|
+
canvasWrap.hidden = false;
|
|
1277
|
+
outputPanel.hidden = true;
|
|
1278
|
+
resultImage.removeAttribute("src");
|
|
1279
|
+
resetMask();
|
|
1280
|
+
inpaintImage.onload = () => {
|
|
1281
|
+
syncInpaintCanvasSize();
|
|
1282
|
+
resetMask();
|
|
1283
|
+
};
|
|
1284
|
+
inpaintImage.src = sourceImageUrl;
|
|
1285
|
+
recordSourceHistoryItem(sourceImageUrl, origin || "url", sourceImageName);
|
|
1286
|
+
setStatus("Source ready from URL.", "success");
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function clearInpaintUrlParams() {
|
|
1290
|
+
const url = new URL(window.location.href);
|
|
1291
|
+
for (const key of ["tracer", "tracer_id", "trace", "source_image_url", "image_url", "input_image_url", "image", "source_image_name", "image_name", "file_name"]) {
|
|
1292
|
+
url.searchParams.delete(key);
|
|
1293
|
+
}
|
|
1294
|
+
window.history.replaceState({}, "", url.toString());
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function initializeFromUrl() {
|
|
1298
|
+
const urlTracer = firstSearchParam(["tracer", "tracer_id", "trace"]);
|
|
1299
|
+
if (urlTracer) {
|
|
1300
|
+
sessionTracer = urlTracer;
|
|
1301
|
+
tracerInput.value = urlTracer;
|
|
1302
|
+
tracerAutoPending = false;
|
|
1303
|
+
} else {
|
|
1304
|
+
sessionTracer = "";
|
|
1305
|
+
tracerInput.value = "";
|
|
1306
|
+
tracerAutoPending = true;
|
|
1307
|
+
}
|
|
1308
|
+
loadSourceUrl(firstSearchParam(["source_image_url", "image_url", "input_image_url", "image"]), "url");
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
async function addAttachmentFilesForScope(files, scope) {
|
|
1312
|
+
if (!scope) return;
|
|
1313
|
+
const selected = Array.from(files || []);
|
|
1314
|
+
if (!selected.length) return;
|
|
1315
|
+
if (inpaintAttachments.length + selected.length > maxInpaintAttachments) {
|
|
1316
|
+
setStatus("Use up to " + maxInpaintAttachments + " reference images.", "error");
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const previousDisabled = inpaintSubmit.disabled;
|
|
1320
|
+
inpaintSubmit.disabled = true;
|
|
1321
|
+
setStatus("Uploading reference image...", "");
|
|
1322
|
+
try {
|
|
1323
|
+
for (const file of selected) {
|
|
1324
|
+
if (!isAllowedInpaintAttachment(file)) {
|
|
1325
|
+
throw new Error("Reference attachments must be JPEG, PNG, or WebP images.");
|
|
1326
|
+
}
|
|
1327
|
+
const fileName = file.name || "reference.png";
|
|
1328
|
+
const uploadedUrl = await uploadTemporaryBlob(file, fileName, file.type || "image/png");
|
|
1329
|
+
inpaintAttachments.push({
|
|
1330
|
+
id: attachmentId(),
|
|
1331
|
+
scope: scope.scope === "color" ? "color" : "global",
|
|
1332
|
+
color: scope.scope === "color" ? normalizeHexColor(scope.color) : "",
|
|
1333
|
+
fileName,
|
|
1334
|
+
url: absoluteUrl(uploadedUrl)
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
renderAllAttachmentChips();
|
|
1338
|
+
setStatus("Reference image ready.", "success");
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
console.error(error);
|
|
1341
|
+
setStatus(error instanceof Error ? error.message : "Unable to upload reference image.", "error");
|
|
1342
|
+
} finally {
|
|
1343
|
+
inpaintSubmit.disabled = previousDisabled;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
async function addAttachmentFiles(files) {
|
|
1348
|
+
const scope = pendingInpaintAttachmentScope;
|
|
1349
|
+
pendingInpaintAttachmentScope = null;
|
|
1350
|
+
await addAttachmentFilesForScope(files, scope);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function resetMask() {
|
|
1354
|
+
inpaintStrokes = [];
|
|
1355
|
+
redoStrokes = [];
|
|
1356
|
+
currentInpaintStroke = null;
|
|
1357
|
+
isInpaintPainting = false;
|
|
1358
|
+
redrawInpaintMask();
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function resetWorkspace() {
|
|
1362
|
+
sessionLocked = false;
|
|
1363
|
+
sourceImageUrl = "";
|
|
1364
|
+
sourceImageName = "";
|
|
1365
|
+
sourceName.textContent = "No image selected";
|
|
1366
|
+
emptyState.hidden = false;
|
|
1367
|
+
canvasWrap.hidden = true;
|
|
1368
|
+
outputPanel.hidden = true;
|
|
1369
|
+
resultImage.removeAttribute("src");
|
|
1370
|
+
inpaintImage.removeAttribute("src");
|
|
1371
|
+
inpaintCanvas.width = 1;
|
|
1372
|
+
inpaintCanvas.height = 1;
|
|
1373
|
+
inpaintGlobalPrompt.value = "";
|
|
1374
|
+
inpaintMaskColors = [{ color: "#ff2d55", prompt: "" }];
|
|
1375
|
+
inpaintAttachments = [];
|
|
1376
|
+
pendingInpaintAttachmentScope = null;
|
|
1377
|
+
activeInpaintColor = "#ff2d55";
|
|
1378
|
+
inpaintColorPicker.value = activeInpaintColor;
|
|
1379
|
+
sourceHistoryItems = [];
|
|
1380
|
+
resetMask();
|
|
1381
|
+
renderInpaintControls();
|
|
1382
|
+
setStatus("", "");
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
async function loadSourceFile(file) {
|
|
1386
|
+
if (!file || !file.type.startsWith("image/")) {
|
|
1387
|
+
setStatus("Choose an image file.", "error");
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
uploadButton.disabled = true;
|
|
1391
|
+
inpaintSubmit.disabled = true;
|
|
1392
|
+
setStatus("Uploading source image...", "");
|
|
1393
|
+
try {
|
|
1394
|
+
const fileName = file.name || "source.png";
|
|
1395
|
+
// Name the session up-front (when still auto-pending) so the source upload
|
|
1396
|
+
// lands in this session's folder, then unlock any prior generated edit.
|
|
1397
|
+
const wasAutoPending = tracerAutoPending;
|
|
1398
|
+
if (wasAutoPending) nameTracerFromSeed(fileName.replace(/\\.[a-z0-9]+$/i, ""));
|
|
1399
|
+
const uploadedUrl = await uploadTemporaryBlob(file, fileName, file.type || "image/png", "inpaints/" + getTracerValue());
|
|
1400
|
+
setSessionLocked(false);
|
|
1401
|
+
sourceImageUrl = absoluteUrl(uploadedUrl);
|
|
1402
|
+
sourceImageName = fileName;
|
|
1403
|
+
sourceName.textContent = fileName;
|
|
1404
|
+
emptyState.hidden = true;
|
|
1405
|
+
canvasWrap.hidden = false;
|
|
1406
|
+
outputPanel.hidden = true;
|
|
1407
|
+
resultImage.removeAttribute("src");
|
|
1408
|
+
resetMask();
|
|
1409
|
+
inpaintImage.onload = () => {
|
|
1410
|
+
syncInpaintCanvasSize();
|
|
1411
|
+
resetMask();
|
|
1412
|
+
};
|
|
1413
|
+
inpaintImage.src = sourceImageUrl;
|
|
1414
|
+
if (wasAutoPending) {
|
|
1415
|
+
void loadInpaintHistory();
|
|
1416
|
+
void loadTracerSessions();
|
|
1417
|
+
}
|
|
1418
|
+
recordSourceHistoryItem(sourceImageUrl, "upload", fileName);
|
|
1419
|
+
setStatus("Source ready.", "success");
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
console.error(error);
|
|
1422
|
+
setStatus(error instanceof Error ? error.message : "Unable to upload source image.", "error");
|
|
1423
|
+
} finally {
|
|
1424
|
+
uploadButton.disabled = false;
|
|
1425
|
+
inpaintSubmit.disabled = false;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function extractOutputUrl(payload) {
|
|
1430
|
+
const result = payload && payload.result ? payload.result : {};
|
|
1431
|
+
return result.output?.image?.file_url
|
|
1432
|
+
|| result.output?.primary_file_url
|
|
1433
|
+
|| result.image?.file_url
|
|
1434
|
+
|| result.primary_file_url
|
|
1435
|
+
|| (Array.isArray(result.output?.files) ? result.output.files[0] : "")
|
|
1436
|
+
|| (Array.isArray(result.files) ? result.files[0] : "");
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function isPendingJobStatus(status) {
|
|
1440
|
+
return ["queued", "running", "waiting_for_provider", "waiting_for_child", "waiting_for_human"].includes(String(status || ""));
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function pendingHistoryItemKey(jobId) {
|
|
1444
|
+
return "job:" + String(jobId || "");
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function makePendingInpaintHistoryItem(input) {
|
|
1448
|
+
const jobId = String(input?.jobId || input?.job_id || "");
|
|
1449
|
+
if (!jobId) return null;
|
|
1450
|
+
return {
|
|
1451
|
+
key: pendingHistoryItemKey(jobId),
|
|
1452
|
+
kind: "pending",
|
|
1453
|
+
jobId,
|
|
1454
|
+
status: String(input?.status || "queued"),
|
|
1455
|
+
progress: Number(input?.progress || 0),
|
|
1456
|
+
createdAt: String(input?.createdAt || input?.created_at || new Date().toISOString()),
|
|
1457
|
+
updatedAt: String(input?.updatedAt || input?.updated_at || new Date().toISOString())
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function upsertInpaintHistoryItem(item) {
|
|
1462
|
+
if (!item || !item.key) return;
|
|
1463
|
+
inpaintHistoryItems = inpaintHistoryItems.filter((entry) => (
|
|
1464
|
+
entry.key !== item.key
|
|
1465
|
+
&& (!item.jobId || entry.jobId !== item.jobId)
|
|
1466
|
+
));
|
|
1467
|
+
inpaintHistoryItems.unshift(item);
|
|
1468
|
+
inpaintHistoryItems.sort((a, b) => Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0));
|
|
1469
|
+
renderInpaintHistory();
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Record the image a session started from (upload / generation / deep-link) so
|
|
1473
|
+
// it appears in History. De-duplicated by URL; kept in sourceHistoryItems so a
|
|
1474
|
+
// subsequent server-history reload doesn't drop it.
|
|
1475
|
+
function recordSourceHistoryItem(url, origin, name) {
|
|
1476
|
+
if (!url) return;
|
|
1477
|
+
let abs = "";
|
|
1478
|
+
try { abs = absoluteUrl(url); } catch { return; }
|
|
1479
|
+
const key = "source:" + abs;
|
|
1480
|
+
let item = sourceHistoryItems.find((entry) => entry.key === key);
|
|
1481
|
+
if (!item) {
|
|
1482
|
+
item = {
|
|
1483
|
+
key,
|
|
1484
|
+
kind: "source",
|
|
1485
|
+
origin: origin || "upload",
|
|
1486
|
+
url: abs,
|
|
1487
|
+
name: name || sourceNameFromUrl(abs),
|
|
1488
|
+
createdAt: new Date().toISOString()
|
|
1489
|
+
};
|
|
1490
|
+
sourceHistoryItems.push(item);
|
|
1491
|
+
}
|
|
1492
|
+
upsertInpaintHistoryItem(item);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function reinjectSourceHistoryItems() {
|
|
1496
|
+
for (const item of sourceHistoryItems) {
|
|
1497
|
+
if (!inpaintHistoryItems.some((entry) => entry.key === item.key)) {
|
|
1498
|
+
inpaintHistoryItems.push(item);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
inpaintHistoryItems.sort((a, b) => Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0));
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Mirror each completed inpaint output into /temp/inpaints/<tracer>/ so a
|
|
1505
|
+
// session's outputs are browsable as files (grouped by the same tracer that
|
|
1506
|
+
// groups the History strip). Best-effort + de-duplicated; never blocks the UI.
|
|
1507
|
+
const persistedInpaintOutputs = new Set();
|
|
1508
|
+
async function persistInpaintOutputToTemp(url, jobId) {
|
|
1509
|
+
const dedupeKey = String(jobId || url || "");
|
|
1510
|
+
if (!url || persistedInpaintOutputs.has(dedupeKey)) return;
|
|
1511
|
+
persistedInpaintOutputs.add(dedupeKey);
|
|
1512
|
+
try {
|
|
1513
|
+
const response = await fetch(url, { credentials: "same-origin" });
|
|
1514
|
+
if (!response.ok) throw new Error("fetch " + response.status);
|
|
1515
|
+
const blob = await response.blob();
|
|
1516
|
+
const contentType = blob.type || "image/png";
|
|
1517
|
+
const ext = contentType.includes("jpeg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
|
|
1518
|
+
const fileName = "inpaint-output-" + (jobId || Date.now().toString(36)) + "." + ext;
|
|
1519
|
+
await uploadTemporaryBlob(blob, fileName, contentType, "inpaints/" + getTracerValue());
|
|
1520
|
+
void loadInpaintHistory();
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
persistedInpaintOutputs.delete(dedupeKey);
|
|
1523
|
+
console.warn("Unable to mirror inpaint output to temp files.", error);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function applyCompletedInpaintJob(payload) {
|
|
1528
|
+
const outputUrl = extractOutputUrl(payload);
|
|
1529
|
+
if (!outputUrl) return "";
|
|
1530
|
+
const url = absoluteUrl(outputUrl);
|
|
1531
|
+
const jobId = String(payload?.job_id || payload?.jobId || "");
|
|
1532
|
+
resultImage.src = url;
|
|
1533
|
+
outputPanel.hidden = false;
|
|
1534
|
+
setSessionLocked(true);
|
|
1535
|
+
void persistInpaintOutputToTemp(url, jobId);
|
|
1536
|
+
upsertInpaintHistoryItem({
|
|
1537
|
+
key: historyItemKey({ jobId: jobId || "inpaint" }, url, 0),
|
|
1538
|
+
kind: "output",
|
|
1539
|
+
jobId,
|
|
1540
|
+
url,
|
|
1541
|
+
createdAt: String(payload?.completed_at || payload?.updated_at || new Date().toISOString())
|
|
1542
|
+
});
|
|
1543
|
+
return url;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function historyEntryUrls(entry) {
|
|
1547
|
+
const urls = Array.isArray(entry?.outputMediaUrls) ? entry.outputMediaUrls : [];
|
|
1548
|
+
return urls.filter((url) => {
|
|
1549
|
+
const value = String(url || "");
|
|
1550
|
+
if (!/\\.(png|jpe?g|webp|gif|avif)(\\?|#|$)/i.test(value)) {
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
const parsed = new URL(value, window.location.href);
|
|
1555
|
+
const storageKey = parsed.searchParams.get("key") || decodeURIComponent(parsed.pathname).replace(/^\\/+/, "");
|
|
1556
|
+
return !/\\/temporary\\/inpaints\\/_inputs(?:\\/|$)/i.test(storageKey)
|
|
1557
|
+
&& !/\\/temporary\\/inpaint(?:-|\\/|$)/i.test(storageKey)
|
|
1558
|
+
&& !/\\/temporary\\/inpaint-masks(?:\\/|$)/i.test(storageKey)
|
|
1559
|
+
&& !/inpaint-mask-[^/]+\\.png/i.test(storageKey);
|
|
1560
|
+
} catch {
|
|
1561
|
+
return !/\\/temporary\\/inpaints\\/_inputs(?:\\/|$)/i.test(value)
|
|
1562
|
+
&& !/\\/temporary\\/inpaint(?:-|\\/|$)/i.test(value)
|
|
1563
|
+
&& !/\\/temporary\\/inpaint-masks(?:\\/|$)/i.test(value)
|
|
1564
|
+
&& !/inpaint-mask-[^/]+\\.png/i.test(value);
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function isInpaintHistoryEntry(entry) {
|
|
1570
|
+
const templateId = String(entry?.linkedJob?.template_id || entry?.linkedJob?.templateId || "").toLowerCase();
|
|
1571
|
+
const path = String(entry?.path || "").toLowerCase();
|
|
1572
|
+
return templateId === "primitive:image_inpaint"
|
|
1573
|
+
|| path.includes("/primitives/images/inpaint")
|
|
1574
|
+
|| path.includes("/primitives/image_inpaint");
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function historyItemKey(entry, url, index) {
|
|
1578
|
+
return String(entry?.jobId || entry?.job_id || entry?.id || "inpaint") + ":" + index + ":" + String(url || "");
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function setHistoryEmpty(message) {
|
|
1582
|
+
historyList.innerHTML = "";
|
|
1583
|
+
const empty = document.createElement("div");
|
|
1584
|
+
empty.className = "custom-inpaint-history-empty";
|
|
1585
|
+
empty.textContent = message;
|
|
1586
|
+
historyList.append(empty);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function renderInpaintHistory() {
|
|
1590
|
+
historyList.innerHTML = "";
|
|
1591
|
+
const visible = inpaintHistoryItems.filter((item) => !hiddenInpaintHistoryKeys.has(item.key));
|
|
1592
|
+
if (!visible.length) {
|
|
1593
|
+
setHistoryEmpty("No inpaint outputs yet.");
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
for (const item of visible) {
|
|
1597
|
+
const row = document.createElement("div");
|
|
1598
|
+
row.className = "custom-inpaint-history-item";
|
|
1599
|
+
row.dataset.historyKey = item.key;
|
|
1600
|
+
row.dataset.kind = item.kind;
|
|
1601
|
+
let thumbButton;
|
|
1602
|
+
if (item.kind === "pending") {
|
|
1603
|
+
thumbButton = document.createElement("div");
|
|
1604
|
+
thumbButton.className = "custom-inpaint-history-generating";
|
|
1605
|
+
thumbButton.setAttribute("aria-label", "Image edit generating");
|
|
1606
|
+
thumbButton.setAttribute("role", "img");
|
|
1607
|
+
} else {
|
|
1608
|
+
thumbButton = document.createElement("button");
|
|
1609
|
+
thumbButton.type = "button";
|
|
1610
|
+
thumbButton.className = "custom-inpaint-history-thumb-button";
|
|
1611
|
+
thumbButton.dataset.historyPreview = item.key;
|
|
1612
|
+
thumbButton.setAttribute("aria-label", "View inpaint output");
|
|
1613
|
+
const thumb = document.createElement("img");
|
|
1614
|
+
thumb.className = "custom-inpaint-history-thumb";
|
|
1615
|
+
thumb.src = item.url;
|
|
1616
|
+
thumb.alt = "Image edit output";
|
|
1617
|
+
thumb.loading = "lazy";
|
|
1618
|
+
thumbButton.append(thumb);
|
|
1619
|
+
}
|
|
1620
|
+
const meta = document.createElement("div");
|
|
1621
|
+
meta.className = "custom-inpaint-history-meta";
|
|
1622
|
+
const title = document.createElement("div");
|
|
1623
|
+
title.className = "custom-inpaint-history-title";
|
|
1624
|
+
title.textContent = item.kind === "pending"
|
|
1625
|
+
? "Generating inpaint"
|
|
1626
|
+
: item.kind === "source"
|
|
1627
|
+
? (item.origin === "generated" ? "Generated starting image"
|
|
1628
|
+
: item.origin === "upload" ? "Uploaded starting image"
|
|
1629
|
+
: "Starting image")
|
|
1630
|
+
: (item.createdAt ? new Date(item.createdAt).toLocaleString() : "Image edit output");
|
|
1631
|
+
if (item.kind === "pending") {
|
|
1632
|
+
const subtitle = document.createElement("div");
|
|
1633
|
+
subtitle.className = "custom-inpaint-history-subtitle";
|
|
1634
|
+
const progress = Number(item.progress || 0);
|
|
1635
|
+
subtitle.textContent = "Job " + (item.status || "queued") + (progress > 0 ? " · " + Math.round(progress * 100) + "%" : "");
|
|
1636
|
+
meta.append(title, subtitle);
|
|
1637
|
+
} else if (item.kind === "source") {
|
|
1638
|
+
const subtitle = document.createElement("div");
|
|
1639
|
+
subtitle.className = "custom-inpaint-history-subtitle";
|
|
1640
|
+
subtitle.textContent = "Where you started" + (item.createdAt ? " · " + new Date(item.createdAt).toLocaleString() : "");
|
|
1641
|
+
meta.append(title, subtitle);
|
|
1642
|
+
} else {
|
|
1643
|
+
meta.append(title);
|
|
1644
|
+
}
|
|
1645
|
+
const actions = document.createElement("div");
|
|
1646
|
+
actions.className = "custom-inpaint-history-actions";
|
|
1647
|
+
if (item.kind === "pending") {
|
|
1648
|
+
const check = document.createElement("button");
|
|
1649
|
+
check.type = "button";
|
|
1650
|
+
check.className = "custom-inpaint-history-check";
|
|
1651
|
+
check.dataset.historyCheck = item.jobId || "";
|
|
1652
|
+
check.textContent = "Check Status";
|
|
1653
|
+
actions.append(check);
|
|
1654
|
+
} else {
|
|
1655
|
+
const download = document.createElement("a");
|
|
1656
|
+
download.className = "custom-inpaint-history-icon";
|
|
1657
|
+
download.href = item.url;
|
|
1658
|
+
download.target = "_blank";
|
|
1659
|
+
download.rel = "noopener noreferrer";
|
|
1660
|
+
download.download = imageDownloadName("inpaint-history", item.createdAt);
|
|
1661
|
+
download.setAttribute("aria-label", "Download inpaint output");
|
|
1662
|
+
download.setAttribute("title", "Download");
|
|
1663
|
+
download.innerHTML = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 3v9"></path><path d="m6.5 8.5 3.5 3.5 3.5-3.5"></path><path d="M4 16h12"></path></svg>';
|
|
1664
|
+
const copy = document.createElement("button");
|
|
1665
|
+
copy.type = "button";
|
|
1666
|
+
copy.className = "custom-inpaint-history-icon";
|
|
1667
|
+
copy.dataset.historyCopy = item.key;
|
|
1668
|
+
copy.setAttribute("aria-label", "Copy inpaint output URL");
|
|
1669
|
+
copy.setAttribute("title", "Copy URL");
|
|
1670
|
+
copy.innerHTML = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1"></path><path d="M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2"></path></svg>';
|
|
1671
|
+
const save = document.createElement("button");
|
|
1672
|
+
save.type = "button";
|
|
1673
|
+
save.className = "custom-inpaint-history-icon";
|
|
1674
|
+
save.dataset.historySave = item.key;
|
|
1675
|
+
save.setAttribute("aria-label", "Save this output to My Files");
|
|
1676
|
+
save.setAttribute("title", "Save to Files");
|
|
1677
|
+
save.innerHTML = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 4h8l2 2v10H5z"></path><path d="M8 4v4h4V4"></path><path d="M8 12h4"></path></svg>';
|
|
1678
|
+
actions.append(download, copy, save);
|
|
1679
|
+
}
|
|
1680
|
+
const remove = document.createElement("button");
|
|
1681
|
+
remove.type = "button";
|
|
1682
|
+
remove.className = "custom-inpaint-history-icon";
|
|
1683
|
+
remove.dataset.historyRemove = item.key;
|
|
1684
|
+
remove.setAttribute("aria-label", "Remove inpaint output from history strip");
|
|
1685
|
+
remove.setAttribute("title", "Remove");
|
|
1686
|
+
remove.innerHTML = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 5l10 10"></path><path d="M15 5 5 15"></path></svg>';
|
|
1687
|
+
actions.append(remove);
|
|
1688
|
+
meta.append(actions);
|
|
1689
|
+
const confirm = document.createElement("div");
|
|
1690
|
+
confirm.className = "custom-inpaint-history-popconfirm";
|
|
1691
|
+
confirm.hidden = true;
|
|
1692
|
+
confirm.dataset.historyConfirm = item.key;
|
|
1693
|
+
const confirmText = document.createElement("p");
|
|
1694
|
+
confirmText.textContent = "Remove from this history strip?";
|
|
1695
|
+
const confirmActions = document.createElement("div");
|
|
1696
|
+
confirmActions.className = "custom-inpaint-history-popconfirm-actions";
|
|
1697
|
+
const cancel = document.createElement("button");
|
|
1698
|
+
cancel.type = "button";
|
|
1699
|
+
cancel.dataset.historyCancel = item.key;
|
|
1700
|
+
cancel.textContent = "Cancel";
|
|
1701
|
+
const confirmRemove = document.createElement("button");
|
|
1702
|
+
confirmRemove.type = "button";
|
|
1703
|
+
confirmRemove.dataset.historyConfirmRemove = item.key;
|
|
1704
|
+
confirmRemove.dataset.danger = "true";
|
|
1705
|
+
confirmRemove.textContent = "Remove";
|
|
1706
|
+
confirmActions.append(cancel, confirmRemove);
|
|
1707
|
+
confirm.append(confirmText, confirmActions);
|
|
1708
|
+
row.append(thumbButton, meta, confirm);
|
|
1709
|
+
historyList.append(row);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function closeInpaintHistoryConfirms(exceptKey) {
|
|
1714
|
+
historyList.querySelectorAll("[data-history-confirm]").forEach((entry) => {
|
|
1715
|
+
if (!(entry instanceof HTMLElement)) return;
|
|
1716
|
+
entry.hidden = exceptKey ? entry.dataset.historyConfirm !== exceptKey : true;
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function extractInpaintHistoryItems(entries) {
|
|
1721
|
+
const items = [];
|
|
1722
|
+
const seen = new Set();
|
|
1723
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
1724
|
+
if (!isInpaintHistoryEntry(entry)) continue;
|
|
1725
|
+
const linkedJob = entry?.linkedJob || null;
|
|
1726
|
+
if (linkedJob && isPendingJobStatus(linkedJob.status)) {
|
|
1727
|
+
const pending = makePendingInpaintHistoryItem({
|
|
1728
|
+
job_id: linkedJob.job_id,
|
|
1729
|
+
status: linkedJob.status,
|
|
1730
|
+
progress: linkedJob.progress,
|
|
1731
|
+
created_at: linkedJob.created_at || entry.createdAt,
|
|
1732
|
+
updated_at: linkedJob.updated_at || entry.createdAt
|
|
1733
|
+
});
|
|
1734
|
+
if (pending && !seen.has(pending.key)) {
|
|
1735
|
+
seen.add(pending.key);
|
|
1736
|
+
items.push(pending);
|
|
1737
|
+
}
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
historyEntryUrls(entry).forEach((rawUrl, index) => {
|
|
1741
|
+
const url = absoluteUrl(rawUrl);
|
|
1742
|
+
const key = historyItemKey(entry, url, index);
|
|
1743
|
+
if (seen.has(key)) return;
|
|
1744
|
+
seen.add(key);
|
|
1745
|
+
items.push({
|
|
1746
|
+
key,
|
|
1747
|
+
kind: "output",
|
|
1748
|
+
jobId: String(entry?.jobId || entry?.job_id || entry?.linkedJob?.job_id || ""),
|
|
1749
|
+
url,
|
|
1750
|
+
createdAt: String(entry.createdAt || new Date().toISOString())
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
return items;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
async function copyText(value) {
|
|
1758
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
1759
|
+
try {
|
|
1760
|
+
await navigator.clipboard.writeText(value);
|
|
1761
|
+
return true;
|
|
1762
|
+
} catch {}
|
|
1763
|
+
}
|
|
1764
|
+
const field = document.createElement("textarea");
|
|
1765
|
+
field.value = value;
|
|
1766
|
+
field.setAttribute("readonly", "");
|
|
1767
|
+
field.style.position = "fixed";
|
|
1768
|
+
field.style.opacity = "0";
|
|
1769
|
+
document.body.appendChild(field);
|
|
1770
|
+
field.select();
|
|
1771
|
+
let copied = false;
|
|
1772
|
+
try {
|
|
1773
|
+
copied = document.execCommand("copy");
|
|
1774
|
+
} finally {
|
|
1775
|
+
field.remove();
|
|
1776
|
+
}
|
|
1777
|
+
return copied;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
async function loadInpaintHistory() {
|
|
1781
|
+
const tracer = resolveTracer();
|
|
1782
|
+
// Brand-new (unnamed) session: nothing on the server yet — just show any
|
|
1783
|
+
// local starting image, without minting a random tracer.
|
|
1784
|
+
if (!tracer) {
|
|
1785
|
+
inpaintHistoryItems = [];
|
|
1786
|
+
reinjectSourceHistoryItems();
|
|
1787
|
+
renderInpaintHistory();
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
historyRefresh.disabled = true;
|
|
1791
|
+
try {
|
|
1792
|
+
const endpoint = new URL("/job-runs/history", window.location.origin);
|
|
1793
|
+
endpoint.searchParams.set("limit", "120");
|
|
1794
|
+
endpoint.searchParams.set("template_id", "primitive:image_inpaint");
|
|
1795
|
+
endpoint.searchParams.set("tracer", tracer);
|
|
1796
|
+
const response = await fetch(endpoint, { credentials: "same-origin" });
|
|
1797
|
+
const payload = await parseJsonResponse(response);
|
|
1798
|
+
if (!response.ok || (payload && payload.error)) throw new Error(payload.error || "Unable to load inpaint history.");
|
|
1799
|
+
inpaintHistoryItems = extractInpaintHistoryItems(payload.entries);
|
|
1800
|
+
reinjectSourceHistoryItems();
|
|
1801
|
+
renderInpaintHistory();
|
|
1802
|
+
} catch (error) {
|
|
1803
|
+
console.error(error);
|
|
1804
|
+
setHistoryEmpty(error instanceof Error ? error.message : "Unable to load inpaint history.");
|
|
1805
|
+
} finally {
|
|
1806
|
+
historyRefresh.disabled = false;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
async function checkInpaintJobStatus(jobId, button) {
|
|
1811
|
+
if (!jobId) return null;
|
|
1812
|
+
if (button instanceof HTMLButtonElement) {
|
|
1813
|
+
button.disabled = true;
|
|
1814
|
+
button.textContent = "Checking";
|
|
1815
|
+
}
|
|
1816
|
+
try {
|
|
1817
|
+
const response = await fetch("/api/v1/primitives/image_inpaint/jobs/" + encodeURIComponent(jobId), {
|
|
1818
|
+
credentials: "same-origin"
|
|
1819
|
+
});
|
|
1820
|
+
const payload = await parseJsonResponse(response);
|
|
1821
|
+
if (!response.ok) throw new Error(payload.error || "Unable to check inpaint job.");
|
|
1822
|
+
if (payload.status === "failed" || payload.status === "cancelled") {
|
|
1823
|
+
const pending = makePendingInpaintHistoryItem(payload);
|
|
1824
|
+
upsertInpaintHistoryItem({
|
|
1825
|
+
...(pending || { key: pendingHistoryItemKey(jobId), kind: "pending", jobId, createdAt: new Date().toISOString() }),
|
|
1826
|
+
status: payload.status,
|
|
1827
|
+
progress: Number(payload.progress || 0)
|
|
1828
|
+
});
|
|
1829
|
+
throw new Error(payload.error?.message || "Image edit job " + payload.status + ".");
|
|
1830
|
+
}
|
|
1831
|
+
const outputUrl = extractOutputUrl(payload);
|
|
1832
|
+
if (payload.status === "succeeded" || payload.status === "complete" || outputUrl) {
|
|
1833
|
+
const url = applyCompletedInpaintJob(payload);
|
|
1834
|
+
if (!url) throw new Error("Image edit completed without an output URL.");
|
|
1835
|
+
setStatus("Image edit complete.", "success");
|
|
1836
|
+
return payload;
|
|
1837
|
+
}
|
|
1838
|
+
upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
|
|
1839
|
+
setStatus("Image edit job " + jobId + " is " + (payload.status || "running") + ".", "");
|
|
1840
|
+
return payload;
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
console.error(error);
|
|
1843
|
+
setStatus(error instanceof Error ? error.message : "Unable to check inpaint job.", "error");
|
|
1844
|
+
return null;
|
|
1845
|
+
} finally {
|
|
1846
|
+
if (button instanceof HTMLButtonElement) {
|
|
1847
|
+
button.disabled = false;
|
|
1848
|
+
button.textContent = "Check Status";
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
async function pollInpaintJob(jobId) {
|
|
1854
|
+
for (let attempt = 0; attempt < 90; attempt += 1) {
|
|
1855
|
+
const response = await fetch("/api/v1/primitives/image_inpaint/jobs/" + encodeURIComponent(jobId), {
|
|
1856
|
+
credentials: "same-origin"
|
|
1857
|
+
});
|
|
1858
|
+
const payload = await parseJsonResponse(response);
|
|
1859
|
+
if (!response.ok) throw new Error(payload.error || "Unable to poll inpaint job.");
|
|
1860
|
+
if (payload.status === "failed" || payload.status === "cancelled") {
|
|
1861
|
+
throw new Error(payload.error?.message || "Image edit job " + payload.status + ".");
|
|
1862
|
+
}
|
|
1863
|
+
const outputUrl = extractOutputUrl(payload);
|
|
1864
|
+
if (payload.status === "succeeded" || payload.status === "complete" || outputUrl) {
|
|
1865
|
+
if (!outputUrl) throw new Error("Image edit completed without an output URL.");
|
|
1866
|
+
applyCompletedInpaintJob(payload);
|
|
1867
|
+
setStatus("Image edit complete.", "success");
|
|
1868
|
+
return outputUrl;
|
|
1869
|
+
}
|
|
1870
|
+
upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
|
|
1871
|
+
setStatus("Image edit job " + jobId + " is " + (payload.status || "running") + ".", "");
|
|
1872
|
+
await new Promise((resolve) => window.setTimeout(resolve, attempt < 8 ? 1500 : 3000));
|
|
1873
|
+
}
|
|
1874
|
+
upsertInpaintHistoryItem(makePendingInpaintHistoryItem({ job_id: jobId, status: "running" }));
|
|
1875
|
+
setStatus("Image edit job is still running. Use Check Status in History.", "");
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
async function submitInpaintJob() {
|
|
1880
|
+
if (!sourceImageUrl || inpaintSubmit.disabled) {
|
|
1881
|
+
setStatus("Upload a source image first.", "error");
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (!inpaintStrokes.length) {
|
|
1885
|
+
setStatus("Paint at least one mask region.", "error");
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const usedColors = new Set(inpaintStrokes.map((stroke) => normalizeHexColor(stroke.color)));
|
|
1889
|
+
const globalPrompt = inpaintGlobalPrompt.value.trim();
|
|
1890
|
+
const missingPromptColor = inpaintMaskColors.find((entry) => usedColors.has(entry.color) && !String(entry.prompt || "").trim());
|
|
1891
|
+
if (!globalPrompt && missingPromptColor) {
|
|
1892
|
+
setStatus("Add a replacement prompt for " + missingPromptColor.color.toUpperCase() + ".", "error");
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
const instruction = buildInpaintInstruction();
|
|
1896
|
+
if (!instruction) {
|
|
1897
|
+
setStatus("Add a replacement prompt for at least one color.", "error");
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
setSubmitLoading(true);
|
|
1901
|
+
uploadButton.disabled = true;
|
|
1902
|
+
setStatus("Uploading masks...", "");
|
|
1903
|
+
try {
|
|
1904
|
+
const maskUrl = await uploadTemporaryBlob(await createProviderMaskBlob(), "inpaint-mask-" + Date.now() + ".png", "image/png");
|
|
1905
|
+
const tracer = tracerAutoPending
|
|
1906
|
+
? nameTracerFromSeed(globalPrompt || (inpaintMaskColors.find((entry) => String(entry.prompt || "").trim()) || {}).prompt || "image-edit")
|
|
1907
|
+
: getTracerValue();
|
|
1908
|
+
setStatus("Creating image edit job...", "");
|
|
1909
|
+
const response = await fetch("/api/v1/primitives/images/inpaint", {
|
|
1910
|
+
method: "POST",
|
|
1911
|
+
credentials: "same-origin",
|
|
1912
|
+
headers: { "content-type": "application/json" },
|
|
1913
|
+
body: JSON.stringify({
|
|
1914
|
+
tracer,
|
|
1915
|
+
payload: {
|
|
1916
|
+
source_image_url: sourceImageUrl,
|
|
1917
|
+
mask_url: absoluteUrl(maskUrl),
|
|
1918
|
+
instruction,
|
|
1919
|
+
reference_attachments: inpaintAttachments.map((attachment) => attachment.url),
|
|
1920
|
+
...(inpaintProviderSelect.value ? { provider: inpaintProviderSelect.value } : {}),
|
|
1921
|
+
size: "auto",
|
|
1922
|
+
...(getInpaintAspectRatio() ? { aspect_ratio: getInpaintAspectRatio() } : {}),
|
|
1923
|
+
output_format: "png"
|
|
1924
|
+
}
|
|
1925
|
+
})
|
|
1926
|
+
});
|
|
1927
|
+
const payload = await parseJsonResponse(response);
|
|
1928
|
+
if (!response.ok) throw new Error(payload.error || "Unable to create image edit job.");
|
|
1929
|
+
setStatus("Image edit job " + (payload.job_id || "") + " started.", "success");
|
|
1930
|
+
upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
|
|
1931
|
+
await pollInpaintJob(payload.job_id);
|
|
1932
|
+
await loadInpaintHistory();
|
|
1933
|
+
void loadTracerSessions();
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
console.error(error);
|
|
1936
|
+
setStatus(error instanceof Error ? error.message : "Unable to create image edit.", "error");
|
|
1937
|
+
} finally {
|
|
1938
|
+
setSubmitLoading(false);
|
|
1939
|
+
// Respect the post-generate lock — don't re-enable source upload if the
|
|
1940
|
+
// job succeeded and froze the session.
|
|
1941
|
+
uploadButton.disabled = sessionLocked;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
renderInpaintControls();
|
|
1946
|
+
void loadInpaintProviders();
|
|
1947
|
+
initializeFromUrl();
|
|
1948
|
+
void loadInpaintHistory();
|
|
1949
|
+
uploadButton.addEventListener("click", () => fileInput.click());
|
|
1950
|
+
historyRefresh.addEventListener("click", () => {
|
|
1951
|
+
void loadInpaintHistory();
|
|
1952
|
+
});
|
|
1953
|
+
historyList.addEventListener("pointerdown", (event) => {
|
|
1954
|
+
const target = event.target;
|
|
1955
|
+
if (target instanceof Element && target.closest("[data-history-confirm]")) {
|
|
1956
|
+
event.stopPropagation();
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
historyList.addEventListener("click", (event) => {
|
|
1960
|
+
const target = event.target;
|
|
1961
|
+
if (!(target instanceof Element)) return;
|
|
1962
|
+
const confirmPopover = target.closest("[data-history-confirm]");
|
|
1963
|
+
if (confirmPopover instanceof HTMLElement) {
|
|
1964
|
+
event.stopPropagation();
|
|
1965
|
+
}
|
|
1966
|
+
const previewButton = target.closest("[data-history-preview]");
|
|
1967
|
+
if (previewButton instanceof HTMLElement) {
|
|
1968
|
+
const key = previewButton.dataset.historyPreview || "";
|
|
1969
|
+
const item = inpaintHistoryItems.find((entry) => entry.key === key);
|
|
1970
|
+
if (!item) return;
|
|
1971
|
+
openImagePreview(item.url, item.createdAt ? new Date(item.createdAt).toLocaleString() : "Image edit output", imageDownloadName("image-edit-history", item.createdAt));
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const copyButton = target.closest("[data-history-copy]");
|
|
1975
|
+
if (copyButton instanceof HTMLElement) {
|
|
1976
|
+
const key = copyButton.dataset.historyCopy || "";
|
|
1977
|
+
const item = inpaintHistoryItems.find((entry) => entry.key === key);
|
|
1978
|
+
if (!item) return;
|
|
1979
|
+
void copyText(item.url).then((copied) => {
|
|
1980
|
+
copyButton.setAttribute("title", copied ? "Copied" : "Copy failed");
|
|
1981
|
+
window.setTimeout(() => copyButton.setAttribute("title", "Copy URL"), 1200);
|
|
1982
|
+
});
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
const saveButton = target.closest("[data-history-save]");
|
|
1986
|
+
if (saveButton instanceof HTMLElement) {
|
|
1987
|
+
const key = saveButton.dataset.historySave || "";
|
|
1988
|
+
const item = inpaintHistoryItems.find((entry) => entry.key === key);
|
|
1989
|
+
if (item && item.url) saveMediaToFiles(item.url, imageDownloadName("inpaint-output", item.createdAt));
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
const checkButton = target.closest("[data-history-check]");
|
|
1993
|
+
if (checkButton instanceof HTMLButtonElement) {
|
|
1994
|
+
void checkInpaintJobStatus(checkButton.dataset.historyCheck || "", checkButton);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const removeButton = target.closest("[data-history-remove]");
|
|
1998
|
+
if (removeButton instanceof HTMLElement) {
|
|
1999
|
+
const key = removeButton.dataset.historyRemove || "";
|
|
2000
|
+
const confirm = historyList.querySelector('[data-history-confirm="' + CSS.escape(key) + '"]');
|
|
2001
|
+
const shouldOpen = !(confirm instanceof HTMLElement) || confirm.hidden;
|
|
2002
|
+
closeInpaintHistoryConfirms();
|
|
2003
|
+
if (confirm instanceof HTMLElement) {
|
|
2004
|
+
confirm.hidden = !shouldOpen;
|
|
2005
|
+
}
|
|
2006
|
+
removeButton.setAttribute("aria-expanded", shouldOpen ? "true" : "false");
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
const cancelButton = target.closest("[data-history-cancel]");
|
|
2010
|
+
if (cancelButton instanceof HTMLElement) {
|
|
2011
|
+
const confirm = cancelButton.closest("[data-history-confirm]");
|
|
2012
|
+
if (confirm instanceof HTMLElement) confirm.hidden = true;
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const confirmRemove = target.closest("[data-history-confirm-remove]");
|
|
2016
|
+
if (confirmRemove instanceof HTMLElement) {
|
|
2017
|
+
const key = confirmRemove.dataset.historyConfirmRemove || "";
|
|
2018
|
+
if (key) {
|
|
2019
|
+
hiddenInpaintHistoryKeys.add(key);
|
|
2020
|
+
renderInpaintHistory();
|
|
2021
|
+
}
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
resultOpenButton.addEventListener("click", () => {
|
|
2026
|
+
if (!resultImage.currentSrc && !resultImage.src) return;
|
|
2027
|
+
openImagePreview(resultImage.currentSrc || resultImage.src, "Image edit result", imageDownloadName("image-edit-result"));
|
|
2028
|
+
});
|
|
2029
|
+
const resultSaveButton = document.querySelector("[data-custom-inpaint-save]");
|
|
2030
|
+
if (resultSaveButton) {
|
|
2031
|
+
resultSaveButton.addEventListener("click", () => {
|
|
2032
|
+
const url = resultImage.currentSrc || resultImage.src;
|
|
2033
|
+
if (!url) { setStatus("Generate an image first, then save it.", "error"); return; }
|
|
2034
|
+
saveMediaToFiles(url, imageDownloadName("image-edit-result"));
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
lightboxCopy.addEventListener("click", () => {
|
|
2038
|
+
const url = lightboxCopy.dataset.copyUrl || lightboxImage.currentSrc || lightboxImage.src || "";
|
|
2039
|
+
if (!url) return;
|
|
2040
|
+
void copyText(url).then((copied) => {
|
|
2041
|
+
lightboxCopy.setAttribute("title", copied ? "Copied" : "Copy failed");
|
|
2042
|
+
window.setTimeout(() => lightboxCopy.setAttribute("title", "Copy URL"), 1200);
|
|
2043
|
+
});
|
|
2044
|
+
});
|
|
2045
|
+
lightboxClose.addEventListener("click", () => closeImagePreview());
|
|
2046
|
+
lightbox.addEventListener("click", (event) => {
|
|
2047
|
+
if (event.target === lightbox) closeImagePreview();
|
|
2048
|
+
});
|
|
2049
|
+
startNewButton.addEventListener("click", () => {
|
|
2050
|
+
sessionTracer = "";
|
|
2051
|
+
tracerAutoPending = true;
|
|
2052
|
+
tracerInput.value = "";
|
|
2053
|
+
clearInpaintUrlParams();
|
|
2054
|
+
resetWorkspace();
|
|
2055
|
+
void loadInpaintHistory();
|
|
2056
|
+
void loadTracerSessions();
|
|
2057
|
+
setStatus("New session — it will be named from your first prompt or upload.", "");
|
|
2058
|
+
});
|
|
2059
|
+
tracerInput.addEventListener("input", () => {
|
|
2060
|
+
tracerAutoPending = !tracerInput.value.trim();
|
|
2061
|
+
sessionTracer = tracerInput.value.trim();
|
|
2062
|
+
queueInpaintHistoryReload();
|
|
2063
|
+
});
|
|
2064
|
+
fileInput.addEventListener("change", () => {
|
|
2065
|
+
void loadSourceFile(fileInput.files && fileInput.files[0]);
|
|
2066
|
+
fileInput.value = "";
|
|
2067
|
+
});
|
|
2068
|
+
inpaintAttachmentInput.addEventListener("change", () => {
|
|
2069
|
+
void addAttachmentFiles(inpaintAttachmentInput.files);
|
|
2070
|
+
inpaintAttachmentInput.value = "";
|
|
2071
|
+
});
|
|
2072
|
+
inpaintGlobalAttachments.addEventListener("click", (event) => {
|
|
2073
|
+
const target = event.target;
|
|
2074
|
+
if (!(target instanceof Element)) return;
|
|
2075
|
+
const removeColor = target.closest("[data-editor-inpaint-remove-color]");
|
|
2076
|
+
if (removeColor instanceof HTMLElement && removeColor.dataset.editorInpaintRemoveColor) {
|
|
2077
|
+
removeInpaintColor(removeColor.dataset.editorInpaintRemoveColor);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
const remove = target.closest("[data-editor-inpaint-remove-attachment]");
|
|
2081
|
+
if (remove instanceof HTMLElement) {
|
|
2082
|
+
inpaintAttachments = inpaintAttachments.filter((attachment) => attachment.id !== remove.dataset.editorInpaintRemoveAttachment);
|
|
2083
|
+
renderAllAttachmentChips();
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
document.querySelector('[data-editor-inpaint-attach-scope="global"]')?.addEventListener("click", () => {
|
|
2087
|
+
openAttachmentPicker("global");
|
|
2088
|
+
});
|
|
2089
|
+
inpaintGlobalPrompt.addEventListener("paste", (event) => {
|
|
2090
|
+
handleAttachmentPaste(event, "global");
|
|
2091
|
+
});
|
|
2092
|
+
inpaintUndo.addEventListener("click", () => {
|
|
2093
|
+
const stroke = inpaintStrokes.pop();
|
|
2094
|
+
if (stroke) redoStrokes.push(stroke);
|
|
2095
|
+
redrawInpaintMask();
|
|
2096
|
+
setStatus("", "");
|
|
2097
|
+
});
|
|
2098
|
+
inpaintRedo.addEventListener("click", () => {
|
|
2099
|
+
const stroke = redoStrokes.pop();
|
|
2100
|
+
if (stroke) inpaintStrokes.push(stroke);
|
|
2101
|
+
redrawInpaintMask();
|
|
2102
|
+
setStatus("", "");
|
|
2103
|
+
});
|
|
2104
|
+
inpaintClear.addEventListener("click", () => {
|
|
2105
|
+
resetMask();
|
|
2106
|
+
setStatus("", "");
|
|
2107
|
+
});
|
|
2108
|
+
inpaintBrush.addEventListener("input", () => {
|
|
2109
|
+
inpaintBrushLabel.textContent = inpaintBrush.value;
|
|
2110
|
+
updateBrushCursorSize();
|
|
2111
|
+
});
|
|
2112
|
+
inpaintColorPicker.addEventListener("input", () => {
|
|
2113
|
+
activeInpaintColor = normalizeHexColor(inpaintColorPicker.value);
|
|
2114
|
+
renderInpaintControls();
|
|
2115
|
+
});
|
|
2116
|
+
inpaintAddColor.addEventListener("click", () => addInpaintColor(inpaintColorPicker.value));
|
|
2117
|
+
inpaintPrompts.addEventListener("click", (event) => {
|
|
2118
|
+
const target = event.target;
|
|
2119
|
+
if (!(target instanceof Element)) return;
|
|
2120
|
+
const remove = target.closest("[data-editor-inpaint-remove-attachment]");
|
|
2121
|
+
if (remove instanceof HTMLElement) {
|
|
2122
|
+
inpaintAttachments = inpaintAttachments.filter((attachment) => attachment.id !== remove.dataset.editorInpaintRemoveAttachment);
|
|
2123
|
+
renderAllAttachmentChips();
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
const attach = target.closest("[data-editor-inpaint-attach-scope]");
|
|
2127
|
+
if (attach instanceof HTMLElement && attach.dataset.editorInpaintAttachScope === "color" && attach.dataset.color) {
|
|
2128
|
+
openAttachmentPicker("color", attach.dataset.color);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
const row = target.closest(".editor-inpaint-color-row");
|
|
2132
|
+
if (row instanceof HTMLElement && row.dataset.color) selectInpaintColor(row.dataset.color);
|
|
2133
|
+
});
|
|
2134
|
+
inpaintPrompts.addEventListener("focusin", (event) => {
|
|
2135
|
+
const target = event.target;
|
|
2136
|
+
if (!(target instanceof Element)) return;
|
|
2137
|
+
const row = target.closest(".editor-inpaint-color-row");
|
|
2138
|
+
if (row instanceof HTMLElement && row.dataset.color) selectInpaintColor(row.dataset.color);
|
|
2139
|
+
});
|
|
2140
|
+
inpaintPrompts.addEventListener("input", (event) => {
|
|
2141
|
+
const target = event.target;
|
|
2142
|
+
if (target instanceof HTMLTextAreaElement) updateInpaintPrompt(target.dataset.color || "", target.value);
|
|
2143
|
+
});
|
|
2144
|
+
inpaintPrompts.addEventListener("paste", (event) => {
|
|
2145
|
+
const target = event.target;
|
|
2146
|
+
if (target instanceof HTMLTextAreaElement && target.dataset.color) {
|
|
2147
|
+
handleAttachmentPaste(event, "color", target.dataset.color);
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
inpaintCanvas.addEventListener("pointerenter", (event) => updateBrushCursorPosition(event));
|
|
2151
|
+
inpaintCanvas.addEventListener("pointerdown", (event) => {
|
|
2152
|
+
updateBrushCursorPosition(event);
|
|
2153
|
+
beginInpaintStroke(event);
|
|
2154
|
+
});
|
|
2155
|
+
inpaintCanvas.addEventListener("pointermove", (event) => {
|
|
2156
|
+
updateBrushCursorPosition(event);
|
|
2157
|
+
continueInpaintStroke(event);
|
|
2158
|
+
});
|
|
2159
|
+
inpaintCanvas.addEventListener("pointerup", (event) => endInpaintStroke(event));
|
|
2160
|
+
inpaintCanvas.addEventListener("pointercancel", (event) => {
|
|
2161
|
+
endInpaintStroke(event);
|
|
2162
|
+
hideBrushCursor();
|
|
2163
|
+
});
|
|
2164
|
+
inpaintCanvas.addEventListener("pointerleave", (event) => {
|
|
2165
|
+
endInpaintStroke(event);
|
|
2166
|
+
hideBrushCursor();
|
|
2167
|
+
});
|
|
2168
|
+
inpaintSubmit.addEventListener("click", () => {
|
|
2169
|
+
void submitInpaintJob();
|
|
2170
|
+
});
|
|
2171
|
+
function openSettingsModal() {
|
|
2172
|
+
settingsPopover.hidden = false;
|
|
2173
|
+
document.body.style.overflow = "hidden";
|
|
2174
|
+
window.setTimeout(() => tracerInput.focus(), 0);
|
|
2175
|
+
}
|
|
2176
|
+
function closeSettingsModal() {
|
|
2177
|
+
settingsPopover.hidden = true;
|
|
2178
|
+
if (!sessionsPanel || !sessionsPanel.classList.contains("is-open")) {
|
|
2179
|
+
document.body.style.overflow = "";
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
settingsButton.addEventListener("click", () => {
|
|
2183
|
+
if (settingsPopover.hidden) openSettingsModal(); else closeSettingsModal();
|
|
2184
|
+
});
|
|
2185
|
+
if (settingsClose) settingsClose.addEventListener("click", () => closeSettingsModal());
|
|
2186
|
+
settingsPopover.addEventListener("click", (event) => {
|
|
2187
|
+
if (event.target === settingsPopover) closeSettingsModal();
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
// ===== past-sessions (tracers) left panel ==================================
|
|
2191
|
+
function openSessionsPanel() {
|
|
2192
|
+
if (!sessionsPanel) return;
|
|
2193
|
+
sessionsPanel.classList.add("is-open");
|
|
2194
|
+
if (sessionsBackdrop) sessionsBackdrop.hidden = false;
|
|
2195
|
+
document.body.style.overflow = "hidden";
|
|
2196
|
+
void loadTracerSessions();
|
|
2197
|
+
}
|
|
2198
|
+
function closeSessionsPanel() {
|
|
2199
|
+
if (!sessionsPanel) return;
|
|
2200
|
+
sessionsPanel.classList.remove("is-open");
|
|
2201
|
+
if (sessionsBackdrop) sessionsBackdrop.hidden = true;
|
|
2202
|
+
if (settingsPopover.hidden) document.body.style.overflow = "";
|
|
2203
|
+
}
|
|
2204
|
+
function isImageSessionEntry(entry) {
|
|
2205
|
+
const templateId = String(entry && entry.linkedJob && (entry.linkedJob.template_id || entry.linkedJob.templateId) || "").toLowerCase();
|
|
2206
|
+
const path = String(entry && entry.path || "").toLowerCase();
|
|
2207
|
+
return templateId.includes("image_inpaint")
|
|
2208
|
+
|| templateId.includes("image_generate")
|
|
2209
|
+
|| path.includes("/primitives/images/inpaint")
|
|
2210
|
+
|| path.includes("/primitives/image_inpaint")
|
|
2211
|
+
|| path.includes("/primitives/images/generate")
|
|
2212
|
+
|| path.includes("/primitives/image_generate");
|
|
2213
|
+
}
|
|
2214
|
+
function relTimeShort(value) {
|
|
2215
|
+
const then = Date.parse(value || 0);
|
|
2216
|
+
if (!then) return "";
|
|
2217
|
+
const secs = Math.max(0, Math.floor((Date.now() - then) / 1000));
|
|
2218
|
+
if (secs < 60) return "just now";
|
|
2219
|
+
const mins = Math.floor(secs / 60);
|
|
2220
|
+
if (mins < 60) return mins + "m ago";
|
|
2221
|
+
const hours = Math.floor(mins / 60);
|
|
2222
|
+
if (hours < 24) return hours + "h ago";
|
|
2223
|
+
const days = Math.floor(hours / 24);
|
|
2224
|
+
if (days < 7) return days + "d ago";
|
|
2225
|
+
return new Date(then).toLocaleDateString();
|
|
2226
|
+
}
|
|
2227
|
+
function renderTracerSessions(groups, message) {
|
|
2228
|
+
if (!sessionsList) return;
|
|
2229
|
+
sessionsList.innerHTML = "";
|
|
2230
|
+
if (message) {
|
|
2231
|
+
const note = document.createElement("div");
|
|
2232
|
+
note.className = "rk-ip-sessions-empty";
|
|
2233
|
+
note.textContent = message;
|
|
2234
|
+
sessionsList.append(note);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
if (!groups.length) {
|
|
2238
|
+
const note = document.createElement("div");
|
|
2239
|
+
note.className = "rk-ip-sessions-empty";
|
|
2240
|
+
note.textContent = "No past sessions yet. Generate or edit an image to start one.";
|
|
2241
|
+
sessionsList.append(note);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
const active = resolveTracer();
|
|
2245
|
+
for (const group of groups) {
|
|
2246
|
+
const isImage = group.kind === "image";
|
|
2247
|
+
const row = document.createElement("button");
|
|
2248
|
+
row.type = "button";
|
|
2249
|
+
row.className = "rk-ip-session" + (!isImage && group.tracer === active ? " is-active" : "");
|
|
2250
|
+
if (isImage) {
|
|
2251
|
+
row.dataset.sessionImage = group.url;
|
|
2252
|
+
row.title = "Load this image into a new edit session";
|
|
2253
|
+
} else {
|
|
2254
|
+
row.dataset.sessionTracer = group.tracer;
|
|
2255
|
+
}
|
|
2256
|
+
const thumbUrl = group.thumb || (isImage ? group.url : "");
|
|
2257
|
+
if (thumbUrl) {
|
|
2258
|
+
const thumb = document.createElement("img");
|
|
2259
|
+
thumb.className = "rk-ip-session-thumb";
|
|
2260
|
+
thumb.src = thumbUrl;
|
|
2261
|
+
thumb.alt = "";
|
|
2262
|
+
thumb.loading = "lazy";
|
|
2263
|
+
row.append(thumb);
|
|
2264
|
+
} else {
|
|
2265
|
+
const ph = document.createElement("span");
|
|
2266
|
+
ph.className = "rk-ip-session-thumb is-empty";
|
|
2267
|
+
ph.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="m6 15 3-3 3 3 3-4 3 4"/></svg>';
|
|
2268
|
+
row.append(ph);
|
|
2269
|
+
}
|
|
2270
|
+
const meta = document.createElement("div");
|
|
2271
|
+
meta.className = "rk-ip-session-meta";
|
|
2272
|
+
const title = document.createElement("div");
|
|
2273
|
+
title.className = "rk-ip-session-title";
|
|
2274
|
+
title.textContent = isImage ? (group.name || "Standalone image") : group.tracer;
|
|
2275
|
+
const sub = document.createElement("div");
|
|
2276
|
+
sub.className = "rk-ip-session-sub";
|
|
2277
|
+
const when = relTimeShort(group.createdAt);
|
|
2278
|
+
sub.textContent = isImage
|
|
2279
|
+
? "Standalone image — tap to edit" + (when ? " · " + when : "")
|
|
2280
|
+
: (group.count ? group.count + " image" + (group.count === 1 ? "" : "s") : "session") + (when ? " · " + when : "");
|
|
2281
|
+
meta.append(title, sub);
|
|
2282
|
+
row.append(meta);
|
|
2283
|
+
sessionsList.append(row);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
let sessionsLoading = false;
|
|
2287
|
+
async function loadTracerSessions() {
|
|
2288
|
+
if (!sessionsList || sessionsLoading) return;
|
|
2289
|
+
sessionsLoading = true;
|
|
2290
|
+
try {
|
|
2291
|
+
const endpoint = new URL("/job-runs/history", window.location.origin);
|
|
2292
|
+
endpoint.searchParams.set("limit", "500");
|
|
2293
|
+
const response = await fetch(endpoint, { credentials: "same-origin" });
|
|
2294
|
+
const payload = await parseJsonResponse(response);
|
|
2295
|
+
if (!response.ok || (payload && payload.error)) throw new Error((payload && payload.error) || "Unable to load sessions.");
|
|
2296
|
+
const groups = new Map();
|
|
2297
|
+
// Past image gens that carry NO session tracer (e.g. one-off generations
|
|
2298
|
+
// from other surfaces) can't be reopened as a session, so surface each as
|
|
2299
|
+
// a standalone image the user can load fresh into a new inpaint session.
|
|
2300
|
+
const loose = [];
|
|
2301
|
+
const looseSeen = new Set();
|
|
2302
|
+
for (const entry of Array.isArray(payload.entries) ? payload.entries : []) {
|
|
2303
|
+
if (!isImageSessionEntry(entry)) continue;
|
|
2304
|
+
const urls = historyEntryUrls(entry);
|
|
2305
|
+
const tracer = String(entry.tracer || "").trim();
|
|
2306
|
+
if (!tracer) {
|
|
2307
|
+
for (const raw of urls) {
|
|
2308
|
+
let url = "";
|
|
2309
|
+
try { url = absoluteUrl(raw); } catch { continue; }
|
|
2310
|
+
if (looseSeen.has(url)) continue;
|
|
2311
|
+
looseSeen.add(url);
|
|
2312
|
+
loose.push({ kind: "image", url, thumb: url, name: sourceNameFromUrl(url), createdAt: entry.createdAt || "" });
|
|
2313
|
+
}
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
let group = groups.get(tracer);
|
|
2317
|
+
if (!group) { group = { kind: "session", tracer, thumb: "", count: 0, createdAt: entry.createdAt || "" }; groups.set(tracer, group); }
|
|
2318
|
+
if (!group.thumb && urls[0]) { try { group.thumb = absoluteUrl(urls[0]); } catch {} }
|
|
2319
|
+
group.count += urls.length;
|
|
2320
|
+
if (Date.parse(entry.createdAt || 0) > Date.parse(group.createdAt || 0)) group.createdAt = entry.createdAt;
|
|
2321
|
+
}
|
|
2322
|
+
const list = Array.from(groups.values()).concat(loose)
|
|
2323
|
+
.sort((a, b) => Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0));
|
|
2324
|
+
renderTracerSessions(list);
|
|
2325
|
+
} catch (error) {
|
|
2326
|
+
console.error(error);
|
|
2327
|
+
renderTracerSessions([], error instanceof Error ? error.message : "Unable to load sessions.");
|
|
2328
|
+
} finally {
|
|
2329
|
+
sessionsLoading = false;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
function switchToTracer(tracer, sourceUrl) {
|
|
2333
|
+
const value = String(tracer || "").trim();
|
|
2334
|
+
if (!value) return;
|
|
2335
|
+
clearInpaintUrlParams();
|
|
2336
|
+
resetWorkspace();
|
|
2337
|
+
sessionTracer = value;
|
|
2338
|
+
tracerInput.value = value;
|
|
2339
|
+
tracerAutoPending = false;
|
|
2340
|
+
closeSessionsPanel();
|
|
2341
|
+
void loadInpaintHistory();
|
|
2342
|
+
void loadTracerSessions();
|
|
2343
|
+
// Load this session's latest image straight onto the canvas so it's ready
|
|
2344
|
+
// to paint/refine — reopening a session with a blank stage is useless.
|
|
2345
|
+
if (sourceUrl) {
|
|
2346
|
+
loadSourceUrl(sourceUrl, "history");
|
|
2347
|
+
setStatus("Opened session " + value + " — paint mask regions to keep editing.", "success");
|
|
2348
|
+
} else {
|
|
2349
|
+
setStatus("Opened session " + value + ".", "");
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
// A past image gen with no session tracer: start a brand-new session and drop
|
|
2353
|
+
// that image onto the editor as the source so it can be edited immediately.
|
|
2354
|
+
function startSessionFromImage(url) {
|
|
2355
|
+
const value = String(url || "").trim();
|
|
2356
|
+
if (!value) return;
|
|
2357
|
+
sessionTracer = "";
|
|
2358
|
+
tracerAutoPending = true;
|
|
2359
|
+
tracerInput.value = "";
|
|
2360
|
+
clearInpaintUrlParams();
|
|
2361
|
+
resetWorkspace();
|
|
2362
|
+
closeSessionsPanel();
|
|
2363
|
+
loadSourceUrl(value, "history");
|
|
2364
|
+
void loadTracerSessions();
|
|
2365
|
+
setStatus("New session started from that image — paint mask regions to edit it.", "success");
|
|
2366
|
+
}
|
|
2367
|
+
if (sessionsToggle) sessionsToggle.addEventListener("click", () => {
|
|
2368
|
+
if (sessionsPanel && sessionsPanel.classList.contains("is-open")) closeSessionsPanel(); else openSessionsPanel();
|
|
2369
|
+
});
|
|
2370
|
+
if (sessionsClose) sessionsClose.addEventListener("click", () => closeSessionsPanel());
|
|
2371
|
+
if (sessionsBackdrop) sessionsBackdrop.addEventListener("click", () => closeSessionsPanel());
|
|
2372
|
+
if (sessionsList) sessionsList.addEventListener("click", (event) => {
|
|
2373
|
+
const target = event.target;
|
|
2374
|
+
if (!(target instanceof Element)) return;
|
|
2375
|
+
const imageRow = target.closest("[data-session-image]");
|
|
2376
|
+
if (imageRow instanceof HTMLElement && imageRow.dataset.sessionImage) {
|
|
2377
|
+
startSessionFromImage(imageRow.dataset.sessionImage);
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
const row = target.closest("[data-session-tracer]");
|
|
2381
|
+
if (row instanceof HTMLElement && row.dataset.sessionTracer) {
|
|
2382
|
+
const thumb = row.querySelector("img.rk-ip-session-thumb");
|
|
2383
|
+
switchToTracer(row.dataset.sessionTracer, thumb instanceof HTMLImageElement ? thumb.src : "");
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
|
|
2387
|
+
window.addEventListener("click", (event) => {
|
|
2388
|
+
const target = event.target;
|
|
2389
|
+
if (!(target instanceof Node)) return;
|
|
2390
|
+
const isHistoryConfirmTarget = target instanceof Element && Boolean(target.closest("[data-history-confirm]"));
|
|
2391
|
+
if (!historyList.contains(target) && !isHistoryConfirmTarget) {
|
|
2392
|
+
closeInpaintHistoryConfirms();
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2395
|
+
window.addEventListener("resize", () => syncInpaintCanvasSize());
|
|
2396
|
+
window.addEventListener("keydown", (event) => {
|
|
2397
|
+
if (event.key !== "Escape") return;
|
|
2398
|
+
if (lightbox.getAttribute("data-open") === "true") { closeImagePreview(); return; }
|
|
2399
|
+
if (!settingsPopover.hidden) { closeSettingsModal(); return; }
|
|
2400
|
+
if (sessionsPanel && sessionsPanel.classList.contains("is-open")) { closeSessionsPanel(); return; }
|
|
2401
|
+
});
|
|
2402
|
+
|
|
2403
|
+
// ===== text-to-image "Create Image" empty-state composer =====================
|
|
2404
|
+
// Layered on top of the legacy inpaint logic; reuses its in-scope helpers
|
|
2405
|
+
// (setStatus, loadSourceUrl, uploadTemporaryBlob, getTracerValue, parseJsonResponse,
|
|
2406
|
+
// absoluteUrl, extractOutputUrl, inpaintProviderSelect). A generated image becomes
|
|
2407
|
+
// the editable SOURCE so you can immediately paint to refine it.
|
|
2408
|
+
var genPrompt = document.getElementById('rk-ip-genprompt');
|
|
2409
|
+
var genClip = document.getElementById('rk-ip-genclip');
|
|
2410
|
+
var genSend = document.getElementById('rk-ip-gensend');
|
|
2411
|
+
var genRefsEl = document.getElementById('rk-ip-genrefs');
|
|
2412
|
+
var genStatusEl = document.getElementById('rk-ip-genstatus');
|
|
2413
|
+
var genWrap = document.getElementById('rk-ip-gen');
|
|
2414
|
+
var GEN_SEND_HTML = genSend ? genSend.innerHTML : '';
|
|
2415
|
+
var genRefs = [];
|
|
2416
|
+
var genBusy = false;
|
|
2417
|
+
|
|
2418
|
+
function setGenStatus(m, t){ if(!genStatusEl) return; genStatusEl.textContent = m || ''; genStatusEl.setAttribute('data-tone', t || ''); }
|
|
2419
|
+
function canGenNew(){ return Boolean((genPrompt && genPrompt.value || '').trim()) && !genBusy; }
|
|
2420
|
+
function syncGenSend(){
|
|
2421
|
+
if(genPrompt) genPrompt.disabled = genBusy;
|
|
2422
|
+
if(genClip) genClip.disabled = genBusy;
|
|
2423
|
+
if(!genSend) return;
|
|
2424
|
+
genSend.disabled = !canGenNew();
|
|
2425
|
+
genSend.classList.toggle('is-busy', genBusy);
|
|
2426
|
+
genSend.innerHTML = genBusy ? '<span class="rk-ip-gen-spin" aria-hidden="true"></span>' : GEN_SEND_HTML;
|
|
2427
|
+
}
|
|
2428
|
+
function renderGenRefs(){
|
|
2429
|
+
if(!genRefsEl) return;
|
|
2430
|
+
genRefsEl.hidden = genRefs.length === 0;
|
|
2431
|
+
genRefsEl.innerHTML = '';
|
|
2432
|
+
genRefs.forEach(function(url, idx){
|
|
2433
|
+
var chip = document.createElement('span'); chip.className = 'rk-ip-gen-ref';
|
|
2434
|
+
var im = document.createElement('img'); im.src = url; im.alt = ''; im.loading = 'lazy'; chip.appendChild(im);
|
|
2435
|
+
var lbl = document.createElement('span'); var nm = url.split('/').pop() || 'reference'; lbl.textContent = decodeURIComponent(nm).slice(0, 28); chip.appendChild(lbl);
|
|
2436
|
+
var x = document.createElement('button'); x.type = 'button'; x.setAttribute('aria-label', 'Remove reference'); x.innerHTML = '×';
|
|
2437
|
+
x.addEventListener('click', function(){ genRefs.splice(idx, 1); renderGenRefs(); }); chip.appendChild(x);
|
|
2438
|
+
genRefsEl.appendChild(chip);
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// Paperclip -> shared file-directory explorer (pop panel) to attach image refs.
|
|
2443
|
+
var genFileModal = null;
|
|
2444
|
+
function closeGenFilePanel(){ if(genFileModal){ try{ genFileModal._handle && genFileModal._handle.destroy && genFileModal._handle.destroy(); }catch(_){} genFileModal.remove(); genFileModal = null; document.removeEventListener('keydown', onGenFileEsc, true); } }
|
|
2445
|
+
function onGenFileEsc(e){ if(e.key === 'Escape') closeGenFilePanel(); }
|
|
2446
|
+
function openGenFilePanel(){
|
|
2447
|
+
if(genFileModal) return;
|
|
2448
|
+
var overlay = document.createElement('div'); overlay.className = 'rk-ip-filemodal';
|
|
2449
|
+
var panel = document.createElement('div'); panel.className = 'rk-ip-filemodal-panel';
|
|
2450
|
+
var head = document.createElement('div'); head.className = 'rk-ip-filemodal-head';
|
|
2451
|
+
var title = document.createElement('span'); title.className = 'rk-ip-filemodal-title'; title.textContent = 'Attach reference images';
|
|
2452
|
+
var close = document.createElement('button'); close.type = 'button'; close.className = 'rk-ip-filemodal-close'; close.setAttribute('aria-label', 'Close'); close.innerHTML = '×';
|
|
2453
|
+
head.appendChild(title); head.appendChild(close);
|
|
2454
|
+
var mountEl = document.createElement('div'); mountEl.className = 'rk-ip-filemodal-mount';
|
|
2455
|
+
panel.appendChild(head); panel.appendChild(mountEl); overlay.appendChild(panel);
|
|
2456
|
+
document.body.appendChild(overlay); genFileModal = overlay;
|
|
2457
|
+
close.addEventListener('click', closeGenFilePanel);
|
|
2458
|
+
overlay.addEventListener('click', function(e){ if(e.target === overlay) closeGenFilePanel(); });
|
|
2459
|
+
document.addEventListener('keydown', onGenFileEsc, true);
|
|
2460
|
+
if(window.__vidfarmDirectory && window.__vidfarmDirectory.mount){
|
|
2461
|
+
genFileModal._handle = window.__vidfarmDirectory.mount(mountEl, {
|
|
2462
|
+
mode: 'attach', showMultiSelect: true, primaryClick: 'attach',
|
|
2463
|
+
onAttach: function(items){
|
|
2464
|
+
var added = 0;
|
|
2465
|
+
(items || []).forEach(function(it){
|
|
2466
|
+
var ct = (it.contentType || ''); var isImg = ct.indexOf('image/') === 0 || /\\.(png|jpe?g|gif|webp|svg|avif)$/i.test(it.name || '');
|
|
2467
|
+
if(it.viewUrl && isImg && genRefs.indexOf(it.viewUrl) === -1){ genRefs.push(it.viewUrl); added++; }
|
|
2468
|
+
});
|
|
2469
|
+
renderGenRefs();
|
|
2470
|
+
if(added){ setGenStatus(added + ' reference' + (added > 1 ? 's' : '') + ' attached.', 'success'); closeGenFilePanel(); }
|
|
2471
|
+
else setGenStatus('Pick an image file to use as a reference.', 'error');
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
} else {
|
|
2475
|
+
mountEl.innerHTML = '<p style="padding:16px;color:var(--rk-text-muted);font-size:13px">File browser is unavailable right now.</p>';
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
function pollImageGenerateJob(jobId){
|
|
2480
|
+
var attempt = 0;
|
|
2481
|
+
function step(){
|
|
2482
|
+
return fetch('/api/v1/primitives/image_generate/jobs/' + encodeURIComponent(jobId), { credentials: 'same-origin' }).then(function(r){
|
|
2483
|
+
return parseJsonResponse(r).then(function(payload){
|
|
2484
|
+
if(!r.ok) throw new Error(payload.error || 'Unable to poll image job.');
|
|
2485
|
+
if(payload.status === 'failed' || payload.status === 'cancelled'){ throw new Error((payload.error && payload.error.message) || ('Image job ' + payload.status + '.')); }
|
|
2486
|
+
var out = extractOutputUrl(payload);
|
|
2487
|
+
if(payload.status === 'succeeded' || payload.status === 'complete' || out){
|
|
2488
|
+
if(!out) throw new Error('Image job completed without an output URL.');
|
|
2489
|
+
return absoluteUrl(out);
|
|
2490
|
+
}
|
|
2491
|
+
setGenStatus('Generating image (' + (payload.status || 'running') + ')\\u2026', '');
|
|
2492
|
+
attempt++;
|
|
2493
|
+
if(attempt >= 90){ setGenStatus('Still running \\u2014 check back shortly.', ''); return ''; }
|
|
2494
|
+
return new Promise(function(res){ setTimeout(res, attempt < 8 ? 1500 : 3000); }).then(step);
|
|
2495
|
+
});
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
return step();
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
function submitGenerate(){
|
|
2502
|
+
var p = (genPrompt && genPrompt.value || '').trim();
|
|
2503
|
+
if(!p){ setGenStatus('Type a prompt to generate an image.', 'error'); if(genPrompt) genPrompt.focus(); return; }
|
|
2504
|
+
if(genBusy) return;
|
|
2505
|
+
genBusy = true; syncGenSend(); if(genWrap) genWrap.classList.add('is-busy');
|
|
2506
|
+
setGenStatus('Creating image\\u2026', '');
|
|
2507
|
+
var payload = { prompt: p, output_format: 'png' };
|
|
2508
|
+
if(genRefs.length) payload.prompt_attachments = genRefs.slice();
|
|
2509
|
+
if(inpaintProviderSelect && inpaintProviderSelect.value) payload.provider = inpaintProviderSelect.value;
|
|
2510
|
+
var genTracer = tracerAutoPending ? nameTracerFromSeed(p) : getTracerValue();
|
|
2511
|
+
if(tracerAutoPending === false){ void loadTracerSessions(); }
|
|
2512
|
+
fetch('/api/v1/primitives/images/generate', {
|
|
2513
|
+
method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' },
|
|
2514
|
+
body: JSON.stringify({ tracer: genTracer, payload: payload })
|
|
2515
|
+
}).then(parseJsonResponse).then(function(j){
|
|
2516
|
+
if(!j || !j.job_id){ throw new Error((j && j.error) || 'Unable to start image generation.'); }
|
|
2517
|
+
setGenStatus('Generating image\\u2026', '');
|
|
2518
|
+
return pollImageGenerateJob(j.job_id);
|
|
2519
|
+
}).then(function(out){
|
|
2520
|
+
if(out){ loadSourceUrl(out, 'generated'); setStatus('Generated image ready. Paint mask regions to refine, or download it.', 'success'); }
|
|
2521
|
+
}).catch(function(err){
|
|
2522
|
+
var msg = (err && err.message) ? err.message : 'Unable to generate image.';
|
|
2523
|
+
if(/401|403|sign in|unauth/i.test(msg)) msg = 'Sign in to generate images.';
|
|
2524
|
+
setGenStatus(msg, 'error');
|
|
2525
|
+
}).then(function(){ genBusy = false; if(genWrap) genWrap.classList.remove('is-busy'); syncGenSend(); });
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
if(genPrompt){
|
|
2529
|
+
genPrompt.addEventListener('input', syncGenSend);
|
|
2530
|
+
genPrompt.addEventListener('keydown', function(e){ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); submitGenerate(); } });
|
|
2531
|
+
}
|
|
2532
|
+
if(genClip) genClip.addEventListener('click', openGenFilePanel);
|
|
2533
|
+
if(genSend) genSend.addEventListener('click', submitGenerate);
|
|
2534
|
+
renderGenRefs();
|
|
2535
|
+
syncGenSend();
|
|
2536
|
+
|
|
2537
|
+
// Drag-and-drop onto the upload dropzone (legacy only wired click).
|
|
2538
|
+
var dropZone = document.querySelector('[data-custom-inpaint-upload]');
|
|
2539
|
+
if(dropZone){
|
|
2540
|
+
['dragenter', 'dragover'].forEach(function(ev){ dropZone.addEventListener(ev, function(e){ e.preventDefault(); dropZone.classList.add('is-drag'); }); });
|
|
2541
|
+
['dragleave', 'drop'].forEach(function(ev){ dropZone.addEventListener(ev, function(e){ e.preventDefault(); dropZone.classList.remove('is-drag'); }); });
|
|
2542
|
+
dropZone.addEventListener('drop', function(e){ var f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if(f) void loadSourceFile(f); });
|
|
2543
|
+
}
|
|
537
2544
|
})();`;
|
|
538
2545
|
return reskinDocument({
|
|
539
2546
|
title: "vidfarm reskin — Create Image",
|
|
540
|
-
description: "Reskinned vidfarm masked image editor — upload,
|
|
2547
|
+
description: "Reskinned vidfarm masked image editor — upload or generate, paint multi-color masks with per-color prompts, and generate on your own AI keys.",
|
|
541
2548
|
activeSlug: "inpaint",
|
|
542
2549
|
pageCss,
|
|
543
2550
|
body,
|