@mevdragon/vidfarm-devcli 0.2.12 → 0.2.14

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.
Files changed (205) hide show
  1. package/.env.example +37 -1
  2. package/README.md +27 -183
  3. package/SKILL.director.md +293 -0
  4. package/SKILL.platform.md +312 -0
  5. package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +275 -0
  6. package/auto-create-hyperframe-templates/extractor-system-prompt.md +128 -0
  7. package/auto-create-hyperframe-templates/input-manifest.schema.json +56 -0
  8. package/auto-create-hyperframe-templates/lambda-memory-estimator.md +118 -0
  9. package/auto-create-hyperframe-templates/production-graph.schema.json +314 -0
  10. package/auto-create-hyperframe-templates/runbook.md +198 -0
  11. package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +368 -0
  12. package/auto-create-hyperframe-templates/template-plan.schema.json +311 -0
  13. package/auto-create-hyperframe-templates/template-planner-prompt.md +144 -0
  14. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +116 -0
  15. package/auto-create-templates/extractor-system-prompt.md +6 -1
  16. package/auto-create-templates/template-plan.schema.json +73 -2
  17. package/auto-create-templates/template-planner-prompt.md +62 -1
  18. package/dist/src/account-pages-legacy.js +363 -89
  19. package/dist/src/account-pages-legacy.js.map +1 -0
  20. package/dist/src/account-pages.js +3 -2
  21. package/dist/src/account-pages.js.map +1 -0
  22. package/dist/src/app.js +7096 -1294
  23. package/dist/src/app.js.map +1 -0
  24. package/dist/src/cli.js +115 -29
  25. package/dist/src/cli.js.map +1 -0
  26. package/dist/src/composition-runtime.js +581 -0
  27. package/dist/src/composition-runtime.js.map +1 -0
  28. package/dist/src/config.js +69 -11
  29. package/dist/src/config.js.map +1 -0
  30. package/dist/src/context.js +280 -21
  31. package/dist/src/context.js.map +1 -0
  32. package/dist/src/dev-app-legacy.js +18 -17
  33. package/dist/src/dev-app-legacy.js.map +1 -0
  34. package/dist/src/dev-app.js +1 -0
  35. package/dist/src/dev-app.js.map +1 -0
  36. package/dist/src/dev-serve.js +169 -0
  37. package/dist/src/dev-serve.js.map +1 -0
  38. package/dist/src/domain.js +2 -1
  39. package/dist/src/domain.js.map +1 -0
  40. package/dist/src/editor-chat-history.js +10 -0
  41. package/dist/src/editor-chat-history.js.map +1 -0
  42. package/dist/src/editor-chat.js +135 -25
  43. package/dist/src/editor-chat.js.map +1 -0
  44. package/dist/src/editor-dark-theme.js +1128 -0
  45. package/dist/src/editor-dark-theme.js.map +1 -0
  46. package/dist/src/frontend/flockposter-cache-store.js +10 -2
  47. package/dist/src/frontend/flockposter-cache-store.js.map +1 -0
  48. package/dist/src/frontend/homepage-client.js +6 -3
  49. package/dist/src/frontend/homepage-client.js.map +1 -0
  50. package/dist/src/frontend/homepage-shared.js +1 -0
  51. package/dist/src/frontend/homepage-shared.js.map +1 -0
  52. package/dist/src/frontend/homepage-store.js +1 -0
  53. package/dist/src/frontend/homepage-store.js.map +1 -0
  54. package/dist/src/frontend/homepage-view.js +131 -18
  55. package/dist/src/frontend/homepage-view.js.map +1 -0
  56. package/dist/src/frontend/page-runtime-client.js +27 -3
  57. package/dist/src/frontend/page-runtime-client.js.map +1 -0
  58. package/dist/src/frontend/page-runtime-store.js +1 -0
  59. package/dist/src/frontend/page-runtime-store.js.map +1 -0
  60. package/dist/src/frontend/sentry.js +42 -0
  61. package/dist/src/frontend/sentry.js.map +1 -0
  62. package/dist/src/frontend/template-editor-chat.js +1711 -212
  63. package/dist/src/frontend/template-editor-chat.js.map +1 -0
  64. package/dist/src/help-page.js +333 -0
  65. package/dist/src/help-page.js.map +1 -0
  66. package/dist/src/homepage.js +169 -47
  67. package/dist/src/homepage.js.map +1 -0
  68. package/dist/src/hyperframes/composition.js +180 -0
  69. package/dist/src/hyperframes/composition.js.map +1 -0
  70. package/dist/src/index.js +1 -0
  71. package/dist/src/index.js.map +1 -0
  72. package/dist/src/instrument.js +30 -0
  73. package/dist/src/instrument.js.map +1 -0
  74. package/dist/src/lib/crypto.js +1 -0
  75. package/dist/src/lib/crypto.js.map +1 -0
  76. package/dist/src/lib/dev-log.js +54 -0
  77. package/dist/src/lib/dev-log.js.map +1 -0
  78. package/dist/src/lib/display-name.js +11 -0
  79. package/dist/src/lib/display-name.js.map +1 -0
  80. package/dist/src/lib/ids.js +21 -1
  81. package/dist/src/lib/ids.js.map +1 -0
  82. package/dist/src/lib/images.js +1 -0
  83. package/dist/src/lib/images.js.map +1 -0
  84. package/dist/src/lib/json.js +1 -0
  85. package/dist/src/lib/json.js.map +1 -0
  86. package/dist/src/lib/template-dna.js +1 -0
  87. package/dist/src/lib/template-dna.js.map +1 -0
  88. package/dist/src/lib/template-paths.js +1 -0
  89. package/dist/src/lib/template-paths.js.map +1 -0
  90. package/dist/src/lib/template-style-options.js +29 -3
  91. package/dist/src/lib/template-style-options.js.map +1 -0
  92. package/dist/src/lib/time.js +1 -0
  93. package/dist/src/lib/time.js.map +1 -0
  94. package/dist/src/lib/video-quality-harness.js +60 -0
  95. package/dist/src/lib/video-quality-harness.js.map +1 -0
  96. package/dist/src/page-runtime.js +1 -0
  97. package/dist/src/page-runtime.js.map +1 -0
  98. package/dist/src/page-shell.js +403 -29
  99. package/dist/src/page-shell.js.map +1 -0
  100. package/dist/src/primitive-context.js +144 -8
  101. package/dist/src/primitive-context.js.map +1 -0
  102. package/dist/src/primitive-registry.js +1919 -78
  103. package/dist/src/primitive-registry.js.map +1 -0
  104. package/dist/src/primitive-sdk.js +1 -0
  105. package/dist/src/primitive-sdk.js.map +1 -0
  106. package/dist/src/primitives/remotion/html-image.js +3 -1
  107. package/dist/src/primitives/remotion/html-image.js.map +1 -0
  108. package/dist/src/primitives/remotion/media-slideshow.js +60 -0
  109. package/dist/src/primitives/remotion/media-slideshow.js.map +1 -0
  110. package/dist/src/react-page-shell.js +1 -0
  111. package/dist/src/react-page-shell.js.map +1 -0
  112. package/dist/src/ready-post-schedule-component.js +1 -0
  113. package/dist/src/ready-post-schedule-component.js.map +1 -0
  114. package/dist/src/registry.js +237 -24
  115. package/dist/src/registry.js.map +1 -0
  116. package/dist/src/runtime.js +3 -0
  117. package/dist/src/runtime.js.map +1 -0
  118. package/dist/src/services/api-call-history.js +4 -0
  119. package/dist/src/services/api-call-history.js.map +1 -0
  120. package/dist/src/services/auth.js +40 -50
  121. package/dist/src/services/auth.js.map +1 -0
  122. package/dist/src/services/billing.js +20 -44
  123. package/dist/src/services/billing.js.map +1 -0
  124. package/dist/src/services/chat-threads.js +10 -6
  125. package/dist/src/services/chat-threads.js.map +1 -0
  126. package/dist/src/services/fork-access.js +93 -0
  127. package/dist/src/services/fork-access.js.map +1 -0
  128. package/dist/src/services/fork-manifest.js +43 -0
  129. package/dist/src/services/fork-manifest.js.map +1 -0
  130. package/dist/src/services/ghostcut.js +132 -0
  131. package/dist/src/services/ghostcut.js.map +1 -0
  132. package/dist/src/services/hyperframes.js +1014 -0
  133. package/dist/src/services/hyperframes.js.map +1 -0
  134. package/dist/src/services/job-capacity.js +14 -0
  135. package/dist/src/services/job-capacity.js.map +1 -0
  136. package/dist/src/services/job-logs.js +4 -0
  137. package/dist/src/services/job-logs.js.map +1 -0
  138. package/dist/src/services/jobs.js +99 -91
  139. package/dist/src/services/jobs.js.map +1 -0
  140. package/dist/src/services/media-processing.js +743 -0
  141. package/dist/src/services/media-processing.js.map +1 -0
  142. package/dist/src/services/primitive-media-lambda.js +280 -0
  143. package/dist/src/services/primitive-media-lambda.js.map +1 -0
  144. package/dist/src/services/providers.js +1560 -178
  145. package/dist/src/services/providers.js.map +1 -0
  146. package/dist/src/services/rate-limits.js +3 -2
  147. package/dist/src/services/rate-limits.js.map +1 -0
  148. package/dist/src/services/remotion.js +495 -92
  149. package/dist/src/services/remotion.js.map +1 -0
  150. package/dist/src/services/serverless-auth.js +374 -0
  151. package/dist/src/services/serverless-auth.js.map +1 -0
  152. package/dist/src/services/serverless-jobs.js +1074 -0
  153. package/dist/src/services/serverless-jobs.js.map +1 -0
  154. package/dist/src/services/serverless-provider-keys.js +401 -0
  155. package/dist/src/services/serverless-provider-keys.js.map +1 -0
  156. package/dist/src/services/serverless-records.js +1088 -0
  157. package/dist/src/services/serverless-records.js.map +1 -0
  158. package/dist/src/services/serverless-template-configs.js +67 -0
  159. package/dist/src/services/serverless-template-configs.js.map +1 -0
  160. package/dist/src/services/storage.js +171 -35
  161. package/dist/src/services/storage.js.map +1 -0
  162. package/dist/src/services/template-certification.js +295 -3
  163. package/dist/src/services/template-certification.js.map +1 -0
  164. package/dist/src/services/template-loader.js +45 -1
  165. package/dist/src/services/template-loader.js.map +1 -0
  166. package/dist/src/services/template-runtime-bundles.js +217 -0
  167. package/dist/src/services/template-runtime-bundles.js.map +1 -0
  168. package/dist/src/services/template-sources.js +452 -87
  169. package/dist/src/services/template-sources.js.map +1 -0
  170. package/dist/src/services/video-normalization.js +2 -0
  171. package/dist/src/services/video-normalization.js.map +1 -0
  172. package/dist/src/services/webhooks.js +7 -6
  173. package/dist/src/services/webhooks.js.map +1 -0
  174. package/dist/src/template-editor-pages.js +2051 -1869
  175. package/dist/src/template-editor-pages.js.map +1 -0
  176. package/dist/src/template-editor-shell.js +1376 -181
  177. package/dist/src/template-editor-shell.js.map +1 -0
  178. package/dist/src/template-sdk.js +1 -0
  179. package/dist/src/template-sdk.js.map +1 -0
  180. package/dist/src/worker.js +11 -226
  181. package/dist/src/worker.js.map +1 -0
  182. package/package.json +47 -12
  183. package/public/assets/homepage-client-app.js +34 -8
  184. package/public/assets/page-runtime-client-app.js +44 -18
  185. package/readme.secret.md +89 -0
  186. package/templates/vidfarm_template_0000/README.md +106 -0
  187. package/templates/vidfarm_template_0000/SKILL.md +266 -0
  188. package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
  189. package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  190. package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
  191. package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
  192. package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  193. package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
  194. package/templates/vidfarm_template_0000/composition.json +11 -0
  195. package/templates/vidfarm_template_0000/src/lib/images.js +221 -0
  196. package/templates/vidfarm_template_0000/src/remotion/Root.js +33 -0
  197. package/templates/vidfarm_template_0000/src/remotion/index.js +3 -0
  198. package/templates/vidfarm_template_0000/src/sdk.js +3 -0
  199. package/templates/vidfarm_template_0000/src/style-options.js +200 -0
  200. package/templates/vidfarm_template_0000/src/template-dna.js +9 -0
  201. package/templates/vidfarm_template_0000/src/template.js +1566 -0
  202. package/templates/vidfarm_template_0000/template.config.json +21 -0
  203. package/GETTING_STARTED.developers.md +0 -87
  204. package/SKILL.developer.md +0 -1186
  205. package/dist/src/db.js +0 -2007
@@ -1,1829 +1,2086 @@
1
- import { escapeAttribute, escapeHtml, renderPageShell, withAccountQuery } from "./page-shell.js";
2
- import { renderTemplateEditorChrome, TEMPLATE_EDITOR_SHELL_SCRIPT, TEMPLATE_EDITOR_SHELL_STYLES } from "./template-editor-shell.js";
3
- export function renderTemplateEditorDocsPage(input) {
4
- const backHref = withAccountQuery("/discover", input.account.userId);
5
- const hasHistoryAccess = input.account.isLoggedIn;
6
- const preview = input.template.previewUrl
7
- ? renderPreview(input.template.previewUrl, `${input.template.title} preview`)
8
- : `<div class="editor-preview editor-preview-empty">No preview asset</div>`;
1
+ import { renderBrandLockup, renderPageShell, renderPrimaryNav } from "./page-shell.js";
2
+ export function renderCustomInpaintPage(input) {
9
3
  return renderPageShell({
10
- title: `${input.template.title} editor`,
4
+ title: "Inpaint",
11
5
  mainClass: "is-wide",
12
- runtimeBoot: input.editorChat ? {
13
- pageKind: "legacy",
14
- title: `${input.template.title} editor`,
15
- userId: input.account.userId ?? null,
16
- editorChat: input.editorChat
17
- } : undefined,
18
6
  style: `
19
- ${TEMPLATE_EDITOR_SHELL_STYLES}
20
-
21
- .editor-workbench,
22
- .editor-main,
23
- .editor-shell {
24
- padding-bottom: 0;
25
- }
26
-
27
- .editor-shell {
28
- display: grid;
29
- gap: 18px;
7
+ .custom-inpaint-frame {
8
+ height: 100%;
30
9
  min-height: 0;
31
- grid-template-rows: auto auto minmax(0, 1fr);
32
10
  }
33
11
 
34
- .editor-hero,
35
- .editor-section {
36
- border: 1px solid rgba(102, 82, 43, 0.14);
37
- border-radius: 28px;
38
- background: rgba(255, 251, 244, 0.78);
39
- box-shadow: 0 14px 34px rgba(15, 23, 42, 0.04);
12
+ .custom-inpaint-shell {
13
+ height: 100%;
14
+ min-height: 0;
15
+ display: grid;
16
+ grid-template-rows: auto minmax(0, 1fr);
17
+ gap: 14px;
40
18
  }
41
19
 
42
- .editor-panel {
43
- border: 1px solid rgba(102, 82, 43, 0.14);
44
- border-radius: 28px;
45
- background: rgba(255, 251, 244, 0.78);
46
- box-shadow: 0 14px 34px rgba(15, 23, 42, 0.04);
20
+ .custom-inpaint-workbench {
21
+ height: 100%;
22
+ min-height: 0;
23
+ display: grid;
24
+ grid-template-columns: 280px minmax(0, 1fr) 340px;
25
+ gap: 14px;
47
26
  overflow: hidden;
48
27
  }
49
28
 
50
- .editor-panel > summary {
51
- display: flex;
52
- align-items: center;
53
- justify-content: space-between;
54
- gap: 16px;
55
- padding: 18px 22px;
56
- cursor: pointer;
57
- list-style: none;
58
- }
59
-
60
- .editor-panel > summary::-webkit-details-marker {
61
- display: none;
29
+ .custom-inpaint-panel,
30
+ .custom-inpaint-stage,
31
+ .custom-inpaint-output,
32
+ .editor-inpaint-panel {
33
+ border: 1px solid rgba(191, 164, 109, 0.24);
34
+ border-radius: 18px;
35
+ background: rgba(255, 255, 255, 0.74);
62
36
  }
63
37
 
64
- .editor-panel-summary {
38
+ .custom-inpaint-panel {
39
+ min-height: 0;
65
40
  display: grid;
66
- gap: 4px;
41
+ align-content: start;
42
+ gap: 12px;
43
+ padding: 14px;
44
+ overflow: auto;
67
45
  }
68
46
 
69
- .editor-panel-summary h2 {
47
+ .custom-inpaint-panel h1,
48
+ .custom-inpaint-output h2 {
70
49
  margin: 0;
71
- font-size: 1.55rem;
72
- letter-spacing: -0.04em;
50
+ color: #253041;
51
+ font-size: 1.02rem;
73
52
  }
74
53
 
75
- .editor-panel-summary p {
54
+ .custom-inpaint-panel p,
55
+ .custom-inpaint-status,
56
+ .editor-inpaint-label {
76
57
  margin: 0;
77
58
  color: #617087;
78
- line-height: 1.6;
59
+ font-size: 0.8rem;
60
+ line-height: 1.45;
79
61
  }
80
62
 
81
- .editor-panel-toggle {
82
- display: inline-flex;
63
+ .custom-inpaint-panel-head,
64
+ .custom-inpaint-tool-head {
65
+ display: flex;
83
66
  align-items: center;
67
+ justify-content: space-between;
84
68
  gap: 10px;
85
- color: #445066;
86
- font-size: 0.82rem;
87
- font-weight: 700;
88
- letter-spacing: 0.08em;
89
- text-transform: uppercase;
90
- white-space: nowrap;
91
69
  }
92
70
 
93
- .editor-panel-toggle::before {
94
- content: "";
95
- width: 11px;
96
- height: 11px;
97
- border-right: 2px solid currentColor;
98
- border-bottom: 2px solid currentColor;
99
- transform: rotate(45deg) translateY(-2px);
100
- transition: transform 180ms ease;
71
+ .custom-inpaint-upload {
72
+ min-height: 96px;
73
+ display: grid;
74
+ place-items: center;
75
+ gap: 5px;
76
+ padding: 12px;
77
+ border: 1px dashed rgba(97, 112, 135, 0.36);
78
+ border-radius: 16px;
79
+ background: rgba(255, 251, 244, 0.72);
80
+ color: #253041;
81
+ cursor: pointer;
101
82
  }
102
83
 
103
- .editor-panel[open] .editor-panel-toggle::before {
104
- transform: rotate(225deg) translateY(-1px);
84
+ .custom-inpaint-upload svg {
85
+ width: 22px;
86
+ height: 22px;
105
87
  }
106
88
 
107
- .editor-panel-body {
108
- padding: 0 22px 22px;
89
+ .custom-inpaint-upload span {
90
+ max-width: 100%;
91
+ overflow-wrap: anywhere;
92
+ word-break: break-word;
93
+ text-align: center;
94
+ line-height: 1.25;
109
95
  }
110
96
 
111
- .editor-panel-body > .editor-hero,
112
- .editor-panel-body > .editor-section {
113
- border: 0;
114
- border-radius: 0;
115
- background: transparent;
116
- box-shadow: none;
117
- padding: 0;
97
+ .custom-inpaint-stage {
98
+ position: relative;
99
+ min-height: 0;
100
+ display: grid;
101
+ place-items: center;
102
+ overflow: auto;
103
+ background:
104
+ linear-gradient(45deg, rgba(255,255,255,0.06) 25%, transparent 25%),
105
+ linear-gradient(-45deg, rgba(255,255,255,0.06) 25%, transparent 25%),
106
+ linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.06) 75%),
107
+ linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.06) 75%),
108
+ #252d3d;
109
+ background-size: 22px 22px;
110
+ background-position: 0 0, 0 11px, 11px -11px, -11px 0;
118
111
  }
119
112
 
120
- .editor-hero {
113
+ .custom-inpaint-empty {
114
+ position: absolute;
115
+ inset: 0;
116
+ z-index: 1;
121
117
  display: grid;
122
- grid-template-columns: minmax(280px, 420px) minmax(0, 1fr);
123
- gap: 22px;
124
- padding: 22px;
118
+ place-items: center;
119
+ padding: 24px;
120
+ text-align: center;
121
+ color: rgba(255, 251, 244, 0.74);
122
+ font-weight: 700;
123
+ pointer-events: none;
125
124
  }
126
125
 
127
- .editor-preview {
128
- width: 100%;
129
- min-height: 320px;
130
- max-height: 560px;
131
- object-fit: contain;
132
- border-radius: 24px;
133
- border: 1px solid rgba(209, 219, 233, 0.96);
134
- background: rgba(255, 255, 255, 0.92);
126
+ .custom-inpaint-empty[hidden] {
127
+ display: none;
135
128
  }
136
129
 
137
- .editor-preview-empty {
138
- display: grid;
139
- place-items: center;
140
- color: #74829c;
130
+ .editor-inpaint-canvas-wrap {
131
+ position: relative;
132
+ display: inline-block;
133
+ max-width: 100%;
134
+ line-height: 0;
135
+ --inpaint-brush-cursor-size: 70px;
136
+ --inpaint-brush-cursor-x: 0px;
137
+ --inpaint-brush-cursor-y: 0px;
141
138
  }
142
139
 
143
- .editor-copy {
144
- display: grid;
145
- align-content: start;
146
- gap: 18px;
147
- min-width: 0;
140
+ .editor-inpaint-canvas-wrap img {
141
+ max-width: 100%;
142
+ max-height: calc(100vh - 180px);
143
+ display: block;
144
+ object-fit: contain;
145
+ user-select: none;
148
146
  }
149
147
 
150
- .editor-kicker {
151
- color: #8d99ae;
152
- font-size: 0.85rem;
153
- text-transform: uppercase;
154
- letter-spacing: 0.12em;
148
+ .editor-inpaint-canvas-wrap canvas {
149
+ position: absolute;
150
+ inset: 0;
151
+ width: 100%;
152
+ height: 100%;
153
+ opacity: 0.86;
154
+ cursor: none;
155
+ touch-action: none;
155
156
  }
156
157
 
157
- .editor-title {
158
- margin: 0;
159
- font-size: clamp(2rem, 3.6vw, 3.1rem);
160
- line-height: 0.94;
161
- letter-spacing: -0.06em;
158
+ .editor-inpaint-brush-cursor {
159
+ position: absolute;
160
+ z-index: 2;
161
+ left: 0;
162
+ top: 0;
163
+ width: var(--inpaint-brush-cursor-size);
164
+ height: var(--inpaint-brush-cursor-size);
165
+ border: 2px solid rgba(255, 255, 255, 0.92);
166
+ border-radius: 999px;
167
+ box-shadow:
168
+ 0 0 0 1px rgba(37, 48, 65, 0.78),
169
+ 0 0 18px rgba(37, 48, 65, 0.28);
170
+ opacity: 0;
171
+ pointer-events: none;
172
+ transform: translate(var(--inpaint-brush-cursor-x), var(--inpaint-brush-cursor-y)) translate(-50%, -50%);
162
173
  }
163
174
 
164
- .editor-copy p {
165
- margin: 0;
166
- color: #5f6c83;
167
- font-size: 1rem;
168
- line-height: 1.7;
175
+ .editor-inpaint-brush-cursor[data-visible="true"] {
176
+ opacity: 1;
177
+ }
178
+
179
+ .editor-inpaint-panel {
180
+ height: 100%;
181
+ max-height: calc(100vh - 150px);
182
+ min-height: 0;
183
+ display: grid;
184
+ grid-template-rows: auto;
185
+ align-content: start;
186
+ gap: 12px;
187
+ overflow-x: hidden;
188
+ overflow-y: auto;
189
+ overscroll-behavior: contain;
190
+ padding: 12px;
169
191
  }
170
192
 
171
- .editor-pills,
172
- .editor-route-list {
193
+ .editor-inpaint-toolbar,
194
+ .editor-inpaint-color-row,
195
+ .custom-inpaint-actions {
173
196
  display: flex;
174
- flex-wrap: wrap;
197
+ align-items: center;
175
198
  gap: 10px;
176
199
  }
177
200
 
178
- .editor-toolbar {
179
- display: flex;
201
+ .editor-inpaint-toolbar,
202
+ .custom-inpaint-actions {
180
203
  flex-wrap: wrap;
181
- gap: 10px;
182
- margin-top: 14px;
183
204
  }
184
205
 
185
- .editor-copy-button {
186
- appearance: none;
187
- border: 1px solid rgba(102, 82, 43, 0.18);
188
- background: rgba(255, 255, 255, 0.88);
189
- color: #2f3c4f;
190
- border-radius: 999px;
191
- padding: 10px 14px;
192
- font: inherit;
193
- font-size: 0.88rem;
194
- font-weight: 700;
195
- cursor: pointer;
206
+ .editor-inpaint-brush {
207
+ display: grid;
208
+ gap: 6px;
209
+ min-width: 150px;
196
210
  }
197
211
 
198
- .editor-copy-button:hover {
199
- border-color: rgba(102, 82, 43, 0.3);
200
- background: #fff;
212
+ .editor-inpaint-brush input[type="range"] {
213
+ width: 100%;
201
214
  }
202
215
 
203
- .editor-pill,
204
- .editor-route-method {
205
- display: inline-flex;
206
- align-items: center;
207
- justify-content: center;
208
- min-height: 42px;
209
- padding: 0 16px;
210
- border-radius: 999px;
211
- border: 1px solid rgba(191, 164, 109, 0.34);
212
- background: rgba(255, 255, 255, 0.9);
216
+ .editor-inpaint-color-picker {
217
+ width: 42px;
218
+ height: 38px;
219
+ padding: 2px;
220
+ border: 1px solid rgba(97, 112, 135, 0.24);
221
+ border-radius: 12px;
222
+ background: #fff;
213
223
  }
214
224
 
215
- .editor-grid {
216
- display: grid;
217
- grid-template-columns: minmax(0, 1fr);
218
- gap: 18px;
219
- min-height: 0;
225
+ .editor-inpaint-swatches {
226
+ display: none;
220
227
  }
221
228
 
222
- .editor-section {
229
+ .editor-inpaint-prompts {
223
230
  display: grid;
224
231
  align-content: start;
225
- gap: 16px;
226
- padding: 22px;
232
+ gap: 10px;
227
233
  }
228
234
 
229
- .editor-section h2,
230
- .editor-section h3 {
231
- margin: 0;
235
+ .editor-inpaint-provider {
236
+ display: none;
237
+ gap: 6px;
232
238
  }
233
239
 
234
- .editor-section h2 {
235
- font-size: 1.55rem;
236
- letter-spacing: -0.04em;
240
+ .editor-inpaint-provider[data-visible="true"] {
241
+ display: grid;
237
242
  }
238
243
 
239
- .editor-section h3 {
240
- font-size: 1rem;
244
+ .editor-inpaint-provider select {
245
+ min-height: 38px;
246
+ padding: 0 34px 0 12px;
247
+ border-radius: 12px;
248
+ border: 1px solid rgba(191, 164, 109, 0.34);
249
+ background: rgba(255, 255, 255, 0.96);
241
250
  color: #243041;
251
+ font: inherit;
252
+ font-weight: 800;
253
+ }
254
+
255
+ .editor-inpaint-color-row {
256
+ position: relative;
257
+ align-items: flex-start;
258
+ padding: 4px;
259
+ border-radius: 16px;
242
260
  }
243
261
 
244
- .editor-section-header {
262
+ .editor-inpaint-color-tools {
263
+ flex: 0 0 auto;
245
264
  display: grid;
265
+ justify-items: center;
246
266
  gap: 6px;
247
267
  }
248
268
 
249
- .editor-section-header p,
250
- .editor-note {
251
- margin: 0;
252
- color: #617087;
253
- line-height: 1.7;
269
+ .editor-inpaint-prompt-stack {
270
+ min-width: 0;
271
+ flex: 1 1 auto;
272
+ display: grid;
273
+ align-content: start;
274
+ gap: 8px;
254
275
  }
255
276
 
256
- .editor-route-card {
277
+ .editor-inpaint-global {
257
278
  display: grid;
258
- gap: 10px;
259
- padding: 16px;
260
- border-radius: 20px;
261
- border: 1px solid rgba(191, 164, 109, 0.22);
262
- background: rgba(255, 255, 255, 0.72);
279
+ align-content: start;
280
+ gap: 8px;
263
281
  }
264
282
 
265
- .editor-route-head {
283
+ .editor-inpaint-prompt-field {
284
+ position: relative;
285
+ min-width: 0;
286
+ display: grid;
287
+ }
288
+
289
+ .editor-inpaint-attachment-tools {
266
290
  display: flex;
267
- flex-wrap: wrap;
268
291
  align-items: center;
269
- gap: 10px;
292
+ flex-wrap: wrap;
293
+ gap: 6px;
270
294
  }
271
295
 
272
- .editor-route-method {
273
- min-height: 30px;
274
- padding: 0 10px;
275
- font-size: 0.77rem;
276
- font-weight: 800;
296
+ .editor-inpaint-attach-button {
297
+ position: absolute;
298
+ right: 8px;
299
+ bottom: 8px;
300
+ transform: translateY(-10px);
301
+ width: 30px;
302
+ height: 30px;
303
+ display: inline-grid;
304
+ place-items: center;
305
+ padding: 0;
306
+ border-radius: 999px;
307
+ border: 1px solid transparent;
308
+ background: rgba(255, 255, 255, 0.72);
309
+ color: rgba(37, 48, 65, 0.56);
310
+ font: inherit;
311
+ cursor: pointer;
277
312
  }
278
313
 
279
- .editor-route-method[data-method="POST"] {
280
- color: #7a4b16;
314
+ .editor-inpaint-attach-button:hover,
315
+ .editor-inpaint-attach-button:focus-visible {
316
+ border-color: rgba(191, 164, 109, 0.34);
317
+ background: rgba(255, 251, 244, 0.94);
318
+ color: #253041;
281
319
  }
282
320
 
283
- .editor-route-method[data-method="GET"] {
284
- color: #1e5c52;
321
+ .editor-inpaint-attach-button svg {
322
+ width: 16px;
323
+ height: 16px;
285
324
  }
286
325
 
287
- .editor-route-path {
288
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
289
- font-size: 0.82rem;
290
- line-height: 1.5;
291
- word-break: break-all;
292
- color: #324156;
326
+ .editor-inpaint-attachment-chips {
327
+ display: flex;
328
+ align-items: center;
329
+ flex-wrap: wrap;
330
+ gap: 6px;
331
+ min-width: 0;
293
332
  }
294
333
 
295
- .editor-code-block {
296
- margin: 0;
297
- padding: 14px 16px;
298
- border-radius: 18px;
299
- background: rgba(37, 45, 61, 0.97);
300
- color: #f7efe1;
301
- overflow: auto;
302
- font-size: 0.82rem;
303
- line-height: 1.55;
334
+ .editor-inpaint-attachment-chip {
335
+ min-width: 0;
336
+ max-width: 210px;
337
+ display: inline-flex;
338
+ align-items: center;
339
+ gap: 4px;
340
+ padding: 1px 4px 1px 1px;
341
+ border-radius: 999px;
342
+ border: 1px solid rgba(191, 164, 109, 0.3);
343
+ background: rgba(247, 241, 230, 0.82);
344
+ color: #253041;
345
+ font-size: 0.74rem;
346
+ font-weight: 800;
304
347
  }
305
348
 
306
- .editor-quickstart-grid {
307
- display: grid;
308
- grid-template-columns: repeat(2, minmax(0, 1fr));
309
- gap: 14px;
349
+ .editor-inpaint-attachment-chip img {
350
+ width: 22px;
351
+ height: 22px;
352
+ min-width: 22px;
353
+ max-width: 22px;
354
+ aspect-ratio: 1 / 1;
355
+ flex: 0 0 22px;
356
+ object-fit: cover;
357
+ border-radius: 999px;
358
+ background: rgba(37, 48, 65, 0.08);
310
359
  }
311
360
 
312
- .editor-quickstart-field {
313
- display: grid;
314
- gap: 8px;
361
+ .editor-inpaint-attachment-chip span {
315
362
  min-width: 0;
363
+ overflow: hidden;
364
+ text-overflow: ellipsis;
365
+ white-space: nowrap;
316
366
  }
317
367
 
318
- .editor-quickstart-field-head {
319
- display: flex;
320
- align-items: center;
321
- justify-content: flex-start;
322
- gap: 8px;
323
- min-width: 0;
368
+ .editor-inpaint-attachment-chip button {
369
+ width: 18px;
370
+ height: 18px;
371
+ min-width: 18px;
372
+ max-width: 18px;
373
+ aspect-ratio: 1 / 1;
374
+ flex: 0 0 18px;
375
+ display: inline-grid;
376
+ place-items: center;
377
+ padding: 0;
378
+ border: 0;
379
+ border-radius: 999px;
380
+ background: rgba(37, 48, 65, 0.1);
381
+ color: #253041;
382
+ font: inherit;
383
+ line-height: 1;
384
+ cursor: pointer;
324
385
  }
325
386
 
326
- .editor-quickstart-field-head span {
327
- color: #324156;
328
- font-size: 0.82rem;
329
- font-weight: 800;
387
+ .editor-inpaint-color-row[data-active="true"] {
388
+ background: rgba(219, 193, 122, 0.16);
330
389
  }
331
390
 
332
- .editor-quickstart-copy {
333
- appearance: none;
334
- box-sizing: border-box;
335
- display: inline-flex;
336
- align-items: center;
337
- justify-content: center;
338
- width: 36px;
339
- min-width: 36px;
340
- height: 36px;
341
- min-height: 36px;
342
- flex: 0 0 36px;
343
- gap: 0;
344
- padding: 0;
391
+ .editor-inpaint-color-dot {
392
+ width: 26px;
393
+ height: 26px;
394
+ min-width: 26px;
395
+ max-width: 26px;
396
+ aspect-ratio: 1 / 1;
397
+ flex: 0 0 26px;
398
+ margin-top: 8px;
345
399
  border-radius: 999px;
346
- border: 1px solid rgba(171, 138, 68, 0.48);
347
- background: #dbc17a;
348
- color: #243041;
400
+ border: 2px solid rgba(37, 48, 65, 0.18);
401
+ background: var(--inpaint-color, #ff2d55);
402
+ box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.8);
349
403
  cursor: pointer;
350
- line-height: 1;
351
- box-shadow:
352
- inset 0 1px 0 rgba(255, 255, 255, 0.58),
353
- 0 6px 14px rgba(102, 82, 43, 0.12);
354
- transition:
355
- transform 120ms ease,
356
- border-color 120ms ease,
357
- background 120ms ease,
358
- color 120ms ease;
359
- }
360
-
361
- .editor-quickstart-copy:hover {
362
- border-color: rgba(102, 82, 43, 0.44);
363
- background: #e4cb84;
364
- color: #243041;
365
404
  }
366
405
 
367
- .editor-quickstart-copy:active {
368
- transform: scale(0.94);
406
+ .editor-inpaint-color-remove {
407
+ width: 22px;
408
+ height: 22px;
409
+ min-width: 22px;
410
+ max-width: 22px;
411
+ aspect-ratio: 1 / 1;
412
+ flex: 0 0 22px;
413
+ display: inline-grid;
414
+ place-items: center;
415
+ padding: 0;
416
+ border: 1px solid rgba(37, 48, 65, 0.12);
417
+ border-radius: 999px;
418
+ background: rgba(255, 255, 255, 0.72);
419
+ color: rgba(37, 48, 65, 0.56);
420
+ cursor: pointer;
369
421
  }
370
422
 
371
- .editor-quickstart-copy::after {
372
- content: "✓";
373
- display: none;
374
- color: currentColor;
375
- font-size: 1rem;
376
- font-weight: 900;
377
- line-height: 1;
423
+ .editor-inpaint-color-remove:hover,
424
+ .editor-inpaint-color-remove:focus-visible {
425
+ border-color: rgba(191, 164, 109, 0.36);
426
+ color: #253041;
378
427
  }
379
428
 
380
- .editor-quickstart-copy[data-copying="true"] {
381
- border-color: rgba(102, 82, 43, 0.5);
382
- background: #ead89d;
383
- color: #243041;
384
- transform: scale(0.96);
429
+ .editor-inpaint-color-remove svg {
430
+ width: 13px;
431
+ height: 13px;
385
432
  }
386
433
 
387
- .editor-quickstart-copy[data-copied="true"] {
388
- border-color: rgba(30, 92, 82, 0.42);
389
- background: #b9e1cf;
390
- color: #1e5c52;
434
+ .editor-inpaint-color-row[data-active="true"] .editor-inpaint-color-dot {
435
+ border-color: #253041;
436
+ box-shadow: 0 0 0 3px rgba(219, 193, 122, 0.45), inset 0 0 0 2px rgba(255, 255, 255, 0.8);
391
437
  }
392
438
 
393
- .editor-quickstart-copy[data-copy-error="true"] {
394
- border-color: rgba(155, 45, 45, 0.42);
395
- background: #f0c5bd;
396
- color: #8d2f24;
439
+ .editor-inpaint-prompt {
440
+ width: 100%;
441
+ min-height: 72px;
442
+ display: block;
443
+ resize: vertical;
444
+ padding: 10px 44px 18px 12px;
445
+ border-radius: 14px;
446
+ border: 1px solid rgba(191, 164, 109, 0.34);
447
+ background: rgba(255, 255, 255, 0.96);
448
+ color: #243041;
449
+ font: inherit;
450
+ line-height: 1.45;
397
451
  }
398
452
 
399
- .editor-quickstart-copy[data-copy-error="true"]::after {
400
- content: "!";
401
- display: block;
453
+ .editor-history-button {
454
+ min-height: 38px;
455
+ display: inline-flex;
456
+ align-items: center;
457
+ justify-content: center;
458
+ gap: 6px;
459
+ padding: 0 12px;
460
+ border-radius: 12px;
461
+ border: 1px solid rgba(191, 164, 109, 0.28);
462
+ background: rgba(255, 255, 255, 0.86);
463
+ color: #253041;
464
+ font-size: 0.82rem;
465
+ font-weight: 800;
466
+ cursor: pointer;
402
467
  }
403
468
 
404
- .editor-quickstart-copy[data-copied="true"]::after {
405
- display: block;
469
+ .editor-history-button.is-primary {
470
+ border-color: rgba(37, 48, 65, 0.9);
471
+ background: #253041;
472
+ color: #fffaf0;
406
473
  }
407
474
 
408
- .editor-quickstart-copy[data-copy-error="true"] svg,
409
- .editor-quickstart-copy[data-copied="true"] svg {
410
- display: none;
475
+ .editor-history-button[disabled] {
476
+ opacity: 0.58;
477
+ cursor: wait;
411
478
  }
412
479
 
413
- .editor-quickstart-copy svg {
414
- display: block;
415
- width: 17px;
416
- height: 17px;
417
- stroke: currentColor;
480
+ .editor-history-button.is-icon {
481
+ width: 38px;
482
+ padding: 0;
483
+ flex: 0 0 auto;
418
484
  }
419
485
 
420
- .editor-quickstart-textarea {
486
+ .editor-history-button.is-block {
421
487
  width: 100%;
422
- min-height: 190px;
423
- resize: vertical;
424
- border: 1px solid rgba(191, 164, 109, 0.24);
425
- border-radius: 18px;
426
- background: rgba(255, 255, 255, 0.72);
427
- color: #324156;
428
- padding: 14px 15px;
429
- font: 0.82rem/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
430
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.76);
431
488
  }
432
489
 
433
- .editor-quickstart-textarea:focus {
434
- outline: 2px solid rgba(219, 193, 122, 0.38);
435
- outline-offset: 2px;
490
+ .editor-history-button svg {
491
+ width: 18px;
492
+ height: 18px;
436
493
  }
437
494
 
438
- .editor-quickstart-samples {
439
- display: grid;
440
- gap: 8px;
495
+ .editor-history-button[data-loading="true"]::before {
496
+ content: "";
497
+ width: 14px;
498
+ height: 14px;
499
+ border-radius: 999px;
500
+ border: 2px solid currentColor;
501
+ border-top-color: transparent;
502
+ animation: editor-inpaint-spin 0.75s linear infinite;
441
503
  }
442
504
 
443
- .editor-quickstart-samples > h3 {
444
- margin: 2px 0 0;
505
+ @keyframes editor-inpaint-spin {
506
+ to { transform: rotate(360deg); }
445
507
  }
446
508
 
447
- .editor-sample-prompt-row {
448
- display: flex;
449
- gap: 10px;
450
- overflow-x: auto;
451
- overflow-y: hidden;
452
- padding: 2px 0 8px;
453
- scrollbar-width: thin;
509
+ .custom-inpaint-settings-wrap {
510
+ position: relative;
454
511
  }
455
512
 
456
- .editor-sample-prompt {
457
- flex: 0 0 min(360px, 86vw);
513
+ .custom-inpaint-settings {
514
+ position: absolute;
515
+ right: 0;
516
+ top: calc(100% + 8px);
517
+ z-index: 20;
518
+ width: 260px;
519
+ display: grid;
520
+ gap: 8px;
521
+ padding: 12px;
522
+ border-radius: 16px;
523
+ border: 1px solid rgba(191, 164, 109, 0.32);
524
+ background: rgba(255, 251, 244, 0.98);
525
+ box-shadow: 0 18px 44px rgba(15, 23, 42, 0.16);
458
526
  }
459
527
 
460
- .editor-sample-prompt .editor-quickstart-textarea {
461
- min-height: 150px;
462
- resize: none;
528
+ .custom-inpaint-settings[hidden],
529
+ .custom-inpaint-output[hidden] {
530
+ display: none;
463
531
  }
464
532
 
465
- .editor-history-toolbar {
466
- display: flex;
467
- flex-wrap: wrap;
468
- align-items: center;
469
- justify-content: space-between;
470
- gap: 12px;
471
- margin-bottom: 18px;
533
+ .custom-inpaint-settings input {
534
+ width: 100%;
535
+ min-height: 38px;
536
+ padding: 0 12px;
537
+ border-radius: 12px;
538
+ border: 1px solid rgba(191, 164, 109, 0.34);
539
+ background: rgba(255, 255, 255, 0.96);
540
+ color: #243041;
541
+ font: inherit;
472
542
  }
473
543
 
474
- .editor-history-toolbar p {
475
- margin: 0;
544
+ .custom-inpaint-output {
545
+ display: grid;
546
+ gap: 10px;
547
+ padding: 12px;
476
548
  }
477
549
 
478
- .editor-history-chip-row {
479
- display: flex;
480
- flex-wrap: wrap;
481
- gap: 8px;
550
+ .custom-inpaint-preview {
551
+ width: 100%;
552
+ display: grid;
553
+ padding: 0;
554
+ border: 0;
555
+ border-radius: 12px;
556
+ background: #252d3d;
557
+ cursor: zoom-in;
482
558
  }
483
559
 
484
- .editor-history-toolbar-actions,
485
- .editor-history-group-actions {
486
- display: flex;
487
- flex-wrap: wrap;
488
- align-items: center;
489
- gap: 8px;
560
+ .custom-inpaint-output img {
561
+ width: 100%;
562
+ max-height: 320px;
563
+ object-fit: contain;
564
+ border-radius: 12px;
565
+ background: #252d3d;
490
566
  }
491
567
 
492
- .editor-history-toolbar-actions {
493
- justify-content: flex-end;
568
+ .custom-inpaint-history {
569
+ display: grid;
570
+ gap: 8px;
571
+ padding: 12px;
572
+ border: 1px solid rgba(191, 164, 109, 0.24);
573
+ border-radius: 18px;
574
+ background: rgba(255, 255, 255, 0.58);
494
575
  }
495
576
 
496
- .editor-history-button,
497
- .editor-history-remove {
498
- display: inline-flex;
577
+ .custom-inpaint-history-head {
578
+ display: flex;
499
579
  align-items: center;
500
- justify-content: center;
501
- min-height: 32px;
502
- padding: 0 12px;
503
- border-radius: 999px;
504
- border: 1px solid rgba(191, 164, 109, 0.34);
505
- background: rgba(255, 255, 255, 0.92);
506
- color: #324156;
507
- font-size: 0.78rem;
508
- font-weight: 700;
509
- cursor: pointer;
510
- }
511
-
512
- .editor-history-remove {
513
- color: #8d4a3b;
580
+ justify-content: space-between;
581
+ gap: 8px;
514
582
  }
515
583
 
516
- .editor-history-button.is-primary {
517
- background: #dbc17a;
518
- color: #243041;
584
+ .custom-inpaint-history-head h2 {
585
+ margin: 0;
586
+ color: #253041;
587
+ font-size: 1rem;
519
588
  }
520
589
 
521
- .editor-history-refresh {
590
+ .custom-inpaint-history-refresh {
522
591
  width: 30px;
523
- min-height: 30px;
592
+ height: 30px;
593
+ min-width: 30px;
524
594
  padding: 0;
525
- border-radius: 999px;
526
595
  }
527
596
 
528
- .editor-history-refresh svg {
597
+ .custom-inpaint-history-refresh svg {
529
598
  width: 15px;
530
599
  height: 15px;
531
600
  }
532
601
 
533
- .editor-history-refresh[data-loading="true"] svg {
534
- animation: editor-history-refresh-spin 0.8s linear infinite;
602
+ .custom-inpaint-history-list {
603
+ display: grid;
604
+ gap: 8px;
605
+ overflow: visible;
606
+ padding: 2px;
607
+ }
608
+
609
+ .custom-inpaint-history-empty {
610
+ color: #617087;
611
+ font-size: 0.8rem;
612
+ line-height: 1.4;
535
613
  }
536
614
 
537
- .editor-history-menu-wrap {
615
+ .custom-inpaint-history-item {
538
616
  position: relative;
539
- display: inline-flex;
617
+ min-width: 0;
618
+ display: grid;
619
+ grid-template-columns: 54px minmax(0, 1fr);
620
+ align-items: center;
621
+ gap: 8px;
622
+ padding: 6px;
623
+ border: 1px solid rgba(191, 164, 109, 0.2);
624
+ border-radius: 12px;
625
+ background: rgba(255, 251, 244, 0.62);
540
626
  }
541
627
 
542
- .editor-history-menu-button {
543
- width: 30px;
544
- min-height: 30px;
628
+ .custom-inpaint-history-thumb-button {
629
+ width: 54px;
630
+ height: 54px;
631
+ display: grid;
545
632
  padding: 0;
633
+ border: 0;
634
+ border-radius: 10px;
635
+ background: transparent;
636
+ cursor: zoom-in;
546
637
  }
547
638
 
548
- .editor-history-menu {
549
- position: absolute;
550
- right: 0;
551
- top: calc(100% + 6px);
552
- z-index: 20;
553
- display: none;
554
- min-width: 148px;
555
- padding: 6px;
556
- border: 1px solid rgba(191, 164, 109, 0.28);
639
+ .custom-inpaint-history-thumb {
640
+ width: 54px;
641
+ height: 54px;
642
+ border: 1px solid rgba(191, 164, 109, 0.3);
557
643
  border-radius: 10px;
558
- background: rgba(255, 252, 247, 0.98);
559
- box-shadow: 0 14px 34px rgba(37, 45, 61, 0.16);
644
+ object-fit: cover;
645
+ background: #252d3d;
560
646
  }
561
647
 
562
- .editor-history-menu-wrap[data-open="true"] .editor-history-menu {
648
+ .custom-inpaint-history-generating {
649
+ width: 54px;
650
+ height: 54px;
563
651
  display: grid;
564
- gap: 2px;
565
- }
566
-
567
- .editor-history-menu button {
568
- all: unset;
569
- padding: 8px 9px;
570
- border-radius: 7px;
571
- color: #324156;
572
- font-size: 0.78rem;
573
- font-weight: 700;
574
- cursor: pointer;
652
+ place-items: center;
653
+ border: 1px solid rgba(191, 164, 109, 0.3);
654
+ border-radius: 10px;
655
+ background: linear-gradient(135deg, rgba(37, 48, 65, 0.92), rgba(122, 75, 22, 0.84));
656
+ color: #fff7e8;
575
657
  }
576
658
 
577
- .editor-history-menu button:hover {
578
- background: rgba(219, 193, 122, 0.16);
659
+ .custom-inpaint-history-generating::before {
660
+ content: "";
661
+ width: 22px;
662
+ height: 22px;
663
+ border-radius: 999px;
664
+ border: 2px solid rgba(255, 247, 232, 0.34);
665
+ border-top-color: #fff7e8;
666
+ animation: editor-inpaint-spin 0.75s linear infinite;
579
667
  }
580
668
 
581
- .editor-history-menu button[data-danger="true"] {
582
- color: #8d4a3b;
669
+ .custom-inpaint-history-meta {
670
+ min-width: 0;
671
+ display: grid;
672
+ gap: 5px;
583
673
  }
584
674
 
585
- .editor-history-group-previews {
586
- display: flex;
587
- flex-wrap: nowrap;
588
- gap: 6px;
589
- margin-top: 10px;
675
+ .custom-inpaint-history-title {
676
+ min-width: 0;
677
+ color: #253041;
678
+ font-size: 0.78rem;
679
+ font-weight: 800;
590
680
  overflow: hidden;
681
+ text-overflow: ellipsis;
682
+ white-space: nowrap;
591
683
  }
592
684
 
593
- .editor-history-group-previews.is-expanded {
594
- max-width: min(100%, calc(100vw - 160px));
595
- overflow-x: auto;
596
- overflow-y: hidden;
597
- padding-bottom: 5px;
598
- scrollbar-width: thin;
599
- }
600
-
601
- .editor-history-group-preview {
602
- position: relative;
603
- width: 52px;
604
- height: 52px;
605
- padding: 0;
606
- border-radius: 9px;
607
- border: 1px solid rgba(191, 164, 109, 0.26);
608
- background: rgba(255, 255, 255, 0.74);
685
+ .custom-inpaint-history-subtitle {
686
+ min-width: 0;
687
+ color: #617087;
688
+ font-size: 0.72rem;
689
+ font-weight: 700;
609
690
  overflow: hidden;
610
- flex: 0 0 auto;
611
- cursor: zoom-in;
691
+ text-overflow: ellipsis;
692
+ white-space: nowrap;
612
693
  }
613
694
 
614
- .editor-history-group-preview img,
615
- .editor-history-group-preview video {
616
- width: 100%;
617
- height: 100%;
618
- object-fit: cover;
619
- display: block;
695
+ .custom-inpaint-history-actions {
696
+ display: flex;
697
+ align-items: center;
698
+ justify-content: flex-start;
699
+ gap: 4px;
620
700
  }
621
701
 
622
- .editor-history-group-preview-more {
623
- display: inline-flex;
624
- align-items: center;
625
- justify-content: center;
702
+ .custom-inpaint-history-icon {
703
+ width: 24px;
704
+ height: 24px;
705
+ display: inline-grid;
706
+ place-items: center;
626
707
  padding: 0;
627
- width: 52px;
628
- height: 52px;
629
- border-radius: 9px;
630
- border: 1px dashed rgba(191, 164, 109, 0.32);
631
- color: #617087;
632
- font-size: 0.75rem;
633
- font-weight: 800;
634
- flex: 0 0 auto;
708
+ border: 1px solid rgba(191, 164, 109, 0.24);
709
+ border-radius: 999px;
710
+ background: rgba(255, 255, 255, 0.88);
711
+ color: #253041;
635
712
  cursor: pointer;
713
+ text-decoration: none;
636
714
  }
637
715
 
638
- button.editor-history-group-preview-more:hover,
639
- button.editor-history-group-preview-more:focus-visible {
640
- border-style: solid;
641
- color: #324156;
716
+ .custom-inpaint-history-check {
717
+ min-height: 24px;
718
+ padding: 0 9px;
719
+ border: 1px solid rgba(191, 164, 109, 0.3);
720
+ border-radius: 999px;
642
721
  background: rgba(255, 255, 255, 0.9);
722
+ color: #253041;
723
+ font-size: 0.72rem;
724
+ font-weight: 850;
725
+ cursor: pointer;
643
726
  }
644
727
 
645
- .editor-history-chip {
646
- display: inline-flex;
647
- align-items: center;
648
- min-height: 30px;
649
- padding: 0 10px;
650
- border-radius: 999px;
651
- border: 1px solid rgba(191, 164, 109, 0.34);
652
- background: rgba(255, 255, 255, 0.9);
653
- color: #3b465a;
654
- font-size: 0.78rem;
655
- font-weight: 700;
728
+ .custom-inpaint-history-check[disabled] {
729
+ cursor: wait;
730
+ opacity: 0.64;
656
731
  }
657
732
 
658
- .editor-history-chip.is-empty {
659
- color: #74829c;
660
- border-style: dashed;
733
+ .custom-inpaint-history-icon svg {
734
+ width: 13px;
735
+ height: 13px;
661
736
  }
662
737
 
663
- .editor-history-groups {
738
+ .custom-inpaint-history-popconfirm {
739
+ position: absolute;
740
+ right: 6px;
741
+ top: 6px;
742
+ z-index: 6;
743
+ width: 156px;
664
744
  display: grid;
665
- gap: 12px;
745
+ gap: 7px;
746
+ padding: 8px;
747
+ border: 1px solid rgba(191, 164, 109, 0.34);
748
+ border-radius: 12px;
749
+ background: rgba(255, 251, 244, 0.98);
750
+ box-shadow: 0 14px 30px rgba(15, 23, 42, 0.16);
666
751
  }
667
752
 
668
- .editor-history-empty {
669
- padding: 18px;
670
- border-radius: 18px;
671
- border: 1px dashed rgba(191, 164, 109, 0.34);
672
- background: rgba(255, 255, 255, 0.56);
673
- color: #617087;
753
+ .custom-inpaint-history-popconfirm[hidden] {
754
+ display: none;
674
755
  }
675
756
 
676
- .editor-history-group {
677
- border: 1px solid rgba(191, 164, 109, 0.22);
678
- border-radius: 20px;
679
- background: rgba(255, 255, 255, 0.62);
680
- overflow: hidden;
757
+ .custom-inpaint-history-popconfirm p {
758
+ margin: 0;
759
+ color: #253041;
760
+ font-size: 0.76rem;
761
+ font-weight: 800;
762
+ line-height: 1.25;
681
763
  }
682
764
 
683
- .editor-history-group > summary {
765
+ .custom-inpaint-history-popconfirm-actions {
684
766
  display: flex;
685
- align-items: center;
686
- justify-content: space-between;
687
- gap: 12px;
688
- padding: 14px 16px;
689
- cursor: pointer;
690
- list-style: none;
767
+ gap: 5px;
691
768
  }
692
769
 
693
- .editor-history-group > summary::-webkit-details-marker {
694
- display: none;
695
- }
696
-
697
- .editor-history-group-main {
698
- display: grid;
699
- gap: 4px;
700
- min-width: 0;
770
+ .custom-inpaint-history-popconfirm-actions button {
771
+ flex: 1 1 0;
772
+ min-height: 26px;
773
+ padding: 0 7px;
774
+ border-radius: 9px;
775
+ border: 1px solid rgba(191, 164, 109, 0.28);
776
+ background: rgba(255, 255, 255, 0.92);
777
+ color: #253041;
778
+ font: inherit;
779
+ font-size: 0.72rem;
780
+ font-weight: 800;
781
+ cursor: pointer;
701
782
  }
702
783
 
703
- .editor-history-group-main h3 {
704
- margin: 0;
705
- font-size: 1rem;
706
- color: #243041;
707
- word-break: break-all;
784
+ .custom-inpaint-history-popconfirm-actions button[data-danger="true"] {
785
+ border-color: rgba(155, 45, 45, 0.3);
786
+ color: #9b2d2d;
708
787
  }
709
788
 
710
- .editor-history-group-main p {
711
- margin: 0;
712
- color: #74829c;
713
- font-size: 0.82rem;
789
+ .custom-inpaint-status[data-tone="error"] {
790
+ color: #9b2d2d;
714
791
  }
715
792
 
716
- .editor-history-group-toggle {
717
- color: #617087;
718
- font-size: 0.8rem;
719
- font-weight: 700;
720
- letter-spacing: 0.08em;
721
- text-transform: uppercase;
793
+ .custom-inpaint-status[data-tone="success"] {
794
+ color: #2f7d4f;
722
795
  }
723
796
 
724
- .editor-history-active {
725
- display: inline-flex;
726
- align-items: center;
727
- min-height: 26px;
728
- padding: 0 9px;
729
- border-radius: 999px;
730
- background: rgba(219, 193, 122, 0.2);
731
- color: #7a4b16;
732
- font-size: 0.74rem;
733
- font-weight: 800;
734
- letter-spacing: 0.08em;
735
- text-transform: uppercase;
797
+ .custom-inpaint-lightbox {
798
+ position: fixed;
799
+ inset: 0;
800
+ z-index: 80;
801
+ display: none;
802
+ place-items: center;
803
+ padding: 22px;
804
+ background: rgba(15, 23, 42, 0.72);
805
+ backdrop-filter: blur(12px);
736
806
  }
737
807
 
738
- @keyframes editor-history-refresh-spin {
739
- to {
740
- transform: rotate(360deg);
741
- }
808
+ .custom-inpaint-lightbox[data-open="true"] {
809
+ display: grid;
742
810
  }
743
811
 
744
- .editor-history-group-body {
745
- display: grid;
746
- gap: 10px;
747
- padding: 0 16px 16px;
812
+ .custom-inpaint-lightbox[hidden] {
813
+ display: none;
748
814
  }
749
815
 
750
- .editor-history-card {
816
+ .custom-inpaint-lightbox-dialog {
817
+ width: min(1100px, 96vw);
818
+ max-height: 94vh;
751
819
  display: grid;
752
- gap: 8px;
753
- padding: 10px;
754
- border-radius: 12px;
755
- border: 1px solid rgba(191, 164, 109, 0.18);
756
- background: rgba(255, 251, 244, 0.92);
820
+ grid-template-rows: auto minmax(0, 1fr);
821
+ gap: 12px;
822
+ padding: 12px;
823
+ border: 1px solid rgba(255, 251, 244, 0.18);
824
+ border-radius: 18px;
825
+ background: rgba(37, 45, 61, 0.96);
826
+ box-shadow: 0 24px 72px rgba(0, 0, 0, 0.38);
757
827
  }
758
828
 
759
- .editor-history-card-head {
829
+ .custom-inpaint-lightbox-head {
760
830
  display: flex;
761
- flex-wrap: wrap;
831
+ align-items: center;
762
832
  justify-content: space-between;
763
833
  gap: 10px;
764
834
  }
765
835
 
766
- .editor-history-card-main {
767
- display: grid;
768
- gap: 6px;
836
+ .custom-inpaint-lightbox-title {
769
837
  min-width: 0;
838
+ margin: 0;
839
+ color: #fffaf0;
840
+ font-size: 0.9rem;
841
+ font-weight: 800;
842
+ overflow: hidden;
843
+ text-overflow: ellipsis;
844
+ white-space: nowrap;
770
845
  }
771
846
 
772
- .editor-history-card-meta {
847
+ .custom-inpaint-lightbox-actions {
773
848
  display: flex;
774
- flex-wrap: wrap;
775
- gap: 8px;
776
- }
777
-
778
- .editor-history-method,
779
- .editor-history-status {
780
- display: inline-flex;
781
849
  align-items: center;
782
- min-height: 28px;
783
- padding: 0 10px;
784
- border-radius: 999px;
785
- border: 1px solid rgba(191, 164, 109, 0.3);
786
- background: rgba(255, 255, 255, 0.92);
787
- font-size: 0.78rem;
788
- font-weight: 800;
850
+ gap: 8px;
789
851
  }
790
852
 
791
- .editor-history-method {
792
- color: #7a4b16;
853
+ .custom-inpaint-lightbox-stage {
854
+ min-height: 0;
855
+ display: grid;
856
+ place-items: center;
857
+ overflow: auto;
793
858
  }
794
859
 
795
- .editor-history-status.is-success {
796
- color: #1e5c52;
860
+ .custom-inpaint-lightbox-stage img {
861
+ max-width: 100%;
862
+ max-height: calc(94vh - 92px);
863
+ object-fit: contain;
864
+ border-radius: 12px;
865
+ background: #111827;
797
866
  }
798
867
 
799
- .editor-history-status.is-error {
800
- color: #9b2d2d;
801
- }
868
+ @media (max-width: 1100px) {
869
+ .custom-inpaint-workbench {
870
+ grid-template-columns: 1fr;
871
+ overflow: auto;
872
+ }
802
873
 
803
- .editor-history-card-path {
804
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
805
- font-size: 0.8rem;
806
- line-height: 1.55;
807
- color: #324156;
808
- word-break: break-all;
809
- }
810
-
811
- .editor-history-card-time {
812
- color: #74829c;
813
- font-size: 0.8rem;
814
- white-space: nowrap;
815
- }
816
-
817
- .editor-history-card-previews {
818
- display: flex;
819
- flex-wrap: wrap;
820
- gap: 8px;
821
- }
822
-
823
- .editor-history-card-preview {
824
- position: relative;
825
- width: 112px;
826
- height: 112px;
827
- display: inline-flex;
828
- align-items: center;
829
- justify-content: center;
830
- overflow: hidden;
831
- padding: 0;
832
- border-radius: 18px;
833
- border: 1px solid rgba(191, 164, 109, 0.34);
834
- background: rgba(255, 255, 255, 0.94);
835
- color: #324156;
836
- text-decoration: none;
837
- cursor: pointer;
838
- }
839
-
840
- .editor-history-video-play {
841
- position: absolute;
842
- left: 50%;
843
- top: 50%;
844
- width: 34px;
845
- height: 34px;
846
- transform: translate(-50%, -50%);
847
- display: inline-flex;
848
- align-items: center;
849
- justify-content: center;
850
- border-radius: 999px;
851
- background: rgba(18, 24, 34, 0.72);
852
- box-shadow: 0 8px 24px rgba(12, 18, 28, 0.22);
853
- pointer-events: none;
854
- }
855
-
856
- .editor-history-group-preview .editor-history-video-play {
857
- width: 24px;
858
- height: 24px;
859
- }
860
-
861
- .editor-history-video-play::before {
862
- content: "";
863
- display: block;
864
- width: 0;
865
- height: 0;
866
- margin-left: 3px;
867
- border-top: 7px solid transparent;
868
- border-bottom: 7px solid transparent;
869
- border-left: 11px solid #fff8ee;
870
- }
871
-
872
- .editor-history-group-preview .editor-history-video-play::before {
873
- margin-left: 2px;
874
- border-top-width: 5px;
875
- border-bottom-width: 5px;
876
- border-left-width: 8px;
877
- }
878
-
879
- .editor-history-card-preview img,
880
- .editor-history-card-preview video {
881
- width: 100%;
882
- height: 100%;
883
- object-fit: cover;
884
- display: block;
885
- }
886
-
887
- .editor-history-card-preview-label {
888
- position: absolute;
889
- left: 8px;
890
- right: 8px;
891
- bottom: 8px;
892
- min-height: 22px;
893
- display: inline-flex;
894
- align-items: center;
895
- justify-content: center;
896
- padding: 0 8px;
897
- border-radius: 999px;
898
- background: rgba(24, 30, 42, 0.72);
899
- color: #fff8ee;
900
- font-size: 0.72rem;
901
- font-weight: 700;
902
- letter-spacing: 0.02em;
903
- }
904
-
905
- .editor-history-card-preview-fallback {
906
- padding: 0 10px;
907
- text-align: center;
908
- font-size: 0.78rem;
909
- font-weight: 700;
910
- line-height: 1.4;
911
- }
912
-
913
- .editor-history-card-grid {
914
- display: grid;
915
- grid-template-columns: repeat(2, minmax(0, 1fr));
916
- gap: 12px;
917
- }
918
-
919
- .editor-history-card .vf-editor-chat-http-card {
920
- background: rgba(248, 244, 235, 0.74);
921
- }
922
-
923
- .editor-history-card-foot {
924
- display: flex;
925
- justify-content: flex-end;
926
- color: #7b879b;
927
- font-size: 0.72rem;
928
- line-height: 1.3;
929
- }
930
-
931
- .editor-history-sample {
932
- display: grid;
933
- gap: 8px;
934
- }
935
-
936
- .editor-history-sample h4 {
937
- margin: 0;
938
- font-size: 0.82rem;
939
- color: #445066;
940
- text-transform: uppercase;
941
- letter-spacing: 0.08em;
942
- }
943
-
944
- .editor-history-sample pre {
945
- margin: 0;
946
- min-height: 90px;
947
- max-height: 280px;
948
- overflow: auto;
949
- padding: 12px;
950
- border-radius: 14px;
951
- background: rgba(37, 45, 61, 0.96);
952
- color: #f7efe1;
953
- font-size: 0.78rem;
954
- line-height: 1.5;
955
- }
956
-
957
- .editor-history-sample-note {
958
- color: #74829c;
959
- font-size: 0.78rem;
960
- }
961
-
962
- .editor-modal-backdrop {
963
- position: fixed;
964
- inset: 0;
965
- display: none;
966
- align-items: center;
967
- justify-content: center;
968
- padding: 24px;
969
- background: rgba(24, 30, 42, 0.52);
970
- backdrop-filter: blur(10px);
971
- z-index: 1200;
972
- }
973
-
974
- .editor-modal-backdrop[data-open="true"] {
975
- display: flex;
874
+ .editor-inpaint-panel {
875
+ max-height: none;
876
+ }
976
877
  }
878
+ `,
879
+ body: `
880
+ <section class="frame custom-inpaint-frame">
881
+ <header class="topbar">
882
+ ${renderBrandLockup({ displayName: input.account.name, email: input.account.email, accountId: input.account.userId })}
883
+ <div class="topbar-actions">
884
+ ${renderPrimaryNav({ loggedIn: true, current: "inpaint", accountId: input.account.userId })}
885
+ </div>
886
+ </header>
887
+ <div class="frame-body custom-inpaint-shell">
888
+ <section class="custom-inpaint-workbench">
889
+ <aside class="custom-inpaint-panel">
890
+ <div class="custom-inpaint-panel-head">
891
+ <h1>Inpaint</h1>
892
+ <button type="button" class="editor-history-button" data-custom-inpaint-start-new>Start New</button>
893
+ </div>
894
+ <p>Upload a source image, paint replacement regions, then create a reusable image edit for a timeline layer.</p>
895
+ <button type="button" class="custom-inpaint-upload" data-custom-inpaint-upload>
896
+ <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 14V4"></path><path d="m6.5 7.5 3.5-3.5 3.5 3.5"></path><path d="M4 15.8h12"></path></svg>
897
+ <strong>Add Source Image</strong>
898
+ <span data-custom-inpaint-source-name>No image selected</span>
899
+ </button>
900
+ <input data-custom-inpaint-file type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" hidden />
901
+ <p class="custom-inpaint-status" data-custom-inpaint-status aria-live="polite"></p>
902
+ <div class="custom-inpaint-output" data-custom-inpaint-output hidden>
903
+ <h2>Result</h2>
904
+ <button type="button" class="custom-inpaint-preview" data-custom-inpaint-result-open aria-label="View inpaint result">
905
+ <img data-custom-inpaint-result alt="Image edit result" />
906
+ </button>
907
+ <a class="editor-history-button is-primary" data-custom-inpaint-download href="#" target="_blank" rel="noopener noreferrer" download>Download</a>
908
+ </div>
909
+ <section class="custom-inpaint-history" aria-label="Image edit job history">
910
+ <div class="custom-inpaint-history-head">
911
+ <h2>History</h2>
912
+ <button type="button" class="editor-history-button custom-inpaint-history-refresh" data-custom-inpaint-history-refresh aria-label="Refresh image edit history" title="Refresh history">
913
+ <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="M16.5 10a6.5 6.5 0 1 1-1.9-4.6"></path><path d="M16.5 4.2v4.2h-4.2"></path></svg>
914
+ </button>
915
+ </div>
916
+ <div class="custom-inpaint-history-list" data-custom-inpaint-history-list>
917
+ <div class="custom-inpaint-history-empty">Loading history...</div>
918
+ </div>
919
+ </section>
920
+ </aside>
921
+ <main class="custom-inpaint-stage">
922
+ <div class="custom-inpaint-empty" data-custom-inpaint-empty>Upload an image to start painting</div>
923
+ <div class="editor-inpaint-canvas-wrap" data-custom-inpaint-canvas-wrap hidden>
924
+ <img data-editor-inpaint-image alt="" />
925
+ <canvas data-editor-inpaint-canvas></canvas>
926
+ <div class="editor-inpaint-brush-cursor" data-editor-inpaint-brush-cursor aria-hidden="true"></div>
927
+ </div>
928
+ </main>
929
+ <aside class="editor-inpaint-panel" aria-label="Image edit controls">
930
+ <div class="custom-inpaint-tool-head">
931
+ <div class="custom-inpaint-actions" aria-label="Mask history">
932
+ <button type="button" class="editor-history-button is-icon" data-editor-inpaint-undo aria-label="Undo paint stroke" title="Undo">
933
+ <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="M7.2 7H3.8V3.6"></path><path d="M4 7a7 7 0 1 1 1.9 6.6"></path></svg>
934
+ </button>
935
+ <button type="button" class="editor-history-button is-icon" data-editor-inpaint-redo aria-label="Redo paint stroke" title="Redo">
936
+ <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="M12.8 7h3.4V3.6"></path><path d="M16 7a7 7 0 1 0-1.9 6.6"></path></svg>
937
+ </button>
938
+ </div>
939
+ <div class="custom-inpaint-settings-wrap">
940
+ <button type="button" class="editor-history-button is-icon" data-custom-inpaint-settings-button aria-label="Image edit settings" title="Settings">
941
+ <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.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"></path><circle cx="10" cy="8" r="2.4"></circle></svg>
942
+ </button>
943
+ <div class="custom-inpaint-settings" data-custom-inpaint-settings hidden>
944
+ <label>
945
+ <span class="editor-inpaint-label">Tracer</span>
946
+ <input data-custom-inpaint-tracer type="text" autocomplete="off" spellcheck="false" placeholder="image-edit" />
947
+ </label>
948
+ </div>
949
+ </div>
950
+ </div>
951
+ <div class="editor-inpaint-toolbar">
952
+ <label class="editor-inpaint-brush">
953
+ <span class="editor-inpaint-label">Brush <span data-editor-inpaint-brush-label>70</span></span>
954
+ <input data-editor-inpaint-brush type="range" min="6" max="180" step="1" value="70" />
955
+ </label>
956
+ <input class="editor-inpaint-color-picker" data-editor-inpaint-color-picker type="color" value="#ff2d55" aria-label="Mask color" />
957
+ <label class="editor-inpaint-provider" data-editor-inpaint-provider-wrap>
958
+ <span class="editor-inpaint-label">Provider</span>
959
+ <select data-editor-inpaint-provider aria-label="AI provider"></select>
960
+ </label>
961
+ </div>
962
+ <div class="editor-inpaint-global">
963
+ <span class="editor-inpaint-label">Whole image prompt</span>
964
+ <div class="editor-inpaint-prompt-field">
965
+ <textarea class="editor-inpaint-prompt" data-editor-inpaint-global-prompt placeholder="Describe the overall edit, style, constraints, or preservation notes."></textarea>
966
+ <button type="button" class="editor-inpaint-attach-button" data-editor-inpaint-attach-scope="global" aria-label="Attach reference image" title="Attach reference image">
967
+ <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>
968
+ </button>
969
+ </div>
970
+ <div class="editor-inpaint-attachment-tools">
971
+ <span class="editor-inpaint-attachment-chips" data-editor-inpaint-attachments="global"></span>
972
+ </div>
973
+ </div>
974
+ <input data-editor-inpaint-attachment-input type="file" accept="image/jpeg,image/png,image/webp" multiple hidden />
975
+ <div class="editor-inpaint-swatches" data-editor-inpaint-swatches aria-label="Mask colors"></div>
976
+ <div class="editor-inpaint-prompts" data-editor-inpaint-prompts></div>
977
+ <button type="button" class="editor-history-button is-block" data-editor-inpaint-add-color>Add color</button>
978
+ <div class="custom-inpaint-actions">
979
+ <button type="button" class="editor-history-button" data-editor-inpaint-clear>Clear</button>
980
+ <button type="button" class="editor-history-button is-primary" data-editor-inpaint-submit><span data-editor-inpaint-submit-label>Create image edit</span></button>
981
+ </div>
982
+ </aside>
983
+ </section>
984
+ </div>
985
+ </section>
986
+ <div class="custom-inpaint-lightbox" data-custom-inpaint-lightbox data-open="false" hidden>
987
+ <div class="custom-inpaint-lightbox-dialog" role="dialog" aria-modal="true" aria-labelledby="custom-inpaint-lightbox-title">
988
+ <div class="custom-inpaint-lightbox-head">
989
+ <p class="custom-inpaint-lightbox-title" id="custom-inpaint-lightbox-title" data-custom-inpaint-lightbox-title>Image edit output</p>
990
+ <div class="custom-inpaint-lightbox-actions">
991
+ <button type="button" class="editor-history-button is-icon" data-custom-inpaint-lightbox-copy aria-label="Copy image URL" title="Copy URL">
992
+ <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>
993
+ </button>
994
+ <a class="editor-history-button is-primary" data-custom-inpaint-lightbox-download href="#" target="_blank" rel="noopener noreferrer" download>Download</a>
995
+ <button type="button" class="editor-history-button is-icon" data-custom-inpaint-lightbox-close aria-label="Close preview" title="Close">
996
+ <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>
997
+ </button>
998
+ </div>
999
+ </div>
1000
+ <div class="custom-inpaint-lightbox-stage">
1001
+ <img data-custom-inpaint-lightbox-image alt="Image edit output preview" />
1002
+ </div>
1003
+ </div>
1004
+ </div>
1005
+ <script>
1006
+ (() => {
1007
+ const uploadButton = document.querySelector("[data-custom-inpaint-upload]");
1008
+ const startNewButton = document.querySelector("[data-custom-inpaint-start-new]");
1009
+ const fileInput = document.querySelector("[data-custom-inpaint-file]");
1010
+ const sourceName = document.querySelector("[data-custom-inpaint-source-name]");
1011
+ const statusEl = document.querySelector("[data-custom-inpaint-status]");
1012
+ const emptyState = document.querySelector("[data-custom-inpaint-empty]");
1013
+ const canvasWrap = document.querySelector("[data-custom-inpaint-canvas-wrap]");
1014
+ const outputPanel = document.querySelector("[data-custom-inpaint-output]");
1015
+ const resultOpenButton = document.querySelector("[data-custom-inpaint-result-open]");
1016
+ const resultImage = document.querySelector("[data-custom-inpaint-result]");
1017
+ const resultDownload = document.querySelector("[data-custom-inpaint-download]");
1018
+ const historyList = document.querySelector("[data-custom-inpaint-history-list]");
1019
+ const historyRefresh = document.querySelector("[data-custom-inpaint-history-refresh]");
1020
+ const inpaintImage = document.querySelector("[data-editor-inpaint-image]");
1021
+ const inpaintCanvas = document.querySelector("[data-editor-inpaint-canvas]");
1022
+ const inpaintBrushCursor = document.querySelector("[data-editor-inpaint-brush-cursor]");
1023
+ const inpaintUndo = document.querySelector("[data-editor-inpaint-undo]");
1024
+ const inpaintRedo = document.querySelector("[data-editor-inpaint-redo]");
1025
+ const inpaintClear = document.querySelector("[data-editor-inpaint-clear]");
1026
+ const inpaintBrush = document.querySelector("[data-editor-inpaint-brush]");
1027
+ const inpaintBrushLabel = document.querySelector("[data-editor-inpaint-brush-label]");
1028
+ const inpaintColorPicker = document.querySelector("[data-editor-inpaint-color-picker]");
1029
+ const inpaintAddColor = document.querySelector("[data-editor-inpaint-add-color]");
1030
+ const inpaintProviderWrap = document.querySelector("[data-editor-inpaint-provider-wrap]");
1031
+ const inpaintProviderSelect = document.querySelector("[data-editor-inpaint-provider]");
1032
+ const inpaintGlobalPrompt = document.querySelector("[data-editor-inpaint-global-prompt]");
1033
+ const inpaintAttachmentInput = document.querySelector("[data-editor-inpaint-attachment-input]");
1034
+ const inpaintGlobalAttachments = document.querySelector('[data-editor-inpaint-attachments="global"]');
1035
+ const inpaintSwatches = document.querySelector("[data-editor-inpaint-swatches]");
1036
+ const inpaintPrompts = document.querySelector("[data-editor-inpaint-prompts]");
1037
+ const inpaintSubmit = document.querySelector("[data-editor-inpaint-submit]");
1038
+ const inpaintSubmitLabel = document.querySelector("[data-editor-inpaint-submit-label]");
1039
+ const settingsButton = document.querySelector("[data-custom-inpaint-settings-button]");
1040
+ const settingsPopover = document.querySelector("[data-custom-inpaint-settings]");
1041
+ const tracerInput = document.querySelector("[data-custom-inpaint-tracer]");
1042
+ const lightbox = document.querySelector("[data-custom-inpaint-lightbox]");
1043
+ const lightboxTitle = document.querySelector("[data-custom-inpaint-lightbox-title]");
1044
+ const lightboxImage = document.querySelector("[data-custom-inpaint-lightbox-image]");
1045
+ const lightboxCopy = document.querySelector("[data-custom-inpaint-lightbox-copy]");
1046
+ const lightboxDownload = document.querySelector("[data-custom-inpaint-lightbox-download]");
1047
+ const lightboxClose = document.querySelector("[data-custom-inpaint-lightbox-close]");
1048
+ if (!(uploadButton instanceof HTMLButtonElement)
1049
+ || !(startNewButton instanceof HTMLButtonElement)
1050
+ || !(fileInput instanceof HTMLInputElement)
1051
+ || !(sourceName instanceof HTMLElement)
1052
+ || !(statusEl instanceof HTMLElement)
1053
+ || !(emptyState instanceof HTMLElement)
1054
+ || !(canvasWrap instanceof HTMLElement)
1055
+ || !(outputPanel instanceof HTMLElement)
1056
+ || !(resultOpenButton instanceof HTMLButtonElement)
1057
+ || !(resultImage instanceof HTMLImageElement)
1058
+ || !(resultDownload instanceof HTMLAnchorElement)
1059
+ || !(historyList instanceof HTMLElement)
1060
+ || !(historyRefresh instanceof HTMLButtonElement)
1061
+ || !(inpaintImage instanceof HTMLImageElement)
1062
+ || !(inpaintCanvas instanceof HTMLCanvasElement)
1063
+ || !(inpaintBrushCursor instanceof HTMLElement)
1064
+ || !(inpaintUndo instanceof HTMLButtonElement)
1065
+ || !(inpaintRedo instanceof HTMLButtonElement)
1066
+ || !(inpaintClear instanceof HTMLButtonElement)
1067
+ || !(inpaintBrush instanceof HTMLInputElement)
1068
+ || !(inpaintBrushLabel instanceof HTMLElement)
1069
+ || !(inpaintColorPicker instanceof HTMLInputElement)
1070
+ || !(inpaintAddColor instanceof HTMLButtonElement)
1071
+ || !(inpaintProviderWrap instanceof HTMLElement)
1072
+ || !(inpaintProviderSelect instanceof HTMLSelectElement)
1073
+ || !(inpaintGlobalPrompt instanceof HTMLTextAreaElement)
1074
+ || !(inpaintAttachmentInput instanceof HTMLInputElement)
1075
+ || !(inpaintGlobalAttachments instanceof HTMLElement)
1076
+ || !(inpaintSwatches instanceof HTMLElement)
1077
+ || !(inpaintPrompts instanceof HTMLElement)
1078
+ || !(inpaintSubmit instanceof HTMLButtonElement)
1079
+ || !(inpaintSubmitLabel instanceof HTMLElement)
1080
+ || !(settingsButton instanceof HTMLButtonElement)
1081
+ || !(settingsPopover instanceof HTMLElement)
1082
+ || !(tracerInput instanceof HTMLInputElement)
1083
+ || !(lightbox instanceof HTMLElement)
1084
+ || !(lightboxTitle instanceof HTMLElement)
1085
+ || !(lightboxImage instanceof HTMLImageElement)
1086
+ || !(lightboxCopy instanceof HTMLButtonElement)
1087
+ || !(lightboxDownload instanceof HTMLAnchorElement)
1088
+ || !(lightboxClose instanceof HTMLButtonElement)) {
1089
+ return;
1090
+ }
977
1091
 
978
- .editor-modal {
979
- width: min(100%, 460px);
980
- display: grid;
981
- gap: 16px;
982
- padding: 22px;
983
- border-radius: 26px;
984
- border: 1px solid rgba(191, 164, 109, 0.28);
985
- background: rgba(255, 251, 244, 0.98);
986
- box-shadow: 0 24px 64px rgba(15, 23, 42, 0.22);
987
- }
1092
+ let sourceImageUrl = "";
1093
+ let sourceImageName = "";
1094
+ let inpaintMaskColors = [{ color: "#ff2d55", prompt: "" }];
1095
+ let activeInpaintColor = "#ff2d55";
1096
+ let inpaintStrokes = [];
1097
+ let redoStrokes = [];
1098
+ let currentInpaintStroke = null;
1099
+ let isInpaintPainting = false;
1100
+ let inpaintAttachments = [];
1101
+ let pendingInpaintAttachmentScope = null;
1102
+ let inpaintHistoryItems = [];
1103
+ let historyTracerRefreshTimer = 0;
1104
+ const hiddenInpaintHistoryKeys = new Set();
1105
+ const maxInpaintAttachments = 8;
1106
+ const allowedInpaintAttachmentTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
988
1107
 
989
- .editor-modal-head {
990
- display: grid;
991
- gap: 6px;
992
- }
1108
+ function makeTracerId() {
1109
+ return "custom-inpaint-" + (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function"
1110
+ ? globalThis.crypto.randomUUID().replace(/-/g, "").slice(0, 8)
1111
+ : Math.random().toString(16).slice(2, 10).padEnd(8, "0").slice(0, 8));
1112
+ }
993
1113
 
994
- .editor-modal-head h3 {
995
- margin: 0;
996
- font-size: 1.2rem;
997
- }
1114
+ function getTracerValue() {
1115
+ const value = tracerInput.value.trim();
1116
+ if (value) return value;
1117
+ const tracer = makeTracerId();
1118
+ tracerInput.value = tracer;
1119
+ return tracer;
1120
+ }
998
1121
 
999
- .editor-modal-head p,
1000
- .editor-modal-error,
1001
- .editor-modal-label {
1002
- margin: 0;
1003
- color: #617087;
1004
- line-height: 1.6;
1005
- }
1122
+ function queueInpaintHistoryReload() {
1123
+ window.clearTimeout(historyTracerRefreshTimer);
1124
+ historyTracerRefreshTimer = window.setTimeout(() => {
1125
+ void loadInpaintHistory();
1126
+ }, 300);
1127
+ }
1006
1128
 
1007
- .editor-modal-label {
1008
- font-size: 0.82rem;
1009
- font-weight: 700;
1010
- letter-spacing: 0.08em;
1011
- text-transform: uppercase;
1012
- }
1129
+ function setSubmitLoading(isLoading) {
1130
+ inpaintSubmit.disabled = isLoading;
1131
+ inpaintSubmit.setAttribute("data-loading", isLoading ? "true" : "false");
1132
+ inpaintSubmitLabel.textContent = isLoading ? "Creating" : "Create image edit";
1133
+ }
1013
1134
 
1014
- .editor-modal-input {
1015
- width: 100%;
1016
- min-height: 50px;
1017
- padding: 0 14px;
1018
- border-radius: 16px;
1019
- border: 1px solid rgba(191, 164, 109, 0.34);
1020
- background: rgba(255, 255, 255, 0.96);
1021
- color: #243041;
1022
- font: inherit;
1023
- }
1135
+ function setStatus(message, tone) {
1136
+ statusEl.textContent = message || "";
1137
+ if (tone) {
1138
+ statusEl.setAttribute("data-tone", tone);
1139
+ } else {
1140
+ statusEl.removeAttribute("data-tone");
1141
+ }
1142
+ }
1024
1143
 
1025
- .editor-modal-input:focus {
1026
- outline: 2px solid rgba(219, 193, 122, 0.7);
1027
- outline-offset: 2px;
1028
- }
1144
+ function imageDownloadName(prefix, createdAt) {
1145
+ const stamp = String(createdAt || new Date().toISOString()).replace(/[^0-9a-z]+/gi, "-");
1146
+ return prefix + "-" + stamp + ".png";
1147
+ }
1029
1148
 
1030
- .editor-modal-error {
1031
- min-height: 1.4em;
1032
- color: #9b2d2d;
1033
- font-size: 0.84rem;
1034
- }
1149
+ function openImagePreview(url, label, downloadName) {
1150
+ if (!url) return;
1151
+ lightboxImage.src = url;
1152
+ lightboxImage.alt = label || "Image edit output preview";
1153
+ lightboxTitle.textContent = label || "Image edit output";
1154
+ lightboxCopy.dataset.copyUrl = url;
1155
+ lightboxCopy.setAttribute("title", "Copy URL");
1156
+ lightboxDownload.href = url;
1157
+ lightboxDownload.setAttribute("download", downloadName || imageDownloadName("inpaint-output"));
1158
+ lightbox.hidden = false;
1159
+ lightbox.setAttribute("data-open", "true");
1160
+ document.body.style.overflow = "hidden";
1161
+ }
1035
1162
 
1036
- .editor-modal-actions {
1037
- display: flex;
1038
- justify-content: flex-end;
1039
- gap: 10px;
1040
- }
1163
+ function closeImagePreview() {
1164
+ lightbox.setAttribute("data-open", "false");
1165
+ lightbox.hidden = true;
1166
+ lightboxImage.removeAttribute("src");
1167
+ lightboxCopy.dataset.copyUrl = "";
1168
+ lightboxCopy.setAttribute("title", "Copy URL");
1169
+ lightboxDownload.href = "#";
1170
+ document.body.style.overflow = "";
1171
+ }
1041
1172
 
1042
- .editor-media-modal {
1043
- width: min(100%, 980px);
1044
- max-height: calc(100vh - 48px);
1045
- display: grid;
1046
- gap: 14px;
1047
- padding: 18px;
1048
- border-radius: 28px;
1049
- border: 1px solid rgba(191, 164, 109, 0.28);
1050
- background: rgba(255, 251, 244, 0.985);
1051
- box-shadow: 0 28px 80px rgba(15, 23, 42, 0.28);
1052
- }
1173
+ function updateBrushCursorSize() {
1174
+ const rect = inpaintCanvas.getBoundingClientRect();
1175
+ const brushSize = Math.max(1, Number(inpaintBrush.value) || 40);
1176
+ const displayScale = inpaintCanvas.width && rect.width ? rect.width / inpaintCanvas.width : 1;
1177
+ canvasWrap.style.setProperty("--inpaint-brush-cursor-size", Math.max(6, brushSize * displayScale) + "px");
1178
+ }
1053
1179
 
1054
- .editor-media-modal-head {
1055
- display: flex;
1056
- align-items: center;
1057
- justify-content: space-between;
1058
- gap: 12px;
1059
- }
1180
+ function updateBrushCursorPosition(event) {
1181
+ const rect = inpaintCanvas.getBoundingClientRect();
1182
+ canvasWrap.style.setProperty("--inpaint-brush-cursor-x", Math.max(0, Math.min(rect.width, event.clientX - rect.left)) + "px");
1183
+ canvasWrap.style.setProperty("--inpaint-brush-cursor-y", Math.max(0, Math.min(rect.height, event.clientY - rect.top)) + "px");
1184
+ updateBrushCursorSize();
1185
+ inpaintBrushCursor.dataset.visible = "true";
1186
+ }
1060
1187
 
1061
- .editor-media-modal-actions {
1062
- display: inline-flex;
1063
- align-items: center;
1064
- gap: 10px;
1065
- flex: 0 0 auto;
1066
- }
1188
+ function hideBrushCursor() {
1189
+ inpaintBrushCursor.dataset.visible = "false";
1190
+ }
1067
1191
 
1068
- .editor-media-modal-source {
1069
- color: #617087;
1070
- font-size: 0.82rem;
1071
- font-weight: 700;
1072
- text-decoration: underline;
1073
- text-underline-offset: 0.14em;
1074
- }
1192
+ function normalizeHexColor(value) {
1193
+ const raw = String(value || "").trim().toLowerCase();
1194
+ if (/^#[0-9a-f]{6}$/i.test(raw)) return raw;
1195
+ if (/^#[0-9a-f]{3}$/i.test(raw)) {
1196
+ return "#" + raw.slice(1).split("").map((part) => part + part).join("");
1197
+ }
1198
+ return "#ff2d55";
1199
+ }
1075
1200
 
1076
- .editor-media-modal-title {
1077
- margin: 0;
1078
- color: #324156;
1079
- font-size: 0.95rem;
1080
- font-weight: 700;
1081
- word-break: break-all;
1082
- }
1201
+ const inpaintColorPalette = ["#ff2d55", "#3b82f6", "#22c55e", "#f59e0b", "#a855f7", "#06b6d4", "#f97316", "#ec4899"];
1083
1202
 
1084
- .editor-media-modal-body {
1085
- min-height: 0;
1086
- display: grid;
1087
- place-items: center;
1088
- overflow: auto;
1089
- border-radius: 20px;
1090
- background: rgba(37, 45, 61, 0.96);
1091
- }
1203
+ function syncInpaintPromptValuesFromDom() {
1204
+ const fields = Array.from(inpaintPrompts.querySelectorAll("[data-color]"));
1205
+ for (const field of fields) {
1206
+ if (field instanceof HTMLTextAreaElement) {
1207
+ updateInpaintPrompt(field.dataset.color || "", field.value);
1208
+ }
1209
+ }
1210
+ }
1092
1211
 
1093
- .editor-media-modal-body img,
1094
- .editor-media-modal-body video {
1095
- max-width: 100%;
1096
- max-height: calc(100vh - 180px);
1097
- display: block;
1098
- object-fit: contain;
1099
- }
1212
+ function attachmentId() {
1213
+ return "att_" + Date.now().toString(36) + "_" + Math.random().toString(16).slice(2, 8);
1214
+ }
1100
1215
 
1101
- @media (max-width: 767px) {
1102
- .editor-hero,
1103
- .editor-grid {
1104
- grid-template-columns: 1fr;
1105
- }
1216
+ function attachmentsForScope(scope, color) {
1217
+ const normalizedColor = color ? normalizeHexColor(color) : "";
1218
+ return inpaintAttachments.filter((attachment) => (
1219
+ attachment.scope === scope && (!normalizedColor || attachment.color === normalizedColor)
1220
+ ));
1221
+ }
1106
1222
 
1107
- .editor-history-card-grid {
1108
- grid-template-columns: 1fr;
1109
- }
1223
+ function renderAttachmentChips(container, scope, color) {
1224
+ container.innerHTML = "";
1225
+ for (const attachment of attachmentsForScope(scope, color)) {
1226
+ const chip = document.createElement("span");
1227
+ chip.className = "editor-inpaint-attachment-chip";
1228
+ const preview = document.createElement("img");
1229
+ preview.src = attachment.url;
1230
+ preview.alt = "";
1231
+ preview.loading = "lazy";
1232
+ const label = document.createElement("span");
1233
+ label.textContent = attachment.fileName || "reference image";
1234
+ const remove = document.createElement("button");
1235
+ remove.type = "button";
1236
+ remove.textContent = "×";
1237
+ remove.dataset.editorInpaintRemoveAttachment = attachment.id;
1238
+ remove.setAttribute("aria-label", "Remove " + (attachment.fileName || "reference image"));
1239
+ chip.append(preview, label, remove);
1240
+ container.append(chip);
1241
+ }
1242
+ }
1110
1243
 
1111
- .editor-quickstart-grid {
1112
- grid-template-columns: 1fr;
1113
- }
1244
+ function renderAllAttachmentChips() {
1245
+ renderAttachmentChips(inpaintGlobalAttachments, "global");
1246
+ for (const container of Array.from(inpaintPrompts.querySelectorAll("[data-editor-inpaint-attachments]"))) {
1247
+ if (container instanceof HTMLElement && container.dataset.color) {
1248
+ renderAttachmentChips(container, "color", container.dataset.color);
1249
+ }
1250
+ }
1251
+ }
1114
1252
 
1115
- .editor-hero,
1116
- .editor-section,
1117
- .editor-panel {
1118
- border-radius: 22px;
1119
- }
1253
+ function openAttachmentPicker(scope, color) {
1254
+ pendingInpaintAttachmentScope = { scope, color: color ? normalizeHexColor(color) : "" };
1255
+ inpaintAttachmentInput.click();
1256
+ }
1120
1257
 
1121
- .editor-panel > summary {
1122
- align-items: flex-start;
1123
- padding: 16px;
1124
- }
1258
+ function isAllowedInpaintAttachment(file) {
1259
+ return file instanceof File && allowedInpaintAttachmentTypes.has(file.type);
1260
+ }
1125
1261
 
1126
- .editor-panel-toggle {
1127
- font-size: 0;
1128
- }
1262
+ function imageFilesFromPaste(event) {
1263
+ const clipboard = event.clipboardData;
1264
+ if (!clipboard) return [];
1265
+ const files = [];
1266
+ for (const item of Array.from(clipboard.items || [])) {
1267
+ if (item.kind !== "file") continue;
1268
+ const file = item.getAsFile();
1269
+ if (file) files.push(file);
1270
+ }
1271
+ if (!files.length) {
1272
+ for (const file of Array.from(clipboard.files || [])) {
1273
+ files.push(file);
1274
+ }
1275
+ }
1276
+ return files;
1277
+ }
1129
1278
 
1130
- .editor-panel-toggle::before {
1131
- width: 10px;
1132
- height: 10px;
1133
- }
1279
+ function handleAttachmentPaste(event, scope, color) {
1280
+ const files = imageFilesFromPaste(event);
1281
+ if (!files.length) return;
1282
+ event.preventDefault();
1283
+ const allowed = files.filter(isAllowedInpaintAttachment);
1284
+ if (!allowed.length) {
1285
+ setStatus("Reference attachments must be JPEG, PNG, or WebP images.", "error");
1286
+ return;
1287
+ }
1288
+ void addAttachmentFilesForScope(allowed, {
1289
+ scope,
1290
+ color: scope === "color" ? normalizeHexColor(color) : ""
1291
+ });
1292
+ }
1134
1293
 
1135
- .editor-panel-body {
1136
- padding: 0 16px 16px;
1137
- }
1294
+ function pickNextInpaintColor(requestedColor) {
1295
+ const requested = normalizeHexColor(requestedColor);
1296
+ const used = new Set(inpaintMaskColors.map((entry) => entry.color));
1297
+ if (!used.has(requested)) {
1298
+ return requested;
1299
+ }
1300
+ for (const color of inpaintColorPalette) {
1301
+ if (!used.has(color)) {
1302
+ return color;
1303
+ }
1304
+ }
1305
+ for (let index = 0; index < 32; index += 1) {
1306
+ const color = "#" + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, "0");
1307
+ if (!used.has(color)) {
1308
+ return color;
1309
+ }
1310
+ }
1311
+ return requested;
1312
+ }
1138
1313
 
1139
- .editor-hero,
1140
- .editor-section {
1141
- padding: 16px;
1142
- }
1314
+ function renderInpaintControls() {
1315
+ syncInpaintPromptValuesFromDom();
1316
+ inpaintBrushLabel.textContent = inpaintBrush.value;
1317
+ inpaintSwatches.innerHTML = "";
1318
+ inpaintPrompts.innerHTML = "";
1319
+ for (const entry of inpaintMaskColors) {
1320
+ const row = document.createElement("div");
1321
+ row.className = "editor-inpaint-color-row";
1322
+ row.dataset.color = entry.color;
1323
+ row.dataset.active = entry.color === activeInpaintColor ? "true" : "false";
1324
+ const dot = document.createElement("button");
1325
+ dot.type = "button";
1326
+ dot.className = "editor-inpaint-color-dot";
1327
+ dot.style.setProperty("--inpaint-color", entry.color);
1328
+ dot.dataset.color = entry.color;
1329
+ dot.setAttribute("aria-label", "Use mask color " + entry.color);
1330
+ dot.setAttribute("title", entry.color);
1331
+ const colorTools = document.createElement("div");
1332
+ colorTools.className = "editor-inpaint-color-tools";
1333
+ colorTools.append(dot);
1334
+ if (inpaintMaskColors.length > 1) {
1335
+ const removeColor = document.createElement("button");
1336
+ removeColor.type = "button";
1337
+ removeColor.className = "editor-inpaint-color-remove";
1338
+ removeColor.dataset.editorInpaintRemoveColor = entry.color;
1339
+ removeColor.setAttribute("aria-label", "Remove mask color " + entry.color);
1340
+ removeColor.setAttribute("title", "Remove color");
1341
+ 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>';
1342
+ removeColor.addEventListener("click", (event) => {
1343
+ event.preventDefault();
1344
+ event.stopPropagation();
1345
+ removeInpaintColor(entry.color);
1346
+ });
1347
+ colorTools.append(removeColor);
1348
+ }
1349
+ const prompt = document.createElement("textarea");
1350
+ prompt.className = "editor-inpaint-prompt";
1351
+ prompt.dataset.color = entry.color;
1352
+ prompt.value = entry.prompt || "";
1353
+ prompt.placeholder = "In color " + entry.color + " replace with...";
1354
+ const stack = document.createElement("div");
1355
+ stack.className = "editor-inpaint-prompt-stack";
1356
+ const field = document.createElement("div");
1357
+ field.className = "editor-inpaint-prompt-field";
1358
+ const tools = document.createElement("div");
1359
+ tools.className = "editor-inpaint-attachment-tools";
1360
+ const attach = document.createElement("button");
1361
+ attach.type = "button";
1362
+ attach.className = "editor-inpaint-attach-button";
1363
+ attach.dataset.editorInpaintAttachScope = "color";
1364
+ attach.dataset.color = entry.color;
1365
+ attach.setAttribute("aria-label", "Attach reference image for " + entry.color);
1366
+ attach.setAttribute("title", "Attach reference image");
1367
+ 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>';
1368
+ const chips = document.createElement("span");
1369
+ chips.className = "editor-inpaint-attachment-chips";
1370
+ chips.dataset.editorInpaintAttachments = "color";
1371
+ chips.dataset.color = entry.color;
1372
+ field.append(prompt, attach);
1373
+ tools.append(chips);
1374
+ stack.append(field, tools);
1375
+ row.append(colorTools, stack);
1376
+ inpaintPrompts.append(row);
1377
+ }
1378
+ renderAllAttachmentChips();
1379
+ }
1143
1380
 
1144
- .editor-preview {
1145
- min-height: 0;
1146
- max-height: none;
1147
- aspect-ratio: 16 / 11;
1148
- border-radius: 18px;
1149
- }
1381
+ function redrawInpaintMask() {
1382
+ const ctx = inpaintCanvas.getContext("2d");
1383
+ if (!ctx) return;
1384
+ ctx.clearRect(0, 0, inpaintCanvas.width, inpaintCanvas.height);
1385
+ ctx.lineCap = "round";
1386
+ ctx.lineJoin = "round";
1387
+ for (const stroke of inpaintStrokes) {
1388
+ if (!stroke.points || stroke.points.length < 1) continue;
1389
+ ctx.strokeStyle = stroke.color;
1390
+ ctx.lineWidth = stroke.size;
1391
+ ctx.globalAlpha = 1;
1392
+ ctx.beginPath();
1393
+ ctx.moveTo(stroke.points[0].x, stroke.points[0].y);
1394
+ if (stroke.points.length === 1) {
1395
+ ctx.lineTo(stroke.points[0].x + 0.01, stroke.points[0].y + 0.01);
1396
+ } else {
1397
+ for (let index = 1; index < stroke.points.length; index += 1) {
1398
+ ctx.lineTo(stroke.points[index].x, stroke.points[index].y);
1399
+ }
1400
+ }
1401
+ ctx.stroke();
1402
+ }
1403
+ }
1150
1404
 
1151
- .editor-title {
1152
- font-size: clamp(2rem, 12vw, 2.85rem);
1153
- line-height: 0.95;
1154
- }
1405
+ function syncInpaintCanvasSize() {
1406
+ if (!inpaintImage.naturalWidth || !inpaintImage.naturalHeight) return;
1407
+ if (inpaintCanvas.width !== inpaintImage.naturalWidth || inpaintCanvas.height !== inpaintImage.naturalHeight) {
1408
+ inpaintCanvas.width = inpaintImage.naturalWidth;
1409
+ inpaintCanvas.height = inpaintImage.naturalHeight;
1410
+ redrawInpaintMask();
1411
+ }
1412
+ updateBrushCursorSize();
1413
+ }
1155
1414
 
1156
- .editor-toolbar,
1157
- .editor-history-toolbar-actions,
1158
- .editor-history-group-actions {
1159
- display: grid;
1160
- grid-template-columns: 1fr;
1161
- width: 100%;
1162
- }
1415
+ function getInpaintPoint(event) {
1416
+ const rect = inpaintCanvas.getBoundingClientRect();
1417
+ return {
1418
+ x: Math.max(0, Math.min(inpaintCanvas.width, (event.clientX - rect.left) * (inpaintCanvas.width / rect.width))),
1419
+ y: Math.max(0, Math.min(inpaintCanvas.height, (event.clientY - rect.top) * (inpaintCanvas.height / rect.height)))
1420
+ };
1421
+ }
1163
1422
 
1164
- .editor-copy-button,
1165
- .editor-history-button,
1166
- .editor-history-remove {
1167
- width: 100%;
1168
- }
1423
+ function beginInpaintStroke(event) {
1424
+ if (!sourceImageUrl || !inpaintCanvas.width || inpaintSubmit.disabled) return;
1425
+ event.preventDefault();
1426
+ inpaintCanvas.setPointerCapture(event.pointerId);
1427
+ isInpaintPainting = true;
1428
+ currentInpaintStroke = {
1429
+ color: activeInpaintColor,
1430
+ size: Number(inpaintBrush.value) || 40,
1431
+ points: [getInpaintPoint(event)]
1432
+ };
1433
+ inpaintStrokes.push(currentInpaintStroke);
1434
+ redoStrokes = [];
1435
+ redrawInpaintMask();
1436
+ }
1169
1437
 
1170
- .editor-pill,
1171
- .editor-route-path,
1172
- .editor-history-chip {
1173
- max-width: 100%;
1174
- overflow: hidden;
1175
- text-overflow: ellipsis;
1176
- white-space: nowrap;
1177
- }
1178
- }
1179
- `,
1180
- body: `
1181
- <section class="editor-workbench">
1182
- ${renderTemplateEditorChrome({
1183
- backHref,
1184
- templateTitle: input.template.title,
1185
- templateId: input.template.id,
1186
- templateSlug: input.template.slugId
1187
- })}
1188
- <main class="editor-main">
1189
- <section class="editor-shell page-content-start">
1190
- <details class="editor-panel" open>
1191
- <summary>
1192
- <div class="editor-panel-summary">
1193
- <h2>Character card</h2>
1194
- <p>Preview the format, metadata, and editorial notes without losing space for the sections that follow.</p>
1195
- </div>
1196
- <span class="editor-panel-toggle" aria-hidden="true">Toggle</span>
1197
- </summary>
1198
- <div class="editor-panel-body">
1199
- <section class="editor-hero">
1200
- ${preview}
1201
- <div class="editor-copy">
1202
- <div class="editor-kicker">Template Editor Docs</div>
1203
- <h1 class="editor-title">${escapeHtml(input.template.title)}</h1>
1204
- <div class="editor-pills">
1205
- <span class="editor-pill">${escapeHtml(input.template.id)}</span>
1206
- <span class="editor-pill">${escapeHtml(input.template.slugId)}</span>
1207
- <span class="editor-pill">v${escapeHtml(input.template.version)}</span>
1208
- </div>
1209
- <p>${escapeHtml(input.template.viralDna || input.template.visualDna || "No editorial summary is available for this template yet.")}</p>
1210
- ${input.directorSkill?.trim()
1211
- ? `<div class="editor-toolbar">
1212
- <button
1213
- type="button"
1214
- class="editor-copy-button"
1215
- data-editor-copy-content="${escapeAttribute(input.directorSkill)}"
1216
- data-editor-copy-label="Director SKILL.md"
1217
- >Copy Director SKILL.md</button>
1218
- </div>`
1219
- : ""}
1220
- </div>
1221
- </section>
1222
- </div>
1223
- </details>
1224
- <section class="editor-grid">
1225
- <details class="editor-panel" open>
1226
- <summary>
1227
- <div class="editor-panel-summary">
1228
- <h2>Quickstart</h2>
1229
- <p>Use the template prompt scaffolds as readonly references before calling operations or continuing in Copilot.</p>
1230
- </div>
1231
- <span class="editor-panel-toggle" aria-hidden="true">Toggle</span>
1232
- </summary>
1233
- <div class="editor-panel-body">
1234
- <section class="editor-section">
1235
- <div class="editor-quickstart-grid">
1236
- ${renderQuickstartTextarea("Skeleton prompt", input.template.skeletonPrompt, 10)}
1237
- ${renderQuickstartTextarea("Proposal Generator", input.template.proposalGenerator, 10)}
1238
- </div>
1239
- <div class="editor-quickstart-samples">
1240
- <h3>Sample prompts</h3>
1241
- <div class="editor-sample-prompt-row" aria-label="Sample prompts">
1242
- ${input.template.samplePrompts.length
1243
- ? input.template.samplePrompts.map((sample, index) => `
1244
- <div class="editor-sample-prompt">
1245
- ${renderQuickstartTextarea(`Sample ${index + 1}`, sample, 7)}
1246
- </div>
1247
- `).join("")
1248
- : `<div class="editor-sample-prompt">${renderQuickstartTextarea("Sample 1", "No sample prompts are defined for this template yet.", 7)}</div>`}
1249
- </div>
1250
- </div>
1251
- </section>
1252
- </div>
1253
- </details>
1254
- <details class="editor-panel">
1255
- <summary>
1256
- <div class="editor-panel-summary">
1257
- <h2>REST API docs</h2>
1258
- <p>This editor combines template routes with platform primitive routes so asset generation, image edits, and template runs stay in one tracer-aware workflow.</p>
1259
- </div>
1260
- <span class="editor-panel-toggle" aria-hidden="true">Toggle</span>
1261
- </summary>
1262
- <div class="editor-panel-body">
1263
- <section class="editor-section">
1264
- <div class="editor-section-header">
1265
- <p>Use tracer IDs to group a run across requests.</p>
1266
- </div>
1267
- <div class="editor-route-list">
1268
- ${input.docsRoutes.map((route) => `
1269
- <article class="editor-route-card">
1270
- <div class="editor-route-head">
1271
- <span class="editor-route-method" data-method="${escapeAttribute(route.method)}">${escapeHtml(route.method)}</span>
1272
- <div class="editor-route-path">${escapeHtml(route.path)}</div>
1273
- </div>
1274
- <div class="editor-note">${escapeHtml(route.summary)}</div>
1275
- </article>
1276
- `).join("")}
1277
- </div>
1278
- </section>
1279
- </div>
1280
- </details>
1281
- <details class="editor-panel" open>
1282
- <summary>
1283
- <div class="editor-panel-summary">
1284
- <h2>Tracer Logs</h2>
1285
- <p>Recent POST calls are grouped by tracer so parallel workstreams stay readable. Newest requests appear first inside each tracer group.</p>
1286
- </div>
1287
- <span class="editor-panel-toggle" aria-hidden="true">Toggle</span>
1288
- </summary>
1289
- <div class="editor-panel-body">
1290
- <section class="editor-section">
1291
- <div class="editor-history-toolbar">
1292
- <p class="editor-note">Tracer visibility lives here. Removing one only clears it from this page view; the underlying runs remain available in Job Runs.</p>
1293
- <div class="editor-history-toolbar-actions">
1294
- <button type="button" class="editor-history-button is-primary" data-editor-history-add>Add tracer</button>
1295
- <div class="editor-history-chip-row" data-editor-history-tracers>
1296
- ${input.initialTracers.length
1297
- ? input.initialTracers.map((tracer) => `<span class="editor-history-chip">${escapeHtml(tracer)}</span>`).join("")
1298
- : `<span class="editor-history-chip is-empty">No tracer selected yet</span>`}
1299
- </div>
1300
- </div>
1301
- </div>
1302
- <div class="editor-history-groups" data-editor-history-root>
1303
- <div class="editor-history-empty">${hasHistoryAccess ? "Loading POST history…" : "Sign in to load tracer-scoped POST history."}</div>
1304
- </div>
1305
- </section>
1306
- </div>
1307
- </details>
1308
- </section>
1309
- </section>
1310
- </main>
1311
- </section>
1312
- <div class="editor-modal-backdrop" data-editor-tracer-modal data-open="false" hidden>
1313
- <div class="editor-modal" role="dialog" aria-modal="true" aria-labelledby="editor-tracer-modal-title">
1314
- <div class="editor-modal-head">
1315
- <h3 id="editor-tracer-modal-title">Add tracer</h3>
1316
- <p>Type a tracer label to pin it into this page view. Existing labels stay untouched.</p>
1317
- </div>
1318
- <label class="editor-modal-label" for="editor-tracer-input">Tracer id</label>
1319
- <input id="editor-tracer-input" class="editor-modal-input" type="text" autocomplete="off" spellcheck="false" placeholder="${escapeAttribute(`${input.template.slugId}-manual`)}" />
1320
- <p class="editor-modal-error" data-editor-tracer-modal-error aria-live="polite"></p>
1321
- <div class="editor-modal-actions">
1322
- <button type="button" class="editor-history-button" data-editor-tracer-cancel>Cancel</button>
1323
- <button type="button" class="editor-history-button is-primary" data-editor-tracer-submit>Add tracer</button>
1324
- </div>
1325
- </div>
1326
- </div>
1327
- <div class="editor-modal-backdrop" data-editor-media-modal data-open="false" hidden>
1328
- <div class="editor-media-modal" role="dialog" aria-modal="true" aria-labelledby="editor-media-modal-title">
1329
- <div class="editor-media-modal-head">
1330
- <p id="editor-media-modal-title" class="editor-media-modal-title">Output preview</p>
1331
- <div class="editor-media-modal-actions">
1332
- <a class="editor-media-modal-source" data-editor-media-source href="#" target="_blank" rel="noreferrer">Open source</a>
1333
- <button type="button" class="editor-history-button" data-editor-media-close>Close</button>
1334
- </div>
1335
- </div>
1336
- <div class="editor-media-modal-body" data-editor-media-body></div>
1337
- </div>
1338
- </div>
1339
- <div class="vf-editor-chat-http-modal-backdrop" data-editor-http-modal hidden>
1340
- <div class="vf-editor-chat-http-modal" role="dialog" aria-modal="true" aria-labelledby="editor-http-modal-title">
1341
- <div class="vf-editor-chat-http-modal-header">
1342
- <div class="vf-editor-chat-http-modal-header-main" data-editor-http-modal-head>
1343
- <span id="editor-http-modal-title" class="vf-editor-chat-http-modal-url">HTTP details</span>
1344
- </div>
1345
- <button type="button" class="vf-editor-chat-http-modal-close" data-editor-http-close aria-label="Close HTTP details">×</button>
1346
- </div>
1347
- <div class="vf-editor-chat-http-card-body is-modal" data-editor-http-modal-body></div>
1348
- </div>
1349
- </div>
1350
- <script>
1351
- ${TEMPLATE_EDITOR_SHELL_SCRIPT}
1352
- (() => {
1353
- const historyRoot = document.querySelector("[data-editor-history-root]");
1354
- const tracerChipRow = document.querySelector("[data-editor-history-tracers]");
1355
- const addTracerButton = document.querySelector("[data-editor-history-add]");
1356
- const modalBackdrop = document.querySelector("[data-editor-tracer-modal]");
1357
- const modalInput = document.getElementById("editor-tracer-input");
1358
- const modalError = document.querySelector("[data-editor-tracer-modal-error]");
1359
- const modalSubmit = document.querySelector("[data-editor-tracer-submit]");
1360
- const modalCancel = document.querySelector("[data-editor-tracer-cancel]");
1361
- const mediaModalBackdrop = document.querySelector("[data-editor-media-modal]");
1362
- const mediaModalBody = document.querySelector("[data-editor-media-body]");
1363
- const mediaModalTitle = document.getElementById("editor-media-modal-title");
1364
- const mediaModalClose = document.querySelector("[data-editor-media-close]");
1365
- const mediaModalSource = document.querySelector("[data-editor-media-source]");
1366
- const httpModalBackdrop = document.querySelector("[data-editor-http-modal]");
1367
- const httpModalHead = document.querySelector("[data-editor-http-modal-head]");
1368
- const httpModalBody = document.querySelector("[data-editor-http-modal-body]");
1369
- const httpModalClose = document.querySelector("[data-editor-http-close]");
1370
- if (!(historyRoot instanceof HTMLElement) || !(tracerChipRow instanceof HTMLElement)) {
1371
- return;
1438
+ function continueInpaintStroke(event) {
1439
+ if (!isInpaintPainting || !currentInpaintStroke) return;
1440
+ event.preventDefault();
1441
+ currentInpaintStroke.points.push(getInpaintPoint(event));
1442
+ redrawInpaintMask();
1372
1443
  }
1373
1444
 
1374
- const hasTracerModal = addTracerButton instanceof HTMLButtonElement
1375
- && modalBackdrop instanceof HTMLElement
1376
- && modalInput instanceof HTMLInputElement
1377
- && modalError instanceof HTMLElement
1378
- && modalSubmit instanceof HTMLButtonElement
1379
- && modalCancel instanceof HTMLButtonElement;
1380
- const hasMediaModal = mediaModalBackdrop instanceof HTMLElement
1381
- && mediaModalBody instanceof HTMLElement
1382
- && mediaModalTitle instanceof HTMLElement
1383
- && mediaModalClose instanceof HTMLButtonElement
1384
- && mediaModalSource instanceof HTMLAnchorElement;
1385
- const hasHttpModal = httpModalBackdrop instanceof HTMLElement
1386
- && httpModalHead instanceof HTMLElement
1387
- && httpModalBody instanceof HTMLElement
1388
- && httpModalClose instanceof HTMLButtonElement;
1389
-
1390
- const boot = {
1391
- historyEndpoint: ${JSON.stringify(input.historyEndpoint)},
1392
- tracerEndpoint: ${JSON.stringify(input.tracerEndpoint)},
1393
- fallbackHistoryEndpoint: ${JSON.stringify(withAccountQuery(`/job-runs/history?limit=250&template_id=${encodeURIComponent(input.template.id)}`, input.account.userId))},
1394
- isLoggedIn: ${input.account.isLoggedIn ? "true" : "false"}
1395
- };
1396
- let lastHistoryEntries = [];
1397
- let lastSelectedTracers = [];
1398
- const expandedPreviewGroups = new Set();
1399
-
1400
- function escapeHtml(value) {
1401
- return String(value)
1402
- .replace(/&/g, "&amp;")
1403
- .replace(/</g, "&lt;")
1404
- .replace(/>/g, "&gt;")
1405
- .replace(/"/g, "&quot;");
1406
- }
1407
-
1408
- function formatJson(value) {
1409
- if (value == null) {
1410
- return "null";
1411
- }
1412
- if (typeof value === "string") {
1413
- return value;
1414
- }
1445
+ function endInpaintStroke(event) {
1446
+ if (!isInpaintPainting) return;
1447
+ event.preventDefault();
1448
+ isInpaintPainting = false;
1449
+ currentInpaintStroke = null;
1415
1450
  try {
1416
- return JSON.stringify(value, null, 2);
1417
- } catch {
1418
- return String(value);
1451
+ inpaintCanvas.releasePointerCapture(event.pointerId);
1452
+ } catch {}
1453
+ }
1454
+
1455
+ function addInpaintColor(color) {
1456
+ syncInpaintPromptValuesFromDom();
1457
+ const normalized = pickNextInpaintColor(color);
1458
+ if (!inpaintMaskColors.some((entry) => entry.color === normalized)) {
1459
+ inpaintMaskColors.push({ color: normalized, prompt: "" });
1419
1460
  }
1461
+ activeInpaintColor = normalized;
1462
+ inpaintColorPicker.value = normalized;
1463
+ renderInpaintControls();
1420
1464
  }
1421
1465
 
1422
- function setTextBlock(parent, label, value) {
1423
- const block = document.createElement("div");
1424
- block.className = "vf-editor-chat-http-block";
1425
- const heading = document.createElement("div");
1426
- heading.className = "vf-editor-chat-http-label";
1427
- heading.textContent = label;
1428
- const pre = document.createElement("pre");
1429
- pre.className = "vf-editor-chat-http-pre";
1430
- pre.textContent = typeof value === "string" ? value : formatJson(value);
1431
- block.append(heading, pre);
1432
- parent.append(block);
1466
+ function removeInpaintColor(color) {
1467
+ syncInpaintPromptValuesFromDom();
1468
+ const normalized = normalizeHexColor(color);
1469
+ if (inpaintMaskColors.length <= 1 || !inpaintMaskColors.some((entry) => entry.color === normalized)) {
1470
+ setStatus("Keep at least one mask color.", "error");
1471
+ return;
1472
+ }
1473
+ inpaintMaskColors = inpaintMaskColors.filter((entry) => entry.color !== normalized);
1474
+ inpaintAttachments = inpaintAttachments.filter((attachment) => attachment.scope !== "color" || attachment.color !== normalized);
1475
+ inpaintStrokes = inpaintStrokes.filter((stroke) => normalizeHexColor(stroke.color) !== normalized);
1476
+ redoStrokes = redoStrokes.filter((stroke) => normalizeHexColor(stroke.color) !== normalized);
1477
+ if (activeInpaintColor === normalized) {
1478
+ activeInpaintColor = inpaintMaskColors[0]?.color || "#ff2d55";
1479
+ inpaintColorPicker.value = activeInpaintColor;
1480
+ }
1481
+ redrawInpaintMask();
1482
+ renderInpaintControls();
1483
+ setStatus("", "");
1433
1484
  }
1434
1485
 
1435
- function makeHttpPayload(entry) {
1436
- return {
1437
- method: entry.method,
1438
- url: entry.path,
1439
- status: Number(entry.status) || 0,
1440
- statusText: "",
1441
- request: {
1442
- query: entry.request?.query || {},
1443
- body: entry.request?.body ?? null,
1444
- omitted: entry.request?.bodyOmittedReason || null
1445
- },
1446
- response: {
1447
- status: entry.status,
1448
- body: entry.response?.body ?? null,
1449
- omitted: entry.response?.bodyOmittedReason || null
1486
+ function selectInpaintColor(color) {
1487
+ activeInpaintColor = normalizeHexColor(color);
1488
+ inpaintColorPicker.value = activeInpaintColor;
1489
+ for (const row of Array.from(inpaintPrompts.querySelectorAll(".editor-inpaint-color-row"))) {
1490
+ if (row instanceof HTMLElement) {
1491
+ row.dataset.active = row.dataset.color === activeInpaintColor ? "true" : "false";
1450
1492
  }
1451
- };
1452
- }
1453
-
1454
- function encodeHttpPayload(entry) {
1455
- try {
1456
- return encodeURIComponent(JSON.stringify(makeHttpPayload(entry)));
1457
- } catch {
1458
- return encodeURIComponent(JSON.stringify({
1459
- method: entry.method,
1460
- url: entry.path,
1461
- status: Number(entry.status) || 0,
1462
- statusText: "",
1463
- request: { query: {}, body: null, omitted: "Request sample could not be encoded for display." },
1464
- response: { status: entry.status, body: null, omitted: "Response sample could not be encoded for display." }
1465
- }));
1466
1493
  }
1467
1494
  }
1468
1495
 
1469
- function renderHttpAction(entry) {
1470
- const payload = encodeHttpPayload(entry);
1471
- const status = Number(entry.status) || 0;
1472
- const isError = status >= 400;
1473
- const isSuccess = status >= 200 && status < 300;
1474
- return '<div class="vf-editor-chat-http-card">' +
1475
- '<button type="button" class="vf-editor-chat-http-card-header" data-editor-http-detail="' + escapeHtml(payload) + '" aria-label="Open HTTP details for ' + escapeHtml(entry.method) + ' ' + escapeHtml(entry.path) + '">' +
1476
- '<span class="vf-editor-chat-http-pill">' + escapeHtml(entry.method) + '</span>' +
1477
- '<span class="vf-editor-chat-http-url" title="' + escapeHtml(entry.path) + '">' + escapeHtml(entry.path) + '</span>' +
1478
- '<span class="vf-editor-chat-http-status" data-error="' + (isError ? "true" : "false") + '" data-success="' + (isSuccess ? "true" : "false") + '">' + escapeHtml(status) + '</span>' +
1479
- '<span class="vf-editor-chat-http-expand" aria-hidden="true">+</span>' +
1480
- '</button>' +
1481
- '</div>';
1496
+ function updateInpaintPrompt(color, prompt) {
1497
+ const normalized = normalizeHexColor(color);
1498
+ inpaintMaskColors = inpaintMaskColors.map((entry) => (
1499
+ entry.color === normalized ? { ...entry, prompt: String(prompt || "") } : entry
1500
+ ));
1482
1501
  }
1483
1502
 
1484
- function openHttpModal(payload) {
1485
- if (!hasHttpModal) {
1486
- return;
1487
- }
1488
- httpModalHead.innerHTML = "";
1489
- const isError = payload.status >= 400;
1490
- const isSuccess = payload.status >= 200 && payload.status < 300;
1491
- const method = document.createElement("span");
1492
- method.className = "vf-editor-chat-http-pill";
1493
- method.textContent = payload.method || "HTTP";
1494
- const url = document.createElement("span");
1495
- url.className = "vf-editor-chat-http-modal-url";
1496
- url.textContent = payload.url || "";
1497
- const status = document.createElement("span");
1498
- status.className = "vf-editor-chat-http-status";
1499
- status.dataset.error = isError ? "true" : "false";
1500
- status.dataset.success = isSuccess ? "true" : "false";
1501
- status.textContent = String(payload.status || "");
1502
- httpModalHead.append(method, url, status);
1503
- httpModalBody.innerHTML = "";
1504
- setTextBlock(httpModalBody, "URL", payload.url || "");
1505
- if (payload.request?.omitted) {
1506
- setTextBlock(httpModalBody, "Request note", payload.request.omitted);
1507
- }
1508
- setTextBlock(httpModalBody, "Request", {
1509
- query: payload.request?.query || {},
1510
- body: payload.request?.body ?? null
1503
+ function buildInpaintInstruction() {
1504
+ const globalPrompt = inpaintGlobalPrompt.value.trim();
1505
+ const regions = inpaintMaskColors
1506
+ .map((entry) => ({ color: entry.color.toUpperCase(), prompt: String(entry.prompt || "").trim() }))
1507
+ .filter((entry) => entry.prompt);
1508
+ if (!globalPrompt && !regions.length) return "";
1509
+ const attachmentNotes = inpaintAttachments.map((attachment, index) => {
1510
+ const label = "reference image " + (index + 1) + " (" + (attachment.fileName || "attachment") + ")";
1511
+ return attachment.scope === "global"
1512
+ ? label + " applies to the whole image instruction"
1513
+ : label + " applies only to the UI mask color " + String(attachment.color || "").toUpperCase();
1511
1514
  });
1512
- if (payload.response?.omitted) {
1513
- setTextBlock(httpModalBody, "Response note", payload.response.omitted);
1514
- }
1515
- setTextBlock(httpModalBody, "Response", {
1516
- status: payload.response?.status ?? payload.status,
1517
- body: payload.response?.body ?? null
1515
+ return [
1516
+ "INPAINTING SYSTEM WRAPPER:",
1517
+ "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.",
1518
+ "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.",
1519
+ "The final image must keep the original dimensions and aspect ratio unless the user explicitly asks for a dimension change.",
1520
+ globalPrompt ? "Whole image prompt: " + globalPrompt + ". Apply this only as guidance for the masked edit and overall consistency; it does not permit changing unmasked content." : "",
1521
+ regions.map((entry) => (
1522
+ "Masked region " + entry.color + ": replace only that marked area with " + entry.prompt + ". Match the surrounding perspective, lighting, grain, shadows, and edge blending."
1523
+ )).join(" "),
1524
+ 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." : ""
1525
+ ].filter(Boolean).join(" ");
1526
+ }
1527
+
1528
+ function getInpaintAspectRatio() {
1529
+ const width = inpaintCanvas.width || inpaintImage.naturalWidth || 0;
1530
+ const height = inpaintCanvas.height || inpaintImage.naturalHeight || 0;
1531
+ if (!width || !height) return undefined;
1532
+ const ratio = width / height;
1533
+ if (Math.abs(ratio - 1) < 0.08) return "1:1";
1534
+ if (ratio > 1) return ratio >= 1.55 ? "16:9" : "4:3";
1535
+ return ratio <= 0.65 ? "9:16" : "3:4";
1536
+ }
1537
+
1538
+ function canvasToPngBlob(canvas) {
1539
+ return new Promise((resolve, reject) => {
1540
+ canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("Unable to prepare image.")), "image/png");
1518
1541
  });
1519
- httpModalBackdrop.hidden = false;
1520
1542
  }
1521
1543
 
1522
- function closeHttpModal() {
1523
- if (!hasHttpModal) {
1524
- return;
1544
+ async function createProviderMaskBlob() {
1545
+ const maskCanvas = document.createElement("canvas");
1546
+ maskCanvas.width = inpaintCanvas.width;
1547
+ maskCanvas.height = inpaintCanvas.height;
1548
+ const ctx = maskCanvas.getContext("2d");
1549
+ if (!ctx) throw new Error("Unable to prepare mask image.");
1550
+ ctx.fillStyle = "#ffffff";
1551
+ ctx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
1552
+ ctx.globalCompositeOperation = "destination-out";
1553
+ ctx.lineCap = "round";
1554
+ ctx.lineJoin = "round";
1555
+ for (const stroke of inpaintStrokes) {
1556
+ if (!stroke.points || stroke.points.length < 1) continue;
1557
+ ctx.strokeStyle = "rgba(0, 0, 0, 1)";
1558
+ ctx.lineWidth = stroke.size;
1559
+ ctx.beginPath();
1560
+ ctx.moveTo(stroke.points[0].x, stroke.points[0].y);
1561
+ if (stroke.points.length === 1) {
1562
+ ctx.lineTo(stroke.points[0].x + 0.01, stroke.points[0].y + 0.01);
1563
+ } else {
1564
+ for (let index = 1; index < stroke.points.length; index += 1) {
1565
+ ctx.lineTo(stroke.points[index].x, stroke.points[index].y);
1566
+ }
1567
+ }
1568
+ ctx.stroke();
1525
1569
  }
1526
- httpModalBackdrop.hidden = true;
1527
- httpModalBody.innerHTML = "";
1570
+ return canvasToPngBlob(maskCanvas);
1528
1571
  }
1529
1572
 
1530
- function parseTracersFromUrl() {
1531
- const params = new URLSearchParams(window.location.search);
1532
- const values = [...params.getAll("tracers"), ...params.getAll("tracer")];
1533
- const tracers = [];
1534
- const seen = new Set();
1535
- for (const rawValue of values) {
1536
- const trimmed = String(rawValue || "").trim();
1537
- if (!trimmed) {
1538
- continue;
1539
- }
1540
- const inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
1541
- for (const part of inner.split(",")) {
1542
- const tracer = part.trim().replace(/^["']|["']$/g, "");
1543
- if (!tracer || seen.has(tracer)) {
1573
+ function parseJsonResponse(response) {
1574
+ return response.json().catch(() => ({}));
1575
+ }
1576
+
1577
+ async function loadInpaintProviders() {
1578
+ try {
1579
+ const response = await fetch("/api/v1/user/me/provider-keys", { credentials: "same-origin" });
1580
+ const payload = await parseJsonResponse(response);
1581
+ if (!response.ok) return;
1582
+ const supported = new Set(["openai", "gemini", "openrouter"]);
1583
+ const providers = [];
1584
+ const seen = new Set();
1585
+ for (const key of Array.isArray(payload.provider_keys) ? payload.provider_keys : []) {
1586
+ const provider = String(key.provider || "");
1587
+ const status = String(key.status || "active");
1588
+ const cooldownUntil = key.cooldown_until ? Date.parse(String(key.cooldown_until)) : 0;
1589
+ if (!supported.has(provider) || status !== "active" || (cooldownUntil && cooldownUntil > Date.now()) || seen.has(provider)) {
1544
1590
  continue;
1545
1591
  }
1546
- seen.add(tracer);
1547
- tracers.push(tracer);
1592
+ seen.add(provider);
1593
+ providers.push(provider);
1594
+ }
1595
+ inpaintProviderSelect.innerHTML = '<option value="">Auto</option>' + providers.map((provider) => (
1596
+ '<option value="' + provider + '">' + provider.charAt(0).toUpperCase() + provider.slice(1) + '</option>'
1597
+ )).join("");
1598
+ inpaintProviderWrap.setAttribute("data-visible", providers.length > 1 ? "true" : "false");
1599
+ } catch {}
1600
+ }
1601
+
1602
+ async function uploadTemporaryBlob(blob, fileName, contentType) {
1603
+ const presignResponse = await fetch("/api/v1/user/me/temporary-files/presign", {
1604
+ method: "POST",
1605
+ credentials: "same-origin",
1606
+ headers: { "content-type": "application/json" },
1607
+ body: JSON.stringify({
1608
+ file_name: fileName,
1609
+ content_type: contentType,
1610
+ size_bytes: blob.size,
1611
+ folder_path: "inpaint"
1612
+ })
1613
+ });
1614
+ const presignPayload = await parseJsonResponse(presignResponse);
1615
+ if (!presignResponse.ok || !presignPayload || !presignPayload.transport) {
1616
+ throw new Error(presignPayload && presignPayload.error ? presignPayload.error : "Unable to prepare upload.");
1617
+ }
1618
+ if (presignPayload.transport === "server") {
1619
+ const formData = new FormData();
1620
+ formData.append(presignPayload.upload?.form_field || "file", new File([blob], fileName, { type: contentType }));
1621
+ formData.append("folder_path", "inpaint");
1622
+ const uploadResponse = await fetch(presignPayload.upload?.url || "/api/v1/user/me/temporary-files/upload", {
1623
+ method: presignPayload.upload?.method || "POST",
1624
+ credentials: "same-origin",
1625
+ body: formData
1626
+ });
1627
+ const uploadPayload = await parseJsonResponse(uploadResponse);
1628
+ if (!uploadResponse.ok || !uploadPayload.file) {
1629
+ throw new Error(uploadPayload.error || "Unable to upload file.");
1548
1630
  }
1631
+ return uploadPayload.file.viewUrl || uploadPayload.file.view_url || uploadPayload.file.s3Url || uploadPayload.file.s3_url;
1632
+ }
1633
+ const uploadResponse = await fetch(presignPayload.upload.url, {
1634
+ method: presignPayload.upload.method,
1635
+ headers: presignPayload.upload.headers,
1636
+ body: blob
1637
+ });
1638
+ if (!uploadResponse.ok) throw new Error("Upload failed with status " + uploadResponse.status + ".");
1639
+ const finalizeResponse = await fetch("/api/v1/user/me/temporary-files", {
1640
+ method: "POST",
1641
+ credentials: "same-origin",
1642
+ headers: { "content-type": "application/json" },
1643
+ body: JSON.stringify({
1644
+ file_id: presignPayload.file_id,
1645
+ file_name: presignPayload.file_name,
1646
+ content_type: presignPayload.content_type,
1647
+ size_bytes: blob.size,
1648
+ storage_key: presignPayload.storage_key,
1649
+ folder_path: presignPayload.folder_path || "inpaint"
1650
+ })
1651
+ });
1652
+ const finalizePayload = await parseJsonResponse(finalizeResponse);
1653
+ if (!finalizeResponse.ok || !finalizePayload.file) {
1654
+ throw new Error(finalizePayload.error || "Unable to save file.");
1549
1655
  }
1550
- return tracers;
1656
+ return finalizePayload.file.viewUrl || finalizePayload.file.view_url || finalizePayload.file.s3Url || finalizePayload.file.s3_url;
1551
1657
  }
1552
1658
 
1553
- function makeTracerId() {
1554
- const suffix = (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function"
1555
- ? globalThis.crypto.randomUUID().replace(/-/g, "").slice(-8)
1556
- : Math.random().toString(16).slice(2, 10).padEnd(8, "0").slice(0, 8));
1557
- return ${JSON.stringify(input.template.slugId)}
1558
- .replace(/_/g, "-")
1559
- .replace(/[^a-zA-Z0-9-]/g, "-")
1560
- .toLowerCase()
1561
- .replace(/-+/g, "-")
1562
- .replace(/^-|-$/g, "") + "-" + suffix;
1659
+ function absoluteUrl(value) {
1660
+ return new URL(value, window.location.href).toString();
1563
1661
  }
1564
1662
 
1565
- function syncTracerUrl(tracers) {
1566
- const url = new URL(window.location.href);
1567
- if (tracers.length) {
1568
- url.searchParams.set("tracers", tracers.join(","));
1569
- } else {
1570
- url.searchParams.delete("tracers");
1663
+ function firstSearchParam(names) {
1664
+ const params = new URLSearchParams(window.location.search);
1665
+ for (const name of names) {
1666
+ const value = params.get(name);
1667
+ if (value && value.trim()) return value.trim();
1571
1668
  }
1572
- window.history.replaceState({}, "", url.toString());
1573
- window.dispatchEvent(new CustomEvent("vidfarm:editor-tracers-changed", {
1574
- detail: { tracers }
1575
- }));
1669
+ return "";
1576
1670
  }
1577
1671
 
1578
- const tracerStore = window.__vidfarmEditorTracers || (() => {
1579
- let listeners = [];
1580
- const notify = () => {
1581
- const nextTracers = parseTracersFromUrl();
1582
- for (const listener of listeners) {
1583
- listener(nextTracers);
1584
- }
1585
- };
1586
- return {
1587
- get() {
1588
- return parseTracersFromUrl();
1589
- },
1590
- set(tracers) {
1591
- const normalized = [...new Set((Array.isArray(tracers) ? tracers : []).map((value) => String(value || "").trim()).filter(Boolean))];
1592
- syncTracerUrl(normalized);
1593
- notify();
1594
- },
1595
- add(tracer) {
1596
- const nextTracer = String(tracer || "").trim() || makeTracerId();
1597
- const current = this.get().filter((entry) => entry !== nextTracer);
1598
- this.set([nextTracer, ...current]);
1599
- return nextTracer;
1600
- },
1601
- remove(tracer) {
1602
- const current = this.get().filter((entry) => entry !== tracer);
1603
- this.set(current);
1604
- },
1605
- activate(tracer) {
1606
- const current = this.get();
1607
- if (!current.includes(tracer)) {
1608
- this.add(tracer);
1609
- return;
1610
- }
1611
- this.set([tracer, ...current.filter((entry) => entry !== tracer)]);
1612
- },
1613
- subscribe(listener) {
1614
- listeners.push(listener);
1615
- return () => {
1616
- listeners = listeners.filter((entry) => entry !== listener);
1617
- };
1618
- }
1619
- };
1620
- })();
1621
- window.__vidfarmEditorTracers = tracerStore;
1672
+ function sourceNameFromUrl(value) {
1673
+ try {
1674
+ const pathName = new URL(value, window.location.href).pathname;
1675
+ const fileName = decodeURIComponent(pathName.split("/").filter(Boolean).pop() || "");
1676
+ return fileName || "Source image";
1677
+ } catch {
1678
+ return "Source image";
1679
+ }
1680
+ }
1622
1681
 
1623
- function openTracerModal() {
1624
- if (!hasTracerModal) {
1682
+ function loadSourceUrl(value) {
1683
+ if (!value) return;
1684
+ let url = "";
1685
+ try {
1686
+ url = absoluteUrl(value);
1687
+ } catch {
1688
+ setStatus("The source image URL is invalid.", "error");
1625
1689
  return;
1626
1690
  }
1627
- modalBackdrop.hidden = false;
1628
- modalBackdrop.setAttribute("data-open", "true");
1629
- modalError.textContent = "";
1630
- modalInput.value = "";
1631
- window.requestAnimationFrame(() => modalInput.focus());
1691
+ sourceImageUrl = url;
1692
+ sourceImageName = firstSearchParam(["source_image_name", "image_name", "file_name"]) || sourceNameFromUrl(url);
1693
+ sourceName.textContent = sourceImageName;
1694
+ emptyState.hidden = true;
1695
+ canvasWrap.hidden = false;
1696
+ outputPanel.hidden = true;
1697
+ resultImage.removeAttribute("src");
1698
+ resultDownload.href = "#";
1699
+ resultDownload.removeAttribute("download");
1700
+ resetMask();
1701
+ inpaintImage.onload = () => {
1702
+ syncInpaintCanvasSize();
1703
+ resetMask();
1704
+ };
1705
+ inpaintImage.src = sourceImageUrl;
1706
+ setStatus("Source ready from URL.", "success");
1632
1707
  }
1633
1708
 
1634
- function closeTracerModal() {
1635
- if (!hasTracerModal) {
1636
- return;
1709
+ function clearInpaintUrlParams() {
1710
+ const url = new URL(window.location.href);
1711
+ for (const key of ["tracer", "tracer_id", "trace", "source_image_url", "image_url", "input_image_url", "image", "source_image_name", "image_name", "file_name"]) {
1712
+ url.searchParams.delete(key);
1637
1713
  }
1638
- modalBackdrop.setAttribute("data-open", "false");
1639
- modalBackdrop.hidden = true;
1640
- modalError.textContent = "";
1714
+ window.history.replaceState({}, "", url.toString());
1715
+ }
1716
+
1717
+ function initializeFromUrl() {
1718
+ tracerInput.value = firstSearchParam(["tracer", "tracer_id", "trace"]) || makeTracerId();
1719
+ loadSourceUrl(firstSearchParam(["source_image_url", "image_url", "input_image_url", "image"]));
1641
1720
  }
1642
1721
 
1643
- function submitTracerModal() {
1644
- if (!hasTracerModal) {
1722
+ async function addAttachmentFilesForScope(files, scope) {
1723
+ if (!scope) return;
1724
+ const selected = Array.from(files || []);
1725
+ if (!selected.length) return;
1726
+ if (inpaintAttachments.length + selected.length > maxInpaintAttachments) {
1727
+ setStatus("Use up to " + maxInpaintAttachments + " reference images.", "error");
1645
1728
  return;
1646
1729
  }
1647
- const tracer = modalInput.value.trim();
1648
- if (!tracer) {
1649
- modalError.textContent = "Enter a tracer id.";
1650
- modalInput.focus();
1651
- return;
1730
+ const previousDisabled = inpaintSubmit.disabled;
1731
+ inpaintSubmit.disabled = true;
1732
+ setStatus("Uploading reference image...", "");
1733
+ try {
1734
+ for (const file of selected) {
1735
+ if (!isAllowedInpaintAttachment(file)) {
1736
+ throw new Error("Reference attachments must be JPEG, PNG, or WebP images.");
1737
+ }
1738
+ const fileName = file.name || "reference.png";
1739
+ const uploadedUrl = await uploadTemporaryBlob(file, fileName, file.type || "image/png");
1740
+ inpaintAttachments.push({
1741
+ id: attachmentId(),
1742
+ scope: scope.scope === "color" ? "color" : "global",
1743
+ color: scope.scope === "color" ? normalizeHexColor(scope.color) : "",
1744
+ fileName,
1745
+ url: absoluteUrl(uploadedUrl)
1746
+ });
1747
+ }
1748
+ renderAllAttachmentChips();
1749
+ setStatus("Reference image ready.", "success");
1750
+ } catch (error) {
1751
+ console.error(error);
1752
+ setStatus(error instanceof Error ? error.message : "Unable to upload reference image.", "error");
1753
+ } finally {
1754
+ inpaintSubmit.disabled = previousDisabled;
1652
1755
  }
1653
- tracerStore.add(tracer);
1654
- closeTracerModal();
1655
1756
  }
1656
1757
 
1657
- function renderTracerChips(tracers) {
1658
- tracerChipRow.innerHTML = tracers.length
1659
- ? tracers.map((tracer) => '<span class="editor-history-chip">' + escapeHtml(tracer) + '</span>').join("")
1660
- : '<span class="editor-history-chip is-empty">No tracer selected yet</span>';
1758
+ async function addAttachmentFiles(files) {
1759
+ const scope = pendingInpaintAttachmentScope;
1760
+ pendingInpaintAttachmentScope = null;
1761
+ await addAttachmentFilesForScope(files, scope);
1661
1762
  }
1662
1763
 
1663
- function renderEmpty(message) {
1664
- historyRoot.innerHTML = '<div class="editor-history-empty">' + escapeHtml(message) + '</div>';
1764
+ function resetMask() {
1765
+ inpaintStrokes = [];
1766
+ redoStrokes = [];
1767
+ currentInpaintStroke = null;
1768
+ isInpaintPainting = false;
1769
+ redrawInpaintMask();
1665
1770
  }
1666
1771
 
1667
- function renderRefreshIcon() {
1668
- return '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
1669
- '<path d="M21 12a9 9 0 0 1-15.1 6.6L3 16"></path>' +
1670
- '<path d="M3 21v-5h5"></path>' +
1671
- '<path d="M3 12a9 9 0 0 1 15.1-6.6L21 8"></path>' +
1672
- '<path d="M21 3v5h-5"></path>' +
1673
- '</svg>';
1772
+ function resetWorkspace() {
1773
+ sourceImageUrl = "";
1774
+ sourceImageName = "";
1775
+ sourceName.textContent = "No image selected";
1776
+ emptyState.hidden = false;
1777
+ canvasWrap.hidden = true;
1778
+ outputPanel.hidden = true;
1779
+ resultImage.removeAttribute("src");
1780
+ resultDownload.href = "#";
1781
+ resultDownload.removeAttribute("download");
1782
+ inpaintImage.removeAttribute("src");
1783
+ inpaintCanvas.width = 1;
1784
+ inpaintCanvas.height = 1;
1785
+ inpaintGlobalPrompt.value = "";
1786
+ inpaintMaskColors = [{ color: "#ff2d55", prompt: "" }];
1787
+ inpaintAttachments = [];
1788
+ pendingInpaintAttachmentScope = null;
1789
+ activeInpaintColor = "#ff2d55";
1790
+ inpaintColorPicker.value = activeInpaintColor;
1791
+ resetMask();
1792
+ renderInpaintControls();
1793
+ setStatus("", "");
1674
1794
  }
1675
1795
 
1676
- function getPreviewKind(url) {
1677
- if (/\\.(png|jpe?g|gif|webp|avif|svg)(\\?|#|$)/i.test(url)) {
1678
- return "image";
1796
+ async function loadSourceFile(file) {
1797
+ if (!file || !file.type.startsWith("image/")) {
1798
+ setStatus("Choose an image file.", "error");
1799
+ return;
1679
1800
  }
1680
- if (/\\.(mp4|webm|ogg|mov|m4v)(\\?|#|$)/i.test(url)) {
1681
- return "video";
1801
+ uploadButton.disabled = true;
1802
+ inpaintSubmit.disabled = true;
1803
+ setStatus("Uploading source image...", "");
1804
+ try {
1805
+ const fileName = file.name || "source.png";
1806
+ const uploadedUrl = await uploadTemporaryBlob(file, fileName, file.type || "image/png");
1807
+ sourceImageUrl = absoluteUrl(uploadedUrl);
1808
+ sourceImageName = fileName;
1809
+ sourceName.textContent = fileName;
1810
+ emptyState.hidden = true;
1811
+ canvasWrap.hidden = false;
1812
+ outputPanel.hidden = true;
1813
+ resultImage.removeAttribute("src");
1814
+ resultDownload.href = "#";
1815
+ resultDownload.removeAttribute("download");
1816
+ resetMask();
1817
+ inpaintImage.onload = () => {
1818
+ syncInpaintCanvasSize();
1819
+ resetMask();
1820
+ };
1821
+ inpaintImage.src = sourceImageUrl;
1822
+ setStatus("Source ready.", "success");
1823
+ } catch (error) {
1824
+ console.error(error);
1825
+ setStatus(error instanceof Error ? error.message : "Unable to upload source image.", "error");
1826
+ } finally {
1827
+ uploadButton.disabled = false;
1828
+ inpaintSubmit.disabled = false;
1682
1829
  }
1683
- return "file";
1684
1830
  }
1685
1831
 
1686
- function isPreviewableMediaUrl(url) {
1687
- return getPreviewKind(url) !== "file";
1832
+ function extractOutputUrl(payload) {
1833
+ const result = payload && payload.result ? payload.result : {};
1834
+ return result.output?.image?.file_url
1835
+ || result.output?.primary_file_url
1836
+ || result.image?.file_url
1837
+ || result.primary_file_url
1838
+ || (Array.isArray(result.output?.files) ? result.output.files[0] : "")
1839
+ || (Array.isArray(result.files) ? result.files[0] : "");
1688
1840
  }
1689
1841
 
1690
- function openMediaModal(url, kind, label) {
1691
- if (!hasMediaModal) {
1692
- window.open(url, "_blank", "noopener,noreferrer");
1693
- return;
1694
- }
1695
- mediaModalTitle.textContent = label;
1696
- mediaModalSource.href = url;
1697
- if (kind === "image") {
1698
- mediaModalBody.innerHTML = '<img src="' + escapeHtml(url) + '" alt="' + escapeHtml(label) + '" />';
1699
- } else if (kind === "video") {
1700
- mediaModalBody.innerHTML = '<video src="' + escapeHtml(url) + '" controls autoplay playsinline preload="metadata"></video>';
1701
- } else {
1702
- mediaModalBody.innerHTML = '<a class="editor-history-button is-primary" href="' + escapeHtml(url) + '" target="_blank" rel="noreferrer">Open file</a>';
1703
- }
1704
- mediaModalBackdrop.hidden = false;
1705
- mediaModalBackdrop.setAttribute("data-open", "true");
1842
+ function isPendingJobStatus(status) {
1843
+ return ["queued", "running", "waiting_for_provider", "waiting_for_child", "waiting_for_human"].includes(String(status || ""));
1706
1844
  }
1707
1845
 
1708
- function closeMediaModal() {
1709
- if (!hasMediaModal) {
1710
- return;
1711
- }
1712
- mediaModalBackdrop.setAttribute("data-open", "false");
1713
- mediaModalBackdrop.hidden = true;
1714
- mediaModalSource.href = "#";
1715
- mediaModalBody.innerHTML = "";
1846
+ function pendingHistoryItemKey(jobId) {
1847
+ return "job:" + String(jobId || "");
1716
1848
  }
1717
1849
 
1718
- function renderPreviewLinks(urls) {
1719
- if (!Array.isArray(urls) || !urls.length) {
1720
- return "";
1721
- }
1722
- return '<div class="editor-history-card-previews">' + urls.map((url, index) => (
1723
- (() => {
1724
- const kind = getPreviewKind(url);
1725
- const label = 'Output ' + (index + 1);
1726
- if (kind === "image") {
1727
- return '<button type="button" class="editor-history-card-preview" data-preview-url="' + escapeHtml(url) + '" data-preview-kind="' + kind + '" data-preview-label="' + escapeHtml(label) + '">' +
1728
- '<img src="' + escapeHtml(url) + '" alt="' + escapeHtml(label) + '" loading="lazy" decoding="async" />' +
1729
- '<span class="editor-history-card-preview-label">' + escapeHtml(label) + '</span>' +
1730
- '</button>';
1731
- }
1732
- if (kind === "video") {
1733
- return '<button type="button" class="editor-history-card-preview" data-preview-url="' + escapeHtml(url) + '" data-preview-kind="' + kind + '" data-preview-label="' + escapeHtml(label) + '">' +
1734
- '<video src="' + escapeHtml(url) + '" muted playsinline preload="metadata" aria-hidden="true"></video>' +
1735
- '<span class="editor-history-video-play" aria-hidden="true"></span>' +
1736
- '<span class="editor-history-card-preview-label">' + escapeHtml(label) + '</span>' +
1737
- '</button>';
1738
- }
1739
- return '<button type="button" class="editor-history-card-preview" data-preview-url="' + escapeHtml(url) + '" data-preview-kind="' + kind + '" data-preview-label="' + escapeHtml(label) + '">' +
1740
- '<span class="editor-history-card-preview-fallback">' + escapeHtml(label) + '</span>' +
1741
- '</button>';
1742
- })()
1743
- )).join("") + '</div>';
1850
+ function makePendingInpaintHistoryItem(input) {
1851
+ const jobId = String(input?.jobId || input?.job_id || "");
1852
+ if (!jobId) return null;
1853
+ return {
1854
+ key: pendingHistoryItemKey(jobId),
1855
+ kind: "pending",
1856
+ jobId,
1857
+ status: String(input?.status || "queued"),
1858
+ progress: Number(input?.progress || 0),
1859
+ createdAt: String(input?.createdAt || input?.created_at || new Date().toISOString()),
1860
+ updatedAt: String(input?.updatedAt || input?.updated_at || new Date().toISOString())
1861
+ };
1744
1862
  }
1745
1863
 
1746
- function collectGroupPreviewUrls(entries) {
1747
- const seen = new Set();
1748
- const urls = [];
1749
- for (const entry of entries) {
1750
- const candidates = entry.outputMediaUrls && entry.outputMediaUrls.length ? entry.outputMediaUrls : entry.mediaUrls;
1751
- if (!Array.isArray(candidates)) {
1752
- continue;
1753
- }
1754
- for (const url of candidates) {
1755
- if (!url || seen.has(url) || !isPreviewableMediaUrl(url)) {
1756
- continue;
1757
- }
1758
- seen.add(url);
1759
- urls.push(url);
1760
- }
1761
- }
1762
- return urls;
1864
+ function upsertInpaintHistoryItem(item) {
1865
+ if (!item || !item.key) return;
1866
+ inpaintHistoryItems = inpaintHistoryItems.filter((entry) => (
1867
+ entry.key !== item.key
1868
+ && (!item.jobId || entry.jobId !== item.jobId)
1869
+ ));
1870
+ inpaintHistoryItems.unshift(item);
1871
+ inpaintHistoryItems.sort((a, b) => Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0));
1872
+ renderInpaintHistory();
1763
1873
  }
1764
1874
 
1765
- function renderGroupPreviewStrip(entries, groupKey) {
1766
- const urls = collectGroupPreviewUrls(entries);
1767
- if (!urls.length) {
1768
- return "";
1769
- }
1770
- const isExpanded = expandedPreviewGroups.has(groupKey);
1771
- const visibleUrls = isExpanded ? urls : urls.slice(0, 5);
1772
- const visible = visibleUrls.map((url) => {
1773
- const kind = getPreviewKind(url);
1774
- if (kind === "video") {
1775
- const label = 'Output preview';
1776
- return '<button type="button" class="editor-history-group-preview" data-preview-url="' + escapeHtml(url) + '" data-preview-kind="video" data-preview-label="' + escapeHtml(label) + '" aria-label="Preview video output">' +
1777
- '<video src="' + escapeHtml(url) + '" muted playsinline preload="metadata" aria-hidden="true"></video>' +
1778
- '<span class="editor-history-video-play" aria-hidden="true"></span>' +
1779
- '</button>';
1875
+ function applyCompletedInpaintJob(payload) {
1876
+ const outputUrl = extractOutputUrl(payload);
1877
+ if (!outputUrl) return "";
1878
+ const url = absoluteUrl(outputUrl);
1879
+ const jobId = String(payload?.job_id || payload?.jobId || "");
1880
+ resultImage.src = url;
1881
+ resultDownload.href = url;
1882
+ resultDownload.setAttribute("download", "inpaint-" + Date.now() + ".png");
1883
+ outputPanel.hidden = false;
1884
+ upsertInpaintHistoryItem({
1885
+ key: historyItemKey({ jobId: jobId || "inpaint" }, url, 0),
1886
+ kind: "output",
1887
+ jobId,
1888
+ url,
1889
+ createdAt: String(payload?.completed_at || payload?.updated_at || new Date().toISOString())
1890
+ });
1891
+ return url;
1892
+ }
1893
+
1894
+ function historyEntryUrls(entry) {
1895
+ const urls = Array.isArray(entry?.outputMediaUrls) ? entry.outputMediaUrls : [];
1896
+ return urls.filter((url) => {
1897
+ const value = String(url || "");
1898
+ if (!/\\.(png|jpe?g|webp|gif|avif)(\\?|#|$)/i.test(value)) {
1899
+ return false;
1780
1900
  }
1781
- if (kind === "image") {
1782
- const label = 'Output preview';
1783
- return '<button type="button" class="editor-history-group-preview" data-preview-url="' + escapeHtml(url) + '" data-preview-kind="image" data-preview-label="' + escapeHtml(label) + '" aria-label="Preview image output">' +
1784
- '<img src="' + escapeHtml(url) + '" alt="" loading="lazy" decoding="async" />' +
1785
- '</button>';
1901
+ try {
1902
+ const parsed = new URL(value, window.location.href);
1903
+ const storageKey = parsed.searchParams.get("key") || decodeURIComponent(parsed.pathname).replace(/^\\/+/, "");
1904
+ return !/\\/temporary\\/inpaint(?:-|\\/|$)/i.test(storageKey)
1905
+ && !/\\/temporary\\/inpaint-masks(?:\\/|$)/i.test(storageKey)
1906
+ && !/inpaint-mask-[^/]+\\.png/i.test(storageKey);
1907
+ } catch {
1908
+ return !/\\/temporary\\/inpaint(?:-|\\/|$)/i.test(value)
1909
+ && !/\\/temporary\\/inpaint-masks(?:\\/|$)/i.test(value)
1910
+ && !/inpaint-mask-[^/]+\\.png/i.test(value);
1786
1911
  }
1787
- return "";
1788
- }).join("");
1789
- const more = !isExpanded && urls.length > visibleUrls.length
1790
- ? '<button type="button" class="editor-history-group-preview-more" data-editor-expand-media="' + escapeHtml(groupKey) + '" aria-label="Show all media previews">+' + escapeHtml(urls.length - visibleUrls.length) + '</button>'
1791
- : "";
1792
- return '<div class="editor-history-group-previews' + (isExpanded ? ' is-expanded' : '') + '" aria-label="Generated media previews">' + visible + more + '</div>';
1912
+ });
1793
1913
  }
1794
1914
 
1795
- function renderSample(title, payload, omittedReason) {
1796
- return '<section class="editor-history-sample">' +
1797
- '<h4>' + escapeHtml(title) + '</h4>' +
1798
- (omittedReason ? '<div class="editor-history-sample-note">' + escapeHtml(omittedReason) + '</div>' : '') +
1799
- '<pre>' + escapeHtml(formatJson(payload)) + '</pre>' +
1800
- '</section>';
1915
+ function isInpaintHistoryEntry(entry) {
1916
+ const templateId = String(entry?.linkedJob?.template_id || entry?.linkedJob?.templateId || "").toLowerCase();
1917
+ const path = String(entry?.path || "").toLowerCase();
1918
+ return templateId === "primitive:image_inpaint"
1919
+ || path.includes("/primitives/images/inpaint")
1920
+ || path.includes("/primitives/image_inpaint");
1801
1921
  }
1802
1922
 
1803
- function renderEntry(entry) {
1804
- return '<article class="editor-history-card">' +
1805
- renderHttpAction(entry) +
1806
- '<div class="editor-history-card-foot">' +
1807
- '<span>' + escapeHtml(new Date(entry.createdAt).toLocaleString()) + '</span>' +
1808
- '</div>' +
1809
- renderPreviewLinks(entry.outputMediaUrls && entry.outputMediaUrls.length ? entry.outputMediaUrls : entry.mediaUrls) +
1810
- '</article>';
1923
+ function historyItemKey(entry, url, index) {
1924
+ return String(entry?.jobId || entry?.job_id || entry?.id || "inpaint") + ":" + index + ":" + String(url || "");
1811
1925
  }
1812
1926
 
1813
- function hasGeneratedTracerSuffix(tracer) {
1814
- return /-[a-z0-9]{8}$/i.test(String(tracer || "").trim());
1927
+ function setHistoryEmpty(message) {
1928
+ historyList.innerHTML = "";
1929
+ const empty = document.createElement("div");
1930
+ empty.className = "custom-inpaint-history-empty";
1931
+ empty.textContent = message;
1932
+ historyList.append(empty);
1815
1933
  }
1816
1934
 
1817
- function doesTracerMatchSelection(entryTracer, selectedTracer) {
1818
- const normalizedEntry = String(entryTracer || "").trim();
1819
- const normalizedSelected = String(selectedTracer || "").trim();
1820
- if (!normalizedEntry || !normalizedSelected) {
1821
- return false;
1935
+ function renderInpaintHistory() {
1936
+ historyList.innerHTML = "";
1937
+ const visible = inpaintHistoryItems.filter((item) => !hiddenInpaintHistoryKeys.has(item.key));
1938
+ if (!visible.length) {
1939
+ setHistoryEmpty("No inpaint outputs yet.");
1940
+ return;
1941
+ }
1942
+ for (const item of visible) {
1943
+ const row = document.createElement("div");
1944
+ row.className = "custom-inpaint-history-item";
1945
+ row.dataset.historyKey = item.key;
1946
+ let thumbButton;
1947
+ if (item.kind === "pending") {
1948
+ thumbButton = document.createElement("div");
1949
+ thumbButton.className = "custom-inpaint-history-generating";
1950
+ thumbButton.setAttribute("aria-label", "Image edit generating");
1951
+ thumbButton.setAttribute("role", "img");
1952
+ } else {
1953
+ thumbButton = document.createElement("button");
1954
+ thumbButton.type = "button";
1955
+ thumbButton.className = "custom-inpaint-history-thumb-button";
1956
+ thumbButton.dataset.historyPreview = item.key;
1957
+ thumbButton.setAttribute("aria-label", "View inpaint output");
1958
+ const thumb = document.createElement("img");
1959
+ thumb.className = "custom-inpaint-history-thumb";
1960
+ thumb.src = item.url;
1961
+ thumb.alt = "Image edit output";
1962
+ thumb.loading = "lazy";
1963
+ thumbButton.append(thumb);
1964
+ }
1965
+ const meta = document.createElement("div");
1966
+ meta.className = "custom-inpaint-history-meta";
1967
+ const title = document.createElement("div");
1968
+ title.className = "custom-inpaint-history-title";
1969
+ title.textContent = item.kind === "pending"
1970
+ ? "Generating inpaint"
1971
+ : (item.createdAt ? new Date(item.createdAt).toLocaleString() : "Image edit output");
1972
+ if (item.kind === "pending") {
1973
+ const subtitle = document.createElement("div");
1974
+ subtitle.className = "custom-inpaint-history-subtitle";
1975
+ const progress = Number(item.progress || 0);
1976
+ subtitle.textContent = "Job " + (item.status || "queued") + (progress > 0 ? " · " + Math.round(progress * 100) + "%" : "");
1977
+ meta.append(title, subtitle);
1978
+ } else {
1979
+ meta.append(title);
1980
+ }
1981
+ const actions = document.createElement("div");
1982
+ actions.className = "custom-inpaint-history-actions";
1983
+ if (item.kind === "pending") {
1984
+ const check = document.createElement("button");
1985
+ check.type = "button";
1986
+ check.className = "custom-inpaint-history-check";
1987
+ check.dataset.historyCheck = item.jobId || "";
1988
+ check.textContent = "Check Status";
1989
+ actions.append(check);
1990
+ } else {
1991
+ const download = document.createElement("a");
1992
+ download.className = "custom-inpaint-history-icon";
1993
+ download.href = item.url;
1994
+ download.target = "_blank";
1995
+ download.rel = "noopener noreferrer";
1996
+ download.download = imageDownloadName("inpaint-history", item.createdAt);
1997
+ download.setAttribute("aria-label", "Download inpaint output");
1998
+ download.setAttribute("title", "Download");
1999
+ 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>';
2000
+ const copy = document.createElement("button");
2001
+ copy.type = "button";
2002
+ copy.className = "custom-inpaint-history-icon";
2003
+ copy.dataset.historyCopy = item.key;
2004
+ copy.setAttribute("aria-label", "Copy inpaint output URL");
2005
+ copy.setAttribute("title", "Copy URL");
2006
+ 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>';
2007
+ actions.append(download, copy);
2008
+ }
2009
+ const remove = document.createElement("button");
2010
+ remove.type = "button";
2011
+ remove.className = "custom-inpaint-history-icon";
2012
+ remove.dataset.historyRemove = item.key;
2013
+ remove.setAttribute("aria-label", "Remove inpaint output from history strip");
2014
+ remove.setAttribute("title", "Remove");
2015
+ 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>';
2016
+ actions.append(remove);
2017
+ meta.append(actions);
2018
+ const confirm = document.createElement("div");
2019
+ confirm.className = "custom-inpaint-history-popconfirm";
2020
+ confirm.hidden = true;
2021
+ confirm.dataset.historyConfirm = item.key;
2022
+ const confirmText = document.createElement("p");
2023
+ confirmText.textContent = "Remove from this history strip?";
2024
+ const confirmActions = document.createElement("div");
2025
+ confirmActions.className = "custom-inpaint-history-popconfirm-actions";
2026
+ const cancel = document.createElement("button");
2027
+ cancel.type = "button";
2028
+ cancel.dataset.historyCancel = item.key;
2029
+ cancel.textContent = "Cancel";
2030
+ const confirmRemove = document.createElement("button");
2031
+ confirmRemove.type = "button";
2032
+ confirmRemove.dataset.historyConfirmRemove = item.key;
2033
+ confirmRemove.dataset.danger = "true";
2034
+ confirmRemove.textContent = "Remove";
2035
+ confirmActions.append(cancel, confirmRemove);
2036
+ confirm.append(confirmText, confirmActions);
2037
+ row.append(thumbButton, meta, confirm);
2038
+ historyList.append(row);
1822
2039
  }
1823
- if (normalizedEntry === normalizedSelected) {
1824
- return true;
2040
+ }
2041
+
2042
+ function closeInpaintHistoryConfirms(exceptKey) {
2043
+ historyList.querySelectorAll("[data-history-confirm]").forEach((entry) => {
2044
+ if (!(entry instanceof HTMLElement)) return;
2045
+ entry.hidden = exceptKey ? entry.dataset.historyConfirm !== exceptKey : true;
2046
+ });
2047
+ }
2048
+
2049
+ function extractInpaintHistoryItems(entries) {
2050
+ const items = [];
2051
+ const seen = new Set();
2052
+ for (const entry of Array.isArray(entries) ? entries : []) {
2053
+ if (!isInpaintHistoryEntry(entry)) continue;
2054
+ const linkedJob = entry?.linkedJob || null;
2055
+ if (linkedJob && isPendingJobStatus(linkedJob.status)) {
2056
+ const pending = makePendingInpaintHistoryItem({
2057
+ job_id: linkedJob.job_id,
2058
+ status: linkedJob.status,
2059
+ progress: linkedJob.progress,
2060
+ created_at: linkedJob.created_at || entry.createdAt,
2061
+ updated_at: linkedJob.updated_at || entry.createdAt
2062
+ });
2063
+ if (pending && !seen.has(pending.key)) {
2064
+ seen.add(pending.key);
2065
+ items.push(pending);
2066
+ }
2067
+ continue;
2068
+ }
2069
+ historyEntryUrls(entry).forEach((rawUrl, index) => {
2070
+ const url = absoluteUrl(rawUrl);
2071
+ const key = historyItemKey(entry, url, index);
2072
+ if (seen.has(key)) return;
2073
+ seen.add(key);
2074
+ items.push({
2075
+ key,
2076
+ kind: "output",
2077
+ jobId: String(entry?.jobId || entry?.job_id || entry?.linkedJob?.job_id || ""),
2078
+ url,
2079
+ createdAt: String(entry.createdAt || new Date().toISOString())
2080
+ });
2081
+ });
1825
2082
  }
1826
- return normalizedEntry.startsWith(normalizedSelected + "-") && hasGeneratedTracerSuffix(normalizedEntry);
2083
+ return items;
1827
2084
  }
1828
2085
 
1829
2086
  async function copyText(value) {
@@ -1831,9 +2088,7 @@ export function renderTemplateEditorDocsPage(input) {
1831
2088
  try {
1832
2089
  await navigator.clipboard.writeText(value);
1833
2090
  return true;
1834
- } catch {
1835
- // Fall back to the textarea path when browser permissions block clipboard.writeText.
1836
- }
2091
+ } catch {}
1837
2092
  }
1838
2093
  const field = document.createElement("textarea");
1839
2094
  field.value = value;
@@ -1851,459 +2106,386 @@ export function renderTemplateEditorDocsPage(input) {
1851
2106
  return copied;
1852
2107
  }
1853
2108
 
1854
- const copyFeedbackTimers = new WeakMap();
1855
-
1856
- function clearCopyFeedback(button) {
1857
- const previousTimer = copyFeedbackTimers.get(button);
1858
- if (previousTimer) {
1859
- window.clearTimeout(previousTimer);
1860
- }
1861
- copyFeedbackTimers.delete(button);
1862
- button.removeAttribute("data-copying");
1863
- button.removeAttribute("data-copied");
1864
- button.removeAttribute("data-copy-error");
1865
- }
1866
-
1867
- function resetCopyFeedback(button, label) {
1868
- button.removeAttribute("data-copying");
1869
- button.removeAttribute("data-copied");
1870
- button.removeAttribute("data-copy-error");
1871
- button.setAttribute("aria-label", "Copy " + label);
1872
- button.setAttribute("title", "Copy " + label);
1873
- copyFeedbackTimers.delete(button);
1874
- }
1875
-
1876
- function showCopyingFeedback(button, label) {
1877
- clearCopyFeedback(button);
1878
- button.setAttribute("data-copying", "true");
1879
- button.setAttribute("aria-label", "Copying " + label);
1880
- button.setAttribute("title", "Copying");
1881
- }
1882
-
1883
- function showCopyFeedback(button, label) {
1884
- clearCopyFeedback(button);
1885
- button.setAttribute("data-copied", "true");
1886
- button.setAttribute("aria-label", "Copied " + label);
1887
- button.setAttribute("title", "Copied");
1888
- const timer = window.setTimeout(() => {
1889
- resetCopyFeedback(button, label);
1890
- }, 1400);
1891
- copyFeedbackTimers.set(button, timer);
1892
- }
1893
-
1894
- function showCopyErrorFeedback(button, label) {
1895
- clearCopyFeedback(button);
1896
- button.setAttribute("data-copy-error", "true");
1897
- button.setAttribute("aria-label", "Copy failed for " + label);
1898
- button.setAttribute("title", "Copy failed");
1899
- const timer = window.setTimeout(() => {
1900
- resetCopyFeedback(button, label);
1901
- }, 1800);
1902
- copyFeedbackTimers.set(button, timer);
1903
- }
1904
-
1905
- function renderGroups(entries, tracers) {
1906
- if (!tracers.length) {
1907
- renderEmpty("Use Copilot to start or select a tracer, then its grouped POST history will appear here.");
1908
- return;
2109
+ async function loadInpaintHistory() {
2110
+ const tracer = getTracerValue();
2111
+ historyRefresh.disabled = true;
2112
+ try {
2113
+ const endpoint = new URL("/job-runs/history", window.location.origin);
2114
+ endpoint.searchParams.set("limit", "120");
2115
+ endpoint.searchParams.set("template_id", "primitive:image_inpaint");
2116
+ endpoint.searchParams.set("tracer", tracer);
2117
+ const response = await fetch(endpoint, { credentials: "same-origin" });
2118
+ const payload = await parseJsonResponse(response);
2119
+ if (!response.ok || (payload && payload.error)) throw new Error(payload.error || "Unable to load inpaint history.");
2120
+ inpaintHistoryItems = extractInpaintHistoryItems(payload.entries);
2121
+ renderInpaintHistory();
2122
+ } catch (error) {
2123
+ console.error(error);
2124
+ setHistoryEmpty(error instanceof Error ? error.message : "Unable to load inpaint history.");
2125
+ } finally {
2126
+ historyRefresh.disabled = false;
1909
2127
  }
2128
+ }
1910
2129
 
1911
- const grouped = new Map();
1912
- for (const entry of entries) {
1913
- const key = tracers.find((tracer) => doesTracerMatchSelection(entry.tracer, tracer)) || entry.tracer || "No tracer";
1914
- const existing = grouped.get(key) || [];
1915
- existing.push(entry);
1916
- grouped.set(key, existing);
2130
+ async function checkInpaintJobStatus(jobId, button) {
2131
+ if (!jobId) return null;
2132
+ if (button instanceof HTMLButtonElement) {
2133
+ button.disabled = true;
2134
+ button.textContent = "Checking";
1917
2135
  }
1918
-
1919
- historyRoot.innerHTML = tracers.map((tracer, index) => {
1920
- const groupEntries = grouped.get(tracer) || [];
1921
- const activeRefresh = index === 0
1922
- ? '<button type="button" class="editor-history-button editor-history-refresh" data-editor-history-refresh="' + escapeHtml(tracer) + '" aria-label="Refresh tracer history" title="Refresh tracer history">' + renderRefreshIcon() + '</button>'
1923
- : '';
1924
- const menu = '<span class="editor-history-menu-wrap" data-editor-history-menu-wrap>' +
1925
- '<button type="button" class="editor-history-button editor-history-menu-button" data-editor-history-menu-toggle="' + escapeHtml(tracer) + '" aria-label="Tracer actions" title="Tracer actions">...</button>' +
1926
- '<span class="editor-history-menu" role="menu">' +
1927
- '<button type="button" data-editor-history-copy="' + escapeHtml(tracer) + '">Copy Tracer</button>' +
1928
- '<button type="button" data-editor-history-rename="' + escapeHtml(tracer) + '">Rename tracer</button>' +
1929
- '<button type="button" data-editor-history-remove="' + escapeHtml(tracer) + '">Remove from view</button>' +
1930
- '<button type="button" data-editor-history-archive="' + escapeHtml(tracer) + '" data-danger="true">Archive tracer</button>' +
1931
- '</span>' +
1932
- '</span>';
1933
- const callsLabel = groupEntries.length
1934
- ? escapeHtml(groupEntries.length) + ' POST call' + (groupEntries.length === 1 ? '' : 's')
1935
- : 'No POST calls yet';
1936
- return (
1937
- '<details class="editor-history-group"' + (index === 0 ? ' open' : '') + '>' +
1938
- '<summary>' +
1939
- '<div class="editor-history-group-main">' +
1940
- '<h3>' + escapeHtml(tracer) + '</h3>' +
1941
- '<p>' + callsLabel + '</p>' +
1942
- renderGroupPreviewStrip(groupEntries, tracer) +
1943
- '</div>' +
1944
- '<div class="editor-history-group-actions">' +
1945
- activeRefresh +
1946
- menu +
1947
- '<span class="editor-history-group-toggle">Toggle</span>' +
1948
- '</div>' +
1949
- '</summary>' +
1950
- '<div class="editor-history-group-body">' +
1951
- (groupEntries.length
1952
- ? groupEntries.map(renderEntry).join("")
1953
- : '<div class="editor-history-empty">No POST history exists for this tracer yet.</div>') +
1954
- '</div>' +
1955
- '</details>'
1956
- )}).join("");
1957
- }
1958
-
1959
- async function fetchHistoryPayload(endpoint) {
1960
- const response = await fetch(endpoint.toString(), { credentials: "same-origin" });
1961
- let payload = null;
1962
2136
  try {
1963
- payload = await response.json();
1964
- } catch {
1965
- payload = null;
1966
- }
1967
- if (!response.ok) {
1968
- const message = payload && typeof payload.error === "string"
1969
- ? payload.error
1970
- : "Unable to load POST history.";
1971
- throw new Error(message + " (" + response.status + ")");
2137
+ const response = await fetch("/api/v1/primitives/image_inpaint/jobs/" + encodeURIComponent(jobId), {
2138
+ credentials: "same-origin"
2139
+ });
2140
+ const payload = await parseJsonResponse(response);
2141
+ if (!response.ok) throw new Error(payload.error || "Unable to check inpaint job.");
2142
+ if (payload.status === "failed" || payload.status === "cancelled") {
2143
+ const pending = makePendingInpaintHistoryItem(payload);
2144
+ upsertInpaintHistoryItem({
2145
+ ...(pending || { key: pendingHistoryItemKey(jobId), kind: "pending", jobId, createdAt: new Date().toISOString() }),
2146
+ status: payload.status,
2147
+ progress: Number(payload.progress || 0)
2148
+ });
2149
+ throw new Error(payload.error?.message || "Image edit job " + payload.status + ".");
2150
+ }
2151
+ const outputUrl = extractOutputUrl(payload);
2152
+ if (payload.status === "succeeded" || payload.status === "complete" || outputUrl) {
2153
+ const url = applyCompletedInpaintJob(payload);
2154
+ if (!url) throw new Error("Image edit completed without an output URL.");
2155
+ setStatus("Image edit complete.", "success");
2156
+ return payload;
2157
+ }
2158
+ upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
2159
+ setStatus("Image edit job " + jobId + " is " + (payload.status || "running") + ".", "");
2160
+ return payload;
2161
+ } catch (error) {
2162
+ console.error(error);
2163
+ setStatus(error instanceof Error ? error.message : "Unable to check inpaint job.", "error");
2164
+ return null;
2165
+ } finally {
2166
+ if (button instanceof HTMLButtonElement) {
2167
+ button.disabled = false;
2168
+ button.textContent = "Check Status";
2169
+ }
1972
2170
  }
1973
- return payload || {};
1974
2171
  }
1975
2172
 
1976
- async function loadFallbackHistory(selectedTracers) {
1977
- if (!boot.fallbackHistoryEndpoint) {
1978
- throw new Error("No fallback history endpoint is configured.");
1979
- }
1980
- const byId = new Map();
1981
- for (const tracer of selectedTracers) {
1982
- const endpoint = new URL(boot.fallbackHistoryEndpoint, window.location.origin);
1983
- endpoint.searchParams.set("tracer", tracer);
1984
- const payload = await fetchHistoryPayload(endpoint);
1985
- for (const entry of Array.isArray(payload.entries) ? payload.entries : []) {
1986
- byId.set(entry.id, entry);
2173
+ async function pollInpaintJob(jobId) {
2174
+ for (let attempt = 0; attempt < 90; attempt += 1) {
2175
+ const response = await fetch("/api/v1/primitives/image_inpaint/jobs/" + encodeURIComponent(jobId), {
2176
+ credentials: "same-origin"
2177
+ });
2178
+ const payload = await parseJsonResponse(response);
2179
+ if (!response.ok) throw new Error(payload.error || "Unable to poll inpaint job.");
2180
+ if (payload.status === "failed" || payload.status === "cancelled") {
2181
+ throw new Error(payload.error?.message || "Image edit job " + payload.status + ".");
1987
2182
  }
2183
+ const outputUrl = extractOutputUrl(payload);
2184
+ if (payload.status === "succeeded" || payload.status === "complete" || outputUrl) {
2185
+ if (!outputUrl) throw new Error("Image edit completed without an output URL.");
2186
+ applyCompletedInpaintJob(payload);
2187
+ setStatus("Image edit complete.", "success");
2188
+ return outputUrl;
2189
+ }
2190
+ upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
2191
+ setStatus("Image edit job " + jobId + " is " + (payload.status || "running") + ".", "");
2192
+ await new Promise((resolve) => window.setTimeout(resolve, attempt < 8 ? 1500 : 3000));
1988
2193
  }
1989
- return { entries: [...byId.values()] };
2194
+ upsertInpaintHistoryItem(makePendingInpaintHistoryItem({ job_id: jobId, status: "running" }));
2195
+ setStatus("Image edit job is still running. Use Check Status in History.", "");
2196
+ return null;
1990
2197
  }
1991
2198
 
1992
- async function loadHistory(options) {
1993
- const showLoading = Boolean(options && options.showLoading);
1994
- const triggerButton = options && options.triggerButton instanceof HTMLButtonElement ? options.triggerButton : null;
1995
- const selectedTracers = tracerStore.get();
1996
- renderTracerChips(selectedTracers);
1997
- if (!selectedTracers.length) {
1998
- renderEmpty("No tracer selected yet.");
2199
+ async function submitInpaintJob() {
2200
+ if (!sourceImageUrl || inpaintSubmit.disabled) {
2201
+ setStatus("Upload a source image first.", "error");
1999
2202
  return;
2000
2203
  }
2001
- if (!boot.isLoggedIn) {
2002
- renderEmpty("Sign in to load tracer-scoped POST history.");
2204
+ if (!inpaintStrokes.length) {
2205
+ setStatus("Paint at least one mask region.", "error");
2003
2206
  return;
2004
2207
  }
2005
-
2006
- const endpoint = new URL(boot.historyEndpoint, window.location.origin);
2007
- if (selectedTracers.length) {
2008
- endpoint.searchParams.set("tracers", selectedTracers.join(","));
2009
- } else {
2010
- endpoint.searchParams.delete("tracers");
2208
+ const usedColors = new Set(inpaintStrokes.map((stroke) => normalizeHexColor(stroke.color)));
2209
+ const globalPrompt = inpaintGlobalPrompt.value.trim();
2210
+ const missingPromptColor = inpaintMaskColors.find((entry) => usedColors.has(entry.color) && !String(entry.prompt || "").trim());
2211
+ if (!globalPrompt && missingPromptColor) {
2212
+ setStatus("Add a replacement prompt for " + missingPromptColor.color.toUpperCase() + ".", "error");
2213
+ return;
2011
2214
  }
2012
-
2013
- if (triggerButton) {
2014
- triggerButton.disabled = true;
2015
- triggerButton.setAttribute("data-loading", "true");
2016
- } else if (showLoading) {
2017
- historyRoot.innerHTML = '<div class="editor-history-empty">Loading POST history…</div>';
2215
+ const instruction = buildInpaintInstruction();
2216
+ if (!instruction) {
2217
+ setStatus("Add a replacement prompt for at least one color.", "error");
2218
+ return;
2018
2219
  }
2220
+ setSubmitLoading(true);
2221
+ uploadButton.disabled = true;
2222
+ setStatus("Uploading masks...", "");
2019
2223
  try {
2020
- let payload;
2021
- try {
2022
- payload = await fetchHistoryPayload(endpoint);
2023
- } catch (primaryError) {
2024
- console.warn("Editor-scoped history failed; trying job-runs history fallback.", primaryError);
2025
- payload = await loadFallbackHistory(selectedTracers);
2026
- }
2027
- const entries = Array.isArray(payload.entries) ? payload.entries : [];
2028
- lastHistoryEntries = entries;
2029
- lastSelectedTracers = selectedTracers;
2030
- renderGroups(entries, selectedTracers);
2224
+ const maskUrl = await uploadTemporaryBlob(await createProviderMaskBlob(), "inpaint-mask-" + Date.now() + ".png", "image/png");
2225
+ const tracer = getTracerValue();
2226
+ setStatus("Creating image edit job...", "");
2227
+ const response = await fetch("/api/v1/primitives/images/inpaint", {
2228
+ method: "POST",
2229
+ credentials: "same-origin",
2230
+ headers: { "content-type": "application/json" },
2231
+ body: JSON.stringify({
2232
+ tracer,
2233
+ payload: {
2234
+ source_image_url: sourceImageUrl,
2235
+ mask_url: absoluteUrl(maskUrl),
2236
+ instruction,
2237
+ reference_attachments: inpaintAttachments.map((attachment) => attachment.url),
2238
+ ...(inpaintProviderSelect.value ? { provider: inpaintProviderSelect.value } : {}),
2239
+ size: "auto",
2240
+ ...(getInpaintAspectRatio() ? { aspect_ratio: getInpaintAspectRatio() } : {}),
2241
+ output_format: "png"
2242
+ }
2243
+ })
2244
+ });
2245
+ const payload = await parseJsonResponse(response);
2246
+ if (!response.ok) throw new Error(payload.error || "Unable to create image edit job.");
2247
+ setStatus("Image edit job " + (payload.job_id || "") + " started.", "success");
2248
+ upsertInpaintHistoryItem(makePendingInpaintHistoryItem(payload));
2249
+ await pollInpaintJob(payload.job_id);
2250
+ await loadInpaintHistory();
2031
2251
  } catch (error) {
2032
2252
  console.error(error);
2033
- renderEmpty("Unable to load tracer-scoped POST history" + (error instanceof Error && error.message ? ": " + error.message : "."));
2253
+ setStatus(error instanceof Error ? error.message : "Unable to create image edit.", "error");
2034
2254
  } finally {
2035
- if (triggerButton) {
2036
- triggerButton.disabled = false;
2037
- triggerButton.setAttribute("data-loading", "false");
2038
- }
2255
+ setSubmitLoading(false);
2256
+ uploadButton.disabled = false;
2039
2257
  }
2040
2258
  }
2041
2259
 
2042
- async function renameStoredTracer(fromTracer, toTracer) {
2043
- const normalizedFrom = String(fromTracer || "").trim();
2044
- const normalizedTo = String(toTracer || "").trim();
2045
- if (!normalizedFrom || !normalizedTo || normalizedFrom === normalizedTo) {
2046
- return;
2047
- }
2048
- const response = await fetch(boot.tracerEndpoint, {
2049
- method: "PATCH",
2050
- credentials: "same-origin",
2051
- headers: { "content-type": "application/json" },
2052
- body: JSON.stringify({
2053
- from_tracer: normalizedFrom,
2054
- to_tracer: normalizedTo
2055
- })
2056
- });
2057
- if (!response.ok) {
2058
- throw new Error("Unable to update tracer.");
2059
- }
2060
- const nextTracers = tracerStore.get().map((entry) => entry === normalizedFrom ? normalizedTo : entry);
2061
- tracerStore.set(nextTracers);
2062
- await loadHistory({ showLoading: true });
2063
- }
2064
-
2065
- loadHistory({ showLoading: true });
2066
- window.addEventListener("popstate", () => loadHistory({ showLoading: true }));
2067
- window.addEventListener("vidfarm:editor-tracers-changed", () => loadHistory());
2068
- if (hasTracerModal) {
2069
- addTracerButton.addEventListener("click", () => openTracerModal());
2070
- modalSubmit.addEventListener("click", () => submitTracerModal());
2071
- modalCancel.addEventListener("click", () => closeTracerModal());
2072
- modalInput.addEventListener("keydown", (event) => {
2073
- if (event.key === "Enter") {
2074
- event.preventDefault();
2075
- submitTracerModal();
2076
- return;
2077
- }
2078
- if (event.key === "Escape") {
2079
- event.preventDefault();
2080
- closeTracerModal();
2081
- }
2082
- });
2083
- modalBackdrop.addEventListener("click", (event) => {
2084
- if (event.target === modalBackdrop) {
2085
- closeTracerModal();
2086
- }
2087
- });
2088
- }
2089
- if (hasMediaModal) {
2090
- mediaModalClose.addEventListener("click", () => closeMediaModal());
2091
- mediaModalBackdrop.addEventListener("click", (event) => {
2092
- if (event.target === mediaModalBackdrop) {
2093
- closeMediaModal();
2094
- }
2095
- });
2096
- }
2097
- if (hasHttpModal) {
2098
- httpModalClose.addEventListener("click", () => closeHttpModal());
2099
- httpModalBackdrop.addEventListener("click", (event) => {
2100
- if (event.target === httpModalBackdrop) {
2101
- closeHttpModal();
2102
- }
2103
- });
2104
- }
2105
- window.addEventListener("keydown", (event) => {
2106
- if (event.key === "Escape") {
2107
- if (hasHttpModal && httpModalBackdrop.hidden === false) {
2108
- closeHttpModal();
2109
- return;
2110
- }
2111
- if (hasMediaModal && mediaModalBackdrop.getAttribute("data-open") === "true") {
2112
- closeMediaModal();
2113
- return;
2114
- }
2115
- if (hasTracerModal && modalBackdrop.getAttribute("data-open") === "true") {
2116
- closeTracerModal();
2117
- }
2118
- }
2260
+ renderInpaintControls();
2261
+ void loadInpaintProviders();
2262
+ initializeFromUrl();
2263
+ void loadInpaintHistory();
2264
+ uploadButton.addEventListener("click", () => fileInput.click());
2265
+ historyRefresh.addEventListener("click", () => {
2266
+ void loadInpaintHistory();
2119
2267
  });
2120
- historyRoot.addEventListener("click", (event) => {
2268
+ historyList.addEventListener("pointerdown", (event) => {
2121
2269
  const target = event.target;
2122
- if (!(target instanceof Element)) {
2123
- return;
2124
- }
2125
- const previewButton = target.closest("[data-preview-url]");
2126
- if (previewButton instanceof HTMLButtonElement) {
2127
- event.preventDefault();
2270
+ if (target instanceof Element && target.closest("[data-history-confirm]")) {
2128
2271
  event.stopPropagation();
2129
- const url = previewButton.getAttribute("data-preview-url") || "";
2130
- const kind = previewButton.getAttribute("data-preview-kind") || "file";
2131
- const label = previewButton.getAttribute("data-preview-label") || "Output preview";
2132
- if (url) {
2133
- openMediaModal(url, kind, label);
2134
- }
2135
- return;
2136
2272
  }
2137
- const expandMediaButton = target.closest("[data-editor-expand-media]");
2138
- if (expandMediaButton instanceof HTMLButtonElement) {
2139
- event.preventDefault();
2273
+ });
2274
+ historyList.addEventListener("click", (event) => {
2275
+ const target = event.target;
2276
+ if (!(target instanceof Element)) return;
2277
+ const confirmPopover = target.closest("[data-history-confirm]");
2278
+ if (confirmPopover instanceof HTMLElement) {
2140
2279
  event.stopPropagation();
2141
- const key = expandMediaButton.getAttribute("data-editor-expand-media") || "";
2142
- if (key) {
2143
- expandedPreviewGroups.add(key);
2144
- renderGroups(lastHistoryEntries, lastSelectedTracers);
2145
- }
2280
+ }
2281
+ const previewButton = target.closest("[data-history-preview]");
2282
+ if (previewButton instanceof HTMLElement) {
2283
+ const key = previewButton.dataset.historyPreview || "";
2284
+ const item = inpaintHistoryItems.find((entry) => entry.key === key);
2285
+ if (!item) return;
2286
+ openImagePreview(item.url, item.createdAt ? new Date(item.createdAt).toLocaleString() : "Image edit output", imageDownloadName("image-edit-history", item.createdAt));
2146
2287
  return;
2147
2288
  }
2148
- const httpDetailButton = target.closest("[data-editor-http-detail]");
2149
- if (httpDetailButton instanceof HTMLButtonElement) {
2150
- event.preventDefault();
2151
- event.stopPropagation();
2152
- try {
2153
- openHttpModal(JSON.parse(decodeURIComponent(httpDetailButton.getAttribute("data-editor-http-detail") || "")));
2154
- } catch (error) {
2155
- console.error(error);
2156
- }
2289
+ const copyButton = target.closest("[data-history-copy]");
2290
+ if (copyButton instanceof HTMLElement) {
2291
+ const key = copyButton.dataset.historyCopy || "";
2292
+ const item = inpaintHistoryItems.find((entry) => entry.key === key);
2293
+ if (!item) return;
2294
+ void copyText(item.url).then((copied) => {
2295
+ copyButton.setAttribute("title", copied ? "Copied" : "Copy failed");
2296
+ window.setTimeout(() => copyButton.setAttribute("title", "Copy URL"), 1200);
2297
+ });
2157
2298
  return;
2158
2299
  }
2159
- const refreshButton = target.closest("[data-editor-history-refresh]");
2160
- if (refreshButton instanceof HTMLButtonElement) {
2161
- event.preventDefault();
2162
- event.stopPropagation();
2163
- void loadHistory({ triggerButton: refreshButton });
2300
+ const checkButton = target.closest("[data-history-check]");
2301
+ if (checkButton instanceof HTMLButtonElement) {
2302
+ void checkInpaintJobStatus(checkButton.dataset.historyCheck || "", checkButton);
2164
2303
  return;
2165
2304
  }
2166
- const menuToggle = target.closest("[data-editor-history-menu-toggle]");
2167
- if (menuToggle instanceof HTMLButtonElement) {
2168
- event.preventDefault();
2169
- event.stopPropagation();
2170
- const wrap = menuToggle.closest("[data-editor-history-menu-wrap]");
2171
- const isOpen = wrap instanceof HTMLElement && wrap.getAttribute("data-open") === "true";
2172
- document.querySelectorAll("[data-editor-history-menu-wrap]").forEach((entry) => entry.removeAttribute("data-open"));
2173
- if (wrap instanceof HTMLElement && !isOpen) {
2174
- wrap.setAttribute("data-open", "true");
2305
+ const removeButton = target.closest("[data-history-remove]");
2306
+ if (removeButton instanceof HTMLElement) {
2307
+ const key = removeButton.dataset.historyRemove || "";
2308
+ const confirm = historyList.querySelector('[data-history-confirm="' + CSS.escape(key) + '"]');
2309
+ const shouldOpen = !(confirm instanceof HTMLElement) || confirm.hidden;
2310
+ closeInpaintHistoryConfirms();
2311
+ if (confirm instanceof HTMLElement) {
2312
+ confirm.hidden = !shouldOpen;
2175
2313
  }
2314
+ removeButton.setAttribute("aria-expanded", shouldOpen ? "true" : "false");
2176
2315
  return;
2177
2316
  }
2178
- const copyButton = target.closest("[data-editor-history-copy]");
2179
- if (copyButton instanceof HTMLButtonElement) {
2180
- event.preventDefault();
2181
- event.stopPropagation();
2182
- const tracer = copyButton.getAttribute("data-editor-history-copy") || "";
2183
- document.querySelectorAll("[data-editor-history-menu-wrap]").forEach((entry) => entry.removeAttribute("data-open"));
2184
- if (!tracer) {
2185
- return;
2186
- }
2187
- void copyText(tracer).then((copied) => {
2188
- if (!copied) {
2189
- window.prompt("Copy tracer", tracer);
2190
- }
2191
- }).catch((error) => {
2192
- console.error(error);
2193
- window.prompt("Copy tracer", tracer);
2194
- });
2317
+ const cancelButton = target.closest("[data-history-cancel]");
2318
+ if (cancelButton instanceof HTMLElement) {
2319
+ const confirm = cancelButton.closest("[data-history-confirm]");
2320
+ if (confirm instanceof HTMLElement) confirm.hidden = true;
2195
2321
  return;
2196
2322
  }
2197
- const renameButton = target.closest("[data-editor-history-rename]");
2198
- if (renameButton instanceof HTMLButtonElement) {
2199
- event.preventDefault();
2200
- event.stopPropagation();
2201
- const tracer = renameButton.getAttribute("data-editor-history-rename") || "";
2202
- document.querySelectorAll("[data-editor-history-menu-wrap]").forEach((entry) => entry.removeAttribute("data-open"));
2203
- const nextTracer = window.prompt("Rename tracer", tracer);
2204
- if (nextTracer === null || !nextTracer.trim() || nextTracer.trim() === tracer) {
2205
- return;
2323
+ const confirmRemove = target.closest("[data-history-confirm-remove]");
2324
+ if (confirmRemove instanceof HTMLElement) {
2325
+ const key = confirmRemove.dataset.historyConfirmRemove || "";
2326
+ if (key) {
2327
+ hiddenInpaintHistoryKeys.add(key);
2328
+ renderInpaintHistory();
2206
2329
  }
2207
- void renameStoredTracer(tracer, nextTracer.trim()).catch((error) => {
2208
- console.error(error);
2209
- window.alert("Unable to rename tracer.");
2210
- });
2211
2330
  return;
2212
2331
  }
2213
- const archiveButton = target.closest("[data-editor-history-archive]");
2214
- if (archiveButton instanceof HTMLButtonElement) {
2215
- event.preventDefault();
2216
- event.stopPropagation();
2217
- const tracer = archiveButton.getAttribute("data-editor-history-archive") || "";
2218
- document.querySelectorAll("[data-editor-history-menu-wrap]").forEach((entry) => entry.removeAttribute("data-open"));
2219
- if (!tracer || tracer.startsWith("archived_")) {
2220
- return;
2221
- }
2222
- const confirmed = window.confirm('Archive "' + tracer + '"? This renames its POST logs and jobs to "archived_' + tracer + '".');
2223
- if (confirmed) {
2224
- void renameStoredTracer(tracer, "archived_" + tracer).catch((error) => {
2225
- console.error(error);
2226
- window.alert("Unable to archive tracer.");
2227
- });
2228
- }
2332
+ });
2333
+ resultOpenButton.addEventListener("click", () => {
2334
+ if (!resultImage.currentSrc && !resultImage.src) return;
2335
+ openImagePreview(resultImage.currentSrc || resultImage.src, "Image edit result", imageDownloadName("image-edit-result"));
2336
+ });
2337
+ lightboxCopy.addEventListener("click", () => {
2338
+ const url = lightboxCopy.dataset.copyUrl || lightboxImage.currentSrc || lightboxImage.src || "";
2339
+ if (!url) return;
2340
+ void copyText(url).then((copied) => {
2341
+ lightboxCopy.setAttribute("title", copied ? "Copied" : "Copy failed");
2342
+ window.setTimeout(() => lightboxCopy.setAttribute("title", "Copy URL"), 1200);
2343
+ });
2344
+ });
2345
+ lightboxClose.addEventListener("click", () => closeImagePreview());
2346
+ lightbox.addEventListener("click", (event) => {
2347
+ if (event.target === lightbox) closeImagePreview();
2348
+ });
2349
+ startNewButton.addEventListener("click", () => {
2350
+ tracerInput.value = makeTracerId();
2351
+ clearInpaintUrlParams();
2352
+ resetWorkspace();
2353
+ void loadInpaintHistory();
2354
+ });
2355
+ tracerInput.addEventListener("input", () => {
2356
+ queueInpaintHistoryReload();
2357
+ });
2358
+ fileInput.addEventListener("change", () => {
2359
+ void loadSourceFile(fileInput.files && fileInput.files[0]);
2360
+ fileInput.value = "";
2361
+ });
2362
+ inpaintAttachmentInput.addEventListener("change", () => {
2363
+ void addAttachmentFiles(inpaintAttachmentInput.files);
2364
+ inpaintAttachmentInput.value = "";
2365
+ });
2366
+ inpaintGlobalAttachments.addEventListener("click", (event) => {
2367
+ const target = event.target;
2368
+ if (!(target instanceof Element)) return;
2369
+ const removeColor = target.closest("[data-editor-inpaint-remove-color]");
2370
+ if (removeColor instanceof HTMLElement && removeColor.dataset.editorInpaintRemoveColor) {
2371
+ removeInpaintColor(removeColor.dataset.editorInpaintRemoveColor);
2229
2372
  return;
2230
2373
  }
2231
- const removeButton = target.closest("[data-editor-history-remove]");
2232
- if (removeButton instanceof HTMLButtonElement) {
2233
- event.preventDefault();
2234
- event.stopPropagation();
2235
- const tracer = removeButton.getAttribute("data-editor-history-remove") || "";
2236
- document.querySelectorAll("[data-editor-history-menu-wrap]").forEach((entry) => entry.removeAttribute("data-open"));
2237
- if (!tracer) {
2238
- return;
2239
- }
2240
- const confirmed = window.confirm('Remove "' + tracer + '" from this page view? This does not delete its runs. You can still find them in Job Runs.');
2241
- if (confirmed) {
2242
- tracerStore.remove(tracer);
2243
- }
2374
+ const remove = target.closest("[data-editor-inpaint-remove-attachment]");
2375
+ if (remove instanceof HTMLElement) {
2376
+ inpaintAttachments = inpaintAttachments.filter((attachment) => attachment.id !== remove.dataset.editorInpaintRemoveAttachment);
2377
+ renderAllAttachmentChips();
2244
2378
  }
2245
2379
  });
2246
- document.addEventListener("click", (event) => {
2380
+ document.querySelector('[data-editor-inpaint-attach-scope="global"]')?.addEventListener("click", () => {
2381
+ openAttachmentPicker("global");
2382
+ });
2383
+ inpaintGlobalPrompt.addEventListener("paste", (event) => {
2384
+ handleAttachmentPaste(event, "global");
2385
+ });
2386
+ inpaintUndo.addEventListener("click", () => {
2387
+ const stroke = inpaintStrokes.pop();
2388
+ if (stroke) redoStrokes.push(stroke);
2389
+ redrawInpaintMask();
2390
+ setStatus("", "");
2391
+ });
2392
+ inpaintRedo.addEventListener("click", () => {
2393
+ const stroke = redoStrokes.pop();
2394
+ if (stroke) inpaintStrokes.push(stroke);
2395
+ redrawInpaintMask();
2396
+ setStatus("", "");
2397
+ });
2398
+ inpaintClear.addEventListener("click", () => {
2399
+ resetMask();
2400
+ setStatus("", "");
2401
+ });
2402
+ inpaintBrush.addEventListener("input", () => {
2403
+ inpaintBrushLabel.textContent = inpaintBrush.value;
2404
+ updateBrushCursorSize();
2405
+ });
2406
+ inpaintColorPicker.addEventListener("input", () => {
2407
+ activeInpaintColor = normalizeHexColor(inpaintColorPicker.value);
2408
+ renderInpaintControls();
2409
+ });
2410
+ inpaintAddColor.addEventListener("click", () => addInpaintColor(inpaintColorPicker.value));
2411
+ inpaintPrompts.addEventListener("click", (event) => {
2247
2412
  const target = event.target;
2248
- if (!(target instanceof Element)) {
2413
+ if (!(target instanceof Element)) return;
2414
+ const remove = target.closest("[data-editor-inpaint-remove-attachment]");
2415
+ if (remove instanceof HTMLElement) {
2416
+ inpaintAttachments = inpaintAttachments.filter((attachment) => attachment.id !== remove.dataset.editorInpaintRemoveAttachment);
2417
+ renderAllAttachmentChips();
2249
2418
  return;
2250
2419
  }
2251
- const copyButton = target.closest("[data-editor-copy-content]");
2252
- if (!(copyButton instanceof HTMLButtonElement)) {
2420
+ const attach = target.closest("[data-editor-inpaint-attach-scope]");
2421
+ if (attach instanceof HTMLElement && attach.dataset.editorInpaintAttachScope === "color" && attach.dataset.color) {
2422
+ openAttachmentPicker("color", attach.dataset.color);
2253
2423
  return;
2254
2424
  }
2255
- event.preventDefault();
2256
- const value = copyButton.getAttribute("data-editor-copy-content") || "";
2257
- const label = copyButton.getAttribute("data-editor-copy-label") || "Copied text";
2258
- if (!value) {
2259
- return;
2425
+ const row = target.closest(".editor-inpaint-color-row");
2426
+ if (row instanceof HTMLElement && row.dataset.color) selectInpaintColor(row.dataset.color);
2427
+ });
2428
+ inpaintPrompts.addEventListener("focusin", (event) => {
2429
+ const target = event.target;
2430
+ if (!(target instanceof Element)) return;
2431
+ const row = target.closest(".editor-inpaint-color-row");
2432
+ if (row instanceof HTMLElement && row.dataset.color) selectInpaintColor(row.dataset.color);
2433
+ });
2434
+ inpaintPrompts.addEventListener("input", (event) => {
2435
+ const target = event.target;
2436
+ if (target instanceof HTMLTextAreaElement) updateInpaintPrompt(target.dataset.color || "", target.value);
2437
+ });
2438
+ inpaintPrompts.addEventListener("paste", (event) => {
2439
+ const target = event.target;
2440
+ if (target instanceof HTMLTextAreaElement && target.dataset.color) {
2441
+ handleAttachmentPaste(event, "color", target.dataset.color);
2442
+ }
2443
+ });
2444
+ inpaintCanvas.addEventListener("pointerenter", (event) => updateBrushCursorPosition(event));
2445
+ inpaintCanvas.addEventListener("pointerdown", (event) => {
2446
+ updateBrushCursorPosition(event);
2447
+ beginInpaintStroke(event);
2448
+ });
2449
+ inpaintCanvas.addEventListener("pointermove", (event) => {
2450
+ updateBrushCursorPosition(event);
2451
+ continueInpaintStroke(event);
2452
+ });
2453
+ inpaintCanvas.addEventListener("pointerup", (event) => endInpaintStroke(event));
2454
+ inpaintCanvas.addEventListener("pointercancel", (event) => {
2455
+ endInpaintStroke(event);
2456
+ hideBrushCursor();
2457
+ });
2458
+ inpaintCanvas.addEventListener("pointerleave", (event) => {
2459
+ endInpaintStroke(event);
2460
+ hideBrushCursor();
2461
+ });
2462
+ inpaintSubmit.addEventListener("click", () => {
2463
+ void submitInpaintJob();
2464
+ });
2465
+ settingsButton.addEventListener("click", () => {
2466
+ settingsPopover.hidden = !settingsPopover.hidden;
2467
+ if (!settingsPopover.hidden) tracerInput.focus();
2468
+ });
2469
+ window.addEventListener("click", (event) => {
2470
+ const target = event.target;
2471
+ if (!(target instanceof Node)) return;
2472
+ const isHistoryConfirmTarget = target instanceof Element && Boolean(target.closest("[data-history-confirm]"));
2473
+ if (!historyList.contains(target) && !isHistoryConfirmTarget) {
2474
+ closeInpaintHistoryConfirms();
2475
+ }
2476
+ if (!settingsPopover.hidden && !settingsPopover.contains(target) && !settingsButton.contains(target)) {
2477
+ settingsPopover.hidden = true;
2478
+ }
2479
+ });
2480
+ window.addEventListener("resize", () => syncInpaintCanvasSize());
2481
+ window.addEventListener("keydown", (event) => {
2482
+ if (event.key === "Escape" && lightbox.getAttribute("data-open") === "true") {
2483
+ closeImagePreview();
2260
2484
  }
2261
- showCopyingFeedback(copyButton, label);
2262
- void copyText(value).then((copied) => {
2263
- if (!copied) {
2264
- showCopyErrorFeedback(copyButton, label);
2265
- window.prompt(label, value);
2266
- return;
2267
- }
2268
- showCopyFeedback(copyButton, label);
2269
- }).catch((error) => {
2270
- console.error(error);
2271
- showCopyErrorFeedback(copyButton, label);
2272
- window.prompt(label, value);
2273
- });
2274
2485
  });
2275
2486
  })();
2276
2487
  </script>
2277
2488
  `
2278
2489
  });
2279
2490
  }
2280
- function renderPreview(url, alt) {
2281
- if (/\.(mp4|webm|ogg|mov)(\?|#|$)/i.test(url)) {
2282
- return `<video class="editor-preview" src="${escapeAttribute(url)}" controls playsinline preload="metadata" aria-label="${escapeAttribute(alt)}"></video>`;
2283
- }
2284
- return `<img class="editor-preview" src="${escapeAttribute(url)}" alt="${escapeAttribute(alt)}" loading="lazy" decoding="async" />`;
2285
- }
2286
- function renderQuickstartTextarea(label, value, rows) {
2287
- const text = value.trim() || "Not configured for this template yet.";
2288
- return `
2289
- <div class="editor-quickstart-field">
2290
- <div class="editor-quickstart-field-head">
2291
- <span>${escapeHtml(label)}</span>
2292
- <button
2293
- type="button"
2294
- class="editor-quickstart-copy"
2295
- data-editor-copy-content="${escapeAttribute(text)}"
2296
- data-editor-copy-label="${escapeAttribute(label)}"
2297
- aria-label="Copy ${escapeAttribute(label)}"
2298
- title="Copy ${escapeAttribute(label)}"
2299
- >
2300
- <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
2301
- <rect x="9" y="9" width="10" height="10" rx="2" fill="none" stroke="currentColor" stroke-width="2"></rect>
2302
- <path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
2303
- </svg>
2304
- </button>
2305
- </div>
2306
- <textarea class="editor-quickstart-textarea" readonly rows="${rows}" spellcheck="false" aria-label="${escapeAttribute(label)}">${escapeHtml(text)}</textarea>
2307
- </div>
2308
- `;
2309
- }
2491
+ //# sourceMappingURL=template-editor-pages.js.map