@dickpy/dsh-imagegen 1.5.2 → 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 +23 -2
- package/lib/client.js +5803 -1942
- package/lib/client.js.map +1 -1
- package/lib/index.js +1151 -33
- package/package.json +1 -1
- package/src/canvas-store.ts +376 -0
- package/src/client/CanvasWorkspace.tsx +1569 -0
- package/src/client/ImageGenPanel.tsx +164 -96
- package/src/client/SettingsCard.tsx +182 -4
- package/src/client/api.ts +48 -1
- package/src/client/canvas-workspace.module.css +929 -0
- package/src/client/helpers.ts +71 -33
- package/src/client/index.ts +59 -7
- package/src/client/locales.ts +1452 -772
- package/src/client/panel.module.css +83 -33
- package/src/client/use-language.ts +14 -0
- package/src/engine.ts +302 -12
- 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 +71 -4
- package/src/model-catalog.ts +10 -1
- package/src/presets.ts +15 -0
- package/src/protocol.ts +121 -1
- package/src/routes.ts +231 -1
- package/src/storage-sync.ts +105 -0
|
@@ -17,7 +17,9 @@ 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'
|
|
22
|
+
import { useImageGenLanguageTick } from './use-language.ts'
|
|
21
23
|
import type { EcommerceRefRole, GeneratedImage, GenerateMode, GenerateRequest, GenerationTask, GenerationTaskStatus, HistoryEntry, HistoryImageRef, ProductSetDraft, ProductSetSlot, UpdateInfo } from '../protocol.ts'
|
|
22
24
|
import { AGENT_IMAGE_API } from '../protocol.ts'
|
|
23
25
|
import type { ImageGenConfig, ImageGenScope } from './settings-scope.ts'
|
|
@@ -350,19 +352,38 @@ function formatTime(timestamp: number): string {
|
|
|
350
352
|
|
|
351
353
|
function defaultEcommerceDraft(): ProductSetDraft {
|
|
352
354
|
return {
|
|
353
|
-
projectId: '', projectName: '', category: '通用商品', platform: '通用', language: '中文', size: '1:1',
|
|
355
|
+
projectId: '', projectName: '', category: '通用商品', platform: '通用', language: '中文', customLanguage: '', size: '1:1',
|
|
354
356
|
productName: '', sellingPoints: '', protectedFeatures: '', styleHint: '',
|
|
355
357
|
slots: PRODUCT_SET_SLOTS.map(slot => ({ ...slot })),
|
|
356
358
|
}
|
|
357
359
|
}
|
|
358
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
|
+
|
|
359
379
|
function ecommercePrompt(draft: ProductSetDraft, slot: ProductSetSlot): string {
|
|
360
380
|
const points = draft.sellingPoints.trim() || '突出商品真实材质、结构和核心价值'
|
|
361
381
|
const protectedFeatures = draft.protectedFeatures.trim() || '保持商品颜色、形状、Logo、包装文字和结构真实,不添加不存在的配件'
|
|
382
|
+
const language = effectiveEcommerceLanguage(draft) || '中文'
|
|
362
383
|
const refClause = slot.refRole !== undefined && slot.refRole !== 'none'
|
|
363
384
|
? `本图以上传的${ECOMMERCE_ROLE_PROMPT_LABELS[slot.refRole]}图片为参考,商品与风格必须与参考图保持一致;`
|
|
364
385
|
: ''
|
|
365
|
-
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()}`
|
|
366
387
|
}
|
|
367
388
|
|
|
368
389
|
/** Consistency prefix for slots generated after the main image exists. */
|
|
@@ -375,7 +396,7 @@ type PanelTab = GenerateMode | 'gallery'
|
|
|
375
396
|
|
|
376
397
|
/** Top-level workspaces inside the panel. 'normal' is the classic studio;
|
|
377
398
|
* more task-oriented modes (prototype, …) can join alongside 'ecommerce'. */
|
|
378
|
-
type PanelWorkspace = 'normal' | 'ecommerce'
|
|
399
|
+
type PanelWorkspace = 'normal' | 'ecommerce' | 'canvas'
|
|
379
400
|
|
|
380
401
|
type GalleryFilter = string
|
|
381
402
|
type ComparisonSession = { taskIds: string[]; prompt: string; comparisonId: string }
|
|
@@ -440,6 +461,10 @@ export function ImageGenPanel(props: {
|
|
|
440
461
|
}) {
|
|
441
462
|
const { api, scope, sessions, conversation } = props
|
|
442
463
|
const config = useConfig(scope)
|
|
464
|
+
// The plugin language follows the DSH interface (bridged from ctx.locale);
|
|
465
|
+
// this tick re-renders the tree so every tt() switches live — the template
|
|
466
|
+
// library and the inspiration wall render inside this tree.
|
|
467
|
+
useImageGenLanguageTick()
|
|
443
468
|
const enabled = config?.enabled ?? true
|
|
444
469
|
// Channel-aware model options: the panel lists every configured alias
|
|
445
470
|
// (default channel first); legacy flat fields remain the upgrade fallback.
|
|
@@ -459,13 +484,18 @@ export function ImageGenPanel(props: {
|
|
|
459
484
|
const apiKeySet = (config?.channels ?? []).length > 0 ? channelKeySet : legacyKeySet
|
|
460
485
|
const connected = enabled && configured && apiKeySet
|
|
461
486
|
|
|
462
|
-
const [tab, setTab] = useState<PanelTab>('text')
|
|
463
|
-
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>()
|
|
464
490
|
/** Switch to a normal-generation tab, leaving any task workspace. */
|
|
465
|
-
const openTab = (next: PanelTab): void => {
|
|
466
|
-
setWorkspace('normal')
|
|
467
|
-
setTab(next)
|
|
468
|
-
}
|
|
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
|
+
}
|
|
469
499
|
const [prompt, setPrompt] = useState('')
|
|
470
500
|
const [size, setSize] = useState<string>('auto')
|
|
471
501
|
const [quality, setQuality] = useState<string>('auto')
|
|
@@ -479,6 +509,7 @@ export function ImageGenPanel(props: {
|
|
|
479
509
|
const [images, setImages] = useState<GeneratedImage[]>([])
|
|
480
510
|
const [addingToConversation, setAddingToConversation] = useState<number | string | null>(null)
|
|
481
511
|
const [galleryConversationAddingId, setGalleryConversationAddingId] = useState<string | null>(null)
|
|
512
|
+
const [historyConversationAddingId, setHistoryConversationAddingId] = useState<string | null>(null)
|
|
482
513
|
const [conversationMessage, setConversationMessage] = useState<string | null>(null)
|
|
483
514
|
const [error, setError] = useState<string | null>(null)
|
|
484
515
|
// Submission is brief; actual generation stays visible until the host
|
|
@@ -914,7 +945,7 @@ export function ImageGenPanel(props: {
|
|
|
914
945
|
...(asset !== undefined ? { image: asset.dataUrl, refName: asset.name } : {}),
|
|
915
946
|
workflow: 'ecommerce' as const,
|
|
916
947
|
projectId,
|
|
917
|
-
projectName: ecommerce.
|
|
948
|
+
projectName: ecommerce.productName.trim(),
|
|
918
949
|
slotKey: `${slot.key}-${index + 1}`,
|
|
919
950
|
slotLabel: slot.label,
|
|
920
951
|
}
|
|
@@ -1130,38 +1161,6 @@ export function ImageGenPanel(props: {
|
|
|
1130
1161
|
}
|
|
1131
1162
|
}
|
|
1132
1163
|
|
|
1133
|
-
/** Restore one comparison group, including its selected model set. */
|
|
1134
|
-
const restoreHistoryGroup = async (group: HistoryGroup): Promise<void> => {
|
|
1135
|
-
const entry = group.entries[0]
|
|
1136
|
-
if (entry === undefined) return
|
|
1137
|
-
// The product-set form draft cannot be rebuilt from a compiled prompt, so
|
|
1138
|
-
// restoring a product set reopens its grouped results canvas.
|
|
1139
|
-
if (entry.workflow === 'ecommerce' && entry.projectId !== undefined) {
|
|
1140
|
-
await viewEcommerceProject(group)
|
|
1141
|
-
return
|
|
1142
|
-
}
|
|
1143
|
-
try {
|
|
1144
|
-
const restored = await loadHistoryGroup(group)
|
|
1145
|
-
openTab(entry.mode)
|
|
1146
|
-
setPrompt(entry.prompt)
|
|
1147
|
-
setSize(normalizeSize(entry.size))
|
|
1148
|
-
setQuality(normalizeQuality(entry.quality))
|
|
1149
|
-
setDetail((DETAILS as readonly string[]).includes(entry.detail) ? entry.detail : '')
|
|
1150
|
-
setCount(entry.n >= 1 && entry.n <= 4 ? entry.n : 1)
|
|
1151
|
-
setModel(imageModels.includes(entry.model) ? entry.model : imageModels[0])
|
|
1152
|
-
setCompareModels(group.models.filter(candidate => imageModels.includes(candidate)))
|
|
1153
|
-
setCompareEnabled(group.models.filter(candidate => imageModels.includes(candidate)).length > 1)
|
|
1154
|
-
setRefImage(null)
|
|
1155
|
-
setImages(restored)
|
|
1156
|
-
setComparison(null)
|
|
1157
|
-
setError(null)
|
|
1158
|
-
setViewingHistoryId(entry.id)
|
|
1159
|
-
setGalleryViewingId(null)
|
|
1160
|
-
} catch (caught) {
|
|
1161
|
-
setError(errorMessage(caught))
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
1164
|
/** Remove every persisted row belonging to one comparison group. */
|
|
1166
1165
|
const deleteHistoryGroup = async (group: HistoryGroup): Promise<void> => {
|
|
1167
1166
|
const ids = new Set(group.entries.map(entry => entry.id))
|
|
@@ -1309,6 +1308,21 @@ export function ImageGenPanel(props: {
|
|
|
1309
1308
|
}
|
|
1310
1309
|
}
|
|
1311
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
|
+
|
|
1312
1326
|
/** Load a persisted gallery image and add it to the current chat draft. */
|
|
1313
1327
|
const addGalleryEntryToConversation = async (entry: HistoryEntry): Promise<void> => {
|
|
1314
1328
|
if (galleryConversationAddingId !== null || addingToConversation !== null || entry.images.length === 0) return
|
|
@@ -1338,27 +1352,6 @@ export function ImageGenPanel(props: {
|
|
|
1338
1352
|
}
|
|
1339
1353
|
}
|
|
1340
1354
|
|
|
1341
|
-
/** Restore a gallery entry's parameters (and its images) into the form. */
|
|
1342
|
-
const restoreGalleryEntry = async (entry: HistoryEntry): Promise<void> => {
|
|
1343
|
-
try {
|
|
1344
|
-
const restored = await historyImagesToGenerated(entry.images)
|
|
1345
|
-
openTab(entry.mode)
|
|
1346
|
-
setPrompt(entry.prompt)
|
|
1347
|
-
setSize(normalizeSize(entry.size))
|
|
1348
|
-
setQuality(normalizeQuality(entry.quality))
|
|
1349
|
-
setDetail((DETAILS as readonly string[]).includes(entry.detail) ? entry.detail : '')
|
|
1350
|
-
setCount(entry.n >= 1 && entry.n <= 4 ? entry.n : 1)
|
|
1351
|
-
setModel(imageModels.includes(entry.model) ? entry.model : imageModels[0])
|
|
1352
|
-
setRefImage(null)
|
|
1353
|
-
setImages(restored)
|
|
1354
|
-
setError(null)
|
|
1355
|
-
setViewingHistoryId(null)
|
|
1356
|
-
setGalleryViewingId(null)
|
|
1357
|
-
} catch (caught) {
|
|
1358
|
-
setError(errorMessage(caught))
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
1355
|
/** Remove one gallery entry. */
|
|
1363
1356
|
const deleteGalleryEntry = async (id: string): Promise<void> => {
|
|
1364
1357
|
setGallery(gallery.filter(entry => entry.id !== id))
|
|
@@ -1449,7 +1442,7 @@ export function ImageGenPanel(props: {
|
|
|
1449
1442
|
const generateDisabled = submitting || modeModels.length === 0
|
|
1450
1443
|
const ecommerceSlots = ecommerce.slots.filter(slot => slot.enabled && slot.count > 0)
|
|
1451
1444
|
const ecommerceTotal = ecommerceSlots.reduce((total, slot) => total + slot.count, 0)
|
|
1452
|
-
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) === '')
|
|
1453
1446
|
const ecommerceFileInput = useRef<HTMLInputElement>(null)
|
|
1454
1447
|
// The results canvas merges live tasks of the active project with restored
|
|
1455
1448
|
// history entries of the same project; restored slots that were regenerated
|
|
@@ -1479,7 +1472,7 @@ export function ImageGenPanel(props: {
|
|
|
1479
1472
|
const ecommerceResultGroups = [...new Set(ecommerceMergedItems.map(item => item.label))]
|
|
1480
1473
|
.filter(label => label !== '')
|
|
1481
1474
|
.map(label => ({ label, items: ecommerceMergedItems.filter(item => item.label === label) }))
|
|
1482
|
-
const conversationBusy = addingToConversation !== null || galleryConversationAddingId !== null
|
|
1475
|
+
const conversationBusy = addingToConversation !== null || galleryConversationAddingId !== null || historyConversationAddingId !== null
|
|
1483
1476
|
const viewingEntry = viewingHistoryId === null ? null : history.find(entry => entry.id === viewingHistoryId) ?? null
|
|
1484
1477
|
const viewingGalleryEntry = galleryViewingId === null ? null : gallery.find(entry => entry.id === galleryViewingId) ?? null
|
|
1485
1478
|
const previewImage = preview === null ? null : preview.images[preview.index] ?? null
|
|
@@ -1562,6 +1555,16 @@ export function ImageGenPanel(props: {
|
|
|
1562
1555
|
>
|
|
1563
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>
|
|
1564
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>
|
|
1565
1568
|
{history.length > 0 ? (
|
|
1566
1569
|
<button type="button" className={css.historyClear} data-history-clear="" onClick={() => { void clearHistory() }}>
|
|
1567
1570
|
{tt('history.clear')}
|
|
@@ -1623,22 +1626,53 @@ export function ImageGenPanel(props: {
|
|
|
1623
1626
|
</span>
|
|
1624
1627
|
</button>
|
|
1625
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}
|
|
1626
1642
|
{entry.images.length > 0 ? (
|
|
1627
1643
|
<button
|
|
1628
1644
|
type="button"
|
|
1629
|
-
className={css.
|
|
1645
|
+
className={css.historyIconAction}
|
|
1630
1646
|
disabled={galleryAdding}
|
|
1631
1647
|
title={tt('gallery.add')}
|
|
1648
|
+
aria-label={tt('gallery.add')}
|
|
1649
|
+
data-history-add-gallery=""
|
|
1632
1650
|
onClick={() => { void addHistoryEntryToGallery(entry) }}
|
|
1633
1651
|
>
|
|
1634
|
-
|
|
1635
|
-
</button>
|
|
1636
|
-
) : null}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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>
|
|
1642
1676
|
</button>
|
|
1643
1677
|
</span>
|
|
1644
1678
|
</div>
|
|
@@ -1665,11 +1699,12 @@ export function ImageGenPanel(props: {
|
|
|
1665
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>
|
|
1666
1700
|
</a>
|
|
1667
1701
|
</span>
|
|
1668
|
-
<nav className={css.topNav} role="tablist" aria-label={tt('workspace.label')}>
|
|
1669
|
-
<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>
|
|
1670
|
-
<button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab === 'gallery' ? '' : undefined} onClick={() => { openTab('gallery') }}>{tt('gallery.title')}</button>
|
|
1671
|
-
<span className={css.topNavDivider} aria-hidden="true" />
|
|
1672
|
-
<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>
|
|
1673
1708
|
</nav>
|
|
1674
1709
|
<span className={css.panelHeaderActions}>
|
|
1675
1710
|
<button
|
|
@@ -1711,7 +1746,7 @@ export function ImageGenPanel(props: {
|
|
|
1711
1746
|
|
|
1712
1747
|
<div className={css.studio}>
|
|
1713
1748
|
{/* ------------------------------- left history + generation workspace */}
|
|
1714
|
-
<div className={css.generation}>
|
|
1749
|
+
<div className={css.generation} data-workspace={workspace}>
|
|
1715
1750
|
{/* ------------------------------------------------ config sidebar */}
|
|
1716
1751
|
<aside
|
|
1717
1752
|
ref={configAsideRef}
|
|
@@ -1800,10 +1835,6 @@ export function ImageGenPanel(props: {
|
|
|
1800
1835
|
<span className={css.ecommerceFieldLabel}>{tt('ecommerce.productName')}</span>
|
|
1801
1836
|
<input value={ecommerce.productName} placeholder={tt('ecommerce.productName')} onChange={event => setEcommerce(previous => ({ ...previous, productName: event.target.value }))} />
|
|
1802
1837
|
</label>
|
|
1803
|
-
<label className={css.ecommerceField}>
|
|
1804
|
-
<span className={css.ecommerceFieldLabel}>{tt('ecommerce.projectName')}</span>
|
|
1805
|
-
<input value={ecommerce.projectName} placeholder={tt('ecommerce.projectName')} onChange={event => setEcommerce(previous => ({ ...previous, projectName: event.target.value }))} />
|
|
1806
|
-
</label>
|
|
1807
1838
|
{ecommerceAssets.length === 0 ? (
|
|
1808
1839
|
<button
|
|
1809
1840
|
type="button"
|
|
@@ -1867,7 +1898,18 @@ export function ImageGenPanel(props: {
|
|
|
1867
1898
|
</label>
|
|
1868
1899
|
<label className={css.ecommerceField}>
|
|
1869
1900
|
<span className={css.ecommerceFieldLabel}>{tt('ecommerce.languageLabel')}</span>
|
|
1870
|
-
<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}
|
|
1871
1913
|
</label>
|
|
1872
1914
|
<label className={css.ecommerceField}>
|
|
1873
1915
|
<span className={css.ecommerceFieldLabel}>{tt('ecommerce.ratioLabel')}</span>
|
|
@@ -1913,12 +1955,12 @@ export function ImageGenPanel(props: {
|
|
|
1913
1955
|
</div>
|
|
1914
1956
|
{ecommerceSlots.length > 0 ? (
|
|
1915
1957
|
<>
|
|
1916
|
-
<button type="button" className={css.ecommerceAdvancedToggle} aria-expanded={ecommerceRefOpen} onClick={() => { setEcommerceRefOpen(open => !open) }}>
|
|
1917
|
-
{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>
|
|
1918
1960
|
<span className={css.ecommerceAdvancedChevron} aria-hidden="true">{ecommerceRefOpen ? '⌃' : '⌄'}</span>
|
|
1919
1961
|
</button>
|
|
1920
1962
|
{ecommerceRefOpen ? (
|
|
1921
|
-
<div className={css.ecommerceAdvancedBody}>
|
|
1963
|
+
<div id="dsh-ecommerce-reference-settings" className={css.ecommerceAdvancedBody}>
|
|
1922
1964
|
{ecommerceSlots.map(slot => (
|
|
1923
1965
|
<label key={slot.key} className={css.ecommerceRefRow}>
|
|
1924
1966
|
<span>{slot.label}</span>
|
|
@@ -2194,9 +2236,24 @@ export function ImageGenPanel(props: {
|
|
|
2194
2236
|
) : tt('generate')}
|
|
2195
2237
|
</Button> : null}
|
|
2196
2238
|
</section>
|
|
2197
|
-
</aside>
|
|
2198
|
-
|
|
2199
|
-
{
|
|
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 */}
|
|
2200
2257
|
<section className={css.canvas} data-gallery={workspace === 'normal' && tab === 'gallery' ? 'true' : undefined}>
|
|
2201
2258
|
{workspace === 'normal' && tab === 'gallery' ? (
|
|
2202
2259
|
<div className={css.galleryWorkspace}>
|
|
@@ -2222,6 +2279,7 @@ export function ImageGenPanel(props: {
|
|
|
2222
2279
|
<option value="newest">{tt('gallery.newest')}</option>
|
|
2223
2280
|
<option value="oldest">{tt('gallery.oldest')}</option>
|
|
2224
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>
|
|
2225
2283
|
{gallery.length > 0 ? <button type="button" className={css.galleryClear} data-gallery-clear="" onClick={() => { void clearGalleryAll() }}>{tt('gallery.clear')}</button> : null}
|
|
2226
2284
|
</div>
|
|
2227
2285
|
</header>
|
|
@@ -2252,7 +2310,7 @@ export function ImageGenPanel(props: {
|
|
|
2252
2310
|
<span className={css.galleryBadge}>{entry.mode === 'edit' ? tt('mode.edit') : tt('mode.text')}</span>
|
|
2253
2311
|
</button>
|
|
2254
2312
|
<div className={css.galleryCardActions}>
|
|
2255
|
-
<button
|
|
2313
|
+
<button
|
|
2256
2314
|
type="button"
|
|
2257
2315
|
className={css.galleryCardAction}
|
|
2258
2316
|
data-gallery-add-conversation=""
|
|
@@ -2261,9 +2319,19 @@ export function ImageGenPanel(props: {
|
|
|
2261
2319
|
onClick={(event) => { event.stopPropagation(); void addGalleryEntryToConversation(entry) }}
|
|
2262
2320
|
>
|
|
2263
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>
|
|
2264
|
-
{galleryConversationAddingId === entry.id || addingToConversation === `gallery:${entry.id}` ? tt('conversation.adding') : tt('conversation.add')}
|
|
2265
|
-
</button>
|
|
2266
|
-
|
|
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>
|
|
2267
2335
|
<div className={css.galleryCardFooter}>
|
|
2268
2336
|
<span className={css.galleryAvatar}>{entry.model.toLowerCase().startsWith('nanobanana') ? 'N' : entry.model.toLowerCase().startsWith('seedream') ? 'S' : entry.model.startsWith('grok') ? 'G' : 'D'}</span>
|
|
2269
2337
|
<span className={css.galleryCardInfo}>
|