@dickpy/dsh-imagegen 1.0.9 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/docs/images/prompt-template-library.png +0 -0
- package/lib/client.js +1151 -464
- package/lib/client.js.map +1 -1
- package/lib/index.js +441 -36
- package/package.json +3 -3
- package/src/client/ImageGenPanel.tsx +452 -62
- package/src/client/api.ts +38 -1
- package/src/client/locales.ts +71 -22
- package/src/client/mount.tsx +11 -6
- package/src/client/panel.module.css +209 -7
- package/src/engine.ts +78 -11
- package/src/gallery-store.ts +266 -0
- package/src/index.ts +2 -1
- package/src/protocol.ts +21 -5
- package/src/routes.ts +119 -1
package/src/client/api.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* data access path the panel uses — plain fetch, same origin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { GENERATE_API, HISTORY_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type HistoryEntry, type TemplateListResult, type TemplateRefreshResult, type UpdateInfo } from '../protocol.ts'
|
|
6
|
+
import { GALLERY_API, GENERATE_API, HISTORY_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult, type UpdateInfo } from '../protocol.ts'
|
|
7
7
|
|
|
8
8
|
/** Error carrying the route's JSON error message. */
|
|
9
9
|
export class ImageGenApiError extends Error {
|
|
@@ -98,6 +98,43 @@ export class ImageGenApi {
|
|
|
98
98
|
return body.entries
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** List the host-persisted gallery (newest first). */
|
|
102
|
+
async galleryList(): Promise<HistoryEntry[]> {
|
|
103
|
+
const response = await fetch(GALLERY_API.list, { method: 'POST' })
|
|
104
|
+
const body = await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)
|
|
105
|
+
return body.entries
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Append one image to the gallery. The host assigns the id and skips the
|
|
109
|
+
* append when a content-identical image is already in the gallery. */
|
|
110
|
+
async galleryAppend(entry: HistoryEntryInput): Promise<{ entries: HistoryEntry[]; added: boolean }> {
|
|
111
|
+
const response = await fetch(GALLERY_API.append, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'content-type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({ entry }),
|
|
115
|
+
})
|
|
116
|
+
const body = await readEnvelope<{ ok: true; entries: HistoryEntry[]; added: boolean }>(response)
|
|
117
|
+
return { entries: body.entries, added: body.added }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Remove one gallery entry by id. */
|
|
121
|
+
async galleryRemove(id: string): Promise<HistoryEntry[]> {
|
|
122
|
+
const response = await fetch(GALLERY_API.remove, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'content-type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({ id }),
|
|
126
|
+
})
|
|
127
|
+
const body = await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)
|
|
128
|
+
return body.entries
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Clear the entire gallery. */
|
|
132
|
+
async galleryClear(): Promise<HistoryEntry[]> {
|
|
133
|
+
const response = await fetch(GALLERY_API.clear, { method: 'POST' })
|
|
134
|
+
const body = await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)
|
|
135
|
+
return body.entries
|
|
136
|
+
}
|
|
137
|
+
|
|
101
138
|
/** Fetch the prompt-template library (bundled snapshot or refreshed copy). */
|
|
102
139
|
async templatesList(): Promise<TemplateListResult> {
|
|
103
140
|
const response = await fetch(TEMPLATES_API.list, { method: 'POST' })
|
package/src/client/locales.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
export const zh = {
|
|
6
6
|
'entry.label': 'AI 生图',
|
|
7
|
-
'entry.tooltip': 'AI 生图面板(gpt-image-2)',
|
|
7
|
+
'entry.tooltip': 'AI 生图面板(gpt-image-2 / grok-imagine-image)',
|
|
8
8
|
'panel.title': 'AI 生图',
|
|
9
|
-
'panel.
|
|
9
|
+
'panel.githubTip': '觉得好用或有建议?欢迎来 GitHub 提 issues、点个 star 支持一下!',
|
|
10
10
|
// mode
|
|
11
11
|
'mode.text': '文生图',
|
|
12
12
|
'mode.edit': '图生图',
|
|
@@ -20,16 +20,18 @@ export const zh = {
|
|
|
20
20
|
'params.count': '生成数量',
|
|
21
21
|
'params.detail': '细节',
|
|
22
22
|
'size.auto': '自动',
|
|
23
|
-
'size.square': '
|
|
24
|
-
'size.
|
|
25
|
-
'size.
|
|
26
|
-
'size.
|
|
27
|
-
'size.
|
|
28
|
-
'size.
|
|
23
|
+
'size.square': '1:1 方图',
|
|
24
|
+
'size.portrait34': '3:4 标准竖图',
|
|
25
|
+
'size.landscape43': '4:3 标准横图',
|
|
26
|
+
'size.portrait916': '9:16 竖屏',
|
|
27
|
+
'size.portrait23': '2:3 竖图',
|
|
28
|
+
'size.landscape32': '3:2 横图',
|
|
29
|
+
'size.wide169': '16:9 宽屏',
|
|
30
|
+
'size.ultrawide21': '21:9 超宽屏',
|
|
29
31
|
'quality.auto': '自动',
|
|
30
|
-
'quality.
|
|
31
|
-
'quality.
|
|
32
|
-
'quality.
|
|
32
|
+
'quality.1k': '1K',
|
|
33
|
+
'quality.2k': '2K',
|
|
34
|
+
'quality.4k': '4K',
|
|
33
35
|
'count.one': '1 张',
|
|
34
36
|
'count.two': '2 张',
|
|
35
37
|
'count.three': '3 张',
|
|
@@ -65,6 +67,29 @@ export const zh = {
|
|
|
65
67
|
'history.delete': '删除',
|
|
66
68
|
'history.images': '张',
|
|
67
69
|
'history.viewing': '历史 · {time}',
|
|
70
|
+
// gallery
|
|
71
|
+
'gallery.title': '画廊',
|
|
72
|
+
'gallery.categories': '分类',
|
|
73
|
+
'gallery.all': '全部作品',
|
|
74
|
+
'gallery.gpt': 'gpt-image-2',
|
|
75
|
+
'gallery.grok': 'grok-imagine-image',
|
|
76
|
+
'gallery.ratio': '画面比例',
|
|
77
|
+
'gallery.filterHint': '按生成模式、模型和比例筛选画廊',
|
|
78
|
+
'gallery.count': '· 共 {count} 幅',
|
|
79
|
+
'gallery.viewMode': '视图模式',
|
|
80
|
+
'gallery.masonry': '瀑布流',
|
|
81
|
+
'gallery.grid': '整齐网格',
|
|
82
|
+
'gallery.sort': '排序',
|
|
83
|
+
'gallery.newest': '最新发布',
|
|
84
|
+
'gallery.oldest': '最早发布',
|
|
85
|
+
'gallery.untitled': '未命名作品',
|
|
86
|
+
'gallery.add': '加入画廊',
|
|
87
|
+
'gallery.added': '已加入画廊',
|
|
88
|
+
'gallery.already': '已在画廊中',
|
|
89
|
+
'gallery.delete': '移出画廊',
|
|
90
|
+
'gallery.clear': '清空画廊',
|
|
91
|
+
'gallery.empty': '画廊还是空的,把喜欢的图片加入进来吧',
|
|
92
|
+
'gallery.viewing': '画廊 · {time}',
|
|
68
93
|
// preview
|
|
69
94
|
'preview.title': '图片预览',
|
|
70
95
|
'preview.open': '点击预览',
|
|
@@ -154,9 +179,9 @@ export const zh = {
|
|
|
154
179
|
|
|
155
180
|
export const en: Record<keyof typeof zh, string> = {
|
|
156
181
|
'entry.label': 'AI Image',
|
|
157
|
-
'entry.tooltip': 'AI image generation studio (gpt-image-2)',
|
|
182
|
+
'entry.tooltip': 'AI image generation studio (gpt-image-2 / grok-imagine-image)',
|
|
158
183
|
'panel.title': 'AI Image',
|
|
159
|
-
'panel.
|
|
184
|
+
'panel.githubTip': 'Like it or have suggestions? Head to GitHub to open issues and star us!',
|
|
160
185
|
'mode.text': 'Text to Image',
|
|
161
186
|
'mode.edit': 'Image to Image',
|
|
162
187
|
'prompt.placeholder': 'Describe the picture you want, e.g. an orange cat in an astronaut helmet raising a telescope on the moon, watercolor style, soft light…',
|
|
@@ -167,16 +192,18 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
167
192
|
'params.count': 'Count',
|
|
168
193
|
'params.detail': 'Detail',
|
|
169
194
|
'size.auto': 'Auto',
|
|
170
|
-
'size.square': '
|
|
171
|
-
'size.
|
|
172
|
-
'size.
|
|
173
|
-
'size.
|
|
174
|
-
'size.
|
|
175
|
-
'size.
|
|
195
|
+
'size.square': '1:1 Square',
|
|
196
|
+
'size.portrait34': '3:4 Portrait',
|
|
197
|
+
'size.landscape43': '4:3 Landscape',
|
|
198
|
+
'size.portrait916': '9:16 Vertical',
|
|
199
|
+
'size.portrait23': '2:3 Portrait',
|
|
200
|
+
'size.landscape32': '3:2 Landscape',
|
|
201
|
+
'size.wide169': '16:9 Widescreen',
|
|
202
|
+
'size.ultrawide21': '21:9 Ultrawide',
|
|
176
203
|
'quality.auto': 'Auto',
|
|
177
|
-
'quality.
|
|
178
|
-
'quality.
|
|
179
|
-
'quality.
|
|
204
|
+
'quality.1k': '1K',
|
|
205
|
+
'quality.2k': '2K',
|
|
206
|
+
'quality.4k': '4K',
|
|
180
207
|
'count.one': '1',
|
|
181
208
|
'count.two': '2',
|
|
182
209
|
'count.three': '3',
|
|
@@ -208,6 +235,28 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
208
235
|
'history.delete': 'Delete',
|
|
209
236
|
'history.images': 'images',
|
|
210
237
|
'history.viewing': 'History · {time}',
|
|
238
|
+
'gallery.title': 'Gallery',
|
|
239
|
+
'gallery.categories': 'Categories',
|
|
240
|
+
'gallery.all': 'All works',
|
|
241
|
+
'gallery.gpt': 'gpt-image-2',
|
|
242
|
+
'gallery.grok': 'grok-imagine-image',
|
|
243
|
+
'gallery.ratio': 'Aspect ratio',
|
|
244
|
+
'gallery.filterHint': 'Filter by mode, model, and aspect ratio',
|
|
245
|
+
'gallery.count': '· {count} works',
|
|
246
|
+
'gallery.viewMode': 'View mode',
|
|
247
|
+
'gallery.masonry': 'Masonry',
|
|
248
|
+
'gallery.grid': 'Grid',
|
|
249
|
+
'gallery.sort': 'Sort',
|
|
250
|
+
'gallery.newest': 'Newest',
|
|
251
|
+
'gallery.oldest': 'Oldest',
|
|
252
|
+
'gallery.untitled': 'Untitled work',
|
|
253
|
+
'gallery.add': 'Add to gallery',
|
|
254
|
+
'gallery.added': 'Added to gallery',
|
|
255
|
+
'gallery.already': 'Already in gallery',
|
|
256
|
+
'gallery.delete': 'Remove',
|
|
257
|
+
'gallery.clear': 'Clear gallery',
|
|
258
|
+
'gallery.empty': 'The gallery is empty — add images you like here',
|
|
259
|
+
'gallery.viewing': 'Gallery · {time}',
|
|
211
260
|
'preview.title': 'Image preview',
|
|
212
261
|
'preview.open': 'Click to preview',
|
|
213
262
|
'preview.close': 'Close',
|
package/src/client/mount.tsx
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The `conversation` slot is single-occupant (ui-conversation) and external
|
|
5
5
|
* plugins cannot declare slots, so the panel takes over the center column at
|
|
6
|
-
* the DOM level: a container is appended inside the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* the DOM level: a container is appended inside the conversation grid item
|
|
7
|
+
* (an extra trailing child React never manages), and a stylesheet rule hides
|
|
8
|
+
* the conversation content while the panel is active. Toggling is a data
|
|
9
|
+
* attribute on <html> — no React involvement, so the conversation subtree
|
|
10
|
+
* underneath stays mounted and stateful.
|
|
11
|
+
*
|
|
12
|
+
* Shell compatibility: the center column is `[data-pane="conversation"]` on
|
|
13
|
+
* legacy shells and `[class*="centerCol"]` on the rc.6+ AppFrame layout (the
|
|
14
|
+
* same dual selector the dsh-ssh / task-board panels use); both are queried
|
|
15
|
+
* and both get the `position: relative` base in panel.module.css.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { createRoot, type Root } from 'react-dom/client'
|
|
@@ -20,7 +25,7 @@ import css from './panel.module.css'
|
|
|
20
25
|
/** The injected panel container (kept in the DOM, hidden when inactive). */
|
|
21
26
|
export const PANEL_VIEW_SELECTOR = '[data-dsh-imagegen-view]'
|
|
22
27
|
|
|
23
|
-
const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"]'
|
|
28
|
+
const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"], [class*="centerCol"]'
|
|
24
29
|
const ACTIVE_ATTR = 'data-dsh-imagegen-active'
|
|
25
30
|
/** Sibling panels' activation attributes, removed when this panel opens. */
|
|
26
31
|
const OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
/* --- center-column takeover (global rules, attribute-scoped) ----------------- */
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/* The panel container rides inside the conversation grid item as an extra
|
|
14
|
+
trailing child; hidden unless the panel is active. The position rule covers
|
|
15
|
+
both shells: the legacy `[data-pane="conversation"]` and the rc.6+
|
|
16
|
+
AppFrame class-based column ([class*="centerCol"]) — without a positioned
|
|
17
|
+
ancestor the absolutely-positioned view resolves against the frame. */
|
|
18
|
+
[data-pane='conversation'],
|
|
19
|
+
[class*='centerCol'] {
|
|
14
20
|
position: relative;
|
|
15
21
|
}
|
|
16
22
|
|
|
@@ -107,6 +113,7 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
.panel {
|
|
116
|
+
position: relative;
|
|
110
117
|
display: flex;
|
|
111
118
|
flex-direction: column;
|
|
112
119
|
height: 100%;
|
|
@@ -143,12 +150,23 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
143
150
|
white-space: nowrap;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
/* GitHub icon button next to the title (opens the repo). */
|
|
154
|
+
.githubLink {
|
|
155
|
+
display: inline-flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
justify-content: center;
|
|
158
|
+
flex: none;
|
|
159
|
+
width: 22px;
|
|
160
|
+
height: 22px;
|
|
161
|
+
border-radius: 6px;
|
|
162
|
+
color: var(--dsw-alias-label-secondary);
|
|
163
|
+
text-decoration: none;
|
|
164
|
+
transition: color 0.12s, background 0.12s;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.githubLink:hover {
|
|
168
|
+
color: var(--dsw-alias-label-primary);
|
|
169
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
152
170
|
}
|
|
153
171
|
|
|
154
172
|
/* --- connection status and update notice -------------------------------------- */
|
|
@@ -715,6 +733,10 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
715
733
|
.modelSelect {
|
|
716
734
|
width: 100%;
|
|
717
735
|
height: 36px;
|
|
736
|
+
display: flex;
|
|
737
|
+
align-items: center;
|
|
738
|
+
justify-content: space-between;
|
|
739
|
+
gap: 8px;
|
|
718
740
|
padding: 0 12px;
|
|
719
741
|
font-size: 13px;
|
|
720
742
|
color: var(--dsw-alias-label-primary);
|
|
@@ -724,6 +746,7 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
724
746
|
outline: none;
|
|
725
747
|
font-family: inherit;
|
|
726
748
|
cursor: pointer;
|
|
749
|
+
text-align: left;
|
|
727
750
|
}
|
|
728
751
|
|
|
729
752
|
.modelSelect:focus-visible {
|
|
@@ -732,6 +755,59 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
732
755
|
|
|
733
756
|
.modelSelect:disabled {
|
|
734
757
|
opacity: 0.55;
|
|
758
|
+
cursor: default;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/* Model dropdown trigger + upward-opening menu. */
|
|
762
|
+
.modelMenu {
|
|
763
|
+
position: relative;
|
|
764
|
+
display: block;
|
|
765
|
+
min-width: 0;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/* The list opens UPWARD (absolute above the trigger) so it never hides
|
|
769
|
+
behind the footer edge below. */
|
|
770
|
+
.modelMenuList {
|
|
771
|
+
position: absolute;
|
|
772
|
+
left: 0;
|
|
773
|
+
right: 0;
|
|
774
|
+
bottom: calc(100% + 6px);
|
|
775
|
+
z-index: 40;
|
|
776
|
+
display: flex;
|
|
777
|
+
flex-direction: column;
|
|
778
|
+
padding: 4px;
|
|
779
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
780
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
781
|
+
border-radius: 12px;
|
|
782
|
+
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.18);
|
|
783
|
+
overflow: hidden;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.modelMenuItem {
|
|
787
|
+
display: block;
|
|
788
|
+
width: 100%;
|
|
789
|
+
padding: 7px 10px;
|
|
790
|
+
font: inherit;
|
|
791
|
+
font-size: 13px;
|
|
792
|
+
color: var(--dsw-alias-label-primary);
|
|
793
|
+
background: transparent;
|
|
794
|
+
border: none;
|
|
795
|
+
border-radius: 8px;
|
|
796
|
+
cursor: pointer;
|
|
797
|
+
text-align: left;
|
|
798
|
+
white-space: nowrap;
|
|
799
|
+
overflow: hidden;
|
|
800
|
+
text-overflow: ellipsis;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.modelMenuItem:hover {
|
|
804
|
+
background: var(--dsw-alias-bg-hover);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.modelMenuItem[data-selected] {
|
|
808
|
+
color: var(--dsw-alias-brand-primary);
|
|
809
|
+
font-weight: 600;
|
|
810
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
735
811
|
}
|
|
736
812
|
|
|
737
813
|
.generateButton {
|
|
@@ -908,6 +984,42 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
908
984
|
background: var(--dsw-alias-bg-base);
|
|
909
985
|
}
|
|
910
986
|
|
|
987
|
+
/* Add-to-gallery pill (top-left, mirrors .download; shows on card hover). */
|
|
988
|
+
.galleryAdd {
|
|
989
|
+
position: absolute;
|
|
990
|
+
top: 8px;
|
|
991
|
+
left: 8px;
|
|
992
|
+
display: inline-flex;
|
|
993
|
+
align-items: center;
|
|
994
|
+
gap: 5px;
|
|
995
|
+
padding: 2px 10px;
|
|
996
|
+
font: inherit;
|
|
997
|
+
font-size: 12px;
|
|
998
|
+
line-height: 20px;
|
|
999
|
+
font-weight: 500;
|
|
1000
|
+
color: var(--dsw-alias-label-primary);
|
|
1001
|
+
background: var(--dsw-alias-bg-mask-1);
|
|
1002
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
1003
|
+
border-radius: 999px;
|
|
1004
|
+
cursor: pointer;
|
|
1005
|
+
opacity: 0;
|
|
1006
|
+
transition: opacity 0.12s;
|
|
1007
|
+
backdrop-filter: blur(4px);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.imageCard:hover .galleryAdd {
|
|
1011
|
+
opacity: 1;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.galleryAdd:hover {
|
|
1015
|
+
background: var(--dsw-alias-bg-base);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.galleryAdd:disabled {
|
|
1019
|
+
opacity: 0.4;
|
|
1020
|
+
cursor: default;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
911
1023
|
/* Hover hint for the click-to-zoom behavior. */
|
|
912
1024
|
.zoomHint {
|
|
913
1025
|
position: absolute;
|
|
@@ -1211,9 +1323,99 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
1211
1323
|
to { transform: rotate(360deg); }
|
|
1212
1324
|
}
|
|
1213
1325
|
|
|
1326
|
+
/* Transient "added to gallery" toast, centered above the canvas bottom. */
|
|
1327
|
+
.galleryToast {
|
|
1328
|
+
position: absolute;
|
|
1329
|
+
left: 50%;
|
|
1330
|
+
bottom: 24px;
|
|
1331
|
+
transform: translateX(-50%);
|
|
1332
|
+
z-index: 30;
|
|
1333
|
+
display: inline-flex;
|
|
1334
|
+
align-items: center;
|
|
1335
|
+
gap: 7px;
|
|
1336
|
+
padding: 6px 16px;
|
|
1337
|
+
font-size: 13px;
|
|
1338
|
+
font-weight: 500;
|
|
1339
|
+
color: var(--dsw-alias-label-primary);
|
|
1340
|
+
background: var(--dsw-alias-bg-mask-1);
|
|
1341
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
1342
|
+
border-radius: 999px;
|
|
1343
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
|
|
1344
|
+
backdrop-filter: blur(6px);
|
|
1345
|
+
pointer-events: none;
|
|
1346
|
+
animation: dshImageGenToastIn 160ms ease-out;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
@keyframes dshImageGenToastIn {
|
|
1350
|
+
from { opacity: 0; transform: translate(-50%, 6px); }
|
|
1351
|
+
to { opacity: 1; transform: translate(-50%, 0); }
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1214
1354
|
@media (prefers-reduced-motion: reduce) {
|
|
1215
1355
|
.download, .spinner, .bigSpinner {
|
|
1216
1356
|
transition: none;
|
|
1217
1357
|
animation-duration: 1.5s;
|
|
1218
1358
|
}
|
|
1219
1359
|
}
|
|
1360
|
+
|
|
1361
|
+
/* --- gallery workspace -------------------------------------------------------- */
|
|
1362
|
+
.config[data-gallery='true'] .configScroll > *:not(:first-child),
|
|
1363
|
+
.config[data-gallery='true'] .footer,
|
|
1364
|
+
.canvas[data-gallery='true'] > .canvasState,
|
|
1365
|
+
.canvas[data-gallery='true'] > .canvasError,
|
|
1366
|
+
.canvas[data-gallery='true'] > .canvasBody,
|
|
1367
|
+
.studio:has(.config[data-gallery='true']) > .history { display: none; }
|
|
1368
|
+
.config[data-gallery='true'] .configScroll { display: flex; flex: none; overflow: visible; order: 1; }
|
|
1369
|
+
.config[data-gallery='true'] .galleryFilters { order: 2; flex: 1; min-height: 0; }
|
|
1370
|
+
.galleryFilters { padding: 18px 14px; overflow-y: auto; overflow-x: hidden; }
|
|
1371
|
+
.galleryFilterHeading { margin: 0 4px 10px; color: var(--dsw-alias-label-tertiary); font-size: 12px; font-weight: 600; }
|
|
1372
|
+
.galleryFilter { display: flex; align-items: center; justify-content: space-between; width: 100%; min-height: 34px; padding: 0 10px; border: 0; border-radius: 9px; color: var(--dsw-alias-label-secondary); background: transparent; cursor: pointer; text-align: left; }
|
|
1373
|
+
.galleryFilter:hover, .galleryFilter[data-active] { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
|
|
1374
|
+
.galleryFilterCount { min-width: 20px; padding: 1px 6px; border-radius: 999px; color: var(--dsw-alias-label-tertiary); background: var(--dsw-alias-bg-layer-2); font-size: 11px; text-align: center; }
|
|
1375
|
+
.galleryFilterDivider { height: 1px; margin: 18px 4px; background: var(--dsw-alias-border-l1); }
|
|
1376
|
+
.galleryRatioList { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1377
|
+
.galleryRatio { padding: 6px 10px; border: 0; border-radius: 999px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-2); cursor: pointer; font-size: 12px; }
|
|
1378
|
+
.galleryRatio[data-active] { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 13%, transparent); }
|
|
1379
|
+
.galleryFilterNote { margin: 20px 4px 0; color: var(--dsw-alias-label-quaternary); font-size: 11px; line-height: 1.5; }
|
|
1380
|
+
.galleryWorkspace { position: absolute; inset: 0; display: flex; width: 100%; height: 100%; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; padding: 22px 24px 26px; box-sizing: border-box; }
|
|
1381
|
+
.galleryToolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex: none; min-width: 0; margin-bottom: 18px; }
|
|
1382
|
+
.galleryHeading { display: inline; margin: 0; color: var(--dsw-alias-label-primary); font-size: 20px; font-weight: 700; }
|
|
1383
|
+
.galleryCount { margin-left: 8px; color: var(--dsw-alias-label-tertiary); font-size: 13px; }
|
|
1384
|
+
.galleryToolbarActions { display: flex; align-items: center; gap: 10px; min-width: 0; flex-wrap: wrap; }
|
|
1385
|
+
.galleryViewToggle { display: flex; padding: 3px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 9px; background: var(--dsw-alias-bg-layer-1); }
|
|
1386
|
+
.galleryViewToggle button, .gallerySort, .galleryClear { min-height: 30px; padding: 0 10px; border: 0; border-radius: 7px; color: var(--dsw-alias-label-secondary); background: transparent; cursor: pointer; font: inherit; font-size: 12px; }
|
|
1387
|
+
.galleryViewToggle button[data-active], .galleryViewToggle button:hover, .gallerySort:hover, .galleryClear:hover { color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 11%, transparent); }
|
|
1388
|
+
.gallerySort { border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); }
|
|
1389
|
+
.galleryClear { border: 1px solid var(--dsw-alias-border-l1); }
|
|
1390
|
+
.galleryMasonry { display: grid; flex: 1 1 auto; width: 100%; height: 0; max-width: 100%; min-width: 0; min-height: 0; box-sizing: border-box; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: max-content; align-content: start; gap: 16px; overflow-y: scroll; overflow-x: hidden; overscroll-behavior: contain; padding: 2px 8px 16px 2px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2) transparent; }
|
|
1391
|
+
.galleryMasonry::-webkit-scrollbar { width: 8px; }
|
|
1392
|
+
.galleryMasonry::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l2); border-radius: 999px; }
|
|
1393
|
+
.galleryCard { display: block; width: 100%; margin: 0; overflow: hidden; border: 1px solid var(--dsw-alias-border-l1); border-radius: 14px; background: var(--dsw-alias-bg-layer-1); box-shadow: 0 5px 18px rgb(24 32 54 / 7%); }
|
|
1394
|
+
.galleryImageButton { position: relative; display: block; width: 100%; padding: 0; border: 0; background: var(--dsw-alias-bg-base); cursor: zoom-in; }
|
|
1395
|
+
.galleryImage { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
|
|
1396
|
+
.galleryMasonry[data-view='masonry'] .galleryCard:nth-child(3n + 1) .galleryImage { aspect-ratio: 4 / 5; }
|
|
1397
|
+
.galleryMasonry[data-view='masonry'] .galleryCard:nth-child(3n + 2) .galleryImage { aspect-ratio: 4 / 3; }
|
|
1398
|
+
.galleryMasonry[data-view='masonry'] .galleryCard:nth-child(3n) .galleryImage { aspect-ratio: 3 / 4; }
|
|
1399
|
+
.galleryBadge { position: absolute; top: 10px; left: 10px; padding: 4px 9px; border-radius: 999px; color: #fff; background: rgb(18 23 36 / 76%); font-size: 11px; backdrop-filter: blur(4px); }
|
|
1400
|
+
.galleryCardFooter { display: flex; align-items: center; gap: 9px; min-width: 0; padding: 10px 12px; }
|
|
1401
|
+
.galleryAvatar { display: grid; flex: none; width: 27px; height: 27px; place-items: center; border-radius: 50%; color: #fff; background: var(--dsw-alias-brand-primary); font-size: 12px; font-weight: 700; }
|
|
1402
|
+
.galleryCardInfo { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 2px; }
|
|
1403
|
+
.galleryCardInfo strong, .galleryCardInfo small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1404
|
+
.galleryCardInfo strong { color: var(--dsw-alias-label-primary); font-size: 12px; }
|
|
1405
|
+
.galleryCardInfo small { color: var(--dsw-alias-label-tertiary); font-size: 10px; }
|
|
1406
|
+
.galleryRemove { flex: none; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%; color: var(--dsw-alias-label-tertiary); background: transparent; cursor: pointer; font-size: 18px; }
|
|
1407
|
+
.galleryRemove:hover { color: var(--dsw-alias-state-error); background: var(--dsw-alias-bg-layer-2); }
|
|
1408
|
+
|
|
1409
|
+
@media (max-width: 1100px) {
|
|
1410
|
+
.galleryMasonry { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
@media (max-width: 760px) {
|
|
1414
|
+
.studio { flex-direction: column; overflow: auto; }
|
|
1415
|
+
.config { width: auto; max-width: none; height: auto; min-height: 0; }
|
|
1416
|
+
.config[data-gallery='true'] { flex: none; }
|
|
1417
|
+
.canvas { min-height: 560px; }
|
|
1418
|
+
.galleryToolbar { align-items: flex-start; flex-direction: column; }
|
|
1419
|
+
.galleryToolbarActions { width: 100%; flex-wrap: wrap; }
|
|
1420
|
+
.galleryMasonry { grid-template-columns: 1fr; }
|
|
1421
|
+
}
|
package/src/engine.ts
CHANGED
|
@@ -42,6 +42,33 @@ const MAX_EDIT_IMAGE_BYTES = 10 * 1024 * 1024
|
|
|
42
42
|
/** Sizes dall-e-3 accepts; anything else falls back to its square default. */
|
|
43
43
|
const DALLE3_SIZES = new Set(['1024x1024', '1792x1024', '1024x1792'])
|
|
44
44
|
|
|
45
|
+
/** Whether the model is an xAI Grok Imagine model (grok-imagine-image,
|
|
46
|
+
* grok-imagine-image-2.0, …). Grok Imagine speaks JSON on both endpoints
|
|
47
|
+
* and exposes its own aspect-ratio / response-format knobs instead of the
|
|
48
|
+
* OpenAI size/quality/detail passthrough. */
|
|
49
|
+
function isGrokImagine(model: string): boolean {
|
|
50
|
+
return /^grok-imagine(?:-|$)/.test(model)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The panel's aspect ratios mapped to the closest OpenAI pixel size
|
|
54
|
+
* (gpt-image-2 / generic OpenAI-compatible endpoints). */
|
|
55
|
+
const OPENAI_SIZE_BY_RATIO: Readonly<Record<string, string>> = {
|
|
56
|
+
'1:1': '1024x1024',
|
|
57
|
+
'3:4': '1024x1536',
|
|
58
|
+
'4:3': '1536x1024',
|
|
59
|
+
'9:16': '1024x1792',
|
|
60
|
+
'2:3': '1024x1536',
|
|
61
|
+
'3:2': '1536x1024',
|
|
62
|
+
'16:9': '1792x1024',
|
|
63
|
+
'21:9': '1792x1024',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Panel ratios that need renaming for a model's vocabulary. Grok documents
|
|
67
|
+
* 20:9 as its ultra-wide ratio, so the panel's 21:9 label is sent as 20:9. */
|
|
68
|
+
const GROK_ASPECT_ALIASES: Readonly<Record<string, string>> = {
|
|
69
|
+
'21:9': '20:9',
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
/** Content-type extension hints for URL-fetched images. */
|
|
46
73
|
function mimeOfExtension(path: string): string | undefined {
|
|
47
74
|
const match = /\.([a-z0-9]+)$/i.exec(path)
|
|
@@ -87,17 +114,44 @@ function effectiveParams(request: GenerateRequest): {
|
|
|
87
114
|
size?: string
|
|
88
115
|
quality?: string
|
|
89
116
|
detail?: string
|
|
117
|
+
aspect_ratio?: string
|
|
118
|
+
resolution?: string
|
|
119
|
+
response_format?: string
|
|
90
120
|
} {
|
|
91
121
|
const model = request.model.trim() === '' ? 'gpt-image-2' : request.model.trim()
|
|
92
122
|
// dall-e-3 has no quality/detail knobs and only produces one image.
|
|
93
123
|
if (model === 'dall-e-3') {
|
|
94
|
-
const
|
|
124
|
+
const pixel = OPENAI_SIZE_BY_RATIO[request.size]
|
|
125
|
+
const size = (pixel !== undefined && DALLE3_SIZES.has(pixel)) ? pixel : '1024x1024'
|
|
95
126
|
return { model, size }
|
|
96
127
|
}
|
|
128
|
+
// Grok Imagine: the panel's aspect ratios are sent as-is (21:9 aliased to
|
|
129
|
+
// the documented 20:9), the clarity tiers become the resolution parameter
|
|
130
|
+
// (the API documents 1k / 2k only, so 4k falls back to 2k), and base64
|
|
131
|
+
// output keeps the temporary signed result URLs from expiring before the
|
|
132
|
+
// host downloads them.
|
|
133
|
+
if (isGrokImagine(model)) {
|
|
134
|
+
return {
|
|
135
|
+
model,
|
|
136
|
+
...request.size !== '' && request.size !== 'auto'
|
|
137
|
+
? { aspect_ratio: GROK_ASPECT_ALIASES[request.size] ?? request.size }
|
|
138
|
+
: {},
|
|
139
|
+
...request.quality !== '' && request.quality !== 'auto'
|
|
140
|
+
? { resolution: request.quality === '4k' ? '2k' : request.quality }
|
|
141
|
+
: {},
|
|
142
|
+
response_format: 'b64_json',
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// OpenAI-compatible endpoints: nearest pixel size, clarity tiers mapped to
|
|
146
|
+
// the quality levels (1k→low / 2k→medium / 4k→high), detail passthrough.
|
|
97
147
|
return {
|
|
98
148
|
model,
|
|
99
|
-
...request.size !== '' && request.size !== 'auto'
|
|
100
|
-
|
|
149
|
+
...request.size !== '' && request.size !== 'auto' && OPENAI_SIZE_BY_RATIO[request.size] !== undefined
|
|
150
|
+
? { size: OPENAI_SIZE_BY_RATIO[request.size] }
|
|
151
|
+
: {},
|
|
152
|
+
...request.quality === '1k' ? { quality: 'low' } : {},
|
|
153
|
+
...request.quality === '2k' ? { quality: 'medium' } : {},
|
|
154
|
+
...request.quality === '4k' ? { quality: 'high' } : {},
|
|
101
155
|
...request.detail !== '' ? { detail: request.detail } : {},
|
|
102
156
|
}
|
|
103
157
|
}
|
|
@@ -178,14 +232,27 @@ async function requestOneImage(
|
|
|
178
232
|
if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) {
|
|
179
233
|
throw new ImageGenError('参考图片超过 10MB 上限', 'edit-image-too-large')
|
|
180
234
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
235
|
+
// Grok Imagine /images/edits takes a JSON image_url object (a base64 data
|
|
236
|
+
// URI is accepted) instead of OpenAI's multipart form-data upload.
|
|
237
|
+
if (isGrokImagine(params.model)) {
|
|
238
|
+
headers['content-type'] = 'application/json'
|
|
239
|
+
body = JSON.stringify({
|
|
240
|
+
model: params.model,
|
|
241
|
+
prompt: request.prompt,
|
|
242
|
+
image: { url: request.image, type: 'image_url' },
|
|
243
|
+
...params.aspect_ratio !== undefined ? { aspect_ratio: params.aspect_ratio } : {},
|
|
244
|
+
response_format: 'b64_json',
|
|
245
|
+
})
|
|
246
|
+
} else {
|
|
247
|
+
const form = new FormData()
|
|
248
|
+
form.append('image', new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf(parsed.mime)}`)
|
|
249
|
+
form.append('prompt', request.prompt)
|
|
250
|
+
form.append('model', params.model)
|
|
251
|
+
if (params.size !== undefined) form.append('size', params.size)
|
|
252
|
+
if (params.quality !== undefined) form.append('quality', params.quality)
|
|
253
|
+
if (params.detail !== undefined) form.append('detail', params.detail)
|
|
254
|
+
body = form
|
|
255
|
+
}
|
|
189
256
|
} else {
|
|
190
257
|
headers['content-type'] = 'application/json'
|
|
191
258
|
body = JSON.stringify({ prompt: request.prompt, ...params } as Record<string, unknown>)
|