@linxin666/dsh-pet 0.1.6 → 0.1.8
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 +4 -4
- package/lib/client.js +68 -20
- package/lib/client.js.map +1 -1
- package/lib/types/client/PetDockEntry.d.ts +10 -8
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +14 -6
- package/lib/types/client/WhalePet.d.ts.map +1 -1
- package/lib/types/client/WhalePet.js +23 -4
- package/lib/types/client/index.d.ts +11 -11
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +30 -26
- package/lib/types/client/locales.d.ts +16 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +27 -0
- package/package.json +1 -1
- package/src/client/PetDockEntry.tsx +17 -10
- package/src/client/WhalePet.tsx +27 -4
- package/src/client/index.ts +30 -27
- package/src/client/locales.ts +29 -0
- package/src/client/pet.module.css +15 -1
- package/lib/types/client/slots-augment.d.ts +0 -19
- package/lib/types/client/slots-augment.d.ts.map +0 -1
- package/lib/types/client/slots-augment.js +0 -1
- package/src/client/slots-augment.ts +0 -27
|
@@ -36,6 +36,7 @@ export function WhalePet(props) {
|
|
|
36
36
|
const [nameDraft, setNameDraft] = useState('');
|
|
37
37
|
const [dragPos, setDragPos] = useState(null);
|
|
38
38
|
const dragRef = useRef(null);
|
|
39
|
+
const hideTimerRef = useRef(null);
|
|
39
40
|
const frameRef = useRef({
|
|
40
41
|
track: null,
|
|
41
42
|
index: 0,
|
|
@@ -161,6 +162,12 @@ export function WhalePet(props) {
|
|
|
161
162
|
// `draggedRef` records whether the pointer actually moved, so the browser's
|
|
162
163
|
// trailing click (fired after pointerup) does not pet the whale.
|
|
163
164
|
const draggedRef = useRef(false);
|
|
165
|
+
const clearHideTimer = () => {
|
|
166
|
+
if (hideTimerRef.current !== null) {
|
|
167
|
+
window.clearTimeout(hideTimerRef.current);
|
|
168
|
+
hideTimerRef.current = null;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
164
171
|
const onPointerDown = (e) => {
|
|
165
172
|
e.preventDefault();
|
|
166
173
|
e.target.setPointerCapture?.(e.pointerId);
|
|
@@ -191,15 +198,22 @@ export function WhalePet(props) {
|
|
|
191
198
|
const pos = dragPos ?? { right: display.right, bottom: display.bottom };
|
|
192
199
|
const spriteWidth = Math.round(FRAME_WIDTH * spriteScale);
|
|
193
200
|
const spriteHeight = Math.round(FRAME_HEIGHT * spriteScale);
|
|
194
|
-
const float = (_jsxs("div", { ref: floatRef, className: styles.float, style: { right: pos.right, bottom: pos.bottom, zIndex: 2147483000 }, onPointerEnter: () =>
|
|
201
|
+
const float = (_jsxs("div", { ref: floatRef, className: styles.float, style: { right: pos.right, bottom: pos.bottom, zIndex: 2147483000 }, onPointerEnter: () => {
|
|
202
|
+
clearHideTimer();
|
|
203
|
+
setHovered(true);
|
|
204
|
+
}, onPointerLeave: (e) => {
|
|
195
205
|
// The panel and bubble render OUTSIDE the container's box (absolute,
|
|
196
206
|
// above the sprite), so moving onto them fires pointerleave on the
|
|
197
207
|
// container. Treat a target still inside the container's DOM (the
|
|
198
|
-
// overflowed panel) as "still hovering"
|
|
208
|
+
// overflowed panel) as "still hovering"; otherwise give the pointer a
|
|
209
|
+
// short grace period to reach the panel across the gap above it. The
|
|
210
|
+
// bridge (`.panel::after`) keeps the pointer inside the hit area, and
|
|
211
|
+
// the grace period covers a slow mouse crossing the remaining sliver.
|
|
199
212
|
const next = e.relatedTarget;
|
|
200
213
|
if (next instanceof Node && floatRef.current?.contains(next))
|
|
201
214
|
return;
|
|
202
|
-
|
|
215
|
+
clearHideTimer();
|
|
216
|
+
hideTimerRef.current = window.setTimeout(() => setHovered(false), 300);
|
|
203
217
|
}, children: [_jsx("div", { ref: spriteRef, className: styles.sprite, style: {
|
|
204
218
|
width: spriteWidth,
|
|
205
219
|
height: spriteHeight,
|
|
@@ -214,7 +228,12 @@ export function WhalePet(props) {
|
|
|
214
228
|
if (draggedRef.current)
|
|
215
229
|
return;
|
|
216
230
|
props.onPet();
|
|
217
|
-
}, role: "button", "aria-label": "whale girl" }), feedback !== null && (_jsx("div", { className: `${styles.bubble} ${feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet}`, children: feedback.text }, feedback.at)), hovered && dragRef.current === null && (_jsx("div", { className: styles.panel,
|
|
231
|
+
}, role: "button", "aria-label": "whale girl" }), feedback !== null && (_jsx("div", { className: `${styles.bubble} ${feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet}`, children: feedback.text }, feedback.at)), hovered && dragRef.current === null && (_jsx("div", { className: styles.panel, onPointerEnter: () => {
|
|
232
|
+
// Reaching the panel (or its bridge) must cancel any hide timer
|
|
233
|
+
// the container's pointerleave may have armed while the pointer
|
|
234
|
+
// crossed the sliver between the sprite and the panel.
|
|
235
|
+
clearHideTimer();
|
|
236
|
+
}, children: renaming ? (_jsxs("div", { className: styles.renameRow, children: [_jsx("input", { className: styles.nameInput, value: nameDraft, maxLength: 20, placeholder: props.t('pet.namePlaceholder'), autoFocus: true, onChange: (e) => setNameDraft(e.target.value), onKeyDown: (e) => {
|
|
218
237
|
if (e.key === 'Enter') {
|
|
219
238
|
const trimmed = nameDraft.trim();
|
|
220
239
|
if (trimmed !== '') {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-pet browser half —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* host
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* dsh-pet browser half — mounts the whale-girl as a global floating surface
|
|
3
|
+
* and drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll
|
|
4
|
+
* the host snapshot (~800 ms), forward interactions, persist drag positions.
|
|
5
|
+
* The pet is host-global (no session dimension), so it mounts directly onto
|
|
6
|
+
* `document.body` via a single React root rather than a session-scoped slot —
|
|
7
|
+
* on the new-conversation screen no session exists, and a dock-mounted pet
|
|
8
|
+
* would vanish there (issue #48). When the pet is hidden the entry becomes a
|
|
9
|
+
* fixed-position summon button.
|
|
10
10
|
* @module @linxin666/dsh-pet/client
|
|
11
11
|
*/
|
|
12
12
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
@@ -37,9 +37,9 @@ export interface SettingsPluginItemOwnerProps {
|
|
|
37
37
|
children?: never;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Client plugin body: register dictionaries, mount the
|
|
41
|
-
* loop while the plugin is enabled, and seat the settings card in the
|
|
42
|
-
* plugin group.
|
|
40
|
+
* Client plugin body: register dictionaries, mount the global pet entry and
|
|
41
|
+
* poll loop while the plugin is enabled, and seat the settings card in the
|
|
42
|
+
* Web UI plugin group.
|
|
43
43
|
* @param ctx - client root context.
|
|
44
44
|
*/
|
|
45
45
|
export declare function apply(ctx: ClientContext): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAwD3E,yBAAyB;AACzB,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAEtF,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,OAAO;QACf;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,sEAAsE;AACtE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAmK9C"}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-pet browser half —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* host
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* dsh-pet browser half — mounts the whale-girl as a global floating surface
|
|
3
|
+
* and drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll
|
|
4
|
+
* the host snapshot (~800 ms), forward interactions, persist drag positions.
|
|
5
|
+
* The pet is host-global (no session dimension), so it mounts directly onto
|
|
6
|
+
* `document.body` via a single React root rather than a session-scoped slot —
|
|
7
|
+
* on the new-conversation screen no session exists, and a dock-mounted pet
|
|
8
|
+
* would vanish there (issue #48). When the pet is hidden the entry becomes a
|
|
9
|
+
* fixed-position summon button.
|
|
10
10
|
* @module @linxin666/dsh-pet/client
|
|
11
11
|
*/
|
|
12
|
+
import { createElement } from 'react';
|
|
13
|
+
import { createRoot } from 'react-dom/client';
|
|
12
14
|
import { createPetStore } from "./pet-store.js";
|
|
13
15
|
import { PetDockEntry } from "./PetDockEntry.js";
|
|
14
16
|
import { PetSettingsCard, PetSettingsCardController } from "./PetSettingsCard.js";
|
|
15
|
-
import { NS, en, zh } from "./locales.js";
|
|
17
|
+
import { NS, en, zh, t } from "./locales.js";
|
|
16
18
|
/** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
|
|
17
19
|
async function petFetch(path, body) {
|
|
18
20
|
const response = await fetch(path, body === undefined
|
|
@@ -42,9 +44,9 @@ const PET_SETTINGS_NS = 'pet';
|
|
|
42
44
|
/** Required services. */
|
|
43
45
|
export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote'];
|
|
44
46
|
/**
|
|
45
|
-
* Client plugin body: register dictionaries, mount the
|
|
46
|
-
* loop while the plugin is enabled, and seat the settings card in the
|
|
47
|
-
* plugin group.
|
|
47
|
+
* Client plugin body: register dictionaries, mount the global pet entry and
|
|
48
|
+
* poll loop while the plugin is enabled, and seat the settings card in the
|
|
49
|
+
* Web UI plugin group.
|
|
48
50
|
* @param ctx - client root context.
|
|
49
51
|
*/
|
|
50
52
|
export function apply(ctx) {
|
|
@@ -66,8 +68,8 @@ export function apply(ctx) {
|
|
|
66
68
|
locale: NS,
|
|
67
69
|
inject: () => petSettings.inject(),
|
|
68
70
|
}, PetSettingsCard));
|
|
69
|
-
// The
|
|
70
|
-
// enabled; toggling the setting off hides the pet and stops polling.
|
|
71
|
+
// The global pet entry, its store, and the poll loop live while the plugin
|
|
72
|
+
// is enabled; toggling the setting off hides the pet and stops polling.
|
|
71
73
|
let disposeUi;
|
|
72
74
|
const syncUi = () => {
|
|
73
75
|
if (enabled() && disposeUi === undefined) {
|
|
@@ -179,19 +181,21 @@ export function apply(ctx) {
|
|
|
179
181
|
setFeedback(null);
|
|
180
182
|
},
|
|
181
183
|
});
|
|
182
|
-
// The
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// vanish on the new-conversation screen.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
// The pet is host-global (its state/display/interactions have no session
|
|
185
|
+
// dimension), and the official rc.6 shell declares no root-scoped slot
|
|
186
|
+
// for a global floating surface — the dock is session-scoped, so a pet
|
|
187
|
+
// mounted there would vanish on the new-conversation screen (issue #48).
|
|
188
|
+
// The entry therefore mounts straight onto document.body via a single
|
|
189
|
+
// React root for the page lifetime: WhalePet portals itself to body when
|
|
190
|
+
// visible, and the hidden-state summon button is fixed-positioned.
|
|
191
|
+
const container = document.createElement('div');
|
|
192
|
+
container.dataset.dshPetRoot = '';
|
|
193
|
+
document.body.appendChild(container);
|
|
194
|
+
const petRoot = createRoot(container);
|
|
195
|
+
petRoot.render(createElement(PetDockEntry, { ...injected(), t }));
|
|
193
196
|
disposeUi = () => {
|
|
194
|
-
|
|
197
|
+
petRoot.unmount();
|
|
198
|
+
container.remove();
|
|
195
199
|
disposePoll();
|
|
196
200
|
disposeUi = undefined;
|
|
197
201
|
};
|
|
@@ -92,6 +92,22 @@ export declare const en: {
|
|
|
92
92
|
export type PetKey = keyof typeof zh;
|
|
93
93
|
/** The settings-card slice of the pet dictionary. */
|
|
94
94
|
export type SettingsCardKey = PetKey;
|
|
95
|
+
/**
|
|
96
|
+
* Active dictionary, picked by the document language at call time. The pet
|
|
97
|
+
* mounts as a global floating surface (not a session-scoped slot), so it has
|
|
98
|
+
* no framework locale seat and resolves its copy the same tiny way the
|
|
99
|
+
* task-board's DOM-injected surface does.
|
|
100
|
+
*/
|
|
101
|
+
export declare function dictionary(): Record<PetKey, string>;
|
|
102
|
+
/**
|
|
103
|
+
* Translate a key with optional `{name}` template params. Mirrors the slot
|
|
104
|
+
* `Translate` contract `(key, params?) => string` so it can be handed to the
|
|
105
|
+
* same components that used to receive the framework-injected `t` seat. The
|
|
106
|
+
* key is typed loosely (`string`) so the function is assignable to the slot's
|
|
107
|
+
* `TranslateNS<'pet'>` (whose key domain also spans the shared common
|
|
108
|
+
* vocabulary); a missing key degrades to the key itself rather than throwing.
|
|
109
|
+
*/
|
|
110
|
+
export declare function t(key: string, params?: Record<string, unknown>): string;
|
|
95
111
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
96
112
|
interface LocaleNamespaceMap {
|
|
97
113
|
/** dsh-pet UI copy. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
|
|
@@ -90,3 +90,30 @@ export const en = {
|
|
|
90
90
|
'settings.saveFailed': 'The deployment did not accept these values; they were left for you to correct.',
|
|
91
91
|
'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
|
|
92
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* Active dictionary, picked by the document language at call time. The pet
|
|
95
|
+
* mounts as a global floating surface (not a session-scoped slot), so it has
|
|
96
|
+
* no framework locale seat and resolves its copy the same tiny way the
|
|
97
|
+
* task-board's DOM-injected surface does.
|
|
98
|
+
*/
|
|
99
|
+
export function dictionary() {
|
|
100
|
+
const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh';
|
|
101
|
+
return lang.toLowerCase().startsWith('en') ? en : zh;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Translate a key with optional `{name}` template params. Mirrors the slot
|
|
105
|
+
* `Translate` contract `(key, params?) => string` so it can be handed to the
|
|
106
|
+
* same components that used to receive the framework-injected `t` seat. The
|
|
107
|
+
* key is typed loosely (`string`) so the function is assignable to the slot's
|
|
108
|
+
* `TranslateNS<'pet'>` (whose key domain also spans the shared common
|
|
109
|
+
* vocabulary); a missing key degrades to the key itself rather than throwing.
|
|
110
|
+
*/
|
|
111
|
+
export function t(key, params) {
|
|
112
|
+
let text = dictionary()[key] ?? key;
|
|
113
|
+
if (params !== undefined) {
|
|
114
|
+
for (const [name, value] of Object.entries(params)) {
|
|
115
|
+
text = text.replaceAll(`{${name}}`, String(value));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return text;
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
3
|
"description": "鲸鱼娘宠物插件 for the dsh web GUI: a soft healing whale-girl companion that reacts to model activity (idle/waiting/thinking/tool/done), with petting/feeding interactions and an affinity score",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Global floating pet entry. The pet is host-global (its state, display and
|
|
3
|
+
* interactions live on `/api/pet/*` endpoints with no session dimension), so
|
|
4
|
+
* it must not ride a session-scoped slot — on the new-conversation screen no
|
|
5
|
+
* session exists to scope a slot by, and the pet would vanish (issue #48).
|
|
6
|
+
* The client half therefore mounts this entry straight onto `document.body`
|
|
7
|
+
* (see index.ts): while visible it renders the floating WhalePet (a portal),
|
|
8
|
+
* while hidden it renders a fixed-position summon button.
|
|
7
9
|
* @module @linxin666/dsh-pet/client/PetDockEntry
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import { useEffect, useSyncExternalStore, type ReactElement } from 'react'
|
|
11
|
-
import type { PropsLocale
|
|
12
|
-
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
13
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
|
13
14
|
import type { PetDisplayConfig } from '../persist.ts'
|
|
14
15
|
import type { PetStoreInstance } from './pet-store.ts'
|
|
15
16
|
import { WhalePet } from './WhalePet.tsx'
|
|
@@ -38,10 +39,9 @@ export interface PetInjected {
|
|
|
38
39
|
feedbackDone: () => void
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
/** Composed props of the
|
|
42
|
+
/** Composed props of the global pet entry (locale + injected; no slot runtime share). */
|
|
42
43
|
export type PetDockEntryProps =
|
|
43
|
-
|
|
44
|
-
& PetInjected
|
|
44
|
+
PetInjected
|
|
45
45
|
& PropsLocale<typeof NS>
|
|
46
46
|
|
|
47
47
|
const DEFAULT_DISPLAY: PetDisplayConfig = { visible: true, size: 160, right: 24, bottom: 20 }
|
|
@@ -82,10 +82,17 @@ export function PetDockEntry(props: PetDockEntryProps): ReactElement {
|
|
|
82
82
|
</span>
|
|
83
83
|
)
|
|
84
84
|
}
|
|
85
|
+
const display = snapshot?.display ?? DEFAULT_DISPLAY
|
|
85
86
|
return (
|
|
86
87
|
<button
|
|
87
88
|
type="button"
|
|
88
89
|
className={styles.summon}
|
|
90
|
+
style={{
|
|
91
|
+
position: 'fixed',
|
|
92
|
+
right: display.right,
|
|
93
|
+
bottom: display.bottom,
|
|
94
|
+
zIndex: 2147483000,
|
|
95
|
+
}}
|
|
89
96
|
onClick={props.summon}
|
|
90
97
|
data-testid="pet-summon"
|
|
91
98
|
>
|
package/src/client/WhalePet.tsx
CHANGED
|
@@ -71,6 +71,7 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
|
|
|
71
71
|
const [nameDraft, setNameDraft] = useState('')
|
|
72
72
|
const [dragPos, setDragPos] = useState<{ right: number; bottom: number } | null>(null)
|
|
73
73
|
const dragRef = useRef<{ startX: number; startY: number; right: number; bottom: number } | null>(null)
|
|
74
|
+
const hideTimerRef = useRef<number | null>(null)
|
|
74
75
|
const frameRef = useRef<{ track: PetAnimation | null; index: number; elapsed: number }>({
|
|
75
76
|
track: null,
|
|
76
77
|
index: 0,
|
|
@@ -193,6 +194,13 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
|
|
|
193
194
|
// `draggedRef` records whether the pointer actually moved, so the browser's
|
|
194
195
|
// trailing click (fired after pointerup) does not pet the whale.
|
|
195
196
|
const draggedRef = useRef(false)
|
|
197
|
+
const clearHideTimer = (): void => {
|
|
198
|
+
if (hideTimerRef.current !== null) {
|
|
199
|
+
window.clearTimeout(hideTimerRef.current)
|
|
200
|
+
hideTimerRef.current = null
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
196
204
|
const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>): void => {
|
|
197
205
|
e.preventDefault()
|
|
198
206
|
;(e.target as HTMLElement).setPointerCapture?.(e.pointerId)
|
|
@@ -226,15 +234,22 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
|
|
|
226
234
|
ref={floatRef}
|
|
227
235
|
className={styles.float}
|
|
228
236
|
style={{ right: pos.right, bottom: pos.bottom, zIndex: 2147483000 }}
|
|
229
|
-
onPointerEnter={() =>
|
|
237
|
+
onPointerEnter={() => {
|
|
238
|
+
clearHideTimer()
|
|
239
|
+
setHovered(true)
|
|
240
|
+
}}
|
|
230
241
|
onPointerLeave={(e) => {
|
|
231
242
|
// The panel and bubble render OUTSIDE the container's box (absolute,
|
|
232
243
|
// above the sprite), so moving onto them fires pointerleave on the
|
|
233
244
|
// container. Treat a target still inside the container's DOM (the
|
|
234
|
-
// overflowed panel) as "still hovering"
|
|
245
|
+
// overflowed panel) as "still hovering"; otherwise give the pointer a
|
|
246
|
+
// short grace period to reach the panel across the gap above it. The
|
|
247
|
+
// bridge (`.panel::after`) keeps the pointer inside the hit area, and
|
|
248
|
+
// the grace period covers a slow mouse crossing the remaining sliver.
|
|
235
249
|
const next = e.relatedTarget
|
|
236
250
|
if (next instanceof Node && floatRef.current?.contains(next)) return
|
|
237
|
-
|
|
251
|
+
clearHideTimer()
|
|
252
|
+
hideTimerRef.current = window.setTimeout(() => setHovered(false), 300)
|
|
238
253
|
}}
|
|
239
254
|
>
|
|
240
255
|
<div
|
|
@@ -267,7 +282,15 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
|
|
|
267
282
|
</div>
|
|
268
283
|
)}
|
|
269
284
|
{hovered && dragRef.current === null && (
|
|
270
|
-
<div
|
|
285
|
+
<div
|
|
286
|
+
className={styles.panel}
|
|
287
|
+
onPointerEnter={() => {
|
|
288
|
+
// Reaching the panel (or its bridge) must cancel any hide timer
|
|
289
|
+
// the container's pointerleave may have armed while the pointer
|
|
290
|
+
// crossed the sliver between the sprite and the panel.
|
|
291
|
+
clearHideTimer()
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
271
294
|
{renaming ? (
|
|
272
295
|
<div className={styles.renameRow}>
|
|
273
296
|
<input
|
package/src/client/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-pet browser half —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* host
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* dsh-pet browser half — mounts the whale-girl as a global floating surface
|
|
3
|
+
* and drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll
|
|
4
|
+
* the host snapshot (~800 ms), forward interactions, persist drag positions.
|
|
5
|
+
* The pet is host-global (no session dimension), so it mounts directly onto
|
|
6
|
+
* `document.body` via a single React root rather than a session-scoped slot —
|
|
7
|
+
* on the new-conversation screen no session exists, and a dock-mounted pet
|
|
8
|
+
* would vanish there (issue #48). When the pet is hidden the entry becomes a
|
|
9
|
+
* fixed-position summon button.
|
|
10
10
|
* @module @linxin666/dsh-pet/client
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -20,10 +20,12 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
|
20
20
|
import type { PetDisplayConfig } from '../persist.ts'
|
|
21
21
|
import type { PetInteractResult, PetStateView } from '../service.ts'
|
|
22
22
|
import type { PetInteraction } from '../affinity.ts'
|
|
23
|
+
import { createElement } from 'react'
|
|
24
|
+
import { createRoot } from 'react-dom/client'
|
|
23
25
|
import { createPetStore, type PetStoreInstance } from './pet-store.ts'
|
|
24
26
|
import { PetDockEntry, type PetInjected } from './PetDockEntry.tsx'
|
|
25
27
|
import { PetSettingsCard, PetSettingsCardController, type PetSettings } from './PetSettingsCard.tsx'
|
|
26
|
-
import { NS, en, zh } from './locales.ts'
|
|
28
|
+
import { NS, en, zh, t } from './locales.ts'
|
|
27
29
|
|
|
28
30
|
/** The host pet API as the browser sees it (same-origin JSON endpoints). */
|
|
29
31
|
interface PetHttpApi {
|
|
@@ -91,9 +93,9 @@ export interface SettingsPluginItemOwnerProps {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/**
|
|
94
|
-
* Client plugin body: register dictionaries, mount the
|
|
95
|
-
* loop while the plugin is enabled, and seat the settings card in the
|
|
96
|
-
* plugin group.
|
|
96
|
+
* Client plugin body: register dictionaries, mount the global pet entry and
|
|
97
|
+
* poll loop while the plugin is enabled, and seat the settings card in the
|
|
98
|
+
* Web UI plugin group.
|
|
97
99
|
* @param ctx - client root context.
|
|
98
100
|
*/
|
|
99
101
|
export function apply(ctx: ClientContext): void {
|
|
@@ -118,8 +120,8 @@ export function apply(ctx: ClientContext): void {
|
|
|
118
120
|
inject: () => petSettings.inject(),
|
|
119
121
|
}, PetSettingsCard))
|
|
120
122
|
|
|
121
|
-
// The
|
|
122
|
-
// enabled; toggling the setting off hides the pet and stops polling.
|
|
123
|
+
// The global pet entry, its store, and the poll loop live while the plugin
|
|
124
|
+
// is enabled; toggling the setting off hides the pet and stops polling.
|
|
123
125
|
let disposeUi: (() => void) | undefined
|
|
124
126
|
const syncUi = (): void => {
|
|
125
127
|
if (enabled() && disposeUi === undefined) {
|
|
@@ -233,21 +235,22 @@ export function apply(ctx: ClientContext): void {
|
|
|
233
235
|
},
|
|
234
236
|
})
|
|
235
237
|
|
|
236
|
-
// The
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
// vanish on the new-conversation screen.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
238
|
+
// The pet is host-global (its state/display/interactions have no session
|
|
239
|
+
// dimension), and the official rc.6 shell declares no root-scoped slot
|
|
240
|
+
// for a global floating surface — the dock is session-scoped, so a pet
|
|
241
|
+
// mounted there would vanish on the new-conversation screen (issue #48).
|
|
242
|
+
// The entry therefore mounts straight onto document.body via a single
|
|
243
|
+
// React root for the page lifetime: WhalePet portals itself to body when
|
|
244
|
+
// visible, and the hidden-state summon button is fixed-positioned.
|
|
245
|
+
const container = document.createElement('div')
|
|
246
|
+
container.dataset.dshPetRoot = ''
|
|
247
|
+
document.body.appendChild(container)
|
|
248
|
+
const petRoot = createRoot(container)
|
|
249
|
+
petRoot.render(createElement(PetDockEntry, { ...injected(), t }))
|
|
248
250
|
|
|
249
251
|
disposeUi = () => {
|
|
250
|
-
|
|
252
|
+
petRoot.unmount()
|
|
253
|
+
container.remove()
|
|
251
254
|
disposePoll()
|
|
252
255
|
disposeUi = undefined
|
|
253
256
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -100,6 +100,35 @@ export type PetKey = keyof typeof zh
|
|
|
100
100
|
/** The settings-card slice of the pet dictionary. */
|
|
101
101
|
export type SettingsCardKey = PetKey
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Active dictionary, picked by the document language at call time. The pet
|
|
105
|
+
* mounts as a global floating surface (not a session-scoped slot), so it has
|
|
106
|
+
* no framework locale seat and resolves its copy the same tiny way the
|
|
107
|
+
* task-board's DOM-injected surface does.
|
|
108
|
+
*/
|
|
109
|
+
export function dictionary(): Record<PetKey, string> {
|
|
110
|
+
const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
|
|
111
|
+
return lang.toLowerCase().startsWith('en') ? en : zh
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Translate a key with optional `{name}` template params. Mirrors the slot
|
|
116
|
+
* `Translate` contract `(key, params?) => string` so it can be handed to the
|
|
117
|
+
* same components that used to receive the framework-injected `t` seat. The
|
|
118
|
+
* key is typed loosely (`string`) so the function is assignable to the slot's
|
|
119
|
+
* `TranslateNS<'pet'>` (whose key domain also spans the shared common
|
|
120
|
+
* vocabulary); a missing key degrades to the key itself rather than throwing.
|
|
121
|
+
*/
|
|
122
|
+
export function t(key: string, params?: Record<string, unknown>): string {
|
|
123
|
+
let text: string = (dictionary() as Record<string, string>)[key] ?? key
|
|
124
|
+
if (params !== undefined) {
|
|
125
|
+
for (const [name, value] of Object.entries(params)) {
|
|
126
|
+
text = text.replaceAll(`{${name}}`, String(value))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return text
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
104
133
|
interface LocaleNamespaceMap {
|
|
105
134
|
/** dsh-pet UI copy. */
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
.panel {
|
|
62
62
|
position: absolute;
|
|
63
|
-
bottom:
|
|
63
|
+
bottom: 100%;
|
|
64
64
|
background: rgba(15, 23, 42, 0.92);
|
|
65
65
|
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
66
66
|
border-radius: 10px;
|
|
@@ -75,6 +75,20 @@
|
|
|
75
75
|
min-width: 132px;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/* 面板底边与图标顶边之间的 hover 桥接(issue #49):
|
|
79
|
+
面板绝对定位在图标上方(bottom: 100%),若两者之间存在哪怕几个
|
|
80
|
+
像素的间隙,指针滑过间隙时会触发容器的 pointerleave,面板在指针
|
|
81
|
+
抵达按钮前就收起。这段透明伪元素把命中区向下延伸、覆盖图标顶部,
|
|
82
|
+
使指针在图标与按钮之间始终停留在 hover 区域内。 */
|
|
83
|
+
.panel::after {
|
|
84
|
+
content: '';
|
|
85
|
+
position: absolute;
|
|
86
|
+
top: 100%;
|
|
87
|
+
left: 0;
|
|
88
|
+
right: 0;
|
|
89
|
+
height: 14px;
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
.rankRow {
|
|
79
93
|
display: flex;
|
|
80
94
|
justify-content: space-between;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
2
|
-
interface SlotMap {
|
|
3
|
-
/**
|
|
4
|
-
* The input selector context-chip hole: feature chips rendered right
|
|
5
|
-
* after the workspace selector. Session-maybe: entries stay mounted
|
|
6
|
-
* without a session and hide themselves when their data source is absent.
|
|
7
|
-
*/
|
|
8
|
-
'conversation.input.selector.context': {
|
|
9
|
-
kind: 'list';
|
|
10
|
-
scope: 'session-maybe';
|
|
11
|
-
owner: InputSelectorContextOwnerProps;
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
/** Owner share of the input selector context-chip hole (empty by contract). */
|
|
15
|
-
interface InputSelectorContextOwnerProps {
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
export {};
|
|
19
|
-
//# sourceMappingURL=slots-augment.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"slots-augment.d.ts","sourceRoot":"","sources":["../../../src/client/slots-augment.ts"],"names":[],"mappings":"AAUA,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,OAAO;QACf;;;;WAIG;QACH,qCAAqC,EAAE;YACrC,IAAI,EAAE,MAAM,CAAA;YACZ,KAAK,EAAE,eAAe,CAAA;YACtB,KAAK,EAAE,8BAA8B,CAAA;SACtC,CAAA;KACF;IAED,+EAA+E;IAC/E,UAAU,8BAA8B;KAAG;CAC5C"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local slot-table augmentation for slots the official NPM SDK
|
|
3
|
-
* (ui-conversation rc.6) does not publish yet, but the running dsh worktree
|
|
4
|
-
* boots with (`conversation.input.selector.*`). Type-only supplement — the
|
|
5
|
-
* runtime surface comes from the dsh profile. Module form (with an import)
|
|
6
|
-
* so TS merges into the package declarations instead of shadowing them.
|
|
7
|
-
* Remove once the SDK ships these slots.
|
|
8
|
-
*/
|
|
9
|
-
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
10
|
-
|
|
11
|
-
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
12
|
-
interface SlotMap {
|
|
13
|
-
/**
|
|
14
|
-
* The input selector context-chip hole: feature chips rendered right
|
|
15
|
-
* after the workspace selector. Session-maybe: entries stay mounted
|
|
16
|
-
* without a session and hide themselves when their data source is absent.
|
|
17
|
-
*/
|
|
18
|
-
'conversation.input.selector.context': {
|
|
19
|
-
kind: 'list'
|
|
20
|
-
scope: 'session-maybe'
|
|
21
|
-
owner: InputSelectorContextOwnerProps
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Owner share of the input selector context-chip hole (empty by contract). */
|
|
26
|
-
interface InputSelectorContextOwnerProps {}
|
|
27
|
-
}
|