@dickpy/dsh-imagegen 1.5.1 → 1.5.3

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/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 { CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult, type UpdateInfo } from '../protocol.ts'
6
+ import { CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATE_FAVORITES_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateCase, type TemplateFavorite, type TemplateListResult, type TemplateRefreshResult, type TemplateSample, type UpdateInfo } from '../protocol.ts'
7
7
 
8
8
  /** Error carrying the route's JSON error message. */
9
9
  export class ImageGenApiError extends Error {
@@ -181,11 +181,12 @@ export class ImageGenApi {
181
181
  return (await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)).entries
182
182
  }
183
183
 
184
- /** Fetch the prompt-template library (bundled snapshot or refreshed copy). */
185
- async templatesList(): Promise<TemplateListResult> {
186
- const response = await fetch(TEMPLATES_API.list, { method: 'POST' })
184
+ /** Fetch one template source's list (bundled snapshot or refreshed copy). */
185
+ async templatesList(sourceId: string): Promise<TemplateListResult> {
186
+ const response = await fetch(TEMPLATES_API.list, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source: sourceId }) })
187
187
  const body = await readEnvelope<TemplateListResult & { ok: true }>(response)
188
188
  return {
189
+ sourceId: body.sourceId,
189
190
  cases: body.cases,
190
191
  total: body.total,
191
192
  origin: body.origin,
@@ -194,10 +195,42 @@ export class ImageGenApi {
194
195
  }
195
196
  }
196
197
 
197
- /** Re-download the template library from the upstream mirror (host-side). */
198
- async templatesRefresh(): Promise<TemplateRefreshResult> {
199
- const response = await fetch(TEMPLATES_API.refresh, { method: 'POST' })
198
+ /** Re-download one template source's list from its upstream mirror (host-side). */
199
+ async templatesRefresh(sourceId: string): Promise<TemplateRefreshResult> {
200
+ const response = await fetch(TEMPLATES_API.refresh, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source: sourceId }) })
200
201
  const body = await readEnvelope<TemplateRefreshResult & { ok: true }>(response)
201
- return { total: body.total, fetchedAt: body.fetchedAt }
202
+ return { sourceId: body.sourceId, total: body.total, fetchedAt: body.fetchedAt }
203
+ }
204
+
205
+ /** Draw random cases across every source (studio inspiration wall). */
206
+ async templatesSample(count: number): Promise<TemplateSample[]> {
207
+ const response = await fetch(TEMPLATES_API.sample, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ count }) })
208
+ return (await readEnvelope<{ ok: true; samples: TemplateSample[] }>(response)).samples
209
+ }
210
+
211
+ /** List the host-persisted template favorites. */
212
+ async favoritesList(): Promise<TemplateFavorite[]> {
213
+ const response = await fetch(TEMPLATE_FAVORITES_API.list, { method: 'POST' })
214
+ return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
215
+ }
216
+
217
+ /** Star one template (the host keeps a full case snapshot). */
218
+ async favoritesAdd(sourceId: string, item: TemplateCase): Promise<TemplateFavorite[]> {
219
+ const response = await fetch(TEMPLATE_FAVORITES_API.add, {
220
+ method: 'POST',
221
+ headers: { 'content-type': 'application/json' },
222
+ body: JSON.stringify({ source: sourceId, case: item }),
223
+ })
224
+ return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
225
+ }
226
+
227
+ /** Unstar one template by its favorites key. */
228
+ async favoritesRemove(key: string): Promise<TemplateFavorite[]> {
229
+ const response = await fetch(TEMPLATE_FAVORITES_API.remove, {
230
+ method: 'POST',
231
+ headers: { 'content-type': 'application/json' },
232
+ body: JSON.stringify({ key }),
233
+ })
234
+ return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
202
235
  }
203
236
  }
@@ -1,33 +1,71 @@
1
- /**
2
- * Shared panel helpers: the active-dictionary pick (document-language based,
3
- * dsh-ssh precedent) bound to the dsh-imagegen interpolator, plus a small
4
- * error-message extractor. All copy stays in the locale dictionaries.
5
- */
6
-
7
- import { en, zh, type ImageGenKey } from './locales.ts'
8
-
9
- /** Template values accepted by the interpolator. */
10
- export type TranslateValues = Record<string, string | number>
11
-
12
- /** Active dictionary, picked by the document language at call time. */
13
- export function dictionary(): Record<string, string> {
14
- const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
15
- return lang.toLowerCase().startsWith('en') ? { ...en } : { ...zh }
16
- }
17
-
18
- /** Translate a key with optional {name} template params (current language). */
19
- export function tt(key: ImageGenKey, values?: TranslateValues): string {
20
- const text = dictionary()[key] ?? key
21
- if (values === undefined) return text
22
- let rendered = text
23
- for (const [name, value] of Object.entries(values)) {
24
- rendered = rendered.replaceAll(`{${name}}`, String(value))
25
- }
26
- return rendered
27
- }
28
-
29
- /** Human-readable error text from an unknown thrown value. */
30
- export function errorMessage(error: unknown): string {
31
- if (error instanceof Error) return error.message
32
- return String(error)
33
- }
1
+ /**
2
+ * Shared panel helpers: the active-dictionary pick bound to the dsh-imagegen
3
+ * interpolator, the plugin locale that follows the DSH interface language
4
+ * (bridged in client/index.ts from ctx.locale the plugin ships zh / en / ru
5
+ * and registers itself as a DSH language pack for Русский), plus a small
6
+ * error-message extractor. All copy stays in the locale dictionaries.
7
+ */
8
+
9
+ import { en, ru, zh, type ImageGenKey } from './locales.ts'
10
+
11
+ /** Template values accepted by the interpolator. */
12
+ export type TranslateValues = Record<string, string | number>
13
+
14
+ /** Languages with a shipped dictionary. */
15
+ export type ImageGenLanguage = 'zh' | 'en' | 'ru'
16
+
17
+ const DICTIONARIES: Record<ImageGenLanguage, Record<string, string>> = { zh, en, ru }
18
+
19
+ /** The active DSH locale mapped onto our dictionary (module-level, one value per app). */
20
+ let activeLocale: ImageGenLanguage = 'zh'
21
+
22
+ /** Bumped on every locale change; useSyncExternalStore version. */
23
+ let languageVersion = 0
24
+
25
+ const languageListeners = new Set<() => void>()
26
+
27
+ /**
28
+ * Adopt the DSH interface language. Unknown ids (future language packs)
29
+ * resolve to English the same per-key fallback convention the host locale
30
+ * chain uses.
31
+ */
32
+ export function applyHostLocale(id: unknown): void {
33
+ const next: ImageGenLanguage = id === 'zh' || id === 'ru' ? id : 'en'
34
+ if (next === activeLocale) return
35
+ activeLocale = next
36
+ languageVersion += 1
37
+ for (const listener of [...languageListeners]) listener()
38
+ }
39
+
40
+ /** Monotonic version of the active locale (external-store snapshot). */
41
+ export function getImageGenLanguageVersion(): number {
42
+ return languageVersion
43
+ }
44
+
45
+ /** Observe locale changes; returns the unsubscriber. */
46
+ export function subscribeImageGenLanguage(listener: () => void): () => void {
47
+ languageListeners.add(listener)
48
+ return () => { languageListeners.delete(listener) }
49
+ }
50
+
51
+ /** Active dictionary for the current DSH language. */
52
+ export function dictionary(): Record<string, string> {
53
+ return DICTIONARIES[activeLocale]
54
+ }
55
+
56
+ /** Translate a key with optional {name} template params (current language). */
57
+ export function tt(key: ImageGenKey, values?: TranslateValues): string {
58
+ const text = dictionary()[key] ?? key
59
+ if (values === undefined) return text
60
+ let rendered = text
61
+ for (const [name, value] of Object.entries(values)) {
62
+ rendered = rendered.replaceAll(`{${name}}`, String(value))
63
+ }
64
+ return rendered
65
+ }
66
+
67
+ /** Human-readable error text from an unknown thrown value. */
68
+ export function errorMessage(error: unknown): string {
69
+ if (error instanceof Error) return error.message
70
+ return String(error)
71
+ }
@@ -11,18 +11,18 @@
11
11
  * whole boot when a plugin apply throws, and an external plugin must not take
12
12
  * the GUI down.
13
13
  */
14
- import type { Context as ClientContext } from '@deepseek-ai/cordis'
15
- import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
14
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
15
+ import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
16
16
  import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
17
17
  // Type-only: pulls the locale plugin's Context merge (ctx.locale).
18
- import type {} from '@deepseek-ai/dsh-client-locale/client'
19
- import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
20
- // Type-only: pulls the LocaleNamespaceMap merge table.
18
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
19
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
20
+ // Type-only: pulls the LocaleNamespaceMap merge table.
21
21
  import type {} from '@deepseek-ai/dsh-client-ui-slots'
22
22
  import { ImageGenApi } from './api.ts'
23
23
  import { ImageGenController } from './controller.ts'
24
- import { tt } from './helpers.ts'
25
- import { en, zh, type ImageGenKey } from './locales.ts'
24
+ import { tt, applyHostLocale } from './helpers.ts'
25
+ import { en, ru, zh, type ImageGenKey } from './locales.ts'
26
26
  import { mountPanel } from './mount.tsx'
27
27
  import { mountSidebarEntry } from './sidebar-entry.ts'
28
28
  import { ImageGenSettingsCard, ImageGenSettingsCardController } from './SettingsCard.tsx'
@@ -68,7 +68,39 @@ export const inject = ['slots', 'locale', 'connection', 'sessions', 'conversatio
68
68
  * @param ctx - client root context (services: slots, locale, connection).
69
69
  */
70
70
  export function apply(ctx: ClientContext): void {
71
+ // The host locale service only knows zh/en dictionaries (its type is
72
+ // fixed); ru rides the untyped single-locale registration instead.
71
73
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-imagegen: dictionaries')
74
+ // Russian ships as a language pack: the dictionary lands in this plugin's
75
+ // namespace, and Русский joins the shared DSH language catalog (Settings →
76
+ // General → Language) with per-key fallback to English for host copy.
77
+ // Registration order/aggregation between register(NS, 'ru', …) and
78
+ // addLanguage differs across host builds and a duplicate throws — these
79
+ // surfaces must degrade silently, never fail the GUI boot.
80
+ ctx.effect(() => {
81
+ try {
82
+ return ctx.locale.register(NS, 'ru', ru)
83
+ } catch (error) {
84
+ console.warn('[dsh-imagegen] ru dictionary not registered:', error)
85
+ return () => {}
86
+ }
87
+ }, 'dsh-imagegen: ru dictionary')
88
+ ctx.effect(() => {
89
+ try {
90
+ if (ctx.locale.getLocale().locales.some(locale => locale.id === 'ru')) return () => {}
91
+ return ctx.locale.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })
92
+ } catch (error) {
93
+ console.warn('[dsh-imagegen] ru language not added to the catalog:', error)
94
+ return () => {}
95
+ }
96
+ }, 'dsh-imagegen: ru language pack')
97
+ // Every plugin surface renders through tt(); bridge DSH locale switches
98
+ // into it so the whole plugin follows the interface language.
99
+ ctx.effect(() => {
100
+ const applyLocale = (): void => { applyHostLocale(ctx.locale.getLocale().active) }
101
+ applyLocale()
102
+ return ctx.locale.subscribe(applyLocale)
103
+ }, 'dsh-imagegen: follow host locale')
72
104
  registerImageToolviews(ctx)
73
105
 
74
106
  const connection = ctx.get('connection') as ConnectionHandle | undefined
@@ -119,6 +151,26 @@ export function apply(ctx: ClientContext): void {
119
151
  tt('entry.tooltip'),
120
152
  ))
121
153
  disposers.push(mountPanel(controller, api, scope, { sessions, conversation }))
154
+ // The imperative sidebar tabs render their labels once; relabel them on
155
+ // every DSH language switch so the entry follows the interface too.
156
+ disposers.push(ctx.locale.subscribe(() => {
157
+ const root = document.querySelector('[data-dsh-imagegen-sidebar-root]')
158
+ if (root === null) return
159
+ const labels: Array<[string, string, string]> = [
160
+ ['new-session', tt('entry.newSession'), tt('entry.newSessionTooltip')],
161
+ ['image', tt('entry.image'), tt('entry.tooltip')],
162
+ ]
163
+ for (const [tab, label, tooltip] of labels) {
164
+ const button = root.querySelector<HTMLButtonElement>(`[data-dsh-imagegen-tab="${tab}"]`)
165
+ if (button === null) continue
166
+ button.setAttribute('aria-label', label)
167
+ button.setAttribute('title', tooltip)
168
+ const labelSpan = button.querySelector('span:nth-child(2)')
169
+ if (labelSpan !== null) labelSpan.textContent = label
170
+ }
171
+ const tablist = root.querySelector<HTMLDivElement>('[role="tablist"][data-dsh-imagegen-session-tabs]')
172
+ tablist?.setAttribute('aria-label', tt('entry.tooltip'))
173
+ }))
122
174
  } catch (error) {
123
175
  // DOM failures degrade the studio, never the GUI.
124
176
  console.warn('[dsh-imagegen] mount failed:', error)
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Inspiration-wall styles for the studio's empty canvas. Same conventions as
3
+ * panel.module.css: dsh --dsw-* tokens, scoped to this component's subtree.
4
+ */
5
+
6
+ .wrap {
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: center;
10
+ gap: 16px;
11
+ width: min(880px, 100%);
12
+ margin: auto;
13
+ padding: 24px;
14
+ font-family: var(--dsw-font-family);
15
+ }
16
+
17
+ .title {
18
+ font-size: 15px;
19
+ font-weight: 650;
20
+ color: var(--dsw-alias-label-primary);
21
+ }
22
+
23
+ /* Fallback (sample failed / libraries empty): mirrors the panel's plain
24
+ canvas empty state so the swap is invisible. */
25
+ .emptyIcon {
26
+ display: inline-flex;
27
+ color: var(--dsw-alias-label-dimmed);
28
+ }
29
+
30
+ .emptyTitle {
31
+ margin-top: -6px;
32
+ font-size: 15px;
33
+ font-weight: 650;
34
+ color: var(--dsw-alias-label-primary);
35
+ }
36
+
37
+ .emptyHint {
38
+ font-size: 12.5px;
39
+ color: var(--dsw-alias-label-tertiary);
40
+ }
41
+
42
+ .grid {
43
+ display: grid;
44
+ grid-template-columns: repeat(4, 1fr);
45
+ gap: 12px;
46
+ width: 100%;
47
+ }
48
+
49
+ /* Narrow canvases: fold the wall down instead of shrinking tiles into slivers. */
50
+ @media (max-width: 900px) {
51
+ .grid {
52
+ grid-template-columns: repeat(3, 1fr);
53
+ }
54
+ }
55
+
56
+ @media (max-width: 560px) {
57
+ .grid {
58
+ grid-template-columns: repeat(2, 1fr);
59
+ }
60
+ }
61
+
62
+ .tile {
63
+ position: relative;
64
+ display: block;
65
+ width: 100%;
66
+ aspect-ratio: 1 / 1;
67
+ padding: 0;
68
+ overflow: hidden;
69
+ border: 1px solid var(--dsw-alias-border-l1);
70
+ border-radius: 12px;
71
+ background: var(--dsw-alias-bg-layer-1);
72
+ cursor: pointer;
73
+ transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
74
+ }
75
+
76
+ .tile:hover {
77
+ border-color: var(--dsw-alias-brand-primary);
78
+ transform: translateY(-2px);
79
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
80
+ }
81
+
82
+ .thumbWrap {
83
+ position: absolute;
84
+ inset: 0;
85
+ display: block;
86
+ }
87
+
88
+ .thumb {
89
+ display: block;
90
+ width: 100%;
91
+ height: 100%;
92
+ object-fit: cover;
93
+ }
94
+
95
+ .thumbFallback {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ width: 100%;
100
+ height: 100%;
101
+ padding: 6px;
102
+ overflow: hidden;
103
+ background: var(--dsw-alias-bg-layer-2);
104
+ color: var(--dsw-alias-label-tertiary);
105
+ font-size: 11px;
106
+ line-height: 1.4;
107
+ text-align: center;
108
+ }
109
+
110
+ .thumbTitle {
111
+ position: absolute;
112
+ inset: auto 0 0 0;
113
+ padding: 14px 8px 6px;
114
+ overflow: hidden;
115
+ text-overflow: ellipsis;
116
+ white-space: nowrap;
117
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.55));
118
+ color: #ffffff;
119
+ font-size: 10.5px;
120
+ text-align: center;
121
+ opacity: 0;
122
+ transition: opacity 0.15s ease;
123
+ pointer-events: none;
124
+ }
125
+
126
+ .tile:hover .thumbTitle {
127
+ opacity: 1;
128
+ }
129
+
130
+ .spinner {
131
+ width: 20px;
132
+ height: 20px;
133
+ border: 2px solid var(--dsw-alias-border-l2);
134
+ border-top-color: var(--dsw-alias-brand-primary);
135
+ border-radius: 50%;
136
+ animation: dsh-imagegen-inspiration-spin 0.9s linear infinite;
137
+ }
138
+
139
+ @keyframes dsh-imagegen-inspiration-spin {
140
+ to { transform: rotate(360deg); }
141
+ }
142
+
143
+ /* Touch shells have no hover: keep titles visible. */
144
+ @media (hover: none) {
145
+ .thumbTitle {
146
+ opacity: 1;
147
+ }
148
+ }