@dickpy/dsh-imagegen 1.5.5 → 1.5.7
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 +3 -1
- package/lib/client.js +1032 -464
- package/lib/client.js.map +1 -1
- package/lib/index.js +260 -54
- package/package.json +81 -81
- package/src/canvas-store.ts +375 -376
- package/src/client/CanvasWorkspace.tsx +273 -30
- package/src/client/ImageGenPanel.tsx +160 -86
- package/src/client/SettingsCard.tsx +1128 -1114
- package/src/client/canvas-workspace.module.css +144 -8
- package/src/client/locales.ts +1515 -1455
- package/src/client/panel.module.css +27 -26
- package/src/engine.ts +998 -828
- package/src/gallery-store.ts +311 -311
- package/src/generation-runtime.ts +2 -2
- package/src/history-store.ts +275 -275
- package/src/image-storage-path.ts +12 -0
- package/src/index.ts +430 -424
- package/src/model-catalog.ts +149 -124
- package/src/presets.ts +95 -86
- package/src/prompt-enhancer.ts +151 -137
- package/src/protocol.ts +580 -574
- package/src/routes.ts +3 -0
- package/src/task-queue.ts +4 -5
|
@@ -17,15 +17,15 @@ 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'
|
|
21
|
-
import { CanvasWorkspace } from './CanvasWorkspace.tsx'
|
|
20
|
+
import { InspirationGallery } from './InspirationGallery.tsx'
|
|
21
|
+
import { CanvasWorkspace } from './CanvasWorkspace.tsx'
|
|
22
22
|
import { useImageGenLanguageTick } from './use-language.ts'
|
|
23
23
|
import type { EcommerceRefRole, GeneratedImage, GenerateMode, GenerateRequest, GenerationTask, GenerationTaskStatus, HistoryEntry, HistoryImageRef, ProductSetDraft, ProductSetSlot, UpdateInfo } from '../protocol.ts'
|
|
24
24
|
import { AGENT_IMAGE_API } from '../protocol.ts'
|
|
25
25
|
import type { ImageGenConfig, ImageGenScope } from './settings-scope.ts'
|
|
26
26
|
import { imageModelOptions } from './settings-scope.ts'
|
|
27
27
|
import { normalizeImageModels } from '../image-models.ts'
|
|
28
|
-
import { describeModel } from '../model-catalog.ts'
|
|
28
|
+
import { describeModel, promptCharLimit } from '../model-catalog.ts'
|
|
29
29
|
import { CHAT_IMAGE_EVENT, type ChatImageEventDetail, type ConversationService } from './conversation-sync.ts'
|
|
30
30
|
import css from './panel.module.css'
|
|
31
31
|
|
|
@@ -396,7 +396,7 @@ type PanelTab = GenerateMode | 'gallery'
|
|
|
396
396
|
|
|
397
397
|
/** Top-level workspaces inside the panel. 'normal' is the classic studio;
|
|
398
398
|
* more task-oriented modes (prototype, …) can join alongside 'ecommerce'. */
|
|
399
|
-
type PanelWorkspace = 'normal' | 'ecommerce' | 'canvas'
|
|
399
|
+
type PanelWorkspace = 'normal' | 'ecommerce' | 'canvas'
|
|
400
400
|
|
|
401
401
|
type GalleryFilter = string
|
|
402
402
|
type ComparisonSession = { taskIds: string[]; prompt: string; comparisonId: string }
|
|
@@ -484,18 +484,18 @@ export function ImageGenPanel(props: {
|
|
|
484
484
|
const apiKeySet = (config?.channels ?? []).length > 0 ? channelKeySet : legacyKeySet
|
|
485
485
|
const connected = enabled && configured && apiKeySet
|
|
486
486
|
|
|
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>()
|
|
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>()
|
|
490
490
|
/** Switch to a normal-generation tab, leaving any task workspace. */
|
|
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
|
-
}
|
|
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
|
+
}
|
|
499
499
|
const [prompt, setPrompt] = useState('')
|
|
500
500
|
const [size, setSize] = useState<string>('auto')
|
|
501
501
|
const [quality, setQuality] = useState<string>('auto')
|
|
@@ -524,6 +524,7 @@ export function ImageGenPanel(props: {
|
|
|
524
524
|
const [galleryAdding, setGalleryAdding] = useState(false)
|
|
525
525
|
const [galleryMessage, setGalleryMessage] = useState<string | null>(null)
|
|
526
526
|
const [galleryFilter, setGalleryFilter] = useState<GalleryFilter>('all')
|
|
527
|
+
const galleryUploadRef = useRef<HTMLInputElement>(null)
|
|
527
528
|
const [galleryRatio, setGalleryRatio] = useState('all')
|
|
528
529
|
const [galleryTagFilter, setGalleryTagFilter] = useState<string | null>(null)
|
|
529
530
|
const [galleryView, setGalleryView] = useState<'masonry' | 'grid'>('masonry')
|
|
@@ -1253,6 +1254,47 @@ export function ImageGenPanel(props: {
|
|
|
1253
1254
|
}
|
|
1254
1255
|
}
|
|
1255
1256
|
|
|
1257
|
+
/** Save local image files into the asset library so the canvas and studio
|
|
1258
|
+
* can reuse them later; entries dedupe by content host-side. */
|
|
1259
|
+
const uploadGalleryFiles = async (files: File[]): Promise<void> => {
|
|
1260
|
+
if (files.length === 0 || galleryAdding) return
|
|
1261
|
+
setGalleryAdding(true)
|
|
1262
|
+
try {
|
|
1263
|
+
let entries: HistoryEntry[] | undefined
|
|
1264
|
+
for (const file of files) {
|
|
1265
|
+
const dataUrl = await new Promise<string>((resolve, reject) => {
|
|
1266
|
+
const reader = new FileReader()
|
|
1267
|
+
reader.onload = () => resolve(String(reader.result))
|
|
1268
|
+
reader.onerror = () => reject(new Error(tt('canvas.dropSub')))
|
|
1269
|
+
reader.readAsDataURL(file)
|
|
1270
|
+
})
|
|
1271
|
+
const separator = dataUrl.indexOf(',')
|
|
1272
|
+
const result = await api.galleryAppend({
|
|
1273
|
+
id: '',
|
|
1274
|
+
createdAt: Date.now(),
|
|
1275
|
+
mode: 'text',
|
|
1276
|
+
model: tt('gallery.uploadModel'),
|
|
1277
|
+
prompt: file.name.replace(/\.[a-z0-9]+$/i, ''),
|
|
1278
|
+
size: 'auto',
|
|
1279
|
+
quality: 'auto',
|
|
1280
|
+
detail: '',
|
|
1281
|
+
n: 1,
|
|
1282
|
+
images: [{ b64: separator >= 0 ? dataUrl.slice(separator + 1) : dataUrl, mime: file.type || 'image/png' }],
|
|
1283
|
+
})
|
|
1284
|
+
entries = result.entries
|
|
1285
|
+
}
|
|
1286
|
+
if (entries !== undefined) {
|
|
1287
|
+
setGallery(entries)
|
|
1288
|
+
setGalleryMessage(tt('gallery.uploaded'))
|
|
1289
|
+
window.setTimeout(() => { setGalleryMessage(null) }, 2200)
|
|
1290
|
+
}
|
|
1291
|
+
} catch (caught) {
|
|
1292
|
+
setError(errorMessage(caught))
|
|
1293
|
+
} finally {
|
|
1294
|
+
setGalleryAdding(false)
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1256
1298
|
/** Put a generated image into the native conversation composer. */
|
|
1257
1299
|
const addImageToConversation = async (image: GeneratedImage, index: number, actionKey: number | string = index): Promise<void> => {
|
|
1258
1300
|
if (addingToConversation !== null) return
|
|
@@ -1439,7 +1481,15 @@ export function ImageGenPanel(props: {
|
|
|
1439
1481
|
})
|
|
1440
1482
|
}
|
|
1441
1483
|
|
|
1442
|
-
|
|
1484
|
+
// Prompt hard limit of the model the generate button will actually use
|
|
1485
|
+
// (mirrors handleGenerate's pick). Over the limit the counter turns red and
|
|
1486
|
+
// the button locks — the engine would fast-fail anyway, but the user sees
|
|
1487
|
+
// why before spending a click.
|
|
1488
|
+
const activeModel = modeModels.includes(model) ? model : modeModels[0] ?? ''
|
|
1489
|
+
const promptLimit = promptCharLimit(activeModel)
|
|
1490
|
+
const promptOverLimit = promptLimit !== null && prompt.trim().length >= promptLimit
|
|
1491
|
+
|
|
1492
|
+
const generateDisabled = submitting || modeModels.length === 0 || promptOverLimit
|
|
1443
1493
|
const ecommerceSlots = ecommerce.slots.filter(slot => slot.enabled && slot.count > 0)
|
|
1444
1494
|
const ecommerceTotal = ecommerceSlots.reduce((total, slot) => total + slot.count, 0)
|
|
1445
1495
|
const ecommerceGenerateDisabled = submitting || ecommerceGenerating || ecommerceSlots.length === 0 || ecommerce.productName.trim() === '' || (ecommerce.language === 'custom' && effectiveEcommerceLanguage(ecommerce) === '')
|
|
@@ -1626,8 +1676,8 @@ export function ImageGenPanel(props: {
|
|
|
1626
1676
|
</span>
|
|
1627
1677
|
</button>
|
|
1628
1678
|
<span className={css.historyActions}>
|
|
1629
|
-
{entry.images.length > 0 ? (
|
|
1630
|
-
<button
|
|
1679
|
+
{entry.images.length > 0 ? (
|
|
1680
|
+
<button
|
|
1631
1681
|
type="button"
|
|
1632
1682
|
className={css.historyIconAction}
|
|
1633
1683
|
disabled={conversationBusy}
|
|
@@ -1650,21 +1700,21 @@ export function ImageGenPanel(props: {
|
|
|
1650
1700
|
onClick={() => { void addHistoryEntryToGallery(entry) }}
|
|
1651
1701
|
>
|
|
1652
1702
|
<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
|
|
1703
|
+
</button>
|
|
1704
|
+
) : null}
|
|
1705
|
+
{entry.images.length > 0 ? (
|
|
1706
|
+
<button
|
|
1707
|
+
type="button"
|
|
1708
|
+
className={css.historyIconAction}
|
|
1709
|
+
title={tt('canvas.addToCanvas')}
|
|
1710
|
+
aria-label={tt('canvas.addToCanvas')}
|
|
1711
|
+
data-history-add-canvas=""
|
|
1712
|
+
onClick={() => { addEntryToCanvas('history', entry.id, 0) }}
|
|
1713
|
+
>
|
|
1714
|
+
<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>
|
|
1715
|
+
</button>
|
|
1716
|
+
) : null}
|
|
1717
|
+
<button
|
|
1668
1718
|
type="button"
|
|
1669
1719
|
className={css.historyIconAction}
|
|
1670
1720
|
data-danger
|
|
@@ -1683,6 +1733,9 @@ export function ImageGenPanel(props: {
|
|
|
1683
1733
|
</aside>
|
|
1684
1734
|
)
|
|
1685
1735
|
|
|
1736
|
+
const isGallery = workspace === 'normal' && tab === 'gallery'
|
|
1737
|
+
const isGeneration = workspace === 'normal' && tab !== 'gallery'
|
|
1738
|
+
|
|
1686
1739
|
return (
|
|
1687
1740
|
<div className={css.panel}>
|
|
1688
1741
|
<header className={css.panelHeader}>
|
|
@@ -1699,12 +1752,12 @@ export function ImageGenPanel(props: {
|
|
|
1699
1752
|
<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>
|
|
1700
1753
|
</a>
|
|
1701
1754
|
</span>
|
|
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>
|
|
1755
|
+
<nav className={css.topNav} role="tablist" aria-label={tt('workspace.label')}>
|
|
1756
|
+
<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>
|
|
1757
|
+
<button type="button" className={css.topNavItem} data-active={workspace === 'normal' && tab === 'gallery' ? '' : undefined} onClick={() => { openTab('gallery') }}>{tt('gallery.title')}</button>
|
|
1758
|
+
<span className={css.topNavDivider} aria-hidden="true" />
|
|
1759
|
+
<button type="button" className={css.topNavItem} data-active={workspace === 'canvas' ? '' : undefined} onClick={() => { setWorkspace('canvas') }}>{tt('workspace.canvas')}</button>
|
|
1760
|
+
<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>
|
|
1708
1761
|
</nav>
|
|
1709
1762
|
<span className={css.panelHeaderActions}>
|
|
1710
1763
|
<button
|
|
@@ -1746,7 +1799,7 @@ export function ImageGenPanel(props: {
|
|
|
1746
1799
|
|
|
1747
1800
|
<div className={css.studio}>
|
|
1748
1801
|
{/* ------------------------------- left history + generation workspace */}
|
|
1749
|
-
<div className={css.generation} data-workspace={workspace}>
|
|
1802
|
+
<div className={css.generation} data-workspace={workspace}>
|
|
1750
1803
|
{/* ------------------------------------------------ config sidebar */}
|
|
1751
1804
|
<aside
|
|
1752
1805
|
ref={configAsideRef}
|
|
@@ -1770,7 +1823,7 @@ export function ImageGenPanel(props: {
|
|
|
1770
1823
|
</svg>
|
|
1771
1824
|
</button>
|
|
1772
1825
|
</div>
|
|
1773
|
-
{
|
|
1826
|
+
{isGallery ? (
|
|
1774
1827
|
<div className={css.galleryFilters}>
|
|
1775
1828
|
<div className={css.galleryFilterHeading}>{tt('gallery.categories')}</div>
|
|
1776
1829
|
{[
|
|
@@ -2000,7 +2053,7 @@ export function ImageGenPanel(props: {
|
|
|
2000
2053
|
</section>
|
|
2001
2054
|
) : null}
|
|
2002
2055
|
|
|
2003
|
-
{tab === 'edit' ? (
|
|
2056
|
+
{isGeneration && tab === 'edit' ? (
|
|
2004
2057
|
<section className={css.card}>
|
|
2005
2058
|
{refImage === null
|
|
2006
2059
|
? (
|
|
@@ -2048,7 +2101,7 @@ export function ImageGenPanel(props: {
|
|
|
2048
2101
|
) : null}
|
|
2049
2102
|
|
|
2050
2103
|
{/* prompt (normal workspace only — ecommerce has its own form) */}
|
|
2051
|
-
{
|
|
2104
|
+
{isGeneration ? (<>
|
|
2052
2105
|
<section className={css.card}>
|
|
2053
2106
|
<textarea
|
|
2054
2107
|
className={css.prompt}
|
|
@@ -2075,7 +2128,15 @@ export function ImageGenPanel(props: {
|
|
|
2075
2128
|
>
|
|
2076
2129
|
{enhancing ? tt('prompt.enhancing') : tt('prompt.enhance')}
|
|
2077
2130
|
</button>
|
|
2078
|
-
<span
|
|
2131
|
+
<span
|
|
2132
|
+
className={css.promptCount}
|
|
2133
|
+
data-over={promptOverLimit ? '' : undefined}
|
|
2134
|
+
title={promptOverLimit ? tt('prompt.overLimit', { limit: promptLimit ?? 0 }) : undefined}
|
|
2135
|
+
>
|
|
2136
|
+
{promptLimit === null
|
|
2137
|
+
? tt('prompt.count', { count: prompt.length })
|
|
2138
|
+
: tt('prompt.countLimit', { count: prompt.length, limit: promptLimit })}
|
|
2139
|
+
</span>
|
|
2079
2140
|
</div>
|
|
2080
2141
|
</section>
|
|
2081
2142
|
|
|
@@ -2172,7 +2233,7 @@ export function ImageGenPanel(props: {
|
|
|
2172
2233
|
)}
|
|
2173
2234
|
</div>
|
|
2174
2235
|
) : null}
|
|
2175
|
-
{
|
|
2236
|
+
{isGeneration ? <label className={css.modelWrap}>
|
|
2176
2237
|
<span className={css.modelLabel}>{tt('model.label')}</span>
|
|
2177
2238
|
<span ref={modelMenuRef} className={css.modelMenu} data-open={modelOpen ? 'true' : 'false'}>
|
|
2178
2239
|
<button
|
|
@@ -2204,8 +2265,8 @@ export function ImageGenPanel(props: {
|
|
|
2204
2265
|
</div>
|
|
2205
2266
|
) : null}
|
|
2206
2267
|
</span>
|
|
2207
|
-
</label>}
|
|
2208
|
-
{
|
|
2268
|
+
</label> : null}
|
|
2269
|
+
{isGeneration ? <div className={css.compareControl}>
|
|
2209
2270
|
<label className={css.compareToggle}>
|
|
2210
2271
|
<input type="checkbox" checked={compareEnabled} onChange={event => { setCompareEnabled(event.target.checked) }} />
|
|
2211
2272
|
<span>{tt('compare.enable')}</span>
|
|
@@ -2221,7 +2282,7 @@ export function ImageGenPanel(props: {
|
|
|
2221
2282
|
</div>
|
|
2222
2283
|
) : null}
|
|
2223
2284
|
</div> : null}
|
|
2224
|
-
{
|
|
2285
|
+
{isGeneration ? <Button
|
|
2225
2286
|
variant="primary"
|
|
2226
2287
|
size="md"
|
|
2227
2288
|
className={css.generateButton}
|
|
@@ -2236,26 +2297,26 @@ export function ImageGenPanel(props: {
|
|
|
2236
2297
|
) : tt('generate')}
|
|
2237
2298
|
</Button> : null}
|
|
2238
2299
|
</section>
|
|
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 */}
|
|
2257
|
-
<section className={css.canvas} data-gallery={
|
|
2258
|
-
{
|
|
2300
|
+
</aside>
|
|
2301
|
+
|
|
2302
|
+
{workspace === 'canvas' ? (
|
|
2303
|
+
<CanvasWorkspace
|
|
2304
|
+
api={api}
|
|
2305
|
+
imageModels={imageModels}
|
|
2306
|
+
defaultChannelId={defaultChannelId}
|
|
2307
|
+
connected={connected}
|
|
2308
|
+
history={history}
|
|
2309
|
+
gallery={gallery}
|
|
2310
|
+
tasks={tasks}
|
|
2311
|
+
importRequest={canvasImportRequest}
|
|
2312
|
+
onImportRequestHandled={() => { setCanvasImportRequest(undefined) }}
|
|
2313
|
+
onOpenSettings={() => { openSettingsGuide('generation') }}
|
|
2314
|
+
/>
|
|
2315
|
+
) : null}
|
|
2316
|
+
|
|
2317
|
+
{/* ------------------------------------------------------- canvas */}
|
|
2318
|
+
<section className={css.canvas} data-gallery={isGallery ? 'true' : undefined}>
|
|
2319
|
+
{isGallery ? (
|
|
2259
2320
|
<div className={css.galleryWorkspace}>
|
|
2260
2321
|
<header className={css.galleryToolbar}>
|
|
2261
2322
|
<div>
|
|
@@ -2264,6 +2325,19 @@ export function ImageGenPanel(props: {
|
|
|
2264
2325
|
</div>
|
|
2265
2326
|
<div className={css.galleryToolbarActions}>
|
|
2266
2327
|
<input className={css.gallerySearch} value={galleryQuery} onChange={event => { setGalleryQuery(event.target.value) }} placeholder={tt('gallery.search')} aria-label={tt('gallery.search')} />
|
|
2328
|
+
<button type="button" className={css.gallerySelectMode} disabled={galleryAdding} title={tt('gallery.uploadHint')} onClick={() => galleryUploadRef.current?.click()}>{galleryAdding ? '…' : tt('gallery.upload')}</button>
|
|
2329
|
+
<input
|
|
2330
|
+
ref={galleryUploadRef}
|
|
2331
|
+
type="file"
|
|
2332
|
+
accept="image/png,image/jpeg,image/webp,image/gif"
|
|
2333
|
+
multiple
|
|
2334
|
+
hidden
|
|
2335
|
+
onChange={event => {
|
|
2336
|
+
const files = [...(event.target.files ?? [])]
|
|
2337
|
+
event.target.value = ''
|
|
2338
|
+
if (files.length > 0) void uploadGalleryFiles(files)
|
|
2339
|
+
}}
|
|
2340
|
+
/>
|
|
2267
2341
|
<button type="button" className={css.gallerySelectMode} data-active={gallerySelecting ? '' : undefined} aria-pressed={gallerySelecting} onClick={() => { setGallerySelecting(previous => !previous) }}>
|
|
2268
2342
|
{gallerySelecting ? tt('gallery.selectionDone') : tt('gallery.select')}
|
|
2269
2343
|
</button>
|
|
@@ -2310,7 +2384,7 @@ export function ImageGenPanel(props: {
|
|
|
2310
2384
|
<span className={css.galleryBadge}>{entry.mode === 'edit' ? tt('mode.edit') : tt('mode.text')}</span>
|
|
2311
2385
|
</button>
|
|
2312
2386
|
<div className={css.galleryCardActions}>
|
|
2313
|
-
<button
|
|
2387
|
+
<button
|
|
2314
2388
|
type="button"
|
|
2315
2389
|
className={css.galleryCardAction}
|
|
2316
2390
|
data-gallery-add-conversation=""
|
|
@@ -2319,19 +2393,19 @@ export function ImageGenPanel(props: {
|
|
|
2319
2393
|
onClick={(event) => { event.stopPropagation(); void addGalleryEntryToConversation(entry) }}
|
|
2320
2394
|
>
|
|
2321
2395
|
<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>
|
|
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>
|
|
2396
|
+
{galleryConversationAddingId === entry.id || addingToConversation === `gallery:${entry.id}` ? tt('conversation.adding') : tt('conversation.add')}
|
|
2397
|
+
</button>
|
|
2398
|
+
<button
|
|
2399
|
+
type="button"
|
|
2400
|
+
className={css.galleryCardAction}
|
|
2401
|
+
data-gallery-add-canvas=""
|
|
2402
|
+
title={tt('canvas.addToCanvas')}
|
|
2403
|
+
onClick={(event) => { event.stopPropagation(); addEntryToCanvas('gallery', entry.id, 0) }}
|
|
2404
|
+
>
|
|
2405
|
+
<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>
|
|
2406
|
+
{tt('canvas.addToCanvas')}
|
|
2407
|
+
</button>
|
|
2408
|
+
</div>
|
|
2335
2409
|
<div className={css.galleryCardFooter}>
|
|
2336
2410
|
<span className={css.galleryAvatar}>{entry.model.toLowerCase().startsWith('nanobanana') ? 'N' : entry.model.toLowerCase().startsWith('seedream') ? 'S' : entry.model.startsWith('grok') ? 'G' : 'D'}</span>
|
|
2337
2411
|
<span className={css.galleryCardInfo}>
|
|
@@ -2426,7 +2500,7 @@ export function ImageGenPanel(props: {
|
|
|
2426
2500
|
)}
|
|
2427
2501
|
</div>
|
|
2428
2502
|
) : null}
|
|
2429
|
-
{
|
|
2503
|
+
{!isGallery && tasks.length > 0 ? (
|
|
2430
2504
|
<section className={css.taskTray} data-open={taskTrayOpen ? 'true' : 'false'} aria-label={tt('tasks.title')}>
|
|
2431
2505
|
<header className={css.taskTrayHeader}>
|
|
2432
2506
|
<button type="button" className={css.taskTrayToggle} aria-expanded={taskTrayOpen} onClick={() => { setTaskTrayOpen(open => !open) }}>
|
|
@@ -2474,7 +2548,7 @@ export function ImageGenPanel(props: {
|
|
|
2474
2548
|
</div>
|
|
2475
2549
|
</section>
|
|
2476
2550
|
) : null}
|
|
2477
|
-
{generating && comparison === null &&
|
|
2551
|
+
{generating && comparison === null && isGeneration ? (
|
|
2478
2552
|
<div className={css.canvasState} data-generation-state={activeTask?.status ?? 'submitting'} role="status">
|
|
2479
2553
|
<span className={css.bigSpinner} />
|
|
2480
2554
|
<span className={css.canvasStateTitle}>
|
|
@@ -2496,7 +2570,7 @@ export function ImageGenPanel(props: {
|
|
|
2496
2570
|
<div className={css.canvasError} role="alert">{tt('canvas.error', { error })}</div>
|
|
2497
2571
|
) : null}
|
|
2498
2572
|
|
|
2499
|
-
{!generating && !error && images.length === 0
|
|
2573
|
+
{isGeneration && !generating && !error && images.length === 0 ? (
|
|
2500
2574
|
<InspirationGallery
|
|
2501
2575
|
api={api}
|
|
2502
2576
|
onUse={(text) => {
|
|
@@ -2506,7 +2580,7 @@ export function ImageGenPanel(props: {
|
|
|
2506
2580
|
/>
|
|
2507
2581
|
) : null}
|
|
2508
2582
|
|
|
2509
|
-
{!generating && images.length > 0
|
|
2583
|
+
{isGeneration && !generating && images.length > 0 ? (
|
|
2510
2584
|
<div className={css.canvasBody}>
|
|
2511
2585
|
<div className={css.canvasMeta}>
|
|
2512
2586
|
<span>{tt('canvas.images', { count: images.length })}</span>
|