@linxin666/dsh-pet 0.1.1
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/LICENSE +29 -0
- package/README.md +106 -0
- package/assets/whale/pet.json +7 -0
- package/assets/whale/previews/failed.gif +0 -0
- package/assets/whale/previews/idle.gif +0 -0
- package/assets/whale/previews/jumping.gif +0 -0
- package/assets/whale/previews/review.gif +0 -0
- package/assets/whale/previews/running-left.gif +0 -0
- package/assets/whale/previews/running-right.gif +0 -0
- package/assets/whale/previews/running.gif +0 -0
- package/assets/whale/previews/waiting.gif +0 -0
- package/assets/whale/previews/waving.gif +0 -0
- package/assets/whale/spritesheet.webp +0 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +1515 -0
- package/lib/index.js +642 -0
- package/lib/invariant.js +34 -0
- package/lib/types/affinity.d.ts +83 -0
- package/lib/types/affinity.d.ts.map +1 -0
- package/lib/types/client/PetDockEntry.d.ts +44 -0
- package/lib/types/client/PetDockEntry.d.ts.map +1 -0
- package/lib/types/client/PetSettingsCard.d.ts +67 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -0
- package/lib/types/client/PluginSettingsCard.d.ts +78 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
- package/lib/types/client/WhalePet.d.ts +49 -0
- package/lib/types/client/WhalePet.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +46 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +101 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/pet-store.d.ts +49 -0
- package/lib/types/client/pet-store.d.ts.map +1 -0
- package/lib/types/client/settings-form.d.ts +119 -0
- package/lib/types/client/settings-form.d.ts.map +1 -0
- package/lib/types/client/slots-augment.d.ts +19 -0
- package/lib/types/client/slots-augment.d.ts.map +1 -0
- package/lib/types/client/spritesheet.d.ts +69 -0
- package/lib/types/client/spritesheet.d.ts.map +1 -0
- package/lib/types/index.d.ts +44 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/invariant.d.ts +10 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/persist.d.ts +45 -0
- package/lib/types/persist.d.ts.map +1 -0
- package/lib/types/routes.d.ts +22 -0
- package/lib/types/routes.d.ts.map +1 -0
- package/lib/types/service.d.ts +161 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/state.d.ts +80 -0
- package/lib/types/state.d.ts.map +1 -0
- package/lib/types/treats.d.ts +59 -0
- package/lib/types/treats.d.ts.map +1 -0
- package/package.json +100 -0
- package/src/affinity.test.ts +74 -0
- package/src/affinity.ts +144 -0
- package/src/client/PetDockEntry.tsx +95 -0
- package/src/client/PetSettingsCard.tsx +186 -0
- package/src/client/PluginSettingsCard.tsx +207 -0
- package/src/client/WhalePet.tsx +342 -0
- package/src/client/css-modules.d.ts +6 -0
- package/src/client/index.ts +261 -0
- package/src/client/locales.ts +108 -0
- package/src/client/pet-store.ts +80 -0
- package/src/client/pet.module.css +185 -0
- package/src/client/settings-card.module.css +277 -0
- package/src/client/settings-form.ts +294 -0
- package/src/client/slots-augment.ts +27 -0
- package/src/client/spritesheet.ts +138 -0
- package/src/index.ts +148 -0
- package/src/invariant.ts +40 -0
- package/src/persist.test.ts +96 -0
- package/src/persist.ts +128 -0
- package/src/routes.ts +171 -0
- package/src/service.ts +389 -0
- package/src/state.test.ts +65 -0
- package/src/state.ts +156 -0
- package/src/treats.test.ts +86 -0
- package/src/treats.ts +101 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-pet browser half — registers the whale-girl into the conversation
|
|
3
|
+
* input selector row (the same every-phase row the git branch chip uses) and
|
|
4
|
+
* drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll the
|
|
5
|
+
* host snapshot (~800 ms), forward interactions, persist drag positions. The
|
|
6
|
+
* row anchor mounts the floating pet via portal; when the pet is hidden the
|
|
7
|
+
* anchor becomes the summon button. Anchoring in the selector row (rather
|
|
8
|
+
* than the session-only composer dock band) keeps the pet floating on the
|
|
9
|
+
* new-conversation screen too, where no session exists to scope a slot by.
|
|
10
|
+
* @module @linxin666/dsh-pet/client
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
14
|
+
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
15
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
16
|
+
// Type-only: pulls the settings-surface Context merge (ctx.settingsScope).
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
20
|
+
import type { PetDisplayConfig } from '../persist.ts'
|
|
21
|
+
import type { PetInteractResult, PetStateView } from '../service.ts'
|
|
22
|
+
import type { PetInteraction } from '../affinity.ts'
|
|
23
|
+
import { createPetStore, type PetStoreInstance } from './pet-store.ts'
|
|
24
|
+
import { PetDockEntry, type PetInjected } from './PetDockEntry.tsx'
|
|
25
|
+
import { PetSettingsCard, PetSettingsCardController, type PetSettings } from './PetSettingsCard.tsx'
|
|
26
|
+
import { NS, en, zh } from './locales.ts'
|
|
27
|
+
|
|
28
|
+
/** The host pet API as the browser sees it (same-origin JSON endpoints). */
|
|
29
|
+
interface PetHttpApi {
|
|
30
|
+
state(): Promise<PetStateView>
|
|
31
|
+
interact(kind: PetInteraction): Promise<PetInteractResult>
|
|
32
|
+
setVisible(visible: boolean): Promise<{ ok: true; display: PetDisplayConfig }>
|
|
33
|
+
setConfig(patch: Partial<PetDisplayConfig>): Promise<{ ok: true; display: PetDisplayConfig }>
|
|
34
|
+
setName(name: string): Promise<{ ok: true; name: string } | { ok: false; error: string }>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
|
|
38
|
+
async function petFetch<T>(path: string, body?: unknown): Promise<T> {
|
|
39
|
+
const response = await fetch(path, body === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'content-type': 'application/json' },
|
|
44
|
+
body: JSON.stringify(body),
|
|
45
|
+
})
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`pet ${path} failed: ${response.status}`)
|
|
48
|
+
}
|
|
49
|
+
return (await response.json()) as T
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The live host API instance (always defined; failures surface per call). */
|
|
53
|
+
const petApi: PetHttpApi = {
|
|
54
|
+
state: () => petFetch('/api/pet/state'),
|
|
55
|
+
interact: (kind) => petFetch('/api/pet/interact', { kind }),
|
|
56
|
+
setVisible: (visible) => petFetch('/api/pet/set-visible', { visible }),
|
|
57
|
+
setConfig: (patch) => petFetch('/api/pet/set-config', patch),
|
|
58
|
+
setName: (name) => petFetch('/api/pet/set-name', { name }),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Poll interval for the host snapshot. */
|
|
62
|
+
const POLL_MS = 800
|
|
63
|
+
|
|
64
|
+
/** Settings namespace the pet settings card edits (the Host plugin registers it). */
|
|
65
|
+
const PET_SETTINGS_NS = 'pet'
|
|
66
|
+
|
|
67
|
+
/** Required services. */
|
|
68
|
+
export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote']
|
|
69
|
+
|
|
70
|
+
/** Re-exported for consumers that type against the injected face. */
|
|
71
|
+
export type { PetInjected, PetDockEntryProps } from './PetDockEntry.tsx'
|
|
72
|
+
export type { PetUiState, PetFeedback } from './pet-store.ts'
|
|
73
|
+
export type { PetSettingsCardFace, PetSettingsCardState } from './PetSettingsCard.tsx'
|
|
74
|
+
|
|
75
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
76
|
+
interface SlotMap {
|
|
77
|
+
/**
|
|
78
|
+
* The child slot the Web UI plugin group declares; this card registers
|
|
79
|
+
* into the group instead of the top-level `settings.plugin.item` list.
|
|
80
|
+
* Spelled here with the same shape so this package can register without
|
|
81
|
+
* depending on the sibling UI package.
|
|
82
|
+
*/
|
|
83
|
+
'web-ui.plugin.item': { kind: 'list'; scope: 'root'; owner: SettingsPluginItemOwnerProps }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Owner share of a plugin card (the group card supplies nothing). */
|
|
88
|
+
export interface SettingsPluginItemOwnerProps {
|
|
89
|
+
/** Marker field: card owner props are intentionally empty. */
|
|
90
|
+
children?: never
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Client plugin body: register dictionaries, mount the dock entry and poll
|
|
95
|
+
* loop while the plugin is enabled, and seat the settings card in the Web UI
|
|
96
|
+
* plugin group.
|
|
97
|
+
* @param ctx - client root context.
|
|
98
|
+
*/
|
|
99
|
+
export function apply(ctx: ClientContext): void {
|
|
100
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'pet: dictionaries')
|
|
101
|
+
|
|
102
|
+
const settingsScope = ctx.settingsScope.bind<PetSettings>({ namespace: PET_SETTINGS_NS })
|
|
103
|
+
const enabled = (): boolean => {
|
|
104
|
+
const snapshot = settingsScope.getSnapshot()
|
|
105
|
+
return snapshot.status === 'ready'
|
|
106
|
+
? snapshot.value?.enabled ?? true
|
|
107
|
+
: snapshot.status === 'unavailable'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Plugin configuration card: one staged form over the `pet` settings
|
|
111
|
+
// namespace, contributed to the Web UI plugin group.
|
|
112
|
+
const petSettings = new PetSettingsCardController(settingsScope)
|
|
113
|
+
ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({
|
|
114
|
+
name: 'web-ui.plugin.item',
|
|
115
|
+
id: 'pet-settings',
|
|
116
|
+
order: 140,
|
|
117
|
+
locale: NS,
|
|
118
|
+
inject: () => petSettings.inject(),
|
|
119
|
+
}, PetSettingsCard))
|
|
120
|
+
|
|
121
|
+
// The dock entry, its store, and the poll loop live while the plugin is
|
|
122
|
+
// enabled; toggling the setting off hides the pet and stops polling.
|
|
123
|
+
let disposeUi: (() => void) | undefined
|
|
124
|
+
const syncUi = (): void => {
|
|
125
|
+
if (enabled() && disposeUi === undefined) {
|
|
126
|
+
// ONE store instance for the whole app, owned by this apply body. The
|
|
127
|
+
// pet is host-global (state/display/interactions are /api/pet/*
|
|
128
|
+
// endpoints with no session dimension), so the slot system's per-session
|
|
129
|
+
// store scoping would only reset the pet on session switches and leave
|
|
130
|
+
// it stateless on the new-conversation screen (no session to scope by).
|
|
131
|
+
const petStore: PetStoreInstance = createPetStore().create()
|
|
132
|
+
const setSnapshot = petStore.actions.setSnapshot
|
|
133
|
+
const setState = petStore.actions.setState
|
|
134
|
+
const setFeedback = petStore.actions.setFeedback
|
|
135
|
+
|
|
136
|
+
const pollNow = (): void => {
|
|
137
|
+
petApi.state().then((snapshot) => {
|
|
138
|
+
setSnapshot(snapshot)
|
|
139
|
+
}, () => {
|
|
140
|
+
setState('error', 'pet.state transport error')
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const disposePoll = ctx.effect(() => {
|
|
145
|
+
// Poll only while the tab is visible: the host snapshot does not
|
|
146
|
+
// change while the page is hidden, so a background interval would
|
|
147
|
+
// only burn RPCs (browser throttling is an unreliable backstop).
|
|
148
|
+
// Coming back to the tab refreshes the pet immediately instead of
|
|
149
|
+
// waiting out the next 800 ms cycle.
|
|
150
|
+
let timer: number | undefined
|
|
151
|
+
const stop = (): void => {
|
|
152
|
+
if (timer !== undefined) {
|
|
153
|
+
window.clearInterval(timer)
|
|
154
|
+
timer = undefined
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const start = (): void => {
|
|
158
|
+
if (timer === undefined && document.visibilityState === 'visible') {
|
|
159
|
+
timer = window.setInterval(pollNow, POLL_MS)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const onVisibility = (): void => {
|
|
163
|
+
if (document.visibilityState === 'visible') {
|
|
164
|
+
pollNow()
|
|
165
|
+
start()
|
|
166
|
+
} else {
|
|
167
|
+
stop()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
start()
|
|
171
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
172
|
+
return () => {
|
|
173
|
+
stop()
|
|
174
|
+
document.removeEventListener('visibilitychange', onVisibility)
|
|
175
|
+
}
|
|
176
|
+
}, 'pet: poll')
|
|
177
|
+
|
|
178
|
+
const injected = (): PetInjected => ({
|
|
179
|
+
store: petStore,
|
|
180
|
+
ensure: pollNow,
|
|
181
|
+
pet: () => {
|
|
182
|
+
petApi.interact('pet').then((result) => {
|
|
183
|
+
setFeedback({
|
|
184
|
+
text: result.reaction,
|
|
185
|
+
kind: 'pet',
|
|
186
|
+
at: Date.now(),
|
|
187
|
+
})
|
|
188
|
+
}, () => {
|
|
189
|
+
// Ignore transport errors on interactions; the next poll resyncs.
|
|
190
|
+
})
|
|
191
|
+
},
|
|
192
|
+
feed: () => {
|
|
193
|
+
petApi.interact('feed').then((result) => {
|
|
194
|
+
setFeedback({
|
|
195
|
+
text: result.reaction,
|
|
196
|
+
kind: 'feed',
|
|
197
|
+
at: Date.now(),
|
|
198
|
+
})
|
|
199
|
+
}, () => {
|
|
200
|
+
// Ignore transport errors on interactions; the next poll resyncs.
|
|
201
|
+
})
|
|
202
|
+
},
|
|
203
|
+
hide: () => {
|
|
204
|
+
petApi.setVisible(false).then(() => {
|
|
205
|
+
pollNow()
|
|
206
|
+
}, () => {
|
|
207
|
+
// Ignore; next poll resyncs.
|
|
208
|
+
})
|
|
209
|
+
},
|
|
210
|
+
summon: () => {
|
|
211
|
+
petApi.setVisible(true).then(() => {
|
|
212
|
+
pollNow()
|
|
213
|
+
}, () => {
|
|
214
|
+
// Ignore; next poll resyncs.
|
|
215
|
+
})
|
|
216
|
+
},
|
|
217
|
+
dragEnd: (right, bottom) => {
|
|
218
|
+
petApi.setConfig({ right, bottom }).then(() => {
|
|
219
|
+
pollNow()
|
|
220
|
+
}, () => {
|
|
221
|
+
// Ignore; next poll resyncs.
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
rename: (name) => {
|
|
225
|
+
petApi.setName(name).then((result) => {
|
|
226
|
+
if (result.ok) pollNow()
|
|
227
|
+
}, () => {
|
|
228
|
+
// Ignore; next poll resyncs.
|
|
229
|
+
})
|
|
230
|
+
},
|
|
231
|
+
feedbackDone: () => {
|
|
232
|
+
setFeedback(null)
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// The input selector row mounts in EVERY conversation phase (cold
|
|
237
|
+
// start, blank-session hero, active seat) — the composer dock band
|
|
238
|
+
// only renders for an active session, which is why the pet used to
|
|
239
|
+
// vanish on the new-conversation screen.
|
|
240
|
+
const disposeDock = ctx.slots.inject('conversation.input.selector.context', () =>
|
|
241
|
+
ctx.slots.register({
|
|
242
|
+
name: 'conversation.input.selector.context',
|
|
243
|
+
id: 'pet',
|
|
244
|
+
order: 110,
|
|
245
|
+
inject: injected,
|
|
246
|
+
locale: NS,
|
|
247
|
+
}, PetDockEntry))
|
|
248
|
+
|
|
249
|
+
disposeUi = () => {
|
|
250
|
+
disposeDock()
|
|
251
|
+
disposePoll()
|
|
252
|
+
disposeUi = undefined
|
|
253
|
+
}
|
|
254
|
+
} else if (!enabled() && disposeUi !== undefined) {
|
|
255
|
+
disposeUi()
|
|
256
|
+
disposeUi = undefined
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
settingsScope.subscribe(syncUi)
|
|
260
|
+
syncUi()
|
|
261
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-pet locale dictionaries (zh/en).
|
|
3
|
+
* @module @linxin666/dsh-pet/client/locales
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Dictionary namespace this package registers. */
|
|
7
|
+
export const NS = 'pet'
|
|
8
|
+
|
|
9
|
+
/** Chinese copy. */
|
|
10
|
+
export const zh = {
|
|
11
|
+
'pet.feed': '喂食',
|
|
12
|
+
'pet.hide': '隐藏',
|
|
13
|
+
'pet.rename': '改名',
|
|
14
|
+
'pet.confirm': '确定',
|
|
15
|
+
'pet.namePlaceholder': '输入新名字',
|
|
16
|
+
'pet.summon': '召唤{name}',
|
|
17
|
+
'pet.rank': '亲密度 {rank}',
|
|
18
|
+
'pet.points': '{points} 点',
|
|
19
|
+
'pet.treats': '小鱼干 ×{n}',
|
|
20
|
+
'pet.state.loading': '鲸鱼娘正在赶来…',
|
|
21
|
+
'pet.state.error': '鲸鱼娘迷路了(连接失败)',
|
|
22
|
+
// 插件设置卡片(settings.plugin.item 席位)。
|
|
23
|
+
'settings.title': '宠物',
|
|
24
|
+
'settings.description': '鲸鱼娘的显示布局与名字。',
|
|
25
|
+
'settings.enabled': '启用宠物',
|
|
26
|
+
'settings.enabledHint': '关闭后隐藏宠物并停止轮询,可在设置里重新启用。',
|
|
27
|
+
'settings.visible': '显示宠物',
|
|
28
|
+
'settings.visibleHint': '关闭后宠物隐藏,可从聊天输入区重新召唤。',
|
|
29
|
+
'settings.size': '大小(px)',
|
|
30
|
+
'settings.sizeHint': '精灵单元高度,范围 32–512。',
|
|
31
|
+
'settings.right': '距右侧(px)',
|
|
32
|
+
'settings.rightHint': '距视口右边缘的水平内缩距离。',
|
|
33
|
+
'settings.bottom': '距底部(px)',
|
|
34
|
+
'settings.bottomHint': '距视口底边的垂直内缩距离。',
|
|
35
|
+
'settings.name': '名字',
|
|
36
|
+
'settings.nameHint': '宠物显示名,1–20 个字符。',
|
|
37
|
+
'settings.inherit': '继承',
|
|
38
|
+
'settings.on': '开',
|
|
39
|
+
'settings.off': '关',
|
|
40
|
+
'settings.overridden': '已覆盖',
|
|
41
|
+
'settings.reset': '恢复默认',
|
|
42
|
+
'settings.readOnly': '当前部署的设置只读。',
|
|
43
|
+
'settings.expand': '展开设置',
|
|
44
|
+
'settings.collapse': '收起设置',
|
|
45
|
+
'settings.save': '保存',
|
|
46
|
+
'settings.saving': '保存中…',
|
|
47
|
+
'settings.discard': '放弃',
|
|
48
|
+
'settings.unsaved': '未保存',
|
|
49
|
+
'settings.saveFailed': '部署未接受这些值,已保留供你修改。',
|
|
50
|
+
'settings.invalidNumber': '请输入数字,留空则使用默认值。',
|
|
51
|
+
} as const
|
|
52
|
+
|
|
53
|
+
/** English copy. */
|
|
54
|
+
export const en = {
|
|
55
|
+
'pet.feed': 'Feed',
|
|
56
|
+
'pet.hide': 'Hide',
|
|
57
|
+
'pet.rename': 'Rename',
|
|
58
|
+
'pet.confirm': 'OK',
|
|
59
|
+
'pet.namePlaceholder': 'Enter a new name',
|
|
60
|
+
'pet.summon': 'Summon {name}',
|
|
61
|
+
'pet.rank': 'Affinity {rank}',
|
|
62
|
+
'pet.points': '{points} pts',
|
|
63
|
+
'pet.treats': 'Treats ×{n}',
|
|
64
|
+
'pet.state.loading': 'The whale girl is on her way…',
|
|
65
|
+
'pet.state.error': 'The whale girl is lost (connection failed)',
|
|
66
|
+
// Plugin settings card (the `settings.plugin.item` seat).
|
|
67
|
+
'settings.title': 'Pet',
|
|
68
|
+
'settings.description': 'The whale girl\u2019s display layout and name.',
|
|
69
|
+
'settings.enabled': 'Enable the pet',
|
|
70
|
+
'settings.enabledHint': 'When off, the pet hides and polling stops; re-enable it here.',
|
|
71
|
+
'settings.visible': 'Show the pet',
|
|
72
|
+
'settings.visibleHint': 'When off, the pet hides; summon it again from the input row.',
|
|
73
|
+
'settings.size': 'Size (px)',
|
|
74
|
+
'settings.sizeHint': 'Sprite cell height, 32\u2013512.',
|
|
75
|
+
'settings.right': 'Right inset (px)',
|
|
76
|
+
'settings.rightHint': 'Horizontal inset from the viewport right edge.',
|
|
77
|
+
'settings.bottom': 'Bottom inset (px)',
|
|
78
|
+
'settings.bottomHint': 'Vertical inset from the viewport bottom edge.',
|
|
79
|
+
'settings.name': 'Name',
|
|
80
|
+
'settings.nameHint': 'The pet\u2019s display name, 1\u201320 characters.',
|
|
81
|
+
'settings.inherit': 'Inherit',
|
|
82
|
+
'settings.on': 'On',
|
|
83
|
+
'settings.off': 'Off',
|
|
84
|
+
'settings.overridden': 'Overridden',
|
|
85
|
+
'settings.reset': 'Reset to default',
|
|
86
|
+
'settings.readOnly': 'This deployment stores settings read-only.',
|
|
87
|
+
'settings.expand': 'Show settings',
|
|
88
|
+
'settings.collapse': 'Hide settings',
|
|
89
|
+
'settings.save': 'Save',
|
|
90
|
+
'settings.saving': 'Saving\u2026',
|
|
91
|
+
'settings.discard': 'Discard',
|
|
92
|
+
'settings.unsaved': 'Unsaved',
|
|
93
|
+
'settings.saveFailed': 'The deployment did not accept these values; they were left for you to correct.',
|
|
94
|
+
'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
|
|
95
|
+
} as const
|
|
96
|
+
|
|
97
|
+
/** Key union for this namespace. */
|
|
98
|
+
export type PetKey = keyof typeof zh
|
|
99
|
+
|
|
100
|
+
/** The settings-card slice of the pet dictionary. */
|
|
101
|
+
export type SettingsCardKey = PetKey
|
|
102
|
+
|
|
103
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
104
|
+
interface LocaleNamespaceMap {
|
|
105
|
+
/** dsh-pet UI copy. */
|
|
106
|
+
pet: PetKey
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side pet store: the pet state snapshot plus transient UI feedback
|
|
3
|
+
* (reaction bubbles), written only through the store's audit actions. The
|
|
4
|
+
* RPC polling and interactions live in the plugin apply body; components
|
|
5
|
+
* only ever read snapshots.
|
|
6
|
+
* @module @linxin666/dsh-pet/client/pet-store
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { defineStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
10
|
+
import type { EngineStoreHandle, EngineStoreInstance } from '@deepseek-ai/dsh-client-runtime/client'
|
|
11
|
+
import type { PetStateView } from '../service.ts'
|
|
12
|
+
import type { PetInteraction } from '../affinity.ts'
|
|
13
|
+
|
|
14
|
+
/** One transient reaction bubble on the pet. */
|
|
15
|
+
export interface PetFeedback {
|
|
16
|
+
/** Bubble copy. */
|
|
17
|
+
text: string
|
|
18
|
+
/** Interaction kind driving the reaction animation. */
|
|
19
|
+
kind: PetInteraction | 'none'
|
|
20
|
+
/** Epoch ms when the bubble appeared (for expiry). */
|
|
21
|
+
at: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Pet UI state as consumers see it. */
|
|
25
|
+
export interface PetUiState {
|
|
26
|
+
/** Latest host snapshot; null before the first successful fetch. */
|
|
27
|
+
snapshot: PetStateView | null
|
|
28
|
+
/** Fetch lifecycle. */
|
|
29
|
+
state: 'loading' | 'ready' | 'error'
|
|
30
|
+
/** Transport error message (for the debug surface), when any. */
|
|
31
|
+
error: string | null
|
|
32
|
+
/** Active reaction bubble, if any. */
|
|
33
|
+
feedback: PetFeedback | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Store write set. */
|
|
37
|
+
export type PetUiActions = {
|
|
38
|
+
/** Replace the host snapshot (poll result). */
|
|
39
|
+
setSnapshot: (draft: PetUiState, snapshot: PetStateView) => void
|
|
40
|
+
/** Mark the fetch lifecycle. */
|
|
41
|
+
setState: (draft: PetUiState, state: PetUiState['state'], error: string | null) => void
|
|
42
|
+
/** Show a reaction bubble. */
|
|
43
|
+
setFeedback: (draft: PetUiState, feedback: PetFeedback | null) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Create the pet store handle (apply world only; never module-level). */
|
|
47
|
+
export function createPetStore(): EngineStoreHandle<PetUiState, PetUiActions> {
|
|
48
|
+
return defineStore({
|
|
49
|
+
init: (): PetUiState => ({
|
|
50
|
+
snapshot: null,
|
|
51
|
+
state: 'loading',
|
|
52
|
+
error: null,
|
|
53
|
+
feedback: null,
|
|
54
|
+
}),
|
|
55
|
+
actions: {
|
|
56
|
+
setSnapshot: (draft, snapshot) => {
|
|
57
|
+
draft.snapshot = snapshot
|
|
58
|
+
draft.state = 'ready'
|
|
59
|
+
draft.error = null
|
|
60
|
+
},
|
|
61
|
+
setState: (draft, state, error) => {
|
|
62
|
+
draft.state = state
|
|
63
|
+
draft.error = error
|
|
64
|
+
},
|
|
65
|
+
setFeedback: (draft, feedback) => {
|
|
66
|
+
draft.feedback = feedback
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type { PetInteraction }
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A live pet store instance (one per host, owned by the plugin apply body —
|
|
76
|
+
* the pet itself is host-global, so its UI state must not ride the slot
|
|
77
|
+
* system's per-session store scoping).
|
|
78
|
+
*/
|
|
79
|
+
export type PetStoreInstance = EngineStoreInstance<PetUiState, PetUiActions>
|
|
80
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
.float {
|
|
2
|
+
position: fixed;
|
|
3
|
+
pointer-events: auto;
|
|
4
|
+
user-select: none;
|
|
5
|
+
-webkit-user-select: none;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
align-items: center;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.sprite {
|
|
12
|
+
position: relative;
|
|
13
|
+
image-rendering: auto;
|
|
14
|
+
touch-action: none;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.bubble {
|
|
18
|
+
position: absolute;
|
|
19
|
+
bottom: 100%;
|
|
20
|
+
margin-bottom: 6px;
|
|
21
|
+
white-space: nowrap;
|
|
22
|
+
padding: 4px 10px;
|
|
23
|
+
border-radius: 999px;
|
|
24
|
+
font-size: 12px;
|
|
25
|
+
line-height: 1.4;
|
|
26
|
+
color: #fff;
|
|
27
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
|
28
|
+
animation: pet-bubble-pop 2.6s ease-out forwards;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.bubblePet {
|
|
33
|
+
background: rgba(244, 114, 182, 0.92);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.bubbleFeed {
|
|
37
|
+
background: rgba(56, 189, 248, 0.92);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@keyframes pet-bubble-pop {
|
|
41
|
+
0% {
|
|
42
|
+
opacity: 0;
|
|
43
|
+
transform: translateY(6px) scale(0.85);
|
|
44
|
+
}
|
|
45
|
+
15% {
|
|
46
|
+
opacity: 1;
|
|
47
|
+
transform: translateY(0) scale(1.05);
|
|
48
|
+
}
|
|
49
|
+
25% {
|
|
50
|
+
transform: translateY(0) scale(1);
|
|
51
|
+
}
|
|
52
|
+
75% {
|
|
53
|
+
opacity: 1;
|
|
54
|
+
}
|
|
55
|
+
100% {
|
|
56
|
+
opacity: 0;
|
|
57
|
+
transform: translateY(-8px) scale(0.95);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.panel {
|
|
62
|
+
position: absolute;
|
|
63
|
+
bottom: calc(100% + 4px);
|
|
64
|
+
background: rgba(15, 23, 42, 0.92);
|
|
65
|
+
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
66
|
+
border-radius: 10px;
|
|
67
|
+
padding: 8px 10px;
|
|
68
|
+
color: #e2e8f0;
|
|
69
|
+
font-size: 12px;
|
|
70
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
|
71
|
+
backdrop-filter: blur(6px);
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
min-width: 132px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.rankRow {
|
|
79
|
+
display: flex;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
gap: 10px;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.nameCell {
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.renameRow {
|
|
90
|
+
display: flex;
|
|
91
|
+
gap: 6px;
|
|
92
|
+
align-items: center;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.nameInput {
|
|
96
|
+
flex: 1;
|
|
97
|
+
min-width: 0;
|
|
98
|
+
border: 1px solid rgba(125, 211, 252, 0.5);
|
|
99
|
+
border-radius: 6px;
|
|
100
|
+
background: rgba(30, 41, 59, 0.9);
|
|
101
|
+
color: #e2e8f0;
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
padding: 3px 6px;
|
|
104
|
+
outline: none;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.nameInput:focus {
|
|
108
|
+
border-color: #38bdf8;
|
|
109
|
+
/* 2px accent ring (theme-agnostic) replacing the removed default outline. */
|
|
110
|
+
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.45);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.actions {
|
|
114
|
+
display: flex;
|
|
115
|
+
gap: 6px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.action {
|
|
119
|
+
flex: 1;
|
|
120
|
+
border: none;
|
|
121
|
+
border-radius: 6px;
|
|
122
|
+
padding: 4px 8px;
|
|
123
|
+
font-size: 12px;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
color: #0f172a;
|
|
126
|
+
background: linear-gradient(180deg, #7dd3fc, #38bdf8);
|
|
127
|
+
transition: filter 120ms ease, box-shadow 120ms ease;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.action:hover {
|
|
131
|
+
filter: brightness(1.08);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.action:active {
|
|
135
|
+
filter: brightness(0.94);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.action:focus-visible {
|
|
139
|
+
outline: none;
|
|
140
|
+
/* 2px accent ring on the pet's own palette; stays visible on the dark
|
|
141
|
+
glass panel regardless of the GUI theme (theme-agnostic). */
|
|
142
|
+
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.85);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.summon {
|
|
146
|
+
border: 1px dashed rgba(125, 211, 252, 0.6);
|
|
147
|
+
background: rgba(15, 23, 42, 0.75);
|
|
148
|
+
color: #7dd3fc;
|
|
149
|
+
border-radius: 999px;
|
|
150
|
+
padding: 2px 10px;
|
|
151
|
+
font-size: 11px;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
transition: border-color 120ms ease, color 120ms ease,
|
|
154
|
+
background 120ms ease, box-shadow 120ms ease;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.summon:hover {
|
|
158
|
+
border-color: rgba(125, 211, 252, 0.95);
|
|
159
|
+
color: #bae6fd;
|
|
160
|
+
background: rgba(15, 23, 42, 0.9);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.summon:active {
|
|
164
|
+
color: #7dd3fc;
|
|
165
|
+
border-color: rgba(125, 211, 252, 0.8);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.summon:focus-visible {
|
|
169
|
+
outline: none;
|
|
170
|
+
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.85);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Reduced motion: freeze the reaction bubble and kill the overlay transitions
|
|
174
|
+
so the pet's motion is static or minimal without hiding interaction. */
|
|
175
|
+
@media (prefers-reduced-motion: reduce) {
|
|
176
|
+
.bubble {
|
|
177
|
+
animation: none;
|
|
178
|
+
opacity: 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.action,
|
|
182
|
+
.summon {
|
|
183
|
+
transition: none;
|
|
184
|
+
}
|
|
185
|
+
}
|