@world-engines/authoring-ui 0.1.0-alpha.0
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 +46 -0
- package/dist/DesktopAuthoringShell.d.ts +178 -0
- package/dist/DesktopAuthoringShell.js +235 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +12 -0
- package/dist/ladybug/BrowserGraphCanvasAdapter.d.ts +12 -0
- package/dist/ladybug/BrowserGraphCanvasAdapter.js +8 -0
- package/dist/ladybug/GraphCanvas.d.ts +121 -0
- package/dist/ladybug/GraphCanvas.js +1439 -0
- package/dist/ladybug/GraphEdge.d.ts +26 -0
- package/dist/ladybug/GraphEdge.js +41 -0
- package/dist/ladybug/LadybugExplorer.d.ts +58 -0
- package/dist/ladybug/LadybugExplorer.js +83 -0
- package/dist/ladybug/LadybugSurfaces.d.ts +25 -0
- package/dist/ladybug/LadybugSurfaces.js +9 -0
- package/dist/ladybug/LadybugTable.d.ts +15 -0
- package/dist/ladybug/LadybugTable.js +26 -0
- package/dist/ladybug/entity-props.d.ts +98 -0
- package/dist/ladybug/entity-props.js +363 -0
- package/dist/ladybug/graph-edge-geometry.d.ts +19 -0
- package/dist/ladybug/graph-edge-geometry.js +78 -0
- package/dist/ladybug/graph-edge-mask.d.ts +21 -0
- package/dist/ladybug/graph-edge-mask.js +52 -0
- package/dist/ladybug/graph-groups.d.ts +47 -0
- package/dist/ladybug/graph-groups.js +245 -0
- package/dist/ladybug/graph-layout-contract.d.ts +34 -0
- package/dist/ladybug/graph-layout-contract.js +55 -0
- package/dist/ladybug/graph-layout-solver.d.ts +16 -0
- package/dist/ladybug/graph-layout-solver.js +96 -0
- package/dist/ladybug/graph-layout-wasm.d.ts +9 -0
- package/dist/ladybug/graph-layout-wasm.js +88 -0
- package/dist/ladybug/graph-layout-worker-protocol.d.ts +18 -0
- package/dist/ladybug/graph-layout-worker-protocol.js +1 -0
- package/dist/ladybug/graph-layout-worker.d.ts +1 -0
- package/dist/ladybug/graph-layout-worker.js +66 -0
- package/dist/ladybug/graph-layout.d.ts +2 -0
- package/dist/ladybug/graph-layout.js +1 -0
- package/dist/ladybug/graph-layout.wasm +0 -0
- package/dist/ladybug/graph-node-metrics.d.ts +33 -0
- package/dist/ladybug/graph-node-metrics.js +173 -0
- package/dist/ladybug/graph-physics.d.ts +49 -0
- package/dist/ladybug/graph-physics.js +266 -0
- package/dist/ladybug/graph-placement.d.ts +33 -0
- package/dist/ladybug/graph-placement.js +253 -0
- package/dist/ladybug/graph.css +530 -0
- package/dist/ladybug/graph.d.ts +18 -0
- package/dist/ladybug/graph.js +9 -0
- package/dist/ladybug/score-tags.d.ts +11 -0
- package/dist/ladybug/score-tags.js +1 -0
- package/dist/map/MapWorkspace.d.ts +14 -0
- package/dist/map/MapWorkspace.js +9 -0
- package/dist/score/ScoreRadar.d.ts +10 -0
- package/dist/score/ScoreRadar.js +66 -0
- package/dist/score/index.d.ts +3 -0
- package/dist/score/index.js +3 -0
- package/dist/score/radar-index.d.ts +3 -0
- package/dist/score/radar-index.js +2 -0
- package/dist/score/scenario-scoring-features.d.ts +90 -0
- package/dist/score/scenario-scoring-features.js +380 -0
- package/dist/score/scenario-structural-score.d.ts +76 -0
- package/dist/score/scenario-structural-score.js +211 -0
- package/dist/score/score-presentation.d.ts +51 -0
- package/dist/score/score-presentation.js +44 -0
- package/dist/score/score-radar.css +1 -0
- package/dist/sync/DownloadPanel.d.ts +42 -0
- package/dist/sync/DownloadPanel.js +22 -0
- package/dist/sync/SyncSidebar.d.ts +47 -0
- package/dist/sync/SyncSidebar.js +31 -0
- package/dist/sync/UploadPanel.d.ts +64 -0
- package/dist/sync/UploadPanel.js +36 -0
- package/dist/trigger/TriggerBlueprint.d.ts +37 -0
- package/dist/trigger/TriggerBlueprint.js +26 -0
- package/dist/trigger/tmw/TmwBlueprint.d.ts +71 -0
- package/dist/trigger/tmw/TmwBlueprint.js +206 -0
- package/dist/trigger/tmw/TmwFlowLane.d.ts +22 -0
- package/dist/trigger/tmw/TmwFlowLane.js +44 -0
- package/dist/trigger/tmw/TmwSourceLane.d.ts +17 -0
- package/dist/trigger/tmw/TmwSourceLane.js +38 -0
- package/dist/trigger/tmw/TmwTabs.d.ts +20 -0
- package/dist/trigger/tmw/TmwTabs.js +8 -0
- package/dist/trigger/tmw/TmwTraceLane.d.ts +9 -0
- package/dist/trigger/tmw/TmwTraceLane.js +6 -0
- package/dist/trigger/tmw/TriggerDebugTab.d.ts +9 -0
- package/dist/trigger/tmw/TriggerDebugTab.js +25 -0
- package/dist/trigger/tmw/translate.d.ts +3 -0
- package/dist/trigger/tmw/translate.js +2 -0
- package/dist/view/ViewWorkspace.d.ts +19 -0
- package/dist/view/ViewWorkspace.js +8 -0
- package/package.json +89 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { detectAllMatchedArchetypes, detectPrimaryArchetype, extractScenarioScoringFeatures, toStructuralInput } from "./scenario-scoring-features.js";
|
|
2
|
+
export const STRUCTURAL_WEIGHT_ENTITY_COUNT = 22;
|
|
3
|
+
export const STRUCTURAL_WEIGHT_RELATION_COUNT = 20;
|
|
4
|
+
export const STRUCTURAL_WEIGHT_ATTR_RICHNESS = 16;
|
|
5
|
+
export const STRUCTURAL_WEIGHT_LORE_COMPLETENESS = 16;
|
|
6
|
+
export const STRUCTURAL_WEIGHT_TEXT_VOLUME = 14;
|
|
7
|
+
export const STRUCTURAL_WEIGHT_ASSET_REFS = 12;
|
|
8
|
+
export const STRUCTURAL_SAT_K_ENTITY_COUNT = 60;
|
|
9
|
+
export const STRUCTURAL_SAT_K_RELATION_COUNT = 80;
|
|
10
|
+
export const STRUCTURAL_SAT_K_ATTR_RICHNESS = 6;
|
|
11
|
+
export const STRUCTURAL_SAT_K_LORE_COMPLETENESS = 8;
|
|
12
|
+
export const STRUCTURAL_SAT_K_TEXT_VOLUME = 4000;
|
|
13
|
+
export const STRUCTURAL_SAT_K_ASSET_REFS = 12;
|
|
14
|
+
export function sat(x, k) {
|
|
15
|
+
if (k <= 0 || x <= 0)
|
|
16
|
+
return 0;
|
|
17
|
+
return 1 - Math.exp(-x / k);
|
|
18
|
+
}
|
|
19
|
+
function attrRichnessRaw(entityCount, totalPropKeys) {
|
|
20
|
+
if (entityCount === 0)
|
|
21
|
+
return 0;
|
|
22
|
+
return totalPropKeys / entityCount;
|
|
23
|
+
}
|
|
24
|
+
function assetRefsRaw(input) {
|
|
25
|
+
return (input.coverVariantCount +
|
|
26
|
+
(input.hasViewRef ? 1 : 0) +
|
|
27
|
+
input.uiHtmlVariantCount +
|
|
28
|
+
input.mediaBindingCount);
|
|
29
|
+
}
|
|
30
|
+
export function computeStructuralDims(input) {
|
|
31
|
+
const textVolumeRaw = input.descriptionChars + input.entityPropsTextChars;
|
|
32
|
+
return {
|
|
33
|
+
entity_count: sat(input.entityCount, STRUCTURAL_SAT_K_ENTITY_COUNT) * STRUCTURAL_WEIGHT_ENTITY_COUNT,
|
|
34
|
+
relation_count: sat(input.relationCount, STRUCTURAL_SAT_K_RELATION_COUNT) *
|
|
35
|
+
STRUCTURAL_WEIGHT_RELATION_COUNT,
|
|
36
|
+
attr_richness: sat(attrRichnessRaw(input.entityCount, input.totalPropKeys), STRUCTURAL_SAT_K_ATTR_RICHNESS) * STRUCTURAL_WEIGHT_ATTR_RICHNESS,
|
|
37
|
+
lore_completeness: sat(input.loreEntityCount, STRUCTURAL_SAT_K_LORE_COMPLETENESS) *
|
|
38
|
+
STRUCTURAL_WEIGHT_LORE_COMPLETENESS,
|
|
39
|
+
text_volume: sat(textVolumeRaw, STRUCTURAL_SAT_K_TEXT_VOLUME) * STRUCTURAL_WEIGHT_TEXT_VOLUME,
|
|
40
|
+
asset_refs: sat(assetRefsRaw(input), STRUCTURAL_SAT_K_ASSET_REFS) * STRUCTURAL_WEIGHT_ASSET_REFS
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** 与 Rust `compute_structural_dims_with_archetype` 保持同口径。 */
|
|
44
|
+
export function computeStructuralDimsWithArchetype(input, features, archetypeId) {
|
|
45
|
+
const base = computeStructuralDims(input);
|
|
46
|
+
const p = features.per_type_counts;
|
|
47
|
+
const media = features.media_breakdown;
|
|
48
|
+
switch (archetypeId) {
|
|
49
|
+
case "solo_protagonist":
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
relation_count: (sat(features.max_edge, 6) + sat(features.p90_edge, 4)) *
|
|
53
|
+
0.5 * STRUCTURAL_WEIGHT_RELATION_COUNT,
|
|
54
|
+
attr_richness: sat(features.max_attr, 12) * STRUCTURAL_WEIGHT_ATTR_RICHNESS
|
|
55
|
+
};
|
|
56
|
+
case "world_codex": {
|
|
57
|
+
const half = (STRUCTURAL_WEIGHT_LORE_COMPLETENESS + STRUCTURAL_WEIGHT_ATTR_RICHNESS) * 0.5;
|
|
58
|
+
return {
|
|
59
|
+
...base,
|
|
60
|
+
attr_richness: 0,
|
|
61
|
+
lore_completeness: sat(features.lore_entity_count, STRUCTURAL_SAT_K_LORE_COMPLETENESS) * half +
|
|
62
|
+
sat(features.median_attr, 6) * half
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
case "sparse_prototype":
|
|
66
|
+
case "replay_hotspot":
|
|
67
|
+
case "other":
|
|
68
|
+
return base;
|
|
69
|
+
case "attribute_farm": {
|
|
70
|
+
const half = STRUCTURAL_WEIGHT_LORE_COMPLETENESS * 0.5;
|
|
71
|
+
return {
|
|
72
|
+
...base,
|
|
73
|
+
lore_completeness: sat(features.median_attr, 12) * half +
|
|
74
|
+
sat(features.total_prop_keys, 60) * half
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case "ensemble_cast": {
|
|
78
|
+
const ratio = features.median_edge / Math.max(features.max_edge, 1);
|
|
79
|
+
return { ...base, attr_richness: base.attr_richness + sat(ratio, 0.5) * 4 };
|
|
80
|
+
}
|
|
81
|
+
case "relation_web":
|
|
82
|
+
return {
|
|
83
|
+
...base,
|
|
84
|
+
relation_count: Math.min(features.relation_count / STRUCTURAL_SAT_K_RELATION_COUNT, 2) *
|
|
85
|
+
STRUCTURAL_WEIGHT_RELATION_COUNT * 1.2
|
|
86
|
+
};
|
|
87
|
+
case "flat_graph": {
|
|
88
|
+
const half = STRUCTURAL_WEIGHT_RELATION_COUNT * 0.5;
|
|
89
|
+
return {
|
|
90
|
+
...base,
|
|
91
|
+
relation_count: sat(features.entity_count, STRUCTURAL_SAT_K_ENTITY_COUNT) * half +
|
|
92
|
+
sat(features.median_attr, 6) * half
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
case "faction_conflict": {
|
|
96
|
+
const half = STRUCTURAL_WEIGHT_ATTR_RICHNESS * 0.5;
|
|
97
|
+
return {
|
|
98
|
+
...base,
|
|
99
|
+
attr_richness: base.attr_richness * 0.5 + sat(p.faction, 8) * half
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
case "location_atlas": {
|
|
103
|
+
const half = STRUCTURAL_WEIGHT_ATTR_RICHNESS * 0.5;
|
|
104
|
+
return {
|
|
105
|
+
...base,
|
|
106
|
+
attr_richness: sat(p.location, 12) * half + sat(features.median_edge, 4) * half
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
case "item_catalog": {
|
|
110
|
+
const half = STRUCTURAL_WEIGHT_RELATION_COUNT * 0.5;
|
|
111
|
+
return {
|
|
112
|
+
...base,
|
|
113
|
+
relation_count: sat(p.item, 20) * half + sat(features.median_attr, 8) * half
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
case "event_driven": {
|
|
117
|
+
const half = STRUCTURAL_WEIGHT_LORE_COMPLETENESS * 0.5;
|
|
118
|
+
return {
|
|
119
|
+
...base,
|
|
120
|
+
lore_completeness: sat(p.event, 12) * half + sat(features.relation_count, 40) * half
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case "cg_gallery": {
|
|
124
|
+
const half = STRUCTURAL_WEIGHT_LORE_COMPLETENESS * 0.5;
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
lore_completeness: sat(media.cg_image + media.cg_video, 30) * half +
|
|
128
|
+
sat(features.cover_variant_count, 8) * half
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
case "voice_theater":
|
|
132
|
+
return { ...base, asset_refs: sat(media.audio, 20) * STRUCTURAL_WEIGHT_ASSET_REFS };
|
|
133
|
+
case "puppet_show":
|
|
134
|
+
return { ...base, asset_refs: sat(media.puppet, 5) * 20 };
|
|
135
|
+
case "text_heavyweight":
|
|
136
|
+
return {
|
|
137
|
+
...base,
|
|
138
|
+
text_volume: Math.min(((features.description_chars + features.entity_props_text_chars) / 20_000) *
|
|
139
|
+
STRUCTURAL_WEIGHT_TEXT_VOLUME, STRUCTURAL_WEIGHT_TEXT_VOLUME * 1.5)
|
|
140
|
+
};
|
|
141
|
+
case "ui_showcase": {
|
|
142
|
+
const uiBonus = sat(features.ui_html_variant_count, 3) + (features.has_view_ref ? 1 : 0);
|
|
143
|
+
return {
|
|
144
|
+
...base,
|
|
145
|
+
asset_refs: base.asset_refs + uiBonus * STRUCTURAL_WEIGHT_ASSET_REFS * 0.5
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
case "compact_tight":
|
|
149
|
+
return {
|
|
150
|
+
entity_count: base.entity_count * 1.05,
|
|
151
|
+
relation_count: base.relation_count * 1.05,
|
|
152
|
+
attr_richness: base.attr_richness * 1.05,
|
|
153
|
+
lore_completeness: base.lore_completeness * 1.05,
|
|
154
|
+
text_volume: base.text_volume * 1.05,
|
|
155
|
+
asset_refs: base.asset_refs * 1.05
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export function computeStructuralScoreWithArchetype(input, features, archetypeId) {
|
|
160
|
+
return Math.min(100, Math.max(0, sumStructuralDims(computeStructuralDimsWithArchetype(input, features, archetypeId))));
|
|
161
|
+
}
|
|
162
|
+
export function sumStructuralDims(dims) {
|
|
163
|
+
return (dims.entity_count +
|
|
164
|
+
dims.relation_count +
|
|
165
|
+
dims.attr_richness +
|
|
166
|
+
dims.lore_completeness +
|
|
167
|
+
dims.text_volume +
|
|
168
|
+
dims.asset_refs);
|
|
169
|
+
}
|
|
170
|
+
export function computeStructuralScore(input) {
|
|
171
|
+
return sumStructuralDims(computeStructuralDims(input));
|
|
172
|
+
}
|
|
173
|
+
/** Host-facing descriptive alias;与历史 computeStructuralScore 使用同一公式实现。 */
|
|
174
|
+
export const computeScenarioStructuralScore = computeStructuralScore;
|
|
175
|
+
export function buildStructuralInput(params) {
|
|
176
|
+
return toStructuralInput(extractScenarioScoringFeatures({
|
|
177
|
+
entities: params.entities,
|
|
178
|
+
relations: params.relations,
|
|
179
|
+
assets: {
|
|
180
|
+
description_chars: Array.from(params.description).length,
|
|
181
|
+
cover_variant_count: params.coverVariantCount,
|
|
182
|
+
has_view_ref: params.hasViewRef,
|
|
183
|
+
ui_html_variant_count: params.uiHtmlVariantCount,
|
|
184
|
+
media_binding_count: params.mediaBindingCount
|
|
185
|
+
}
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
/** 本地顶部预估只使用结构事实;不接受 media 自报值,也不伪造 quality/base 或 dual-radar 轴。 */
|
|
189
|
+
export function computeLocalStructuralProjection(input) {
|
|
190
|
+
if (!Number.isSafeInteger(input.source_revision) || input.source_revision < 0)
|
|
191
|
+
throw new Error("invalid_source_revision");
|
|
192
|
+
if (!/^[a-f0-9]{64}$/.test(input.source_digest))
|
|
193
|
+
throw new Error("invalid_source_digest");
|
|
194
|
+
const features = extractScenarioScoringFeatures({
|
|
195
|
+
entities: input.entities,
|
|
196
|
+
relations: input.relations,
|
|
197
|
+
assets: { description_chars: Array.from(input.description).length },
|
|
198
|
+
});
|
|
199
|
+
const structuralInput = toStructuralInput(features);
|
|
200
|
+
const primary = detectPrimaryArchetype(features);
|
|
201
|
+
const dimensions = computeStructuralDimsWithArchetype(structuralInput, features, primary);
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
source_revision: input.source_revision,
|
|
204
|
+
source_digest: input.source_digest,
|
|
205
|
+
features,
|
|
206
|
+
archetype: Object.freeze({ primary, all_matched: Object.freeze(detectAllMatchedArchetypes(features)) }),
|
|
207
|
+
structural: Object.freeze({ score: computeStructuralScoreWithArchetype(structuralInput, features, primary), dimensions }),
|
|
208
|
+
quality: null,
|
|
209
|
+
base: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const STORY_SCORE_AXES: readonly [{
|
|
2
|
+
readonly key: "narrative_depth";
|
|
3
|
+
readonly label: "叙事深度";
|
|
4
|
+
}, {
|
|
5
|
+
readonly key: "world_breadth";
|
|
6
|
+
readonly label: "世界广度";
|
|
7
|
+
}, {
|
|
8
|
+
readonly key: "relation_complexity";
|
|
9
|
+
readonly label: "关系复杂度";
|
|
10
|
+
}, {
|
|
11
|
+
readonly key: "interaction_system";
|
|
12
|
+
readonly label: "交互系统";
|
|
13
|
+
}, {
|
|
14
|
+
readonly key: "content_presentation";
|
|
15
|
+
readonly label: "内容呈现";
|
|
16
|
+
}, {
|
|
17
|
+
readonly key: "replay_potential";
|
|
18
|
+
readonly label: "重玩潜力";
|
|
19
|
+
}];
|
|
20
|
+
export declare const PRESENTATION_SCORE_AXES: readonly [{
|
|
21
|
+
readonly key: "visual_design";
|
|
22
|
+
readonly label: "视觉设计";
|
|
23
|
+
}, {
|
|
24
|
+
readonly key: "responsive_adaptation";
|
|
25
|
+
readonly label: "响应式适配";
|
|
26
|
+
}, {
|
|
27
|
+
readonly key: "interaction_completeness";
|
|
28
|
+
readonly label: "交互完成度";
|
|
29
|
+
}, {
|
|
30
|
+
readonly key: "media_orchestration";
|
|
31
|
+
readonly label: "媒体编排";
|
|
32
|
+
}, {
|
|
33
|
+
readonly key: "audio_performance";
|
|
34
|
+
readonly label: "声音表现";
|
|
35
|
+
}, {
|
|
36
|
+
readonly key: "puppet_performance";
|
|
37
|
+
readonly label: "人偶演出";
|
|
38
|
+
}];
|
|
39
|
+
export type StoryScoreAxisKey = (typeof STORY_SCORE_AXES)[number]["key"];
|
|
40
|
+
export type PresentationScoreAxisKey = (typeof PRESENTATION_SCORE_AXES)[number]["key"];
|
|
41
|
+
export interface ScoreAxisValues {
|
|
42
|
+
readonly self: number | null;
|
|
43
|
+
readonly mean?: number | null;
|
|
44
|
+
readonly median?: number | null;
|
|
45
|
+
}
|
|
46
|
+
export type ScoreAxisMap<Key extends string> = Readonly<Partial<Record<Key, ScoreAxisValues>>>;
|
|
47
|
+
export type ScoreSource = "draft" | "reviewed";
|
|
48
|
+
export declare function finiteScore(value: number | null | undefined): number | null;
|
|
49
|
+
export declare function scoreScaleValue(value: number | null | undefined): number | null;
|
|
50
|
+
export declare function formatCompactScore(value: number | null | undefined): string;
|
|
51
|
+
export declare function formatFullScore(value: number | null | undefined): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const STORY_SCORE_AXES = [
|
|
2
|
+
{ key: "narrative_depth", label: "叙事深度" },
|
|
3
|
+
{ key: "world_breadth", label: "世界广度" },
|
|
4
|
+
{ key: "relation_complexity", label: "关系复杂度" },
|
|
5
|
+
{ key: "interaction_system", label: "交互系统" },
|
|
6
|
+
{ key: "content_presentation", label: "内容呈现" },
|
|
7
|
+
{ key: "replay_potential", label: "重玩潜力" },
|
|
8
|
+
];
|
|
9
|
+
export const PRESENTATION_SCORE_AXES = [
|
|
10
|
+
{ key: "visual_design", label: "视觉设计" },
|
|
11
|
+
{ key: "responsive_adaptation", label: "响应式适配" },
|
|
12
|
+
{ key: "interaction_completeness", label: "交互完成度" },
|
|
13
|
+
{ key: "media_orchestration", label: "媒体编排" },
|
|
14
|
+
{ key: "audio_performance", label: "声音表现" },
|
|
15
|
+
{ key: "puppet_performance", label: "人偶演出" },
|
|
16
|
+
];
|
|
17
|
+
export function finiteScore(value) {
|
|
18
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
19
|
+
}
|
|
20
|
+
export function scoreScaleValue(value) {
|
|
21
|
+
const finite = finiteScore(value);
|
|
22
|
+
return finite === null ? null : Math.min(100, Math.max(0, finite));
|
|
23
|
+
}
|
|
24
|
+
function compactNumber(value) {
|
|
25
|
+
const rounded = Math.round(value * 10) / 10;
|
|
26
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
27
|
+
}
|
|
28
|
+
export function formatCompactScore(value) {
|
|
29
|
+
const finite = finiteScore(value);
|
|
30
|
+
if (finite === null)
|
|
31
|
+
return "N/A";
|
|
32
|
+
const absolute = Math.abs(finite);
|
|
33
|
+
if (absolute >= 1_000_000_000)
|
|
34
|
+
return `${compactNumber(finite / 1_000_000_000)}B`;
|
|
35
|
+
if (absolute >= 1_000_000)
|
|
36
|
+
return `${compactNumber(finite / 1_000_000)}M`;
|
|
37
|
+
if (absolute >= 1_000)
|
|
38
|
+
return `${compactNumber(finite / 1_000)}K`;
|
|
39
|
+
return compactNumber(finite);
|
|
40
|
+
}
|
|
41
|
+
export function formatFullScore(value) {
|
|
42
|
+
const finite = finiteScore(value);
|
|
43
|
+
return finite === null ? "N/A" : finite.toLocaleString("zh-CN", { maximumFractionDigits: 10 });
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.score-radar{--score-self:#32d551;--score-mean:#4f9cff;--score-median:#d15cff;background:#161516;color:#f2f2f2;padding:18px}.score-radar__header{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;margin-bottom:16px}.score-radar__header h2,.score-radar__card h3{margin:0}.score-radar__header p{margin:5px 0 0;color:#aaa7aa;font-size:.78rem}.score-radar__legend{display:flex;flex-wrap:wrap;gap:12px;margin:0;padding:0;list-style:none;font-size:.72rem}.score-radar__legend li{display:flex;align-items:center;gap:6px}.score-radar__legend li::before{width:18px;height:2px;background:currentColor;content:""}.score-radar__legend [data-series="self"]{color:var(--score-self)}.score-radar__legend [data-series="mean"]{color:var(--score-mean)}.score-radar__legend [data-series="median"]{color:var(--score-median)}.score-radar__cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.score-radar__card{min-width:0;border:1px solid #414041;border-radius:8px;background:#201f20;padding:14px}.score-radar__card h3{font-size:.92rem}.score-radar__plot{display:block;width:min(100%,360px);height:auto;margin:0 auto;overflow:visible}.score-radar__grid,.score-radar__boundary,.score-radar__spoke{fill:none;stroke:#414041;stroke-width:1}.score-radar__boundary{stroke:#5a585a}.score-radar__series{fill:none;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}.score-radar__series--self,.score-radar__point--self{stroke:var(--score-self)}.score-radar__series--mean,.score-radar__point--mean{stroke:var(--score-mean)}.score-radar__series--median,.score-radar__point--median{stroke:var(--score-median)}.score-radar__point{fill:#161516;stroke-width:2}.score-radar__axis-label{fill:#d4d1d4;font-size:11px;font-weight:600}.score-radar__empty{fill:#aaa7aa;font-size:12px}.score-radar__values{display:grid;gap:6px;margin:0;padding:0;list-style:none}.score-radar__values li{display:grid;grid-template-columns:minmax(100px,1fr) repeat(3,minmax(56px,auto));align-items:center;gap:8px;border-top:1px solid #343334;padding-top:6px;font-size:.72rem}.score-radar__values li>span:not(:first-child){display:grid;text-align:right}.score-radar__values small{color:#817f81;font-size:.62rem}.score-radar__values strong{font-variant-numeric:tabular-nums}@media(max-width:760px){.score-radar__header{display:grid}.score-radar__cards{grid-template-columns:1fr}.score-radar__values li{grid-template-columns:minmax(100px,1fr) repeat(3,minmax(48px,auto))}}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface DownloadReplaceUiPlan {
|
|
2
|
+
readonly operation_id: string;
|
|
3
|
+
readonly plan_digest: string;
|
|
4
|
+
readonly target_pair_id: string;
|
|
5
|
+
readonly selection: "scenario" | "view" | "both";
|
|
6
|
+
readonly local_overwrite_summary: {
|
|
7
|
+
readonly scenario_file_count: string;
|
|
8
|
+
readonly scenario_bytes: string;
|
|
9
|
+
readonly view_file_count: string;
|
|
10
|
+
readonly view_bytes: string;
|
|
11
|
+
readonly current_snapshot_digest: string;
|
|
12
|
+
} | null;
|
|
13
|
+
readonly dependency_diff: {
|
|
14
|
+
readonly added: readonly string[];
|
|
15
|
+
readonly removed: readonly string[];
|
|
16
|
+
readonly changed: readonly string[];
|
|
17
|
+
readonly lockfile_changes: boolean;
|
|
18
|
+
} | null;
|
|
19
|
+
readonly local_inspection_digest: string;
|
|
20
|
+
readonly expected_project_revision: number;
|
|
21
|
+
}
|
|
22
|
+
export interface DownloadTargetPairProjection {
|
|
23
|
+
readonly target_id: string;
|
|
24
|
+
readonly scenario: string;
|
|
25
|
+
readonly view: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DownloadReplaceUiPort {
|
|
28
|
+
prepare(target_pair_id: string, selection: DownloadReplaceUiPlan["selection"]): Promise<void>;
|
|
29
|
+
apply(plan: DownloadReplaceUiPlan, confirmation: {
|
|
30
|
+
operation_id: string;
|
|
31
|
+
plan_digest: string;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
reconcile(operation_id: string): Promise<void>;
|
|
34
|
+
retry(operation_id: string): Promise<void>;
|
|
35
|
+
restore(operation_id: string): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/** 只消费上层注入的 typed frozen plan;本组件不构造 HTTP 请求或猜测远端 wire。 */
|
|
38
|
+
export declare function DownloadPanel({ plan, port, targetPairs }: {
|
|
39
|
+
readonly plan: DownloadReplaceUiPlan | null;
|
|
40
|
+
readonly port: DownloadReplaceUiPort;
|
|
41
|
+
readonly targetPairs: readonly DownloadTargetPairProjection[];
|
|
42
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
/** 只消费上层注入的 typed frozen plan;本组件不构造 HTTP 请求或猜测远端 wire。 */
|
|
4
|
+
export function DownloadPanel({ plan, port, targetPairs }) {
|
|
5
|
+
const [targetPairId, setTargetPairId] = useState(plan?.target_pair_id ?? targetPairs[0]?.target_id ?? "");
|
|
6
|
+
const [selection, setSelection] = useState(plan?.selection ?? "both");
|
|
7
|
+
const [confirmed, setConfirmed] = useState(false);
|
|
8
|
+
const [error, setError] = useState(null);
|
|
9
|
+
const [busy, setBusy] = useState(false);
|
|
10
|
+
const invoke = async (action) => { setBusy(true); setError(null); try {
|
|
11
|
+
await action();
|
|
12
|
+
}
|
|
13
|
+
catch (cause) {
|
|
14
|
+
setError(cause instanceof Error ? cause.message : "下载替换失败");
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
setBusy(false);
|
|
18
|
+
} };
|
|
19
|
+
const apply = async () => { if (plan !== null)
|
|
20
|
+
await invoke(() => port.apply(plan, { operation_id: plan.operation_id, plan_digest: plan.plan_digest })); };
|
|
21
|
+
return _jsxs("section", { "aria-label": "Download Replace", children: [_jsxs("label", { children: ["\u76EE\u6807\u5BF9", _jsx("select", { "aria-label": "\u4E0B\u8F7D\u76EE\u6807\u5BF9", disabled: busy || targetPairs.length === 0, onChange: (event) => setTargetPairId(event.target.value), value: targetPairId, children: targetPairs.map((pair) => _jsxs("option", { value: pair.target_id, children: [pair.target_id, ": ", pair.scenario, " / ", pair.view] }, pair.target_id)) })] }), _jsxs("label", { children: ["\u66FF\u6362\u8303\u56F4", _jsxs("select", { "aria-label": "\u4E0B\u8F7D\u66FF\u6362\u8303\u56F4", disabled: busy, onChange: (event) => setSelection(event.target.value), value: selection, children: [_jsx("option", { value: "scenario", children: "scenario" }), _jsx("option", { value: "view", children: "view" }), _jsx("option", { value: "both", children: "both" })] })] }), _jsx("button", { disabled: busy || targetPairId === "", onClick: () => void invoke(() => port.prepare(targetPairId, selection)), type: "button", children: "\u751F\u6210\u4E0B\u8F7D\u8BA1\u5212" }), plan === null ? _jsx("p", { role: "status", children: "\u5C1A\u65E0\u51BB\u7ED3\u7684\u4E0B\u8F7D\u8BA1\u5212\u3002" }) : _jsxs(_Fragment, { children: [_jsxs("p", { children: ["\u5C06\u4ECE ", plan.target_pair_id, " \u66FF\u6362\u672C\u5730 ", plan.selection] }), _jsxs("p", { children: ["\u672C\u5730\u68C0\u67E5\uFF1A", _jsx("code", { children: plan.local_inspection_digest }), " \u00B7 revision ", plan.expected_project_revision] }), plan.local_overwrite_summary === null ? null : _jsxs("section", { "aria-label": "\u672C\u5730\u8986\u76D6\u6458\u8981", children: [_jsx("h3", { children: "\u672C\u5730\u8986\u76D6\u6458\u8981" }), _jsxs("p", { children: ["Scenario\uFF1A", plan.local_overwrite_summary.scenario_file_count, " \u4E2A\u6587\u4EF6\uFF0C", plan.local_overwrite_summary.scenario_bytes, " bytes"] }), _jsxs("p", { children: ["View\uFF1A", plan.local_overwrite_summary.view_file_count, " \u4E2A\u6587\u4EF6\uFF0C", plan.local_overwrite_summary.view_bytes, " bytes"] }), _jsxs("p", { children: ["\u5F53\u524D\u672C\u5730\u5FEB\u7167\uFF1A", _jsx("code", { children: plan.local_overwrite_summary.current_snapshot_digest || "未提供" })] })] }), plan.dependency_diff === null ? null : _jsxs("section", { "aria-label": "\u4F9D\u8D56\u53D8\u66F4", children: [_jsx("h3", { children: "\u4F9D\u8D56\u53D8\u66F4" }), _jsxs("p", { children: ["\u65B0\u589E\uFF1A", plan.dependency_diff.added.length === 0 ? "无" : plan.dependency_diff.added.join("、")] }), _jsxs("p", { children: ["\u5220\u9664\uFF1A", plan.dependency_diff.removed.length === 0 ? "无" : plan.dependency_diff.removed.join("、")] }), _jsxs("p", { children: ["\u53D8\u66F4\uFF1A", plan.dependency_diff.changed.length === 0 ? "无" : plan.dependency_diff.changed.join("、")] }), _jsxs("p", { children: ["\u4F9D\u8D56\u5B89\u88C5\u5F71\u54CD\uFF1A", plan.dependency_diff.lockfile_changes ? "依赖锁定文件将变更;替换后需要安装依赖。" : "无 lockfile 变更;不需要因本计划安装依赖。"] })] }), _jsxs("label", { children: [_jsx("input", { checked: confirmed, disabled: busy, onChange: (event) => setConfirmed(event.target.checked), type: "checkbox" }), "\u6211\u786E\u8BA4\u672C\u5730\u66FF\u6362"] }), _jsx("button", { disabled: !confirmed || busy, onClick: () => void apply(), type: "button", children: "\u786E\u8BA4\u5E76\u66FF\u6362" }), _jsx("button", { disabled: busy, onClick: () => void invoke(() => port.reconcile(plan.operation_id)), type: "button", children: "Reconcile download" }), _jsx("button", { disabled: busy, onClick: () => void invoke(() => port.retry(plan.operation_id)), type: "button", children: "Retry same download" }), _jsx("button", { disabled: busy, onClick: () => void invoke(() => port.restore(plan.operation_id)), type: "button", children: "Restore local preimage" })] }), error === null ? null : _jsx("p", { role: "alert", children: error })] });
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type UploadConfirmation, type UploadFanoutPlanProjection, type UploadOperationProjection } from "./UploadPanel.js";
|
|
2
|
+
import { type DownloadReplaceUiPlan } from "./DownloadPanel.js";
|
|
3
|
+
export interface SyncSidebarSnapshot {
|
|
4
|
+
readonly account: {
|
|
5
|
+
readonly label: string;
|
|
6
|
+
} | null;
|
|
7
|
+
readonly inventory: {
|
|
8
|
+
readonly project_id: string;
|
|
9
|
+
readonly project_revision: number;
|
|
10
|
+
readonly source_digest: string;
|
|
11
|
+
readonly scenario_entries: number;
|
|
12
|
+
readonly view_entries: number;
|
|
13
|
+
} | null;
|
|
14
|
+
readonly target_pairs: readonly {
|
|
15
|
+
readonly target_id: string;
|
|
16
|
+
readonly scenario: string;
|
|
17
|
+
readonly view: string;
|
|
18
|
+
}[];
|
|
19
|
+
readonly history: readonly {
|
|
20
|
+
readonly operation_id: string;
|
|
21
|
+
readonly detail: string;
|
|
22
|
+
}[];
|
|
23
|
+
readonly upload_plan: UploadFanoutPlanProjection | null;
|
|
24
|
+
readonly upload_operations: readonly UploadOperationProjection[];
|
|
25
|
+
readonly download_plan: DownloadReplaceUiPlan | null;
|
|
26
|
+
}
|
|
27
|
+
/** GUI 只消费本地 adapter 的投影;远端身份、wire 与网络 I/O 必须留在 adapter 外。 */
|
|
28
|
+
export interface SyncSidebarAdapter {
|
|
29
|
+
readSnapshot(): Promise<SyncSidebarSnapshot>;
|
|
30
|
+
subscribe?(listener: () => void): () => void;
|
|
31
|
+
confirmUploadTarget(confirmation: UploadConfirmation): Promise<void>;
|
|
32
|
+
reconcileUpload(operation_id: string): Promise<void>;
|
|
33
|
+
retryUpload(operation_id: string): Promise<void>;
|
|
34
|
+
undoUpload(operation_id: string): Promise<void>;
|
|
35
|
+
prepareDownload?(target_pair_id: string, selection: DownloadReplaceUiPlan["selection"]): Promise<void>;
|
|
36
|
+
applyDownload?(plan: DownloadReplaceUiPlan, confirmation: {
|
|
37
|
+
operation_id: string;
|
|
38
|
+
plan_digest: string;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
reconcileDownload?(operation_id: string): Promise<void>;
|
|
41
|
+
retryDownload?(operation_id: string): Promise<void>;
|
|
42
|
+
restoreDownload?(operation_id: string): Promise<void>;
|
|
43
|
+
prepareUpload?(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export declare function SyncSidebar({ adapter }: {
|
|
46
|
+
readonly adapter: SyncSidebarAdapter;
|
|
47
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { UploadPanel, } from "./UploadPanel.js";
|
|
4
|
+
import { DownloadPanel } from "./DownloadPanel.js";
|
|
5
|
+
export function SyncSidebar({ adapter }) {
|
|
6
|
+
const [snapshot, setSnapshot] = useState(null);
|
|
7
|
+
const [failure, setFailure] = useState(null);
|
|
8
|
+
const reload = useCallback(async () => {
|
|
9
|
+
try {
|
|
10
|
+
setSnapshot(await adapter.readSnapshot());
|
|
11
|
+
setFailure(null);
|
|
12
|
+
}
|
|
13
|
+
catch (cause) {
|
|
14
|
+
setFailure(cause instanceof Error ? cause.message : "无法读取同步状态。");
|
|
15
|
+
}
|
|
16
|
+
}, [adapter]);
|
|
17
|
+
useEffect(() => { void reload(); }, [reload]);
|
|
18
|
+
useEffect(() => adapter.subscribe?.(() => { void reload(); }), [adapter, reload]);
|
|
19
|
+
const invoke = useCallback(async (action) => {
|
|
20
|
+
try {
|
|
21
|
+
await action();
|
|
22
|
+
await reload();
|
|
23
|
+
}
|
|
24
|
+
catch (cause) {
|
|
25
|
+
setFailure(cause instanceof Error ? cause.message : "同步操作失败。");
|
|
26
|
+
}
|
|
27
|
+
}, [reload]);
|
|
28
|
+
if (snapshot === null)
|
|
29
|
+
return _jsx("p", { role: "status", children: "\u6B63\u5728\u8BFB\u53D6\u540C\u6B65\u72B6\u6001\u2026" });
|
|
30
|
+
return (_jsxs("aside", { "aria-label": "\u540C\u6B65\u4FA7\u8FB9\u680F", children: [failure === null ? null : _jsx("p", { role: "alert", children: failure }), snapshot.account === null ? _jsx("p", { role: "status", children: "\u5F53\u524D\u9879\u76EE\u5C1A\u672A\u767B\u5F55\u3002" }) : _jsxs("p", { children: ["\u8D26\u6237\uFF1A", snapshot.account.label] }), snapshot.inventory === null ? null : _jsxs("section", { "aria-label": "\u672C\u5730\u540C\u6B65\u76D8\u70B9", children: [_jsx("h2", { children: "\u672C\u5730\u76D8\u70B9" }), _jsxs("p", { children: [snapshot.inventory.project_id, " \u00B7 revision ", snapshot.inventory.project_revision] }), _jsxs("p", { children: ["Scenario ", snapshot.inventory.scenario_entries, " \u00B7 View ", snapshot.inventory.view_entries] }), _jsxs("p", { children: ["source readback: ", _jsx("code", { children: snapshot.inventory.source_digest })] })] }), _jsxs("section", { "aria-label": "\u540C\u6B65\u76EE\u6807\u5BF9", children: [_jsx("h2", { children: "Scenario / View \u76EE\u6807\u5BF9" }), snapshot.target_pairs.length === 0 ? _jsx("p", { children: "\u5C1A\u65E0\u8FDC\u7AEF\u76EE\u6807\u5BF9\u3002" }) : _jsx("ul", { children: snapshot.target_pairs.map((pair) => _jsxs("li", { children: [pair.target_id, ": ", pair.scenario, " / ", pair.view] }, pair.target_id)) })] }), _jsx(UploadPanel, { ...(adapter.prepareUpload === undefined ? {} : { onPrepare: () => invoke(() => adapter.prepareUpload()) }), onConfirmTarget: (confirmation) => invoke(() => adapter.confirmUploadTarget(confirmation)), onReconcile: (operation_id) => invoke(() => adapter.reconcileUpload(operation_id)), onRetry: (operation_id) => invoke(() => adapter.retryUpload(operation_id)), onUndo: (operation_id) => invoke(() => adapter.undoUpload(operation_id)), operations: snapshot.upload_operations, plan: snapshot.upload_plan }), adapter.prepareDownload === undefined || adapter.applyDownload === undefined ? null : _jsx(DownloadPanel, { plan: snapshot.download_plan, targetPairs: snapshot.target_pairs, port: { prepare: (target_pair_id, selection) => invoke(() => adapter.prepareDownload(target_pair_id, selection)), apply: (plan, confirmation) => adapter.applyDownload(plan, confirmation), reconcile: (operation_id) => invoke(() => adapter.reconcileDownload(operation_id)), retry: (operation_id) => invoke(() => adapter.retryDownload(operation_id)), restore: (operation_id) => invoke(() => adapter.restoreDownload(operation_id)) } }), _jsxs("section", { "aria-label": "\u540C\u6B65\u5386\u53F2", children: [_jsx("h2", { children: "\u540C\u6B65\u5386\u53F2" }), snapshot.history.length === 0 ? _jsx("p", { children: "\u6682\u65E0\u5386\u53F2\u3002" }) : _jsx("ul", { children: snapshot.history.map((item) => _jsxs("li", { children: [item.operation_id, ": ", item.detail] }, item.operation_id)) })] })] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type UploadPartKind = "scenario" | "view";
|
|
2
|
+
export type UploadTargetAction = "existing" | "create" | "copy_on_bind";
|
|
3
|
+
export type UploadOperationState = "prepared" | "in_flight" | "completed" | "partial" | "unknown" | "failed" | "undone";
|
|
4
|
+
export interface UploadSnapshotProjection {
|
|
5
|
+
readonly snapshot_id: string;
|
|
6
|
+
readonly snapshot_digest: string;
|
|
7
|
+
readonly parts: readonly UploadPartKind[];
|
|
8
|
+
readonly bytes: number;
|
|
9
|
+
}
|
|
10
|
+
export interface UploadTargetProjection {
|
|
11
|
+
readonly target_id: string;
|
|
12
|
+
readonly scenario: {
|
|
13
|
+
readonly identity: string;
|
|
14
|
+
readonly action: "existing" | "create";
|
|
15
|
+
};
|
|
16
|
+
readonly view: {
|
|
17
|
+
readonly identity: string;
|
|
18
|
+
readonly action: UploadTargetAction;
|
|
19
|
+
};
|
|
20
|
+
readonly current_head: string | null;
|
|
21
|
+
readonly current_digest: string | null;
|
|
22
|
+
readonly binding_revision: string | null;
|
|
23
|
+
readonly warnings: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
export interface UploadRetentionProjection {
|
|
26
|
+
readonly remote_preimage: string;
|
|
27
|
+
readonly local_preimage: string;
|
|
28
|
+
readonly local_preimage_pinnable: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface UploadFanoutPlanProjection {
|
|
31
|
+
readonly operation_id: string;
|
|
32
|
+
readonly plan_digest: string;
|
|
33
|
+
readonly expires_at: string;
|
|
34
|
+
readonly snapshot: UploadSnapshotProjection;
|
|
35
|
+
readonly targets: readonly UploadTargetProjection[];
|
|
36
|
+
readonly retention: UploadRetentionProjection;
|
|
37
|
+
}
|
|
38
|
+
export interface UploadOperationProjection {
|
|
39
|
+
readonly operation_id: string;
|
|
40
|
+
readonly target_id: string;
|
|
41
|
+
readonly state: UploadOperationState;
|
|
42
|
+
readonly detail: string | null;
|
|
43
|
+
readonly retryable: boolean;
|
|
44
|
+
readonly undoable: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface UploadConfirmation {
|
|
47
|
+
readonly plan_digest: string;
|
|
48
|
+
readonly target_id: string;
|
|
49
|
+
readonly confirmation: "force_overwrite";
|
|
50
|
+
}
|
|
51
|
+
export interface UploadPanelProps {
|
|
52
|
+
readonly plan: UploadFanoutPlanProjection | null;
|
|
53
|
+
readonly operations?: readonly UploadOperationProjection[];
|
|
54
|
+
readonly now?: Date;
|
|
55
|
+
readonly onConfirmTarget?: (confirmation: UploadConfirmation) => void | Promise<void>;
|
|
56
|
+
readonly onReconcile?: (operation_id: string) => void | Promise<void>;
|
|
57
|
+
readonly onRetry?: (operation_id: string) => void | Promise<void>;
|
|
58
|
+
readonly onUndo?: (operation_id: string) => void | Promise<void>;
|
|
59
|
+
readonly onPrepare?: () => void | Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 只渲染已由上层冻结的计划;组件不构造远端请求,也不把当前 UI 输入当作 authority。
|
|
63
|
+
*/
|
|
64
|
+
export declare function UploadPanel({ plan, operations, now, onConfirmTarget, onReconcile, onRetry, onUndo, onPrepare, }: UploadPanelProps): JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
function targetActionLabel(action) {
|
|
4
|
+
if (action === "existing")
|
|
5
|
+
return "覆盖现有对象";
|
|
6
|
+
if (action === "create")
|
|
7
|
+
return "新建对象";
|
|
8
|
+
return "Copy-on-Bind";
|
|
9
|
+
}
|
|
10
|
+
function operationLabel(state) {
|
|
11
|
+
return {
|
|
12
|
+
prepared: "已准备",
|
|
13
|
+
in_flight: "进行中",
|
|
14
|
+
completed: "已完成",
|
|
15
|
+
partial: "部分完成",
|
|
16
|
+
unknown: "结果未知,需 reconcile",
|
|
17
|
+
failed: "失败",
|
|
18
|
+
undone: "已撤销",
|
|
19
|
+
}[state];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 只渲染已由上层冻结的计划;组件不构造远端请求,也不把当前 UI 输入当作 authority。
|
|
23
|
+
*/
|
|
24
|
+
export function UploadPanel({ plan, operations = [], now = new Date(), onConfirmTarget, onReconcile, onRetry, onUndo, onPrepare, }) {
|
|
25
|
+
const [confirmed, setConfirmed] = useState(() => new Set());
|
|
26
|
+
const expired = useMemo(() => plan !== null && Date.parse(plan.expires_at) <= now.getTime(), [now, plan]);
|
|
27
|
+
if (plan === null)
|
|
28
|
+
return _jsxs("section", { "aria-label": "Upload Fanout", "data-testid": "upload-panel", children: [_jsx("h2", { children: "Upload Fanout" }), _jsx("button", { disabled: onPrepare === undefined, onClick: () => { void onPrepare?.(); }, type: "button", children: "\u751F\u6210\u4E0A\u4F20\u8BA1\u5212" }), _jsx("p", { role: "status", children: "\u5C1A\u65E0\u51BB\u7ED3\u7684\u4E0A\u4F20\u8BA1\u5212\uFF1B\u4E0D\u4F1A\u4E0A\u4F20 staging \u5BF9\u8C61\u3002" })] });
|
|
29
|
+
const confirm = async (target_id) => {
|
|
30
|
+
if (expired || onConfirmTarget === undefined)
|
|
31
|
+
return;
|
|
32
|
+
await onConfirmTarget({ plan_digest: plan.plan_digest, target_id, confirmation: "force_overwrite" });
|
|
33
|
+
setConfirmed((current) => new Set(current).add(target_id));
|
|
34
|
+
};
|
|
35
|
+
return (_jsxs("section", { "aria-label": "Upload Fanout", "data-testid": "upload-panel", children: [_jsx("h2", { children: "Upload Fanout" }), _jsxs("p", { children: ["\u5FEB\u7167 ", plan.snapshot.snapshot_id, " \u00B7 ", plan.snapshot.snapshot_digest] }), _jsxs("p", { children: ["parts: ", plan.snapshot.parts.join(", "), " \u00B7 ", plan.snapshot.bytes, " bytes"] }), _jsxs("p", { children: ["\u8BA1\u5212 digest: ", _jsx("code", { children: plan.plan_digest })] }), _jsx("p", { role: expired ? "alert" : "status", children: expired ? "计划已过期;请重新生成计划。" : `计划有效至 ${plan.expires_at}` }), _jsxs("p", { children: ["\u8FDC\u7AEF preimage \u4FDD\u7559 ", plan.retention.remote_preimage, "\uFF1B\u672C\u5730 preimage \u4FDD\u7559 ", plan.retention.local_preimage, plan.retention.local_preimage_pinnable ? "(可固定)" : "", "\u3002"] }), _jsx("ul", { "aria-label": "\u4E0A\u4F20\u76EE\u6807", children: plan.targets.map((target) => (_jsxs("li", { "data-testid": `upload-target-${target.target_id}`, children: [_jsx("h3", { children: target.target_id }), _jsxs("p", { children: ["Scenario: ", target.scenario.identity, "\uFF08", targetActionLabel(target.scenario.action), "\uFF09"] }), _jsxs("p", { children: ["View: ", target.view.identity, "\uFF08", targetActionLabel(target.view.action), "\uFF09"] }), _jsxs("p", { children: ["\u5F53\u524D head: ", target.current_head ?? "新建", "\uFF1Bdigest: ", target.current_digest ?? "新建", "\uFF1Bbinding: ", target.binding_revision ?? "新建"] }), target.warnings.length === 0 ? null : _jsx("ul", { "aria-label": `${target.target_id} warnings`, children: target.warnings.map((warning) => _jsx("li", { children: warning }, warning)) }), _jsx("button", { disabled: expired || confirmed.has(target.target_id) || onConfirmTarget === undefined, onClick: () => { void confirm(target.target_id); }, type: "button", children: confirmed.has(target.target_id) ? "已确认" : `确认 ${target.target_id} force_overwrite` })] }, target.target_id))) }), _jsxs("section", { "aria-label": "\u4E0A\u4F20\u64CD\u4F5C\u5386\u53F2", children: [_jsx("h3", { children: "\u64CD\u4F5C\u5386\u53F2" }), operations.length === 0 ? _jsx("p", { children: "\u6682\u65E0\u64CD\u4F5C\u3002" }) : (_jsx("ul", { children: operations.map((operation) => (_jsxs("li", { "data-testid": `upload-operation-${operation.operation_id}`, children: [_jsxs("span", { children: [operation.target_id, ": ", operationLabel(operation.state)] }), operation.detail === null ? null : _jsxs("span", { children: [" \u00B7 ", operation.detail] }), operation.state === "unknown" ? _jsx("button", { onClick: () => { void onReconcile?.(operation.operation_id); }, type: "button", children: "Reconcile" }) : null, operation.retryable ? _jsx("button", { onClick: () => { void onRetry?.(operation.operation_id); }, type: "button", children: "Retry same operation" }) : null, operation.undoable ? _jsx("button", { onClick: () => { void onUndo?.(operation.operation_id); }, type: "button", children: "Undo upload" }) : null] }, operation.operation_id))) }))] })] }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { SpatialObjectV1 } from "@world-engines/spatial-authoring";
|
|
3
|
+
export interface TriggerNode {
|
|
4
|
+
readonly stable_id: string;
|
|
5
|
+
readonly kind: "event" | "condition" | "effect" | "presentation";
|
|
6
|
+
readonly label: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TriggerEdge {
|
|
9
|
+
readonly from_stable_id: string;
|
|
10
|
+
readonly to_stable_id: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TriggerBlueprintProjection {
|
|
13
|
+
readonly revision: number;
|
|
14
|
+
readonly nodes: readonly TriggerNode[];
|
|
15
|
+
readonly edges: readonly TriggerEdge[];
|
|
16
|
+
readonly source: Readonly<Record<string, unknown>>;
|
|
17
|
+
readonly layout: Readonly<Record<string, unknown>>;
|
|
18
|
+
readonly validation: Readonly<Record<string, unknown>> | null;
|
|
19
|
+
readonly simulation: Readonly<Record<string, unknown>> | null;
|
|
20
|
+
}
|
|
21
|
+
export interface TriggerBlueprintProps {
|
|
22
|
+
readonly blueprint: TriggerBlueprintProjection;
|
|
23
|
+
readonly disabled: boolean;
|
|
24
|
+
readonly onValidate: () => Promise<void>;
|
|
25
|
+
readonly onSimulate: () => Promise<void>;
|
|
26
|
+
readonly onRepairLocationTarget: (trigger_id: string, spatial_id: string) => Promise<void>;
|
|
27
|
+
readonly spatialObjects: readonly SpatialObjectV1[];
|
|
28
|
+
readonly renderBlueprint?: (props: TriggerBlueprintProps) => ReactNode;
|
|
29
|
+
}
|
|
30
|
+
interface TriggerSpatialTarget {
|
|
31
|
+
readonly triggerId: string;
|
|
32
|
+
readonly spatialId: string;
|
|
33
|
+
}
|
|
34
|
+
/** 从 Trigger source 读出空间目标;未知格式只显示为空,绝不尝试猜测或写回。 */
|
|
35
|
+
export declare function collectTriggerSpatialTargets(source: Readonly<Record<string, unknown>>): readonly TriggerSpatialTarget[];
|
|
36
|
+
export declare function TriggerBlueprint(props: TriggerBlueprintProps): JSX.Element;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** 从 Trigger source 读出空间目标;未知格式只显示为空,绝不尝试猜测或写回。 */
|
|
3
|
+
export function collectTriggerSpatialTargets(source) {
|
|
4
|
+
const triggers = source.triggers;
|
|
5
|
+
if (!Array.isArray(triggers))
|
|
6
|
+
return [];
|
|
7
|
+
return triggers.flatMap((value) => {
|
|
8
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
9
|
+
return [];
|
|
10
|
+
const record = value;
|
|
11
|
+
if (typeof record.id !== "string" || !Array.isArray(record.spatial_refs))
|
|
12
|
+
return [];
|
|
13
|
+
return record.spatial_refs.filter((spatialId) => typeof spatialId === "string").map((spatialId) => ({ triggerId: record.id, spatialId }));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function TriggerBlueprint(props) {
|
|
17
|
+
if (props.renderBlueprint !== undefined)
|
|
18
|
+
return _jsx(_Fragment, { children: props.renderBlueprint(props) });
|
|
19
|
+
const locationObjects = props.spatialObjects.filter((object) => object.kind === "location");
|
|
20
|
+
const targets = collectTriggerSpatialTargets(props.blueprint.source);
|
|
21
|
+
return (_jsxs("section", { "aria-label": "Trigger Blueprint", "data-testid": "trigger-blueprint", children: [_jsxs("header", { children: [_jsxs("strong", { children: ["Blueprint revision ", props.blueprint.revision] }), _jsx("button", { disabled: props.disabled, onClick: () => void props.onValidate(), type: "button", children: "\u9A8C\u8BC1" }), _jsx("button", { disabled: props.disabled, onClick: () => void props.onSimulate(), type: "button", children: "\u6A21\u62DF" })] }), props.blueprint.nodes.length === 0 ? _jsx("p", { children: "\u5F53\u524D\u9879\u76EE\u6CA1\u6709 Trigger \u8282\u70B9\u3002" }) : (_jsx("ul", { children: props.blueprint.nodes.map((node) => _jsxs("li", { children: [node.label, " \u00B7 ", node.kind] }, node.stable_id)) })), _jsxs("p", { children: ["\u8FDE\u63A5\u6570\uFF1A", props.blueprint.edges.length] }), _jsxs("section", { "aria-label": "Trigger location targets", children: [_jsx("h2", { children: "\u7A7A\u95F4\u4F4D\u7F6E\u76EE\u6807" }), targets.length === 0 ? _jsx("p", { children: "\u5F53\u524D Trigger \u6CA1\u6709\u7A7A\u95F4\u4F4D\u7F6E\u76EE\u6807\u3002" }) : (_jsx("ul", { children: targets.map((target) => {
|
|
22
|
+
const valid = locationObjects.some((object) => object.id === target.spatialId);
|
|
23
|
+
return (_jsxs("li", { children: [_jsxs("span", { children: [target.triggerId, " \u2192 ", target.spatialId] }), valid ? _jsx("span", { children: "\uFF08\u6709\u6548\uFF09" }) : (_jsxs("label", { children: ["\u65E0\u6548\u76EE\u6807\uFF0C\u4FEE\u590D\u4E3A", _jsxs("select", { "aria-label": `${target.triggerId} 的 location target`, disabled: props.disabled || locationObjects.length === 0, defaultValue: "", onChange: (event) => { if (event.target.value.length > 0)
|
|
24
|
+
void props.onRepairLocationTarget(target.triggerId, event.target.value); }, children: [_jsx("option", { disabled: true, value: "", children: "\u9009\u62E9 location" }), locationObjects.map((object) => _jsx("option", { value: object.id, children: object.label }, object.id))] })] }))] }, `${target.triggerId}:${target.spatialId}`));
|
|
25
|
+
}) }))] }), _jsxs("details", { children: [_jsx("summary", { children: "Trigger source" }), _jsx("pre", { children: JSON.stringify(props.blueprint.source, null, 2) })] }), _jsxs("details", { children: [_jsx("summary", { children: "Blueprint layout" }), _jsx("pre", { children: JSON.stringify(props.blueprint.layout, null, 2) })] }), props.blueprint.validation === null ? null : _jsx("output", { "aria-label": "Trigger \u9A8C\u8BC1\u7ED3\u679C", children: JSON.stringify(props.blueprint.validation) }), props.blueprint.simulation === null ? null : _jsx("output", { "aria-label": "Trigger trace", children: JSON.stringify(props.blueprint.simulation) })] }));
|
|
26
|
+
}
|