@dickpy/dsh-imagegen 1.5.3 → 1.5.4
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 +21 -1
- package/lib/client.js +5310 -1934
- package/lib/client.js.map +1 -1
- package/lib/index.js +986 -32
- package/package.json +1 -1
- package/src/canvas-store.ts +376 -0
- package/src/client/CanvasWorkspace.tsx +1569 -0
- package/src/client/ImageGenPanel.tsx +159 -96
- package/src/client/SettingsCard.tsx +173 -1
- package/src/client/api.ts +48 -1
- package/src/client/canvas-workspace.module.css +929 -0
- package/src/client/locales.ts +1443 -1146
- package/src/client/panel.module.css +83 -33
- package/src/engine.ts +156 -14
- package/src/gallery-store.ts +5 -0
- package/src/generation-runtime.ts +1 -0
- package/src/history-store.ts +5 -0
- package/src/index.ts +70 -3
- package/src/protocol.ts +121 -1
- package/src/routes.ts +231 -1
- package/src/storage-sync.ts +105 -0
|
@@ -17,7 +17,8 @@ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
|
17
17
|
import type { ImageGenApi } from './api.ts'
|
|
18
18
|
import { errorMessage, tt } from './helpers.ts'
|
|
19
19
|
import { TemplateLibrary } from './TemplateLibrary.tsx'
|
|
20
|
-
import { InspirationGallery } from './InspirationGallery.tsx'
|
|
20
|
+
import { InspirationGallery } from './InspirationGallery.tsx'
|
|
21
|
+
import { CanvasWorkspace } from './CanvasWorkspace.tsx'
|
|
21
22
|
import { useImageGenLanguageTick } from './use-language.ts'
|
|
22
23
|
import type { EcommerceRefRole, GeneratedImage, GenerateMode, GenerateRequest, GenerationTask, GenerationTaskStatus, HistoryEntry, HistoryImageRef, ProductSetDraft, ProductSetSlot, UpdateInfo } from '../protocol.ts'
|
|
23
24
|
import { AGENT_IMAGE_API } from '../protocol.ts'
|
|
@@ -351,19 +352,38 @@ function formatTime(timestamp: number): string {
|
|
|
351
352
|
|
|
352
353
|
function defaultEcommerceDraft(): ProductSetDraft {
|
|
353
354
|
return {
|
|
354
|
-
projectId: '', projectName: '', category: '通用商品', platform: '通用', language: '中文', size: '1:1',
|
|
355
|
+
projectId: '', projectName: '', category: '通用商品', platform: '通用', language: '中文', customLanguage: '', size: '1:1',
|
|
355
356
|
productName: '', sellingPoints: '', protectedFeatures: '', styleHint: '',
|
|
356
357
|
slots: PRODUCT_SET_SLOTS.map(slot => ({ ...slot })),
|
|
357
358
|
}
|
|
358
359
|
}
|
|
359
360
|
|
|
361
|
+
/** Standard copy-language choices; custom keeps uncommon locales usable. */
|
|
362
|
+
const ECOMMERCE_COPY_LANGUAGES = [
|
|
363
|
+
['中文', '中文'],
|
|
364
|
+
['English', 'English'],
|
|
365
|
+
['Русский', 'Русский'],
|
|
366
|
+
['日本語', '日本語'],
|
|
367
|
+
['한국어', '한국어'],
|
|
368
|
+
['Français', 'Français'],
|
|
369
|
+
['Deutsch', 'Deutsch'],
|
|
370
|
+
['Español', 'Español'],
|
|
371
|
+
['Português', 'Português'],
|
|
372
|
+
['custom', '自定义'],
|
|
373
|
+
] as const
|
|
374
|
+
|
|
375
|
+
function effectiveEcommerceLanguage(draft: ProductSetDraft): string {
|
|
376
|
+
return draft.language === 'custom' ? draft.customLanguage?.trim() ?? '' : draft.language
|
|
377
|
+
}
|
|
378
|
+
|
|
360
379
|
function ecommercePrompt(draft: ProductSetDraft, slot: ProductSetSlot): string {
|
|
361
380
|
const points = draft.sellingPoints.trim() || '突出商品真实材质、结构和核心价值'
|
|
362
381
|
const protectedFeatures = draft.protectedFeatures.trim() || '保持商品颜色、形状、Logo、包装文字和结构真实,不添加不存在的配件'
|
|
382
|
+
const language = effectiveEcommerceLanguage(draft) || '中文'
|
|
363
383
|
const refClause = slot.refRole !== undefined && slot.refRole !== 'none'
|
|
364
384
|
? `本图以上传的${ECOMMERCE_ROLE_PROMPT_LABELS[slot.refRole]}图片为参考,商品与风格必须与参考图保持一致;`
|
|
365
385
|
: ''
|
|
366
|
-
return `电商${slot.label}:为${draft.productName.trim() || '该商品'}制作${slot.description}。商品品类:${draft.category};平台:${draft.platform};语言:${
|
|
386
|
+
return `电商${slot.label}:为${draft.productName.trim() || '该商品'}制作${slot.description}。商品品类:${draft.category};平台:${draft.platform};语言:${language}。商品卖点:${points}。必须遵守:${protectedFeatures}。${refClause}整体要求:商品主体清晰、比例真实、光线自然、画面干净、适合电商发布;${draft.styleHint.trim()}`
|
|
367
387
|
}
|
|
368
388
|
|
|
369
389
|
/** Consistency prefix for slots generated after the main image exists. */
|
|
@@ -376,7 +396,7 @@ type PanelTab = GenerateMode | 'gallery'
|
|
|
376
396
|
|
|
377
397
|
/** Top-level workspaces inside the panel. 'normal' is the classic studio;
|
|
378
398
|
* more task-oriented modes (prototype, …) can join alongside 'ecommerce'. */
|
|
379
|
-
type PanelWorkspace = 'normal' | 'ecommerce'
|
|
399
|
+
type PanelWorkspace = 'normal' | 'ecommerce' | 'canvas'
|
|
380
400
|
|
|
381
401
|
type GalleryFilter = string
|
|
382
402
|
type ComparisonSession = { taskIds: string[]; prompt: string; comparisonId: string }
|
|
@@ -464,13 +484,18 @@ export function ImageGenPanel(props: {
|
|
|
464
484
|
const apiKeySet = (config?.channels ?? []).length > 0 ? channelKeySet : legacyKeySet
|
|
465
485
|
const connected = enabled && configured && apiKeySet
|
|
466
486
|
|
|
467
|
-
const [tab, setTab] = useState<PanelTab>('text')
|
|
468
|
-
const [workspace, setWorkspace] = useState<PanelWorkspace>('normal')
|
|
487
|
+
const [tab, setTab] = useState<PanelTab>('text')
|
|
488
|
+
const [workspace, setWorkspace] = useState<PanelWorkspace>('normal')
|
|
489
|
+
const [canvasImportRequest, setCanvasImportRequest] = useState<{ source: 'history' | 'gallery'; entryId: string; imageIndex: number } | undefined>()
|
|
469
490
|
/** Switch to a normal-generation tab, leaving any task workspace. */
|
|
470
|
-
const openTab = (next: PanelTab): void => {
|
|
471
|
-
setWorkspace('normal')
|
|
472
|
-
setTab(next)
|
|
473
|
-
}
|
|
491
|
+
const openTab = (next: PanelTab): void => {
|
|
492
|
+
setWorkspace('normal')
|
|
493
|
+
setTab(next)
|
|
494
|
+
}
|
|
495
|
+
const addEntryToCanvas = (source: 'history' | 'gallery', entryId: string, imageIndex = 0): void => {
|
|
496
|
+
setCanvasImportRequest({ source, entryId, imageIndex })
|
|
497
|
+
setWorkspace('canvas')
|
|
498
|
+
}
|
|
474
499
|
const [prompt, setPrompt] = useState('')
|
|
475
500
|
const [size, setSize] = useState<string>('auto')
|
|
476
501
|
const [quality, setQuality] = useState<string>('auto')
|
|
@@ -484,6 +509,7 @@ export function ImageGenPanel(props: {
|
|
|
484
509
|
const [images, setImages] = useState<GeneratedImage[]>([])
|
|
485
510
|
const [addingToConversation, setAddingToConversation] = useState<number | string | null>(null)
|
|
486
511
|
const [galleryConversationAddingId, setGalleryConversationAddingId] = useState<string | null>(null)
|
|
512
|
+
const [historyConversationAddingId, setHistoryConversationAddingId] = useState<string | null>(null)
|
|
487
513
|
const [conversationMessage, setConversationMessage] = useState<string | null>(null)
|
|
488
514
|
const [error, setError] = useState<string | null>(null)
|
|
489
515
|
// Submission is brief; actual generation stays visible until the host
|
|
@@ -919,7 +945,7 @@ export function ImageGenPanel(props: {
|
|
|
919
945
|
...(asset !== undefined ? { image: asset.dataUrl, refName: asset.name } : {}),
|
|
920
946
|
workflow: 'ecommerce' as const,
|
|
921
947
|
projectId,
|
|
922
|
-
projectName: ecommerce.
|
|
948
|
+
projectName: ecommerce.productName.trim(),
|
|
923
949
|
slotKey: `${slot.key}-${index + 1}`,
|
|
924
950
|
slotLabel: slot.label,
|
|
925
951
|
}
|
|
@@ -1135,38 +1161,6 @@ export function ImageGenPanel(props: {
|
|
|
1135
1161
|
}
|
|
1136
1162
|
}
|
|
1137
1163
|
|
|
1138
|
-
/** Restore one comparison group, including its selected model set. */
|
|
1139
|
-
const restoreHistoryGroup = async (group: HistoryGroup): Promise<void> => {
|
|
1140
|
-
const entry = group.entries[0]
|
|
1141
|
-
if (entry === undefined) return
|
|
1142
|
-
// The product-set form draft cannot be rebuilt from a compiled prompt, so
|
|
1143
|
-
// restoring a product set reopens its grouped results canvas.
|
|
1144
|
-
if (entry.workflow === 'ecommerce' && entry.projectId !== undefined) {
|
|
1145
|
-
await viewEcommerceProject(group)
|
|
1146
|
-
return
|
|
1147
|
-
}
|
|
1148
|
-
try {
|
|
1149
|
-
const restored = await loadHistoryGroup(group)
|
|
1150
|
-
openTab(entry.mode)
|
|
1151
|
-
setPrompt(entry.prompt)
|
|
1152
|
-
setSize(normalizeSize(entry.size))
|
|
1153
|
-
setQuality(normalizeQuality(entry.quality))
|
|
1154
|
-
setDetail((DETAILS as readonly string[]).includes(entry.detail) ? entry.detail : '')
|
|
1155
|
-
setCount(entry.n >= 1 && entry.n <= 4 ? entry.n : 1)
|
|
1156
|
-
setModel(imageModels.includes(entry.model) ? entry.model : imageModels[0])
|
|
1157
|
-
setCompareModels(group.models.filter(candidate => imageModels.includes(candidate)))
|
|
1158
|
-
setCompareEnabled(group.models.filter(candidate => imageModels.includes(candidate)).length > 1)
|
|
1159
|
-
setRefImage(null)
|
|
1160
|
-
setImages(restored)
|
|
1161
|
-
setComparison(null)
|
|
1162
|
-
setError(null)
|
|
1163
|
-
setViewingHistoryId(entry.id)
|
|
1164
|
-
setGalleryViewingId(null)
|
|
1165
|
-
} catch (caught) {
|
|
1166
|
-
setError(errorMessage(caught))
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
1164
|
/** Remove every persisted row belonging to one comparison group. */
|
|
1171
1165
|
const deleteHistoryGroup = async (group: HistoryGroup): Promise<void> => {
|
|
1172
1166
|
const ids = new Set(group.entries.map(entry => entry.id))
|
|
@@ -1314,6 +1308,21 @@ export function ImageGenPanel(props: {
|
|
|
1314
1308
|
}
|
|
1315
1309
|
}
|
|
1316
1310
|
|
|
1311
|
+
/** Add one history entry's first image to the current chat draft. */
|
|
1312
|
+
const addHistoryEntryToConversation = async (entry: HistoryEntry): Promise<void> => {
|
|
1313
|
+
if (historyConversationAddingId !== null || addingToConversation !== null || galleryConversationAddingId !== null || entry.images.length === 0) return
|
|
1314
|
+
setHistoryConversationAddingId(entry.id)
|
|
1315
|
+
try {
|
|
1316
|
+
const [image] = await historyImagesToGenerated(entry.images.slice(0, 1))
|
|
1317
|
+
if (image === undefined) return
|
|
1318
|
+
await addImageToConversation(image, 0, `history:${entry.id}`)
|
|
1319
|
+
} catch (caught) {
|
|
1320
|
+
setError(errorMessage(caught))
|
|
1321
|
+
} finally {
|
|
1322
|
+
setHistoryConversationAddingId(null)
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1317
1326
|
/** Load a persisted gallery image and add it to the current chat draft. */
|
|
1318
1327
|
const addGalleryEntryToConversation = async (entry: HistoryEntry): Promise<void> => {
|
|
1319
1328
|
if (galleryConversationAddingId !== null || addingToConversation !== null || entry.images.length === 0) return
|
|
@@ -1343,27 +1352,6 @@ export function ImageGenPanel(props: {
|
|
|
1343
1352
|
}
|
|
1344
1353
|
}
|
|
1345
1354
|
|
|
1346
|
-
/** Restore a gallery entry's parameters (and its images) into the form. */
|
|
1347
|
-
const restoreGalleryEntry = async (entry: HistoryEntry): Promise<void> => {
|
|
1348
|
-
try {
|
|
1349
|
-
const restored = await historyImagesToGenerated(entry.images)
|
|
1350
|
-
openTab(entry.mode)
|
|
1351
|
-
setPrompt(entry.prompt)
|
|
1352
|
-
setSize(normalizeSize(entry.size))
|
|
1353
|
-
setQuality(normalizeQuality(entry.quality))
|
|
1354
|
-
setDetail((DETAILS as readonly string[]).includes(entry.detail) ? entry.detail : '')
|
|
1355
|
-
setCount(entry.n >= 1 && entry.n <= 4 ? entry.n : 1)
|
|
1356
|
-
setModel(imageModels.includes(entry.model) ? entry.model : imageModels[0])
|
|
1357
|
-
setRefImage(null)
|
|
1358
|
-
setImages(restored)
|
|
1359
|
-
setError(null)
|
|
1360
|
-
setViewingHistoryId(null)
|
|
1361
|
-
setGalleryViewingId(null)
|
|
1362
|
-
} catch (caught) {
|
|
1363
|
-
setError(errorMessage(caught))
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
1355
|
/** Remove one gallery entry. */
|
|
1368
1356
|
const deleteGalleryEntry = async (id: string): Promise<void> => {
|
|
1369
1357
|
setGallery(gallery.filter(entry => entry.id !== id))
|
|
@@ -1454,7 +1442,7 @@ export function ImageGenPanel(props: {
|
|
|
1454
1442
|
const generateDisabled = submitting || modeModels.length === 0
|
|
1455
1443
|
const ecommerceSlots = ecommerce.slots.filter(slot => slot.enabled && slot.count > 0)
|
|
1456
1444
|
const ecommerceTotal = ecommerceSlots.reduce((total, slot) => total + slot.count, 0)
|
|
1457
|
-
const ecommerceGenerateDisabled = submitting || ecommerceGenerating || ecommerceSlots.length === 0 || ecommerce.productName.trim() === ''
|
|
1445
|
+
const ecommerceGenerateDisabled = submitting || ecommerceGenerating || ecommerceSlots.length === 0 || ecommerce.productName.trim() === '' || (ecommerce.language === 'custom' && effectiveEcommerceLanguage(ecommerce) === '')
|
|
1458
1446
|
const ecommerceFileInput = useRef<HTMLInputElement>(null)
|
|
1459
1447
|
// The results canvas merges live tasks of the active project with restored
|
|
1460
1448
|
// history entries of the same project; restored slots that were regenerated
|
|
@@ -1484,7 +1472,7 @@ export function ImageGenPanel(props: {
|
|
|
1484
1472
|
const ecommerceResultGroups = [...new Set(ecommerceMergedItems.map(item => item.label))]
|
|
1485
1473
|
.filter(label => label !== '')
|
|
1486
1474
|
.map(label => ({ label, items: ecommerceMergedItems.filter(item => item.label === label) }))
|
|
1487
|
-
const conversationBusy = addingToConversation !== null || galleryConversationAddingId !== null
|
|
1475
|
+
const conversationBusy = addingToConversation !== null || galleryConversationAddingId !== null || historyConversationAddingId !== null
|
|
1488
1476
|
const viewingEntry = viewingHistoryId === null ? null : history.find(entry => entry.id === viewingHistoryId) ?? null
|
|
1489
1477
|
const viewingGalleryEntry = galleryViewingId === null ? null : gallery.find(entry => entry.id === galleryViewingId) ?? null
|
|
1490
1478
|
const previewImage = preview === null ? null : preview.images[preview.index] ?? null
|
|
@@ -1567,6 +1555,16 @@ export function ImageGenPanel(props: {
|
|
|
1567
1555
|
>
|
|
1568
1556
|
<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" aria-hidden="true"><path d="M8 3v10M3 8h10" /></svg>
|
|
1569
1557
|
</button>
|
|
1558
|
+
<button
|
|
1559
|
+
type="button"
|
|
1560
|
+
className={css.historyNew}
|
|
1561
|
+
data-history-open-folder=""
|
|
1562
|
+
aria-label={tt('gallery.openFolder')}
|
|
1563
|
+
title={tt('gallery.openFolderHint')}
|
|
1564
|
+
onClick={() => { void api.openDataFolder() }}
|
|
1565
|
+
>
|
|
1566
|
+
<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M1.5 4.2A1.2 1.2 0 0 1 2.7 3h2.9l1.6 1.9h6.1A1.2 1.2 0 0 1 14.5 6.1v6.2a1.2 1.2 0 0 1-1.2 1.2H2.7a1.2 1.2 0 0 1-1.2-1.2z" /></svg>
|
|
1567
|
+
</button>
|
|
1570
1568
|
{history.length > 0 ? (
|
|
1571
1569
|
<button type="button" className={css.historyClear} data-history-clear="" onClick={() => { void clearHistory() }}>
|
|
1572
1570
|
{tt('history.clear')}
|
|
@@ -1628,22 +1626,53 @@ export function ImageGenPanel(props: {
|
|
|
1628
1626
|
</span>
|
|
1629
1627
|
</button>
|
|
1630
1628
|
<span className={css.historyActions}>
|
|
1629
|
+
{entry.images.length > 0 ? (
|
|
1630
|
+
<button
|
|
1631
|
+
type="button"
|
|
1632
|
+
className={css.historyIconAction}
|
|
1633
|
+
disabled={conversationBusy}
|
|
1634
|
+
title={`${tt('conversation.add')}:${tt('conversation.addHint')}`}
|
|
1635
|
+
aria-label={tt('conversation.add')}
|
|
1636
|
+
data-history-add-conversation=""
|
|
1637
|
+
onClick={() => { void addHistoryEntryToConversation(entry) }}
|
|
1638
|
+
>
|
|
1639
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M4 2.5h8A1.5 1.5 0 0 1 13.5 4v5a1.5 1.5 0 0 1-1.5 1.5H8.5L5.5 13v-2.5H4A1.5 1.5 0 0 1 2.5 9V4A1.5 1.5 0 0 1 4 2.5z" /></svg>
|
|
1640
|
+
</button>
|
|
1641
|
+
) : null}
|
|
1631
1642
|
{entry.images.length > 0 ? (
|
|
1632
1643
|
<button
|
|
1633
1644
|
type="button"
|
|
1634
|
-
className={css.
|
|
1645
|
+
className={css.historyIconAction}
|
|
1635
1646
|
disabled={galleryAdding}
|
|
1636
1647
|
title={tt('gallery.add')}
|
|
1648
|
+
aria-label={tt('gallery.add')}
|
|
1649
|
+
data-history-add-gallery=""
|
|
1637
1650
|
onClick={() => { void addHistoryEntryToGallery(entry) }}
|
|
1638
1651
|
>
|
|
1639
|
-
|
|
1640
|
-
</button>
|
|
1641
|
-
) : null}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1652
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2.5" y="3" width="11" height="10" rx="1.5" /><circle cx="5.9" cy="6.1" r="1" /><path d="M13.5 10.2l-3.1-3.1L4.6 13" /></svg>
|
|
1653
|
+
</button>
|
|
1654
|
+
) : null}
|
|
1655
|
+
{entry.images.length > 0 ? (
|
|
1656
|
+
<button
|
|
1657
|
+
type="button"
|
|
1658
|
+
className={css.historyIconAction}
|
|
1659
|
+
title={tt('canvas.addToCanvas')}
|
|
1660
|
+
aria-label={tt('canvas.addToCanvas')}
|
|
1661
|
+
data-history-add-canvas=""
|
|
1662
|
+
onClick={() => { addEntryToCanvas('history', entry.id, 0) }}
|
|
1663
|
+
>
|
|
1664
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="11" height="11" rx="1.5" /><path d="M5 8h6M8 5v6" /></svg>
|
|
1665
|
+
</button>
|
|
1666
|
+
) : null}
|
|
1667
|
+
<button
|
|
1668
|
+
type="button"
|
|
1669
|
+
className={css.historyIconAction}
|
|
1670
|
+
data-danger
|
|
1671
|
+
title={tt('history.delete')}
|
|
1672
|
+
aria-label={tt('history.delete')}
|
|
1673
|
+
onClick={() => { void deleteHistoryGroup(group) }}
|
|
1674
|
+
>
|
|
1675
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M2.5 4.5h11" /><path d="M6 4.5V3.2a.7.7 0 0 1 .7-.7h2.6a.7.7 0 0 1 .7.7v1.3" /><path d="M4.3 4.5l.6 8.1a1 1 0 0 0 1 .9h4.2a1 1 0 0 0 1-.9l.6-8.1" /><path d="M6.7 7v4M9.3 7v4" /></svg>
|
|
1647
1676
|
</button>
|
|
1648
1677
|
</span>
|
|
1649
1678
|
</div>
|
|
@@ -1670,11 +1699,12 @@ export function ImageGenPanel(props: {
|
|
|
1670
1699
|
<svg viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
1671
1700
|
</a>
|
|
1672
1701
|
</span>
|
|
1673
|
-
<nav className={css.topNav} role="tablist" aria-label={tt('workspace.label')}>
|
|
1674
|
-
<button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab !== 'gallery' ? '' : undefined} onClick={() => { if (workspace !== 'normal' || tab === 'gallery') openTab('text') }}>{tt('workspace.normal')}</button>
|
|
1675
|
-
<button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab === 'gallery' ? '' : undefined} onClick={() => { openTab('gallery') }}>{tt('gallery.title')}</button>
|
|
1676
|
-
<span className={css.topNavDivider} aria-hidden="true" />
|
|
1677
|
-
<button type="button" className={css.topNavItem} data-active={workspace === '
|
|
1702
|
+
<nav className={css.topNav} role="tablist" aria-label={tt('workspace.label')}>
|
|
1703
|
+
<button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab !== 'gallery' ? '' : undefined} onClick={() => { if (workspace !== 'normal' || tab === 'gallery') openTab('text') }}>{tt('workspace.normal')}</button>
|
|
1704
|
+
<button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab === 'gallery' ? '' : undefined} onClick={() => { openTab('gallery') }}>{tt('gallery.title')}</button>
|
|
1705
|
+
<span className={css.topNavDivider} aria-hidden="true" />
|
|
1706
|
+
<button type="button" className={css.topNavItem} data-active={workspace === 'canvas' ? '' : undefined} onClick={() => { setWorkspace('canvas') }}>{tt('workspace.canvas')}</button>
|
|
1707
|
+
<button type="button" className={css.topNavItem} data-active={workspace === 'ecommerce' ? '' : undefined} onClick={() => { setWorkspace('ecommerce') }}>{tt('workspace.ecommerce')}<span className={css.previewBadge}>{tt('ecommerce.badge')}</span></button>
|
|
1678
1708
|
</nav>
|
|
1679
1709
|
<span className={css.panelHeaderActions}>
|
|
1680
1710
|
<button
|
|
@@ -1716,7 +1746,7 @@ export function ImageGenPanel(props: {
|
|
|
1716
1746
|
|
|
1717
1747
|
<div className={css.studio}>
|
|
1718
1748
|
{/* ------------------------------- left history + generation workspace */}
|
|
1719
|
-
<div className={css.generation}>
|
|
1749
|
+
<div className={css.generation} data-workspace={workspace}>
|
|
1720
1750
|
{/* ------------------------------------------------ config sidebar */}
|
|
1721
1751
|
<aside
|
|
1722
1752
|
ref={configAsideRef}
|
|
@@ -1805,10 +1835,6 @@ export function ImageGenPanel(props: {
|
|
|
1805
1835
|
<span className={css.ecommerceFieldLabel}>{tt('ecommerce.productName')}</span>
|
|
1806
1836
|
<input value={ecommerce.productName} placeholder={tt('ecommerce.productName')} onChange={event => setEcommerce(previous => ({ ...previous, productName: event.target.value }))} />
|
|
1807
1837
|
</label>
|
|
1808
|
-
<label className={css.ecommerceField}>
|
|
1809
|
-
<span className={css.ecommerceFieldLabel}>{tt('ecommerce.projectName')}</span>
|
|
1810
|
-
<input value={ecommerce.projectName} placeholder={tt('ecommerce.projectName')} onChange={event => setEcommerce(previous => ({ ...previous, projectName: event.target.value }))} />
|
|
1811
|
-
</label>
|
|
1812
1838
|
{ecommerceAssets.length === 0 ? (
|
|
1813
1839
|
<button
|
|
1814
1840
|
type="button"
|
|
@@ -1872,7 +1898,18 @@ export function ImageGenPanel(props: {
|
|
|
1872
1898
|
</label>
|
|
1873
1899
|
<label className={css.ecommerceField}>
|
|
1874
1900
|
<span className={css.ecommerceFieldLabel}>{tt('ecommerce.languageLabel')}</span>
|
|
1875
|
-
<select value={ecommerce.language} onChange={event => setEcommerce(previous => ({ ...previous, language: event.target.value }))}
|
|
1901
|
+
<select aria-label={tt('ecommerce.languageLabel')} value={ecommerce.language} onChange={event => setEcommerce(previous => ({ ...previous, language: event.target.value }))}>
|
|
1902
|
+
{ECOMMERCE_COPY_LANGUAGES.map(([value, label]) => <option key={value} value={value}>{value === 'custom' ? tt('ecommerce.customLanguageOption') : label}</option>)}
|
|
1903
|
+
</select>
|
|
1904
|
+
{ecommerce.language === 'custom' ? (
|
|
1905
|
+
<input
|
|
1906
|
+
value={ecommerce.customLanguage ?? ''}
|
|
1907
|
+
maxLength={40}
|
|
1908
|
+
placeholder={tt('ecommerce.customLanguagePlaceholder')}
|
|
1909
|
+
aria-label={tt('ecommerce.customLanguageLabel')}
|
|
1910
|
+
onChange={event => setEcommerce(previous => ({ ...previous, customLanguage: event.target.value }))}
|
|
1911
|
+
/>
|
|
1912
|
+
) : null}
|
|
1876
1913
|
</label>
|
|
1877
1914
|
<label className={css.ecommerceField}>
|
|
1878
1915
|
<span className={css.ecommerceFieldLabel}>{tt('ecommerce.ratioLabel')}</span>
|
|
@@ -1918,12 +1955,12 @@ export function ImageGenPanel(props: {
|
|
|
1918
1955
|
</div>
|
|
1919
1956
|
{ecommerceSlots.length > 0 ? (
|
|
1920
1957
|
<>
|
|
1921
|
-
<button type="button" className={css.ecommerceAdvancedToggle} aria-expanded={ecommerceRefOpen} onClick={() => { setEcommerceRefOpen(open => !open) }}>
|
|
1922
|
-
{tt('ecommerce.refSettings')}
|
|
1958
|
+
<button type="button" className={css.ecommerceAdvancedToggle} aria-expanded={ecommerceRefOpen} aria-controls="dsh-ecommerce-reference-settings" onClick={() => { setEcommerceRefOpen(open => !open) }}>
|
|
1959
|
+
<span>{tt('ecommerce.refSettings')}</span>
|
|
1923
1960
|
<span className={css.ecommerceAdvancedChevron} aria-hidden="true">{ecommerceRefOpen ? '⌃' : '⌄'}</span>
|
|
1924
1961
|
</button>
|
|
1925
1962
|
{ecommerceRefOpen ? (
|
|
1926
|
-
<div className={css.ecommerceAdvancedBody}>
|
|
1963
|
+
<div id="dsh-ecommerce-reference-settings" className={css.ecommerceAdvancedBody}>
|
|
1927
1964
|
{ecommerceSlots.map(slot => (
|
|
1928
1965
|
<label key={slot.key} className={css.ecommerceRefRow}>
|
|
1929
1966
|
<span>{slot.label}</span>
|
|
@@ -2199,9 +2236,24 @@ export function ImageGenPanel(props: {
|
|
|
2199
2236
|
) : tt('generate')}
|
|
2200
2237
|
</Button> : null}
|
|
2201
2238
|
</section>
|
|
2202
|
-
</aside>
|
|
2203
|
-
|
|
2204
|
-
{
|
|
2239
|
+
</aside>
|
|
2240
|
+
|
|
2241
|
+
{workspace === 'canvas' ? (
|
|
2242
|
+
<CanvasWorkspace
|
|
2243
|
+
api={api}
|
|
2244
|
+
imageModels={imageModels}
|
|
2245
|
+
defaultChannelId={defaultChannelId}
|
|
2246
|
+
connected={connected}
|
|
2247
|
+
history={history}
|
|
2248
|
+
gallery={gallery}
|
|
2249
|
+
tasks={tasks}
|
|
2250
|
+
importRequest={canvasImportRequest}
|
|
2251
|
+
onImportRequestHandled={() => { setCanvasImportRequest(undefined) }}
|
|
2252
|
+
onOpenSettings={() => { openSettingsGuide('generation') }}
|
|
2253
|
+
/>
|
|
2254
|
+
) : null}
|
|
2255
|
+
|
|
2256
|
+
{/* ------------------------------------------------------- canvas */}
|
|
2205
2257
|
<section className={css.canvas} data-gallery={workspace === 'normal' && tab === 'gallery' ? 'true' : undefined}>
|
|
2206
2258
|
{workspace === 'normal' && tab === 'gallery' ? (
|
|
2207
2259
|
<div className={css.galleryWorkspace}>
|
|
@@ -2227,6 +2279,7 @@ export function ImageGenPanel(props: {
|
|
|
2227
2279
|
<option value="newest">{tt('gallery.newest')}</option>
|
|
2228
2280
|
<option value="oldest">{tt('gallery.oldest')}</option>
|
|
2229
2281
|
</select>
|
|
2282
|
+
<button type="button" className={css.galleryClear} data-gallery-open-folder="" title={tt('gallery.openFolderHint')} onClick={() => { void api.openDataFolder() }}>{tt('gallery.openFolder')}</button>
|
|
2230
2283
|
{gallery.length > 0 ? <button type="button" className={css.galleryClear} data-gallery-clear="" onClick={() => { void clearGalleryAll() }}>{tt('gallery.clear')}</button> : null}
|
|
2231
2284
|
</div>
|
|
2232
2285
|
</header>
|
|
@@ -2257,7 +2310,7 @@ export function ImageGenPanel(props: {
|
|
|
2257
2310
|
<span className={css.galleryBadge}>{entry.mode === 'edit' ? tt('mode.edit') : tt('mode.text')}</span>
|
|
2258
2311
|
</button>
|
|
2259
2312
|
<div className={css.galleryCardActions}>
|
|
2260
|
-
<button
|
|
2313
|
+
<button
|
|
2261
2314
|
type="button"
|
|
2262
2315
|
className={css.galleryCardAction}
|
|
2263
2316
|
data-gallery-add-conversation=""
|
|
@@ -2266,9 +2319,19 @@ export function ImageGenPanel(props: {
|
|
|
2266
2319
|
onClick={(event) => { event.stopPropagation(); void addGalleryEntryToConversation(entry) }}
|
|
2267
2320
|
>
|
|
2268
2321
|
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 4.5h10v7H3z"/><path d="M5.5 2.5h5M8 6v4M6 8h4"/></svg>
|
|
2269
|
-
{galleryConversationAddingId === entry.id || addingToConversation === `gallery:${entry.id}` ? tt('conversation.adding') : tt('conversation.add')}
|
|
2270
|
-
</button>
|
|
2271
|
-
|
|
2322
|
+
{galleryConversationAddingId === entry.id || addingToConversation === `gallery:${entry.id}` ? tt('conversation.adding') : tt('conversation.add')}
|
|
2323
|
+
</button>
|
|
2324
|
+
<button
|
|
2325
|
+
type="button"
|
|
2326
|
+
className={css.galleryCardAction}
|
|
2327
|
+
data-gallery-add-canvas=""
|
|
2328
|
+
title={tt('canvas.addToCanvas')}
|
|
2329
|
+
onClick={(event) => { event.stopPropagation(); addEntryToCanvas('gallery', entry.id, 0) }}
|
|
2330
|
+
>
|
|
2331
|
+
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="11" height="11" rx="1.5" /><path d="M5 8h6M8 5v6" /></svg>
|
|
2332
|
+
{tt('canvas.addToCanvas')}
|
|
2333
|
+
</button>
|
|
2334
|
+
</div>
|
|
2272
2335
|
<div className={css.galleryCardFooter}>
|
|
2273
2336
|
<span className={css.galleryAvatar}>{entry.model.toLowerCase().startsWith('nanobanana') ? 'N' : entry.model.toLowerCase().startsWith('seedream') ? 'S' : entry.model.startsWith('grok') ? 'G' : 'D'}</span>
|
|
2274
2337
|
<span className={css.galleryCardInfo}>
|