@khorsheed/dsh-game 0.0.0-stage → 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/CHANGELOG.md +25 -0
- package/README.en.md +53 -0
- package/README.i18n.yaml +6 -0
- package/README.md +52 -2
- package/cordis.patch.yml +23 -0
- package/lib/client.js +30690 -0
- package/lib/index.js +4297 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3876 -0
- package/lib/typert.remote-client.d.ts +104 -0
- package/lib/typert.remote-client.js +2138 -0
- package/lib/types/agent.d.ts +20 -0
- package/lib/types/agent.js +43 -0
- package/lib/types/checks.d.ts +44 -0
- package/lib/types/checks.js +267 -0
- package/lib/types/client/DesignView.d.ts +22 -0
- package/lib/types/client/DesignView.js +134 -0
- package/lib/types/client/ElementsView.d.ts +21 -0
- package/lib/types/client/ElementsView.js +195 -0
- package/lib/types/client/GameTab.d.ts +14 -0
- package/lib/types/client/GameTab.js +261 -0
- package/lib/types/client/PlayFrame.d.ts +54 -0
- package/lib/types/client/PlayFrame.js +129 -0
- package/lib/types/client/PlayView.d.ts +19 -0
- package/lib/types/client/PlayView.js +163 -0
- package/lib/types/client/Preview3D.d.ts +52 -0
- package/lib/types/client/Preview3D.js +178 -0
- package/lib/types/client/ProjectSwitcher.d.ts +20 -0
- package/lib/types/client/ProjectSwitcher.js +75 -0
- package/lib/types/client/Recipe3DEditor.d.ts +19 -0
- package/lib/types/client/Recipe3DEditor.js +44 -0
- package/lib/types/client/StyleGallery.d.ts +18 -0
- package/lib/types/client/StyleGallery.js +27 -0
- package/lib/types/client/TurnCard.d.ts +62 -0
- package/lib/types/client/TurnCard.js +100 -0
- package/lib/types/client/VersionsView.d.ts +17 -0
- package/lib/types/client/VersionsView.js +68 -0
- package/lib/types/client/chat-node.d.ts +42 -0
- package/lib/types/client/chat-node.js +77 -0
- package/lib/types/client/contract.d.ts +131 -0
- package/lib/types/client/contract.js +6 -0
- package/lib/types/client/definition.d.ts +22 -0
- package/lib/types/client/definition.js +28 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/index.js +263 -0
- package/lib/types/client/locales.d.ts +379 -0
- package/lib/types/client/locales.js +733 -0
- package/lib/types/client/looks.d.ts +36 -0
- package/lib/types/client/looks.js +105 -0
- package/lib/types/client/markdown.d.ts +11 -0
- package/lib/types/client/markdown.js +52 -0
- package/lib/types/client/preset-visibility.d.ts +94 -0
- package/lib/types/client/preset-visibility.js +115 -0
- package/lib/types/client/preview-entry.d.ts +24 -0
- package/lib/types/client/preview-entry.js +110 -0
- package/lib/types/client/refs.d.ts +36 -0
- package/lib/types/client/refs.js +44 -0
- package/lib/types/client/reveal.d.ts +43 -0
- package/lib/types/client/reveal.js +73 -0
- package/lib/types/client/style-samples.d.ts +5 -0
- package/lib/types/client/style-samples.js +196 -0
- package/lib/types/fsutil.d.ts +26 -0
- package/lib/types/fsutil.js +104 -0
- package/lib/types/hooks.d.ts +83 -0
- package/lib/types/hooks.js +108 -0
- package/lib/types/index.d.ts +30 -0
- package/lib/types/index.js +22 -0
- package/lib/types/kits/canvas2d.d.ts +6 -0
- package/lib/types/kits/canvas2d.js +60 -0
- package/lib/types/kits/ids.d.ts +11 -0
- package/lib/types/kits/ids.js +6 -0
- package/lib/types/kits/index.d.ts +26 -0
- package/lib/types/kits/index.js +28 -0
- package/lib/types/kits/three3d.d.ts +12 -0
- package/lib/types/kits/three3d.js +188 -0
- package/lib/types/kits/types.d.ts +52 -0
- package/lib/types/kits/types.js +6 -0
- package/lib/types/look3d.d.ts +20 -0
- package/lib/types/look3d.js +175 -0
- package/lib/types/panel-session.d.ts +21 -0
- package/lib/types/panel-session.js +16 -0
- package/lib/types/project.d.ts +138 -0
- package/lib/types/project.js +504 -0
- package/lib/types/remote.d.ts +184 -0
- package/lib/types/remote.js +263 -0
- package/lib/types/skills.d.ts +13 -0
- package/lib/types/skills.js +141 -0
- package/lib/types/store.d.ts +346 -0
- package/lib/types/store.js +1383 -0
- package/lib/types/tools.d.ts +12 -0
- package/lib/types/tools.js +309 -0
- package/lib/types/trash.d.ts +5 -0
- package/lib/types/trash.js +64 -0
- package/lib/types/types.d.ts +548 -0
- package/lib/types/types.js +85 -0
- package/lib/types/vfs.d.ts +94 -0
- package/lib/types/vfs.js +217 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +132 -3
- package/runtime/dsh3d.js +1279 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The project switcher: every game as a card — the last check's screen, the
|
|
4
|
+
* title, live version, art style, element count, last change and the pitch —
|
|
5
|
+
* with the session's own project marked. The first card starts a new game:
|
|
6
|
+
* creation goes through the agent (the light gate needs the user's pitch,
|
|
7
|
+
* element kinds and style), so it writes an opener into the composer.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
10
|
+
import { artStyleLabel, artStyleName } from "../project.js";
|
|
11
|
+
import { failureOf } from "./contract.js";
|
|
12
|
+
import css from './GameTab.module.css';
|
|
13
|
+
/** "3 分钟前" style relative time. */
|
|
14
|
+
function ago(t, iso) {
|
|
15
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
16
|
+
if (!Number.isFinite(ms))
|
|
17
|
+
return '';
|
|
18
|
+
const min = Math.round(ms / 60000);
|
|
19
|
+
if (min < 1)
|
|
20
|
+
return t('switcher.justNow');
|
|
21
|
+
if (min < 60)
|
|
22
|
+
return t('switcher.minutes', { count: min });
|
|
23
|
+
const h = Math.round(min / 60);
|
|
24
|
+
if (h < 24)
|
|
25
|
+
return t('switcher.hours', { count: h });
|
|
26
|
+
return t('switcher.days', { count: Math.round(h / 24) });
|
|
27
|
+
}
|
|
28
|
+
/** The last path segment (the project folder), the full path goes in the tooltip. */
|
|
29
|
+
function folderOf(root) {
|
|
30
|
+
return root.split(/[\\/]/).filter(Boolean).pop() ?? root;
|
|
31
|
+
}
|
|
32
|
+
/** The card grid. */
|
|
33
|
+
export function ProjectSwitcher({ t, api, sessionId, currentId, rev, onChoose, onNew, refresh, toast }) {
|
|
34
|
+
const [cards, setCards] = useState(null);
|
|
35
|
+
const [reload, setReload] = useState(0);
|
|
36
|
+
// The card being deleted: its inline confirmation, and the last failure to explain there.
|
|
37
|
+
const [confirming, setConfirming] = useState(null);
|
|
38
|
+
const [busy, setBusy] = useState(false);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let live = true;
|
|
41
|
+
void api.projectCards().then(r => { if (live)
|
|
42
|
+
setCards(r.ok ? r.value : []); }).catch(() => { if (live)
|
|
43
|
+
setCards([]); });
|
|
44
|
+
return () => { live = false; };
|
|
45
|
+
}, [api, rev, reload]);
|
|
46
|
+
const remove = (card, mode) => {
|
|
47
|
+
setBusy(true);
|
|
48
|
+
void api.deleteProject(sessionId, card.id, mode).then(r => {
|
|
49
|
+
setBusy(false);
|
|
50
|
+
const failure = failureOf(r);
|
|
51
|
+
if (failure !== undefined) {
|
|
52
|
+
const why = r.ok && !r.value.ok ? r.value.reason ?? '' : '';
|
|
53
|
+
const reason = failure === 'trash-failed' ? t('switcher.deleteTrashFailed', { reason: why })
|
|
54
|
+
: failure === 'not-a-project' ? t('switcher.deleteNotProject')
|
|
55
|
+
: failure === 'is-workspace' ? t('switcher.deleteIsWorkspace')
|
|
56
|
+
: failure === 'denied' ? t('switcher.deleteDenied') : failure;
|
|
57
|
+
setConfirming({ id: card.id, error: reason });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setConfirming(null);
|
|
61
|
+
toast(mode === 'trash' ? t('switcher.deletedTrash', { title: card.title }) : t('switcher.deletedForget', { title: card.title }));
|
|
62
|
+
setReload(n => n + 1);
|
|
63
|
+
refresh();
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
return _jsxs("div", { className: css.projectGrid, children: [_jsxs("button", { type: "button", className: css.newCard, onClick: onNew, children: [_jsx("span", { className: css.newPlus, "aria-hidden": "true", children: "\uFF0B" }), _jsx("strong", { children: t('switcher.new') }), _jsx("span", { className: css.metaLine, children: t('switcher.newHint') })] }), cards === null && _jsx("div", { className: css.projectCard, "data-loading": "true", children: _jsx("span", { className: css.metaLine, children: t('loading') }) }), cards?.map(card => {
|
|
67
|
+
const current = card.id === currentId;
|
|
68
|
+
if (confirming?.id === card.id) {
|
|
69
|
+
return _jsx("div", { className: css.projectCard, "data-confirm": "true", role: "group", "aria-label": t('switcher.deleteTitle', { title: card.title }), children: _jsxs("span", { className: css.projectBody, children: [_jsx("strong", { className: css.projectTitle, children: t('switcher.deleteTitle', { title: card.title }) }), _jsx("span", { className: css.metaLine, children: t('switcher.deleteBody', { folder: card.root }) }), confirming.error !== undefined && _jsx("span", { className: css.deleteError, children: confirming.error }), _jsxs("span", { className: css.deleteActions, children: [_jsx("button", { type: "button", className: css.danger, disabled: busy, onClick: () => { remove(card, 'trash'); }, children: t('switcher.deleteTrash') }), _jsx("button", { type: "button", className: css.ghost, disabled: busy, onClick: () => { remove(card, 'forget'); }, children: t('switcher.deleteForget') }), _jsx("button", { type: "button", className: css.link, disabled: busy, onClick: () => { setConfirming(null); }, children: t('switcher.deleteCancel') })] })] }) }, card.id);
|
|
70
|
+
}
|
|
71
|
+
return _jsxs("div", { className: css.projectCardWrap, children: [_jsxs("button", { type: "button", className: css.projectCard, "aria-current": current ? 'true' : undefined, title: card.root, onClick: () => { onChoose(card.id); }, children: [_jsxs("span", { className: css.projectThumb, children: [card.thumb !== undefined
|
|
72
|
+
? _jsx("img", { src: `data:image/webp;base64,${card.thumb}`, alt: "" })
|
|
73
|
+
: _jsx("span", { className: css.projectInitial, children: card.title.slice(0, 1) }), current && _jsx("span", { className: css.projectHere, children: t('switcher.here') })] }), _jsxs("span", { className: css.projectBody, children: [_jsx("strong", { className: css.projectTitle, children: card.title }), _jsxs("span", { className: css.projectChips, children: [_jsx("span", { className: css.headChip, "data-tone": card.version === null ? 'muted' : 'ok', children: card.version === null ? t('switcher.noVersion') : `v${card.version}` }), card.artStyle !== '' && _jsx("span", { className: css.headChip, title: artStyleLabel(card.artStyle), children: artStyleName(card.artStyle) }), _jsx("span", { className: css.headChip, children: t('switcher.elements', { count: card.elements }) })] }), card.pitch !== '' && _jsx("span", { className: css.projectPitch, children: card.pitch }), _jsxs("span", { className: css.metaLine, children: [ago(t, card.updatedAt), " \u00B7 ", folderOf(card.root)] })] })] }), _jsx("button", { type: "button", className: css.projectDelete, "aria-label": t('switcher.delete', { title: card.title }), title: t('switcher.delete', { title: card.title }), onClick: () => { setConfirming({ id: card.id }); }, children: "\u00D7" })] }, card.id);
|
|
74
|
+
})] });
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 3D look editor: a `parts3d` recipe as sliders and color wells,
|
|
3
|
+
* generated from the recipe itself — proportions, every part's numbers
|
|
4
|
+
* (built-in keys and a project part's own `extra` numbers alike), palette,
|
|
5
|
+
* motions and scale. Nothing here knows a theme: a project's own bodies and
|
|
6
|
+
* parts get the same controls as the built-ins.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
9
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
|
|
10
|
+
import type { Parts3DLook } from '../types.ts';
|
|
11
|
+
/** Props of {@link Recipe3DEditor}. */
|
|
12
|
+
export interface Recipe3DEditorProps {
|
|
13
|
+
readonly t: TranslateNS<'game'>;
|
|
14
|
+
readonly look: Parts3DLook;
|
|
15
|
+
readonly onChange: (next: Parts3DLook) => void;
|
|
16
|
+
}
|
|
17
|
+
/** The recipe editor. */
|
|
18
|
+
export declare function Recipe3DEditor({ t, look, onChange }: Recipe3DEditorProps): ReactNode;
|
|
19
|
+
//# sourceMappingURL=Recipe3DEditor.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { zh } from "./locales.js";
|
|
3
|
+
import css from './GameTab.module.css';
|
|
4
|
+
/** Integer counts and their ranges (the validator's, mirrored). */
|
|
5
|
+
const COUNT_RANGE = { legs: [0, 8], heads: [1, 9], segments: [3, 40], count: [1, 12] };
|
|
6
|
+
/** Part numbers and their ranges (the validator's, mirrored). */
|
|
7
|
+
const PART_RANGE = {
|
|
8
|
+
size: [0.05, 3, 0.01], length: [0.05, 3, 0.01], fan: [0, 360, 5], thickness: [0.01, 0.5, 0.005],
|
|
9
|
+
};
|
|
10
|
+
/** The controls each built-in part always gets, with the value dsh3d uses when the recipe leaves it out. */
|
|
11
|
+
const PART_DEFAULTS = {
|
|
12
|
+
ear: { count: 2, size: 0.15 }, horn: { count: 2, length: 0.25 }, tail: { count: 1, length: 0.9, fan: 0, thickness: 0.09 },
|
|
13
|
+
wing: { count: 2, size: 1 }, fin: { count: 2, size: 1 }, eye: { count: 2 },
|
|
14
|
+
};
|
|
15
|
+
/** A label for a recipe key: the dictionary's when it has one, else the key itself. */
|
|
16
|
+
function labelOf(t, key) {
|
|
17
|
+
const k = `recipe.key.${key}`;
|
|
18
|
+
return Object.hasOwn(zh, k) ? t(k) : key;
|
|
19
|
+
}
|
|
20
|
+
function Slider({ label, value, min, max, step, onChange }) {
|
|
21
|
+
return _jsxs("label", { className: css.param, children: [_jsx("span", { className: css.paramLabel, children: label }), _jsx("input", { type: "range", className: css.range, min: min, max: max, step: step, value: value, onChange: e => { onChange(Number(e.target.value)); } }), _jsx("span", { className: css.paramValue, children: step >= 1 ? String(Math.round(value)) : value.toFixed(step < 0.01 ? 3 : 2) })] });
|
|
22
|
+
}
|
|
23
|
+
/** The recipe editor. */
|
|
24
|
+
export function Recipe3DEditor({ t, look, onChange }) {
|
|
25
|
+
const setProportion = (key, v) => { onChange({ ...look, proportions: { ...look.proportions, [key]: v } }); };
|
|
26
|
+
const setPart = (index, patch) => {
|
|
27
|
+
onChange({ ...look, parts: look.parts.map((p, i) => (i === index ? { ...p, ...patch } : p)) });
|
|
28
|
+
};
|
|
29
|
+
const setMotion = (name, patch) => {
|
|
30
|
+
onChange({ ...look, motion: { ...look.motion, [name]: { ...look.motion[name], ...patch } } });
|
|
31
|
+
};
|
|
32
|
+
const proportions = Object.entries(look.proportions);
|
|
33
|
+
const motions = Object.entries(look.motion);
|
|
34
|
+
return _jsxs("div", { className: css.recipe, children: [_jsx("p", { className: css.metaLine, children: t('recipe.base', { base: labelOf(t, look.base) }) }), proportions.length > 0 && _jsxs("section", { className: css.recipeGroup, children: [_jsx("h4", { className: css.blockTitle, children: t('recipe.proportions') }), proportions.map(([key, value]) => {
|
|
35
|
+
const count = COUNT_RANGE[key];
|
|
36
|
+
return _jsx(Slider, { label: labelOf(t, key), value: value, min: count?.[0] ?? 0.05, max: count?.[1] ?? 3, step: count === undefined ? 0.01 : 1, onChange: v => { setProportion(key, v); } }, key);
|
|
37
|
+
})] }), look.parts.length > 0 && _jsxs("section", { className: css.recipeGroup, children: [_jsx("h4", { className: css.blockTitle, children: t('recipe.parts') }), look.parts.map((part, index) => _jsxs("div", { className: css.recipePart, children: [_jsx("strong", { className: css.recipePartName, children: labelOf(t, part.type) }), (part.count !== undefined || PART_DEFAULTS[part.type]?.count !== undefined) && _jsx(Slider, { label: labelOf(t, 'count'), value: part.count ?? PART_DEFAULTS[part.type].count, min: 1, max: 12, step: 1, onChange: v => { setPart(index, { count: v }); } }), ['size', 'length', 'fan', 'thickness'].filter(k => part[k] !== undefined || PART_DEFAULTS[part.type]?.[k] !== undefined).map(k => {
|
|
38
|
+
const [min, max, step] = PART_RANGE[k];
|
|
39
|
+
return _jsx(Slider, { label: labelOf(t, k), value: part[k] ?? PART_DEFAULTS[part.type][k], min: min, max: max, step: step, onChange: v => { setPart(index, { [k]: v }); } }, k);
|
|
40
|
+
}), Object.entries(part.extra ?? {}).filter((e) => typeof e[1] === 'number').map(([k, v]) => {
|
|
41
|
+
const span = Math.max(1, Math.abs(v) * 2);
|
|
42
|
+
return _jsx(Slider, { label: labelOf(t, k), value: v, min: v < 0 ? -span : 0, max: span, step: span > 20 ? 1 : 0.01, onChange: next => { setPart(index, { extra: { ...part.extra, [k]: next } }); } }, k);
|
|
43
|
+
})] }, index))] }), Object.keys(look.palette).length > 0 && _jsxs("section", { className: css.recipeGroup, children: [_jsx("h4", { className: css.blockTitle, children: t('recipe.palette') }), _jsx("div", { className: css.recipeColors, children: Object.entries(look.palette).map(([key, color]) => _jsxs("label", { className: css.recipeColor, children: [_jsx("input", { type: "color", value: color, onChange: e => { onChange({ ...look, palette: { ...look.palette, [key]: e.target.value } }); } }), _jsx("span", { children: labelOf(t, key) })] }, key)) })] }), motions.length > 0 && _jsxs("section", { className: css.recipeGroup, children: [_jsx("h4", { className: css.blockTitle, children: t('recipe.motion') }), motions.map(([name, m]) => _jsxs("div", { className: css.recipePart, children: [_jsx("strong", { className: css.recipePartName, children: labelOf(t, name) }), _jsx(Slider, { label: labelOf(t, 'amplitude'), value: m.amplitude ?? 0, min: 0, max: name === 'breath' || name === 'bob' ? 0.5 : 90, step: name === 'breath' || name === 'bob' ? 0.005 : 1, onChange: v => { setMotion(name, { amplitude: v }); } }), _jsx(Slider, { label: labelOf(t, 'period'), value: m.period ?? 1.5, min: 0.1, max: 10, step: 0.1, onChange: v => { setMotion(name, { period: v }); } })] }, name))] }), _jsx("section", { className: css.recipeGroup, children: _jsx(Slider, { label: labelOf(t, 'scale'), value: look.scale ?? 1, min: 0.2, max: 5, step: 0.05, onChange: v => { onChange({ ...look, scale: v }); } }) })] });
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The art-style gallery (decision D10): the same little scene drawn in each
|
|
3
|
+
* built-in style, so the choice is made by looking. Before a project exists
|
|
4
|
+
* a pick puts 「美术风格选 X」 into the composer; inside a project it asks
|
|
5
|
+
* for a restyle — everything is redrawn and the result always waits as a
|
|
6
|
+
* candidate.
|
|
7
|
+
*/
|
|
8
|
+
import { type ReactNode } from 'react';
|
|
9
|
+
import type { ViewProps } from './contract.ts';
|
|
10
|
+
/** Props of the gallery. */
|
|
11
|
+
export interface StyleGalleryProps {
|
|
12
|
+
readonly t: ViewProps['t'];
|
|
13
|
+
readonly current?: string | undefined;
|
|
14
|
+
readonly onPick: (styleId: string, label: string) => void;
|
|
15
|
+
}
|
|
16
|
+
/** The gallery grid. */
|
|
17
|
+
export declare function StyleGallery({ t, current, onPick }: StyleGalleryProps): ReactNode;
|
|
18
|
+
//# sourceMappingURL=StyleGallery.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The art-style gallery (decision D10): the same little scene drawn in each
|
|
4
|
+
* built-in style, so the choice is made by looking. Before a project exists
|
|
5
|
+
* a pick puts 「美术风格选 X」 into the composer; inside a project it asks
|
|
6
|
+
* for a restyle — everything is redrawn and the result always waits as a
|
|
7
|
+
* candidate.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useRef } from 'react';
|
|
10
|
+
import { ART_STYLES } from "../types.js";
|
|
11
|
+
import { drawStyleSample } from "./style-samples.js";
|
|
12
|
+
import css from './GameTab.module.css';
|
|
13
|
+
/** One sample canvas. */
|
|
14
|
+
function Sample({ styleId }) {
|
|
15
|
+
const ref = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const canvas = ref.current;
|
|
18
|
+
const g = canvas?.getContext('2d');
|
|
19
|
+
if (canvas && g)
|
|
20
|
+
drawStyleSample(g, styleId, canvas.width, canvas.height);
|
|
21
|
+
}, [styleId]);
|
|
22
|
+
return _jsx("canvas", { ref: ref, className: css.sample, width: 200, height: 112, "aria-hidden": "true" });
|
|
23
|
+
}
|
|
24
|
+
/** The gallery grid. */
|
|
25
|
+
export function StyleGallery({ t, current, onPick }) {
|
|
26
|
+
return _jsx("div", { className: css.gallery, children: ART_STYLES.map(style => _jsxs("button", { type: "button", className: css.styleCard, "aria-pressed": current === style.id, onClick: () => { onPick(style.id, style.label); }, children: [_jsx(Sample, { styleId: style.id }), _jsxs("span", { className: css.styleName, children: [style.label, current === style.id && _jsx("span", { className: css.badge, children: t('style.current') })] }), _jsx("span", { className: css.metaLine, children: style.look }), _jsx("span", { className: css.paletteStrip, "aria-hidden": "true", children: style.palette.slice(0, 8).map(c => _jsx("span", { style: { background: c } }, c)) })] }, style.id)) });
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn card: beneath each finished agent turn (the `conversation.chat.turnTail`
|
|
3
|
+
* seat, outside the fold — the canvas-novel precedent), what the turn changed
|
|
4
|
+
* in the game: the version that went live or waits as a candidate, the
|
|
5
|
+
* check, elements, design chapters, parameters, code and tests — each with a
|
|
6
|
+
* jump to the place in the game tab where the change can be seen.
|
|
7
|
+
*/
|
|
8
|
+
import { type ReactNode } from 'react';
|
|
9
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
|
|
10
|
+
import type { GameTabInjected } from './contract.ts';
|
|
11
|
+
/** The key this package's turn data lives under. */
|
|
12
|
+
export declare const GAME_TURN_KEY = "dsh-game-turn";
|
|
13
|
+
interface EventLike {
|
|
14
|
+
readonly type: string;
|
|
15
|
+
readonly data?: unknown;
|
|
16
|
+
}
|
|
17
|
+
interface TurnState {
|
|
18
|
+
readonly turn: number;
|
|
19
|
+
readonly ended: boolean;
|
|
20
|
+
}
|
|
21
|
+
interface LocationData {
|
|
22
|
+
readonly kind: 'turn';
|
|
23
|
+
readonly turn: number;
|
|
24
|
+
readonly key: string;
|
|
25
|
+
readonly value: TurnState;
|
|
26
|
+
}
|
|
27
|
+
/** Tracks each turn's number and whether it ended; the report itself comes from the host. */
|
|
28
|
+
export declare const gameTurnDefinition: {
|
|
29
|
+
kind: string;
|
|
30
|
+
match(event: EventLike): {
|
|
31
|
+
id: string;
|
|
32
|
+
role: "start" | "update";
|
|
33
|
+
} | null;
|
|
34
|
+
start(_context: unknown, match: {
|
|
35
|
+
event: EventLike;
|
|
36
|
+
}): TurnState;
|
|
37
|
+
update(context: {
|
|
38
|
+
readonly state: TurnState;
|
|
39
|
+
}, match: {
|
|
40
|
+
event: EventLike;
|
|
41
|
+
}): TurnState;
|
|
42
|
+
buildLocationData(context: {
|
|
43
|
+
readonly state: TurnState | undefined;
|
|
44
|
+
}, scope: string, previous: LocationData | null | undefined): LocationData | null;
|
|
45
|
+
};
|
|
46
|
+
/** Props of the card (ui-chat's turn-tail owner props, structurally, plus our face). */
|
|
47
|
+
export type GameTurnCardProps = GameTabInjected & {
|
|
48
|
+
readonly sessionId: string;
|
|
49
|
+
readonly turn: {
|
|
50
|
+
readonly data: {
|
|
51
|
+
get: (key: string) => unknown;
|
|
52
|
+
};
|
|
53
|
+
readonly end?: {
|
|
54
|
+
readonly seq: number;
|
|
55
|
+
} | null;
|
|
56
|
+
};
|
|
57
|
+
readonly t: TranslateNS<'game'>;
|
|
58
|
+
};
|
|
59
|
+
/** The card. Renders nothing until the turn ended and only when it changed something. */
|
|
60
|
+
export declare function GameTurnCard(props: GameTurnCardProps): ReactNode;
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=TurnCard.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The turn card: beneath each finished agent turn (the `conversation.chat.turnTail`
|
|
4
|
+
* seat, outside the fold — the canvas-novel precedent), what the turn changed
|
|
5
|
+
* in the game: the version that went live or waits as a candidate, the
|
|
6
|
+
* check, elements, design chapters, parameters, code and tests — each with a
|
|
7
|
+
* jump to the place in the game tab where the change can be seen.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
10
|
+
import { normalizeLook3d } from "../look3d.js";
|
|
11
|
+
import { Preview3D } from "./Preview3D.js";
|
|
12
|
+
import css from './TurnCard.module.css';
|
|
13
|
+
/** The key this package's turn data lives under. */
|
|
14
|
+
export const GAME_TURN_KEY = 'dsh-game-turn';
|
|
15
|
+
function turnOf(event) {
|
|
16
|
+
const turn = event.data?.turn;
|
|
17
|
+
return typeof turn === 'number' ? turn : null;
|
|
18
|
+
}
|
|
19
|
+
/** Tracks each turn's number and whether it ended; the report itself comes from the host. */
|
|
20
|
+
export const gameTurnDefinition = {
|
|
21
|
+
kind: GAME_TURN_KEY,
|
|
22
|
+
match(event) {
|
|
23
|
+
const turn = turnOf(event);
|
|
24
|
+
if (turn === null)
|
|
25
|
+
return null;
|
|
26
|
+
if (event.type === 'turn/start')
|
|
27
|
+
return { id: String(turn), role: 'start' };
|
|
28
|
+
if (event.type === 'turn/end')
|
|
29
|
+
return { id: String(turn), role: 'update' };
|
|
30
|
+
return null;
|
|
31
|
+
},
|
|
32
|
+
start(_context, match) {
|
|
33
|
+
return { turn: turnOf(match.event) ?? -1, ended: false };
|
|
34
|
+
},
|
|
35
|
+
update(context, match) {
|
|
36
|
+
return match.event.type === 'turn/end' ? { ...context.state, ended: true } : context.state;
|
|
37
|
+
},
|
|
38
|
+
buildLocationData(context, scope, previous) {
|
|
39
|
+
if (scope !== 'turn' || context.state === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
if (previous?.kind === 'turn' && previous.turn === context.state.turn && previous.value.ended === context.state.ended)
|
|
42
|
+
return previous;
|
|
43
|
+
return { kind: 'turn', turn: context.state.turn, key: GAME_TURN_KEY, value: context.state };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/** One line of the card. */
|
|
47
|
+
function Row({ icon, tone, children, go, t, onGo }) {
|
|
48
|
+
return _jsxs("li", { className: css.row, "data-tone": tone, children: [_jsx("span", { className: css.icon, "aria-hidden": "true", children: icon }), _jsx("span", { className: css.text, children: children }), go !== undefined && _jsx("button", { type: "button", className: css.go, onClick: () => { onGo(go); }, children: t('turn.see') })] });
|
|
49
|
+
}
|
|
50
|
+
/** A 3D look's change as two stills side by side (rendered lazily in the sandbox, then cached as images). */
|
|
51
|
+
function BeforeAfter({ t, api, projectId, version, looks }) {
|
|
52
|
+
const parse = (json) => {
|
|
53
|
+
if (json === null)
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
return normalizeLook3d(JSON.parse(json));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const before = parse(looks.before), after = parse(looks.after);
|
|
63
|
+
if (after === null)
|
|
64
|
+
return null;
|
|
65
|
+
return _jsxs("span", { className: css.beforeAfter, children: [before !== null && _jsxs("figure", { children: [_jsx(Preview3D, { api: api, projectId: projectId, version: version, look: before, mode: "still", className: css.still, emptyLabel: "" }), _jsx("figcaption", { children: t('turn.lookBefore') })] }), before !== null && _jsx("span", { className: css.arrow, "aria-hidden": "true", children: "\u2192" }), _jsxs("figure", { children: [_jsx(Preview3D, { api: api, projectId: projectId, version: version, look: after, mode: "still", className: css.still, emptyLabel: "" }), _jsx("figcaption", { children: t('turn.lookAfter') })] })] });
|
|
66
|
+
}
|
|
67
|
+
/** The card. Renders nothing until the turn ended and only when it changed something. */
|
|
68
|
+
export function GameTurnCard(props) {
|
|
69
|
+
const { t, api, sessionId, openAt, subscribeSessionEvents } = props;
|
|
70
|
+
let state;
|
|
71
|
+
try {
|
|
72
|
+
state = props.turn.data.get(GAME_TURN_KEY) ?? undefined;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
state = undefined;
|
|
76
|
+
}
|
|
77
|
+
const turn = state?.turn;
|
|
78
|
+
const ended = state?.ended === true || props.turn.end != null;
|
|
79
|
+
const [report, setReport] = useState(null);
|
|
80
|
+
const [rev, setRev] = useState(0);
|
|
81
|
+
useEffect(() => subscribeSessionEvents(id => { if (id === sessionId)
|
|
82
|
+
setRev(n => n + 1); }), [subscribeSessionEvents, sessionId]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (turn === undefined || !ended)
|
|
85
|
+
return;
|
|
86
|
+
let live = true;
|
|
87
|
+
// The host writes the report right after turn/end; a second read covers the race.
|
|
88
|
+
const read = () => api.turnReport(sessionId, turn).then(r => { if (live && r.ok)
|
|
89
|
+
setReport(r.value); }).catch(() => { });
|
|
90
|
+
void read();
|
|
91
|
+
const again = setTimeout(() => { void read(); }, 1500);
|
|
92
|
+
return () => { live = false; clearTimeout(again); };
|
|
93
|
+
}, [api, sessionId, turn, ended, rev]);
|
|
94
|
+
if (report === null)
|
|
95
|
+
return null;
|
|
96
|
+
const looks = report.elements.filter(e => e.what === 'look' || e.what === 'new');
|
|
97
|
+
const fieldOnly = report.elements.filter(e => e.what === 'fields');
|
|
98
|
+
const removed = report.elements.filter(e => e.what === 'removed');
|
|
99
|
+
return _jsxs("section", { className: css.card, "aria-label": t('turn.title', { title: report.title }), children: [_jsxs("header", { className: css.head, children: [_jsx("span", { className: css.badge, "aria-hidden": "true", children: "\u25C6" }), _jsx("strong", { children: t('turn.title', { title: report.title }) }), _jsx("span", { className: css.spacer }), _jsx("button", { type: "button", className: css.open, onClick: () => { openAt({ view: 'play' }); }, children: t('turn.openPanel') })] }), _jsxs("ul", { className: css.list, children: [report.version !== null && _jsxs(Row, { t: t, onGo: openAt, icon: "\u25B6", tone: report.version.kind === 'deployed' ? 'ok' : 'warn', go: { view: 'play' }, children: [report.version.kind === 'deployed' ? t('turn.deployed', { version: report.version.version }) : t('turn.candidate', { version: report.version.version }), report.check !== null && report.check.ok && _jsxs("span", { className: css.meta, children: [t('turn.checkOk', { passed: report.check.passed }), report.check.fixes > 0 ? ` · ${t('turn.fixed', { count: report.check.fixes })}` : ''] })] }), report.check !== null && !report.check.ok && _jsx(Row, { t: t, onGo: openAt, icon: "!", tone: report.check.gaveUp ? 'warn' : 'error', go: { view: 'versions' }, children: report.check.gaveUp ? t('turn.gaveUp', { count: report.check.failures }) : t('turn.checkFailed', { count: report.check.failures }) }), looks.map(e => _jsxs(Row, { t: t, onGo: openAt, icon: "\u25C7", go: { view: 'elements', elementId: e.id }, children: [e.what === 'new' ? t('turn.elementNew', { name: e.name }) : t('turn.elementLook', { name: e.name }), e.looks !== undefined && _jsx(BeforeAfter, { t: t, api: api, projectId: report.projectId, version: report.version?.version ?? 0, looks: e.looks })] }, `l${e.id}`)), fieldOnly.length > 0 && fieldOnly.map(e => _jsx(Row, { t: t, onGo: openAt, icon: "\u25C7", go: { view: 'elements', elementId: e.id }, children: t('turn.elementFields', { name: e.name, fields: (e.fields ?? []).slice(0, 4).join('、') }) }, `f${e.id}`)), removed.length > 0 && _jsx(Row, { t: t, onGo: openAt, icon: "\u2212", go: { view: 'elements' }, children: t('turn.elementRemoved', { names: removed.map(e => e.name).join('、') }) }), report.chapters.map(c => _jsx(Row, { t: t, onGo: openAt, icon: "\u00B6", go: { view: 'design', chapterId: c.id }, children: t('turn.chapter', { title: c.title }) }, `c${c.id}`)), report.params.length > 0 && _jsx(Row, { t: t, onGo: openAt, icon: "\u2261", go: { view: 'play' }, children: t('turn.params', { list: report.params.slice(0, 4).map(p => (p.from === undefined ? `${p.label} ${p.to}` : `${p.label} ${p.from}→${p.to}`)).join('、') }) }), report.code.length > 0 && _jsxs(Row, { t: t, onGo: openAt, icon: "{}", go: { view: 'versions' }, children: [t('turn.code', { count: report.code.length }), _jsxs("span", { className: css.meta, children: [report.code.slice(0, 3).join('、'), report.code.length > 3 ? '…' : ''] })] }), report.tests.length > 0 && _jsx(Row, { t: t, onGo: openAt, icon: "\u2713", go: { view: 'versions' }, children: t('turn.tests', { count: report.tests.length }) })] })] });
|
|
100
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版本: the last check (verdict, findings, the screenshot the headless run
|
|
3
|
+
* took), the version timeline (current / candidate / rejected / past, with
|
|
4
|
+
* 切回 and 与当前对比), the playtest log, and the single-file export.
|
|
5
|
+
*/
|
|
6
|
+
import { type ReactNode } from 'react';
|
|
7
|
+
import type { ProjectView } from '../types.ts';
|
|
8
|
+
import { type ViewProps } from './contract.ts';
|
|
9
|
+
/** Props of the versions view. */
|
|
10
|
+
export interface VersionsViewProps extends ViewProps {
|
|
11
|
+
readonly project: ProjectView;
|
|
12
|
+
}
|
|
13
|
+
/** `MM-DD HH:mm` in local time. */
|
|
14
|
+
export declare function shortTime(iso: string): string;
|
|
15
|
+
/** The versions view. */
|
|
16
|
+
export declare function VersionsView({ t, sessionId, api, project, refresh, toast }: VersionsViewProps): ReactNode;
|
|
17
|
+
//# sourceMappingURL=VersionsView.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* 版本: the last check (verdict, findings, the screenshot the headless run
|
|
4
|
+
* took), the version timeline (current / candidate / rejected / past, with
|
|
5
|
+
* 切回 and 与当前对比), the playtest log, and the single-file export.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { exportHtml } from "../vfs.js";
|
|
9
|
+
import { failureOf } from "./contract.js";
|
|
10
|
+
import css from './GameTab.module.css';
|
|
11
|
+
/** `MM-DD HH:mm` in local time. */
|
|
12
|
+
export function shortTime(iso) {
|
|
13
|
+
const d = new Date(iso);
|
|
14
|
+
if (Number.isNaN(d.getTime()))
|
|
15
|
+
return iso;
|
|
16
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
17
|
+
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
18
|
+
}
|
|
19
|
+
/** Download a text file from the panel (a user gesture in the host page). */
|
|
20
|
+
function download(name, text) {
|
|
21
|
+
const url = URL.createObjectURL(new Blob([text], { type: 'text/html' }));
|
|
22
|
+
const a = document.createElement('a');
|
|
23
|
+
a.href = url;
|
|
24
|
+
a.download = name;
|
|
25
|
+
a.click();
|
|
26
|
+
setTimeout(() => { URL.revokeObjectURL(url); }, 2000);
|
|
27
|
+
}
|
|
28
|
+
/** The versions view. */
|
|
29
|
+
export function VersionsView({ t, sessionId, api, project, refresh, toast }) {
|
|
30
|
+
const check = project.lastCheck;
|
|
31
|
+
const [shot, setShot] = useState(null);
|
|
32
|
+
const [diffFor, setDiffFor] = useState(null);
|
|
33
|
+
const [diff, setDiff] = useState(null);
|
|
34
|
+
const current = project.current?.version;
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
let live = true;
|
|
37
|
+
void api.checkShot(project.id).then(r => { if (live)
|
|
38
|
+
setShot(r.ok && r.value.ok ? r.value.webp : null); });
|
|
39
|
+
return () => { live = false; };
|
|
40
|
+
}, [api, project.id, check?.at]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (diffFor === null || current === undefined) {
|
|
43
|
+
setDiff(null);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let live = true;
|
|
47
|
+
void api.diff(project.id, diffFor, current).then(r => { if (live)
|
|
48
|
+
setDiff(r.ok && r.value.ok ? r.value.diff : null); });
|
|
49
|
+
return () => { live = false; };
|
|
50
|
+
}, [api, project.id, diffFor, current]);
|
|
51
|
+
const exportVersion = (version) => {
|
|
52
|
+
void api.versionBundle(project.id, version).then(r => {
|
|
53
|
+
if (!(r.ok && r.value.ok)) {
|
|
54
|
+
toast(t('versions.exportFailed'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
download(`${project.title}-v${version}.html`, exportHtml(project.title, r.value.bundle.files));
|
|
58
|
+
toast(t('toast.exported', { version }));
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
return _jsxs("div", { className: css.scroll, children: [_jsxs("section", { className: css.block, children: [_jsx("h4", { className: css.blockTitle, children: t('versions.lastCheck') }), check === null
|
|
62
|
+
? _jsx("p", { className: css.lead, children: t('checks.none') })
|
|
63
|
+
: _jsxs("div", { className: css.checkCard, "data-tone": check.ok ? 'ok' : check.gaveUp ? 'warn' : 'error', children: [_jsxs("div", { className: css.checkHead, children: [_jsx("strong", { children: check.ok ? t('checks.ok') : t('checks.fail') }), _jsxs("span", { className: css.metaLine, children: [shortTime(check.at), check.attempt > 0 ? ` · ${t('checks.attempt', { attempt: check.attempt })}` : '', check.passed > 0 ? ` · ${t('checks.passed', { count: check.passed })}` : ''] })] }), _jsxs("ol", { className: css.layers, children: [_jsx("li", { "data-ok": check.failures.some(f => f.where === 'data' || f.where.startsWith('elements/')) ? 'false' : 'true', children: t('checks.layerData') }), _jsx("li", { "data-ok": check.skipped !== undefined ? 'skip' : check.failures.some(f => f.where === 'smoke') ? 'false' : 'true', children: t('checks.layerSmoke') }), _jsx("li", { "data-ok": check.skipped !== undefined ? 'skip' : check.failures.some(f => f.where.startsWith('tests/')) ? 'false' : 'true', children: t('checks.layerTests') })] }), check.gaveUp === true && _jsx("p", { className: css.warnText, children: t('checks.gaveUp') }), check.skipped !== undefined && _jsx("p", { className: css.metaLine, children: check.skipped }), check.failures.length > 0 && _jsx("ul", { className: css.findings, children: check.failures.map((f, i) => _jsxs("li", { children: [_jsx("code", { children: f.where }), _jsx("span", { children: f.message })] }, i)) }), shot !== null && _jsxs("figure", { className: css.shot, children: [_jsx("img", { src: `data:image/webp;base64,${shot}`, alt: t('versions.shotAlt') }), _jsx("figcaption", { className: css.metaLine, children: t('versions.shotCaption') })] })] })] }), _jsxs("section", { className: css.block, children: [_jsx("h4", { className: css.blockTitle, children: t('versions.title') }), project.versions.length === 0 && _jsx("p", { className: css.lead, children: t('versions.none') }), _jsx("ol", { className: css.timeline, children: project.versions.map((v) => _jsxs("li", { className: css.tlItem, "data-status": v.status, children: [_jsx("span", { className: css.tlDot, "aria-hidden": "true" }), _jsxs("div", { className: css.tlBody, children: [_jsxs("div", { className: css.tlHead, children: [_jsxs("strong", { children: ["v", v.version] }), _jsx("span", { className: css.tlTag, "data-status": v.status, children: t(`versions.status.${v.status}`) }), v.by !== undefined && _jsx("span", { className: css.tlBy, "data-by": v.by, children: t(`versions.by.${v.by}`) }), _jsx("span", { className: css.metaLine, children: shortTime(v.at) })] }), v.note !== undefined && _jsx("span", { className: css.metaLine, children: v.note }), _jsxs("div", { className: css.tlActions, children: [v.status !== 'current' && v.status !== 'candidate' && _jsx("button", { type: "button", className: css.link, onClick: () => {
|
|
64
|
+
void api.rollback(sessionId, project.id, v.version).then(r => { if (failureOf(r) === undefined)
|
|
65
|
+
toast(t('toast.rolledBack', { version: v.version })); refresh(); });
|
|
66
|
+
}, children: t('versions.rollback') }), current !== undefined && v.version !== current && _jsx("button", { type: "button", className: css.link, "aria-pressed": diffFor === v.version, onClick: () => { setDiffFor(d => (d === v.version ? null : v.version)); }, children: t('versions.compare', { version: current }) }), _jsx("button", { type: "button", className: css.link, onClick: () => { exportVersion(v.version); }, children: t('versions.export') })] }), diffFor === v.version && diff !== null && _jsxs("div", { className: css.diff, children: [diff.added.length + diff.removed.length + diff.changed.length === 0 && _jsx("span", { className: css.metaLine, children: t('versions.same') }), diff.changed.map(f => _jsxs("span", { "data-kind": "changed", children: ["~ ", f] }, f)), diff.added.map(f => _jsxs("span", { "data-kind": "added", children: ["+ ", f] }, f)), diff.removed.map(f => _jsxs("span", { "data-kind": "removed", children: ["\u2212 ", f] }, f))] })] })] }, v.version)) })] }), _jsxs("section", { className: css.block, children: [_jsx("h4", { className: css.blockTitle, children: t('versions.share') }), _jsx("p", { className: css.metaLine, children: t('versions.shareNote') }), _jsx("div", { className: css.rowEnd, children: _jsx("button", { type: "button", className: css.primary, disabled: current === undefined, onClick: () => { if (current !== undefined)
|
|
67
|
+
exportVersion(current); }, children: t('versions.exportCurrent') }) }), _jsx("p", { className: css.metaLine, children: t('versions.later') })] }), _jsxs("section", { className: css.block, children: [_jsx("h4", { className: css.blockTitle, children: t('versions.log') }), project.log.length === 0 && _jsx("p", { className: css.metaLine, children: t('versions.logEmpty') }), _jsx("ul", { className: css.logList, children: project.log.slice(0, 30).map((e, i) => _jsxs("li", { children: [_jsx("span", { className: css.logTime, children: shortTime(e.at) }), _jsx("span", { children: e.text })] }, i)) })] })] });
|
|
68
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The game's rows in the conversation (proposal option B): the messages the
|
|
3
|
+
* plugin puts into a turn — check findings steered back to the agent, and
|
|
4
|
+
* the panel changes attached to a send — are `dsh-game`-sourced user
|
|
5
|
+
* messages, which the official chat view projects as hidden `context` nodes.
|
|
6
|
+
* This Definition projects the same events into our own node kind and a
|
|
7
|
+
* keyed `conversation.chat.node` renderer draws them as one collapsible row:
|
|
8
|
+
* red 「检查未通过 1/2」 with the findings, grey 「已附上」 with the items.
|
|
9
|
+
* The official node stays hidden, so nothing renders twice (the
|
|
10
|
+
* message-tools restored-row precedent).
|
|
11
|
+
*/
|
|
12
|
+
import { type ReactNode } from 'react';
|
|
13
|
+
import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
14
|
+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
15
|
+
/** The chat node kind (also the renderer's key). */
|
|
16
|
+
export declare const GAME_CHAT_KIND = "dsh-game-message";
|
|
17
|
+
/** What the row needs from the persisted message. */
|
|
18
|
+
export interface GameChatData {
|
|
19
|
+
readonly seq: number;
|
|
20
|
+
readonly time: number;
|
|
21
|
+
readonly op: 'check-failed' | 'attached' | 'other';
|
|
22
|
+
readonly attempt: number;
|
|
23
|
+
readonly cap: number;
|
|
24
|
+
readonly failures: readonly {
|
|
25
|
+
readonly where: string;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}[];
|
|
28
|
+
readonly items: readonly string[];
|
|
29
|
+
readonly text: string;
|
|
30
|
+
}
|
|
31
|
+
declare module '@deepseek-ai/dsh-client-ui-chat/client' {
|
|
32
|
+
interface ChatNodeDataMap {
|
|
33
|
+
'dsh-game-message': GameChatData;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The projection: every appended `dsh-game` user message becomes one row. */
|
|
37
|
+
export declare const gameMessageDefinition: ConversationNodeDefinition<GameChatData>;
|
|
38
|
+
/** Props of the row renderer. */
|
|
39
|
+
export type GameChatRowProps = PropsRuntime<'conversation.chat.node', 'dsh-game-message'> & PropsLocale<'game'>;
|
|
40
|
+
/** The collapsible row. */
|
|
41
|
+
export declare function GameChatRow({ node, t }: GameChatRowProps): ReactNode;
|
|
42
|
+
//# sourceMappingURL=chat-node.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The game's rows in the conversation (proposal option B): the messages the
|
|
4
|
+
* plugin puts into a turn — check findings steered back to the agent, and
|
|
5
|
+
* the panel changes attached to a send — are `dsh-game`-sourced user
|
|
6
|
+
* messages, which the official chat view projects as hidden `context` nodes.
|
|
7
|
+
* This Definition projects the same events into our own node kind and a
|
|
8
|
+
* keyed `conversation.chat.node` renderer draws them as one collapsible row:
|
|
9
|
+
* red 「检查未通过 1/2」 with the findings, grey 「已附上」 with the items.
|
|
10
|
+
* The official node stays hidden, so nothing renders twice (the
|
|
11
|
+
* message-tools restored-row precedent).
|
|
12
|
+
*/
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session/surface';
|
|
15
|
+
import css from './ChatRow.module.css';
|
|
16
|
+
/** The chat node kind (also the renderer's key). */
|
|
17
|
+
export const GAME_CHAT_KIND = 'dsh-game-message';
|
|
18
|
+
/** Whether a message source is ours. */
|
|
19
|
+
function isGameSource(source) {
|
|
20
|
+
return source?.kind === 'dsh-game';
|
|
21
|
+
}
|
|
22
|
+
/** The source's structured part, defensively. */
|
|
23
|
+
function dataOf(seq, time, source, content) {
|
|
24
|
+
const text = content.map(block => block.type === 'text' ? block.text : '').join('\n');
|
|
25
|
+
const op = source.op === 'check-failed' || source.op === 'attached' ? source.op : 'other';
|
|
26
|
+
const failures = Array.isArray(source.failures)
|
|
27
|
+
? source.failures.filter((f) => typeof f?.where === 'string' && typeof f.message === 'string')
|
|
28
|
+
: [];
|
|
29
|
+
const items = Array.isArray(source.items) ? source.items.filter((i) => typeof i === 'string') : [];
|
|
30
|
+
return {
|
|
31
|
+
seq, time, op, text, failures, items,
|
|
32
|
+
attempt: typeof source.attempt === 'number' ? source.attempt : 0,
|
|
33
|
+
cap: typeof source.cap === 'number' ? source.cap : 2,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The projection: every appended `dsh-game` user message becomes one row. */
|
|
37
|
+
export const gameMessageDefinition = {
|
|
38
|
+
kind: GAME_CHAT_KIND,
|
|
39
|
+
target: 'chat',
|
|
40
|
+
match: (event) => {
|
|
41
|
+
if (event.type !== 'user/message' || !isAppendSurfaceEvent(event) || !isGameSource(event.data.source))
|
|
42
|
+
return null;
|
|
43
|
+
return { id: String(event.seq), role: 'start' };
|
|
44
|
+
},
|
|
45
|
+
start: (_context, match) => {
|
|
46
|
+
const event = match.event;
|
|
47
|
+
if (event.type !== 'user/message' || !isGameSource(event.data.source))
|
|
48
|
+
throw new Error('dsh-game row requires a dsh-game message');
|
|
49
|
+
return dataOf(event.seq, event.time, event.data.source, event.data.content);
|
|
50
|
+
},
|
|
51
|
+
update: context => context.state,
|
|
52
|
+
buildViewNode: (context) => context.state === undefined
|
|
53
|
+
? null
|
|
54
|
+
: {
|
|
55
|
+
key: context.key,
|
|
56
|
+
kind: GAME_CHAT_KIND,
|
|
57
|
+
id: context.id,
|
|
58
|
+
target: 'chat',
|
|
59
|
+
anchorSeq: context.state.seq,
|
|
60
|
+
location: context.start?.location ?? { kind: 'unresolved' },
|
|
61
|
+
visibility: 'visible',
|
|
62
|
+
data: context.state,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
/** The collapsible row. */
|
|
66
|
+
export function GameChatRow({ node, t }) {
|
|
67
|
+
const data = node.data;
|
|
68
|
+
const [open, setOpen] = useState(false);
|
|
69
|
+
if (data.op === 'attached') {
|
|
70
|
+
return _jsxs("div", { className: css.wrap, "data-tone": "muted", "data-align": "end", children: [_jsxs("button", { type: "button", className: css.head, "aria-expanded": open, onClick: () => { setOpen(o => !o); }, children: [_jsx("span", { className: css.dot, "aria-hidden": "true" }), _jsx("span", { className: css.title, children: t('chat.attached', { count: data.items.length }) }), _jsxs("span", { className: css.summary, children: [data.items.slice(0, 2).join(' · '), data.items.length > 2 ? ' …' : ''] }), _jsx("span", { className: css.chevron, "aria-hidden": "true", children: open ? '▾' : '▸' })] }), open && _jsx("ul", { className: css.list, children: data.items.map((item, i) => _jsx("li", { children: item }, i)) })] });
|
|
71
|
+
}
|
|
72
|
+
if (data.op === 'check-failed') {
|
|
73
|
+
const last = data.attempt >= data.cap;
|
|
74
|
+
return _jsxs("div", { className: css.wrap, "data-tone": last ? 'warn' : 'error', children: [_jsxs("button", { type: "button", className: css.head, "aria-expanded": open, onClick: () => { setOpen(o => !o); }, children: [_jsx("span", { className: css.dot, "aria-hidden": "true" }), _jsx("span", { className: css.title, children: t('chat.checkFailed', { attempt: data.attempt, cap: data.cap }) }), _jsx("span", { className: css.summary, children: data.failures[0] === undefined ? '' : `${data.failures[0].where}:${data.failures[0].message}` }), _jsx("span", { className: css.chevron, "aria-hidden": "true", children: open ? '▾' : '▸' })] }), open && _jsxs("div", { className: css.body, children: [_jsx("ul", { className: css.list, children: data.failures.map((f, i) => _jsxs("li", { children: [_jsx("code", { children: f.where }), _jsx("span", { children: f.message })] }, i)) }), _jsx("p", { className: css.note, children: last ? t('chat.lastAttempt') : t('chat.fixing') })] })] });
|
|
75
|
+
}
|
|
76
|
+
return _jsxs("div", { className: css.wrap, "data-tone": "muted", children: [_jsxs("button", { type: "button", className: css.head, "aria-expanded": open, onClick: () => { setOpen(o => !o); }, children: [_jsx("span", { className: css.dot, "aria-hidden": "true" }), _jsx("span", { className: css.title, children: t('chat.other') }), _jsx("span", { className: css.chevron, "aria-hidden": "true", children: open ? '▾' : '▸' })] }), open && _jsx("pre", { className: css.pre, children: data.text })] });
|
|
77
|
+
}
|